npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vite-dependency-classifier

v0.2.1

Published

Check that a Vite project's dependencies/devDependencies split matches what the production build actually contains, and that production source does not import an undeclared package.

Downloads

55

Readme

vite-dependency-classifier

Check that a Vite project's dependencies / devDependencies split matches what the production build actually contains, and that production source does not import an undeclared package.

The classification is derived from a real production build, not from a source scan. That is the point: a source-level check cannot tell that a dev-only import behind import.meta.env.DEV was eliminated, and would flag the pattern the policy requires.

Install

npm install --save-dev vite-dependency-classifier

vite ^8 is a peer dependency and must already be present. The check loads that project's vite, not one installed next to this package, so npx vite-dependency-classifier still builds with the app's copy.

CLI

From a Vite project root:

npx vite-dependency-classifier
npx vite-dependency-classifier /path/to/project
npx vite-dependency-classifier --config vite.config.prod.ts
npx vite-dependency-classifier --json
npx vite-dependency-classifier --library
# some-peer: required at runtime, never in the bundle by name
npx vite-dependency-classifier --runtime-peer some-peer
# some-util: listed as a devDependency; only in the bundle via a library
npx vite-dependency-classifier --transitive-dev some-util
  • -h, --help — print usage and exit
  • -v, --version — print the package version and exit
  • -c, --config <file> — Vite config file (default: the names Vite searches)
  • --input <file> — entry module, for projects without a Vite config
  • --app — treat the project as an application (default). A first-party import listed only in peerDependencies is unlisted.
  • --library — treat the project as a library. A first-party import listed only in peerDependencies is declared, so it is not unlisted.
  • --runtime-peer <name> — exempt a runtime peer from the extra check (repeatable). Put the reason in a comment next to the flag in the script or CI step that passes it.
  • --transitive-dev <name> — exempt a transitive-only devDependency from the missing check (repeatable). Put the reason in a comment next to the flag in the script or CI step that passes it. Do not change the declaration.
  • --json — print the result as JSON (ok, missing, extra, unlisted, packages, chunkCount, projectType)
  • -q, --quiet — print only classification failures

The command reads that project's package.json and Vite config (the same names Vite searches: vite.config.js, .mjs, .ts, .cjs, .mts, .cts), runs a production build (nothing is written to disk), and exits 1 if a declared devDependency reached the bundle, a declared dependency did not, or production source imported a package listed in none of dependencies, devDependencies, or optionalDependencies (and, with --library, none of peerDependencies either). Default is application. A usage error (unknown flag, extra argument, both --app and --library) exits 2.

The published CLI is compiled JavaScript (dist/check.js). A clone can still run the TypeScript source with Node's type stripper:

node src/check.ts

CI

Add a script, then run it after install. The check does its own production build (nothing is written to disk), so it does not need a prior npm run build.

"scripts": {
  "check:deps": "vite-dependency-classifier"
}
- run: npm run check:deps

Leave -q off so a green job still prints the chunk and package counts. Do not put --runtime-peer or --transitive-dev in the npm script: JSON cannot hold the required reason next to the flag. Put the command in a commentable file — a small scripts/check-deps.sh, or the CI step itself:

vite-dependency-classifier \
  --runtime-peer some-peer \  # required at runtime, never in the bundle by name
  --transitive-dev some-util  # listed as a devDependency; only in the bundle via a library

Programmatic use

import { check } from "vite-dependency-classifier";

const { ok, missing, extra, unlisted } = await check();

await check({ projectType: "library" });
const { check } = require("vite-dependency-classifier");

const { ok, missing, extra, unlisted } = await check();

Pass runtimePeers the same way the CLI takes --runtime-peer, and transitiveDevs the same way it takes --transitive-dev, with the reason on the line that adds each name. Pass projectType: "library" the same way the CLI takes --library.

collectBundledPackages (the bundle set and directPackages), classifyPackages, and classifyUnlisted are also exported for callers that want the halves separately.

The published tarball is compiled JavaScript (dist/), generated at pack time. dist/ is not committed. Installing from a GitHub source tree needs npm run build before import / require / the bin will resolve.

Dependencies

package.json splits packages into dependencies and devDependencies. missing and extra follow that split. unlisted also treats optionalDependencies as declared. peerDependencies count as declared for unlisted only when the project is a library (--library / projectType: "library"). The default is an application.

The definition

A dependency is anything imported into production code and reachable at runtime in a user's browser.

Everything else is a devDependency: build tooling, linters, test runners, type packages, and anything eliminated from the production bundle.

Cases that are not obvious

A first-party production import listed in none of the declared fields is unlisted. First-party means source under this project's root, after hopping Vite virtuals and same-package wrappers (CommonJS proxies). It resolved because it was hoisted or walked up from another install. It shipped, so it belongs in dependencies. There is no per-package exemption flag.

A Vite alias that points at another package's source is not first-party. Those files sit outside this project's root, so their imports are treated like another package's: they are not unlisted here. Install the workspace package through node_modules (workspace:*, file:) and run this check on that package if you want its own package.json classified.

A package listed only in peerDependencies is unlisted in an application, and listed in a library. Applications should put a first-party production import in dependencies. Libraries use peers for packages the host must provide; pass --library (or projectType: "library") so those count as declared. optionalDependencies are declared in both modes.

A package can be a runtime dependency without being imported by name. Peer dependencies pulled in at runtime by a dependency's own code still belong in dependencies. They often reach the bundle under their own names, in which case this check sees them without help. A package that is required at runtime and never appears in the bundle under its own name needs an explicit exemption — --runtime-peer on the CLI, or runtimePeers on check() — with a stated reason next to that flag or call.

A dev-only tool imported from a production source file must be behind an import.meta.env.DEV guard, so the bundler removes it — rather than relying on the vendor to render nothing in production. A package that ships a no-op in production builds is still a package the bundler had to resolve; the guard is what makes the devDependency classification true rather than merely harmless. A DEV-only import listed nowhere is out of scope: it is not in the production graph, so it is not unlisted.

A package imported from JavaScript only as a stylesheet still ships. import "pkg" whose entry is CSS: Vite extracts the stylesheet into an asset and drops the JS placeholders those imports used, so the package never appears in a remaining JavaScript chunk. It is still in the production output and belongs in dependencies. An undeclared CSS-only production import is unlisted, not silent.

A package pulled in only through plain CSS @import still ships. Vite inlines those files (postcss-import / LightningCSS), so they never become Vite module ids. They are still in the compiled CSS and belong in dependencies. An undeclared first-party production @import is unlisted, not silent. Nested @imports inside that package ship too, but they are not unlisted unless first-party source wrote them. A Vite alias (@/theme.css) that resolves to first-party source is walked the same way; we use the path Vite already resolved. A Node #imports specifier that maps to a package is a first-party import of that package.

A package pulled in only through Sass @use / @forward still ships. The preprocessor inlines those files, so they never become Vite module ids. They are still in the compiled CSS and belong in dependencies. An undeclared Sass-only production import is unlisted, not silent.

A package pulled in only through Less @import still ships. The preprocessor inlines those files, so they never become Vite module ids. They are still in the compiled CSS and belong in dependencies. An undeclared Less-only production import is unlisted, not silent. Nested @imports inside that package ship too, but they are not unlisted unless first-party source wrote them.

A type-only import is not a runtime dependency. import type ... is erased at build time and contributes nothing to the bundle, so a package used only that way belongs in devDependencies. An undeclared type-only import is not unlisted. This check will not report it.

A correct devDependency can still appear in the bundle as a transitive of something that ships. The app listed it for tests or tooling; a production library also depends on it. That is not a reason to move it to dependencies, and it is not unlisted unless first-party source imported it. The bundle cannot tell that overlap from a real unguarded import, so the exemption is explicit — --transitive-dev on the CLI, or transitiveDevs on check() — with a stated reason next to that flag or call. Do not change the declaration to silence the finding.

Why the split matters

It decides which security advisories are urgent. npm audit --omit=dev is only as trustworthy as the classification. A devDependency that ships is an exposure that command will not show you.

Where the policy is enforced

This package is the check. missing is a declared devDependency present in the production bundle, unless it is listed in transitiveDevs because that presence is only transitive. extra is a declared dependency absent from it. unlisted is a first-party production import listed in none of dependencies, devDependencies, or optionalDependencies — and, for a library, none of peerDependencies either. First-party means source under this project's root. Transitive packages that appear in the bundle and are declared nowhere are not reported unless that source imported them.

Engines

"node": "^22.18.0 || ^24.3.0 || >=26.0.0"

Vite 8 is ^20.19.0 || >=22.12.0. This range is narrower on purpose: Node 20 is dropped, and the 22 / 24 floors are the first minors where type stripping runs without --experimental-strip-types or an ExperimentalWarning. A clone needs that for node src/check.ts. The published CLI is compiled JavaScript and would run on Vite's wider range; the engines still follow the source-run path.

.npmrc sets engine-strict=true, so npm install fails on an unsupported Node.

TypeScript

Typecheck runs TypeScript 7 via @typescript/native (node_modules/.bin/tsc). The typescript package name is aliased to TypeScript 6 so typescript-eslint can keep using the 6.x compiler API.

The tsconfig extends @tsconfig/strictest and also turns on erasableSyntaxOnly and noUncheckedSideEffectImports. Development typechecks with noEmit. npm run build (also prepack) emits dist/ for the published package. npm publish runs prepublishOnly (npm test) before that pack, so a red tree cannot ship.

Development

npm install
npm test
npm run typecheck
npm run lint
npm run format:check
npm run build

Fixture projects import fixture-lib, fixture-leaf, fixture-css, fixture-css-theme, fixture-sass, fixture-less, fixture-less-leaf, and fixture-cjs from test/fixtures/packages, installed at the repo root as file: devDependencies. Vite's walk-up resolution then yields real /node_modules/<name>/ module ids.

CI (npm ci) runs format, lint, typecheck, build, and tests against the lockfile. A second job deletes package-lock.json and runs npm install so devDependency ranges (including vite@^8) resolve to latest matching versions, then typechecks and tests. Both jobs run on pull requests, pushes to main, a Tuesday 06:00 UTC cron, and manual workflow_dispatch.