2026-09-06 · npm · node · typescript · programming
You Cannot Tell It Does Not Ship Until You Ship It
I published three CLIs of my own to npm. All three turned out to be broken, found in the last few minutes before publishing.
Every one of them ran locally. The tests passed. What was broken was the installed state, and that is not visible until you actually install it.
1. npm cannot run TypeScript inside node_modules
Node 22.18 and later runs .ts directly, so I pointed bin at the source and
expected to ship with no build step.
{ "bin": { "mytool": "src/cli.ts" } }
It works locally. Packed and installed, it does this:
Error [ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING]:
Stripping types is currently unsupported for files under node_modules,
for ".../node_modules/@scope/mytool/src/cli.ts"
Node refuses to strip types from anything under node_modules. It is a
deliberate restriction — a dependency's contents should not be transformed at
runtime.
So npx fails on the very first run.
The fix
Compile and ship dist/. A clone still runs the source with no build step.
{
"bin": { "mytool": "dist/cli.js" },
"files": ["dist", "README.md", "LICENSE"],
"scripts": { "prepack": "tsc -p tsconfig.json" }
}
Set rewriteRelativeImportExtensions (TypeScript 5.7+) so import "./x.ts" in
the source becomes "./x.js" in the output:
{ "compilerOptions": { "rewriteRelativeImportExtensions": true } }
typescript is a devDependency. The published package still has zero
runtime dependencies.
2. prepack alone does not reach anyone installing from git
npx github:owner/repo is a real way people install things, and plenty of
READMEs recommend it.
prepack runs for the registry. It does not run for a git install. The
package lands with no dist/, so the file bin points at does not exist:
sh: mytool: command not found
Add prepare. That one does run on git installs.
{
"scripts": {
"build": "tsc -p tsconfig.json",
"prepare": "tsc -p tsconfig.json",
"prepack": "tsc -p tsconfig.json"
}
}
Writing both looks redundant. With only one, the other path is guaranteed broken.
3. The install command in my README had never worked
This is the one that mattered.
A published repository's README said:
npx github:quintetkit/ccheck # from your repository root
I ran it in an empty directory:
npm error code ENOENT
npm error enoent Could not read package.json
npm cannot install a repository that has no package.json.
That repository had none — it has zero dependencies and runs with
node src/cli.ts, so it never seemed necessary. Everyone who followed the
first instruction in that README got this error. Since the day it was
published.
It had no stars, so the damage was small. Finding this after traffic arrived would have been the expensive version.
What these share
All three are the same shape: "works on my machine" and "works as installed" are different claims.
| what you can see | what you cannot |
|---|---|
node src/cli.ts runs |
it will not run inside node_modules |
npm pack succeeds |
a git install has no dist |
| a clone works | npm cannot install a repo with no package.json |
Looking only at the left column keeps the right column invisible forever.
Put the check in CI
The fix is to build the installed state and actually run it.
- name: it still runs once packaged
run: |
npm pack --silent
tmp=$(mktemp -d); cd "$tmp"
npm init -y > /dev/null
npm install --silent "$GITHUB_WORKSPACE"/*.tgz
npx mytool --help > /dev/null
npm pack succeeding guarantees nothing. Installing and running it does.
The git path is one line:
cd $(mktemp -d) && npx -y github:owner/repo --help
In an empty directory, so no local cache or global install is answering for it.
Worth doing before you publish
- Read the
npm packfile list, one line at a time. I was shipping compiled tests — eight files nobody runs - Set
filesexplicitly. Without it you ship things you did not picture - Check the
repositoryURL actually exists. Publishing one that 404s is easy - Check
versionagainst your git tags. Bumping one and not the other makes the history unfollowable later
Takeaways
- Node will not strip types under
node_modules—npxdies on the first run - Compile for the registry; a clone can still run the source
prepareandprepackcover different paths. One alone breaks the other- npm cannot install a repository with no
package.json - "Runs here" and "runs installed" are separate claims. The first hides the second
- In CI: pack it, install it, run it. A successful pack proves nothing
Related
- Distribute Custom Tools as Composite GitHub Actions (Without npm Publishing)
- Every Test Passed and It Could Not Read a Single Real Record
- Treating "It Worked" as Verification Ships Code That Only Works With One Commit
I publish the configuration for splitting Claude Code into separate personas —
Architect, Coder, Reviewer, Conflict Resolver — under MIT. Copy it, run
./setup.sh, and it works. It does not depend on your tech stack.
https://github.com/quintetkit/quartet
I built one real tool using nothing but this workflow. Every Issue, PR, review and merge is still there. The parts that went wrong were not deleted.
https://github.com/quintetkit/mdlinkcheck
The version that adds a UI Designer persona, review criteria, a per-Issue parallel execution script and a 10-chapter guide is on the product page.
The full kit — five personas, the scripts and the complete guide in English and Japanese — is on BOOTH, a Japanese store with an English interface that takes international cards.
https://quartet-dev.booth.pm/items/8807156
The workflow itself is available
Quartet, the four-persona version, is published free under MIT. Quintet adds a UI Designer persona, review criteria, a per-Issue parallel execution script, and a 10-chapter guide.
See the free version Product page