DEV Community

Cover image for You Cannot Tell It Does Not Ship Until You Ship It
quintetkit
quintetkit

Posted on

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" } }
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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" }
}
Enter fullscreen mode Exit fullscreen mode

Set rewriteRelativeImportExtensions (TypeScript 5.7+) so import "./x.ts" in
the source becomes "./x.js" in the output:

{ "compilerOptions": { "rewriteRelativeImportExtensions": true } }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

I ran it in an empty directory:

npm error code ENOENT
npm error enoent Could not read package.json
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In an empty directory, so no local cache or global install is answering for
it.

Worth doing before you publish

  • Read the npm pack file list, one line at a time. I was shipping compiled tests — eight files nobody runs
  • Set files explicitly. Without it you ship things you did not picture
  • Check the repository URL actually exists. Publishing one that 404s is easy
  • Check version against your git tags. Bumping one and not the other makes the history unfollowable later

Takeaways

  • Node will not strip types under node_modulesnpx dies on the first run
  • Compile for the registry; a clone can still run the source
  • prepare and prepack cover 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


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 — is available here.

https://quintetkit.gumroad.com/l/quintet

Top comments (0)