google/zx · error · Fail
Can't link node_modules: ${target} doesn't exist or is not a
Error message
Can't link node_modules: ${target} doesn't exist or is not a directory What it means
Thrown by linkNodeModules() in the zx CLI when the resolved node_modules target does not exist or is not a directory. With --prefer-local=<path>, zx symlinks cwd/node_modules to <path>/node_modules (or to <path> directly if its basename is already node_modules), so the target must be a real directory. The guard uses lstat, so a broken symlink or a file at that path also fails the isDirectory() check.
Source
Thrown at src/cli.ts:187
// TODO: fix unanalyzable-dynamic-import to work correctly with jsr.io
await import(url.pathToFileURL(scriptPath).toString())
} finally {
rmTemp()
}
}
function linkNodeModules(cwd: string, external: string): string {
const nm = 'node_modules'
const alias = path.resolve(cwd, nm)
const target =
path.basename(external) === nm
? path.resolve(external)
: path.resolve(external, nm)
const aliasStat = lstat(alias)
const targetStat = lstat(target)
if (!targetStat?.isDirectory())
throw new Fail(
`Can't link node_modules: ${target} doesn't exist or is not a directory`
)
if (aliasStat?.isDirectory() && alias !== target)
throw new Fail(`Can't link node_modules: ${alias} already exists`)
if (aliasStat) return ''
fs.symlinkSync(target, alias, 'junction')
return alias
}
function lstat(p: string) {
try {
return fs.lstatSync(p)
} catch {}
}
async function readScript() {
const [firstArg] = argv._View on GitHub (pinned to 00a2c484e2)
Solutions
- Confirm the target exists and is a directory: `ls -la <prefer-local-path>/node_modules`.
- Run the package manager install in the external directory so node_modules exists (e.g. `cd <path> && npm i`).
- If pointing --prefer-local directly at the node_modules folder, ensure its basename is exactly `node_modules` (zx then uses it directly).
- Drop the --prefer-local flag if cross-project linking is not required.
Example fix
// before: ./libs/shared has no node_modules $ zx --prefer-local=./libs/shared run.mjs // after: ensure deps are installed in the external dir first $ (cd ./libs/shared && npm i) && zx --prefer-local=./libs/shared run.mjs
Defensive patterns
Strategy: validation
Validate before calling
import fs from 'node:fs'
import path from 'node:path'
function resolvePreferLocalTarget(external: string): string {
const nm = 'node_modules'
const target = path.basename(external) === nm
? path.resolve(external)
: path.resolve(external, nm)
const stat = fs.lstatSync(target)
if (!stat.isDirectory()) throw new Error(`not a directory: ${target}`)
return target
}
// run before launching zx with --prefer-local
resolvePreferLocalTarget(argv.preferLocal) Try / catch
import { Fail } from 'zx'
try {
await runScriptWithPreferLocal(path)
} catch (e) {
if (e instanceof Fail && /doesn't exist or is not a directory/.test(e.message)) {
console.error('prefer-local target missing — run npm i in the external dir')
}
throw e
} Prevention
- Always run the package manager install in the external directory before pointing --prefer-local at it.
- Resolve --prefer-local to an absolute path and assert node_modules exists in a preflight check.
- Treat an empty/missing node_modules as a setup failure, not a runtime retry.
When it happens
Trigger: Running `zx --prefer-local=./libs/shared run.mjs` (or `-l <path>`) where <path>/node_modules does not exist, is a file, or is a dangling symlink. Also when argv.preferLocal is a string pointing at a path whose node_modules was deleted/moved after install, or pointing --prefer-local at a file rather than a project directory.
Common situations: Typo in the prefer-local path; pointing at a sibling project that has never run `npm install`; CI where the external deps directory is not mounted; pointing --prefer-local at the node_modules folder of a freshly-cloned repo before deps are installed.
Related errors
- Can't link node_modules: ${alias} already exists
- No script provided
- Failed to fetch remote script: ${remote} (${res.status})
AI-assisted analysis of google/zx@00a2c484e2 (2026-08-13).
Data as JSON: /api/errors/cf896e457a816855.
Report an issue: GitHub.