Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 }}"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -22,10 +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.

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.
create-prisma checks the selected package manager before writing project files and never upgrades your package manager automatically.

The deployment prompt is:

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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"
}
14 changes: 11 additions & 3 deletions scripts/smoke-node.mjs
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -59,11 +59,19 @@ 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);
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 });
Expand Down
16 changes: 10 additions & 6 deletions src/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ 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 { verifyPackageManagerEffect } from "../utils/package-manager";
import { getUnsupportedNodeMessage, runsOnNode, supportsPrisma } from "../utils/node-version";
import { type PackageManagerVersion, verifyPackageManagerEffect } from "../utils/package-manager";
import { atCreateStage } from "../workflow/failure";
import {
collectCreateContext,
Expand Down Expand Up @@ -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(() => {
Expand All @@ -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(
Expand Down Expand Up @@ -180,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({
Expand All @@ -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),
Expand All @@ -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),
};
});

Expand Down
9 changes: 9 additions & 0 deletions src/templates/render-create-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -25,6 +29,7 @@ type CreateTemplateContext = {
provider: DatabaseProvider;
authoring: AuthoringStyle;
packageManager?: PackageManager;
packageManagerManifest: string | null;
skillAgents: readonly AgentSkillTarget[];
tsdownEntry: string | null;
};
Expand All @@ -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[];
};
Expand All @@ -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,
};
Expand Down
11 changes: 1 addition & 10 deletions src/templates/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,14 @@ 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(
"runScriptCommand",
(packageManager: PackageManager | undefined, scriptName: string) =>
packageManager ? getRunScriptCommand(packageManager, scriptName) : "",
);
Handlebars.registerHelper(
"packageManagerManifestValue",
(packageManager: PackageManager | undefined) =>
getPackageManagerManifestValue(packageManager) ?? "",
);
Handlebars.registerHelper(
"runtimeScript",
(
Expand Down
37 changes: 29 additions & 8 deletions src/utils/node-version.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,44 @@
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<number, Version> = 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<Record<"node" | "bun" | "deno", string>> = 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");
}
Loading
Loading