npm "Cannot find module" After --omit=dev - Build Tool Stripped in CI
A production install (--omit=dev or NODE_ENV=production) drops devDependencies. If your build step needs a tool that lives there - TypeScript, a bundler, a plugin - the build then fails to find it.
What this error means
Install succeeds, but the build step crashes with Cannot find module for a build tool (e.g. typescript, vite, a webpack loader). It works in a normal install but breaks once --omit=dev strips devDependencies.
$ npm ci --omit=dev
$ npm run build
> tsc -p tsconfig.json
sh: 1: tsc: not found
npm error code 127Diagnose it: reproduce the CI install locally
Install failures are usually environment drift rather than a broken lockfile: a different package-manager major, a different Node version, or a cache that is being restored from a run with different inputs. Reproduce the CI conditions before changing the lockfile, because regenerating it hides the real cause.
# match the runner exactly, then install from a clean slate
node --version && npm --version
rm -rf node_modules
npm ci --foreground-scripts
# if that succeeds locally but fails in CI, the difference is the cache
# or the package-manager version, not your lockfileCommon causes
Build tooling is in devDependencies but the build runs after a prod install
Compilers and bundlers are devDependencies by convention. A --omit=dev install removes them, so the build cannot run.
NODE_ENV=production implicitly omits dev deps
Setting NODE_ENV=production makes npm skip devDependencies even without --omit=dev, producing the same gap.
How to fix it
Install dev deps for the build, prune after
Do a full install to build, then prune to production for the runtime artifact.
npm ci # includes devDependencies
npm run build
npm prune --omit=dev # drop dev deps from the final imageSplit build and runtime installs
- Run the build with a complete install (no
--omit=dev). - Produce the runtime image with
npm ci --omit=devover the built output. - Do not set
NODE_ENV=productionfor the build stage.
Verify the fix survives a cold cache
A green run immediately after a fix often proves nothing, because it restored a cache written before the change. Force a cold install once to confirm the fix is real.
# temporarily bust the cache key to prove the fix on a cold runner
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: package-lock.json
# then bump this suffix once, run, and remove it
# key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}-v2How to prevent it
- Build with all deps; prune to production afterward.
- Use multi-stage Docker to separate build and runtime installs.
- Avoid NODE_ENV=production during the build step.