OpusMill

ENOENT spawning node_modules/.bin/… on Windows

Verified on Windows 11, Node 24.20.0 · OpusMill

Error: spawn node_modules/.bin/mocha ENOENT
The fix

Do not spawn the shim by path. Resolve the package's real entry point and run it with node:

const bin = require.resolve("mocha/bin/mocha.js");
execFile(process.execPath, [bin, ...args], cb);

If you must use the shim, append .cmd on Windows and pass shell: true — both, not either.

Why it happens

On macOS and Linux, node_modules/.bin/mocha is an extensionless file with a #!/usr/bin/env node line, and the kernel knows what to do with it.

On Windows there is no shebang mechanism, so npm writes different files: mocha.cmd for cmd.exe and mocha.ps1 for PowerShell. The extensionless mocha is either absent or is a shell script Windows cannot execute. Asking for it by that exact path is asking for a file that is not there, so you get ENOENT.

Reproducing it

execFile("node_modules/.bin/mocha", ["--version"], (e) => {
  console.log(e && e.code);   -> ENOENT
});

Verified on Windows 11 / Node 24.20.0. Note that unlike the .cmd EINVAL case, this one does arrive through the callback rather than throwing synchronously — so if (error) return will catch it, and if that is all you do, your program carries on believing the tool ran.

Why .cmd alone is not enough

The obvious patch is to append the extension on Windows. That gets you a file that exists, and then it fails differently: since Node 18.20.2 / 20.12.2, spawning a .cmd without shell: true throws EINVAL (the fix for CVE-2024-27980). So the half-fix moves you from ENOENT to EINVAL.

The one that avoids all of it

If the tool is something you can invoke as a library or a JS file, require.resolve plus process.execPath sidesteps shims, extensions and shells entirely, and behaves identically on every platform. It is also faster, because nothing spawns a shell.

For npm scripts specifically you usually do not need to spawn anything: "test": "mocha" in package.json already resolves through node_modules/.bin correctly on every platform, because npm does this work for you.

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:

Background: I scanned the 600 most-downloaded npm CLI packages — 17.4% have a package.json script that cannot run on Windows.