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

  1. Confirm the target exists and is a directory: `ls -la <prefer-local-path>/node_modules`.
  2. Run the package manager install in the external directory so node_modules exists (e.g. `cd <path> && npm i`).
  3. If pointing --prefer-local directly at the node_modules folder, ensure its basename is exactly `node_modules` (zx then uses it directly).
  4. 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

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


AI-assisted analysis of google/zx@00a2c484e2 (2026-08-13). Data as JSON: /api/errors/cf896e457a816855. Report an issue: GitHub.