Node ERR_MODULE_NOT_FOUND - Fix ESM Import Resolution in CI
Under native ESM, Node does not guess file extensions the way CommonJS did. An import without an explicit extension (or to a non-existent path) fails with ERR_MODULE_NOT_FOUND.
What this error means
A type: module package (or .mjs) crashes resolving a relative import with ERR_MODULE_NOT_FOUND. It frequently appears after compiling TypeScript to ESM, where emitted imports lack the .js extension.
node:internal/errors ... [ERR_MODULE_NOT_FOUND]:
Cannot find module '/app/dist/utils' imported from /app/dist/index.js
Did you mean to import ./utils.js?Diagnose it: what is different about the runner?
A build that passes locally and fails on a runner differs in a small number of predictable ways. Check those before changing build configuration, because the build config is usually not the thing that changed.
- run: |
node --version && npm --version
echo "NODE_ENV=$NODE_ENV CI=$CI"
nproc && free -h && df -h /
ls -la node_modules/.bin | headCommon causes
Missing file extension in ESM imports
Native ESM requires the full specifier, including the extension. import x from "./utils" fails; ./utils.js is required.
TypeScript emitted extensionless imports
TS source written ./utils and the compiler did not rewrite it to ./utils.js, so the emitted ESM cannot be resolved by Node.
Wrong path or missing index resolution
ESM does not auto-resolve directory index.js the way CJS does unless explicitly configured; a bare directory import fails.
How to fix it
Use explicit extensions in ESM
Add the file extension to relative imports.
// before
import { x } from './utils';
// after
import { x } from './utils.js';Align module settings
- Set tsconfig
module/moduleResolutionto a Node ESM mode (e.g. NodeNext). - Reference directory entry points explicitly (
./dir/index.js). - Verify dist output paths exist and match the import specifiers.
The three that account for most of them
- Case sensitivity. Linux runners are case sensitive, macOS is not. An import with the wrong case resolves locally and fails in CI.
- Out of memory. Exit code 137 is a SIGKILL from the kernel, not a build error. Raise
--max-old-space-sizeor use a larger runner. - devDependencies pruned.
NODE_ENV=productionmakesnpm ciskip devDependencies, so the build tool itself goes missing. Set it after install, not before.
How to prevent it
- Use explicit
.jsextensions in ESM/TS-to-ESM imports. - Adopt NodeNext module resolution in tsconfig.
- Test the built ESM output in CI, not just the TS source.