Skip to content

[Kit]: Anchor drizzle-orm package probes to the project directory - #6172

Open
SayantanDutt wants to merge 1 commit into
drizzle-team:mainfrom
SayantanDutt:fix/6156-package-probe-resolution
Open

[Kit]: Anchor drizzle-orm package probes to the project directory#6172
SayantanDutt wants to merge 1 commit into
drizzle-team:mainfrom
SayantanDutt:fix/6156-package-probe-resolution

Conversation

@SayantanDutt

Copy link
Copy Markdown

This addresses part of #6156. I'm deliberately not using a closing keyword here, since this fixes the reported symptom but not the whole problem. See the known limitation section below.

Problem

Drizzle Kit reports:

Please install latest version of drizzle-orm

even though drizzle-orm is installed correctly. The original report was on nub, and I could also reproduce it with pnpm when the global virtual store is enabled.

Root cause

The probes in drizzle-kit/src/cli/utils.ts detect packages with a bare dynamic import:

const { compatibilityVersion } = await import('drizzle-orm/version');

I traced this down, and the real problem is where that specifier gets resolved from, not which loader does the resolving.

With pnpm's global virtual store enabled, node_modules/drizzle-kit ends up as a symlink into a store outside the project:

node_modules/drizzle-kit -> ~/AppData/Local/pnpm/store/v11/links/@/drizzle-kit/0.31.10/<hash>/node_modules/drizzle-kit

Node resolves the CLI's realpath, so bin.cjs actually executes from inside that store. Both ESM and CJS resolve a bare specifier by walking node_modules upward from the importing module's own location, and from inside the store, that walk never reaches the user's project.

I measured this directly from inside the store, same cwd, same specifier:

Strategy Result
await import('drizzle-orm/version') ✗ ERR_MODULE_NOT_FOUND
require('drizzle-orm/version') ✗ MODULE_NOT_FOUND
createRequire(process.cwd()).resolve(...) ✓ resolves

The identical file placed inside the project resolves fine via plain import(), so this is purely about the CLI's own location relative to the project.

I want to flag two corrections to the original issue's diagnosis, both of which I verified directly:

  • This isn't an ESM vs CJS difference. require() fails identically to import() in this layout. Swapping loaders alone doesn't fix anything.
  • NODE_PATH isn't the trigger. It's a narrower secondary rescue path: setting it does make a store-located require() succeed, because NODE_PATH feeds module.globalPaths, which only the CJS resolver consults. But I could reproduce the failure with NODE_PATH unset, so it isn't what's actually breaking here.

The fix works because createRequire(process.cwd()) anchors resolution to the user's project directory, independent of where the CLI binary physically lives. That anchor is the part doing the work, not CJS, not NODE_PATH.

The fix

I added one helper in drizzle-kit/src/cli/utils.ts and routed the probes through it:

const cwdRequire = createRequire(join(process.cwd(), 'index.js'));

export const importPackage = async (pkg: string): Promise<any> => {
  try {
    return await import(pkg);
  } catch (importError) {
    let resolved: string;
    try {
      resolved = cwdRequire.resolve(pkg);
    } catch {
      // fallback can't see it either, report the original failure
      throw importError;
    }
    return await import(pathToFileURL(resolved).href);
  }
};

A few notes on why I built it this way:

  • import() stays the fast path. The fallback only runs after it fails, so layouts that already worked are untouched.
  • I resolve with require but load with import. require.resolve gives an absolute path that honors package exports subpaths, and importing that resolved path keeps ESM-only packages loadable, which a direct require() wouldn't allow (ERR_REQUIRE_ESM).
  • I anchored at process.cwd() since that's the actual fix. It also sidesteps import.meta.url / __filename, neither of which works across both bundles drizzle-kit ships (import.meta.url breaks the CJS build, __filename breaks the ESM one).
  • I kept the original import() error when neither strategy resolves, so a genuinely missing package still reports as missing instead of a confusing fallback error.

I updated all six probe functions that had this bug: ormVersionGt, checkPackage, assertPackages, assertEitherPackage, assertOrmCoreVersion, ormCoreVersions.

I kept the scope confined to this one file. I didn't touch any snapshot, diffing, or dialect logic.

⚠️ Known limitation, this doesn't fully fix #6156

This fixes the specific probe failure that was reported, drizzle-kit falsely claiming drizzle-orm isn't installed, and I verified that against a real pnpm enableGlobalVirtualStore layout (details below).

It doesn't make drizzle-kit work end to end in that layout, though. After this fix, drizzle-kit generate gets past the version check and then fails further downstream:

Error: Cannot find module 'drizzle-orm'
Require stack:
- .../pnpm/store/v11/links/@/drizzle-kit/0.31.10/<hash>/node_modules/drizzle-kit/bin.cjs
    at resolveTsPaths (.../bin.cjs:15120:14)
  code: 'MODULE_NOT_FOUND'

drizzle-orm is marked external in the build, so the shipped bundle resolves it at roughly 63 other call sites, about 33 require("drizzle-orm…") calls (11 bare drizzle-orm, plus /casing, /relations, and the dialect cores) and 30 import("drizzle-orm/…") calls (/version, /relations, /pg-core, and the driver + migrator pairs). None of those are cwd-anchored, so each one still resolves from bin.cjs's own location and the failure just moves to the next unguarded site.

Fixing that bundle-wide is a materially bigger change than this PR, and I see two reasonable approaches that differ a lot in blast radius: an esbuild banner installing a cwd-anchored resolver for the whole bundle, or routing every external drizzle-orm resolution through one shared helper. I didn't want to guess at which one you'd prefer, since it depends on build and packaging constraints and on whether these layouts are even meant to be supported. Worth mentioning the same anchor question applies to this PR too: process.cwd() might need to become the config file's directory instead, for monorepos or invocation from a subdirectory.

Because of all this, I think #6156 should stay open after this merges.

Tests

I added drizzle-kit/tests/cli-package-resolution.test.ts with 3 cases that exercise the real probes in a child process:

  1. A package reachable only via NODE_PATH, asserts plain import() fails while the probe resolves it and the module actually loads.
  2. A package symlinked into node_modules from outside the project.
  3. A genuinely missing package, to guard against a false positive from the fallback.

I confirmed these are real regression tests by reverting the fix, cases 1 and 2 fail (expected false to be true) and case 3 still passes.

One caveat worth flagging on the tests: they exercise the fallback mechanism via NODE_PATH, since that's the easiest way to make a package invisible to import() in-process. They don't reproduce the actual realpath-anchoring scenario that triggers #6156, that required a full pnpm install and I only verified it manually (below). Happy to strengthen this if you'd want the real trigger covered in CI.

Verification

I ran this end to end against a real pnpm global virtual store (pnpm 11.23.0; worth noting enable-global-virtual-store in .npmrc gets silently ignored, it only takes effect via pnpm-workspace.yaml):

Build Result
unfixed Please install latest version of drizzle-orm, exit 1 (reproduces #6156 verbatim)
fixed probe passes, fails later at the bundled require above (see known limitation)
fixed, normal pnpm layout (control) full success, [✓] Your SQL migration file ➜ migrations/0000_….sql

The control run confirms the fix doesn't regress anything in standard layouts.

Static checks:

  • tsc -p tsconfig.build.json --noEmit → clean, exit 0.
  • Full drizzle-kit suite, before and after:
Test files Tests
baseline (no fix) 10 failed, 44 passed 26 failed, 810 passed
with fix 10 failed, 45 passed 26 failed, 813 passed

Same 10 failing files in both runs. Those failures are environmental on my machine, unrelated to this change: Docker wasn't running (connect ENOENT //./pipe/docker_engine, needed by the MySQL/SingleStore/Gel suites) and better-sqlite3's native binding never compiled (no MSVC toolchain here). The +3 passing tests are the new ones I added.

Notes for reviewers

  • The issue mentions the same mechanism exists on the v1.0.0-rc line. I'm only targeting main here, wasn't sure if you'd want this ported elsewhere.
  • One more thing I ran into while testing, separate from this PR but worth flagging: when the downstream failure above happens, generate prints the error but still exits 0 and writes no migration SQL, the only output is an empty migrations/meta directory. Anything that gates on exit code reads that as success. That's independent of this PR and of the layout that triggered it, a misleading success code is a problem regardless of the underlying cause. I didn't fix it here, but happy to open a separate issue if that's useful.

@SayantanDutt
SayantanDutt force-pushed the fix/6156-package-probe-resolution branch from af4d481 to 67e3ba4 Compare August 24, 2026 06:37
drizzle-kit's package probes in utils.ts resolved drizzle-orm relative
to bin.cjs's own realpath rather than the project directory, breaking
detection under pnpm's global virtual store and similar symlink
layouts. Anchor resolution via createRequire(process.cwd()) instead.

Addresses part of drizzle-team#6156, does not fully resolve it. See PR description
for the remaining scope.
Signed-off-by: Sayantan <duttasayantan257@gmail.com>
@SayantanDutt
SayantanDutt force-pushed the fix/6156-package-probe-resolution branch from 67e3ba4 to b63485e Compare August 24, 2026 06:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: Drizzle Kit doesn't work if node_modules contains symlinks outside of project

1 participant