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

bundle-dep-auditor

v0.1.0

Published

Detects duplicate/conflicting package versions bundled across separately-built frontend apps (micro-frontends, multi-app monorepos), from webpack/Rspack stats.json, esbuild metafile.json, or Rollup/Vite visualizer JSON.

Downloads

189

Readme

bundle-dep-auditor

Finds packages that get shipped more than once, at different versions, across separately-built frontend apps (micro-frontends, Module Federation hosts/remotes, or just a few apps in one monorepo). One command reads each app's bundler output, tells you which packages are duplicated, how many bytes that's costing you, and how to fix it.

What you get

  • Duplicate detection — every package, every version it's shipped at, and which apps ship it.
  • A real byte count — how many gzip bytes you'd save if every app used the same version.
  • A license list — for every third-party package in every app, not just the duplicated ones.
  • A pass/fail check you can wire into CI, with sensible defaults and suggested fixes.
  • 4 report formats — JSON, CSV, Markdown (great for PR comments), and HTML.

Install

npm install --save-dev bundle-dep-auditor

Quick start

  1. Build each app and save its bundler's stats output (see the table below for your bundler).

  2. Describe your apps in a small JSON file, e.g. apps.json:

    [
      { "file": "apps/cart/dist/stats.json", "appName": "cart" },
      { "file": "apps/checkout/dist/stats.json", "appName": "checkout" },
      { "file": "apps/search/dist/stats.json", "appName": "search" }
    ]
  3. Run the audit:

    npx bundle-dep-auditor audit --files apps.json --format markdown --out audit-report.md

The command exits with code 1 if a problem is found (great for failing a CI job) and 0 when everything looks clean.

Getting a stats file from your bundler

| Bundler | How to get the file | | --- | --- | | webpack | Run webpack --json > stats.json | | Rspack | Same file as webpack — add --bundler rspack when running the audit so the report labels it correctly | | esbuild | Run your build with --metafile=metafile.json | | Vite / Rollup | Add rollup-plugin-visualizer to your build with json: true, then point this tool at the file it writes | | Parcel | Parcel doesn't produce a stats file on its own — see Using this with Parcel below | | Turbopack | Not supported yet — see Turbopack below |

CLI commands

audit — check one or more apps for duplicate dependencies

Three ways to run it, depending on how many apps you have:

# One app
npx bundle-dep-auditor audit apps/cart/dist/stats.json --app-name cart

# Several apps, each with its own settings — recommended once you have more than one app
npx bundle-dep-auditor audit --files apps.json

# Several apps, none of which need any settings changed
npx bundle-dep-auditor audit apps/cart/dist/stats.json apps/checkout/dist/stats.json

Options:

| Option | Value | Default | What it does | | --- | --- | --- | --- | | --files | path to a JSON file | — | Describes multiple apps at once. See The --files file below. Can't be combined with more than one file path on the command line. | | --app-name | text | the file's parent folder name | Label for this app in the report. Only for the single-app form above. | | --root-dir | path | the file's own folder | Folder to look in for node_modules when resolving package versions/licenses. Only for the single-app form above. | | --bundler | webpack, rspack, esbuild, vite-rollup, or parcel | guessed automatically | Tells the tool which bundler produced the file. Only needed to tell webpack and Rspack apart, since they produce identical files. | | --config | path | bundle-dep-auditor.config.json | Path to your config file. See Config file below. | | --format | json, csv, markdown, or html | markdown | Report format. | | --out | path | — | Write the report to a file instead of printing it. |

The --files file

A JSON file listing every app you want to check. Give it either one object, or an array of them:

[
  { "file": "apps/cart/dist/stats.json", "appName": "cart" },
  { "file": "apps/checkout/dist/stats.json", "bundler": "esbuild" }
]

Each entry accepts:

| Field | Required? | Value | What it does | | --- | --- | --- | --- | | file | Yes | path | The stats/metafile JSON for this app. | | appName | No | text | Label for this app in the report. Defaults to the file's parent folder name. | | rootDir | No | path | Folder to look in for node_modules. Defaults to the file's own folder. | | bundler | No | webpack, rspack, esbuild, vite-rollup, parcel, or unknown | Which bundler produced this file. | | sharedPackages | No | array of package names | Packages this app declares as a Module Federation shared/singleton dependency (or the equivalent for any other multi-app runtime). See Module Federation shared packages below. |

If an entry has a field that isn't in this list, or a value of the wrong type (e.g. a number where text is expected), the command stops with an error explaining exactly what's wrong — it never runs with a mistyped setting silently ignored.

Why a file instead of repeating flags? With several apps, flags like --app-name used to be matched to files by their position in the command — easy to get out of sync by accident. Putting each app's file and settings in one place removes that risk entirely.

Module Federation shared packages

If your apps are Module Federation remotes/hosts, some packages are declared singleton: true in your MF config's shared block (commonly react, react-dom, and any design-system package every remote loads into the same runtime). A version mismatch in one of these is worse than an ordinary duplicate — the shared scope can only resolve to one version, so a mismatch is a runtime break, not just extra bytes downloaded.

List those package names per app in sharedPackages:

[
  { "file": "apps/cart/dist/stats.json", "appName": "cart", "sharedPackages": ["react", "react-dom"] },
  { "file": "apps/checkout/dist/stats.json", "appName": "checkout", "sharedPackages": ["react", "react-dom"] }
]

Any package listed in at least one app's sharedPackages is checked independently of the size budgets below: it fails the moment more than maxDistinctVersionsPerPackage versions of it exist, no matter how small it is. Everything not listed is judged purely on size, same as before this option existed — this is opt-in, so leaving it out changes nothing.

diff — compare the same app at two points in time

The most common real source of version drift isn't two apps in one monorepo (a shared lockfile can't disagree with itself) — it's the same app rebuilt after its dependencies moved on: an old release vs. the current one, or one Module Federation remote's last deployed build vs. its next one, each built independently on its own schedule.

npx bundle-dep-auditor diff \
  --baseline releases/v1.80.0/stats.json --baseline-root-dir releases/v1.80.0 \
  --current dist/stats.json --current-root-dir . \
  --format markdown

This runs the exact same aggregate-and-gate check as audit --files with just these two builds, so a package that moved between the two shows up as an ordinary duplicate-version finding.

Options:

| Option | Value | Default | What it does | | --- | --- | --- | --- | | --baseline | path | — | Required. Stats/metafile JSON for the earlier build. | | --current | path | — | Required. Stats/metafile JSON for the later build. | | --baseline-name / --current-name | text | baseline / current | Label for each build in the report. | | --baseline-root-dir / --current-root-dir | path | each file's own folder | Folder to look in for node_modules, per side. Deliberately separate flags, not one shared --root-dir — the two builds are usually two different checkouts with two different (or no longer existing) node_modules. | | --baseline-bundler / --current-bundler | webpack, rspack, esbuild, vite-rollup, or parcel | guessed automatically | Which bundler produced each file. | | --config | path | bundle-dep-auditor.config.json | Path to your config file. | | --format | json, csv, markdown, or html | markdown | Report format. | | --out | path | — | Write the report to a file instead of printing it. |

collect — preview how one file gets read

Useful for checking that a single stats file is recognized correctly before running a full audit.

npx bundle-dep-auditor collect apps/cart/dist/stats.json --app-name cart

| Option | Value | Default | What it does | | --- | --- | --- | --- | | --app-name | text | the file's parent folder name | Label for this app. | | --root-dir | path | the file's own folder | Folder to look in for node_modules. | | --bundler | webpack, rspack, esbuild, vite-rollup, or parcel | guessed automatically | Which bundler produced the file. | | --out | path | — | Write the result to a file instead of printing it. |

Config file

Optional — create bundle-dep-auditor.config.json in your project root to change the pass/fail rules. If you don't create one, sensible defaults apply.

{
  "gate": {
    "maxDistinctVersionsPerPackage": 1,
    "maxWastedGzipBytesPerPackage": 51200,
    "maxWastedGzipBytesTotal": 153600,
    "maxWastedRawBytesPerPackage": 153600,
    "maxWastedRawBytesTotal": 460800,
    "ignorePackages": ["react", "react-dom"],
    "ignorePackagePatterns": ["^@internal/"]
  }
}

| Setting | Default | What it means | | --- | --- | --- | | maxDistinctVersionsPerPackage | 1 | How many different versions of one package are allowed before it's flagged. | | maxWastedGzipBytesPerPackage | 51200 (50 KB) | How many extra gzip bytes one duplicated package can cost before it fails on its own. | | maxWastedGzipBytesTotal | 153600 (150 KB) | How many extra gzip bytes all duplicates combined can cost before the whole check fails. | | maxWastedRawBytesPerPackage | 153600 (150 KB) | Same as maxWastedGzipBytesPerPackage, but for a package your bundler never reported a gzip size for (see below). | | maxWastedRawBytesTotal | 460800 (450 KB) | Same as maxWastedGzipBytesTotal, but for packages your bundler never reported a gzip size for. | | ignorePackages | [] | Exact package names to skip entirely (e.g. one you're migrating and haven't fixed yet). | | ignorePackagePatterns | [] | Regular expressions matching package names to skip. |

The audit also always flags a package that's listed as a devDependency in an app's own package.json but still ends up in that app's shipped bundle — usually a sign of an accidental import. ignorePackages/ignorePackagePatterns apply to this check too.

Like the --files file above, an unrecognized setting in this file is a validation error rather than being silently ignored.

If your bundler doesn't report gzip sizes (see the Gzip size row in Reading your report below), a duplicated package with no gzip data at all can't be judged against maxWastedGzipBytes* — it would always look like it wastes 0 bytes. For those packages only, the gate falls back to maxWastedRawBytes* instead, and the report adds a warning telling you it did so, so you know to add a compression plugin (or just trust the raw numbers) rather than assuming the gate is silently passing everything.

Reading your report

Whichever format you choose (JSON, CSV, Markdown, or HTML), each row is one package used by one app. Here's what each field means:

| Field | What it tells you | | --- | --- | | Package | The npm package name, e.g. lodash. | | Version | The version of that package this app has bundled. | | How the version was found | resolved-package-json = read straight from that package's own package.json on disk — the reliable case. path-embedded = the install layout named the version in the folder path itself (common with pnpm) — also reliable. unknown = neither worked, usually because the package couldn't be found on disk (see Good to know below). | | Size | How many bytes this package added to the app's bundle. | | Gzip size | The compressed size — closer to what actually gets downloaded. Blank/missing when your bundler didn't compute it (e.g. esbuild never reports this; webpack only does if you're using a compression plugin). | | Duplicated? | Yes if two or more apps bundle different versions of this package. | | Most common version | When a package is duplicated, this is the version most apps agree on — the one the "wasted bytes" number below is measured against. | | Wasted bytes | How many gzip bytes you'd stop shipping if every app switched to the most common version. Only non-zero for duplicated packages. | | Scope | MF shared if this package was listed in some app's sharedPackages (see Module Federation shared packages). A mismatch here fails the gate regardless of size. | | Imported by | Which file in your app's source code pulled this package in — useful for tracking down why it's there. | | Loads | initial = downloaded immediately on page load. async = only loaded when a certain route/feature is used. unknown = your bundler's output didn't say. | | License | The package's license (e.g. MIT), plus where its LICENSE file lives on disk. Blank when the package couldn't be found on disk to check. | | Dev dependency in bundle? | Yes if your app's own package.json lists this package under devDependencies, yet it still shipped in the production bundle — almost always an accidental import worth investigating. |

If a check fails, the report also includes a plain-English reason (which versions, how many apps, how many bytes) and a short list of suggested fixes — e.g. align every app on the same version, or move a wrongly-shipped package out of devDependencies.

Node.js API

import { audit, toMarkdown } from 'bundle-dep-auditor';

const report = audit([
  { file: 'apps/cart/dist/stats.json', appName: 'cart' },
  { file: 'apps/checkout/dist/stats.json', appName: 'checkout' },
]);

console.log(toMarkdown(report));

if (!report.gate.passed) {
  process.exitCode = 1;
}

Using this with Parcel

Parcel doesn't write a stats file the way other bundlers do, so this package ships a Parcel Reporter plugin that writes one during your build instead:

npm install --save-dev @parcel/plugin
// .parcelrc
{
  "extends": "@parcel/config-default",
  "reporters": ["...", "bundle-dep-auditor/parcel-reporter"]
}

By default it writes to ./parcel-bundle-report.json (override with the BUNDLE_DEP_AUDITOR_OUT environment variable). Feed that file into audit exactly like any other app's stats file.

Turbopack

Not supported yet — its stats output isn't stable enough across Next.js releases to build a reliable reader for it. If you're on Next.js, run next build --webpack to get a webpack stats file this tool can already read.

Good to know

  • pnpm's strict linking can leave a package unresolvable on disk in some setups — usually a package only reachable transitively under pnpm's stricter isolation. When that happens, its version and license show up as unknown (see Reading your report above) rather than a guessed wrong answer.
  • Vite/Rollup support depends on the community rollup-plugin-visualizer plugin's JSON shape, which has changed across its major versions. If your version's output isn't recognized, you'll get a clear error rather than a silently wrong report.
  • webpack and Rspack look identical in their stats files, so this tool can't tell them apart automatically — use --bundler rspack if you want the report labeled correctly.