diff --git a/package.json b/package.json index 7124a07..38a1187 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "create-prisma", - "version": "0.14.6", + "version": "0.14.7", "private": false, "description": "Create Prisma 8 projects with first-party templates and great DX.", "homepage": "https://github.com/prisma/create-prisma", @@ -37,7 +37,7 @@ "dev": "tsdown --watch", "start": "bun run ./dist/cli.mjs", "test": "bun run test:unit && bun run test:e2e", - "test:unit": "bun test ./tests/create-context.test.ts ./tests/dependencies.test.ts ./tests/deploy-with-composer.test.ts ./tests/initialize-git.test.ts ./tests/install.test.ts ./tests/json-output.test.ts ./tests/node-version.test.ts ./tests/package-manager-error-code.test.ts ./tests/setup-prisma.test.ts ./tests/telemetry-client.test.ts ./tests/telemetry.test.ts", + "test:unit": "bun test ./tests/create-context.test.ts ./tests/dependencies.test.ts ./tests/deploy-with-composer.test.ts ./tests/initialize-git.test.ts ./tests/install.test.ts ./tests/json-output.test.ts ./tests/node-version.test.ts ./tests/package-manager-error-code.test.ts ./tests/pin-problems.test.ts ./tests/setup-prisma.test.ts ./tests/telemetry-client.test.ts ./tests/telemetry.test.ts", "test:e2e": "bun test --timeout 180000 ./tests/e2e/create-prisma.e2e.test.ts", "check": "bun run format:check && bun run lint", "lint": "oxlint . --deny-warnings", diff --git a/scripts/check-pins.ts b/scripts/check-pins.ts index 8cdf902..6980ad7 100644 --- a/scripts/check-pins.ts +++ b/scripts/check-pins.ts @@ -2,92 +2,37 @@ import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { dependencyVersionMap } from "../src/constants/dependencies"; +import { findPinProblem } from "./pin-problems"; const execFileAsync = promisify(execFile); -async function npmViewJson(packageSpec: string, field: string): Promise { +async function npmViewJson(packageSpec: string, field: string): Promise> { const { stdout } = await execFileAsync("npm", ["view", packageSpec, field, "--json"]); - return JSON.parse(stdout) as unknown; -} - -const cliVersion = dependencyVersionMap.prisma; -const composerVersion = dependencyVersionMap["@prisma/composer"]; -const composerCloudVersion = dependencyVersionMap["@prisma/composer-prisma-cloud"]; -const ormVersion = dependencyVersionMap["@prisma/orm-postgres"]; -const mongoVersion = dependencyVersionMap["@prisma/orm-mongo"]; - -if (!/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(cliVersion)) { - console.error(`prisma is pinned to "${cliVersion}", which is not an exact version.`); - process.exit(1); -} - -if (composerVersion !== composerCloudVersion) { - console.error( - `@prisma/composer is pinned to ${composerVersion} but @prisma/composer-prisma-cloud to ${composerCloudVersion}.`, - ); - process.exit(1); -} - -if (mongoVersion !== ormVersion) { - console.error( - `@prisma/orm-mongo is pinned to ${mongoVersion} but @prisma/orm-postgres to ${ormVersion}.`, - ); - process.exit(1); -} - -const cliDependencies = (await npmViewJson(`prisma@${cliVersion}`, "dependencies")) as Record< - string, - string ->; -const composerCliVersion = cliDependencies["@prisma/composer-cli"]; -const toolchainVersion = cliDependencies["@prisma/orm-toolchain"]; - -if (!composerCliVersion) { - console.error(`prisma@${cliVersion} does not depend on @prisma/composer-cli.`); + return JSON.parse(stdout) as Record; +} + +const [cliDependencies, composerDependencies, composerCloudPeerDependencies] = await Promise.all([ + npmViewJson(`prisma@${dependencyVersionMap.prisma}`, "dependencies"), + npmViewJson(`@prisma/composer@${dependencyVersionMap["@prisma/composer"]}`, "dependencies"), + npmViewJson( + `@prisma/composer-prisma-cloud@${dependencyVersionMap["@prisma/composer-prisma-cloud"]}`, + "peerDependencies", + ), +]); + +const problem = findPinProblem(dependencyVersionMap, { + cliDependencies, + composerDependencies, + composerCloudPeerDependencies, +}); + +if (problem) { + console.error(problem); process.exit(1); } -if (composerCliVersion !== composerVersion) { - console.error( - `Version mismatch: prisma@${cliVersion} depends on @prisma/composer-cli@${composerCliVersion}, ` + - `but scaffolds pin @prisma/composer to ${composerVersion}.\n` + - `Bump the Composer pins to ${composerCliVersion}, or pin prisma to the release that depends on ${composerVersion}.`, - ); - process.exit(1); -} - -const cloudPeerDependencies = (await npmViewJson( - `@prisma/composer-prisma-cloud@${composerCloudVersion}`, - "peerDependencies", -)) as Record; -const cloudOrmPeer = cloudPeerDependencies["@prisma/orm-postgres"]; - -if (!cloudOrmPeer) { - console.error( - `@prisma/composer-prisma-cloud@${composerCloudVersion} does not declare a peerDependency on @prisma/orm-postgres.`, - ); - process.exit(1); -} - -if (cloudOrmPeer !== ormVersion) { - console.error( - `Version mismatch: @prisma/composer-prisma-cloud@${composerCloudVersion} peers ` + - `@prisma/orm-postgres@${cloudOrmPeer}, but scaffolds pin it to ${ormVersion}.\n` + - `Bump the ORM pins to ${cloudOrmPeer}, or pin Composer to the release that peers ${ormVersion}.`, - ); - process.exit(1); -} - -if (toolchainVersion && toolchainVersion !== ormVersion) { - console.warn( - `Note: prisma@${cliVersion} bundles @prisma/orm-toolchain@${toolchainVersion}, ` + - `which differs from the pinned ORM ${ormVersion}. ` + - `The ORM pin follows @prisma/composer-prisma-cloud's peerDependency so npm install stays resolvable.`, - ); -} - console.log( - `prisma@${cliVersion} depends on @prisma/composer-cli@${composerCliVersion}, ` + - `matching the pinned Composer ${composerVersion}; ` + - `@prisma/composer-prisma-cloud peers @prisma/orm-postgres@${cloudOrmPeer}, matching the ORM pins.`, + `prisma@${dependencyVersionMap.prisma} matches Composer ${dependencyVersionMap["@prisma/composer"]}, ` + + `and both use ORM ${dependencyVersionMap["@prisma/orm-postgres"]}, alchemy ${dependencyVersionMap.alchemy} ` + + `and effect ${dependencyVersionMap.effect}.`, ); diff --git a/scripts/pin-problems.ts b/scripts/pin-problems.ts new file mode 100644 index 0000000..114885f --- /dev/null +++ b/scripts/pin-problems.ts @@ -0,0 +1,86 @@ +import { dependencyVersionMap } from "../src/constants/dependencies"; + +export type Pins = Pick< + typeof dependencyVersionMap, + | "prisma" + | "@prisma/composer" + | "@prisma/composer-prisma-cloud" + | "@prisma/orm-postgres" + | "@prisma/orm-mongo" + | "alchemy" + | "effect" +>; + +type Dependencies = Record; + +export type RegistryMetadata = { + cliDependencies: Dependencies; + composerDependencies: Dependencies; + composerCloudPeerDependencies: Dependencies; +}; + +export function findPinProblem(pins: Pins, registry: RegistryMetadata): string | undefined { + const cliVersion = pins.prisma; + const composerVersion = pins["@prisma/composer"]; + const composerCloudVersion = pins["@prisma/composer-prisma-cloud"]; + const ormVersion = pins["@prisma/orm-postgres"]; + const mongoVersion = pins["@prisma/orm-mongo"]; + + if (!/^\d+\.\d+\.\d+(-[\w.]+)?$/.test(cliVersion)) { + return `prisma is pinned to "${cliVersion}", which is not an exact version.`; + } + if (composerVersion !== composerCloudVersion) { + return `@prisma/composer is pinned to ${composerVersion} but @prisma/composer-prisma-cloud to ${composerCloudVersion}.`; + } + if (mongoVersion !== ormVersion) { + return `@prisma/orm-mongo is pinned to ${mongoVersion} but @prisma/orm-postgres to ${ormVersion}.`; + } + + const composerCliVersion = registry.cliDependencies["@prisma/composer-cli"]; + if (!composerCliVersion) { + return `prisma@${cliVersion} does not depend on @prisma/composer-cli.`; + } + if (composerCliVersion !== composerVersion) { + return ( + `Version mismatch: prisma@${cliVersion} depends on @prisma/composer-cli@${composerCliVersion}, ` + + `but scaffolds pin @prisma/composer to ${composerVersion}.\n` + + `Bump the Composer pins to ${composerCliVersion}, or pin prisma to the release that depends on ${composerVersion}.` + ); + } + + for (const name of ["alchemy", "effect"] as const) { + const composerPin = registry.composerDependencies[name]; + if (composerPin !== pins[name]) { + return ( + `Version mismatch: @prisma/composer@${composerVersion} depends on ${name}@${composerPin}, ` + + `but scaffolds pin it to ${pins[name]}.` + ); + } + } + + const cloudOrmPeer = registry.composerCloudPeerDependencies["@prisma/orm-postgres"]; + if (!cloudOrmPeer) { + return `@prisma/composer-prisma-cloud@${composerCloudVersion} does not declare a peerDependency on @prisma/orm-postgres.`; + } + if (cloudOrmPeer !== ormVersion) { + return ( + `Version mismatch: @prisma/composer-prisma-cloud@${composerCloudVersion} peers ` + + `@prisma/orm-postgres@${cloudOrmPeer}, but scaffolds pin it to ${ormVersion}.\n` + + `Bump the ORM pins to ${cloudOrmPeer}, or pin Composer to the release that peers ${ormVersion}.` + ); + } + + const toolchainVersion = registry.cliDependencies["@prisma/orm-toolchain"]; + if (!toolchainVersion) { + return `prisma@${cliVersion} does not depend on @prisma/orm-toolchain.`; + } + if (toolchainVersion !== ormVersion) { + return ( + `Version mismatch: prisma@${cliVersion} bundles @prisma/orm-toolchain@${toolchainVersion}, ` + + `but scaffolds pin the ORM to ${ormVersion}. \`prisma contract emit\` rejects ORM packages ` + + `from another release, so wait for a Composer and prisma release on the same ORM version.` + ); + } + + return undefined; +} diff --git a/src/constants/dependencies.ts b/src/constants/dependencies.ts index d8f1e50..771c5de 100644 --- a/src/constants/dependencies.ts +++ b/src/constants/dependencies.ts @@ -3,27 +3,28 @@ import type { CreateTemplate, PackageManager } from "../types"; export const dependencyVersionMap = { "@astrojs/node": "^11.1.9", "@elysia/node": "^1.4.6", - "@prisma/composer": "0.35.0", - "@prisma/composer-prisma-cloud": "0.35.0", + "@prisma/composer": "0.38.0", + "@prisma/composer-prisma-cloud": "0.38.0", "@prisma/dev": "0.25.2", - "@prisma/orm-mongo": "8.0.0-rc.20", - // Must match @prisma/composer-prisma-cloud's exact peerDependency, even when - // prisma bundles a newer @prisma/orm-toolchain, or npm install fails with - // ERESOLVE. - "@prisma/orm-postgres": "8.0.0-rc.20", + "@prisma/orm-mongo": "8.0.0-rc.23", + // Must match both @prisma/composer-prisma-cloud's exact peerDependency (or npm + // install fails with ERESOLVE) and the @prisma/orm-toolchain the prisma CLI + // bundles (or `prisma contract emit` rejects the ORM packages). + // `bun run check:pins` verifies both. + "@prisma/orm-postgres": "8.0.0-rc.23", "@sveltejs/adapter-node": "^6.0.0", "@types/node": "^26.6.5", "@vitejs/devtools": "0.7.6", - alchemy: "2.0.0-beta.83", + alchemy: "2.0.0-beta.84", arktype: "^2.2.9", dotenv: "^18.0.9", - effect: "4.0.3", + effect: "4.0.5", mongodb: "^7.7.0", "mongodb-memory-server": "^11.3.0", nitro: "^3.0.260903-beta", // Must match the @prisma/composer-cli version this prisma release depends on. // `bun run check:pins` verifies that against the Composer pins above. - prisma: "8.0.0-rc.28", + prisma: "8.0.0-rc.33", // Node 22 needs a global Temporal for the ORM runtime's timestamp columns. "temporal-polyfill": "^1.0.6", tsdown: "^0.23.1", @@ -33,15 +34,13 @@ export const dependencyVersionMap = { } as const; // Pinned to the effect version as a set: @effect/* siblings depend on each other with -// caret prerelease ranges, so a newer rc (e.g. platform-node-shared rc.118) floats in and -// imports effect modules the pinned effect lacks. +// caret ranges, so a newer sibling floats in and imports effect modules the pinned +// effect lacks. export const effectPackages = [ "effect", "@effect/platform-bun", "@effect/platform-node", "@effect/platform-node-shared", - "@effect/sql-d1", - "@effect/sql-sqlite-do", "@effect/vitest", ] as const; diff --git a/templates/create/_package-manager/pnpm-workspace.yaml.hbs b/templates/create/_package-manager/pnpm-workspace.yaml.hbs index 46ff346..c7b1a51 100644 --- a/templates/create/_package-manager/pnpm-workspace.yaml.hbs +++ b/templates/create/_package-manager/pnpm-workspace.yaml.hbs @@ -17,13 +17,11 @@ allowBuilds: minimumReleaseAgeExclude: - "@prisma/*" overrides: - effect: "4.0.3" - "@effect/platform-bun": "4.0.3" - "@effect/platform-node": "4.0.3" - "@effect/platform-node-shared": "4.0.3" - "@effect/sql-d1": "4.0.3" - "@effect/sql-sqlite-do": "4.0.3" - "@effect/vitest": "4.0.3" + effect: "4.0.5" + "@effect/platform-bun": "4.0.5" + "@effect/platform-node": "4.0.5" + "@effect/platform-node-shared": "4.0.5" + "@effect/vitest": "4.0.5" {{#if (eq provider "mongo")}} peerDependencyRules: allowedVersions: diff --git a/templates/create/_shared/src/prisma/users.ts.hbs b/templates/create/_shared/src/prisma/users.ts.hbs index ef28699..46c19d9 100644 --- a/templates/create/_shared/src/prisma/users.ts.hbs +++ b/templates/create/_shared/src/prisma/users.ts.hbs @@ -27,7 +27,7 @@ export async function listUsers(limit = 10) { email: user.email, username: user.username ?? null, name: user.name ?? null, - createdAt: user.createdAt, + createdAt: user.createdAt?.toISOString() ?? null, })); {{/if}} } diff --git a/tests/dependencies.test.ts b/tests/dependencies.test.ts index bc56e1c..b58fc7a 100644 --- a/tests/dependencies.test.ts +++ b/tests/dependencies.test.ts @@ -4,13 +4,13 @@ import { dependencyVersionMap, getDependencyVersion } from "../src/constants/dep describe("Prisma 8 dependency versions", () => { test("uses the selected Prisma 8 and Composer releases", () => { - expect(getDependencyVersion("@prisma/orm-postgres")).toBe("8.0.0-rc.20"); - expect(getDependencyVersion("@prisma/orm-mongo")).toBe("8.0.0-rc.20"); - expect(getDependencyVersion("@prisma/composer")).toBe("0.35.0"); - expect(getDependencyVersion("@prisma/composer-prisma-cloud")).toBe("0.35.0"); - expect(getDependencyVersion("prisma")).toBe("8.0.0-rc.28"); - expect(getDependencyVersion("alchemy")).toBe("2.0.0-beta.83"); - expect(getDependencyVersion("effect")).toBe("4.0.3"); + expect(getDependencyVersion("@prisma/orm-postgres")).toBe("8.0.0-rc.23"); + expect(getDependencyVersion("@prisma/orm-mongo")).toBe("8.0.0-rc.23"); + expect(getDependencyVersion("@prisma/composer")).toBe("0.38.0"); + expect(getDependencyVersion("@prisma/composer-prisma-cloud")).toBe("0.38.0"); + expect(getDependencyVersion("prisma")).toBe("8.0.0-rc.33"); + expect(getDependencyVersion("alchemy")).toBe("2.0.0-beta.84"); + expect(getDependencyVersion("effect")).toBe("4.0.5"); expect(getDependencyVersion("turbo")).toBe("2.11.10"); expect(getDependencyVersion("typescript")).toBe("^6.0.3"); }); diff --git a/tests/e2e/create-prisma.e2e.test.ts b/tests/e2e/create-prisma.e2e.test.ts index 5836a61..db114e0 100644 --- a/tests/e2e/create-prisma.e2e.test.ts +++ b/tests/e2e/create-prisma.e2e.test.ts @@ -533,7 +533,7 @@ describe("create-prisma e2e", () => { expect(packageJson.devDependencies.prisma).toBe(dependencyVersionMap.prisma); expect(packageJson.scripts.postinstall).toBe("prisma skills sync || exit 0"); expect(packageJson.scripts.deploy).toContain("bun run composer:deploy"); - expect(packageJson.overrides.effect).toBe("4.0.3"); + expect(packageJson.overrides.effect).toBe(dependencyVersionMap.effect); expect(moduleSource).toContain("postgres({"); expect(dbSource).toContain("service.load().database.client"); diff --git a/tests/pin-problems.test.ts b/tests/pin-problems.test.ts new file mode 100644 index 0000000..d86eea9 --- /dev/null +++ b/tests/pin-problems.test.ts @@ -0,0 +1,118 @@ +import { describe, expect, test } from "bun:test"; +import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; + +import { findPinProblem, type Pins, type RegistryMetadata } from "../scripts/pin-problems"; +import { dependencyVersionMap } from "../src/constants/dependencies"; + +const pins: Pins = { + prisma: "8.0.0-rc.33", + "@prisma/composer": "0.38.0", + "@prisma/composer-prisma-cloud": "0.38.0", + "@prisma/orm-postgres": "8.0.0-rc.23", + "@prisma/orm-mongo": "8.0.0-rc.23", + alchemy: "2.0.0-beta.84", + effect: "4.0.5", +}; + +function registry(overrides: { + cli?: RegistryMetadata["cliDependencies"]; + composer?: RegistryMetadata["composerDependencies"]; + cloudPeers?: RegistryMetadata["composerCloudPeerDependencies"]; +}): RegistryMetadata { + return { + cliDependencies: { + "@prisma/composer-cli": "0.38.0", + "@prisma/orm-toolchain": "8.0.0-rc.23", + ...overrides.cli, + }, + composerDependencies: { alchemy: "2.0.0-beta.84", effect: "4.0.5", ...overrides.composer }, + composerCloudPeerDependencies: { + "@prisma/orm-postgres": "8.0.0-rc.23", + ...overrides.cloudPeers, + }, + }; +} + +describe("findPinProblem", () => { + test("accepts pins that match the registry", () => { + expect(findPinProblem(pins, registry({}))).toBeUndefined(); + }); + + test("rejects a prisma CLI that bundles another ORM toolchain", () => { + expect( + findPinProblem(pins, registry({ cli: { "@prisma/orm-toolchain": "8.0.0-rc.22" } })), + ).toContain("bundles @prisma/orm-toolchain@8.0.0-rc.22"); + }); + + test("rejects a prisma CLI that does not list the ORM toolchain", () => { + expect( + findPinProblem(pins, registry({ cli: { "@prisma/orm-toolchain": undefined } })), + ).toContain("does not depend on @prisma/orm-toolchain"); + }); + + test("rejects a prisma CLI on another Composer release", () => { + expect(findPinProblem(pins, registry({ cli: { "@prisma/composer-cli": "0.37.0" } }))).toContain( + "@prisma/composer-cli@0.37.0", + ); + }); + + test("rejects Composer peering another ORM release", () => { + expect( + findPinProblem(pins, registry({ cloudPeers: { "@prisma/orm-postgres": "8.0.0-rc.20" } })), + ).toContain("peers @prisma/orm-postgres@8.0.0-rc.20"); + }); + + test("rejects alchemy or effect pins that differ from Composer's", () => { + expect(findPinProblem(pins, registry({ composer: { effect: "4.0.3" } }))).toContain( + "effect@4.0.3", + ); + expect(findPinProblem(pins, registry({ composer: { alchemy: "2.0.0-beta.83" } }))).toContain( + "alchemy@2.0.0-beta.83", + ); + }); +}); + +async function runCheckPins(toolchainVersion: string) { + const binDir = await mkdtemp(path.join(tmpdir(), "create-prisma-pins-")); + const metadata = { + [`prisma@${dependencyVersionMap.prisma}`]: { + "@prisma/composer-cli": dependencyVersionMap["@prisma/composer"], + "@prisma/orm-toolchain": toolchainVersion, + }, + [`@prisma/composer@${dependencyVersionMap["@prisma/composer"]}`]: { + alchemy: dependencyVersionMap.alchemy, + effect: dependencyVersionMap.effect, + }, + [`@prisma/composer-prisma-cloud@${dependencyVersionMap["@prisma/composer-prisma-cloud"]}`]: { + "@prisma/orm-postgres": dependencyVersionMap["@prisma/orm-postgres"], + }, + }; + const npmStub = path.join(binDir, "npm"); + await writeFile( + npmStub, + `#!/usr/bin/env node\nprocess.stdout.write(JSON.stringify(${JSON.stringify(metadata)}[process.argv[3]]));\n`, + ); + await chmod(npmStub, 0o755); + try { + const result = Bun.spawnSync(["bun", "scripts/check-pins.ts"], { + env: { ...process.env, PATH: `${binDir}${path.delimiter}${process.env.PATH}` }, + }); + return { exitCode: result.exitCode, stderr: result.stderr.toString() }; + } finally { + await rm(binDir, { recursive: true, force: true }); + } +} + +describe("check:pins", () => { + test("exits 0 when the registry matches the pins", async () => { + expect((await runCheckPins(dependencyVersionMap["@prisma/orm-postgres"])).exitCode).toBe(0); + }); + + test("exits non-zero when prisma bundles another ORM toolchain", async () => { + const { exitCode, stderr } = await runCheckPins("8.0.0-rc.0"); + expect(exitCode).toBe(1); + expect(stderr).toContain("bundles @prisma/orm-toolchain@8.0.0-rc.0"); + }); +});