Skip to content

Commit 42f09b9

Browse files
Config evaluation works through pnpm symlink layouts; engine 0.2.1 (#222)
## What Any project whose `node_modules` reaches `@prisma/cli-engine` through a pnpm symlink that is not realpath'd — this repo's own dev checkout, and any install with `hoist=false` in `.npmrc` — cannot evaluate any `prisma.config.ts`: every config-reading command fails with `CLI.CONFIG_UNREADABLE` / `Cannot find package 'pathe' imported from .../cli-engine/node_modules/c12/dist/index.mjs`. npm and default-pnpm installs are unaffected. **Why:** the engine loads c12 lazily with a bare `await import("c12")`. When that specifier resolves through a symlink without realpathing, c12's own `import "pathe"` walks up from the symlink path and misses c12's store siblings. ## Changes - **The fix:** the engine imports c12 through its real path (`import.meta.resolve` → `realpathSync` → `pathToFileURL`). Evaluation errors still surface as the existing unreadable-config diagnostic. - **Import purity:** the module lexer cannot see `import.meta.resolve`, so the engine's conformance subjects excuse c12 explicitly via `allowedUnimported: ["c12"]` (conformance script ×2, engine conformance test). The tarball check gains the per-package `allowedUnimported` field. - **Engine 0.2.1** (`pnpm bump-cli-engine-version patch`): the changed engine must ship under a new version. The registry families still peer 0.2.0, so the tarball check carries two recorded transition exceptions; the follow-up PR that pins the families' 0.2.1-peering releases removes them (same train as the 0.2.0 transition, #200#207). - The deferred ledger entry tracking this defect is removed. ## Verification - Dev-layout A/B: with a marked `prisma.config.ts` in a scratch dir, `node packages/cli/dist/cli.js migration list` built from main fails with the pathe error; built from this branch it evaluates the config (remaining error is the expected absent-`orm`-section complaint). - Packed `@prisma/cli` + engine tarballs installed with pnpm and `hoist=false`: config evaluates — the previously broken published shape. - `pnpm run conformance` (packages/cli): **0 failing, 3 allowed** (the recorded transition mismatches), 5 subjects. - Engine suite 819 passed; CLI suite 831 passed; conformance package tests 59 passed; credential-free e2e passed; `check-engine-version.mjs origin/main`: 0.2.1 consistent; typecheck and grammar green. ## After merge 1. main's publish run ships `@prisma/cli-engine@0.2.1`. 2. prisma/composer and prisma/prisma release `@prisma/composer-cli` and `@prisma/orm-toolchain` peering 0.2.1. 3. A follow-up PR here pins those releases and empties the exception list. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
1 parent 37aef39 commit 42f09b9

8 files changed

Lines changed: 49 additions & 14 deletions

File tree

.drive/projects/prisma-cli-v8/deferred.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@ composer can use it.
9393
Running the shipped `prisma` binary against a scratch directory, rather than the ORM family through the test harness, turns up three things. The first is a defect a user hits on their first command.
9494

9595
- **`prisma orm init` scaffolds a project the `prisma` binary cannot read.** It writes `prisma-next.config.ts` — the standalone `prisma-next` bin's config file — and then fails its own last step, `Emit the contract`, with exit 5 and `Config is not a defineConfig result`. Nine files are already on disk at that point. Running any ORM command afterwards fails again, differently: the mounted family reads its configuration from an `orm` section of `prisma.config.ts` (`ormConfigSection`, `packages/1-framework/3-tooling/cli/src/orm/config-section.ts` in prisma/prisma), so it reports `CLI.CONFIG_SECTION_INVALID` and `CONFIG.FILE_NOT_FOUND` — "The orm config section is absent, so prisma-next.config.ts was never evaluated." So `prisma orm init && prisma contract emit` cannot work, and the two config surfaces have different shapes: the section nests the whole config under `orm`, while the scaffolded file exports a `defineConfig` result. Which side moves is the ORM's call; that it is broken today is not in question.
96-
- **Loading a hand-written `prisma.config.ts` failed with `Cannot find package 'pathe'`**, imported by `c12` from `packages/cli-engine/node_modules/c12`. `c12` declares `pathe` and the package is in the workspace store, so this is probably a pnpm layout artifact of running the built binary from inside the monorepo rather than a shipping defect — **but it is unverified**, and if it does reproduce from a packed tarball then every command that reads a config file is broken on install. Worth one run of the S6 tarball check with a config file present.
9796
- **The e2e coverage convention excludes all 22 ORM commands on reasoning #171 disproved.** `tests/e2e-coverage.test.ts` excuses them with "Real e2e lives in prisma/prisma (R7); the shell proves composition in orm-mount.test.ts (R8)." prisma/prisma's suite passed throughout the presentations change while the assembled binary exited 2, and `orm-mount.test.ts` proves composition for exactly one command, `migration list`, not per family. The operator's ruling (2026-08-13) is that every mounted command needs a happy path in this repo, precisely because the product repos cannot reproduce the assembled CLI. The exclusion should become a backlog entry once the first item above is fixed and the commands can run at all.
9897

9998
## A live bug carried out of the port (found closing PR #92, 2026-08-12)

packages/cli-engine/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@prisma/cli-engine",
3-
"version": "0.2.0",
3+
"version": "0.2.1",
44
"description": "The execution engine of the unified Prisma CLI.",
55
"type": "module",
66
"exports": {

packages/cli-engine/src/config-loader.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
*/
2727
import { existsSync, realpathSync } from "node:fs";
2828
import { dirname, join, resolve } from "node:path";
29+
import { fileURLToPath, pathToFileURL } from "node:url";
2930
import type { Diagnostic } from "./protocol";
3031
import type { LoadedConfig } from "./runtime";
3132
import { PRISMA_CONFIG_VERSION } from "./runtime";
@@ -189,7 +190,11 @@ const EVALUATE_ONE_FILE_ONLY = {
189190
* exact path and must not answer with another.
190191
*/
191192
async function evaluateConfigFile(path: string): Promise<unknown> {
192-
const c12 = await import("c12");
193+
// Imported via its realpath: under pnpm symlink layouts c12's own
194+
// dependencies are only reachable from its real location in the store.
195+
const c12 = await import(
196+
pathToFileURL(realpathSync(fileURLToPath(import.meta.resolve("c12")))).href
197+
);
193198
const result = await c12.loadConfig({
194199
name: "prisma",
195200
configFile: path,

packages/cli-engine/tests/conformance.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ describe("conformance: import purity", () => {
3131
label: "@prisma/cli-engine",
3232
output,
3333
manifest,
34+
// c12 is reached via import.meta.resolve plus a realpath'd
35+
// dynamic import (see config-loader.ts), which the lexer
36+
// rightly does not count as an import of the bare specifier.
37+
allowedUnimported: ["c12"],
3438
// Anti-vacuity: a run that swept the wrong directory would
3539
// otherwise report a clean sweep of nothing.
3640
requiredSpecifiers: ["@stricli/core"],

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
},
5050
"dependencies": {
5151
"@manypkg/tools": "^2.1.2",
52-
"@prisma/cli-engine": "workspace:0.2.0",
52+
"@prisma/cli-engine": "workspace:0.2.1",
5353
"@prisma/composer-cli": "0.12.0",
5454
"@prisma/compute-sdk": "0.39.0",
5555
"@prisma/credentials-store": "^7.8.0",

packages/cli/scripts/conformance.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ async function importPurity(): Promise<readonly Finding[]> {
7171
label: "@prisma/cli-engine",
7272
output: await sweepBuiltOutput(join(ENGINE_DIR, "dist")),
7373
manifest: await manifest(ENGINE_DIR),
74+
// c12 is reached via import.meta.resolve plus a realpath'd dynamic
75+
// import (see config-loader.ts), which the lexer rightly does not
76+
// count as an import of the bare specifier.
77+
allowedUnimported: ["c12"],
7478
requiredSpecifiers: ["@stricli/core"],
7579
});
7680
return [...shell, ...unscoped, ...engine];
@@ -98,18 +102,41 @@ async function tarball(): Promise<readonly Finding[]> {
98102
{
99103
name: "@prisma/cli-engine",
100104
dir: ENGINE_DIR,
105+
// Same excuse as check 1: c12 arrives via import.meta.resolve.
106+
allowedUnimported: ["c12"],
101107
},
102108
],
103109
shellPackage: "@prisma/cli",
104110
enginePackage: "@prisma/cli-engine",
105111
familyPackages: ["@prisma/composer-cli", "@prisma/orm-toolchain"],
106-
// No exceptions. Both families declare @prisma/cli-engine as an
107-
// exact peer at the version this repo ships, so one engine
108-
// resolves in an install — what ADR 0004 asks for. An entry here
109-
// exists only while an engine version transition is in flight
110-
// (the engine must publish before a family can peer it), and the
111-
// release PR that pins the families' new versions removes it.
112-
exceptions: [],
112+
// The empty list is the goal state: both families peering the
113+
// exact engine version this repo ships, one engine per install
114+
// (ADR 0004). The two entries below are an engine version
115+
// transition in flight — a family cannot peer an engine version
116+
// that is not on the registry, so the engine publishes first and
117+
// the mismatch is real until both families release against it.
118+
// The entries expire with the versions they name, and the PR
119+
// that pins the families' 0.2.1 releases removes them; while
120+
// they stand, a release could ship the two-engine install they
121+
// describe, which is why they must not outlive the transition.
122+
exceptions: [
123+
{
124+
familyPackage: "@prisma/composer-cli",
125+
familyPin: "0.2.0",
126+
shellPin: "0.2.1",
127+
reason: "engine 0.2.1 must publish before composer-cli can peer it",
128+
removeWhen:
129+
"composer-cli releases peering 0.2.1 and the follow-up bump PR pins that release",
130+
},
131+
{
132+
familyPackage: "@prisma/orm-toolchain",
133+
familyPin: "0.2.0",
134+
shellPin: "0.2.1",
135+
reason: "engine 0.2.1 must publish before orm-toolchain can peer it",
136+
removeWhen:
137+
"orm-toolchain releases peering 0.2.1 and the follow-up bump PR pins that release",
138+
},
139+
],
113140
channel: CHANNEL,
114141
sandboxDir: join(WORK_DIR, "sandbox"),
115142
},

packages/prisma/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
},
5050
"dependencies": {
5151
"@manypkg/tools": "^2.1.2",
52-
"@prisma/cli-engine": "workspace:0.2.0",
52+
"@prisma/cli-engine": "workspace:0.2.1",
5353
"@prisma/composer-cli": "0.12.0",
5454
"@prisma/compute-sdk": "0.39.0",
5555
"@prisma/credentials-store": "^7.8.0",

pnpm-lock.yaml

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)