Cannot find module ‘./foo’ — but only in CI
Measured on Windows, macOS and Linux · OpusMill
— or, from a bundler —
Module not found: Error: Can't resolve './Foo' in '/src'
The capitalisation of your import does not match the file on disk. Change the import to match the file exactly:
- import Button from "./components/button"; + import Button from "./components/Button";
It passes on your machine because macOS and Windows do not care about case. Linux does, and Linux is what your CI runs on.
Why you cannot reproduce it locally
This is the part that wastes the afternoon. “It works on my machine” is not a figure of speech here — it is literally true, and the filesystem will back you up.
The same three lines of code, run on all three operating systems.
Every row below came from a CI run, not from memory: one file called
Foo.js, then ask about foo.js.
| Platform | readdirSync |
existsSync("foo.js") |
require("./foo") |
|---|---|---|---|
| Windows | ["Foo.js"] | true | loaded |
| macOS | ["Foo.js"] | true | loaded |
| Linux | ["Foo.js"] | false | MODULE_NOT_FOUND |
Look at the middle column. On Windows and macOS,
fs.existsSync("foo.js") returns true when the
file is called Foo.js. So the obvious debugging step
— “let me just check the file is there” —
confirms your mistake instead of finding it. Only
readdirSync tells the truth, and nobody reaches for
readdirSync.
Finding it before CI does
You cannot grep for this, because the wrong spelling is only wrong relative to a file you have to look up. What works is comparing every relative import against the real directory entries:
npx github:Hackierz/winbreak
It reads the directory rather than asking whether a path exists, for exactly the reason above, and reports the import, the real filename, and the line. This is the one check the browser version cannot do — it needs your filesystem.
Fixing the file instead of the import
Sometimes the file is misnamed and the import is right. Renaming a file so that only its case changes is genuinely awkward in git on a case-insensitive filesystem: git may simply not notice, and you end up with a commit that changes nothing and a CI run that still fails.
git mv --force button.js Button.js
If that does not take, the reliable two-step is to rename through a temporary name and commit in between:
git mv button.js tmp.js && git commit -m "wip" git mv tmp.js Button.js && git commit --amend
You can also make git stop pretending, per repository:
git config core.ignorecase false. Be aware this makes git
honest about a filesystem that is not, so it will start reporting files
it previously matched loosely — useful on a team with mixed
machines, mildly disruptive on the day you turn it on.
The variants
| Message | Where from |
|---|---|
Cannot find module './foo' | Node itself |
Module not found: Error: Can't resolve './Foo' | webpack |
Failed to resolve import "./Foo" | Vite |
TS2307: Cannot find module './foo' | TypeScript — and forceConsistentCasingInFileNames in tsconfig.json catches this at compile time, on every platform. Turn it on; it is default-true in newer TypeScript but plenty of older configs set it false. |
Why it survives so long in a codebase
Because it is invisible to everyone who could fix it. The author's machine loads it. Their colleague's machine loads it. Code review shows an import that looks fine, because it is fine as text — only the pairing of import and filename is wrong, and no reviewer holds both in their head. It fails for the first person whose filesystem disagrees, which is usually a build runner with nobody watching.
Check your own project for the rest of this class of bug.
Paste your code or your package.json into
the browser checker — nothing is uploaded,
it runs on your machine — or run
npx github:Hackierz/winbreak over the whole repository.
--fix repairs the npm scripts that have one obvious
answer and refuses the ones that need a human.
Other errors in the same family:
- The fix for 'NODE_ENV' is not recognized
- The fix for 'rm' is not recognized
- The fix for spawn EINVAL on Windows
- The fix for ENOENT on node_modules/.bin
- Path too long on Windows
- EPERM on Windows
Background: I scanned the 600 most-downloaded
npm CLI packages — 17.4% have a package.json
script that cannot run on Windows.