From 493efcc3f0a7648a9998183de19ff23d1d651c0e Mon Sep 17 00:00:00 2001 From: willbot Date: Fri, 9 Oct 2026 09:29:03 +0200 Subject: [PATCH 1/7] fix: accept npm 10, the npm that ships with Node.js 22 create-prisma supports what Prisma ORM supports: Node.js 22.18+ or 24.11+ with the npm those releases ship. It refused npm older than 11.6.0 because npm 10 used to crash with "Cannot read properties of null (reading 'edgesOut')" while installing Composer's dependency tree. npm 10.9.3 on Node.js 22.18.0 now installs every generated project, so drop the npm minimum. The missing-npm check, the Yarn minimum and the Node.js 22.18 check stay. Signed-off-by: willbot Signed-off-by: Will Madden --- README.md | 5 +---- src/utils/package-manager.ts | 6 ------ tests/install.test.ts | 19 ++----------------- 3 files changed, 3 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 39f9afd..e435530 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,7 @@ PostgreSQL apps with Composer include `@prisma/dev` as a project devDependency f local Postgres. In a monorepo it belongs at the root, where Composer runs. Prisma 8 does not include this runtime itself. MongoDB and Deno-only apps do not need it. -For npm, use 11.6.0 or newer, which includes the [upstream resolver fix](https://github.com/npm/cli/pull/8448). -Older npm releases can crash resolving the dependency tree (`Cannot read properties of null (reading 'edgesOut')`). Run -`npm install --global npm@11` to update. create-prisma checks the selected package manager -before writing project files and never upgrades your package manager automatically. Yarn 1 is not supported. +The npm that ships with a supported Node.js release works, including npm 10 on Node.js 22. create-prisma checks the selected package manager before writing project files and never upgrades your package manager automatically. Yarn 1 is not supported. The deployment prompt is: diff --git a/src/utils/package-manager.ts b/src/utils/package-manager.ts index 784b7ce..3506db1 100644 --- a/src/utils/package-manager.ts +++ b/src/utils/package-manager.ts @@ -45,12 +45,6 @@ const packageManagerChecks: Record< name: "npm", versionArgs: ["--version"], install: "Install Node.js from https://nodejs.org to get npm", - // https://github.com/npm/cli/pull/8448 shipped in npm 11.6.0. - minimum: { - version: [11, 6, 0], - guidance: - "Older npm releases can crash while resolving Prisma dependencies. Run npm install --global npm@11, then retry create-prisma.", - }, }, pnpm: { name: "pnpm", diff --git a/tests/install.test.ts b/tests/install.test.ts index c74f8d5..12d0c10 100644 --- a/tests/install.test.ts +++ b/tests/install.test.ts @@ -180,30 +180,15 @@ describe("writePrismaDependencies", () => { }); describe("Composer package-manager commands", () => { - test("checks the selected npm binary against the supported resolver version", async () => { + test("accepts every npm release that reports a version, including npm 10 on Node.js 22", async () => { const invalidVersions = ["", "11.6.", "11..0", "11.6.1e3", "11.0x6.0", "11.06.0"]; - for (const version of [ - "10.9.7", - "11.5.1", - "11.5.2", - "11.6.0", - "11.6.1", - "11.6.2", - "11.19.0", - "12.0.2", - ...invalidVersions, - ]) { + for (const version of ["10.9.3", "10.9.7", "11.5.1", "11.6.0", "12.0.2", ...invalidVersions]) { const { result } = verifyPackageManager("npm", `${version}\n`); if (invalidVersions.includes(version)) { await expect(result).rejects.toMatchObject({ reason: "package_manager_check_failed", message: `Could not determine the installed npm version: ${version}`, }); - } else if (["10.9.7", "11.5.1", "11.5.2"].includes(version)) { - await expect(result).rejects.toMatchObject({ - reason: "unsupported_package_manager_version", - message: expect.stringContaining("npm install --global npm@11"), - }); } else { await expect(result).resolves.toBeUndefined(); } From 793edfedfe583352f9e27e4ac2fde4f1212d19cb Mon Sep 17 00:00:00 2001 From: willbot Date: Fri, 9 Oct 2026 09:44:51 +0200 Subject: [PATCH 2/7] ci: run the packed CLI smoke test with Node.js 22.18's own npm Both publish jobs installed npm 11.6.0 before the smoke test, so CI never ran create-prisma or a generated project's npm install with npm 10. Install npm 11.6.0 just before `npm publish`, where trusted publishing needs it. The smoke test now runs on Node.js 22.18.0 with npm 10.9.3 and reports the npm version it used. Signed-off-by: willbot Signed-off-by: Will Madden --- .github/workflows/publish.yml | 30 +++++++++++++++--------------- scripts/smoke-node.mjs | 3 ++- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e1ac208..8c6303a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -104,12 +104,6 @@ jobs: echo "dist_tag=$DIST_TAG" >> "$GITHUB_OUTPUT" echo "publish_next=$PUBLISH_NEXT" >> "$GITHUB_OUTPUT" - - name: Ensure npm version for trusted publishing - run: | - npm install -g npm@11.6.0 - echo "Node: $(node --version)" - echo "npm: $(npm --version)" - - name: Setup Bun uses: oven-sh/setup-bun@v2 with: @@ -148,9 +142,15 @@ jobs: CREATE_PRISMA_TELEMETRY_API_KEY: ${{ steps.preview.outputs.publish_next == 'true' && secrets.CREATE_PRISMA_TELEMETRY_API_KEY || '' }} CREATE_PRISMA_TELEMETRY_HOST: ${{ steps.preview.outputs.publish_next == 'true' && vars.CREATE_PRISMA_TELEMETRY_HOST || '' }} - - name: Test packed CLI on the minimum Node version + - name: Test packed CLI on the minimum Node version and its bundled npm run: node scripts/smoke-node.mjs + - name: Ensure npm version for trusted publishing + run: | + npm install -g npm@11.6.0 + echo "Node: $(node --version)" + echo "npm: $(npm --version)" + - name: Publish to npm run: npm publish --access public --tag "${{ steps.preview.outputs.dist_tag }}" @@ -288,13 +288,6 @@ jobs: echo "exists=false" >> "$GITHUB_OUTPUT" fi - - name: Ensure npm version for trusted publishing - if: steps.check-version.outputs.exists == 'false' - run: | - npm install -g npm@11.6.0 - echo "Node: $(node --version)" - echo "npm: $(npm --version)" - - name: Setup Bun if: steps.check-version.outputs.exists == 'false' uses: oven-sh/setup-bun@v2 @@ -325,10 +318,17 @@ jobs: if: steps.check-version.outputs.exists == 'false' run: bun run build - - name: Test packed CLI on the minimum Node version + - name: Test packed CLI on the minimum Node version and its bundled npm if: steps.check-version.outputs.exists == 'false' run: node scripts/smoke-node.mjs + - name: Ensure npm version for trusted publishing + if: steps.check-version.outputs.exists == 'false' + run: | + npm install -g npm@11.6.0 + echo "Node: $(node --version)" + echo "npm: $(npm --version)" + - name: Publish to npm if: steps.check-version.outputs.exists == 'false' run: npm publish --access public diff --git a/scripts/smoke-node.mjs b/scripts/smoke-node.mjs index 05a0171..c9d8104 100644 --- a/scripts/smoke-node.mjs +++ b/scripts/smoke-node.mjs @@ -62,8 +62,9 @@ try { const options = { cwd: path.join(directory, "app"), env, stdout: "inherit", stderr: "inherit" }; await execa("npm", ["run", "build"], options); await execa("npm", ["exec", "--", "tsc", "--noEmit"], options); + const { stdout: npmVersion } = await execa("npm", ["--version"]); console.log( - `Packed CLI engine check, scaffold, build and typecheck passed on Node ${process.versions.node}.`, + `Packed CLI engine check, scaffold, npm install, build and typecheck passed on Node ${process.versions.node} with npm ${npmVersion}.`, ); } finally { await rm(directory, { recursive: true, force: true }); From 8f8c9c598d54869d4282e6415412af278d4727ab Mon Sep 17 00:00:00 2001 From: willbot Date: Fri, 9 Oct 2026 09:44:52 +0200 Subject: [PATCH 3/7] fix: declare an npm in packageManager that runs on Node.js 22.18 Generated projects declared `packageManager: "npm@12.2.0"`, but npm 12.2.0 needs Node.js ^22.22.2, and the projects support ^22.18.0. Write the npm version create-prisma detected when it is npm 10 or 11, which run on every supported Node.js release. Otherwise write npm 10.9.3, the npm Node.js 22.18.0 ships. The pre-scaffold probe uses the same fallback. Split the npm version test into its accept and reject cases. Signed-off-by: willbot Signed-off-by: Will Madden --- src/commands/create.ts | 12 ++-- src/templates/render-create-template.ts | 9 +++ src/templates/shared.ts | 11 +--- src/utils/package-manager.ts | 20 +++++- templates/create/astro/package.json.hbs | 4 +- templates/create/elysia/package.json.hbs | 4 +- templates/create/hono/package.json.hbs | 4 +- templates/create/minimal/package.json.hbs | 4 +- templates/create/nest/package.json.hbs | 4 +- templates/create/next/package.json.hbs | 4 +- templates/create/nuxt/package.json.hbs | 4 +- templates/create/svelte/package.json.hbs | 4 +- .../create/tanstack-start/package.json.hbs | 4 +- templates/create/turborepo/package.json.hbs | 4 +- tests/install.test.ts | 61 ++++++++++++++----- 15 files changed, 102 insertions(+), 51 deletions(-) diff --git a/src/commands/create.ts b/src/commands/create.ts index ce36ec9..00f10d2 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -24,7 +24,7 @@ import { getCreatePrismaIntro } from "../ui/branding"; import { resolveExecutionSettings } from "../ui/output"; import { getErrorMessage } from "../utils/errors"; import { getUnsupportedNodeMessage, supportsPrisma } from "../utils/node-version"; -import { verifyPackageManagerEffect } from "../utils/package-manager"; +import { type PackageManagerVersion, verifyPackageManagerEffect } from "../utils/package-manager"; import { atCreateStage } from "../workflow/failure"; import { collectCreateContext, @@ -75,7 +75,10 @@ const initializeExistingApp = Effect.fn("Create.initExisting")(function* ( } satisfies CreateCommandResult; }); -const executeCreateContext = Effect.fn("Create.execute")(function* (context: CreatePromptContext) { +const executeCreateContext = Effect.fn("Create.execute")(function* ( + context: CreatePromptContext, + packageManagerVersion: PackageManagerVersion, +) { const output = context.prismaSetupContext.output; const createSpinner = context.prismaSetupContext.verbose ? undefined : spinner({ output }); yield* Effect.sync(() => { @@ -93,6 +96,7 @@ const executeCreateContext = Effect.fn("Create.execute")(function* (context: Cre provider: context.prismaSetupContext.databaseProvider, authoring: context.prismaSetupContext.authoring, packageManager: context.prismaSetupContext.packageManager, + packageManagerVersion, skillAgents: context.prismaSetupContext.skillAgents, }).pipe( Effect.andThen( @@ -198,7 +202,7 @@ const createProjectEffect = Effect.fn("Create.project")(function* ( "unexpected_error", ); yield* Ref.set(contextRef, Option.some(context)); - yield* atCreateStage( + const packageManagerVersion = yield* atCreateStage( context.operation === "orm_init" ? verifyPackageManagerEffect(context.packageManager, context.targetDirectory) : verifyPackageManagerEffect(context.prismaSetupContext.packageManager), @@ -211,7 +215,7 @@ const createProjectEffect = Effect.fn("Create.project")(function* ( result: context.operation === "orm_init" ? yield* initializeExistingApp(context, input) - : yield* executeCreateContext(context), + : yield* executeCreateContext(context, packageManagerVersion), }; }); diff --git a/src/templates/render-create-template.ts b/src/templates/render-create-template.ts index 587c806..2268120 100644 --- a/src/templates/render-create-template.ts +++ b/src/templates/render-create-template.ts @@ -10,6 +10,10 @@ import { type DatabaseProvider, type PackageManager, } from "../types"; +import { + getPackageManagerManifestValue, + type PackageManagerVersion, +} from "../utils/package-manager"; import { renderTemplateTreeEffect, resolveTemplatesDirEffect } from "./shared"; const DEFAULT_PRISMA_SOURCE_DIR = "src/prisma"; @@ -25,6 +29,7 @@ type CreateTemplateContext = { provider: DatabaseProvider; authoring: AuthoringStyle; packageManager?: PackageManager; + packageManagerManifest: string | null; skillAgents: readonly AgentSkillTarget[]; tsdownEntry: string | null; }; @@ -36,6 +41,8 @@ export type ScaffoldCreateTemplateOptions = { provider: DatabaseProvider; authoring: AuthoringStyle; packageManager?: PackageManager; + /** The version create-prisma detected; decides the npm written to `packageManager`. */ + packageManagerVersion?: PackageManagerVersion; /** Agents whose skill files the project gets; defaults to all of them. */ skillAgents?: readonly AgentSkillTarget[]; }; @@ -55,6 +62,8 @@ function createTemplateContext(options: ScaffoldCreateTemplateOptions): CreateTe provider: options.provider, authoring: options.authoring, packageManager: options.packageManager, + packageManagerManifest: + getPackageManagerManifestValue(options.packageManager, options.packageManagerVersion) ?? null, skillAgents: options.skillAgents ?? agentSkillTargets, tsdownEntry: tsdownEntries[options.template] ?? null, }; diff --git a/src/templates/shared.ts b/src/templates/shared.ts index 6829d98..291dd77 100644 --- a/src/templates/shared.ts +++ b/src/templates/shared.ts @@ -5,11 +5,7 @@ import { fileURLToPath } from "node:url"; import { applicationRuntime } from "../runtime"; import type { PackageManager } from "../types"; -import { - getPackageManagerManifestValue, - getRuntimeScriptCommand, - getRunScriptCommand, -} from "../utils/package-manager"; +import { getRuntimeScriptCommand, getRunScriptCommand } from "../utils/package-manager"; Handlebars.registerHelper("eq", (left: unknown, right: unknown) => left === right); Handlebars.registerHelper( @@ -17,11 +13,6 @@ Handlebars.registerHelper( (packageManager: PackageManager | undefined, scriptName: string) => packageManager ? getRunScriptCommand(packageManager, scriptName) : "", ); -Handlebars.registerHelper( - "packageManagerManifestValue", - (packageManager: PackageManager | undefined) => - getPackageManagerManifestValue(packageManager) ?? "", -); Handlebars.registerHelper( "runtimeScript", ( diff --git a/src/utils/package-manager.ts b/src/utils/package-manager.ts index 3506db1..9abd512 100644 --- a/src/utils/package-manager.ts +++ b/src/utils/package-manager.ts @@ -23,14 +23,18 @@ type RuntimeScriptOptions = { // Scaffolding must be able to resolve the freshly published, explicitly pinned Prisma release. const DENO_ALLOW_FRESH_DEPENDENCIES = "--minimum-dependency-age=0"; +// Node.js 22.18.0 ships npm 10.9.3, so it runs on every Node.js release generated projects support. +const NPM_FOR_EVERY_SUPPORTED_NODE = "10.9.3"; +const npmMajorsForEverySupportedNode: ReadonlySet = new Set([10, 11]); + const packageManagerManifestValues = { - npm: "npm@12.2.0", + npm: `npm@${NPM_FOR_EVERY_SUPPORTED_NODE}`, pnpm: "pnpm@11.28.5", yarn: "yarn@4.18.1", bun: "bun@1.4.2", } as const; -type PackageManagerVersion = readonly [major: number, minor: number, patch: number]; +export type PackageManagerVersion = readonly [major: number, minor: number, patch: number]; const packageManagerChecks: Record< PackageManager, @@ -135,6 +139,7 @@ const probePackageManagerEffect = Effect.fn("PackageManager.probe")(function* ( message: `${name} ${version.join(".")} is unsupported. Required: ${name} ${minimum.version.join(".")} or newer. ${minimum.guidance}`, }); } + return version; }); // The reported version depends on the directory's "packageManager", so probe in a temporary @@ -166,7 +171,7 @@ export const verifyPackageManagerEffect = Effect.fn("PackageManager.verify")(fun }), ), ); - yield* probePackageManagerEffect(packageManager, probeDir); + return yield* probePackageManagerEffect(packageManager, probeDir); }, Effect.scoped); function parseUserAgent(userAgent: string | undefined): PackageManager | null { @@ -290,6 +295,7 @@ export function detectPackageManager(projectDir = process.cwd()): Promise { }); describe("Composer package-manager commands", () => { - test("accepts every npm release that reports a version, including npm 10 on Node.js 22", async () => { - const invalidVersions = ["", "11.6.", "11..0", "11.6.1e3", "11.0x6.0", "11.06.0"]; - for (const version of ["10.9.3", "10.9.7", "11.5.1", "11.6.0", "12.0.2", ...invalidVersions]) { - const { result } = verifyPackageManager("npm", `${version}\n`); - if (invalidVersions.includes(version)) { - await expect(result).rejects.toMatchObject({ - reason: "package_manager_check_failed", - message: `Could not determine the installed npm version: ${version}`, - }); - } else { - await expect(result).resolves.toBeUndefined(); - } + test("accepts any npm version and returns it", async () => { + for (const [stdout, version] of [ + ["10.9.3\n", [10, 9, 3]], + ["11.6.0\n", [11, 6, 0]], + ["12.2.0\n", [12, 2, 0]], + ] as const) { + await expect(verifyPackageManager("npm", stdout).result).resolves.toEqual(version); + } + }); + + test("rejects npm output that is not a version", async () => { + for (const output of ["", "11.6.", "11..0", "11.6.1e3", "11.0x6.0", "11.06.0"]) { + await expect(verifyPackageManager("npm", `${output}\n`).result).rejects.toMatchObject({ + reason: "package_manager_check_failed", + message: `Could not determine the installed npm version: ${output}`, + }); } }); test("probes the selected package manager once in a generated project manifest", async () => { for (const [manager, args, stdout, packageManager] of [ - ["npm", ["--version"], "12.2.0\n", "npm@12.2.0"], + ["npm", ["--version"], "11.6.0\n", "npm@10.9.3"], ["pnpm", ["--version"], "11.28.5\n", "pnpm@11.28.5"], ["yarn", ["--version"], "4.18.1\n", "yarn@4.18.1"], ["bun", ["--version"], "1.4.2\n", "bun@1.4.2"], ["deno", ["-V"], "deno 2.9.4\n", undefined], ] as const) { const { result, probes } = verifyPackageManager(manager, stdout); - await expect(result).resolves.toBeUndefined(); + await expect(result).resolves.toBeDefined(); expect(probes).toHaveLength(1); expect(probes[0]!.spec).toMatchObject({ command: manager, args: [...args] }); expect(probes[0]!.spec.cwd).not.toBe(process.cwd()); @@ -308,6 +312,32 @@ describe("generated templates", () => { } }); + test("declares the detected npm when it runs on every supported Node.js release, else npm 10.9.3", async () => { + for (const [packageManagerVersion, expected] of [ + [[10, 9, 7], "npm@10.9.7"], + [[11, 6, 0], "npm@11.6.0"], + [[12, 2, 0], "npm@10.9.3"], + [[9, 9, 4], "npm@10.9.3"], + [undefined, "npm@10.9.3"], + ] as const) { + const projectDir = await mkdtemp(path.join(tmpdir(), "create-prisma-npm-manifest-")); + try { + await scaffoldCreateTemplate({ + projectDir, + projectName: "npm-app", + template: "turborepo", + provider: "postgres", + authoring: "psl", + packageManager: "npm", + packageManagerVersion, + }); + expect((await readPackageJson(projectDir)).packageManager).toBe(expected); + } finally { + await rm(projectDir, { recursive: true, force: true }); + } + } + }); + test("renders Composer into every supported combination", async () => { for (const template of createTemplates) { for (const provider of databaseProviders) { @@ -342,6 +372,9 @@ describe("generated templates", () => { if (packageManager === "bun") { expect(packageJson.packageManager).toBe("bun@1.4.2"); } + if (packageManager === "npm") { + expect(packageJson.packageManager).toBe("npm@10.9.3"); + } const prismaSourceRelative = template === "turborepo" ? "packages/database/src" : "src/prisma"; const dbSource = await readFile( From 6cad106cd550291440869df6e11fc5224d9e4761 Mon Sep 17 00:00:00 2001 From: willbot Date: Fri, 9 Oct 2026 09:52:05 +0200 Subject: [PATCH 4/7] test: check the detected npm reaches the generated packageManager A unit test runs the create command with a stubbed npm 11.6.2 and checks the generated package.json declares it. The CI smoke test checks the generated packageManager matches the npm it ran. The probe test checks the exact parsed version. The npm fallback and allowed majors now sit next to the Node.js version they assume, and a test fails if the minimum Node.js version changes without them. Signed-off-by: willbot Signed-off-by: Will Madden --- scripts/smoke-node.mjs | 13 +++++++++--- src/utils/node-version.ts | 2 +- src/utils/package-manager.ts | 14 +++++++----- tests/install.test.ts | 14 ++++++------ tests/node-version.test.ts | 11 +++++++++- tests/setup-prisma.test.ts | 41 ++++++++++++++++++++++++++++++++++++ 6 files changed, 78 insertions(+), 17 deletions(-) diff --git a/scripts/smoke-node.mjs b/scripts/smoke-node.mjs index c9d8104..80822d2 100644 --- a/scripts/smoke-node.mjs +++ b/scripts/smoke-node.mjs @@ -1,5 +1,5 @@ import { execa } from "execa"; -import { mkdtemp, rm } from "node:fs/promises"; +import { mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; @@ -59,10 +59,17 @@ try { { cwd: directory, env }, ); if (!JSON.parse(created.stdout).ok) throw new Error("Packed CLI did not scaffold successfully"); - const options = { cwd: path.join(directory, "app"), env, stdout: "inherit", stderr: "inherit" }; + const appDirectory = path.join(directory, "app"); + const { stdout: npmVersion } = await execa("npm", ["--version"], { cwd: appDirectory }); + const { packageManager } = JSON.parse( + await readFile(path.join(appDirectory, "package.json"), "utf8"), + ); + if (packageManager !== `npm@${npmVersion}`) { + throw new Error(`Generated packageManager is ${packageManager}, expected npm@${npmVersion}`); + } + const options = { cwd: appDirectory, env, stdout: "inherit", stderr: "inherit" }; await execa("npm", ["run", "build"], options); await execa("npm", ["exec", "--", "tsc", "--noEmit"], options); - const { stdout: npmVersion } = await execa("npm", ["--version"]); console.log( `Packed CLI engine check, scaffold, npm install, build and typecheck passed on Node ${process.versions.node} with npm ${npmVersion}.`, ); diff --git a/src/utils/node-version.ts b/src/utils/node-version.ts index 9515664..ed4b4d1 100644 --- a/src/utils/node-version.ts +++ b/src/utils/node-version.ts @@ -1,4 +1,4 @@ -const MINIMUM_NODE_VERSION = [22, 18, 0] as const; +export const MINIMUM_NODE_VERSION = [22, 18, 0] as const; function parseVersion(version: string): [number, number, number] { const [major = "0", minor = "0", patch = "0"] = version.replace(/^v/, "").split("."); diff --git a/src/utils/package-manager.ts b/src/utils/package-manager.ts index 9abd512..3293bdd 100644 --- a/src/utils/package-manager.ts +++ b/src/utils/package-manager.ts @@ -23,12 +23,16 @@ type RuntimeScriptOptions = { // Scaffolding must be able to resolve the freshly published, explicitly pinned Prisma release. const DENO_ALLOW_FRESH_DEPENDENCIES = "--minimum-dependency-age=0"; -// Node.js 22.18.0 ships npm 10.9.3, so it runs on every Node.js release generated projects support. -const NPM_FOR_EVERY_SUPPORTED_NODE = "10.9.3"; -const npmMajorsForEverySupportedNode: ReadonlySet = new Set([10, 11]); +// Revisit with MINIMUM_NODE_VERSION in node-version.ts: the npm that Node.js release ships, +// and the npm majors that run on it and on every later supported release. +export const npmForMinimumNode = { + node: [22, 18, 0], + fallback: "10.9.3", + majors: new Set([10, 11]), +} as const; const packageManagerManifestValues = { - npm: `npm@${NPM_FOR_EVERY_SUPPORTED_NODE}`, + npm: `npm@${npmForMinimumNode.fallback}`, pnpm: "pnpm@11.28.5", yarn: "yarn@4.18.1", bun: "bun@1.4.2", @@ -308,7 +312,7 @@ export function getPackageManagerManifestValue( if ( packageManager === "npm" && detectedVersion && - npmMajorsForEverySupportedNode.has(detectedVersion[0]) + npmForMinimumNode.majors.has(detectedVersion[0]) ) { return `npm@${detectedVersion.join(".")}`; } diff --git a/tests/install.test.ts b/tests/install.test.ts index 70c29ea..d78cb71 100644 --- a/tests/install.test.ts +++ b/tests/install.test.ts @@ -200,15 +200,15 @@ describe("Composer package-manager commands", () => { }); test("probes the selected package manager once in a generated project manifest", async () => { - for (const [manager, args, stdout, packageManager] of [ - ["npm", ["--version"], "11.6.0\n", "npm@10.9.3"], - ["pnpm", ["--version"], "11.28.5\n", "pnpm@11.28.5"], - ["yarn", ["--version"], "4.18.1\n", "yarn@4.18.1"], - ["bun", ["--version"], "1.4.2\n", "bun@1.4.2"], - ["deno", ["-V"], "deno 2.9.4\n", undefined], + for (const [manager, args, stdout, packageManager, version] of [ + ["npm", ["--version"], "11.6.0\n", "npm@10.9.3", [11, 6, 0]], + ["pnpm", ["--version"], "11.28.5\n", "pnpm@11.28.5", [11, 28, 5]], + ["yarn", ["--version"], "4.18.1\n", "yarn@4.18.1", [4, 18, 1]], + ["bun", ["--version"], "1.4.2\n", "bun@1.4.2", [1, 4, 2]], + ["deno", ["-V"], "deno 2.9.4\n", undefined, [2, 9, 4]], ] as const) { const { result, probes } = verifyPackageManager(manager, stdout); - await expect(result).resolves.toBeDefined(); + await expect(result).resolves.toEqual(version); expect(probes).toHaveLength(1); expect(probes[0]!.spec).toMatchObject({ command: manager, args: [...args] }); expect(probes[0]!.spec.cwd).not.toBe(process.cwd()); diff --git a/tests/node-version.test.ts b/tests/node-version.test.ts index 93a1a54..3051048 100644 --- a/tests/node-version.test.ts +++ b/tests/node-version.test.ts @@ -1,6 +1,11 @@ import { describe, expect, test } from "bun:test"; -import { getUnsupportedNodeMessage, supportsPrisma } from "../src/utils/node-version"; +import { + getUnsupportedNodeMessage, + MINIMUM_NODE_VERSION, + supportsPrisma, +} from "../src/utils/node-version"; +import { npmForMinimumNode } from "../src/utils/package-manager"; describe("Prisma 8 Node compatibility", () => { test("requires Node 22.18 or newer", () => { @@ -13,4 +18,8 @@ describe("Prisma 8 Node compatibility", () => { test("returns an actionable message", () => { expect(getUnsupportedNodeMessage("20.19.0")).toContain("Required: Node.js 22.18 or newer."); }); + + test("pairs the npm fallback for generated projects with the minimum Node.js version", () => { + expect(npmForMinimumNode.node).toEqual(MINIMUM_NODE_VERSION); + }); }); diff --git a/tests/setup-prisma.test.ts b/tests/setup-prisma.test.ts index ccded9d..c6e00d0 100644 --- a/tests/setup-prisma.test.ts +++ b/tests/setup-prisma.test.ts @@ -75,6 +75,47 @@ test("writes pnpm build permissions before the first dependency installation", a }); }); +test("declares the npm version it detected in the generated package.json", async () => { + await withTempProject(async (projectDir) => { + let installChecked = false; + const result = await applicationRuntime.runPromise( + runCreateCommandEffect({ + name: path.relative(process.cwd(), path.join(projectDir, "app")), + template: "minimal", + packageManager: "npm", + json: true, + deploy: false, + }).pipe( + Effect.provideService(CommandRunner, { + run: () => Effect.die("Unexpected unchecked command"), + runChecked: (spec) => + Effect.gen(function* () { + if (spec.args[0] === "--version") { + return { exitCode: 0, stdout: "11.6.2\n", stderr: "" }; + } + expect(spec.args).toEqual(["install"]); + const fs = yield* FileSystem.FileSystem; + const packageJson = yield* fs + .readFileString(path.join(spec.cwd, "package.json")) + .pipe(Effect.orDie); + expect(JSON.parse(packageJson).packageManager).toBe("npm@11.6.2"); + installChecked = true; + return yield* new CommandExecutionError({ + command: spec.command, + args: [...spec.args], + exitCode: 1, + stdout: "", + stderr: "Stopped before installing test dependencies", + }); + }), + }), + ), + ); + expect(installChecked).toBe(true); + expect(result).toMatchObject({ ok: false, error: { stage: "install_dependencies" } }); + }); +}); + test("rejects a missing package manager before writing project files", async () => { await withTempProject(async (projectDir) => { const targetDirectory = path.join(projectDir, "app"); From 13762697fde944653e3fd654bd645bd9c2602141 Mon Sep 17 00:00:00 2001 From: willbot Date: Fri, 9 Oct 2026 10:04:48 +0200 Subject: [PATCH 5/7] fix: support exactly the Node.js range Prisma ORM supports create-prisma accepted any Node.js from 22.18, including Node.js 23 and 24.0 to 24.10, which Prisma ORM does not support. The startup check now accepts `^22.18.0 || ^24.11.0 || >=26.0.0`, names that range in its refusal message, and `engines.node` declares it. The check applies only on Node.js: Bun and Deno report their own Node.js compatibility version, which says nothing about the user's Node.js. The README states the supported Node.js versions, package managers and the Nuxt exception in one place. Signed-off-by: willbot Signed-off-by: Will Madden --- README.md | 11 ++++++--- package.json | 2 +- src/commands/create.ts | 4 ++-- src/utils/node-version.ts | 37 +++++++++++++++++++++------- tests/node-version.test.ts | 49 ++++++++++++++++++++++++++++++++------ 5 files changed, 82 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index e435530..f768ee7 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,13 @@ Create a Prisma 8 app with Prisma Composer built in. -Requires Node.js 22.18 or newer, matching the Prisma CLI. -Generated apps must also meet their selected framework's Node.js requirements. +## Supported versions + +create-prisma supports exactly what Prisma ORM supports: Node.js 22.18 or newer on the 22 line, 24.11 or newer on the 24 line, or 26 and newer (`^22.18.0 || ^24.11.0 || >=26.0.0`). Node.js 23 and 24.0 to 24.10 are not supported. create-prisma refuses unsupported Node.js versions before it writes any files, and the generated minimal, hono, elysia, nest and turborepo projects declare the same range in `engines.node`. + +Each supported Node.js release works with the npm it ships, including npm 10 on Node.js 22. pnpm, Yarn 4.10 or newer, Bun and Deno are also supported. Yarn 1 is not supported. When create-prisma runs on Bun or Deno instead of Node.js, it skips the Node.js check. + +The one exception is the `nuxt` template. Nuxt itself requires Node.js `^22.22.3 || ^24.15.0 || >=26.0.0`, so Nuxt projects declare that narrower range. ## Quick start @@ -22,7 +27,7 @@ PostgreSQL apps with Composer include `@prisma/dev` as a project devDependency f local Postgres. In a monorepo it belongs at the root, where Composer runs. Prisma 8 does not include this runtime itself. MongoDB and Deno-only apps do not need it. -The npm that ships with a supported Node.js release works, including npm 10 on Node.js 22. create-prisma checks the selected package manager before writing project files and never upgrades your package manager automatically. Yarn 1 is not supported. +create-prisma checks the selected package manager before writing project files and never upgrades your package manager automatically. The deployment prompt is: diff --git a/package.json b/package.json index eccb45b..a0980e3 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ }, "engines": { "bun": ">=1.3.0", - "node": ">=22.18.0" + "node": "^22.18.0 || ^24.11.0 || >=26.0.0" }, "packageManager": "bun@1.4.1" } diff --git a/src/commands/create.ts b/src/commands/create.ts index 00f10d2..6a6465a 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -23,7 +23,7 @@ import { decodeCreateCommandInput, type CreateCommandInput } from "../types"; import { getCreatePrismaIntro } from "../ui/branding"; import { resolveExecutionSettings } from "../ui/output"; import { getErrorMessage } from "../utils/errors"; -import { getUnsupportedNodeMessage, supportsPrisma } from "../utils/node-version"; +import { getUnsupportedNodeMessage, runsOnNode, supportsPrisma } from "../utils/node-version"; import { type PackageManagerVersion, verifyPackageManagerEffect } from "../utils/package-manager"; import { atCreateStage } from "../workflow/failure"; import { @@ -184,7 +184,7 @@ const createProjectEffect = Effect.fn("Create.project")(function* ( message: "--verbose cannot be used with --json because JSON mode is output-only.", }); } - if (!supportsPrisma()) { + if (runsOnNode() && !supportsPrisma()) { const message = getUnsupportedNodeMessage(); yield* Effect.sync(() => cancel(message, { output })); return yield* new CreateFailure({ diff --git a/src/utils/node-version.ts b/src/utils/node-version.ts index ed4b4d1..9378354 100644 --- a/src/utils/node-version.ts +++ b/src/utils/node-version.ts @@ -1,23 +1,44 @@ -export const MINIMUM_NODE_VERSION = [22, 18, 0] as const; +type Version = readonly [major: number, minor: number, patch: number]; -function parseVersion(version: string): [number, number, number] { +export const SUPPORTED_NODE_RANGE = "^22.18.0 || ^24.11.0 || >=26.0.0"; + +const minimumByMajor: ReadonlyMap = new Map([ + [22, [22, 18, 0]], + [24, [24, 11, 0]], +]); +const FIRST_MAJOR_WITH_EVERY_RELEASE_SUPPORTED = 26; + +export const MINIMUM_NODE_VERSION = minimumByMajor.get(22)!; + +function parseVersion(version: string): Version { const [major = "0", minor = "0", patch = "0"] = version.replace(/^v/, "").split("."); return [Number(major), Number(minor), Number.parseInt(patch, 10)]; } -export function supportsPrisma(nodeVersion = process.versions.node): boolean { - const current = parseVersion(nodeVersion); - for (let index = 0; index < MINIMUM_NODE_VERSION.length; index += 1) { - if (current[index]! > MINIMUM_NODE_VERSION[index]!) return true; - if (current[index]! < MINIMUM_NODE_VERSION[index]!) return false; +function isAtLeast(current: Version, minimum: Version): boolean { + for (const [index, part] of current.entries()) { + if (part !== minimum[index]) return part > minimum[index]!; } return true; } +export function runsOnNode( + versions: Partial> = process.versions, +): boolean { + return versions.bun === undefined && versions.deno === undefined; +} + +export function supportsPrisma(nodeVersion = process.versions.node): boolean { + const current = parseVersion(nodeVersion); + if (current[0] >= FIRST_MAJOR_WITH_EVERY_RELEASE_SUPPORTED) return true; + const minimum = minimumByMajor.get(current[0]); + return minimum !== undefined && isAtLeast(current, minimum); +} + export function getUnsupportedNodeMessage(nodeVersion = process.versions.node): string { return [ `Node.js ${nodeVersion} is unsupported by create-prisma@latest.`, - "Required: Node.js 22.18 or newer.", + "Required: Node.js 22.18 or newer on the 22 line, 24.11 or newer on the 24 line, or 26 and newer.", "Update Node.js and run the command again.", ].join("\n"); } diff --git a/tests/node-version.test.ts b/tests/node-version.test.ts index 3051048..52e5308 100644 --- a/tests/node-version.test.ts +++ b/tests/node-version.test.ts @@ -1,22 +1,57 @@ import { describe, expect, test } from "bun:test"; +import { readFileSync } from "node:fs"; +import path from "node:path"; import { getUnsupportedNodeMessage, MINIMUM_NODE_VERSION, + runsOnNode, + SUPPORTED_NODE_RANGE, supportsPrisma, } from "../src/utils/node-version"; import { npmForMinimumNode } from "../src/utils/package-manager"; +const repositoryRoot = path.join(import.meta.dir, ".."); + describe("Prisma 8 Node compatibility", () => { - test("requires Node 22.18 or newer", () => { - expect(supportsPrisma("22.17.9")).toBe(false); - expect(supportsPrisma("22.18.0")).toBe(true); - expect(supportsPrisma("22.20.0")).toBe(true); - expect(supportsPrisma("24.0.0")).toBe(true); + test("accepts exactly the Node.js versions Prisma ORM supports", () => { + for (const [version, supported] of [ + ["20.19.0", false], + ["22.17.9", false], + ["v22.17.9", false], + ["22.18.0", true], + ["v22.18.0", true], + ["22.22.3", true], + ["23.0.0", false], + ["23.11.0", false], + ["24.0.0", false], + ["24.10.9", false], + ["24.11.0", true], + ["24.15.0", true], + ["25.9.0", false], + ["26.0.0", true], + ["27.1.0", true], + ] as const) { + expect([version, supportsPrisma(version)]).toEqual([version, supported]); + } + }); + + test("applies the Node.js range only when running on Node.js, not Bun or Deno", () => { + expect(runsOnNode({ node: "22.18.0" })).toBe(true); + expect(runsOnNode({ node: "24.3.0", bun: "1.3.13" })).toBe(false); + expect(runsOnNode({ node: "24.2.0", deno: "2.9.4" })).toBe(false); + }); + + test("names the supported range in the refusal message", () => { + expect(getUnsupportedNodeMessage("24.10.0")).toContain( + "Required: Node.js 22.18 or newer on the 22 line, 24.11 or newer on the 24 line, or 26 and newer.", + ); }); - test("returns an actionable message", () => { - expect(getUnsupportedNodeMessage("20.19.0")).toContain("Required: Node.js 22.18 or newer."); + test("declares the supported range in package.json engines", () => { + const packageJson = JSON.parse(readFileSync(path.join(repositoryRoot, "package.json"), "utf8")); + expect(SUPPORTED_NODE_RANGE).toBe("^22.18.0 || ^24.11.0 || >=26.0.0"); + expect(packageJson.engines.node).toBe(SUPPORTED_NODE_RANGE); }); test("pairs the npm fallback for generated projects with the minimum Node.js version", () => { From 4cd0d3afb6cbdfdb4a803d1fc6deb580ff2e742d Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Fri, 9 Oct 2026 14:02:45 +0530 Subject: [PATCH 6/7] fix: preserve detected npm prerelease versions Signed-off-by: Aman Varshney --- src/utils/package-manager.ts | 21 +++++++++++++-------- tests/install.test.ts | 19 +++++++++++++------ tests/setup-prisma.test.ts | 4 ++-- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/utils/package-manager.ts b/src/utils/package-manager.ts index 3293bdd..ac9e3f2 100644 --- a/src/utils/package-manager.ts +++ b/src/utils/package-manager.ts @@ -38,7 +38,12 @@ const packageManagerManifestValues = { bun: "bun@1.4.2", } as const; -export type PackageManagerVersion = readonly [major: number, minor: number, patch: number]; +type VersionParts = readonly [major: number, minor: number, patch: number]; + +export type PackageManagerVersion = { + readonly version: string; + readonly parts: VersionParts; +}; const packageManagerChecks: Record< PackageManager, @@ -46,7 +51,7 @@ const packageManagerChecks: Record< name: string; versionArgs: string[]; install: string; - minimum?: { version: PackageManagerVersion; guidance: string }; + minimum?: { version: VersionParts; guidance: string }; } > = { npm: { @@ -94,10 +99,10 @@ function parsePackageManagerVersion( ); if (!match) return undefined; const parts = [Number(match[1]), Number(match[2]), Number(match[3])] as const; - return parts.every(Number.isSafeInteger) ? parts : undefined; + return parts.every(Number.isSafeInteger) ? { version: match[0], parts } : undefined; } -function isOlderVersion(version: PackageManagerVersion, minimum: PackageManagerVersion): boolean { +function isOlderVersion(version: VersionParts, minimum: VersionParts): boolean { for (const [index, part] of version.entries()) { if (part !== minimum[index]) return part < minimum[index]!; } @@ -136,11 +141,11 @@ const probePackageManagerEffect = Effect.fn("PackageManager.probe")(function* ( message: `Could not determine the installed ${name} version: ${output}`, }); } - if (minimum && isOlderVersion(version, minimum.version)) { + if (minimum && isOlderVersion(version.parts, minimum.version)) { return yield* new CreateFailure({ stage: "validate_input", reason: "unsupported_package_manager_version", - message: `${name} ${version.join(".")} is unsupported. Required: ${name} ${minimum.version.join(".")} or newer. ${minimum.guidance}`, + message: `${name} ${version.version} is unsupported. Required: ${name} ${minimum.version.join(".")} or newer. ${minimum.guidance}`, }); } return version; @@ -312,9 +317,9 @@ export function getPackageManagerManifestValue( if ( packageManager === "npm" && detectedVersion && - npmForMinimumNode.majors.has(detectedVersion[0]) + npmForMinimumNode.majors.has(detectedVersion.parts[0]) ) { - return `npm@${detectedVersion.join(".")}`; + return `npm@${detectedVersion.version}`; } return packageManagerManifestValues[packageManager]; diff --git a/tests/install.test.ts b/tests/install.test.ts index d78cb71..38b3e1f 100644 --- a/tests/install.test.ts +++ b/tests/install.test.ts @@ -185,8 +185,13 @@ describe("Composer package-manager commands", () => { ["10.9.3\n", [10, 9, 3]], ["11.6.0\n", [11, 6, 0]], ["12.2.0\n", [12, 2, 0]], + ["11.0.0-pre.1\n", [11, 0, 0]], + ["11.0.0-pre.1+build.7\n", [11, 0, 0]], ] as const) { - await expect(verifyPackageManager("npm", stdout).result).resolves.toEqual(version); + await expect(verifyPackageManager("npm", stdout).result).resolves.toEqual({ + version: stdout.trim(), + parts: version, + }); } }); @@ -208,7 +213,7 @@ describe("Composer package-manager commands", () => { ["deno", ["-V"], "deno 2.9.4\n", undefined, [2, 9, 4]], ] as const) { const { result, probes } = verifyPackageManager(manager, stdout); - await expect(result).resolves.toEqual(version); + await expect(result).resolves.toEqual({ version: version.join("."), parts: version }); expect(probes).toHaveLength(1); expect(probes[0]!.spec).toMatchObject({ command: manager, args: [...args] }); expect(probes[0]!.spec.cwd).not.toBe(process.cwd()); @@ -314,10 +319,12 @@ describe("generated templates", () => { test("declares the detected npm when it runs on every supported Node.js release, else npm 10.9.3", async () => { for (const [packageManagerVersion, expected] of [ - [[10, 9, 7], "npm@10.9.7"], - [[11, 6, 0], "npm@11.6.0"], - [[12, 2, 0], "npm@10.9.3"], - [[9, 9, 4], "npm@10.9.3"], + [{ version: "10.9.7", parts: [10, 9, 7] }, "npm@10.9.7"], + [{ version: "11.6.0", parts: [11, 6, 0] }, "npm@11.6.0"], + [{ version: "11.0.0-pre.1", parts: [11, 0, 0] }, "npm@11.0.0-pre.1"], + [{ version: "11.0.0-pre.1+build.7", parts: [11, 0, 0] }, "npm@11.0.0-pre.1+build.7"], + [{ version: "12.2.0", parts: [12, 2, 0] }, "npm@10.9.3"], + [{ version: "9.9.4", parts: [9, 9, 4] }, "npm@10.9.3"], [undefined, "npm@10.9.3"], ] as const) { const projectDir = await mkdtemp(path.join(tmpdir(), "create-prisma-npm-manifest-")); diff --git a/tests/setup-prisma.test.ts b/tests/setup-prisma.test.ts index c6e00d0..d604439 100644 --- a/tests/setup-prisma.test.ts +++ b/tests/setup-prisma.test.ts @@ -91,14 +91,14 @@ test("declares the npm version it detected in the generated package.json", async runChecked: (spec) => Effect.gen(function* () { if (spec.args[0] === "--version") { - return { exitCode: 0, stdout: "11.6.2\n", stderr: "" }; + return { exitCode: 0, stdout: "11.6.2-pre.1\n", stderr: "" }; } expect(spec.args).toEqual(["install"]); const fs = yield* FileSystem.FileSystem; const packageJson = yield* fs .readFileString(path.join(spec.cwd, "package.json")) .pipe(Effect.orDie); - expect(JSON.parse(packageJson).packageManager).toBe("npm@11.6.2"); + expect(JSON.parse(packageJson).packageManager).toBe("npm@11.6.2-pre.1"); installChecked = true; return yield* new CommandExecutionError({ command: spec.command, From acb0f34b875f7266c942299e00fef537d4da58fc Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Fri, 9 Oct 2026 14:02:45 +0530 Subject: [PATCH 7/7] chore(release): 0.14.2 Signed-off-by: Aman Varshney --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a0980e3..93f8b9a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "create-prisma", - "version": "0.14.1", + "version": "0.14.2", "private": false, "description": "Create Prisma 8 projects with first-party templates and great DX.", "homepage": "https://github.com/prisma/create-prisma",