Webpack "Module not found: Can't resolve" - Fix in CI
Webpack walked an import/require and could not find the target on disk or in node_modules. The cause is almost always a dependency that was never installed, a path that is wrong, or a resolve config the build never set up.
What this error means
The build fails with Module not found: Error: Can't resolve '<module>' pointing at the importing file. It is deterministic - the same import fails the same way every run, and re-running never helps.
ERROR in ./src/App.jsx 3:0-29
Module not found: Error: Can't resolve './componets/Header' in '/app/src'
resolve './componets/Header' in '/app/src'
using description file: /app/package.json (relative path: ./src)Diagnose it: is it resolution, transform, or memory?
Bundler failures in CI fall into three families and the error text often points at the wrong one. A module that resolves on your machine and not on the runner is nearly always case sensitivity or a missing optional dependency; a transform error is a config or version mismatch; and an unexplained kill with no stack is the out-of-memory reaper, not a build error at all.
# 1. resolution: does the file exist with EXACTLY that case?
git ls-files | grep -i "the/imported/path"
# 2. transform: what versions is CI actually resolving?
npm ls webpack vite rollup esbuild typescript 2>/dev/null | head -20
# 3. memory: was it killed rather than failed?
# exit 137 = SIGKILL (OOM). Nothing in the bundler log will explain it.
node --max-old-space-size=4096 node_modules/.bin/vite buildCommon causes
Dependency not installed
The package is imported but missing from node_modules - not in package.json, or npm ci ran against a lockfile that does not include it. Works locally where it was installed ad hoc.
Wrong path or case mismatch
A typo (./componets), a wrong relative depth, or a casing difference. The last bites hardest in CI: Linux is case-sensitive, so ./Header vs ./header passes on macOS and fails on the runner.
Missing resolve alias or extension
An alias like @/ or a non-default extension (.ts, .jsx) is not declared in resolve.alias / resolve.extensions, so Webpack cannot map the bare specifier to a file.
How to fix it
Install the dependency and verify the path
Add the missing package and confirm the import resolves to a real file, with exact casing.
npm install <package>
# verify the file exists with that exact case
ls -la src/components/Header.jsxDeclare aliases and extensions
Make custom specifiers resolvable in the Webpack config.
// webpack.config.js
resolve: {
alias: { '@': path.resolve(__dirname, 'src') },
extensions: ['.js', '.jsx', '.ts', '.tsx'],
}Match casing for case-sensitive runners
- Compare the import string against the real filename, character for character.
- Rename the file or the import so casing matches exactly.
- Add
CaseSensitivePathsPluginso the mismatch fails locally too, not only in CI.
Make the build reproducible before you debug it
- Pin the Node major in
setup-nodeand inengines. A bundler that resolves native bindings will pick a different prebuilt binary across majors. - Delete
node_moduleslocally and reinstall from the lockfile before concluding the runner is at fault; most "works locally" reports are stale local state. - Set
CI=truelocally to reproduce. Several toolchains change behaviour under it, including treating warnings as errors. - Exit code 137 is an out-of-memory kill. Raise
--max-old-space-sizeor move to a larger runner rather than searching the bundler config.
How to prevent it
- Commit a lockfile and install with
npm ciso dependencies are reproducible. - Use
CaseSensitivePathsPluginso case bugs surface on macOS/Windows before CI. - Keep Webpack
resolve.aliasin sync with your tsconfigpaths.