← package.json Exports Drift Check

This checker vs. manually checking each exports target by eye

Names no specific product or company — the comparison is against reading an exports map yourself, a habit every package.json author already has. Manually checking needs no tool: open the exports map, and for each subpath and condition, confirm the file it names actually exists. It works for a map with one or two subpaths. It stops working at the scale a real package's exports field actually reaches, because a present target and a missing one read identically as JSON — nothing about the text says whether the file is there.

What each method is

Manually checking means opening package.json, finding the exports field, and for every subpath and every nested condition (import/require/types/…) confirming the target file is really there — usually by clicking through a file tree or running ls against each path in turn. It costs nothing and needs no tool, and for a small map it is completely reliable: a person who checks two files correctly checks two files correctly, every time.

This tool does the same check mechanically, from two pasted text blocks: a package.json and a listing of the files you actually publish. It flattens every subpath, every nested condition, and every position in a fallback array into one flat list of literal targets, then reports the exact ones missing from the listing — with no install, no publish, and no network call either way.

SubpathConditionTargetOn disk?
.import./esm/index.jsyes
.require./cjs/index.jsyes
.types./types/index.d.tsyes
./buttonimport./esm/button.jsyes
./buttonrequire./cjs/button.jsyes
./buttontypes./types/button.d.tsyes
./modalimport./esm/modal.jsyes
./modalrequire./cjs/modal.jsmissing
./modaltypes./types/modal.d.tsyes
./theme—./cjs/theme.jsyes
./package.json—./package.jsonyes

Eleven literal targets across five subpaths. Ten exist. The one that doesn't — the require condition inside ./modal — is where a maintainer who develops and tests the package entirely through its own ESM import path would never personally exercise the CommonJS fallback, so a rename that left cjs/modal.js behind (or a build step that simply never emitted it) passes every test the maintainer actually ran. Eyeballing the same eleven rows, nothing on the page marks the ninth one as different from the other ten — it is exactly as well-formed a piece of JSON as any target that does exist. Pasted into this tool, the same map and file listing resolve to one finding: 1 export target(s) named in package.json do not exist, naming cjs/modal.js specifically.

QuestionWhere to answer it
My exports map has one or two subpaths and I already know every file it points at. Either — manually checking is completely reliable at this size, and costs nothing.
My exports map nests conditions across a dozen or more subpaths and I want to be sure every literal target still exists. This checker — paste both, get the exact list of missing targets in one click, no signup.
I only test my package through its ESM import path and want to know if a require or types target quietly broke. This checker — it checks every condition the same way, regardless of which one your own tests happen to exercise.
I want to wire this into CI as the last gate before npm publish, reading only local files. Neither needs a tool per se, but only the checker's CLI (cli.mjs) is scriptable and deterministic enough to fail a build on.

This tool needs no account, no install, and no network call: it is a static page that runs entirely in your browser, or a zero-dependency Node CLI that reads only local files.

Common questions

What does eyeballing an exports map get wrong?

Every literal target in an exports map reads the same on the page: a quoted relative path next to a subpath and a condition name. Nothing about the text itself says whether the file at the end of that path actually exists — that's a fact about the filesystem, not about the JSON. A small map (one or two subpaths) is realistic to check by opening each file yourself. A real package's map nests import/require/types conditions and fallback arrays across a dozen or more subpaths, and reading all of them correctly, every time, is exactly the kind of bookkeeping that slips under time pressure — especially for a condition like require that a maintainer who only tests the import path never personally exercises.

Can you give a concrete example?

A package.json declares five subpaths — ".", "./button", "./modal", "./theme", "./package.json" — totalling eleven literal targets once every import/require/types condition is flattened. The files actually published are missing exactly one of them: cjs/modal.js, the require condition inside "./modal". Every other target exists. Pasted into this tool, the package.json and the file listing resolve to one finding: 10 export target(s) exist, 1 export target(s) named in package.json do not exist — and it names cjs/modal.js specifically. Eyeballing the same eleven lines, nothing distinguishes the missing one from the other ten; a maintainer who tests only through the ESM import path (the common case in a codebase and its own tests) would never notice modal's require target was broken until a CommonJS consumer's require() call failed.

Is this tool just automating something obvious?

Reading whether one target file exists is obvious once you already know which line to check. What manual review gets wrong is coverage across the whole map: a deeply nested exports field mixes conditions a maintainer actively exercises (typically the one their own build and tests use) with ones they don't (often require, if the project itself only imports its own code with ESM). This tool doesn't know which condition matters more to any given consumer — it checks every literal target in the map, every subpath, every condition, every fallback-array position, the same way, so coverage doesn't depend on which paths a maintainer happens to run.

Does this replace testing the package after publish?

No. It replaces the step of manually re-reading the exports map to decide whether every target it names is present on disk. A clean result means every literal target you pasted a file listing for exists at that path — it is not a guarantee the package installs or imports cleanly, and it does not resolve wildcard targets or bare-specifier re-exports at all. Verify with npm publish --dry-run and a real install, exactly as this tool's own front page and footer already say.