From af8ca04f143d0502bd8715e68c7a223add0545da Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Thu, 13 Aug 2026 20:27:48 +0530 Subject: [PATCH 01/19] feat: add Prisma Composer workflow Signed-off-by: Aman Varshney --- .github/workflows/publish.yml | 4 +- README.md | 181 +-- package.json | 4 +- src/commands/create.ts | 14 +- src/constants/db-packages.ts | 15 +- src/constants/dependencies.ts | 149 +- src/index.ts | 1 - src/tasks/deploy-with-composer.ts | 135 ++ src/tasks/install.ts | 242 ++-- src/tasks/prisma-postgres.ts | 122 -- src/tasks/setup-prisma.ts | 1260 +++-------------- src/telemetry/create.ts | 46 +- src/templates/render-create-template.ts | 34 +- src/templates/shared.ts | 17 +- src/types.ts | 20 +- src/utils/node-version.ts | 23 + src/utils/package-manager.ts | 128 +- src/utils/runtime.ts | 4 +- templates/create/_shared/README.md.hbs | 38 + templates/create/_shared/module.ts.hbs | 28 + .../create/_shared/pnpm-workspace.yaml.hbs | 11 + .../_shared/prisma-composer.config.ts.hbs | 11 + templates/create/_shared/prisma.config.ts.hbs | 10 + templates/create/_shared/service.ts.hbs | 42 + .../create/_shared/src/prisma/composer.ts.hbs | 8 + templates/create/_shared/src/prisma/db.ts.hbs | 41 + .../create/_shared/src/prisma/seed.ts.hbs | 2 +- .../create/_shared/src/prisma/users.ts.hbs | 53 +- templates/create/astro/README.md.hbs | 46 - templates/create/astro/astro.config.mjs | 7 +- templates/create/astro/deno.json.hbs | 5 - templates/create/elysia/README.md.hbs | 43 - templates/create/elysia/deno.json.hbs | 5 - templates/create/elysia/package.json.hbs | 6 +- templates/create/elysia/src/index.ts.hbs | 16 +- templates/create/hono/README.md.hbs | 43 - templates/create/hono/deno.json.hbs | 5 - templates/create/hono/package.json.hbs | 4 +- templates/create/hono/src/index.ts.hbs | 4 +- templates/create/minimal/README.md.hbs | 40 - templates/create/minimal/deno.json.hbs | 5 - templates/create/minimal/package.json.hbs | 4 +- templates/create/minimal/src/index.ts.hbs | 56 +- templates/create/nest/README.md.hbs | 44 - templates/create/nest/deno.json.hbs | 5 - templates/create/nest/package.json.hbs | 4 +- templates/create/nest/src/app.module.ts.hbs | 8 +- templates/create/nest/src/main.ts.hbs | 8 +- .../create/nest/src/prisma.service.ts.hbs | 2 +- .../create/nest/src/users.controller.ts.hbs | 3 +- .../create/nest/src/users.service.ts.hbs | 3 +- templates/create/next/README.md.hbs | 43 - templates/create/next/deno.json.hbs | 12 - templates/create/next/next.config.ts | 2 +- templates/create/next/src/app/page.tsx.hbs | 3 - templates/create/nuxt/README.md.hbs | 46 - templates/create/nuxt/deno.json.hbs | 5 - templates/create/nuxt/nuxt.config.ts | 5 - templates/create/svelte/README.md.hbs | 45 - templates/create/svelte/deno.json.hbs | 5 - templates/create/svelte/package.json.hbs | 1 - templates/create/svelte/svelte.config.js | 5 +- templates/create/svelte/vite.config.ts | 3 +- templates/create/tanstack-start/README.md.hbs | 45 - templates/create/tanstack-start/deno.json.hbs | 12 - .../create/tanstack-start/vite.config.ts | 4 +- tests/dependencies.test.ts | 119 +- tests/e2e/create-prisma.e2e.test.ts | 449 ++---- tests/install.test.ts | 478 ++----- tests/node-version.test.ts | 15 + tests/setup-prisma.test.ts | 42 +- tests/telemetry.test.ts | 145 +- 72 files changed, 1086 insertions(+), 3407 deletions(-) create mode 100644 src/tasks/deploy-with-composer.ts delete mode 100644 src/tasks/prisma-postgres.ts create mode 100644 src/utils/node-version.ts create mode 100644 templates/create/_shared/README.md.hbs create mode 100644 templates/create/_shared/module.ts.hbs create mode 100644 templates/create/_shared/pnpm-workspace.yaml.hbs create mode 100644 templates/create/_shared/prisma-composer.config.ts.hbs create mode 100644 templates/create/_shared/prisma.config.ts.hbs create mode 100644 templates/create/_shared/service.ts.hbs create mode 100644 templates/create/_shared/src/prisma/composer.ts.hbs create mode 100644 templates/create/_shared/src/prisma/db.ts.hbs delete mode 100644 templates/create/astro/README.md.hbs delete mode 100644 templates/create/astro/deno.json.hbs delete mode 100644 templates/create/elysia/README.md.hbs delete mode 100644 templates/create/elysia/deno.json.hbs delete mode 100644 templates/create/hono/README.md.hbs delete mode 100644 templates/create/hono/deno.json.hbs delete mode 100644 templates/create/minimal/README.md.hbs delete mode 100644 templates/create/minimal/deno.json.hbs delete mode 100644 templates/create/nest/README.md.hbs delete mode 100644 templates/create/nest/deno.json.hbs delete mode 100644 templates/create/next/README.md.hbs delete mode 100644 templates/create/next/deno.json.hbs delete mode 100644 templates/create/nuxt/README.md.hbs delete mode 100644 templates/create/nuxt/deno.json.hbs delete mode 100644 templates/create/svelte/README.md.hbs delete mode 100644 templates/create/svelte/deno.json.hbs delete mode 100644 templates/create/tanstack-start/README.md.hbs delete mode 100644 templates/create/tanstack-start/deno.json.hbs create mode 100644 tests/node-version.test.ts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5ceb7a6..847bc6b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -43,7 +43,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "22.14.0" + node-version: "22.18.0" registry-url: "https://registry.npmjs.org" - name: Compute preview metadata @@ -201,7 +201,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "22.14.0" + node-version: "22.18.0" registry-url: "https://registry.npmjs.org" - name: Extract version from commit message diff --git a/README.md b/README.md index 981aa99..58f9760 100644 --- a/README.md +++ b/README.md @@ -1,179 +1,66 @@ # create-prisma -Scaffold a new app with Prisma Next already wired up. +Create a Prisma Next app with Prisma Composer built in. -`create-prisma@next` gives you a project template, Prisma Next setup, database scripts, and a working starting point without making you assemble everything by hand. +## Quick start -## What It Does - -- creates a new app from a supported template -- adds Prisma Next dependencies for PostgreSQL or MongoDB -- runs `prisma-next init --no-install` to scaffold `prisma/contract.*`, `prisma-next.config.ts`, `prisma/db.ts`, `prisma-next.md`, and `.env.example` -- writes a template-specific Prisma Next runtime helper -- adds `contract:emit`, `db:init`, `db:update`, `db:verify`, `db:seed`, `migration:plan`, `migrate`, `migration:status`, and `migration:show` scripts -- adds `db:up` / `db:down` and `docker-compose.yml` for default MongoDB projects -- creates or updates `.env` with `DATABASE_URL` -- can install dependencies and run `prisma-next contract emit` - -`db:init`, migrations, and seeding are never run automatically. PostgreSQL projects show -`db:init` as a manual follow-up command; MongoDB projects show `db:up` plus the migration -plan/apply path for initial schema setup. - -## Quick Start - -Use the package runner you already have: +Use your package manager: ```bash npx create-prisma@next my-app -``` - -```bash pnpm dlx create-prisma@next my-app -``` - -```bash yarn dlx create-prisma@next my-app -``` - -```bash bunx create-prisma@next my-app ``` -```bash -deno run -A npm:create-prisma@next my-app -``` - -If you already have it available locally: - -```bash -create-prisma -``` - -## Common Examples - -Create a project interactively: - -```bash -create-prisma -``` - -Create a Minimal project non-interactively: +The CLI initializes Prisma Next with `@prisma/cli@next`, installs dependencies, emits the contract, and generates a deployable Composer app. PostgreSQL projects use Composer's native Prisma Postgres provider, including migrations and a typed runtime client. -```bash -create-prisma my-script --template minimal --provider postgres -``` +The only Composer prompt is: -Create a Hono app non-interactively: - -```bash -create-prisma my-api --template hono --provider postgres +```text +Deploy to Prisma now? ``` -Create a MongoDB app: +Choose no to deploy later with the generated `deploy` script. -```bash -create-prisma my-api --template hono --provider mongodb -``` +## Templates -Scaffold into the current directory: +- `minimal` +- `hono` +- `elysia` +- `nest` +- `next` +- `svelte` (SvelteKit) +- `astro` +- `nuxt` +- `tanstack-start` -```bash -create-prisma . --template hono --provider postgres -``` - -Use TypeScript contract authoring: +PostgreSQL and MongoDB are supported with PSL or TypeScript contract authoring. npm, pnpm, Yarn, and Bun are supported. -```bash -create-prisma my-app --template next --authoring typescript -``` +## Options -Use Prisma Postgres auto-provisioning: +- positional project name or `--name` +- `--template` +- `--provider postgres|postgresql|mongo|mongodb` +- `--authoring psl|typescript` +- `--package-manager npm|pnpm|yarn|bun` +- `--deploy` / `--no-deploy` +- `--yes` +- `--force` +- `--verbose` -```bash -create-prisma my-app --template nest --provider postgres --prisma-postgres -``` +This branch intentionally targets Prisma Next only. It does not generate a Prisma 7 compatibility path. -Target a specific Prisma Next release (useful for regression / bisect work): - -```bash -create-prisma my-app --template hono --prisma-next-version 0.10.0 -``` - -Scaffold against an open PR via pkg.pr.new: - -```bash -create-prisma my-app --template hono --prisma-next-version pkg-pr-new:bad6795 -``` - -## Supported Templates - -- `minimal` - script-first Prisma Next starter with no web framework -- `hono` - lightweight TypeScript API server -- `elysia` - Bun-friendly TypeScript API server -- `nest` - structured Node API with controllers and services -- `next` - full-stack React app with App Router -- `svelte` - full-stack Svelte 5 app with Vite -- `astro` - content-oriented web app with server routes -- `nuxt` - full-stack Vue app with Nitro server routes -- `tanstack-start` - React app with file routes and server functions - -## Supported Databases - -- `postgres` / `postgresql` -- `mongo` / `mongodb` - -## Supported Package Managers - -- `npm` -- `pnpm` -- `yarn` -- `bun` -- `deno` - -## Useful Flags - -- positional project name or `--name` project name / relative path -- `--template` choose the template -- `--provider postgres|postgresql|mongo|mongodb` (default: `postgres`) -- `--authoring psl|typescript` (default: `psl`) -- `--package-manager` choose the package manager/runtime -- `--database-url` set `DATABASE_URL` -- `--yes` accept defaults and skip prompts -- `--no-install` scaffold only -- `--no-emit` skip `prisma-next contract emit` -- `--prisma-postgres` provision Prisma Postgres for PostgreSQL -- `--prisma-next-version ` target a specific Prisma Next release, npm dist-tag, or - pkg.pr.new PR preview. Accepts a published version (`0.10.0`, `0.11.0-dev.9`), - an npm dist-tag (`latest` (default), `dev`, `next`, …), or `pkg-pr-new:` - to install from `https://pkg.pr.new/prisma/prisma-next/@`. -- `--force` allow scaffolding into a non-empty directory -- `--verbose` print full command output - -Generated Node-based Prisma Next projects document Node.js 24 LTS or newer. - -## Local Development +## Development ```bash bun install +bun run test:unit +bun run typecheck bun run check bun run build -bun run start ``` -Useful repo scripts: - -- `bun run dev` -- `bun run typecheck` -- `bun run format` -- `bun run lint` -- `bun run bump` - ## Telemetry -Published builds may send anonymous usage telemetry to help improve the CLI. It does not include project names, file paths, or database URLs. - -Disable it with any of: - -- `DO_NOT_TRACK` -- `CREATE_PRISMA_DISABLE_TELEMETRY` -- `CREATE_PRISMA_TELEMETRY_DISABLED` +Published builds may send anonymous usage telemetry. It never includes project names, file paths, or database URLs. Disable it with `DO_NOT_TRACK`, `CREATE_PRISMA_DISABLE_TELEMETRY`, or `CREATE_PRISMA_TELEMETRY_DISABLED`. diff --git a/package.json b/package.json index 3a1613c..0de1faf 100644 --- a/package.json +++ b/package.json @@ -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/dependencies.test.ts ./tests/install.test.ts ./tests/setup-prisma.test.ts ./tests/telemetry.test.ts", + "test:unit": "bun test ./tests/dependencies.test.ts ./tests/install.test.ts ./tests/node-version.test.ts ./tests/setup-prisma.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", @@ -73,7 +73,7 @@ }, "engines": { "bun": ">=1.3.0", - "node": ">=24.0.0" + "node": ">=22.18.0" }, "packageManager": "bun@1.3.9" } diff --git a/src/commands/create.ts b/src/commands/create.ts index 5bf2f54..ce8ad30 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -7,7 +7,7 @@ import { trackCreateFailed, type CreateTelemetryFailureStage, } from "../telemetry"; -import { scaffoldCreateTemplate } from "../templates/render-create-template"; +import { scaffoldCreateFrameworkTemplate } from "../templates/render-create-template"; import { writeCreateTemplateDependencies } from "../tasks/install"; import type { PrismaSetupContext } from "../tasks/setup-prisma"; import { collectPrismaSetupContext, executePrismaSetupContext } from "../tasks/setup-prisma"; @@ -18,6 +18,7 @@ import { type CreateTemplate, } from "../types"; import { getCreatePrismaIntro } from "../ui/branding"; +import { getUnsupportedNodeMessage, supportsPrismaNext } from "../utils/node-version"; const DEFAULT_PROJECT_NAME = "my-app"; const DEFAULT_TEMPLATE: CreateTemplate = "minimal"; @@ -188,6 +189,12 @@ export async function runCreateCommand(rawInput: CreateCommandInput = {}): Promi try { input = CreateCommandInputSchema.parse(rawInput); + if (!supportsPrismaNext()) { + cancel(getUnsupportedNodeMessage()); + process.exitCode = 1; + return; + } + intro(getCreatePrismaIntro()); failureStage = "collect_context"; @@ -308,7 +315,7 @@ async function executeCreateContext( log.step(`Scaffolding ${context.template} starter.`); } - await scaffoldCreateTemplate({ + await scaffoldCreateFrameworkTemplate({ projectDir: context.targetDirectory, projectName: context.projectPackageName, template: context.template, @@ -334,7 +341,6 @@ async function executeCreateContext( template: context.template, packageManager: context.prismaSetupContext.packageManager, projectDir: context.targetDirectory, - prismaNextSpec: context.prismaSetupContext.prismaNextSpec, }); } catch (error) { createSpinner?.stop("Could not create Prisma Next project."); @@ -369,6 +375,8 @@ async function executeCreateContext( const didSetupPrisma = await executePrismaSetupContext(context.prismaSetupContext, { prependNextSteps: nextSteps, projectDir: context.targetDirectory, + projectName: context.projectPackageName, + template: context.template, createdProjectPath: context.targetDirectory, includeDevNextStep: true, progressSpinner: createSpinner, diff --git a/src/constants/db-packages.ts b/src/constants/db-packages.ts index ec689fe..c03a43d 100644 --- a/src/constants/db-packages.ts +++ b/src/constants/db-packages.ts @@ -1,17 +1,10 @@ -import type { DatabaseProvider, PackageManager } from "../types"; +import type { DatabaseProvider } from "../types"; -export function getDbPackages( - provider: DatabaseProvider, - _packageManager?: PackageManager, -): string { +export function getDbPackages(provider: DatabaseProvider): string { switch (provider) { case "postgres": - return "@prisma-next/postgres"; + return "@prisma/orm-postgres"; case "mongo": - return "@prisma-next/mongo"; - default: { - const exhaustiveCheck: never = provider; - throw new Error(`Unsupported Prisma Next target: ${String(exhaustiveCheck)}`); - } + return "@prisma/orm-mongo"; } } diff --git a/src/constants/dependencies.ts b/src/constants/dependencies.ts index 7f7d5fd..cda03d8 100644 --- a/src/constants/dependencies.ts +++ b/src/constants/dependencies.ts @@ -1,51 +1,28 @@ import type { CreateTemplate, PackageManager } from "../types"; -import { usesNodeStyleRuntime } from "../utils/runtime"; export const dependencyVersionMap = { + "@astrojs/node": "^10.0.2", "@elysiajs/node": "^1.4.5", + "@prisma/cli-engine": "8.0.0-rc.2", + "@prisma/composer": "0.6.0-dev.18", + "@prisma/composer-prisma-cloud": "0.6.0-dev.18", + "@prisma/orm-mongo": "8.0.0-rc.1", + "@prisma/orm-postgres": "8.0.0-rc.1", + "@sveltejs/adapter-node": "^5.3.2", "@types/node": "^25.6.2", + alchemy: "2.0.0-beta.67", + arktype: "^2.2.3", dotenv: "^17.4.2", + esbuild: "^0.28.1", + effect: "4.0.0-beta.103", + mongodb: "^7.1.0", "mongodb-memory-server": "^11.1.0", + nitro: "^3.0.260610-beta", + "prisma-next": "8.0.0-rc.1", tsx: "^4.21.0", + typescript: "^5.9.3", } as const; -export const PRISMA_NEXT_DEFAULT_VERSION = "latest"; -const PKG_PR_NEW_PREFIX = "pkg-pr-new:"; -const PKG_PR_NEW_BASE_URL = "https://pkg.pr.new/prisma/prisma-next"; - -export type ResolvedPrismaNextSpec = - | { kind: "npm"; spec: string } - | { kind: "pkg-pr-new"; ref: string }; - -export const DEFAULT_PRISMA_NEXT_SPEC: ResolvedPrismaNextSpec = { - kind: "npm", - spec: PRISMA_NEXT_DEFAULT_VERSION, -}; - -export function parsePrismaNextVersionSpec(input: string | undefined): ResolvedPrismaNextSpec { - if (input === undefined) { - return DEFAULT_PRISMA_NEXT_SPEC; - } - - const trimmed = input.trim(); - if (trimmed.length === 0) { - return DEFAULT_PRISMA_NEXT_SPEC; - } - - if (trimmed.startsWith(PKG_PR_NEW_PREFIX)) { - const ref = trimmed.slice(PKG_PR_NEW_PREFIX.length).trim(); - if (ref.length === 0) { - throw new Error( - `Invalid --prisma-next-version value: '${input}'. Expected 'pkg-pr-new:'.`, - ); - } - - return { kind: "pkg-pr-new", ref }; - } - - return { kind: "npm", spec: trimmed }; -} - export type AvailableDependency = keyof typeof dependencyVersionMap; export type CreateTemplateDependencyTarget = { @@ -55,81 +32,51 @@ export type CreateTemplateDependencyTarget = { customDependencies?: Record; }; -export function isPrismaNextPackage(packageName: string): boolean { - return packageName === "prisma-next" || packageName.startsWith("@prisma-next/"); -} - -export function getPrismaNextPackageSpecifier( - packageName: string, - spec: ResolvedPrismaNextSpec = DEFAULT_PRISMA_NEXT_SPEC, -): string { - if (spec.kind === "pkg-pr-new") { - return `${PKG_PR_NEW_BASE_URL}/${packageName}@${spec.ref}`; - } - - return `${packageName}@${spec.spec}`; -} - -export function getDependencyVersion( - packageName: string, - prismaNextSpec: ResolvedPrismaNextSpec = DEFAULT_PRISMA_NEXT_SPEC, -): string | undefined { - if (isPrismaNextPackage(packageName)) { - if (prismaNextSpec.kind === "pkg-pr-new") { - return `${PKG_PR_NEW_BASE_URL}/${packageName}@${prismaNextSpec.ref}`; - } - - return prismaNextSpec.spec; - } - +export function getDependencyVersion(packageName: string): string | undefined { return dependencyVersionMap[packageName as AvailableDependency]; } -function usesViteDevServer(template: CreateTemplate): boolean { +function usesEsbuild(template: CreateTemplate): boolean { return ( - template === "astro" || - template === "nuxt" || - template === "svelte" || - template === "tanstack-start" + template === "minimal" || template === "hono" || template === "elysia" || template === "nest" ); } export function getCreateTemplateDependencies( template: CreateTemplate, - packageManager: PackageManager, + _packageManager: PackageManager, ): CreateTemplateDependencyTarget[] { - const targets: CreateTemplateDependencyTarget[] = []; + const dependencies = ["@prisma/composer", "@prisma/composer-prisma-cloud", "alchemy"]; + const devDependencies = ["@prisma/cli-engine"]; - if (usesViteDevServer(template)) { - targets.push({ - packageJsonPath: "package.json", - dependencies: [], - devDependencies: ["@prisma-next/vite-plugin-contract-emit"], - }); + if (usesEsbuild(template)) { + devDependencies.push("esbuild"); } - - if ( - template === "minimal" || - template === "hono" || - template === "elysia" || - template === "nest" - ) { - const runtimeDevDependencies: string[] = usesNodeStyleRuntime(packageManager) ? ["tsx"] : []; - - if (template === "elysia" && packageManager !== "deno") { - targets.push({ - packageJsonPath: "package.json", - dependencies: ["@elysiajs/node"], - devDependencies: ["@types/node", ...runtimeDevDependencies], - }); - } else if (runtimeDevDependencies.length > 0) { - targets.push({ - packageJsonPath: "package.json", - dependencies: [], - devDependencies: runtimeDevDependencies, - }); - } + if (template === "minimal" || usesEsbuild(template)) { + devDependencies.push("tsx"); + } + if (template === "minimal") { + devDependencies.push("typescript"); + } + if (template === "elysia") { + dependencies.push("@elysiajs/node"); + devDependencies.push("@types/node"); + } + if (template === "svelte") { + devDependencies.push("@sveltejs/adapter-node"); + } + if (template === "astro") { + dependencies.push("@astrojs/node"); + } + if (template === "tanstack-start") { + devDependencies.push("nitro"); } - return targets; + return [ + { + packageJsonPath: "package.json", + dependencies, + devDependencies, + }, + ]; } diff --git a/src/index.ts b/src/index.ts index f346128..1b30c01 100644 --- a/src/index.ts +++ b/src/index.ts @@ -57,6 +57,5 @@ export { CreateCommandInputSchema, CreateTemplateSchema, DatabaseProviderSchema, - DatabaseUrlSchema, PackageManagerSchema, } from "./types"; diff --git a/src/tasks/deploy-with-composer.ts b/src/tasks/deploy-with-composer.ts new file mode 100644 index 0000000..29109c7 --- /dev/null +++ b/src/tasks/deploy-with-composer.ts @@ -0,0 +1,135 @@ +import { log, spinner } from "@clack/prompts"; +import { execa } from "execa"; + +import type { PackageManager } from "../types"; +import { + getPackageExecutionArgs, + getPackageExecutionCommand, + getRunScriptArgs, + getRunScriptCommand, +} from "../utils/package-manager"; + +const PRISMA_CLI_PACKAGE = "@prisma/cli@next"; + +type PrismaCliEnvelope = { + ok: boolean; + result?: unknown; + error?: { summary?: string; message?: string }; +}; + +function redactSecrets(message: string): string { + return message + .replace(/\b((?:prisma\+)?postgres(?:ql)?:\/\/)[^\s'"]+/gi, "$1") + .replace( + /\b([A-Z0-9_]*(?:DATABASE_URL|TOKEN|SECRET|PASSWORD|API_KEY|PRIVATE_KEY)[A-Z0-9_]*=)[^\s]+/g, + "$1", + ); +} + +function getErrorMessage(error: unknown): string { + if (error instanceof Error) return redactSecrets(error.message); + return redactSecrets(String(error)); +} + +export function parsePrismaCliEnvelope(output: string): PrismaCliEnvelope { + const lines = output + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + .reverse(); + + for (const line of lines) { + try { + const parsed = JSON.parse(line) as Record; + const candidate = parsed.kind === "result" ? parsed.envelope : parsed; + if (typeof candidate !== "object" || candidate === null) continue; + if (typeof Reflect.get(candidate, "ok") !== "boolean") continue; + return candidate as PrismaCliEnvelope; + } catch { + // The CLI may print progress lines before its final JSON envelope. + } + } + + throw new Error("Prisma CLI returned output that is not a valid result envelope."); +} + +function getPrismaCliArgs(packageManager: PackageManager, args: string[]) { + return getPackageExecutionArgs(packageManager, [PRISMA_CLI_PACKAGE, ...args]); +} + +async function isAuthenticated(packageManager: PackageManager, projectDir: string) { + const invocation = getPrismaCliArgs(packageManager, [ + "auth", + "whoami", + "--json", + "--no-interactive", + ]); + const result = await execa(invocation.command, invocation.args, { + cwd: projectDir, + env: process.env, + reject: false, + }); + const envelope = parsePrismaCliEnvelope(result.stdout); + if (result.exitCode !== 0 || !envelope.ok) { + throw new Error( + envelope.error?.summary ?? envelope.error?.message ?? "Prisma authentication check failed.", + ); + } + if (typeof envelope.result !== "object" || envelope.result === null) return false; + return Reflect.get(envelope.result, "authenticated") === true; +} + +async function ensureAuthentication(packageManager: PackageManager, projectDir: string) { + if (await isAuthenticated(packageManager, projectDir)) return; + + const loginCommand = getPackageExecutionCommand(packageManager, [ + PRISMA_CLI_PACKAGE, + "auth", + "login", + ]); + if (process.stdin.isTTY !== true) { + throw new Error( + `Sign in first with ${loginCommand}, then run ${getRunScriptCommand(packageManager, "deploy")}.`, + ); + } + + log.info("Sign in to Prisma to deploy."); + const login = getPrismaCliArgs(packageManager, ["auth", "login"]); + await execa(login.command, login.args, { + cwd: projectDir, + env: process.env, + stdio: "inherit", + }); + + if (!(await isAuthenticated(packageManager, projectDir))) { + throw new Error("Prisma sign-in completed without an active workspace session."); + } +} + +export async function deployWithComposer(options: { + packageManager: PackageManager; + projectDir: string; + verbose: boolean; +}): Promise { + const progress = options.verbose ? undefined : spinner(); + + try { + await ensureAuthentication(options.packageManager, options.projectDir); + progress?.start("Deploying to Prisma..."); + + const command = getRunScriptArgs(options.packageManager, "deploy"); + await execa(command.command, command.args, { + cwd: options.projectDir, + env: process.env, + stdio: options.verbose ? "inherit" : "pipe", + }); + + progress?.stop("Deployed to Prisma."); + if (options.verbose) log.success("Deployed to Prisma."); + return true; + } catch (error) { + progress?.stop("Deployment failed."); + log.error(`Deploy failed: ${getErrorMessage(error)}`); + return false; + } +} diff --git a/src/tasks/install.ts b/src/tasks/install.ts index 31eee70..e78ced4 100644 --- a/src/tasks/install.ts +++ b/src/tasks/install.ts @@ -2,60 +2,54 @@ import { execa } from "execa"; import fs from "fs-extra"; import path from "node:path"; -import { - DEFAULT_PRISMA_NEXT_SPEC, - getCreateTemplateDependencies, - getDependencyVersion, - type ResolvedPrismaNextSpec, -} from "../constants/dependencies"; +import { getCreateTemplateDependencies, getDependencyVersion } from "../constants/dependencies"; import { getDbPackages } from "../constants/db-packages"; import type { AuthoringStyle, CreateTemplate, DatabaseProvider, PackageManager } from "../types"; -import { getDenoPrismaSpecifier, getInstallArgs } from "../utils/package-manager"; - -function getPrismaNextScriptMap(packageManager: PackageManager) { - if (packageManager === "deno") { - const prismaNextCli = `deno run -A --env-file=.env ${getDenoPrismaSpecifier()}`; - - return { - "contract:emit": `${prismaNextCli} contract emit`, - "db:init": `${prismaNextCli} db init`, - "db:update": `${prismaNextCli} db update`, - "db:verify": `${prismaNextCli} db verify`, - "db:seed": "deno run -A --env-file=.env src/prisma/seed.ts", - "migration:plan": `${prismaNextCli} migration plan`, - migrate: `${prismaNextCli} migrate`, - "migration:status": `${prismaNextCli} migration status`, - "migration:show": `${prismaNextCli} migration show`, - } as const; - } +import { + getInstallArgs, + getPackageExecutionCommand, + getRunScriptCommand, +} from "../utils/package-manager"; - if (packageManager === "bun") { - const prismaNextCli = "bun prisma-next"; - - return { - "contract:emit": `${prismaNextCli} contract emit`, - "db:init": `${prismaNextCli} db init`, - "db:update": `${prismaNextCli} db update`, - "db:verify": `${prismaNextCli} db verify`, - "db:seed": "bun src/prisma/seed.ts", - "migration:plan": `${prismaNextCli} migration plan`, - migrate: `${prismaNextCli} migrate`, - "migration:status": `${prismaNextCli} migration status`, - "migration:show": `${prismaNextCli} migration show`, - } as const; - } +const PRISMA_CLI_PACKAGE = "@prisma/cli@next"; +function getPrismaNextScriptMap(packageManager: PackageManager): Record { return { "contract:emit": "prisma-next contract emit", "db:init": "prisma-next db init", "db:update": "prisma-next db update", "db:verify": "prisma-next db verify", - "db:seed": "tsx src/prisma/seed.ts", + "db:seed": packageManager === "bun" ? "bun src/prisma/seed.ts" : "tsx src/prisma/seed.ts", "migration:plan": "prisma-next migration plan", migrate: "prisma-next migrate", "migration:status": "prisma-next migration status", "migration:show": "prisma-next migration show", - } as const; + }; +} + +export function getComposerScriptMap(packageManager: PackageManager): Record { + const composerCommand = (subcommand: string, extraArgs: string[] = []) => + getPackageExecutionCommand(packageManager, [ + PRISMA_CLI_PACKAGE, + "composer", + subcommand, + "module.ts", + ...extraArgs, + ]); + + return { + "composer:dev": composerCommand("dev"), + "composer:deploy": composerCommand("deploy"), + "composer:destroy": composerCommand("destroy", ["--production"]), + deploy: `${getRunScriptCommand(packageManager, "build")} && ${getRunScriptCommand( + packageManager, + "composer:deploy", + )}`, + "dev:composer": `${getRunScriptCommand(packageManager, "build")} && ${getRunScriptCommand( + packageManager, + "composer:dev", + )}`, + }; } function unique(items: string[]): string[] { @@ -66,52 +60,6 @@ function sortRecord(record: Record): Record { return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b))); } -function getGeneratedContractTypePackages(provider: DatabaseProvider): string[] { - if (provider === "mongo") { - return ["@prisma-next/adapter-mongo", "@prisma-next/contract", "@prisma-next/mongo-contract"]; - } - - return [ - "@prisma-next/adapter-postgres", - "@prisma-next/contract", - "@prisma-next/sql-contract", - "@prisma-next/target-postgres", - ]; -} - -function getTypeScriptContractPackages(provider: DatabaseProvider): string[] { - if (provider === "mongo") { - return [ - ...getGeneratedContractTypePackages(provider), - "@prisma-next/family-mongo", - "@prisma-next/mongo-contract-ts", - "@prisma-next/target-mongo", - ]; - } - - return [ - ...getGeneratedContractTypePackages(provider), - "@prisma-next/family-sql", - "@prisma-next/sql-contract-ts", - ]; -} - -function getMigrationPackages(provider: DatabaseProvider): string[] { - if (provider === "mongo") { - return ["@prisma-next/family-mongo", "@prisma-next/target-mongo"]; - } - - return ["@prisma-next/target-postgres"]; -} - -function getOrmTypePackages(provider: DatabaseProvider): string[] { - if (provider === "mongo") { - return ["@prisma-next/mongo-orm"]; - } - - return ["@prisma-next/sql-orm-client"]; -} - export async function addPackageDependency(opts: { dependencies?: string[]; devDependencies?: string[]; @@ -119,7 +67,6 @@ export async function addPackageDependency(opts: { scripts?: Record; scriptMode?: "if-missing"; projectDir: string; - prismaNextSpec?: ResolvedPrismaNextSpec; }): Promise { const { dependencies = [], @@ -128,7 +75,6 @@ export async function addPackageDependency(opts: { scripts = {}, scriptMode, projectDir, - prismaNextSpec = DEFAULT_PRISMA_NEXT_SPEC, } = opts; const pkgJsonPath = path.join(projectDir, "package.json"); @@ -139,78 +85,54 @@ export async function addPackageDependency(opts: { } const pkgJson = await fs.readJson(pkgJsonPath); - - if (!pkgJson.dependencies) pkgJson.dependencies = {}; - if (!pkgJson.devDependencies) pkgJson.devDependencies = {}; - if (!pkgJson.scripts) pkgJson.scripts = {}; - - for (const pkgName of unique(dependencies)) { - const version = getDependencyVersion(pkgName, prismaNextSpec); - if (version) { - pkgJson.dependencies[pkgName] = version; - } else { - console.warn(`Warning: Dependency ${pkgName} not found in version map.`); - } + pkgJson.dependencies ??= {}; + pkgJson.devDependencies ??= {}; + pkgJson.scripts ??= {}; + + for (const packageName of unique(dependencies)) { + const version = getDependencyVersion(packageName); + if (!version) throw new Error(`Dependency ${packageName} is missing from the version map.`); + pkgJson.dependencies[packageName] = version; } - - for (const pkgName of unique(devDependencies)) { - const version = getDependencyVersion(pkgName, prismaNextSpec); - if (version) { - pkgJson.devDependencies[pkgName] = version; - } else { - console.warn(`Warning: Dev dependency ${pkgName} not found in version map.`); - } + for (const packageName of unique(devDependencies)) { + const version = getDependencyVersion(packageName); + if (!version) throw new Error(`Dependency ${packageName} is missing from the version map.`); + pkgJson.devDependencies[packageName] = version; } - - for (const [pkgName, version] of Object.entries(customDependencies)) { - pkgJson.dependencies[pkgName] = version; + for (const [packageName, version] of Object.entries(customDependencies)) { + pkgJson.dependencies[packageName] = version; } - for (const [scriptName, command] of Object.entries(scripts)) { - if (scriptMode === "if-missing") { - if ( - typeof pkgJson.scripts[scriptName] !== "string" || - pkgJson.scripts[scriptName].trim().length === 0 - ) { - pkgJson.scripts[scriptName] = command; - } + if ( + scriptMode === "if-missing" && + typeof pkgJson.scripts[scriptName] === "string" && + pkgJson.scripts[scriptName].trim().length > 0 + ) { continue; } - pkgJson.scripts[scriptName] = command; } pkgJson.dependencies = sortRecord(pkgJson.dependencies); pkgJson.devDependencies = sortRecord(pkgJson.devDependencies); - - await fs.writeJson(pkgJsonPath, pkgJson, { - spaces: 2, - }); + pkgJson.scripts = sortRecord(pkgJson.scripts); + await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 }); } export async function writePrismaDependencies( provider: DatabaseProvider, packageManager: PackageManager, - authoring: AuthoringStyle, + _authoring: AuthoringStyle, projectDir = process.cwd(), - prismaNextSpec: ResolvedPrismaNextSpec = DEFAULT_PRISMA_NEXT_SPEC, ): Promise { - const dependencies: string[] = [getDbPackages(provider, packageManager), "dotenv"]; - const devDependencies: string[] = ["prisma-next", "@prisma-next/cli", "@types/node"]; - devDependencies.push(...getGeneratedContractTypePackages(provider)); - devDependencies.push(...getMigrationPackages(provider)); - devDependencies.push(...getOrmTypePackages(provider)); - if (authoring === "typescript") { - devDependencies.push(...getTypeScriptContractPackages(provider)); - } - const prismaScriptMap = getPrismaNextScriptMap(packageManager); + const dependencies = [getDbPackages(provider), "dotenv"]; + if (provider === "mongo") dependencies.push("arktype", "mongodb"); await addPackageDependency({ dependencies, - devDependencies, - scripts: prismaScriptMap, + devDependencies: ["prisma-next", "@types/node"], + scripts: getPrismaNextScriptMap(packageManager), projectDir, - prismaNextSpec, }); } @@ -218,37 +140,37 @@ export async function writeCreateTemplateDependencies(opts: { template: CreateTemplate; packageManager: PackageManager; projectDir?: string; - prismaNextSpec?: ResolvedPrismaNextSpec; }): Promise { - const { - template, - packageManager, - projectDir = process.cwd(), - prismaNextSpec = DEFAULT_PRISMA_NEXT_SPEC, - } = opts; - const targets = getCreateTemplateDependencies(template, packageManager); - - for (const dependencyTarget of targets) { - const targetDirectory = path.join(projectDir, path.dirname(dependencyTarget.packageJsonPath)); + const { template, packageManager, projectDir = process.cwd() } = opts; + for (const target of getCreateTemplateDependencies(template, packageManager)) { await addPackageDependency({ - dependencies: dependencyTarget.dependencies, - devDependencies: dependencyTarget.devDependencies, - customDependencies: dependencyTarget.customDependencies, - projectDir: targetDirectory, - prismaNextSpec, + dependencies: target.dependencies, + devDependencies: target.devDependencies, + customDependencies: target.customDependencies, + scripts: getComposerScriptMap(packageManager), + projectDir: path.join(projectDir, path.dirname(target.packageJsonPath)), }); } + + const packageJsonPath = path.join(projectDir, "package.json"); + const packageJson = await fs.readJson(packageJsonPath); + const effectVersion = getDependencyVersion("effect"); + if (!effectVersion) throw new Error("Dependency effect is missing from the version map."); + + if (packageManager === "yarn") { + packageJson.resolutions = { ...packageJson.resolutions, effect: effectVersion }; + } else if (packageManager !== "pnpm") { + packageJson.overrides = { ...packageJson.overrides, effect: effectVersion }; + } + await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 }); } export async function installProjectDependencies( packageManager: PackageManager, projectDir = process.cwd(), - options: { - verbose?: boolean; - } = {}, + options: { verbose?: boolean } = {}, ): Promise { - const verbose = options.verbose === true; const installCommand = getInstallArgs(packageManager); const env = packageManager === "yarn" @@ -260,6 +182,6 @@ export async function installProjectDependencies( await execa(installCommand.command, installCommand.args, { cwd: projectDir, env, - stdio: verbose ? "inherit" : "pipe", + stdio: options.verbose === true ? "inherit" : "pipe", }); } diff --git a/src/tasks/prisma-postgres.ts b/src/tasks/prisma-postgres.ts deleted file mode 100644 index 45392f0..0000000 --- a/src/tasks/prisma-postgres.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { execa } from "execa"; - -import type { PackageManager } from "../types"; -import { getPackageExecutionArgs, getPackageExecutionCommand } from "../utils/package-manager"; - -type CreateDbJsonPayload = { - success?: boolean; - error?: string; - message?: string; - connectionString?: string; - databaseUrl?: string; - claimUrl?: string; - claimURL?: string; -}; - -type PrismaPostgresResult = { - databaseUrl: string; - claimUrl?: string; -}; - -export const PRISMA_POSTGRES_TEMPORARY_NOTICE = - "Prisma Postgres is temporary for 24 hours. Claim this database before it expires using CLAIM_URL."; -const CREATE_DB_COMMAND_ARGS = ["create-db@latest", "--json"] as const; - -function parseCreateDbJson(rawOutput: string): CreateDbJsonPayload { - const trimmed = rawOutput.trim(); - if (!trimmed) { - throw new Error("create-db returned empty output."); - } - - const jsonCandidates = [trimmed]; - const firstBrace = trimmed.indexOf("{"); - const lastBrace = trimmed.lastIndexOf("}"); - if (firstBrace !== -1 && lastBrace > firstBrace) { - jsonCandidates.push(trimmed.slice(firstBrace, lastBrace + 1)); - } - - for (const candidate of jsonCandidates) { - try { - return JSON.parse(candidate) as CreateDbJsonPayload; - } catch { - // Continue trying candidates. - } - } - - throw new Error(`Unable to parse create-db JSON output: ${trimmed}`); -} - -function pickConnectionString(payload: CreateDbJsonPayload): string | undefined { - if (typeof payload.connectionString === "string" && payload.connectionString.length > 0) { - return payload.connectionString; - } - - if (typeof payload.databaseUrl === "string" && payload.databaseUrl.length > 0) { - return payload.databaseUrl; - } - - return undefined; -} - -function extractErrorMessage(payload: CreateDbJsonPayload, fallback: string): string { - if (typeof payload.message === "string" && payload.message.length > 0) { - return payload.message; - } - - if (typeof payload.error === "string" && payload.error.length > 0) { - return payload.error; - } - - return fallback; -} - -export async function provisionPrismaPostgres( - packageManager: PackageManager, - projectDir = process.cwd(), -): Promise { - const command = getPackageExecutionArgs(packageManager, [...CREATE_DB_COMMAND_ARGS]); - const commandString = getCreateDbCommand(packageManager); - - let stdout: string; - try { - const result = await execa(command.command, command.args, { - cwd: projectDir, - stdio: "pipe", - }); - stdout = result.stdout; - } catch (error) { - if (error instanceof Error && "stderr" in error) { - const stderr = String((error as { stderr?: string }).stderr ?? "").trim(); - const message = stderr.length > 0 ? stderr : error.message; - throw new Error(`Failed to run ${commandString}: ${message}`); - } - - throw error; - } - - const payload = parseCreateDbJson(stdout); - if (payload.success === false) { - throw new Error(extractErrorMessage(payload, "create-db reported failure.")); - } - - const databaseUrl = pickConnectionString(payload); - if (!databaseUrl) { - throw new Error("create-db did not return a connection string."); - } - - const claimUrl = - typeof payload.claimUrl === "string" && payload.claimUrl.length > 0 - ? payload.claimUrl - : typeof payload.claimURL === "string" && payload.claimURL.length > 0 - ? payload.claimURL - : undefined; - - return { - databaseUrl, - claimUrl, - }; -} - -export function getCreateDbCommand(packageManager: PackageManager): string { - return getPackageExecutionCommand(packageManager, [...CREATE_DB_COMMAND_ARGS]); -} diff --git a/src/tasks/setup-prisma.ts b/src/tasks/setup-prisma.ts index 2a2a226..ff26595 100644 --- a/src/tasks/setup-prisma.ts +++ b/src/tasks/setup-prisma.ts @@ -3,324 +3,69 @@ import { execa } from "execa"; import fs from "fs-extra"; import path from "node:path"; -import { installProjectDependencies, writePrismaDependencies } from "./install"; -import { - getCreateDbCommand, - PRISMA_POSTGRES_TEMPORARY_NOTICE, - provisionPrismaPostgres, -} from "./prisma-postgres"; -import { - DEFAULT_PRISMA_NEXT_SPEC, - getDependencyVersion, - getPrismaNextPackageSpecifier, - parsePrismaNextVersionSpec, - type ResolvedPrismaNextSpec, -} from "../constants/dependencies"; +import { scaffoldCreateSharedTemplates } from "../templates/render-create-template"; import { AuthoringStyleSchema, DatabaseProviderSchema, PackageManagerSchema, type AuthoringStyle, + type CreateTemplate, type DatabaseProvider, type PackageManager, type PrismaSetupCommandInput, } from "../types"; import { detectPackageManager, - getDenoPrismaSpecifier, getInstallCommand, - getPackageExecutionArgs, getLocalPackageBinaryArgs, getLocalPackageBinaryCommand, + getPackageExecutionArgs, getRunScriptCommand, } from "../utils/package-manager"; +import { deployWithComposer } from "./deploy-with-composer"; +import { installProjectDependencies, writePrismaDependencies } from "./install"; + +const PRISMA_CLI_PACKAGE = "@prisma/cli@next"; +const DEFAULT_DATABASE_PROVIDER: DatabaseProvider = "postgres"; +const DEFAULT_AUTHORING: AuthoringStyle = "psl"; -type EnvWriteMode = "keep-existing" | "upsert"; +type NextStep = { + command: string; + description: string; +}; type PrismaSetupRunOptions = { prependNextSteps?: NextStep[]; projectDir?: string; + projectName?: string; + template?: CreateTemplate; createdProjectPath?: string; includeDevNextStep?: boolean; progressSpinner?: ReturnType; }; -type PrismaPostgresProvisionResult = { - databaseUrl?: string; - claimUrl?: string; - warning?: string; -}; - -type PrismaNextEmitResult = { - didEmitContract: boolean; - warning?: string; -}; - -type NextStep = { - command: string; - description: string; -}; - export type PrismaSetupContext = { projectDir: string; verbose: boolean; - shouldEmit: boolean; databaseProvider: DatabaseProvider; authoring: AuthoringStyle; - databaseUrl?: string; - shouldUsePrismaPostgres: boolean; packageManager: PackageManager; - shouldInstall: boolean; - prismaNextSpec: ResolvedPrismaNextSpec; -}; - -type FinalizePrismaOptions = { - provider: DatabaseProvider; - databaseUrl?: string; - claimUrl?: string; - projectDir?: string; + shouldDeploy: boolean; }; -const DEFAULT_DATABASE_PROVIDER: DatabaseProvider = "postgres"; -const DEFAULT_AUTHORING: AuthoringStyle = "psl"; -const DEFAULT_INSTALL = true; -const DEFAULT_EMIT = true; -const DEFAULT_INTERACTIVE_PRISMA_POSTGRES = true; -const DEFAULT_AUTOMATED_PRISMA_POSTGRES = true; - -const MONGO_MEMORY_SERVER_SCRIPT = `import { spawn } from "node:child_process"; -import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import path from "node:path"; - -const defaultDatabaseUrl = "mongodb://localhost:27017/mydb?replicaSet=rs0&directConnection=true"; -const dataRoot = path.resolve(process.env.MONGO_DB_PATH ?? ".mongo-data"); -const dbPath = path.join(dataRoot, "db"); -const pidFile = path.join(dataRoot, "mongo.pid"); -const logFile = path.join(dataRoot, "mongo.log"); -const readyTimeoutMs = Number(process.env.MONGO_READY_TIMEOUT_MS ?? 60_000); - -function getMongoConfig() { - const databaseUrl = process.env.DATABASE_URL ?? defaultDatabaseUrl; - const url = new URL(databaseUrl); - if (url.protocol !== "mongodb:") { - throw new Error("DATABASE_URL must use the mongodb:// protocol."); - } - - const port = Number(url.port || "27017"); - if (!Number.isInteger(port) || port < 1 || port > 65535) { - throw new Error(\`DATABASE_URL has an invalid MongoDB port: \${url.port}\`); - } - - return { - databaseUrl, - port, - replSetName: url.searchParams.get("replicaSet") || "rs0", - }; -} - -function readPid() { - if (!existsSync(pidFile)) return null; - const raw = readFileSync(pidFile, "utf8").trim(); - const pid = Number(raw); - return Number.isFinite(pid) && pid > 0 ? pid : null; -} - -function isAlive(pid) { - try { - process.kill(pid, 0); - return true; - } catch { - return false; - } -} - -function getChildCommand() { - const scriptPath = path.resolve(process.argv[1] ?? ""); - const versions = process.versions; - if (versions.deno) return { command: process.execPath, args: ["run", "-A", scriptPath, "_run"] }; - return { command: process.execPath, args: [scriptPath, "_run"] }; -} - -async function runServer() { - mkdirSync(dbPath, { recursive: true }); - const config = getMongoConfig(); - const memoryServer = await import("mongodb-memory-server"); - const { MongoMemoryReplSet } = memoryServer.default ?? memoryServer; - const replSet = await MongoMemoryReplSet.create({ - replSet: { name: config.replSetName, count: 1 }, - instanceOpts: [{ port: config.port, storageEngine: "wiredTiger", dbPath }], - }); - console.log(\`MongoDB server ready for \${config.databaseUrl}\`); - console.log(\`Data directory: \${dbPath}\`); - const shutdown = async () => { - await replSet.stop(); - process.exit(0); - }; - process.on("SIGINT", shutdown); - process.on("SIGTERM", shutdown); -} - -async function up() { - mkdirSync(dataRoot, { recursive: true }); - const existing = readPid(); - if (existing !== null && isAlive(existing)) { - console.log(\`MongoDB is already running (PID \${existing}). Use \\\`db:down\\\` to stop.\`); - return; - } - if (existing !== null) rmSync(pidFile, { force: true }); - - writeFileSync(logFile, ""); - const logFd = openSync(logFile, "a"); - const { command, args } = getChildCommand(); - const child = spawn( - command, - args, - { - detached: true, - stdio: ["ignore", logFd, logFd], - env: process.env, - }, - ); - closeSync(logFd); - if (typeof child.pid !== "number") throw new Error("Failed to spawn MongoDB child process."); - writeFileSync(pidFile, String(child.pid)); - child.unref(); - - const start = Date.now(); - while (Date.now() - start < readyTimeoutMs) { - if (!isAlive(child.pid)) { - console.error("MongoDB failed to start:"); - console.error(readFileSync(logFile, "utf8")); - rmSync(pidFile, { force: true }); - process.exit(1); - } - const log = readFileSync(logFile, "utf8"); - if (log.includes("MongoDB server ready")) { - for (const line of log.split("\\n")) { - if (line.trim().length > 0) console.log(line); - } - console.log(\`Detached (PID \${child.pid}). Logs: \${logFile}\`); - console.log("Stop with \`db:down\` or wipe with \`db:reset\`."); - return; - } - await new Promise((resolve) => setTimeout(resolve, 100)); - } - console.error(\`Timed out waiting for MongoDB after \${readyTimeoutMs}ms.\`); - console.error(readFileSync(logFile, "utf8")); - try { - process.kill(child.pid, "SIGTERM"); - } catch { - // ignore - } - rmSync(pidFile, { force: true }); - process.exit(1); -} - -async function down(wipe) { - const pid = readPid(); - if (pid !== null && isAlive(pid)) { - process.kill(pid, "SIGTERM"); - const deadline = Date.now() + 10_000; - while (Date.now() < deadline && isAlive(pid)) { - await new Promise((resolve) => setTimeout(resolve, 100)); - } - if (isAlive(pid)) { - console.warn(\`MongoDB (PID \${pid}) did not exit within 10s; sending SIGKILL.\`); - try { - process.kill(pid, "SIGKILL"); - } catch { - // ignore - } - } - console.log(\`Stopped MongoDB (PID \${pid}).\`); - } else if (pid !== null) { - console.log("MongoDB was not running (stale PID file)."); - } else { - console.log("MongoDB is not running."); - } - rmSync(pidFile, { force: true }); - if (wipe) { - rmSync(dataRoot, { recursive: true, force: true }); - console.log(\`Removed \${dataRoot}.\`); - } -} - -const cmd = process.argv[2] ?? "up"; -switch (cmd) { - case "up": - await up(); - break; - case "down": - await down(false); - break; - case "reset": - await down(true); - break; - case "_run": - await runServer(); - break; - default: - console.error(\`Unknown command: \${cmd}. Use: up | down | reset\`); - process.exit(2); -} -`; - -function getMongoMemoryScripts(packageManager: PackageManager): Record { - switch (packageManager) { - case "bun": - return { - "db:up": "bun --env-file=.env scripts/mongo.mjs up", - "db:down": "bun --env-file=.env scripts/mongo.mjs down", - "db:reset": "bun --env-file=.env scripts/mongo.mjs reset", - }; - case "deno": - return { - "db:up": "deno run -A --env-file=.env scripts/mongo.mjs up", - "db:down": "deno run -A --env-file=.env scripts/mongo.mjs down", - "db:reset": "deno run -A --env-file=.env scripts/mongo.mjs reset", - }; - default: - return { - "db:up": "node --env-file=.env scripts/mongo.mjs up", - "db:down": "node --env-file=.env scripts/mongo.mjs down", - "db:reset": "node --env-file=.env scripts/mongo.mjs reset", - }; - } -} - -const requiredPrismaFileGroups = [ - ["src/prisma/contract.prisma", "src/prisma/contract.ts"], - ["prisma-next.config.ts"], - ["src/prisma/db.ts"], -] as const; - -function getContractPath(authoring: AuthoringStyle): string { - return `src/prisma/contract${authoring === "typescript" ? ".ts" : ".prisma"}`; -} - async function promptForDatabaseProvider(): Promise { const databaseProvider = await select({ message: "Select your database", initialValue: DEFAULT_DATABASE_PROVIDER, options: [ - { - value: "postgres", - label: "PostgreSQL", - hint: "Relational models with typed ORM, relations, indexes, raw SQL", - }, - { - value: "mongo", - label: "MongoDB", - hint: "Document models with typed ORM, indexes, aggregations", - }, + { value: "postgres", label: "PostgreSQL", hint: "Prisma Postgres with Composer" }, + { value: "mongo", label: "MongoDB", hint: "Connect an existing MongoDB database" }, ], }); - if (isCancel(databaseProvider)) { cancel("Operation cancelled."); - return undefined; + return; } - return DatabaseProviderSchema.parse(databaseProvider); } @@ -329,926 +74,281 @@ async function promptForAuthoringStyle(): Promise { message: "Choose contract authoring style", initialValue: DEFAULT_AUTHORING, options: [ - { value: "psl", label: "PSL", hint: "Schema syntax emits contract.json + types" }, - { - value: "typescript", - label: "TypeScript", - hint: "Builder API emits the same contract artifacts", - }, + { value: "psl", label: "PSL", hint: "Prisma schema syntax" }, + { value: "typescript", label: "TypeScript", hint: "TypeScript contract builder" }, ], }); - if (isCancel(authoring)) { cancel("Operation cancelled."); - return undefined; + return; } - return AuthoringStyleSchema.parse(authoring); } -async function promptForPrismaPostgres(): Promise { - const shouldUsePrismaPostgres = await confirm({ - message: "Provision a Prisma Postgres database?", - active: "Provision Prisma Postgres", - inactive: "Use my own database", - initialValue: DEFAULT_INTERACTIVE_PRISMA_POSTGRES, - }); - - if (isCancel(shouldUsePrismaPostgres)) { - cancel("Operation cancelled."); - return undefined; - } - - return Boolean(shouldUsePrismaPostgres); -} - -async function resolvePrismaPostgresChoice(options: { - explicitChoice?: boolean; - databaseProvider: DatabaseProvider; - databaseUrl?: string; - useDefaults: boolean; -}): Promise { - const { explicitChoice, databaseProvider, databaseUrl, useDefaults } = options; - - if (explicitChoice !== undefined) { - return explicitChoice; - } - - if (databaseProvider !== "postgres" || databaseUrl) { - return false; - } - - return useDefaults ? DEFAULT_AUTOMATED_PRISMA_POSTGRES : await promptForPrismaPostgres(); -} - -function getPackageManagerHint( - option: PackageManager, - detected: PackageManager, -): string | undefined { - const hintByPackageManager = { +function getPackageManagerHint(option: PackageManager, detected: PackageManager) { + const hints = { npm: "Node.js default", - pnpm: "Fast, disk-efficient Node.js package manager", + pnpm: "Fast, disk-efficient package manager", yarn: "Yarn package manager", - bun: "Fast runtime + package manager", - deno: "Deno runtime + task runner", + bun: "Fast runtime and package manager", } satisfies Record; - - const hint = hintByPackageManager[option]; - return option === detected ? `Detected; ${hint}` : hint; + return option === detected ? `Detected; ${hints[option]}` : hints[option]; } async function promptForPackageManager( - detectedPackageManager: PackageManager, + detected: PackageManager, ): Promise { const packageManager = await select({ message: "Choose package manager", - initialValue: detectedPackageManager, - options: [ - { - value: "npm", - label: "npm", - hint: getPackageManagerHint("npm", detectedPackageManager), - }, - { - value: "pnpm", - label: "pnpm", - hint: getPackageManagerHint("pnpm", detectedPackageManager), - }, - { - value: "yarn", - label: "yarn", - hint: getPackageManagerHint("yarn", detectedPackageManager), - }, - { - value: "bun", - label: "bun", - hint: getPackageManagerHint("bun", detectedPackageManager), - }, - { - value: "deno", - label: "deno", - hint: getPackageManagerHint("deno", detectedPackageManager), - }, - ], + initialValue: detected, + options: (["npm", "pnpm", "yarn", "bun"] as const).map((value) => ({ + value, + label: value, + hint: getPackageManagerHint(value, detected), + })), }); - if (isCancel(packageManager)) { cancel("Operation cancelled."); - return undefined; + return; } - return PackageManagerSchema.parse(packageManager); } -async function promptForDependencyInstall( - packageManager: PackageManager, -): Promise { - const installCommand = getInstallCommand(packageManager); - const shouldInstall = await confirm({ - message: `Install dependencies now with ${installCommand}? You can run it later.`, - active: "Install now", - inactive: "Skip for now", - initialValue: true, +async function promptForDeployment(): Promise { + const shouldDeploy = await confirm({ + message: "Deploy to Prisma now?", + initialValue: false, }); - - if (isCancel(shouldInstall)) { + if (isCancel(shouldDeploy)) { cancel("Operation cancelled."); - return undefined; - } - - return Boolean(shouldInstall); -} - -function getCommandErrorMessage(error: unknown): string { - if (error instanceof Error && "stderr" in error) { - const stderr = String((error as { stderr?: string }).stderr ?? "").trim(); - if (stderr.length > 0) { - return stderr; - } + return; } - - return error instanceof Error ? error.message : String(error); + return Boolean(shouldDeploy); } export async function collectPrismaSetupContext( input: PrismaSetupCommandInput, - options: { - projectDir?: string; - } = {}, + options: { projectDir?: string } = {}, ): Promise { const projectDir = path.resolve(options.projectDir ?? process.cwd()); const useDefaults = input.yes === true; - const verbose = input.verbose === true; - const shouldEmit = input.emit ?? DEFAULT_EMIT; - - let prismaNextSpec: ResolvedPrismaNextSpec; - try { - prismaNextSpec = parsePrismaNextVersionSpec(input.prismaNextVersion); - } catch (error) { - cancel(error instanceof Error ? error.message : String(error)); - return; - } const databaseProvider = input.provider ?? (useDefaults ? DEFAULT_DATABASE_PROVIDER : await promptForDatabaseProvider()); - if (!databaseProvider) { - return; - } - - const databaseUrl = input.databaseUrl; - const shouldUsePrismaPostgres = await resolvePrismaPostgresChoice({ - explicitChoice: input.prismaPostgres, - databaseProvider, - databaseUrl, - useDefaults, - }); - if (shouldUsePrismaPostgres === undefined) { - return; - } - - if (shouldUsePrismaPostgres && databaseProvider !== "postgres") { - cancel("--prisma-postgres is only supported with --provider postgres."); - return; - } - if (shouldUsePrismaPostgres && databaseUrl) { - cancel("Use either --database-url or --prisma-postgres, not both."); - return; - } + if (!databaseProvider) return; const authoring = input.authoring ?? (useDefaults ? DEFAULT_AUTHORING : await promptForAuthoringStyle()); - if (!authoring) { - return; - } + if (!authoring) return; const detectedPackageManager = await detectPackageManager(projectDir); const packageManager = input.packageManager ?? (useDefaults ? detectedPackageManager : await promptForPackageManager(detectedPackageManager)); - if (!packageManager) { - return; - } + if (!packageManager) return; - const shouldInstall = - input.install ?? - (useDefaults ? DEFAULT_INSTALL : await promptForDependencyInstall(packageManager)); - if (shouldInstall === undefined) { - return; - } + const shouldDeploy = input.deploy ?? (useDefaults ? false : await promptForDeployment()); + if (shouldDeploy === undefined) return; return { projectDir, - verbose, - shouldEmit, + verbose: input.verbose === true, databaseProvider, authoring, - databaseUrl, - shouldUsePrismaPostgres, packageManager, - shouldInstall, - prismaNextSpec, + shouldDeploy, }; } -function getDefaultDatabaseUrl(provider: DatabaseProvider): string { - switch (provider) { - case "postgres": - return "postgresql://user:password@localhost:5432/mydb"; - case "mongo": - return "mongodb://localhost:27017/mydb?replicaSet=rs0&directConnection=true"; - default: { - const exhaustiveCheck: never = provider; - throw new Error(`Unsupported Prisma Next target: ${String(exhaustiveCheck)}`); - } - } -} - -// Escape regex metacharacters before interpolating dynamic values into RegExp. -function escapeRegExp(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); -} - -function escapeEnvValue(value: string): string { - if (/[\r\n]/.test(value)) { - throw new Error("Environment variable values must be single-line."); - } - - return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); -} - -function hasEnvVar(content: string, envVarName: string): boolean { - const escapedName = escapeRegExp(envVarName); - return new RegExp(`(^|\\n)\\s*${escapedName}\\s*=`).test(content); -} - -function hasEnvComment(content: string, comment: string): boolean { - const escapedComment = escapeRegExp(comment); - return new RegExp(`(^|\\n)\\s*#\\s*${escapedComment}\\s*(?=\\n|$)`).test(content); -} - -async function ensureEnvVarInEnv( - projectDir: string, - envVarName: string, - envVarValue: string, - opts: { - mode: EnvWriteMode; - comment?: string; - }, -): Promise { - const envPath = path.join(projectDir, ".env"); - const envLine = `${envVarName}="${escapeEnvValue(envVarValue)}"`; - - if (!(await fs.pathExists(envPath))) { - const content = opts.comment ? `# ${opts.comment}\n${envLine}\n` : `${envLine}\n`; - await fs.writeFile(envPath, content, "utf8"); - return; - } - - const existingContent = await fs.readFile(envPath, "utf8"); - if (hasEnvVar(existingContent, envVarName)) { - if (opts.mode === "keep-existing") { - return; - } - - const escapedName = escapeRegExp(envVarName); - const lineRegex = new RegExp(`(^|\\n)\\s*${escapedName}\\s*=.*(?=\\n|$)`, "gm"); - const updatedContent = existingContent.replace(lineRegex, `$1${envLine}`); - if (updatedContent === existingContent) { - return; - } - - await fs.writeFile(envPath, updatedContent, "utf8"); - return; - } - - const separator = existingContent.endsWith("\n") ? "" : "\n"; - const commentLine = opts.comment ? `\n# ${opts.comment}\n` : "\n"; - const insertion = `${separator}${commentLine}${envLine}\n`; - await fs.appendFile(envPath, insertion, "utf8"); -} - -async function ensureEnvComment(projectDir: string, comment: string): Promise { - const envPath = path.join(projectDir, ".env"); - const commentLine = `# ${comment}`; - - if (!(await fs.pathExists(envPath))) { - await fs.writeFile(envPath, `${commentLine}\n`, "utf8"); - return; - } - - const existingContent = await fs.readFile(envPath, "utf8"); - if (hasEnvComment(existingContent, comment)) { - return; - } - - const separator = existingContent.endsWith("\n") ? "" : "\n"; - await fs.appendFile(envPath, `${separator}${commentLine}\n`, "utf8"); -} - -function hasGitignoreEntry(content: string, entry: string): boolean { - const escapedEntry = escapeRegExp(entry); - const escapedWithLeadingSlash = escapeRegExp(`/${entry}`); - const escapedWithTrailingSlash = escapeRegExp(`${entry}/`); - const escapedWithLeadingAndTrailingSlash = escapeRegExp(`/${entry}/`); - return new RegExp( - `(^|\\n)\\s*(?:${escapedEntry}|${escapedWithLeadingSlash}|${escapedWithTrailingSlash}|${escapedWithLeadingAndTrailingSlash})\\s*(?=\\n|$)`, - ).test(content); -} - -async function ensureGitignoreEntry(projectDir: string, entry: string): Promise { - const gitignorePath = path.join(projectDir, ".gitignore"); - - if (!(await fs.pathExists(gitignorePath))) { - await fs.writeFile(gitignorePath, `${entry}\n`, "utf8"); - return; - } - - const existingContent = await fs.readFile(gitignorePath, "utf8"); - if (hasGitignoreEntry(existingContent, entry)) { - return; - } - - const separator = existingContent.endsWith("\n") ? "" : "\n"; - await fs.appendFile(gitignorePath, `${separator}${entry}\n`, "utf8"); -} - -async function ensurePackageScripts( - projectDir: string, - scripts: Record, -): Promise { - const packageJsonPath = path.join(projectDir, "package.json"); - if (!(await fs.pathExists(packageJsonPath))) { - return; - } - - const packageJson = await fs.readJson(packageJsonPath); - if (!packageJson.scripts) { - packageJson.scripts = {}; - } - - let didChange = false; - for (const [scriptName, command] of Object.entries(scripts)) { - if ( - typeof packageJson.scripts[scriptName] !== "string" || - packageJson.scripts[scriptName].trim().length === 0 - ) { - packageJson.scripts[scriptName] = command; - didChange = true; - } - } - - if (didChange) { - await fs.writeJson(packageJsonPath, packageJson, { - spaces: 2, - }); - } -} - -async function ensureMongoMemoryServerScript(projectDir: string): Promise { - const scriptPath = path.join(projectDir, "scripts", "mongo.mjs"); - if (await fs.pathExists(scriptPath)) { - return; - } - - await fs.ensureDir(path.dirname(scriptPath)); - await fs.writeFile(scriptPath, MONGO_MEMORY_SERVER_SCRIPT, "utf8"); -} - -async function ensureMongoMemoryServerDevDependency(projectDir: string): Promise { - const packageJsonPath = path.join(projectDir, "package.json"); - if (!(await fs.pathExists(packageJsonPath))) { - return; - } - - const packageJson = await fs.readJson(packageJsonPath); - if (!packageJson.devDependencies) { - packageJson.devDependencies = {}; - } - - const memoryServerVersion = getDependencyVersion("mongodb-memory-server"); - if (packageJson.devDependencies["mongodb-memory-server"] === memoryServerVersion) { - return; - } - - packageJson.devDependencies["mongodb-memory-server"] = memoryServerVersion; - packageJson.devDependencies = Object.fromEntries( - Object.entries(packageJson.devDependencies as Record).sort(([a], [b]) => - a.localeCompare(b), - ), - ); - - await fs.writeJson(packageJsonPath, packageJson, { - spaces: 2, - }); -} - -async function writeMongoLocalHelpersForContext( - context: PrismaSetupContext, - projectDir: string, -): Promise { - if (context.databaseProvider !== "mongo" || context.databaseUrl) { - return true; - } - - try { - await ensureMongoMemoryServerScript(projectDir); - await ensureMongoMemoryServerDevDependency(projectDir); - await ensurePackageScripts(projectDir, getMongoMemoryScripts(context.packageManager)); - await ensureGitignoreEntry(projectDir, ".mongo-data"); - return true; - } catch (error) { - cancel(getCommandErrorMessage(error)); - return false; - } -} - -async function ensureRequiredPrismaFiles(projectDir: string): Promise { - const missingFiles: string[] = []; - - for (const candidates of requiredPrismaFileGroups) { - let foundCandidate = false; - - for (const relativePath of candidates) { - const absolutePath = path.join(projectDir, relativePath); - if (await fs.pathExists(absolutePath)) { - foundCandidate = true; - break; - } - } - - if (!foundCandidate) { - missingFiles.push(candidates.join(" or ")); - } - } - - if (missingFiles.length > 0) { - throw new Error(`Template is missing required Prisma Next files: ${missingFiles.join(", ")}`); - } -} - -async function finalizePrismaFiles(options: FinalizePrismaOptions): Promise { - const projectDir = options.projectDir ?? process.cwd(); - - await ensureRequiredPrismaFiles(projectDir); - - const databaseUrl = options.databaseUrl ?? getDefaultDatabaseUrl(options.provider); - await ensureEnvVarInEnv(projectDir, "DATABASE_URL", databaseUrl, { - mode: options.databaseUrl ? "upsert" : "keep-existing", - comment: "Added by create-prisma", - }); - - if (options.claimUrl) { - await ensureEnvVarInEnv(projectDir, "CLAIM_URL", options.claimUrl, { - mode: "upsert", - comment: PRISMA_POSTGRES_TEMPORARY_NOTICE, - }); - await ensureEnvComment(projectDir, PRISMA_POSTGRES_TEMPORARY_NOTICE); - } - - await ensureGitignoreEntry(projectDir, ".env"); -} - -async function provisionPrismaPostgresIfNeeded( - context: PrismaSetupContext, - projectDir: string, -): Promise { - if (!context.shouldUsePrismaPostgres) { - return { - databaseUrl: context.databaseUrl, - }; - } - - const createDbCommand = getCreateDbCommand(context.packageManager); - if (context.verbose) { - log.step(`Running ${createDbCommand}`); - } - - try { - const prismaPostgresResult = await provisionPrismaPostgres(context.packageManager, projectDir); - - if (context.verbose) { - log.success("Prisma Postgres database provisioned."); - } - return { - databaseUrl: prismaPostgresResult.databaseUrl, - claimUrl: prismaPostgresResult.claimUrl, - }; - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - - return { - databaseUrl: context.databaseUrl, - warning: `Prisma Postgres provisioning failed: ${errorMessage}`, - }; - } -} - -async function writeDependenciesForContext( - context: PrismaSetupContext, - projectDir: string, -): Promise { - try { - await writePrismaDependencies( - context.databaseProvider, - context.packageManager, - context.authoring, - projectDir, - context.prismaNextSpec, - ); - return true; - } catch (error) { - cancel(getCommandErrorMessage(error)); - return false; +function getCommandErrorMessage(error: unknown): string { + if (error instanceof Error && "stderr" in error) { + const stderr = String((error as { stderr?: string }).stderr ?? "").trim(); + if (stderr) return stderr; } + return error instanceof Error ? error.message : String(error); } -function getPrismaNextCliPackageSpecifier( - prismaNextSpec: ResolvedPrismaNextSpec = DEFAULT_PRISMA_NEXT_SPEC, -): string { - return getPrismaNextPackageSpecifier("prisma-next", prismaNextSpec); +function getContractPath(authoring: AuthoringStyle) { + return `src/prisma/contract${authoring === "typescript" ? ".ts" : ".prisma"}`; } -function getPrismaNextInitTarget(provider: DatabaseProvider): "mongodb" | "postgres" { +function getInitTarget(provider: DatabaseProvider): "postgres" | "mongodb" { return provider === "mongo" ? "mongodb" : "postgres"; } -function getPrismaNextInitCliArgs( - packageManager: PackageManager, - prismaNextArgs: string[], - prismaNextSpec: ResolvedPrismaNextSpec = DEFAULT_PRISMA_NEXT_SPEC, -): { command: string; args: string[] } { - if (packageManager === "npm") { - return { - command: "npx", - args: ["--yes", getPrismaNextCliPackageSpecifier(prismaNextSpec), "init", ...prismaNextArgs], - }; - } - - return getPackageExecutionArgs(packageManager, [ - getPrismaNextCliPackageSpecifier(prismaNextSpec), - "init", - ...prismaNextArgs, - ]); -} - -function getPrismaNextInitCliCommand( - packageManager: PackageManager, - prismaNextArgs: string[], - prismaNextSpec: ResolvedPrismaNextSpec = DEFAULT_PRISMA_NEXT_SPEC, -): string { - const execution = getPrismaNextInitCliArgs(packageManager, prismaNextArgs, prismaNextSpec); - return [execution.command, ...execution.args].join(" "); +function getPrismaCliInvocation(packageManager: PackageManager, args: string[]) { + return getPackageExecutionArgs(packageManager, [PRISMA_CLI_PACKAGE, ...args]); } -async function runPrismaNextInitForContext( - context: PrismaSetupContext, - projectDir: string, -): Promise { - const initArgs = [ +async function runPrismaInit(context: PrismaSetupContext, projectDir: string): Promise { + const args = [ + "orm", + "init", "--yes", - "--force", + "--no-interactive", "--target", - getPrismaNextInitTarget(context.databaseProvider), + getInitTarget(context.databaseProvider), "--authoring", context.authoring, "--schema-path", getContractPath(context.authoring), - "--no-install", - "--no-skill", + "--skip-install", + "--skip-skills", ]; - const initCommand = getPrismaNextInitCliCommand( - context.packageManager, - initArgs, - context.prismaNextSpec, - ); - - if (context.verbose) { - log.step(`Running ${initCommand}`); - } - - try { - const initExecution = getPrismaNextInitCliArgs( - context.packageManager, - initArgs, - context.prismaNextSpec, - ); - await execa(initExecution.command, initExecution.args, { - cwd: projectDir, - stdio: context.verbose ? "inherit" : "pipe", - env: { - ...process.env, - CI: "1", - }, - }); - - if (context.verbose) { - log.success("Prisma Next project files ready."); - } - return true; - } catch (error) { - if (context.verbose) { - log.warn("Could not run Prisma Next init."); - } - cancel(`Failed to run ${initCommand}: ${getCommandErrorMessage(error)}`); - return false; - } + const invocation = getPrismaCliInvocation(context.packageManager, args); + if (context.verbose) log.step(`Running ${[invocation.command, ...invocation.args].join(" ")}`); + await execa(invocation.command, invocation.args, { + cwd: projectDir, + stdio: context.verbose ? "inherit" : "pipe", + env: { ...process.env, CI: "1" }, + }); } -async function installDependenciesForContext( - context: PrismaSetupContext, - projectDir: string, -): Promise { - if (!context.shouldInstall) { - return true; - } - - const installCommand = getInstallCommand(context.packageManager); - if (context.verbose) { - log.step(`Running ${installCommand}`); - } - - try { - await installProjectDependencies(context.packageManager, projectDir, { - verbose: context.verbose, - }); - if (context.verbose) { - log.success("Dependencies installed."); - } - return true; - } catch (error) { - cancel(`Failed to run ${installCommand}: ${getCommandErrorMessage(error)}`); - return false; - } +async function ensureGitignoreEntry(projectDir: string, entry: string): Promise { + const gitignorePath = path.join(projectDir, ".gitignore"); + const existing = (await fs.pathExists(gitignorePath)) + ? await fs.readFile(gitignorePath, "utf8") + : ""; + const lines = existing.split(/\r?\n/).map((line) => line.trim()); + if (lines.includes(entry) || lines.includes(`/${entry}`)) return; + const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n" : ""; + await fs.writeFile(gitignorePath, `${existing}${separator}${entry}\n`, "utf8"); } -async function finalizePrismaFilesForContext( - context: PrismaSetupContext, - projectDir: string, - provisionResult: PrismaPostgresProvisionResult, -): Promise { - try { - await finalizePrismaFiles({ - provider: context.databaseProvider, - databaseUrl: provisionResult.databaseUrl, - claimUrl: provisionResult.claimUrl, - projectDir, - }); - - if (context.verbose) { - log.success("Prisma Next environment configured."); - } - return true; - } catch (error) { - cancel(getCommandErrorMessage(error)); - return false; +async function ensureMongoEnvironment(projectDir: string): Promise { + const envPath = path.join(projectDir, ".env"); + if (!(await fs.pathExists(envPath))) { + await fs.writeFile( + envPath, + 'DATABASE_URL="mongodb://localhost:27017/mydb?replicaSet=rs0&directConnection=true"\n', + "utf8", + ); } + await ensureGitignoreEntry(projectDir, ".env"); } -function getPrismaNextCliCommand(packageManager: PackageManager, prismaNextArgs: string[]): string { - if (packageManager === "deno") { - return `deno run -A --env-file=.env ${getDenoPrismaSpecifier()} ${prismaNextArgs.join(" ")}`; +async function ensureComposerTypeScriptOptions(projectDir: string): Promise { + const tsconfigPath = path.join(projectDir, "tsconfig.json"); + const tsconfig = await fs.readFile(tsconfigPath, "utf8"); + const additions: string[] = []; + if (!/"allowImportingTsExtensions"\s*:/.test(tsconfig)) { + additions.push(' "allowImportingTsExtensions": true,'); } - - return getLocalPackageBinaryCommand(packageManager, "prisma-next", prismaNextArgs); -} - -function getPrismaNextCliArgs( - packageManager: PackageManager, - prismaNextArgs: string[], -): { command: string; args: string[] } { - if (packageManager === "deno") { - return { - command: "deno", - args: ["run", "-A", "--env-file=.env", getDenoPrismaSpecifier(), ...prismaNextArgs], - }; + if (!/"noEmit"\s*:/.test(tsconfig)) { + additions.push(' "noEmit": true,'); } + if (additions.length === 0) return; - return getLocalPackageBinaryArgs(packageManager, "prisma-next", prismaNextArgs); -} - -async function emitPrismaNextContractForContext( - context: PrismaSetupContext, - projectDir: string, -): Promise { - if (!context.shouldEmit) { - return { - didEmitContract: false, - }; - } - if (!context.shouldInstall) { - return { - didEmitContract: false, - warning: "Skipped contract emit because dependencies were not installed.", - }; + const updated = tsconfig.replace( + /"compilerOptions"\s*:\s*\{/, + (match) => `${match}\n${additions.join("\n")}`, + ); + if (updated === tsconfig) { + throw new Error("tsconfig.json is missing compilerOptions."); } + await fs.writeFile(tsconfigPath, updated, "utf8"); +} - const emitCommand = getPrismaNextCliCommand(context.packageManager, ["contract", "emit"]); +async function emitContract(context: PrismaSetupContext, projectDir: string): Promise { + const args = getLocalPackageBinaryArgs(context.packageManager, "prisma-next", [ + "contract", + "emit", + ]); if (context.verbose) { - log.step(`Running ${emitCommand}`); - } - - try { - const emitArgs = getPrismaNextCliArgs(context.packageManager, ["contract", "emit"]); - await execa(emitArgs.command, emitArgs.args, { - cwd: projectDir, - stdio: context.verbose ? "inherit" : "pipe", - }); - if (context.verbose) { - log.success("Prisma Next contract artifacts emitted."); - } - - return { - didEmitContract: true, - }; - } catch (error) { - if (context.verbose) { - log.warn("Could not emit Prisma Next contract."); - } - - return { - didEmitContract: false, - warning: `Contract emit failed: ${getCommandErrorMessage(error)}`, - }; + log.step( + getLocalPackageBinaryCommand(context.packageManager, "prisma-next", ["contract", "emit"]), + ); } + await execa(args.command, args.args, { + cwd: projectDir, + stdio: context.verbose ? "inherit" : "pipe", + }); } -function buildWarningLines( - provisionWarning: string | undefined, - emitWarning: string | undefined, -): string[] { - const warningLines: string[] = []; - - if (provisionWarning) { - warningLines.push(`- ${provisionWarning}`); - } - if (emitWarning) { - warningLines.push(`- ${emitWarning}`); - } - - return warningLines; +function formatNextSteps(steps: NextStep[]): string { + return steps.map((step) => `${step.command}\n ${step.description}`).join("\n\n"); } -function buildNextStepsForContext(opts: { - context: PrismaSetupContext; - options: PrismaSetupRunOptions; - didEmitContract: boolean; -}): NextStep[] { - const { context, options, didEmitContract } = opts; - const nextSteps: NextStep[] = [...(options.prependNextSteps ?? [])]; - - if (!context.shouldInstall) { - nextSteps.push({ - command: getInstallCommand(context.packageManager), - description: "Install the project dependencies.", - }); - } - if (!didEmitContract || !context.shouldEmit) { - nextSteps.push({ - command: getRunScriptCommand(context.packageManager, "contract:emit"), - description: "Emit contract.json and TypeScript types from your Prisma Next contract.", - }); - } - if (context.databaseProvider === "postgres") { +function buildNextSteps(context: PrismaSetupContext, options: PrismaSetupRunOptions): NextStep[] { + const nextSteps = [...(options.prependNextSteps ?? [])]; + if (context.databaseProvider === "mongo") { nextSteps.push({ - command: getRunScriptCommand(context.packageManager, "db:init"), - description: "Create the initial PostgreSQL database objects and sign the database.", + command: "Set MONGODB_URL in your environment", + description: "Composer uses this secret when deploying the MongoDB template.", }); } - if (context.databaseProvider === "mongo" && !context.databaseUrl) { + if (options.includeDevNextStep) { nextSteps.push({ - command: getRunScriptCommand(context.packageManager, "db:up"), - description: - "Start the local MongoDB replica set with mongodb-memory-server. Stop with `db:down`, wipe with `db:reset`.", + command: getRunScriptCommand(context.packageManager, "dev:composer"), + description: "Build and start the app with Prisma Composer locally.", }); } nextSteps.push({ - command: getRunScriptCommand(context.packageManager, "migration:plan"), - description: "Compare the contract to the database and write a migration plan.", - }); - nextSteps.push({ - command: getRunScriptCommand(context.packageManager, "migrate"), - description: "Apply the planned migration to the database.", - }); - nextSteps.push({ - command: getRunScriptCommand(context.packageManager, "db:seed"), - description: "Insert the sample users from src/prisma/seed.ts.", + command: getRunScriptCommand(context.packageManager, "deploy"), + description: "Build and deploy the app with Prisma Composer.", }); - if (options.includeDevNextStep) { - nextSteps.push({ - command: getRunScriptCommand(context.packageManager, "dev"), - description: "Start the development server.", - }); - } - return nextSteps; } -function formatNextSteps(nextSteps: NextStep[]): string { - return nextSteps.map((step) => `${step.command}\n ${step.description}`).join("\n\n"); -} - -function formatAgentPrompt(): string { - return [ - "Ask your agent:", - "What can I do with Prisma Next?", - "", - "Learn more:", - `Docs: prisma-next.md`, - "Skills: https://github.com/prisma/prisma-next/tree/main/skills", - ].join("\n"); -} - export async function executePrismaSetupContext( context: PrismaSetupContext, options: PrismaSetupRunOptions = {}, ): Promise { const projectDir = path.resolve(options.projectDir ?? context.projectDir); - const progressSpinner = context.verbose ? undefined : (options.progressSpinner ?? spinner()); - const ownsProgressSpinner = progressSpinner !== undefined && !options.progressSpinner; - - if (ownsProgressSpinner) { - progressSpinner.start("Creating Prisma Next project..."); - } - - const stopProgressOnFailure = () => { - progressSpinner?.stop("Could not create Prisma Next project."); - }; - - if (context.shouldUsePrismaPostgres) { - progressSpinner?.message("Provisioning Prisma Postgres..."); - } - const provisionResult = await provisionPrismaPostgresIfNeeded(context, projectDir); - if (!provisionResult) { - stopProgressOnFailure(); - return false; - } - - progressSpinner?.message("Preparing Prisma Next project files..."); - const didRunPrismaNextInit = await runPrismaNextInitForContext(context, projectDir); - if (!didRunPrismaNextInit) { - stopProgressOnFailure(); - return false; - } + const projectName = options.projectName ?? path.basename(projectDir); + const template = options.template ?? "minimal"; + const progress = context.verbose ? undefined : (options.progressSpinner ?? spinner()); + const ownsProgress = progress !== undefined && !options.progressSpinner; + if (ownsProgress) progress.start("Creating Prisma Next project..."); - const didWriteDependencies = await writeDependenciesForContext(context, projectDir); - if (!didWriteDependencies) { - stopProgressOnFailure(); - return false; - } + try { + progress?.message("Preparing Prisma Next project files..."); + await runPrismaInit(context, projectDir); - const didWriteMongoLocalHelpers = await writeMongoLocalHelpersForContext(context, projectDir); - if (!didWriteMongoLocalHelpers) { - stopProgressOnFailure(); - return false; - } + await scaffoldCreateSharedTemplates({ + projectDir, + projectName, + template, + provider: context.databaseProvider, + authoring: context.authoring, + packageManager: context.packageManager, + }); + await writePrismaDependencies( + context.databaseProvider, + context.packageManager, + context.authoring, + projectDir, + ); + await ensureComposerTypeScriptOptions(projectDir); + if (context.databaseProvider === "mongo") await ensureMongoEnvironment(projectDir); - if (context.shouldInstall) { - progressSpinner?.message("Installing dependencies..."); - } - const dependenciesInstalled = await installDependenciesForContext(context, projectDir); - if (!dependenciesInstalled) { - stopProgressOnFailure(); - return false; - } + progress?.message( + `Installing dependencies with ${getInstallCommand(context.packageManager)}...`, + ); + await installProjectDependencies(context.packageManager, projectDir, { + verbose: context.verbose, + }); - progressSpinner?.message("Configuring Prisma Next..."); - const didFinalizePrismaFiles = await finalizePrismaFilesForContext( - context, - projectDir, - provisionResult, - ); - if (!didFinalizePrismaFiles) { - stopProgressOnFailure(); + progress?.message("Generating Prisma Next contract artifacts..."); + await emitContract(context, projectDir); + progress?.stop("Prisma Next project ready."); + } catch (error) { + progress?.stop("Could not create Prisma Next project."); + cancel(getCommandErrorMessage(error)); return false; } - if (context.shouldEmit && context.shouldInstall) { - progressSpinner?.message("Emitting Prisma Next contract artifacts..."); - } - const emitResult = await emitPrismaNextContractForContext(context, projectDir); - - const warningLines = buildWarningLines(provisionResult.warning, emitResult.warning); - const nextSteps = buildNextStepsForContext({ - context, - options, - didEmitContract: emitResult.didEmitContract, - }); - - progressSpinner?.stop("Prisma Next project ready."); - - if (warningLines.length > 0) { - note(warningLines.map((line) => line.replace(/^- /, "")).join("\n"), "Heads up"); - } - - if (options.createdProjectPath) { - note(path.resolve(options.createdProjectPath), "Project path"); - } - - note(formatAgentPrompt(), "Agent prompt"); - if (context.verbose) { - note(formatNextSteps(nextSteps), "Next steps for Prisma Next"); + if (context.shouldDeploy) { + const didDeploy = await deployWithComposer({ + packageManager: context.packageManager, + projectDir, + verbose: context.verbose, + }); + if (!didDeploy) return false; } - outro("Prisma Next setup complete."); + if (options.createdProjectPath) note(path.resolve(options.createdProjectPath), "Project path"); + note(formatNextSteps(buildNextSteps(context, options)), "Next steps"); + outro(context.shouldDeploy ? "Prisma Next app deployed." : "Prisma Next setup complete."); return true; } diff --git a/src/telemetry/create.ts b/src/telemetry/create.ts index efee005..dac7a42 100644 --- a/src/telemetry/create.ts +++ b/src/telemetry/create.ts @@ -1,44 +1,8 @@ import type { CreatePromptContext } from "../commands/create"; -import { - DEFAULT_PRISMA_NEXT_SPEC, - PRISMA_NEXT_DEFAULT_VERSION, - type ResolvedPrismaNextSpec, -} from "../constants/dependencies"; import type { CreateCommandInput } from "../types"; import { trackCliTelemetry } from "./client"; -export type PrismaNextVersionKind = "default" | "npm-tag" | "npm-version" | "pkg-pr-new"; - -function classifyPrismaNextSpec(spec: ResolvedPrismaNextSpec | undefined): PrismaNextVersionKind { - if (!spec || spec === DEFAULT_PRISMA_NEXT_SPEC) { - return "default"; - } - - if (spec.kind === "pkg-pr-new") { - return "pkg-pr-new"; - } - - if (spec.spec === PRISMA_NEXT_DEFAULT_VERSION) { - return "default"; - } - - // npm dist-tags are sequences of lowercase letters / digits / hyphens that - // don't start with a digit; semver releases always start with a digit. This - // is intentionally a coarse classifier — npm itself accepts anything as a - // tag, but tags collected over the wire are useful as a low-cardinality - // signal for the onboarding audit. - return /^[0-9]/.test(spec.spec) ? "npm-version" : "npm-tag"; -} - -function getPrismaNextVersionSpecString(spec: ResolvedPrismaNextSpec | undefined): string | null { - if (!spec) { - return null; - } - - return spec.kind === "pkg-pr-new" ? `pkg-pr-new:${spec.ref}` : spec.spec; -} - export const CREATE_PRISMA_NEXT_COMPLETED_EVENT = "cli:create_prisma_next_command_completed"; export const CREATE_PRISMA_NEXT_FAILED_EVENT = "cli:create_prisma_next_command_failed"; @@ -65,8 +29,6 @@ function getBaseCreateProperties( input: CreateCommandInput, context?: CreatePromptContext, ): Record { - const resolvedPrismaNextSpec = context?.prismaSetupContext.prismaNextSpec; - return { command: "create", "uses-defaults": input.yes === true, @@ -76,14 +38,8 @@ function getBaseCreateProperties( "database-provider": context?.prismaSetupContext.databaseProvider ?? input.provider ?? null, "authoring-style": context?.prismaSetupContext.authoring ?? input.authoring ?? null, "package-manager": context?.prismaSetupContext.packageManager ?? input.packageManager ?? null, - "should-install": context?.prismaSetupContext.shouldInstall ?? input.install ?? null, - "should-emit": context?.prismaSetupContext.shouldEmit ?? input.emit ?? null, - "uses-prisma-postgres": - context?.prismaSetupContext.shouldUsePrismaPostgres ?? input.prismaPostgres ?? null, + "should-deploy": context?.prismaSetupContext.shouldDeploy ?? input.deploy ?? null, "target-directory-state": context ? getTargetDirectoryState(context) : null, - "prisma-next-version-kind": classifyPrismaNextSpec(resolvedPrismaNextSpec), - "prisma-next-version-spec": - getPrismaNextVersionSpecString(resolvedPrismaNextSpec) ?? input.prismaNextVersion ?? null, }; } diff --git a/src/templates/render-create-template.ts b/src/templates/render-create-template.ts index 6b6315e..fe4cd98 100644 --- a/src/templates/render-create-template.ts +++ b/src/templates/render-create-template.ts @@ -33,7 +33,7 @@ function createTemplateContext( }; } -export async function scaffoldCreateTemplate(opts: { +export async function scaffoldCreateSharedTemplates(opts: { projectDir: string; projectName: string; template: CreateTemplate; @@ -42,14 +42,36 @@ export async function scaffoldCreateTemplate(opts: { packageManager?: PackageManager; }): Promise { const { projectDir, projectName, template, provider, authoring, packageManager } = opts; - const templateRoot = getCreateTemplateDir(template); - const sharedTemplateRoot = getCreateSharedTemplateDir(); - const context = createTemplateContext(projectName, template, provider, authoring, packageManager); await renderTemplateTree({ - templateRoot: sharedTemplateRoot, + templateRoot: getCreateSharedTemplateDir(), outputDir: projectDir, - context, + context: createTemplateContext(projectName, template, provider, authoring, packageManager), }); +} + +export async function scaffoldCreateTemplate(opts: { + projectDir: string; + projectName: string; + template: CreateTemplate; + provider: DatabaseProvider; + authoring: AuthoringStyle; + packageManager?: PackageManager; +}): Promise { + await scaffoldCreateFrameworkTemplate(opts); + await scaffoldCreateSharedTemplates(opts); +} + +export async function scaffoldCreateFrameworkTemplate(opts: { + projectDir: string; + projectName: string; + template: CreateTemplate; + provider: DatabaseProvider; + authoring: AuthoringStyle; + packageManager?: PackageManager; +}): Promise { + const { projectDir, projectName, template, provider, authoring, packageManager } = opts; + const templateRoot = getCreateTemplateDir(template); + const context = createTemplateContext(projectName, template, provider, authoring, packageManager); await renderTemplateTree({ templateRoot, outputDir: projectDir, diff --git a/src/templates/shared.ts b/src/templates/shared.ts index a758078..cb074a8 100644 --- a/src/templates/shared.ts +++ b/src/templates/shared.ts @@ -12,18 +12,6 @@ import { } from "../utils/package-manager"; import { requiresDotenvConfigImport } from "../utils/runtime"; -function getOptionalHashString( - hash: Handlebars.HelperOptions["hash"], - key: string, -): string | undefined { - const value = hash[key]; - return typeof value === "string" && value.length > 0 ? value : undefined; -} - -function getOptionalHashStringList(hash: Handlebars.HelperOptions["hash"], key: string): string[] { - return getOptionalHashString(hash, key)?.split(" ") ?? []; -} - Handlebars.registerHelper("eq", (left: unknown, right: unknown) => left === right); Handlebars.registerHelper( "runScriptCommand", @@ -46,17 +34,14 @@ Handlebars.registerHelper( kind: "dev" | "build" | "start", sourceEntrypoint: string, builtEntrypoint: string | undefined, - options: Handlebars.HelperOptions, + _options: Handlebars.HelperOptions, ) => { if (!packageManager) { return ""; } - const hash = options.hash; - return getRuntimeScriptCommand(packageManager, kind, { sourceEntrypoint, builtEntrypoint, - denoFlags: getOptionalHashStringList(hash, "denoFlags"), }); }, ); diff --git a/src/types.ts b/src/types.ts index 853c17e..27be8db 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,7 +3,7 @@ import { z } from "zod"; export const databaseProviders = ["postgres", "mongo"] as const; export const databaseProviderInputs = ["postgres", "postgresql", "mongo", "mongodb"] as const; -export const packageManagers = ["npm", "pnpm", "yarn", "bun", "deno"] as const; +export const packageManagers = ["npm", "pnpm", "yarn", "bun"] as const; export const authoringStyles = ["psl", "typescript"] as const; export const createTemplates = [ "minimal", @@ -42,8 +42,6 @@ export type AuthoringStyle = z.infer; export const CreateTemplateSchema = z.enum(createTemplates); export type CreateTemplate = z.infer; -export const DatabaseUrlSchema = z.string().trim().min(1, "Please enter a valid database URL"); - export const CommonCommandOptionsSchema = z.object({ yes: z.boolean().optional().describe("Skip prompts and accept default choices"), verbose: z.boolean().optional().describe("Show verbose command output during setup"), @@ -57,21 +55,7 @@ export const PrismaSetupOptionsSchema = z.object({ packageManager: PackageManagerSchema.optional().describe( "Package manager used for dependency installation", ), - prismaPostgres: z - .boolean() - .optional() - .describe("Provision Prisma Postgres with create-db when target is postgres"), - databaseUrl: DatabaseUrlSchema.optional().describe("DATABASE_URL value"), - install: z.boolean().optional().describe("Install dependencies with selected package manager"), - emit: z.boolean().optional().describe("Emit Prisma Next contract artifacts after scaffolding"), - prismaNextVersion: z - .string() - .trim() - .min(1) - .optional() - .describe( - "Prisma Next version, npm dist-tag, or 'pkg-pr-new:' (default: latest)", - ), + deploy: z.boolean().optional().describe("Deploy the generated app to Prisma immediately"), }); export const PrismaSetupCommandInputSchema = CommonCommandOptionsSchema.extend( diff --git a/src/utils/node-version.ts b/src/utils/node-version.ts new file mode 100644 index 0000000..dbdf2f3 --- /dev/null +++ b/src/utils/node-version.ts @@ -0,0 +1,23 @@ +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("."); + return [Number(major), Number(minor), Number.parseInt(patch, 10)]; +} + +export function supportsPrismaNext(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; + } + return true; +} + +export function getUnsupportedNodeMessage(nodeVersion = process.versions.node): string { + return [ + `Node.js ${nodeVersion} is unsupported by create-prisma@next.`, + "Required: Node.js 22.18 or newer.", + "Update Node.js and run the command again.", + ].join("\n"); +} diff --git a/src/utils/package-manager.ts b/src/utils/package-manager.ts index d873bae..4f60d70 100644 --- a/src/utils/package-manager.ts +++ b/src/utils/package-manager.ts @@ -12,12 +12,11 @@ type RuntimeScriptKind = "dev" | "build" | "start"; type RuntimeScriptOptions = { sourceEntrypoint: string; builtEntrypoint?: string; - denoFlags?: string[]; }; const packageManagerManifestValues = { npm: "npm@10.9.0", - pnpm: "pnpm@10.16.1", + pnpm: "pnpm@11.21.0", yarn: "yarn@4.13.0", bun: "bun@1.3.9", } as const; @@ -35,10 +34,6 @@ function parseUserAgent(userAgent: string | undefined): PackageManager | null { return "bun"; } - if (userAgent?.startsWith("deno")) { - return "deno"; - } - if (userAgent?.startsWith("npm")) { return "npm"; } @@ -66,18 +61,6 @@ async function detectFromPackageJson(projectDir: string): Promise { - const configCandidates = ["deno.json", "deno.jsonc"]; - - for (const configFile of configCandidates) { - if (await fs.pathExists(path.join(projectDir, configFile))) { - return "deno"; - } - } - - return null; -} - async function detectFromLockfile(projectDir: string): Promise { const lockfileChecks: Array<{ manager: PackageManager; lockfile: string }> = [ { manager: "pnpm", lockfile: "pnpm-lock.yaml" }, @@ -86,7 +69,6 @@ async function detectFromLockfile(projectDir: string): Promise): string { - return parts.filter((part) => typeof part === "string" && part.length > 0).join(" "); -} - export function getRuntimeScriptCommand( packageManager: PackageManager, kind: RuntimeScriptKind, options: RuntimeScriptOptions, ): string { - const { sourceEntrypoint, builtEntrypoint, denoFlags = [] } = options; - - if (packageManager === "deno") { - switch (kind) { - case "dev": - return joinCommandParts([ - "deno", - "run", - "-A", - "--env-file=.env", - ...denoFlags, - "--watch", - sourceEntrypoint, - ]); - case "build": - return `deno check ${sourceEntrypoint}`; - case "start": - return joinCommandParts([ - "deno", - "run", - "-A", - "--env-file=.env", - ...denoFlags, - sourceEntrypoint, - ]); - } - } + const { sourceEntrypoint, builtEntrypoint } = options; if (packageManager === "bun") { switch (kind) { @@ -232,13 +156,6 @@ export function getRuntimeScriptCommand( } export function getInstallArgs(packageManager: PackageManager): CommandAndArgs { - if (packageManager === "deno") { - return { - command: "deno", - args: ["install", `--allow-scripts=${getDenoAllowedScriptSpecifiers()}`], - }; - } - return { command: packageManager, args: ["install"], @@ -256,20 +173,9 @@ export function getPackageExecutionArgs( return { command: "yarn", args: ["dlx", ...commandArgs] }; case "bun": return { command: "bunx", args: [...commandArgs] }; - case "deno": { - const [packageName, ...args] = commandArgs; - if (!packageName) { - throw new Error("Package execution requires a package name."); - } - - return { - command: "deno", - args: ["run", "-A", getDenoNpmSpecifier(packageName), ...args], - }; - } case "npm": default: - return { command: "npx", args: [...commandArgs] }; + return { command: "npx", args: ["--yes", ...commandArgs] }; } } @@ -293,8 +199,6 @@ export function getLocalPackageBinaryArgs( return { command: "yarn", args: [binaryName, ...binaryArgs] }; case "bun": return { command: "bun", args: [binaryName, ...binaryArgs] }; - case "deno": - return { command: "deno", args: ["run", "-A", `npm:${binaryName}`, ...binaryArgs] }; case "npm": default: return { command: "npm", args: ["exec", binaryName, "--", ...binaryArgs] }; @@ -318,16 +222,26 @@ export function getPrismaCliArgs( return getPackageExecutionArgs(packageManager, ["--bun", "prisma", ...prismaArgs]); } - if (packageManager === "deno") { - return { - command: "deno", - args: ["run", "-A", "--env-file=.env", getDenoPrismaSpecifier(), ...prismaArgs], - }; - } - return getPackageExecutionArgs(packageManager, ["prisma", ...prismaArgs]); } +export function getRunScriptArgs( + packageManager: PackageManager, + scriptName: string, +): CommandAndArgs { + switch (packageManager) { + case "bun": + return { command: "bun", args: ["run", scriptName] }; + case "pnpm": + return { command: "pnpm", args: ["run", scriptName] }; + case "yarn": + return { command: "yarn", args: ["run", scriptName] }; + case "npm": + default: + return { command: "npm", args: ["run", scriptName] }; + } +} + export function getPrismaCliCommand(packageManager: PackageManager, prismaArgs: string[]): string { const execution = getPrismaCliArgs(packageManager, prismaArgs); return [execution.command, ...execution.args].join(" "); diff --git a/src/utils/runtime.ts b/src/utils/runtime.ts index edd7e0b..5d92c9f 100644 --- a/src/utils/runtime.ts +++ b/src/utils/runtime.ts @@ -1,7 +1,7 @@ import type { PackageManager } from "../types"; export function usesNodeStyleRuntime(packageManager: PackageManager | undefined): boolean { - return packageManager !== undefined && packageManager !== "bun" && packageManager !== "deno"; + return packageManager !== undefined && packageManager !== "bun"; } export function requiresDotenvConfigImport(packageManager: PackageManager | undefined): boolean { @@ -11,5 +11,5 @@ export function requiresDotenvConfigImport(packageManager: PackageManager | unde export function requiresPrismaConfigDotenvImport( packageManager: PackageManager | undefined, ): boolean { - return packageManager !== "deno"; + return packageManager !== undefined; } diff --git a/templates/create/_shared/README.md.hbs b/templates/create/_shared/README.md.hbs new file mode 100644 index 0000000..0225df7 --- /dev/null +++ b/templates/create/_shared/README.md.hbs @@ -0,0 +1,38 @@ +# {{projectName}} + +A minimal {{template}} app with Prisma Next and Prisma Composer. + +## Run locally + +```bash +{{runScriptCommand packageManager "dev:composer"}} +``` + +This builds the app and starts it with Composer. PostgreSQL projects get a local Prisma Postgres database and apply the contract automatically. + +## Deploy + +```bash +{{runScriptCommand packageManager "deploy"}} +``` + +The deploy script builds the framework output, provisions Prisma Postgres when selected, applies migrations, and deploys the app to Prisma Compute. + +{{#if (eq provider "mongo")}} +MongoDB is not provisioned by Composer. Set `MONGODB_URL` before running Composer locally or deploying. +{{/if}} + +## Prisma + +- Contract: `src/prisma/contract{{#if (eq authoring "typescript")}}.ts{{else}}.prisma{{/if}}` +- Prisma Next config: `prisma-next.config.ts` +- Universal CLI config: `prisma.config.ts` +- Composer app: `module.ts` and `service.ts` + +After changing the contract, run: + +```bash +{{runScriptCommand packageManager "contract:emit"}} +``` + +To use the framework's development server directly, run `{{runScriptCommand packageManager "dev"}}`. This direct mode requires `DATABASE_URL`. diff --git a/templates/create/_shared/module.ts.hbs b/templates/create/_shared/module.ts.hbs new file mode 100644 index 0000000..e86615b --- /dev/null +++ b/templates/create/_shared/module.ts.hbs @@ -0,0 +1,28 @@ +import { module } from "@prisma/composer"; +{{#if (eq provider "postgres")}} +import { pnPostgres } from "@prisma/composer-prisma-cloud/prisma-next"; + +import { appContract } from "./src/prisma/composer.ts"; +{{else}} +import { envSecret } from "@prisma/composer-prisma-cloud"; +{{/if}} +import app from "./service.ts"; + +export default module("{{projectName}}", ({ provision }) => { +{{#if (eq provider "postgres")}} + const database = provision( + pnPostgres({ + name: "database", + contract: appContract, + config: "./prisma-next.config.ts", + }), + { id: "database" }, + ); + + provision(app, { deps: { database } }); +{{else}} + provision(app, { + input: { databaseUrl: envSecret("MONGODB_URL") }, + }); +{{/if}} +}); diff --git a/templates/create/_shared/pnpm-workspace.yaml.hbs b/templates/create/_shared/pnpm-workspace.yaml.hbs new file mode 100644 index 0000000..c728565 --- /dev/null +++ b/templates/create/_shared/pnpm-workspace.yaml.hbs @@ -0,0 +1,11 @@ +{{#if (eq packageManager "pnpm")}} +allowBuilds: + esbuild: true + msgpackr-extract: true + workerd: true +minimumReleaseAgeExclude: + - "@prisma/*" + - "prisma-next" +overrides: + effect: "4.0.0-beta.103" +{{/if}} diff --git a/templates/create/_shared/prisma-composer.config.ts.hbs b/templates/create/_shared/prisma-composer.config.ts.hbs new file mode 100644 index 0000000..e2561e2 --- /dev/null +++ b/templates/create/_shared/prisma-composer.config.ts.hbs @@ -0,0 +1,11 @@ +import { defineConfig } from "@prisma/composer/config"; +import { nodeBuild } from "@prisma/composer/node/control"; +{{#if (eq template "next")}} +import { nextjsBuild } from "@prisma/composer/nextjs/control"; +{{/if}} +import { prismaCloud, prismaState } from "@prisma/composer-prisma-cloud/control"; + +export default defineConfig({ + extensions: [prismaCloud(), nodeBuild(){{#if (eq template "next")}}, nextjsBuild(){{/if}}], + state: prismaState(), +}); diff --git a/templates/create/_shared/prisma.config.ts.hbs b/templates/create/_shared/prisma.config.ts.hbs new file mode 100644 index 0000000..5e2f733 --- /dev/null +++ b/templates/create/_shared/prisma.config.ts.hbs @@ -0,0 +1,10 @@ +import { defineConfig } from "@prisma/cli-engine"; + +import orm from "./prisma-next.config.ts"; + +export default defineConfig({ + orm, + composer: { + configPath: "./prisma-composer.config.ts", + }, +}); diff --git a/templates/create/_shared/service.ts.hbs b/templates/create/_shared/service.ts.hbs new file mode 100644 index 0000000..9265e86 --- /dev/null +++ b/templates/create/_shared/service.ts.hbs @@ -0,0 +1,42 @@ +{{#if (eq template "next")}} +import nextjs from "@prisma/composer/nextjs"; +{{else}} +import node from "@prisma/composer/node"; +{{/if}} +{{#if (eq provider "mongo")}} +import { secretString } from "@prisma/composer/arktype"; +import { type } from "arktype"; +{{/if}} +import { compute } from "@prisma/composer-prisma-cloud"; +{{#if (eq provider "postgres")}} +import { pnPostgres } from "@prisma/composer-prisma-cloud/prisma-next"; + +import { appContract } from "./src/prisma/composer.ts"; +{{/if}} + +export default compute({ + name: "app", + deps: { +{{#if (eq provider "postgres")}} + database: pnPostgres(appContract), +{{/if}} + }, +{{#if (eq provider "mongo")}} + input: type({ + databaseUrl: secretString(), + }), +{{/if}} +{{#if (eq template "next")}} + build: nextjs({ module: import.meta.url, appDir: "." }), +{{else if (eq template "svelte")}} + build: node({ module: import.meta.url, dir: "./build", entry: "index.js" }), +{{else if (eq template "astro")}} + build: node({ module: import.meta.url, dir: "./dist", entry: "server/entry.mjs" }), +{{else if (eq template "nuxt")}} + build: node({ module: import.meta.url, dir: "./.output", entry: "server/index.mjs" }), +{{else if (eq template "tanstack-start")}} + build: node({ module: import.meta.url, dir: "./.output", entry: "server/index.mjs" }), +{{else}} + build: node({ module: import.meta.url, entry: "./dist/server.mjs" }), +{{/if}} +}); diff --git a/templates/create/_shared/src/prisma/composer.ts.hbs b/templates/create/_shared/src/prisma/composer.ts.hbs new file mode 100644 index 0000000..fad5c5e --- /dev/null +++ b/templates/create/_shared/src/prisma/composer.ts.hbs @@ -0,0 +1,8 @@ +{{#if (eq provider "postgres")}} +import { pnContract } from "@prisma/composer-prisma-cloud/prisma-next"; + +import type { Contract } from "./contract.d.ts"; +import contractJson from "./contract.json" with { type: "json" }; + +export const appContract = pnContract(contractJson); +{{/if}} diff --git a/templates/create/_shared/src/prisma/db.ts.hbs b/templates/create/_shared/src/prisma/db.ts.hbs new file mode 100644 index 0000000..0533753 --- /dev/null +++ b/templates/create/_shared/src/prisma/db.ts.hbs @@ -0,0 +1,41 @@ +{{#if (eq provider "postgres")}} +import postgres from "@prisma/orm-postgres/runtime"; + +import service from "../../service.ts"; +import type { Contract } from "./contract.d.ts"; +import contractJson from "./contract.json" with { type: "json" }; + +function loadComposerDatabase() { + try { + return service.load().database.client; + } catch { + return undefined; + } +} + +export const db = + loadComposerDatabase() ?? + postgres({ + contractJson, + url: process.env.DATABASE_URL!, + }); +{{else}} +import mongo from "@prisma/orm-mongo/runtime"; + +import service from "../../service.ts"; +import type { Contract } from "./contract.d.ts"; +import contractJson from "./contract.json" with { type: "json" }; + +function getDatabaseUrl(): string { + try { + return service.input().databaseUrl.expose(); + } catch { + return process.env.DATABASE_URL!; + } +} + +export const db = mongo({ + contractJson, + url: getDatabaseUrl(), +}); +{{/if}} diff --git a/templates/create/_shared/src/prisma/seed.ts.hbs b/templates/create/_shared/src/prisma/seed.ts.hbs index dba7fcf..2355920 100644 --- a/templates/create/_shared/src/prisma/seed.ts.hbs +++ b/templates/create/_shared/src/prisma/seed.ts.hbs @@ -1,7 +1,7 @@ {{#if (requiresDotenvConfigImport packageManager)}} import "dotenv/config"; {{/if}} -import { db } from "./db{{#if (eq packageManager "deno")}}.ts{{/if}}"; +import { db } from "./db.ts"; const seedUsers = [ { email: "alice@prisma.io", username: "alice", name: "Alice" }, diff --git a/templates/create/_shared/src/prisma/users.ts.hbs b/templates/create/_shared/src/prisma/users.ts.hbs index 5ec37bd..684160a 100644 --- a/templates/create/_shared/src/prisma/users.ts.hbs +++ b/templates/create/_shared/src/prisma/users.ts.hbs @@ -1,56 +1,35 @@ import "dotenv/config"; -import { db } from "./db{{#if (eq packageManager "deno")}}.ts{{/if}}"; -{{#if (eq provider "mongo")}} -import type { DefaultModelRow } from "@prisma-next/mongo-orm"; -{{else}} -import type { DefaultModelRow } from "@prisma-next/sql-orm-client"; -{{/if}} - -import type { Contract } from "./contract.d"; +import { db } from "./db.ts"; export { db }; -type UserRow = DefaultModelRow; - -{{#if (eq provider "mongo")}} -function toStarterUser(user: Pick) { - return { - id: String(user._id), - email: user.email, - username: user.username ?? null, - name: user.name ?? null, - createdAt: null as Date | null, - }; -} -{{else}} -function toStarterUser(user: Pick) { - return { - id: String(user.id), - email: user.email, - username: user.username ?? null, - name: user.name ?? null, - createdAt: user.createdAt, - }; -} -{{/if}} - export async function listUsers(limit = 10) { - {{#if (eq provider "mongo")}} - const users: ReturnType[] = []; + const users = []; for await (const user of db.orm.users.select("_id", "email", "username", "name").take(limit).all()) { - users.push(toStarterUser(user)); + users.push({ + id: String(user._id), + email: user.email, + username: user.username ?? null, + name: user.name ?? null, + createdAt: null as Date | null, + }); } return users; {{else}} const users = await db.orm.public.User.select("id", "email", "username", "name", "createdAt").take(limit).all(); - return users.map(toStarterUser); + return users.map((user) => ({ + id: String(user.id), + email: user.email, + username: user.username ?? null, + name: user.name ?? null, + createdAt: user.createdAt, + })); {{/if}} - } export type StarterUser = Awaited>[number]; diff --git a/templates/create/astro/README.md.hbs b/templates/create/astro/README.md.hbs deleted file mode 100644 index bc0f83d..0000000 --- a/templates/create/astro/README.md.hbs +++ /dev/null @@ -1,46 +0,0 @@ -# {{projectName}} - -Generated by `create-prisma` with the Astro template. - -## Scripts - -- `{{runScriptCommand packageManager "dev"}}` - start the Astro dev server -- `{{runScriptCommand packageManager "build"}}` - build for production -- `{{runScriptCommand packageManager "preview"}}` - preview the production build -- `{{runScriptCommand packageManager "astro"}}` - run Astro CLI commands - -## Prisma Next - -Prisma Next setup is scaffolded in: - -- `src/prisma/contract{{#if (eq authoring "typescript")}}.ts{{else}}.prisma{{/if}}` -- `prisma-next.config.ts` -- `src/prisma/db.ts` -- `src/prisma/users.ts` -- `src/prisma/seed.ts` -- `src/pages/api/users.ts` - -Database helper scripts are added to `package.json`: - -- `{{runScriptCommand packageManager "contract:emit"}}` - emit contract artifacts after contract changes -{{#if (eq provider "mongo")}} -- `{{runScriptCommand packageManager "db:up"}}` - start the local MongoDB replica set with `mongodb-memory-server` (requires Node.js 24+). Data persists in `.mongo-data/` across restarts. -- `{{runScriptCommand packageManager "db:down"}}` - stop the local MongoDB process (data preserved) -- `{{runScriptCommand packageManager "db:reset"}}` - stop and wipe `.mongo-data/` for a clean slate -- `{{runScriptCommand packageManager "migration:plan"}}` - create a MongoDB migration plan -- `{{runScriptCommand packageManager "migrate"}}` - apply the planned MongoDB migration -{{else}} -- `{{runScriptCommand packageManager "db:init"}}` - initialize database state manually -- `{{runScriptCommand packageManager "db:update"}}` - update database state manually -- `{{runScriptCommand packageManager "migration:plan"}}` - create a migration plan -- `{{runScriptCommand packageManager "migrate"}}` - apply a planned migration -{{/if}} - -- `{{runScriptCommand packageManager "db:seed"}}` - insert sample users manually - -For provider-specific Prisma Next reference docs, see `prisma-next.md`. Prisma Next skills live in the upstream `skills/` directory: https://github.com/prisma/prisma-next/tree/main/skills. -The Astro Vite dev server also auto-emits Prisma Next contract artifacts when the contract changes. - -Node-based Prisma Next projects expect Node.js 24 LTS or newer. - -The starter page queries a basic `User` model in `src/pages/index.astro`, and `src/pages/api/users.ts` shows an Astro API route backed by the same Prisma Next helper. diff --git a/templates/create/astro/astro.config.mjs b/templates/create/astro/astro.config.mjs index 8ae04ad..c0028f0 100644 --- a/templates/create/astro/astro.config.mjs +++ b/templates/create/astro/astro.config.mjs @@ -1,10 +1,9 @@ // @ts-check -import { prismaVitePlugin } from "@prisma-next/vite-plugin-contract-emit"; +import node from "@astrojs/node"; import { defineConfig } from "astro/config"; // https://astro.build/config export default defineConfig({ - vite: { - plugins: [prismaVitePlugin()], - }, + output: "server", + adapter: node({ mode: "standalone" }), }); diff --git a/templates/create/astro/deno.json.hbs b/templates/create/astro/deno.json.hbs deleted file mode 100644 index a9ad642..0000000 --- a/templates/create/astro/deno.json.hbs +++ /dev/null @@ -1,5 +0,0 @@ -{{#if (eq packageManager "deno")}} -{ - "nodeModulesDir": "manual" -} -{{/if}} diff --git a/templates/create/elysia/README.md.hbs b/templates/create/elysia/README.md.hbs deleted file mode 100644 index 40078d3..0000000 --- a/templates/create/elysia/README.md.hbs +++ /dev/null @@ -1,43 +0,0 @@ -# {{projectName}} - -Generated by `create-prisma` with the Elysia template. - -## Scripts - -- `{{runScriptCommand packageManager "dev"}}` - start local dev server with hot reload -- `{{runScriptCommand packageManager "build"}}` - {{#if (eq packageManager "deno")}}type-check the app with Deno{{else}}{{#if (eq packageManager "bun")}}type-check the app for Bun{{else}}typecheck and compile{{/if}}{{/if}} -- `{{runScriptCommand packageManager "start"}}` - {{#if (eq packageManager "deno")}}run the server directly from `src/index.ts` with Deno{{else}}{{#if (eq packageManager "bun")}}run the server directly from `src/index.ts` with Bun{{else}}run compiled server from `dist/`{{/if}}{{/if}} - -## Prisma Next - -Prisma Next setup is scaffolded in: - -- `src/prisma/contract{{#if (eq authoring "typescript")}}.ts{{else}}.prisma{{/if}}` -- `prisma-next.config.ts` -- `src/prisma/db.ts` -- `src/prisma/users.ts` -- `src/prisma/seed.ts` - -Database helper scripts are added to `package.json`: - -- `{{runScriptCommand packageManager "contract:emit"}}` - emit contract artifacts after contract changes -{{#if (eq provider "mongo")}} -- `{{runScriptCommand packageManager "db:up"}}` - start the local MongoDB replica set with `mongodb-memory-server` (requires Node.js 24+). Data persists in `.mongo-data/` across restarts. -- `{{runScriptCommand packageManager "db:down"}}` - stop the local MongoDB process (data preserved) -- `{{runScriptCommand packageManager "db:reset"}}` - stop and wipe `.mongo-data/` for a clean slate -- `{{runScriptCommand packageManager "migration:plan"}}` - create a MongoDB migration plan -- `{{runScriptCommand packageManager "migrate"}}` - apply the planned MongoDB migration -{{else}} -- `{{runScriptCommand packageManager "db:init"}}` - initialize database state manually -- `{{runScriptCommand packageManager "db:update"}}` - update database state manually -- `{{runScriptCommand packageManager "migration:plan"}}` - create a migration plan -- `{{runScriptCommand packageManager "migrate"}}` - apply a planned migration -{{/if}} - -- `{{runScriptCommand packageManager "db:seed"}}` - insert sample users manually - -For provider-specific Prisma Next reference docs, see `prisma-next.md`. Prisma Next skills live in the upstream `skills/` directory: https://github.com/prisma/prisma-next/tree/main/skills. - -Node-based Prisma Next projects expect Node.js 24 LTS or newer. - -The template includes a basic `User` model and a sample `GET /users` endpoint. diff --git a/templates/create/elysia/deno.json.hbs b/templates/create/elysia/deno.json.hbs deleted file mode 100644 index a9ad642..0000000 --- a/templates/create/elysia/deno.json.hbs +++ /dev/null @@ -1,5 +0,0 @@ -{{#if (eq packageManager "deno")}} -{ - "nodeModulesDir": "manual" -} -{{/if}} diff --git a/templates/create/elysia/package.json.hbs b/templates/create/elysia/package.json.hbs index c847dfd..81f76b4 100644 --- a/templates/create/elysia/package.json.hbs +++ b/templates/create/elysia/package.json.hbs @@ -6,9 +6,9 @@ {{/if}} "type": "module", "scripts": { - "dev": "{{runtimeScript packageManager "dev" "src/index.ts" "dist/src/index.js" denoFlags="--unstable-net"}}", - "build": "{{runtimeScript packageManager "build" "src/index.ts" "dist/src/index.js"}}", - "start": "{{runtimeScript packageManager "start" "src/index.ts" "dist/src/index.js" denoFlags="--unstable-net"}}" + "dev": "tsx watch src/index.ts", + "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/server.mjs", + "start": "node dist/server.mjs" }, "dependencies": { "@sinclair/typebox": "^0.34.48", diff --git a/templates/create/elysia/src/index.ts.hbs b/templates/create/elysia/src/index.ts.hbs index 6bcd203..0e1d87d 100644 --- a/templates/create/elysia/src/index.ts.hbs +++ b/templates/create/elysia/src/index.ts.hbs @@ -1,20 +1,17 @@ {{#if (requiresDotenvConfigImport packageManager)}} import "dotenv/config"; {{/if}} -{{#if (eq packageManager "deno")}} -{{else}} import { node } from "@elysiajs/node"; -{{/if}} import { Elysia } from "elysia"; -import { listUsers } from "./prisma/users{{#if (eq packageManager "deno")}}.ts{{/if}}"; +import { listUsers } from "./prisma/users"; -const rawPort = ({{#if (eq packageManager "deno")}}Deno.env.get("PORT"){{else}}process.env.PORT{{/if}} ?? "").trim(); +const rawPort = (process.env.PORT ?? "").trim(); const parsedPort = rawPort.length > 0 ? Number(rawPort) : Number.NaN; const port = Number.isInteger(parsedPort) && parsedPort >= 0 && parsedPort <= 65535 ? parsedPort : 3000; -const app = new Elysia({{#if (eq packageManager "deno")}}{{else}}{ adapter: node() }{{/if}}) +const app = new Elysia({ adapter: node() }) .get("/", () => { return { message: "hello from create-prisma + elysia", @@ -35,13 +32,6 @@ const app = new Elysia({{#if (eq packageManager "deno")}}{{else}}{ adapter: node return users; }) -{{#if (eq packageManager "deno")}} - ; - -Deno.serve({ port }, app.fetch); -console.log(`Server running at http://localhost:${port}`); -{{else}} .listen(port); console.log(`Server running at http://localhost:${app.server?.port ?? port}`); -{{/if}} diff --git a/templates/create/hono/README.md.hbs b/templates/create/hono/README.md.hbs deleted file mode 100644 index 6307ce7..0000000 --- a/templates/create/hono/README.md.hbs +++ /dev/null @@ -1,43 +0,0 @@ -# {{projectName}} - -Generated by `create-prisma` with the Hono template. - -## Scripts - -- `{{runScriptCommand packageManager "dev"}}` - start local dev server -- `{{runScriptCommand packageManager "build"}}` - {{#if (eq packageManager "deno")}}type-check the app with Deno{{else}}{{#if (eq packageManager "bun")}}type-check the app for Bun{{else}}typecheck and compile{{/if}}{{/if}} -- `{{runScriptCommand packageManager "start"}}` - {{#if (eq packageManager "deno")}}run the server directly from `src/index.ts` with Deno{{else}}{{#if (eq packageManager "bun")}}run the server directly from `src/index.ts` with Bun{{else}}run compiled server from `dist/`{{/if}}{{/if}} - -## Prisma Next - -Prisma Next setup is scaffolded in: - -- `src/prisma/contract{{#if (eq authoring "typescript")}}.ts{{else}}.prisma{{/if}}` -- `prisma-next.config.ts` -- `src/prisma/db.ts` -- `src/prisma/users.ts` -- `src/prisma/seed.ts` - -Database helper scripts are added to `package.json`: - -- `{{runScriptCommand packageManager "contract:emit"}}` - emit contract artifacts after contract changes -{{#if (eq provider "mongo")}} -- `{{runScriptCommand packageManager "db:up"}}` - start the local MongoDB replica set with `mongodb-memory-server` (requires Node.js 24+). Data persists in `.mongo-data/` across restarts. -- `{{runScriptCommand packageManager "db:down"}}` - stop the local MongoDB process (data preserved) -- `{{runScriptCommand packageManager "db:reset"}}` - stop and wipe `.mongo-data/` for a clean slate -- `{{runScriptCommand packageManager "migration:plan"}}` - create a MongoDB migration plan -- `{{runScriptCommand packageManager "migrate"}}` - apply the planned MongoDB migration -{{else}} -- `{{runScriptCommand packageManager "db:init"}}` - initialize database state manually -- `{{runScriptCommand packageManager "db:update"}}` - update database state manually -- `{{runScriptCommand packageManager "migration:plan"}}` - create a migration plan -- `{{runScriptCommand packageManager "migrate"}}` - apply a planned migration -{{/if}} - -- `{{runScriptCommand packageManager "db:seed"}}` - insert sample users manually - -For provider-specific Prisma Next reference docs, see `prisma-next.md`. Prisma Next skills live in the upstream `skills/` directory: https://github.com/prisma/prisma-next/tree/main/skills. - -Node-based Prisma Next projects expect Node.js 24 LTS or newer. - -The template includes a basic `User` model and a sample `GET /users` endpoint. diff --git a/templates/create/hono/deno.json.hbs b/templates/create/hono/deno.json.hbs deleted file mode 100644 index a9ad642..0000000 --- a/templates/create/hono/deno.json.hbs +++ /dev/null @@ -1,5 +0,0 @@ -{{#if (eq packageManager "deno")}} -{ - "nodeModulesDir": "manual" -} -{{/if}} diff --git a/templates/create/hono/package.json.hbs b/templates/create/hono/package.json.hbs index 16bc20f..4cf510d 100644 --- a/templates/create/hono/package.json.hbs +++ b/templates/create/hono/package.json.hbs @@ -7,8 +7,8 @@ "type": "module", "scripts": { "dev": "{{runtimeScript packageManager "dev" "src/index.ts" "dist/src/index.js"}}", - "build": "{{runtimeScript packageManager "build" "src/index.ts" "dist/src/index.js"}}", - "start": "{{runtimeScript packageManager "start" "src/index.ts" "dist/src/index.js"}}" + "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/server.mjs", + "start": "node dist/server.mjs" }, "dependencies": { "@hono/node-server": "^1.19.9", diff --git a/templates/create/hono/src/index.ts.hbs b/templates/create/hono/src/index.ts.hbs index d3cacf7..4365f93 100644 --- a/templates/create/hono/src/index.ts.hbs +++ b/templates/create/hono/src/index.ts.hbs @@ -4,7 +4,7 @@ import "dotenv/config"; import { serve } from "@hono/node-server"; import { Hono } from "hono"; -import { listUsers } from "./prisma/users{{#if (eq packageManager "deno")}}.ts{{/if}}"; +import { listUsers } from "./prisma/users"; const app = new Hono(); @@ -27,7 +27,7 @@ app.get("/users", async (c) => { return c.json(users); }); -const rawPort = ({{#if (eq packageManager "deno")}}Deno.env.get("PORT"){{else}}process.env.PORT{{/if}} ?? "").trim(); +const rawPort = (process.env.PORT ?? "").trim(); const parsedPort = rawPort.length > 0 ? Number(rawPort) : Number.NaN; const port = Number.isInteger(parsedPort) && parsedPort >= 0 && parsedPort <= 65535 ? parsedPort : 3000; diff --git a/templates/create/minimal/README.md.hbs b/templates/create/minimal/README.md.hbs deleted file mode 100644 index 10bada1..0000000 --- a/templates/create/minimal/README.md.hbs +++ /dev/null @@ -1,40 +0,0 @@ -# {{projectName}} - -Generated by `create-prisma` with the Minimal template. - -## Scripts - -- `{{runScriptCommand packageManager "dev"}}` - run the Prisma Next sample script - -## Prisma Next - -Prisma Next setup is scaffolded in: - -- `src/prisma/contract{{#if (eq authoring "typescript")}}.ts{{else}}.prisma{{/if}}` -- `prisma-next.config.ts` -- `src/prisma/db.ts` -- `src/prisma/users.ts` -- `src/prisma/seed.ts` - -Database helper scripts are added to `package.json`: - -- `{{runScriptCommand packageManager "contract:emit"}}` - emit contract artifacts after contract changes -{{#if (eq provider "mongo")}} -- `{{runScriptCommand packageManager "db:up"}}` - start the local MongoDB replica set with `mongodb-memory-server` (requires Node.js 24+). Data persists in `.mongo-data/` across restarts. -- `{{runScriptCommand packageManager "db:down"}}` - stop the local MongoDB process (data preserved) -- `{{runScriptCommand packageManager "db:reset"}}` - stop and wipe `.mongo-data/` for a clean slate -- `{{runScriptCommand packageManager "migration:plan"}}` - create a MongoDB migration plan -- `{{runScriptCommand packageManager "migrate"}}` - apply the planned MongoDB migration -{{else}} -- `{{runScriptCommand packageManager "db:init"}}` - initialize database state manually -- `{{runScriptCommand packageManager "db:update"}}` - update database state manually -- `{{runScriptCommand packageManager "migration:plan"}}` - create a migration plan -- `{{runScriptCommand packageManager "migrate"}}` - apply a planned migration -{{/if}} -- `{{runScriptCommand packageManager "db:seed"}}` - insert sample users manually - -For provider-specific Prisma Next reference docs, see `prisma-next.md`. Prisma Next skills live in the upstream `skills/` directory: https://github.com/prisma/prisma-next/tree/main/skills. - -Node-based Prisma Next projects expect Node.js 24 LTS or newer. - -Run the migration and seed scripts first, then `{{runScriptCommand packageManager "dev"}}` runs a small write/read round trip from `src/index.ts`. diff --git a/templates/create/minimal/deno.json.hbs b/templates/create/minimal/deno.json.hbs deleted file mode 100644 index a9ad642..0000000 --- a/templates/create/minimal/deno.json.hbs +++ /dev/null @@ -1,5 +0,0 @@ -{{#if (eq packageManager "deno")}} -{ - "nodeModulesDir": "manual" -} -{{/if}} diff --git a/templates/create/minimal/package.json.hbs b/templates/create/minimal/package.json.hbs index 4953a99..d801711 100644 --- a/templates/create/minimal/package.json.hbs +++ b/templates/create/minimal/package.json.hbs @@ -6,7 +6,9 @@ {{/if}} "type": "module", "scripts": { - "dev": "{{runtimeScript packageManager "start" "src/index.ts" ""}}" + "dev": "tsx watch src/index.ts", + "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/server.mjs", + "start": "node dist/server.mjs" }, "dependencies": {}, "devDependencies": {} diff --git a/templates/create/minimal/src/index.ts.hbs b/templates/create/minimal/src/index.ts.hbs index de69713..e4b048c 100644 --- a/templates/create/minimal/src/index.ts.hbs +++ b/templates/create/minimal/src/index.ts.hbs @@ -1,43 +1,21 @@ -import { db, listUsers } from "./prisma/users{{#if (eq packageManager "deno")}}.ts{{/if}}"; +import "dotenv/config"; -const sampleUser = { - email: "first.user@prisma.io", - username: "first-user", - name: "First User", -}; +import { createServer } from "node:http"; -async function main() { -{{#if (eq provider "mongo")}} - const existingUser = await db.orm.users.where({ email: sampleUser.email }).first(); - if (!existingUser) { - await db.orm.users.create(sampleUser); - } - - const user = await db.orm.users.where({ email: sampleUser.email }).first(); -{{else}} - const existingUser = await db.orm.public.User.where({ email: sampleUser.email }).first(); - if (!existingUser) { - await db.orm.public.User.create(sampleUser); - } +import { listUsers } from "./prisma/users"; - const user = await db.orm.public.User.where({ email: sampleUser.email }).first(); -{{/if}} - const users = await listUsers(); +const port = Number(process.env.PORT ?? 3000); - console.log(`Prisma Next is ready. Found ${users.length} user${users.length === 1 ? "" : "s"}.`); - console.log(user); -} - -try { - await main(); -} catch (error) { - console.error("Prisma Next query failed."); - console.error("Emit the contract, set DATABASE_URL, then apply migrations before running this script."); - throw error; -} finally { -{{#if (eq provider "mongo")}} - await db.close(); -{{else}} - await db.close(); -{{/if}} -} +createServer(async (_request, response) => { + try { + const users = await listUsers(); + response.writeHead(200, { "content-type": "application/json" }); + response.end(JSON.stringify({ users })); + } catch (error) { + console.error("Failed to query users:", error); + response.writeHead(500, { "content-type": "application/json" }); + response.end(JSON.stringify({ error: "Could not query users yet." })); + } +}).listen(port, "0.0.0.0", () => { + console.log(`Server running at http://localhost:${port}`); +}); diff --git a/templates/create/nest/README.md.hbs b/templates/create/nest/README.md.hbs deleted file mode 100644 index 932227d..0000000 --- a/templates/create/nest/README.md.hbs +++ /dev/null @@ -1,44 +0,0 @@ -# {{projectName}} - -Generated by `create-prisma` with the NestJS template. - -## Scripts - -- `{{runScriptCommand packageManager "dev"}}` - start the Nest dev server with watch mode -- `{{runScriptCommand packageManager "build"}}` - {{#if (eq packageManager "deno")}}type-check the app with Deno{{else}}{{#if (eq packageManager "bun")}}type-check the app for Bun{{else}}compile the app into `dist/`{{/if}}{{/if}} -- `{{runScriptCommand packageManager "start"}}` - {{#if (eq packageManager "deno")}}run the server directly from `src/main.ts` with Deno{{else}}{{#if (eq packageManager "bun")}}run the server directly from `src/main.ts` with Bun{{else}}run the compiled server from `dist/main.js`{{/if}}{{/if}} - -## Prisma Next - -Prisma Next setup is scaffolded in: - -- `src/prisma/contract{{#if (eq authoring "typescript")}}.ts{{else}}.prisma{{/if}}` -- `prisma-next.config.ts` -- `src/prisma/db.ts` -- `src/prisma/users.ts` -- `src/prisma/seed.ts` -- `src/prisma.service.ts` - -Database helper scripts are added to `package.json`: - -- `{{runScriptCommand packageManager "contract:emit"}}` - emit contract artifacts after contract changes -{{#if (eq provider "mongo")}} -- `{{runScriptCommand packageManager "db:up"}}` - start the local MongoDB replica set with `mongodb-memory-server` (requires Node.js 24+). Data persists in `.mongo-data/` across restarts. -- `{{runScriptCommand packageManager "db:down"}}` - stop the local MongoDB process (data preserved) -- `{{runScriptCommand packageManager "db:reset"}}` - stop and wipe `.mongo-data/` for a clean slate -- `{{runScriptCommand packageManager "migration:plan"}}` - create a MongoDB migration plan -- `{{runScriptCommand packageManager "migrate"}}` - apply the planned MongoDB migration -{{else}} -- `{{runScriptCommand packageManager "db:init"}}` - initialize database state manually -- `{{runScriptCommand packageManager "db:update"}}` - update database state manually -- `{{runScriptCommand packageManager "migration:plan"}}` - create a migration plan -- `{{runScriptCommand packageManager "migrate"}}` - apply a planned migration -{{/if}} - -- `{{runScriptCommand packageManager "db:seed"}}` - insert sample users manually - -For provider-specific Prisma Next reference docs, see `prisma-next.md`. Prisma Next skills live in the upstream `skills/` directory: https://github.com/prisma/prisma-next/tree/main/skills. - -Node-based Prisma Next projects expect Node.js 24 LTS or newer. - -The template includes a basic `User` model and a sample `GET /users` endpoint. diff --git a/templates/create/nest/deno.json.hbs b/templates/create/nest/deno.json.hbs deleted file mode 100644 index a9ad642..0000000 --- a/templates/create/nest/deno.json.hbs +++ /dev/null @@ -1,5 +0,0 @@ -{{#if (eq packageManager "deno")}} -{ - "nodeModulesDir": "manual" -} -{{/if}} diff --git a/templates/create/nest/package.json.hbs b/templates/create/nest/package.json.hbs index 6b2f922..b408920 100644 --- a/templates/create/nest/package.json.hbs +++ b/templates/create/nest/package.json.hbs @@ -7,8 +7,8 @@ "type": "module", "scripts": { "dev": "{{runtimeScript packageManager "dev" "src/main.ts" "dist/main.js"}}", - "build": "{{runtimeScript packageManager "build" "src/main.ts" "dist/main.js"}}", - "start": "{{runtimeScript packageManager "start" "src/main.ts" "dist/main.js"}}" + "build": "esbuild src/main.ts --bundle --platform=node --format=esm --outfile=dist/server.mjs --banner:js=\"import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);\" --external:@nestjs/websockets/* --external:@nestjs/microservices --external:@nestjs/microservices/* --external:@nestjs/platform-socket.io --external:class-transformer --external:class-validator", + "start": "node dist/server.mjs" }, "dependencies": { "@nestjs/common": "^11.1.17", diff --git a/templates/create/nest/src/app.module.ts.hbs b/templates/create/nest/src/app.module.ts.hbs index f1c243e..67d6a94 100644 --- a/templates/create/nest/src/app.module.ts.hbs +++ b/templates/create/nest/src/app.module.ts.hbs @@ -1,9 +1,9 @@ import { Module } from "@nestjs/common"; -import { AppController } from "./app.controller{{#if (eq packageManager "deno")}}.ts{{/if}}"; -import { PrismaService } from "./prisma.service{{#if (eq packageManager "deno")}}.ts{{/if}}"; +import { AppController } from "./app.controller"; +import { PrismaService } from "./prisma.service"; -import { UsersController } from "./users.controller{{#if (eq packageManager "deno")}}.ts{{/if}}"; -import { UsersService } from "./users.service{{#if (eq packageManager "deno")}}.ts{{/if}}"; +import { UsersController } from "./users.controller"; +import { UsersService } from "./users.service"; @Module({ imports: [], diff --git a/templates/create/nest/src/main.ts.hbs b/templates/create/nest/src/main.ts.hbs index f094054..a631d42 100644 --- a/templates/create/nest/src/main.ts.hbs +++ b/templates/create/nest/src/main.ts.hbs @@ -5,11 +5,11 @@ import "dotenv/config"; import { NestFactory } from "@nestjs/core"; -import { AppModule } from "./app.module{{#if (eq packageManager "deno")}}.ts{{/if}}"; +import { AppModule } from "./app.module"; async function bootstrap() { const app = await NestFactory.create(AppModule); - const rawPort = ({{#if (eq packageManager "deno")}}Deno.env.get("PORT"){{else}}process.env.PORT{{/if}} ?? "").trim(); + const rawPort = (process.env.PORT ?? "").trim(); const parsedPort = rawPort.length > 0 ? Number(rawPort) : Number.NaN; const port = Number.isFinite(parsedPort) && parsedPort >= 0 && parsedPort <= 65535 ? parsedPort : 3000; @@ -19,9 +19,5 @@ async function bootstrap() { bootstrap().catch((error) => { console.error("Failed to start server", error); - {{#if (eq packageManager "deno")}} - Deno.exit(1); - {{else}} process.exit(1); - {{/if}} }); diff --git a/templates/create/nest/src/prisma.service.ts.hbs b/templates/create/nest/src/prisma.service.ts.hbs index 82b6aad..f605465 100644 --- a/templates/create/nest/src/prisma.service.ts.hbs +++ b/templates/create/nest/src/prisma.service.ts.hbs @@ -1,5 +1,5 @@ import { Injectable } from "@nestjs/common"; -import { db, listUsers, type StarterUser } from "./prisma/users{{#if (eq packageManager "deno")}}.ts{{/if}}"; +import { db, listUsers, type StarterUser } from "./prisma/users"; @Injectable() export class PrismaService { diff --git a/templates/create/nest/src/users.controller.ts.hbs b/templates/create/nest/src/users.controller.ts.hbs index d61ee6c..1d9abc2 100644 --- a/templates/create/nest/src/users.controller.ts.hbs +++ b/templates/create/nest/src/users.controller.ts.hbs @@ -1,6 +1,6 @@ import { Controller, Get } from "@nestjs/common"; -import { UsersService } from "./users.service{{#if (eq packageManager "deno")}}.ts{{/if}}"; +import { UsersService } from "./users.service"; @Controller("users") export class UsersController { @@ -11,4 +11,3 @@ export class UsersController { return this.usersService.findAll(); } } - diff --git a/templates/create/nest/src/users.service.ts.hbs b/templates/create/nest/src/users.service.ts.hbs index 082429f..25ffa77 100644 --- a/templates/create/nest/src/users.service.ts.hbs +++ b/templates/create/nest/src/users.service.ts.hbs @@ -1,6 +1,6 @@ import { Injectable } from "@nestjs/common"; -import { PrismaService } from "./prisma.service{{#if (eq packageManager "deno")}}.ts{{/if}}"; +import { PrismaService } from "./prisma.service"; @Injectable() export class UsersService { @@ -10,4 +10,3 @@ export class UsersService { return this.prisma.listUsers(10); } } - diff --git a/templates/create/next/README.md.hbs b/templates/create/next/README.md.hbs deleted file mode 100644 index 8b324e3..0000000 --- a/templates/create/next/README.md.hbs +++ /dev/null @@ -1,43 +0,0 @@ -# {{projectName}} - -Generated by `create-prisma` with the Next.js template. - -## Scripts - -- `{{runScriptCommand packageManager "dev"}}` - start local dev server -- `{{runScriptCommand packageManager "build"}}` - production build -- `{{runScriptCommand packageManager "start"}}` - run production server - -## Prisma Next - -Prisma Next setup is scaffolded in: - -- `src/prisma/contract{{#if (eq authoring "typescript")}}.ts{{else}}.prisma{{/if}}` -- `prisma-next.config.ts` -- `src/prisma/db.ts` -- `src/prisma/users.ts` -- `src/prisma/seed.ts` - -Database helper scripts are added to `package.json`: - -- `{{runScriptCommand packageManager "contract:emit"}}` - emit contract artifacts after contract changes -{{#if (eq provider "mongo")}} -- `{{runScriptCommand packageManager "db:up"}}` - start the local MongoDB replica set with `mongodb-memory-server` (requires Node.js 24+). Data persists in `.mongo-data/` across restarts. -- `{{runScriptCommand packageManager "db:down"}}` - stop the local MongoDB process (data preserved) -- `{{runScriptCommand packageManager "db:reset"}}` - stop and wipe `.mongo-data/` for a clean slate -- `{{runScriptCommand packageManager "migration:plan"}}` - create a MongoDB migration plan -- `{{runScriptCommand packageManager "migrate"}}` - apply the planned MongoDB migration -{{else}} -- `{{runScriptCommand packageManager "db:init"}}` - initialize database state manually -- `{{runScriptCommand packageManager "db:update"}}` - update database state manually -- `{{runScriptCommand packageManager "migration:plan"}}` - create a migration plan -- `{{runScriptCommand packageManager "migrate"}}` - apply a planned migration -{{/if}} - -- `{{runScriptCommand packageManager "db:seed"}}` - insert sample users manually - -For provider-specific Prisma Next reference docs, see `prisma-next.md`. Prisma Next skills live in the upstream `skills/` directory: https://github.com/prisma/prisma-next/tree/main/skills. - -Node-based Prisma Next projects expect Node.js 24 LTS or newer. - -The starter page in `src/app/page.tsx` reads from a basic `User` model so you can verify queries quickly after you initialize and apply your schema. diff --git a/templates/create/next/deno.json.hbs b/templates/create/next/deno.json.hbs deleted file mode 100644 index 4d8d169..0000000 --- a/templates/create/next/deno.json.hbs +++ /dev/null @@ -1,12 +0,0 @@ -{{#if (eq packageManager "deno")}} -{ - "nodeModulesDir": "manual", - "unstable": [ - "bare-node-builtins", - "detect-cjs", - "node-globals", - "unsafe-proto", - "sloppy-imports" - ] -} -{{/if}} diff --git a/templates/create/next/next.config.ts b/templates/create/next/next.config.ts index d742c15..68a6c64 100644 --- a/templates/create/next/next.config.ts +++ b/templates/create/next/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - // Add Next config here when needed. + output: "standalone", }; export default nextConfig; diff --git a/templates/create/next/src/app/page.tsx.hbs b/templates/create/next/src/app/page.tsx.hbs index bbe8152..ecd0c21 100644 --- a/templates/create/next/src/app/page.tsx.hbs +++ b/templates/create/next/src/app/page.tsx.hbs @@ -1,7 +1,4 @@ -{{#if (eq packageManager "deno")}} - export const dynamic = "force-dynamic"; -{{/if}} export default async function Home() { diff --git a/templates/create/nuxt/README.md.hbs b/templates/create/nuxt/README.md.hbs deleted file mode 100644 index 5b06434..0000000 --- a/templates/create/nuxt/README.md.hbs +++ /dev/null @@ -1,46 +0,0 @@ -# {{projectName}} - -Generated by `create-prisma` with the Nuxt template. - -## Scripts - -- `{{runScriptCommand packageManager "dev"}}` - start the Nuxt dev server -- `{{runScriptCommand packageManager "build"}}` - build for production -- `{{runScriptCommand packageManager "preview"}}` - preview the production build -- `{{runScriptCommand packageManager "typecheck"}}` - run Nuxt type checks - -## Prisma Next - -Prisma Next setup is scaffolded in: - -- `src/prisma/contract{{#if (eq authoring "typescript")}}.ts{{else}}.prisma{{/if}}` -- `prisma-next.config.ts` -- `src/prisma/db.ts` -- `src/prisma/users.ts` -- `src/prisma/seed.ts` -- `server/api/users.get.ts` - -Database helper scripts are added to `package.json`: - -- `{{runScriptCommand packageManager "contract:emit"}}` - emit contract artifacts after contract changes -{{#if (eq provider "mongo")}} -- `{{runScriptCommand packageManager "db:up"}}` - start the local MongoDB replica set with `mongodb-memory-server` (requires Node.js 24+). Data persists in `.mongo-data/` across restarts. -- `{{runScriptCommand packageManager "db:down"}}` - stop the local MongoDB process (data preserved) -- `{{runScriptCommand packageManager "db:reset"}}` - stop and wipe `.mongo-data/` for a clean slate -- `{{runScriptCommand packageManager "migration:plan"}}` - create a MongoDB migration plan -- `{{runScriptCommand packageManager "migrate"}}` - apply the planned MongoDB migration -{{else}} -- `{{runScriptCommand packageManager "db:init"}}` - initialize database state manually -- `{{runScriptCommand packageManager "db:update"}}` - update database state manually -- `{{runScriptCommand packageManager "migration:plan"}}` - create a migration plan -- `{{runScriptCommand packageManager "migrate"}}` - apply a planned migration -{{/if}} - -- `{{runScriptCommand packageManager "db:seed"}}` - insert sample users manually - -For provider-specific Prisma Next reference docs, see `prisma-next.md`. Prisma Next skills live in the upstream `skills/` directory: https://github.com/prisma/prisma-next/tree/main/skills. -The Nuxt Vite dev server also auto-emits Prisma Next contract artifacts when the contract changes. - -Node-based Prisma Next projects expect Node.js 24 LTS or newer. - -The starter page in `app/pages/index.vue` fetches users from `server/api/users.get.ts` after you initialize and apply your schema. diff --git a/templates/create/nuxt/deno.json.hbs b/templates/create/nuxt/deno.json.hbs deleted file mode 100644 index a9ad642..0000000 --- a/templates/create/nuxt/deno.json.hbs +++ /dev/null @@ -1,5 +0,0 @@ -{{#if (eq packageManager "deno")}} -{ - "nodeModulesDir": "manual" -} -{{/if}} diff --git a/templates/create/nuxt/nuxt.config.ts b/templates/create/nuxt/nuxt.config.ts index 01c4ba7..8c38c81 100644 --- a/templates/create/nuxt/nuxt.config.ts +++ b/templates/create/nuxt/nuxt.config.ts @@ -1,12 +1,7 @@ -import { prismaVitePlugin } from "@prisma-next/vite-plugin-contract-emit"; - // https://nuxt.com/docs/api/configuration/nuxt-config export default defineNuxtConfig({ compatibilityDate: "2025-07-15", devtools: { enabled: true }, - vite: { - plugins: [prismaVitePlugin()], - }, typescript: { tsConfig: { compilerOptions: { diff --git a/templates/create/svelte/README.md.hbs b/templates/create/svelte/README.md.hbs deleted file mode 100644 index 6f78a35..0000000 --- a/templates/create/svelte/README.md.hbs +++ /dev/null @@ -1,45 +0,0 @@ -# {{projectName}} - -Generated by `create-prisma` with the SvelteKit template. - -## Scripts - -- `{{runScriptCommand packageManager "dev"}}` - start the SvelteKit dev server -- `{{runScriptCommand packageManager "build"}}` - build for production -- `{{runScriptCommand packageManager "preview"}}` - preview the production build -- `{{runScriptCommand packageManager "check"}}` - run SvelteKit sync and type checks - -## Prisma Next - -Prisma Next setup is scaffolded in: - -- `src/prisma/contract{{#if (eq authoring "typescript")}}.ts{{else}}.prisma{{/if}}` -- `prisma-next.config.ts` -- `src/prisma/db.ts` -- `src/prisma/users.ts` -- `src/prisma/seed.ts` - -Database helper scripts are added to `package.json`: - -- `{{runScriptCommand packageManager "contract:emit"}}` - emit contract artifacts after contract changes -{{#if (eq provider "mongo")}} -- `{{runScriptCommand packageManager "db:up"}}` - start the local MongoDB replica set with `mongodb-memory-server` (requires Node.js 24+). Data persists in `.mongo-data/` across restarts. -- `{{runScriptCommand packageManager "db:down"}}` - stop the local MongoDB process (data preserved) -- `{{runScriptCommand packageManager "db:reset"}}` - stop and wipe `.mongo-data/` for a clean slate -- `{{runScriptCommand packageManager "migration:plan"}}` - create a MongoDB migration plan -- `{{runScriptCommand packageManager "migrate"}}` - apply the planned MongoDB migration -{{else}} -- `{{runScriptCommand packageManager "db:init"}}` - initialize database state manually -- `{{runScriptCommand packageManager "db:update"}}` - update database state manually -- `{{runScriptCommand packageManager "migration:plan"}}` - create a migration plan -- `{{runScriptCommand packageManager "migrate"}}` - apply a planned migration -{{/if}} - -- `{{runScriptCommand packageManager "db:seed"}}` - insert sample users manually - -For provider-specific Prisma Next reference docs, see `prisma-next.md`. Prisma Next skills live in the upstream `skills/` directory: https://github.com/prisma/prisma-next/tree/main/skills. -The SvelteKit Vite dev server also auto-emits Prisma Next contract artifacts when the contract changes. - -Node-based Prisma Next projects expect Node.js 24 LTS or newer. - -The starter page loads users in `+page.server.ts` and renders them in `+page.svelte` after you initialize and apply your schema. diff --git a/templates/create/svelte/deno.json.hbs b/templates/create/svelte/deno.json.hbs deleted file mode 100644 index a9ad642..0000000 --- a/templates/create/svelte/deno.json.hbs +++ /dev/null @@ -1,5 +0,0 @@ -{{#if (eq packageManager "deno")}} -{ - "nodeModulesDir": "manual" -} -{{/if}} diff --git a/templates/create/svelte/package.json.hbs b/templates/create/svelte/package.json.hbs index 18bb57d..c707b94 100644 --- a/templates/create/svelte/package.json.hbs +++ b/templates/create/svelte/package.json.hbs @@ -15,7 +15,6 @@ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" }, "devDependencies": { - "@sveltejs/adapter-auto": "^7.0.0", "@sveltejs/kit": "^2.50.2", "@sveltejs/vite-plugin-svelte": "^6.2.4", "@types/node": "^24.3.0", diff --git a/templates/create/svelte/svelte.config.js b/templates/create/svelte/svelte.config.js index 75e1de6..d341d7e 100644 --- a/templates/create/svelte/svelte.config.js +++ b/templates/create/svelte/svelte.config.js @@ -1,11 +1,8 @@ -import adapter from "@sveltejs/adapter-auto"; +import adapter from "@sveltejs/adapter-node"; /** @type {import('@sveltejs/kit').Config} */ const config = { kit: { - // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://svelte.dev/docs/kit/adapters for more information about adapters. adapter: adapter(), }, }; diff --git a/templates/create/svelte/vite.config.ts b/templates/create/svelte/vite.config.ts index 892da01..80864b9 100644 --- a/templates/create/svelte/vite.config.ts +++ b/templates/create/svelte/vite.config.ts @@ -1,7 +1,6 @@ -import { prismaVitePlugin } from "@prisma-next/vite-plugin-contract-emit"; import { sveltekit } from "@sveltejs/kit/vite"; import { defineConfig } from "vite"; export default defineConfig({ - plugins: [prismaVitePlugin(), sveltekit()], + plugins: [sveltekit()], }); diff --git a/templates/create/tanstack-start/README.md.hbs b/templates/create/tanstack-start/README.md.hbs deleted file mode 100644 index ad09300..0000000 --- a/templates/create/tanstack-start/README.md.hbs +++ /dev/null @@ -1,45 +0,0 @@ -# {{projectName}} - -Generated by `create-prisma` with the TanStack Start template. - -## Scripts - -- `{{runScriptCommand packageManager "dev"}}` - start the TanStack Start dev server -- `{{runScriptCommand packageManager "build"}}` - build for production -- `{{runScriptCommand packageManager "preview"}}` - preview the production build -- `{{runScriptCommand packageManager "typecheck"}}` - run TypeScript checks - -## Prisma Next - -Prisma Next setup is scaffolded in: - -- `src/prisma/contract{{#if (eq authoring "typescript")}}.ts{{else}}.prisma{{/if}}` -- `prisma-next.config.ts` -- `src/prisma/db.ts` -- `src/prisma/users.ts` -- `src/prisma/seed.ts` - -Database helper scripts are added to `package.json`: - -- `{{runScriptCommand packageManager "contract:emit"}}` - emit contract artifacts after contract changes -{{#if (eq provider "mongo")}} -- `{{runScriptCommand packageManager "db:up"}}` - start the local MongoDB replica set with `mongodb-memory-server` (requires Node.js 24+). Data persists in `.mongo-data/` across restarts. -- `{{runScriptCommand packageManager "db:down"}}` - stop the local MongoDB process (data preserved) -- `{{runScriptCommand packageManager "db:reset"}}` - stop and wipe `.mongo-data/` for a clean slate -- `{{runScriptCommand packageManager "migration:plan"}}` - create a MongoDB migration plan -- `{{runScriptCommand packageManager "migrate"}}` - apply the planned MongoDB migration -{{else}} -- `{{runScriptCommand packageManager "db:init"}}` - initialize database state manually -- `{{runScriptCommand packageManager "db:update"}}` - update database state manually -- `{{runScriptCommand packageManager "migration:plan"}}` - create a migration plan -- `{{runScriptCommand packageManager "migrate"}}` - apply a planned migration -{{/if}} - -- `{{runScriptCommand packageManager "db:seed"}}` - insert sample users manually - -For provider-specific Prisma Next reference docs, see `prisma-next.md`. Prisma Next skills live in the upstream `skills/` directory: https://github.com/prisma/prisma-next/tree/main/skills. -The TanStack Start Vite dev server also auto-emits Prisma Next contract artifacts when the contract changes. - -Node-based Prisma Next projects expect Node.js 24 LTS or newer. - -The home route uses a TanStack Start server function to load users through the Prisma Next helper in `src/prisma/users.ts`. diff --git a/templates/create/tanstack-start/deno.json.hbs b/templates/create/tanstack-start/deno.json.hbs deleted file mode 100644 index 4d8d169..0000000 --- a/templates/create/tanstack-start/deno.json.hbs +++ /dev/null @@ -1,12 +0,0 @@ -{{#if (eq packageManager "deno")}} -{ - "nodeModulesDir": "manual", - "unstable": [ - "bare-node-builtins", - "detect-cjs", - "node-globals", - "unsafe-proto", - "sloppy-imports" - ] -} -{{/if}} diff --git a/templates/create/tanstack-start/vite.config.ts b/templates/create/tanstack-start/vite.config.ts index 0c3fd20..64f91d4 100644 --- a/templates/create/tanstack-start/vite.config.ts +++ b/templates/create/tanstack-start/vite.config.ts @@ -1,14 +1,14 @@ -import { prismaVitePlugin } from "@prisma-next/vite-plugin-contract-emit"; import { defineConfig } from "vite"; import viteReact from "@vitejs/plugin-react"; import { tanstackStart } from "@tanstack/react-start/plugin/vite"; +import { nitro } from "nitro/vite"; import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ plugins: [ - prismaVitePlugin(), tsconfigPaths({ projects: ["./tsconfig.json"] }), tanstackStart(), + nitro(), viteReact(), ], }); diff --git a/tests/dependencies.test.ts b/tests/dependencies.test.ts index 75f55f5..e9e07ce 100644 --- a/tests/dependencies.test.ts +++ b/tests/dependencies.test.ts @@ -1,115 +1,18 @@ import { describe, expect, test } from "bun:test"; -import { - DEFAULT_PRISMA_NEXT_SPEC, - getDependencyVersion, - getPrismaNextPackageSpecifier, - isPrismaNextPackage, - parsePrismaNextVersionSpec, - PRISMA_NEXT_DEFAULT_VERSION, -} from "../src/constants/dependencies"; +import { dependencyVersionMap, getDependencyVersion } from "../src/constants/dependencies"; -describe("parsePrismaNextVersionSpec", () => { - test("defaults to the npm `latest` dist-tag when input is omitted", () => { - expect(parsePrismaNextVersionSpec(undefined)).toEqual(DEFAULT_PRISMA_NEXT_SPEC); - expect(DEFAULT_PRISMA_NEXT_SPEC).toEqual({ kind: "npm", spec: PRISMA_NEXT_DEFAULT_VERSION }); +describe("Prisma Next dependency versions", () => { + test("uses the aligned Prisma Next and Composer releases", () => { + expect(getDependencyVersion("prisma-next")).toBe("8.0.0-rc.1"); + expect(getDependencyVersion("@prisma/orm-postgres")).toBe("8.0.0-rc.1"); + expect(getDependencyVersion("@prisma/cli-engine")).toBe("8.0.0-rc.2"); + expect(getDependencyVersion("@prisma/composer")).toBe("0.6.0-dev.18"); + expect(getDependencyVersion("@prisma/composer-prisma-cloud")).toBe("0.6.0-dev.18"); }); - test("treats blank input as the default", () => { - expect(parsePrismaNextVersionSpec("")).toEqual(DEFAULT_PRISMA_NEXT_SPEC); - expect(parsePrismaNextVersionSpec(" ")).toEqual(DEFAULT_PRISMA_NEXT_SPEC); - }); - - test("passes through published versions verbatim", () => { - expect(parsePrismaNextVersionSpec("0.10.0")).toEqual({ kind: "npm", spec: "0.10.0" }); - expect(parsePrismaNextVersionSpec("0.11.0-dev.9")).toEqual({ - kind: "npm", - spec: "0.11.0-dev.9", - }); - }); - - test("passes through npm dist-tags verbatim", () => { - expect(parsePrismaNextVersionSpec("dev")).toEqual({ kind: "npm", spec: "dev" }); - expect(parsePrismaNextVersionSpec("next")).toEqual({ kind: "npm", spec: "next" }); - expect(parsePrismaNextVersionSpec("latest")).toEqual({ kind: "npm", spec: "latest" }); - }); - - test("extracts a pkg.pr.new ref from the pkg-pr-new: prefix", () => { - expect(parsePrismaNextVersionSpec("pkg-pr-new:bad6795")).toEqual({ - kind: "pkg-pr-new", - ref: "bad6795", - }); - expect(parsePrismaNextVersionSpec("pkg-pr-new:aman/some-branch")).toEqual({ - kind: "pkg-pr-new", - ref: "aman/some-branch", - }); - expect(parsePrismaNextVersionSpec("pkg-pr-new:581")).toEqual({ - kind: "pkg-pr-new", - ref: "581", - }); - }); - - test("rejects an empty pkg-pr-new ref", () => { - expect(() => parsePrismaNextVersionSpec("pkg-pr-new:")).toThrow(/pkg-pr-new:/); - expect(() => parsePrismaNextVersionSpec("pkg-pr-new: ")).toThrow(/pkg-pr-new:/); - }); -}); - -describe("isPrismaNextPackage", () => { - test("matches prisma-next and @prisma-next/* scoped packages", () => { - expect(isPrismaNextPackage("prisma-next")).toBe(true); - expect(isPrismaNextPackage("@prisma-next/cli")).toBe(true); - expect(isPrismaNextPackage("@prisma-next/vite-plugin-contract-emit")).toBe(true); - }); - - test("ignores everything else", () => { - expect(isPrismaNextPackage("prisma")).toBe(false); - expect(isPrismaNextPackage("@prisma/client")).toBe(false); - expect(isPrismaNextPackage("dotenv")).toBe(false); - }); -}); - -describe("getPrismaNextPackageSpecifier", () => { - test("emits name@version for npm specs (default and explicit)", () => { - expect(getPrismaNextPackageSpecifier("prisma-next")).toBe("prisma-next@latest"); - expect(getPrismaNextPackageSpecifier("@prisma-next/cli", { kind: "npm", spec: "0.10.0" })).toBe( - "@prisma-next/cli@0.10.0", - ); - expect(getPrismaNextPackageSpecifier("@prisma-next/cli", { kind: "npm", spec: "dev" })).toBe( - "@prisma-next/cli@dev", - ); - }); - - test("emits a pkg.pr.new URL specifier for pkg-pr-new specs", () => { - expect( - getPrismaNextPackageSpecifier("prisma-next", { kind: "pkg-pr-new", ref: "bad6795" }), - ).toBe("https://pkg.pr.new/prisma/prisma-next/prisma-next@bad6795"); - expect( - getPrismaNextPackageSpecifier("@prisma-next/cli", { kind: "pkg-pr-new", ref: "581" }), - ).toBe("https://pkg.pr.new/prisma/prisma-next/@prisma-next/cli@581"); - }); -}); - -describe("getDependencyVersion", () => { - test("returns the npm spec verbatim for @prisma-next/* packages", () => { - expect(getDependencyVersion("prisma-next")).toBe("latest"); - expect(getDependencyVersion("@prisma-next/cli", { kind: "npm", spec: "0.10.0" })).toBe( - "0.10.0", - ); - }); - - test("returns the full pkg.pr.new URL when the spec is pkg-pr-new", () => { - expect(getDependencyVersion("prisma-next", { kind: "pkg-pr-new", ref: "bad6795" })).toBe( - "https://pkg.pr.new/prisma/prisma-next/prisma-next@bad6795", - ); - expect( - getDependencyVersion("@prisma-next/cli", { kind: "pkg-pr-new", ref: "aman/some-branch" }), - ).toBe("https://pkg.pr.new/prisma/prisma-next/@prisma-next/cli@aman/some-branch"); - }); - - test("ignores the Prisma Next spec for unrelated packages and uses dependencyVersionMap", () => { - expect(getDependencyVersion("dotenv", { kind: "pkg-pr-new", ref: "ignored" })).toBe("^17.4.2"); - expect(getDependencyVersion("tsx", { kind: "npm", spec: "0.10.0" })).toBe("^4.21.0"); - expect(getDependencyVersion("not-a-known-package")).toBeUndefined(); + test("returns undefined for dependencies missing from the version map", () => { + expect(getDependencyVersion("not-a-package")).toBeUndefined(); + expect(dependencyVersionMap.esbuild).toMatch(/^\^/); }); }); diff --git a/tests/e2e/create-prisma.e2e.test.ts b/tests/e2e/create-prisma.e2e.test.ts index 319d223..105f472 100644 --- a/tests/e2e/create-prisma.e2e.test.ts +++ b/tests/e2e/create-prisma.e2e.test.ts @@ -1,398 +1,141 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { startPrismaDevServer, type ServerOptions } from "@prisma/dev"; -import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { createServer } from "node:net"; +import { access, mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { runCreateCommand } from "../../src/commands/create"; -import { - authoringStyles, - createTemplates, - type AuthoringStyle, - type CreateTemplate, - type DatabaseProvider, -} from "../../src/types"; - -const TEST_TIMEOUT = Number(process.env.CREATE_PRISMA_E2E_TIMEOUT_MS ?? 240_000); -const MONGO_STARTUP_TIMEOUT = Number(process.env.CREATE_PRISMA_E2E_MONGO_TIMEOUT_MS ?? 90_000); -const PRISMA_NEXT_VERSION = process.env.CREATE_PRISMA_E2E_PRISMA_NEXT_VERSION; - -type DevDatabase = { - connectionString: string; - close(): Promise; -}; - -type GeneratedProject = { - projectDir: string; - rootDir: string; -}; +const TEST_TIMEOUT = Number(process.env.CREATE_PRISMA_E2E_TIMEOUT_MS ?? 300_000); const tempRoots: string[] = []; -function normalizePostgresConnectionString(raw: string): string { - const url = new URL(raw); - if (url.hostname === "localhost" || url.hostname === "::1") { - url.hostname = "127.0.0.1"; +async function pathExists(filePath: string) { + try { + await access(filePath); + return true; + } catch { + return false; } - return url.toString(); } -async function createDevDatabase(options?: ServerOptions): Promise { - const server = await startPrismaDevServer({ - databaseConnectTimeoutMillis: 1000, - databaseIdleTimeoutMillis: 1000, - ...options, - }); - - return { - connectionString: normalizePostgresConnectionString(server.database.connectionString), - close: () => server.close(), - }; -} - -async function runCommand( - projectDir: string, - args: string[], - extraEnv: Record = {}, -): Promise { - const proc = Bun.spawn({ +async function runCommand(projectDir: string, args: string[]) { + const process = Bun.spawn({ cmd: args, cwd: projectDir, - env: { - ...process.env, - CI: "1", - CREATE_PRISMA_DISABLE_TELEMETRY: "1", - ...extraEnv, - }, + env: { ...Bun.env, CI: "1", CREATE_PRISMA_DISABLE_TELEMETRY: "1" }, stdout: "pipe", stderr: "pipe", }); const [stdout, stderr, exitCode] = await Promise.all([ - new Response(proc.stdout).text(), - new Response(proc.stderr).text(), - proc.exited, + new Response(process.stdout).text(), + new Response(process.stderr).text(), + process.exited, ]); - if (exitCode !== 0) { - throw new Error( - [`Command failed: ${args.join(" ")}`, `Exit code: ${exitCode}`, stdout, stderr] - .filter(Boolean) - .join("\n"), - ); - } - - return [stdout, stderr].filter(Boolean).join("\n"); -} - -async function runScript( - projectDir: string, - scriptName: string, - extraEnv: Record = {}, -): Promise { - return runCommand(projectDir, ["npm", "run", scriptName], extraEnv); -} - -async function writeDatabaseUrl(projectDir: string, databaseUrl: string): Promise { - await writeFile(path.join(projectDir, ".env"), `DATABASE_URL=${JSON.stringify(databaseUrl)}\n`); -} - -async function pathExists(filePath: string): Promise { - try { - await access(filePath); - return true; - } catch { - return false; + throw new Error([`Command failed: ${args.join(" ")}`, stdout, stderr].join("\n")); } + return `${stdout}\n${stderr}`; } -async function readJsonFile(filePath: string): Promise> { - return JSON.parse(await readFile(filePath, "utf8")) as Record; -} - -async function scaffoldProject(opts: { - template: CreateTemplate; - provider: DatabaseProvider; - authoring: AuthoringStyle; - databaseUrl?: string; -}): Promise { - const { template, provider, authoring, databaseUrl } = opts; - const rootDir = await mkdtemp( - path.join(tmpdir(), `create-prisma-e2e-${template}-${provider}-${authoring}-`), +async function verifyComposerDev(projectDir: string) { + const process = Bun.spawn({ + cmd: ["bun", "run", "dev:composer"], + cwd: projectDir, + env: { ...Bun.env, CI: "1" }, + stdout: "pipe", + stderr: "pipe", + }); + const readers = [process.stdout.getReader(), process.stderr.getReader()]; + const pending = readers.map((reader, index) => + reader.read().then((result) => ({ index, result })), ); - tempRoots.push(rootDir); + const decoder = new TextDecoder(); + let output = ""; + let activeReaders = readers.length; + const deadline = Date.now() + 120_000; + let timeoutId: ReturnType | undefined; + const timeout = new Promise((_, reject) => { + timeoutId = setTimeout(() => reject(new Error("Timed out waiting for Composer dev.")), 120_000); + }); - const projectName = `${template}-${provider}-${authoring}-app`; - const previousCwd = process.cwd(); - process.chdir(rootDir); try { - await runCreateCommand({ - name: projectName, - template, - provider, - authoring, - packageManager: "npm", - databaseUrl, - prismaPostgres: false, - install: false, - emit: false, - prismaNextVersion: PRISMA_NEXT_VERSION, - yes: true, - }); - } finally { - process.chdir(previousCwd); - } - - return { - rootDir, - projectDir: path.join(rootDir, projectName), - }; -} - -function getPrismaModuleSpecifier(_template: CreateTemplate): string { - return "./src/prisma/users"; -} - -async function writeVerificationScript( - projectDir: string, - template: CreateTemplate, - provider: DatabaseProvider, -): Promise { - const verifyPath = path.join(projectDir, "verify-seed.ts"); - const modulePath = getPrismaModuleSpecifier(template); - const closeCall = "await db.close();"; - const aliceQuery = - provider === "mongo" - ? 'await db.orm.users.where({ email: "alice@prisma.io" }).first()' - : 'await db.orm.public.User.where({ email: "alice@prisma.io" }).first()'; - - const script = `import "dotenv/config"; -import { db, listUsers } from "${modulePath}"; - -try { - const users = await listUsers(); - const emails = users.map((user) => user.email).sort(); - if (users.length !== 3 || emails.join(",") !== "alice@prisma.io,bob@prisma.io,carol@prisma.io") { - throw new Error(\`Expected 3 seeded users, received \${JSON.stringify(users)}\`); - } - - const alice = ${aliceQuery}; - if (!alice || alice.name !== "Alice" || alice.username !== "alice") { - throw new Error(\`Expected Alice query result, received \${JSON.stringify(alice)}\`); - } -} finally { - ${closeCall} -} -`; - - await writeFile(verifyPath, script); - return verifyPath; -} - -async function installAndEmit(projectDir: string): Promise { - expect(await pathExists(path.join(projectDir, "prisma-next.md"))).toBe(true); - expect(await pathExists(path.join(projectDir, "src/prisma/db.ts"))).toBe(true); - expect(await pathExists(path.join(projectDir, "prisma"))).toBe(false); - expect(await pathExists(path.join(projectDir, ".env.example"))).toBe(true); - - await runCommand(projectDir, ["npm", "install"]); - await runScript(projectDir, "contract:emit"); -} - -async function verifyGeneratedProject( - projectDir: string, - template: CreateTemplate, - provider: DatabaseProvider, -): Promise { - const verifyPath = await writeVerificationScript(projectDir, template, provider); - await runCommand(projectDir, [path.join(projectDir, "node_modules/.bin/tsx"), verifyPath]); + while (Date.now() < deadline) { + const { index, result } = await Promise.race([...pending, timeout]); + if (result.done) { + activeReaders -= 1; + pending[index] = new Promise(() => {}); + if (activeReaders === 0) break; + continue; + } else { + output += decoder.decode(result.value, { stream: true }); + pending[index] = readers[index]!.read().then((nextResult) => ({ + index, + result: nextResult, + })); + } + const match = output.match(/app:\s+(http:\/\/localhost:\d+)/); + if (!match?.[1]) continue; - switch (template) { - case "minimal": - return; - case "next": - await runScript(projectDir, "lint"); - await runScript(projectDir, "build"); - return; - case "svelte": - await runScript(projectDir, "check"); - await runScript(projectDir, "build"); + const response = await fetch(match[1]); + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ users: [] }); return; - case "nuxt": - case "tanstack-start": - await runScript(projectDir, "typecheck"); - await runScript(projectDir, "build"); - return; - default: - await runScript(projectDir, "build"); + } + } finally { + clearTimeout(timeoutId); + process.kill(); + await process.exited; } -} - -function getFreePort(): Promise { - return new Promise((resolve, reject) => { - const server = createServer(); - server.unref(); - server.on("error", reject); - server.listen(0, "127.0.0.1", () => { - const address = server.address(); - if (typeof address !== "object" || address === null) { - server.close(); - reject(new Error("Could not determine free port")); - return; - } - const port = address.port; - server.close(() => resolve(port)); - }); - }); -} - -type MemoryMongoHandle = { - port: number; - stop: () => Promise; -}; -async function startMemoryMongo(projectDir: string): Promise { - const port = await getFreePort(); - await writeDatabaseUrl( - projectDir, - `mongodb://localhost:${port}/mydb?replicaSet=rs0&directConnection=true`, - ); - // db:up is detached and exits when ready, so this returns once the server is listening. - await runScript(projectDir, "db:up", { - MONGO_READY_TIMEOUT_MS: String(MONGO_STARTUP_TIMEOUT), - }); - return { - port, - stop: async () => { - try { - await runScript(projectDir, "db:down"); - } catch { - // best-effort cleanup - } - }, - }; + throw new Error(`Composer dev exited before becoming ready.\n${output}`); } afterEach(async () => { while (tempRoots.length > 0) { - const rootDir = tempRoots.pop(); - if (rootDir) { - await rm(rootDir, { recursive: true, force: true }); - } + const root = tempRoots.pop(); + if (root) await rm(root, { recursive: true, force: true }); } }); describe("create-prisma e2e", () => { test( - "accepts a positional project name", + "generates, builds, and runs a Composer-backed Prisma Postgres app", async () => { - const rootDir = await mkdtemp(path.join(tmpdir(), "create-prisma-e2e-positional-")); + const rootDir = await mkdtemp(path.join(tmpdir(), "create-prisma-next-e2e-")); tempRoots.push(rootDir); + const previousCwd = process.cwd(); + process.chdir(rootDir); + try { + await runCreateCommand({ + name: "composer-app", + template: "minimal", + provider: "postgres", + authoring: "psl", + packageManager: "bun", + deploy: false, + yes: true, + }); + } finally { + process.chdir(previousCwd); + } - const projectName = "positional-app"; - await runCommand(rootDir, [ - "bun", - path.join(process.cwd(), "src/cli.ts"), - projectName, - "--yes", - "--template", - "minimal", - "--provider", - "mongo", - "--package-manager", - "bun", - "--no-install", - "--no-emit", - ...(PRISMA_NEXT_VERSION ? ["--prisma-next-version", PRISMA_NEXT_VERSION] : []), - ]); - - const projectDir = path.join(rootDir, projectName); - expect(await pathExists(path.join(projectDir, "package.json"))).toBe(true); - expect(await pathExists(path.join(projectDir, "src/prisma/db.ts"))).toBe(true); - expect(await pathExists(path.join(projectDir, "prisma"))).toBe(false); + const projectDir = path.join(rootDir, "composer-app"); + const packageJson = JSON.parse( + await readFile(path.join(projectDir, "package.json"), "utf8"), + ) as Record; + const moduleSource = await readFile(path.join(projectDir, "module.ts"), "utf8"); + const dbSource = await readFile(path.join(projectDir, "src/prisma/db.ts"), "utf8"); + + expect(await pathExists(path.join(projectDir, "src/prisma/contract.json"))).toBe(true); + expect(await pathExists(path.join(projectDir, "prisma.config.ts"))).toBe(true); + expect(packageJson.scripts.deploy).toContain("bun run composer:deploy"); + expect(packageJson.overrides.effect).toBe("4.0.0-beta.103"); + expect(moduleSource).toContain("pnPostgres({"); + expect(dbSource).toContain("service.load().database.client"); + + await runCommand(projectDir, ["bun", "run", "build"]); + await runCommand(projectDir, ["bunx", "tsc", "--noEmit"]); + await verifyComposerDev(projectDir); }, TEST_TIMEOUT, ); - - for (const template of createTemplates) { - for (const authoring of authoringStyles) { - test( - `${template} + postgres + ${authoring} runs db init, migrations, seed, generated queries, and validation`, - async () => { - const initDb = await createDevDatabase(); - let project: GeneratedProject | undefined; - try { - project = await scaffoldProject({ - template, - provider: "postgres", - authoring, - databaseUrl: initDb.connectionString, - }); - await installAndEmit(project.projectDir); - - await runScript(project.projectDir, "db:init"); - await runScript(project.projectDir, "db:verify"); - } finally { - await initDb.close(); - } - - expect(project).toBeDefined(); - const migrationDb = await createDevDatabase(); - try { - await writeDatabaseUrl(project!.projectDir, migrationDb.connectionString); - await runScript(project!.projectDir, "migration:plan"); - await runScript(project!.projectDir, "migrate"); - await runScript(project!.projectDir, "db:seed"); - await verifyGeneratedProject(project!.projectDir, template, "postgres"); - } finally { - await migrationDb.close(); - } - }, - TEST_TIMEOUT, - ); - - test( - `${template} + mongo + ${authoring} provisions in-memory mongo and validates the app`, - async () => { - const project = await scaffoldProject({ template, provider: "mongo", authoring }); - - expect(await pathExists(path.join(project.projectDir, "scripts/mongo.mjs"))).toBe(true); - const mongoScript = await readFile( - path.join(project.projectDir, "scripts/mongo.mjs"), - "utf8", - ); - expect(mongoScript).toStartWith('import { spawn } from "node:child_process";'); - const pkgJson = await readJsonFile(path.join(project.projectDir, "package.json")); - const scriptsPkg = pkgJson.scripts as Record | undefined; - expect(scriptsPkg?.["db:up"]).toBe("node --env-file=.env scripts/mongo.mjs up"); - expect(scriptsPkg?.["db:down"]).toBe("node --env-file=.env scripts/mongo.mjs down"); - expect(scriptsPkg?.["db:reset"]).toBe("node --env-file=.env scripts/mongo.mjs reset"); - const devDeps = pkgJson.devDependencies as Record | undefined; - expect(devDeps?.["mongodb-memory-server"]).toBeDefined(); - expect(await pathExists(path.join(project.projectDir, "docker-compose.yml"))).toBe(false); - - await installAndEmit(project.projectDir); - - const mongo = await startMemoryMongo(project.projectDir); - try { - expect(await pathExists(path.join(project.projectDir, ".mongo-data", "db"))).toBe(true); - expect( - await pathExists(path.join(project.projectDir, ".mongo-data", "mongo.log")), - ).toBe(true); - expect( - await pathExists(path.join(project.projectDir, ".mongo-data", "mongo.pid")), - ).toBe(true); - - await runScript(project.projectDir, "migration:plan"); - await runScript(project.projectDir, "migrate"); - await runScript(project.projectDir, "db:seed"); - await verifyGeneratedProject(project.projectDir, template, "mongo"); - } finally { - await mongo.stop(); - } - }, - TEST_TIMEOUT, - ); - } - } }); diff --git a/tests/install.test.ts b/tests/install.test.ts index dcc3546..1aa1d5c 100644 --- a/tests/install.test.ts +++ b/tests/install.test.ts @@ -3,11 +3,15 @@ import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; -import { dependencyVersionMap, PRISMA_NEXT_DEFAULT_VERSION } from "../src/constants/dependencies"; +import { dependencyVersionMap } from "../src/constants/dependencies"; import { scaffoldCreateTemplate } from "../src/templates/render-create-template"; -import { writeCreateTemplateDependencies, writePrismaDependencies } from "../src/tasks/install"; +import { + getComposerScriptMap, + writeCreateTemplateDependencies, + writePrismaDependencies, +} from "../src/tasks/install"; import { authoringStyles, createTemplates, databaseProviders, packageManagers } from "../src/types"; -import { getDenoPrismaSpecifier, getInstallArgs } from "../src/utils/package-manager"; +import { getInstallArgs } from "../src/utils/package-manager"; type PackageJson = { dependencies?: Record; @@ -15,14 +19,10 @@ type PackageJson = { scripts?: Record; }; -async function withPackageJson( - packageJson: Record, - run: (projectDir: string) => Promise, -): Promise { +async function withPackageJson(run: (projectDir: string) => Promise): Promise { const projectDir = await mkdtemp(path.join(tmpdir(), "create-prisma-install-")); - try { - await writeFile(path.join(projectDir, "package.json"), JSON.stringify(packageJson, null, 2)); + await writeFile(path.join(projectDir, "package.json"), '{"name":"app"}\n'); return await run(projectDir); } finally { await rm(projectDir, { recursive: true, force: true }); @@ -33,7 +33,7 @@ async function readPackageJson(projectDir: string): Promise { return JSON.parse(await readFile(path.join(projectDir, "package.json"), "utf8")) as PackageJson; } -async function pathExists(filePath: string): Promise { +async function pathExists(filePath: string) { try { await access(filePath); return true; @@ -42,230 +42,74 @@ async function pathExists(filePath: string): Promise { } } -function expectPrismaNextPackagesUseLatest(packageJson: PackageJson): void { - const dependencies = { - ...packageJson.dependencies, - ...packageJson.devDependencies, - }; - - for (const [packageName, version] of Object.entries(dependencies)) { - if (packageName === "prisma-next" || packageName.startsWith("@prisma-next/")) { - expect(version).toBe(PRISMA_NEXT_DEFAULT_VERSION); - } - } -} - describe("writePrismaDependencies", () => { - test("writes Prisma Next dependencies and scripts before any install command runs", async () => { - await withPackageJson( - { - name: "app", - scripts: { - dev: "bun --watch src/index.ts", - }, - dependencies: { - hono: "^4.12.2", - }, - devDependencies: { - typescript: "^5.8.3", - }, - }, - async (projectDir) => { - await writePrismaDependencies("mongo", "bun", "typescript", projectDir); - - const packageJson = await readPackageJson(projectDir); - expectPrismaNextPackagesUseLatest(packageJson); + test("writes the Prisma Next Postgres runtime and Composer-ready scripts", async () => { + await withPackageJson(async (projectDir) => { + await writePrismaDependencies("postgres", "pnpm", "psl", projectDir); + const packageJson = await readPackageJson(projectDir); - expect(packageJson.dependencies).toMatchObject({ - "@prisma-next/mongo": PRISMA_NEXT_DEFAULT_VERSION, - dotenv: dependencyVersionMap.dotenv, - hono: "^4.12.2", - }); - expect(packageJson.devDependencies).toMatchObject({ - "@prisma-next/cli": PRISMA_NEXT_DEFAULT_VERSION, - "@prisma-next/mongo-contract-ts": PRISMA_NEXT_DEFAULT_VERSION, - "@prisma-next/mongo-orm": PRISMA_NEXT_DEFAULT_VERSION, - "@prisma-next/target-mongo": PRISMA_NEXT_DEFAULT_VERSION, - "@types/node": dependencyVersionMap["@types/node"], - "prisma-next": PRISMA_NEXT_DEFAULT_VERSION, - typescript: "^5.8.3", - }); - expect(packageJson.scripts).toMatchObject({ - dev: "bun --watch src/index.ts", - "contract:emit": "bun prisma-next contract emit", - "db:seed": "bun src/prisma/seed.ts", - "migration:plan": "bun prisma-next migration plan", - migrate: "bun prisma-next migrate", - }); - }, - ); + expect(packageJson.dependencies).toMatchObject({ + "@prisma/orm-postgres": dependencyVersionMap["@prisma/orm-postgres"], + dotenv: dependencyVersionMap.dotenv, + }); + expect(packageJson.devDependencies).toMatchObject({ + "prisma-next": dependencyVersionMap["prisma-next"], + }); + expect(packageJson.scripts).toMatchObject({ + "contract:emit": "prisma-next contract emit", + "db:seed": "tsx src/prisma/seed.ts", + }); + }); }); - test("pins every Prisma Next package to a non-default spec when one is provided", async () => { - await withPackageJson( - { - name: "app", - dependencies: {}, - devDependencies: {}, - }, - async (projectDir) => { - await writePrismaDependencies("postgres", "bun", "psl", projectDir, { - kind: "npm", - spec: "0.10.0", - }); - - const packageJson = await readPackageJson(projectDir); - const allDependencies = { - ...packageJson.dependencies, - ...packageJson.devDependencies, - }; - - for (const [packageName, version] of Object.entries(allDependencies)) { - if (packageName === "prisma-next" || packageName.startsWith("@prisma-next/")) { - expect(version).toBe("0.10.0"); - } - } - }, - ); + test("adds the MongoDB runtime and direct peer dependencies", async () => { + await withPackageJson(async (projectDir) => { + await writePrismaDependencies("mongo", "bun", "typescript", projectDir); + const packageJson = await readPackageJson(projectDir); + expect(packageJson.dependencies).toMatchObject({ + "@prisma/orm-mongo": dependencyVersionMap["@prisma/orm-mongo"], + arktype: dependencyVersionMap.arktype, + mongodb: dependencyVersionMap.mongodb, + }); + expect(packageJson.dependencies?.["@prisma/orm-postgres"]).toBeUndefined(); + expect(packageJson.scripts?.["db:seed"]).toBe("bun src/prisma/seed.ts"); + }); }); +}); - test("writes pkg.pr.new URL specifiers for every Prisma Next dependency", async () => { - await withPackageJson( - { - name: "app", - dependencies: {}, - devDependencies: {}, - }, - async (projectDir) => { - await writePrismaDependencies("postgres", "bun", "psl", projectDir, { - kind: "pkg-pr-new", - ref: "bad6795", - }); - - const packageJson = await readPackageJson(projectDir); - const allDependencies = { - ...packageJson.dependencies, - ...packageJson.devDependencies, - }; - - for (const [packageName, version] of Object.entries(allDependencies)) { - if (packageName === "prisma-next" || packageName.startsWith("@prisma-next/")) { - expect(version).toBe(`https://pkg.pr.new/prisma/prisma-next/${packageName}@bad6795`); - } - } - }, +describe("Composer package-manager commands", () => { + test("uses each selected package manager for Prisma CLI execution", () => { + expect(getComposerScriptMap("npm")["composer:deploy"]).toBe( + "npx --yes @prisma/cli@next composer deploy module.ts", ); - }); - - test("normalizes Prisma Next scripts after prisma-next init writes package-manager defaults", async () => { - await withPackageJson( - { - name: "app", - scripts: { - "contract:emit": "prisma-next contract emit", - }, - dependencies: {}, - devDependencies: {}, - }, - async (projectDir) => { - await writePrismaDependencies("mongo", "deno", "psl", projectDir); - - const packageJson = await readPackageJson(projectDir); - - expect(packageJson.scripts).toMatchObject({ - "contract:emit": "deno run -A --env-file=.env npm:prisma-next contract emit", - "migration:plan": "deno run -A --env-file=.env npm:prisma-next migration plan", - }); - }, + expect(getComposerScriptMap("pnpm")["composer:deploy"]).toBe( + "pnpm dlx @prisma/cli@next composer deploy module.ts", ); - }); -}); - -describe("writeCreateTemplateDependencies", () => { - test("adds Prisma Next Vite auto-emit plugin to Vite-backed templates", async () => { - await withPackageJson( - { - name: "app", - dependencies: {}, - devDependencies: { - vite: "^7.3.3", - }, - }, - async (projectDir) => { - await writeCreateTemplateDependencies({ - template: "svelte", - packageManager: "bun", - projectDir, - }); - - const packageJson = await readPackageJson(projectDir); - - expect(packageJson.devDependencies).toMatchObject({ - "@prisma-next/vite-plugin-contract-emit": PRISMA_NEXT_DEFAULT_VERSION, - vite: "^7.3.3", - }); - }, + expect(getComposerScriptMap("yarn")["composer:deploy"]).toBe( + "yarn dlx @prisma/cli@next composer deploy module.ts", ); - }); - - test("does not add the Vite plugin to non-Vite API templates", async () => { - await withPackageJson( - { - name: "app", - dependencies: {}, - devDependencies: {}, - }, - async (projectDir) => { - await writeCreateTemplateDependencies({ - template: "hono", - packageManager: "bun", - projectDir, - }); - - const packageJson = await readPackageJson(projectDir); - - expect(packageJson.devDependencies).not.toHaveProperty( - "@prisma-next/vite-plugin-contract-emit", - ); - }, + expect(getComposerScriptMap("bun")["composer:deploy"]).toBe( + "bunx @prisma/cli@next composer deploy module.ts", ); }); - test("adds tsx to Minimal projects for Node-style package managers", async () => { - await withPackageJson( - { - name: "app", - dependencies: {}, - devDependencies: {}, - }, - async (projectDir) => { - await writeCreateTemplateDependencies({ - template: "minimal", - packageManager: "npm", - projectDir, - }); - - const packageJson = await readPackageJson(projectDir); - - expect(packageJson.devDependencies).toMatchObject({ - tsx: dependencyVersionMap.tsx, - }); - expect(packageJson.devDependencies).not.toHaveProperty( - "@prisma-next/vite-plugin-contract-emit", - ); - }, - ); + test("keeps installs package-manager native", () => { + for (const packageManager of packageManagers) { + expect(getInstallArgs(packageManager)).toEqual({ + command: packageManager, + args: ["install"], + }); + } }); }); -describe("scaffoldCreateTemplate", () => { - test("renders every template, provider, authoring style, and package manager combination", async () => { +describe("generated templates", () => { + test("renders Composer into every supported combination", async () => { for (const template of createTemplates) { for (const provider of databaseProviders) { for (const authoring of authoringStyles) { for (const packageManager of packageManagers) { - const projectDir = await mkdtemp(path.join(tmpdir(), "create-prisma-template-matrix-")); - + const projectDir = await mkdtemp(path.join(tmpdir(), "create-prisma-matrix-")); try { await scaffoldCreateTemplate({ projectDir, @@ -275,32 +119,46 @@ describe("scaffoldCreateTemplate", () => { authoring, packageManager, }); + await writeCreateTemplateDependencies({ template, packageManager, projectDir }); const packageJson = await readPackageJson(projectDir); - const readme = await readFile(path.join(projectDir, "README.md"), "utf8"); - const seed = await readFile(path.join(projectDir, "src/prisma/seed.ts"), "utf8"); - const users = await readFile(path.join(projectDir, "src/prisma/users.ts"), "utf8"); - const accessor = provider === "postgres" ? "db.orm.public.User" : "db.orm.users"; - const contractExtension = authoring === "typescript" ? ".ts" : ".prisma"; - - expect(packageJson.scripts?.dev).toBeDefined(); - expect(readme).toContain(`src/prisma/contract${contractExtension}`); - expect(seed).toContain(`${accessor}.create(user)`); - expect(seed).not.toContain("createCount"); - expect(users).toContain(`${accessor}.select`); - if (template === "hono" || template === "next") { - expect(packageJson.devDependencies?.typescript).toBe("^5.9.3"); - } - if (provider === "mongo") { - expect(readme).toContain("requires Node.js 24+"); + const moduleSource = await readFile(path.join(projectDir, "module.ts"), "utf8"); + const serviceSource = await readFile(path.join(projectDir, "service.ts"), "utf8"); + const prismaConfig = await readFile( + path.join(projectDir, "prisma.config.ts"), + "utf8", + ); + + expect(packageJson.scripts?.deploy).toBeDefined(); + expect(packageJson.dependencies).toHaveProperty("@prisma/composer"); + expect(prismaConfig).toContain('configPath: "./prisma-composer.config.ts"'); + expect(serviceSource).toContain("compute({"); + if (provider === "postgres") { + expect(moduleSource).toContain("pnPostgres({"); + } else { + expect(moduleSource).toContain('envSecret("MONGODB_URL")'); } - if (template === "next") { - const eslintConfig = await readFile( - path.join(projectDir, "eslint.config.mjs"), - "utf8", + expect(await pathExists(path.join(projectDir, "deno.json"))).toBe(false); + if (packageManager === "pnpm") { + expect(packageJson.pnpm).toBeUndefined(); + expect(await readFile(path.join(projectDir, "pnpm-workspace.yaml"), "utf8")).toBe( + [ + "allowBuilds:", + " esbuild: true", + " msgpackr-extract: true", + " workerd: true", + "minimumReleaseAgeExclude:", + ' - "@prisma/*"', + ' - "prisma-next"', + "overrides:", + ' effect: "4.0.0-beta.103"', + "", + ].join("\n"), ); - expect(eslintConfig).toContain('"migrations/**"'); - expect(eslintConfig).toContain('"src/prisma/**/*.d.ts"'); + } else if (packageManager === "yarn") { + expect(packageJson.resolutions?.effect).toBe("4.0.0-beta.103"); + } else { + expect(packageJson.overrides?.effect).toBe("4.0.0-beta.103"); } } finally { await rm(projectDir, { recursive: true, force: true }); @@ -310,150 +168,4 @@ describe("scaffoldCreateTemplate", () => { } } }); - - test("loads dotenv in seed scripts for Node package managers", async () => { - const projectDir = await mkdtemp(path.join(tmpdir(), "create-prisma-template-")); - - try { - await scaffoldCreateTemplate({ - projectDir, - projectName: "app", - template: "hono", - provider: "postgres", - authoring: "psl", - packageManager: "npm", - }); - - const seed = await readFile(path.join(projectDir, "src/prisma/seed.ts"), "utf8"); - expect(seed).toContain('import "dotenv/config";'); - } finally { - await rm(projectDir, { recursive: true, force: true }); - } - }); - - test("does not add dotenv imports to Bun or Deno seed scripts", async () => { - for (const packageManager of ["bun", "deno"] as const) { - const projectDir = await mkdtemp(path.join(tmpdir(), "create-prisma-template-")); - - try { - await scaffoldCreateTemplate({ - projectDir, - projectName: "app", - template: "hono", - provider: "postgres", - authoring: "psl", - packageManager, - }); - - const seed = await readFile(path.join(projectDir, "src/prisma/seed.ts"), "utf8"); - expect(seed).not.toContain('import "dotenv/config";'); - } finally { - await rm(projectDir, { recursive: true, force: true }); - } - } - }); - - test("renders Minimal as a script-first template without a build pipeline", async () => { - const projectDir = await mkdtemp(path.join(tmpdir(), "create-prisma-template-")); - - try { - await scaffoldCreateTemplate({ - projectDir, - projectName: "app", - template: "minimal", - provider: "mongo", - authoring: "psl", - packageManager: "bun", - }); - - const packageJson = await readPackageJson(projectDir); - const index = await readFile(path.join(projectDir, "src/index.ts"), "utf8"); - - expect(packageJson.scripts).toEqual({ - dev: "bun src/index.ts", - }); - expect(index).toContain("db.orm.users"); - expect(index).toContain("db.orm.users.create(sampleUser)"); - expect(index).not.toContain("createCount"); - expect(index).toContain('username: "first-user"'); - expect(index).toContain("Prisma Next is ready"); - expect(await readFile(path.join(projectDir, "src/prisma/users.ts"), "utf8")).toContain( - 'from "./db', - ); - expect(await pathExists(path.join(projectDir, "prisma"))).toBe(false); - } finally { - await rm(projectDir, { recursive: true, force: true }); - } - }); - - test("renders current Prisma Next ORM accessors for both database targets", async () => { - for (const provider of ["postgres", "mongo"] as const) { - const projectDir = await mkdtemp(path.join(tmpdir(), "create-prisma-template-")); - - try { - await scaffoldCreateTemplate({ - projectDir, - projectName: "app", - template: "hono", - provider, - authoring: "psl", - packageManager: "bun", - }); - - const seed = await readFile(path.join(projectDir, "src/prisma/seed.ts"), "utf8"); - const users = await readFile(path.join(projectDir, "src/prisma/users.ts"), "utf8"); - const accessor = provider === "postgres" ? "db.orm.public.User" : "db.orm.users"; - - expect(seed).toContain(`${accessor}.where`); - expect(seed).toContain(`${accessor}.create(user)`); - expect(seed).not.toContain("createCount"); - expect(seed).toContain("await db.close()"); - expect(users).toContain(`${accessor}.select`); - } finally { - await rm(projectDir, { recursive: true, force: true }); - } - } - }); - - test("renders the Nuxt typecheck dependency", async () => { - const projectDir = await mkdtemp(path.join(tmpdir(), "create-prisma-template-")); - - try { - await scaffoldCreateTemplate({ - projectDir, - projectName: "app", - template: "nuxt", - provider: "mongo", - authoring: "psl", - packageManager: "bun", - }); - - const packageJson = await readPackageJson(projectDir); - expect(packageJson.scripts?.typecheck).toBe("nuxt typecheck"); - expect(packageJson.devDependencies?.["vue-tsc"]).toBe("^3.3.9"); - } finally { - await rm(projectDir, { recursive: true, force: true }); - } - }); -}); - -describe("getInstallArgs", () => { - test("keeps non-Deno installs as plain package-manager install commands", () => { - expect(getInstallArgs("npm")).toEqual({ command: "npm", args: ["install"] }); - expect(getInstallArgs("pnpm")).toEqual({ command: "pnpm", args: ["install"] }); - expect(getInstallArgs("yarn")).toEqual({ command: "yarn", args: ["install"] }); - expect(getInstallArgs("bun")).toEqual({ command: "bun", args: ["install"] }); - }); - - test("uses Deno-compatible npm specifiers for Deno installs", () => { - expect(PRISMA_NEXT_DEFAULT_VERSION).toBe("latest"); - expect(getDenoPrismaSpecifier()).toBe("npm:prisma-next"); - expect(getInstallArgs("deno")).toEqual({ - command: "deno", - args: [ - "install", - "--allow-scripts=npm:prisma-next,npm:@prisma-next/postgres,npm:@prisma-next/mongo,npm:mongodb-memory-server", - ], - }); - }); }); diff --git a/tests/node-version.test.ts b/tests/node-version.test.ts new file mode 100644 index 0000000..529cb4f --- /dev/null +++ b/tests/node-version.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, test } from "bun:test"; + +import { getUnsupportedNodeMessage, supportsPrismaNext } from "../src/utils/node-version"; + +describe("Prisma Next Node compatibility", () => { + test("requires Node 22.18 or newer", () => { + expect(supportsPrismaNext("22.17.9")).toBe(false); + expect(supportsPrismaNext("22.18.0")).toBe(true); + expect(supportsPrismaNext("24.0.0")).toBe(true); + }); + + test("returns an actionable message", () => { + expect(getUnsupportedNodeMessage("20.19.0")).toContain("Required: Node.js 22.18 or newer."); + }); +}); diff --git a/tests/setup-prisma.test.ts b/tests/setup-prisma.test.ts index 34d5ab9..c2f46f2 100644 --- a/tests/setup-prisma.test.ts +++ b/tests/setup-prisma.test.ts @@ -7,7 +7,6 @@ import { collectPrismaSetupContext } from "../src/tasks/setup-prisma"; async function withTempProject(run: (projectDir: string) => Promise): Promise { const projectDir = await mkdtemp(path.join(tmpdir(), "create-prisma-setup-")); - try { return await run(projectDir); } finally { @@ -16,52 +15,39 @@ async function withTempProject(run: (projectDir: string) => Promise): Prom } describe("collectPrismaSetupContext", () => { - test("--yes provisions Prisma Postgres by default for PostgreSQL", async () => { + test("--yes uses Prisma Postgres defaults without deploying", async () => { await withTempProject(async (projectDir) => { const context = await collectPrismaSetupContext( - { - yes: true, - provider: "postgres", - packageManager: "bun", - install: false, - }, + { yes: true, packageManager: "bun" }, { projectDir }, ); - expect(context?.shouldUsePrismaPostgres).toBe(true); + expect(context).toMatchObject({ + databaseProvider: "postgres", + authoring: "psl", + packageManager: "bun", + shouldDeploy: false, + }); }); }); - test("--yes does not provision Prisma Postgres when DATABASE_URL is supplied", async () => { + test("honors an explicit immediate deployment", async () => { await withTempProject(async (projectDir) => { const context = await collectPrismaSetupContext( - { - yes: true, - provider: "postgres", - databaseUrl: "postgresql://user:password@localhost:5432/mydb", - packageManager: "bun", - install: false, - }, + { yes: true, packageManager: "pnpm", deploy: true }, { projectDir }, ); - - expect(context?.shouldUsePrismaPostgres).toBe(false); + expect(context?.shouldDeploy).toBe(true); }); }); - test("--yes does not provision Prisma Postgres for MongoDB", async () => { + test("keeps MongoDB as an explicit provider option", async () => { await withTempProject(async (projectDir) => { const context = await collectPrismaSetupContext( - { - yes: true, - provider: "mongo", - packageManager: "bun", - install: false, - }, + { yes: true, provider: "mongo", packageManager: "npm" }, { projectDir }, ); - - expect(context?.shouldUsePrismaPostgres).toBe(false); + expect(context?.databaseProvider).toBe("mongo"); }); }); }); diff --git a/tests/telemetry.test.ts b/tests/telemetry.test.ts index a2a23a3..e725894 100644 --- a/tests/telemetry.test.ts +++ b/tests/telemetry.test.ts @@ -1,17 +1,11 @@ import { beforeEach, describe, expect, mock, test } from "bun:test"; import type { CreatePromptContext } from "../src/commands/create"; -import { - DEFAULT_PRISMA_NEXT_SPEC, - type ResolvedPrismaNextSpec, -} from "../src/constants/dependencies"; import type { CreateCommandInput } from "../src/types"; const trackCliTelemetry = mock(async () => {}); -mock.module("../src/telemetry/client", () => ({ - trackCliTelemetry, -})); +mock.module("../src/telemetry/client", () => ({ trackCliTelemetry })); const { CREATE_PRISMA_NEXT_COMPLETED_EVENT, @@ -20,114 +14,41 @@ const { trackCreateFailed, } = await import("../src/telemetry/create"); -const createInput = { - yes: true, - name: "app", -} satisfies CreateCommandInput; - -function makeCreateContext( - prismaNextSpec: ResolvedPrismaNextSpec = DEFAULT_PRISMA_NEXT_SPEC, -): CreatePromptContext { - return { - targetDirectory: "/tmp/app", - targetPathState: { - exists: false, - isDirectory: true, - isEmptyDirectory: true, - }, - force: false, - template: "hono", - projectPackageName: "app", - prismaSetupContext: { - projectDir: "/tmp/app", - verbose: false, - shouldEmit: true, - databaseProvider: "mongo", - authoring: "psl", - shouldUsePrismaPostgres: false, - packageManager: "bun", - shouldInstall: true, - prismaNextSpec, - }, - }; -} - -const createContext = makeCreateContext(); - -beforeEach(() => { - trackCliTelemetry.mockClear(); -}); +const createInput = { yes: true, name: "app" } satisfies CreateCommandInput; +const createContext: CreatePromptContext = { + targetDirectory: "/tmp/app", + targetPathState: { exists: false, isDirectory: true, isEmptyDirectory: true }, + force: false, + template: "hono", + projectPackageName: "app", + prismaSetupContext: { + projectDir: "/tmp/app", + verbose: false, + databaseProvider: "postgres", + authoring: "psl", + packageManager: "bun", + shouldDeploy: true, + }, +}; + +beforeEach(() => trackCliTelemetry.mockClear()); describe("create telemetry", () => { - test("tracks Prisma Next-specific completion events", async () => { - await trackCreateCompleted({ - input: createInput, - context: createContext, - durationMs: 123, - }); - + test("tracks Composer deployment intent on completion", async () => { + await trackCreateCompleted({ input: createInput, context: createContext, durationMs: 123 }); expect(trackCliTelemetry).toHaveBeenCalledWith( CREATE_PRISMA_NEXT_COMPLETED_EVENT, expect.objectContaining({ command: "create", template: "hono", - "database-provider": "mongo", + "database-provider": "postgres", + "should-deploy": true, "duration-ms": 123, - "prisma-next-version-kind": "default", - "prisma-next-version-spec": "latest", - }), - ); - }); - - test("classifies a published Prisma Next version as npm-version", async () => { - await trackCreateCompleted({ - input: { ...createInput, prismaNextVersion: "0.10.0" }, - context: makeCreateContext({ kind: "npm", spec: "0.10.0" }), - durationMs: 1, - }); - - expect(trackCliTelemetry).toHaveBeenCalledWith( - CREATE_PRISMA_NEXT_COMPLETED_EVENT, - expect.objectContaining({ - "prisma-next-version-kind": "npm-version", - "prisma-next-version-spec": "0.10.0", }), ); }); - test("classifies a non-default dist-tag as npm-tag", async () => { - await trackCreateCompleted({ - input: { ...createInput, prismaNextVersion: "dev" }, - context: makeCreateContext({ kind: "npm", spec: "dev" }), - durationMs: 1, - }); - - expect(trackCliTelemetry).toHaveBeenCalledWith( - CREATE_PRISMA_NEXT_COMPLETED_EVENT, - expect.objectContaining({ - "prisma-next-version-kind": "npm-tag", - "prisma-next-version-spec": "dev", - }), - ); - }); - - test("classifies a pkg-pr-new spec and round-trips the ref in the spec field", async () => { - await trackCreateCompleted({ - input: { ...createInput, prismaNextVersion: "pkg-pr-new:bad6795" }, - context: makeCreateContext({ kind: "pkg-pr-new", ref: "bad6795" }), - durationMs: 1, - }); - - expect(trackCliTelemetry).toHaveBeenCalledWith( - CREATE_PRISMA_NEXT_COMPLETED_EVENT, - expect.objectContaining({ - "prisma-next-version-kind": "pkg-pr-new", - "prisma-next-version-spec": "pkg-pr-new:bad6795", - }), - ); - }); - - test("tracks Prisma Next-specific failure events", async () => { + test("tracks setup failures", async () => { await trackCreateFailed({ input: createInput, context: createContext, @@ -135,31 +56,13 @@ describe("create telemetry", () => { error: Object.assign(new Error("boom"), { code: "ERR_TEST" }), stage: "prisma_setup", }); - expect(trackCliTelemetry).toHaveBeenCalledWith( CREATE_PRISMA_NEXT_FAILED_EVENT, expect.objectContaining({ - command: "create", "duration-ms": 456, "error-code": "ERR_TEST", "failure-stage": "prisma_setup", }), ); }); - - test("falls back to the raw input spec when no context is available on failure", async () => { - await trackCreateFailed({ - input: { ...createInput, prismaNextVersion: "0.11.0-dev.9" }, - durationMs: 1, - stage: "validate_input", - }); - - expect(trackCliTelemetry).toHaveBeenCalledWith( - CREATE_PRISMA_NEXT_FAILED_EVENT, - expect.objectContaining({ - "prisma-next-version-kind": "default", - "prisma-next-version-spec": "0.11.0-dev.9", - }), - ); - }); }); From 9832003b749db3dbfdd531c909ab13693a4b282f Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Thu, 13 Aug 2026 20:30:02 +0530 Subject: [PATCH 02/19] fix: update tsdown dependency warning config Signed-off-by: Aman Varshney --- tsdown.config.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tsdown.config.ts b/tsdown.config.ts index b016457..379e1ca 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -8,9 +8,7 @@ const telemetryHost = process.env.CREATE_PRISMA_TELEMETRY_HOST || "https://us.i. export default defineConfig({ entry: ["src/index.ts", "src/cli.ts"], format: ["esm"], - deps: { - onlyBundle: false, - }, + inlineOnly: false, clean: true, shims: true, dts: true, From 42150ab8a92ca7731dc58932f67a80ce9232128c Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Fri, 14 Aug 2026 14:13:52 +0530 Subject: [PATCH 03/19] fix: isolate generated TypeScript contract artifacts Write emitted contract JSON and types to a generated directory so TypeScript does not resolve contract.d.ts to the adjacent contract.ts source file. Signed-off-by: Aman Varshney --- templates/create/_shared/.gitattributes.hbs | 11 +++++ .../create/_shared/prisma-next.config.ts.hbs | 12 +++++ .../create/_shared/src/prisma/composer.ts.hbs | 4 +- templates/create/_shared/src/prisma/db.ts.hbs | 8 ++-- tests/e2e/create-prisma.e2e.test.ts | 45 +++++++++++++++++++ tests/install.test.ts | 36 +++++++++++++++ 6 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 templates/create/_shared/.gitattributes.hbs create mode 100644 templates/create/_shared/prisma-next.config.ts.hbs diff --git a/templates/create/_shared/.gitattributes.hbs b/templates/create/_shared/.gitattributes.hbs new file mode 100644 index 0000000..fe00c01 --- /dev/null +++ b/templates/create/_shared/.gitattributes.hbs @@ -0,0 +1,11 @@ +{{#if (eq authoring "typescript")}} +src/prisma/generated/contract.json linguist-generated +src/prisma/generated/contract.d.ts linguist-generated +{{else}} +src/prisma/contract.json linguist-generated +src/prisma/contract.d.ts linguist-generated +{{/if}} +src/prisma/ops.json linguist-generated +src/prisma/migration.json linguist-generated +migrations/snapshots/**/contract.json linguist-generated +migrations/snapshots/**/contract.d.ts linguist-generated diff --git a/templates/create/_shared/prisma-next.config.ts.hbs b/templates/create/_shared/prisma-next.config.ts.hbs new file mode 100644 index 0000000..57a031e --- /dev/null +++ b/templates/create/_shared/prisma-next.config.ts.hbs @@ -0,0 +1,12 @@ +import "dotenv/config"; +import { defineConfig } from "@prisma/orm-{{#if (eq provider "postgres")}}postgres{{else}}mongo{{/if}}/config"; + +export default defineConfig({ + contract: "./src/prisma/contract{{#if (eq authoring "typescript")}}.ts{{else}}.prisma{{/if}}", +{{#if (eq authoring "typescript")}} + output: "./src/prisma/generated", +{{/if}} + db: { + connection: process.env.DATABASE_URL!, + }, +}); diff --git a/templates/create/_shared/src/prisma/composer.ts.hbs b/templates/create/_shared/src/prisma/composer.ts.hbs index fad5c5e..71ee7fe 100644 --- a/templates/create/_shared/src/prisma/composer.ts.hbs +++ b/templates/create/_shared/src/prisma/composer.ts.hbs @@ -1,8 +1,8 @@ {{#if (eq provider "postgres")}} import { pnContract } from "@prisma/composer-prisma-cloud/prisma-next"; -import type { Contract } from "./contract.d.ts"; -import contractJson from "./contract.json" with { type: "json" }; +import type { Contract } from "./{{#if (eq authoring "typescript")}}generated/{{/if}}contract.d.ts"; +import contractJson from "./{{#if (eq authoring "typescript")}}generated/{{/if}}contract.json" with { type: "json" }; export const appContract = pnContract(contractJson); {{/if}} diff --git a/templates/create/_shared/src/prisma/db.ts.hbs b/templates/create/_shared/src/prisma/db.ts.hbs index 0533753..3f2e6f6 100644 --- a/templates/create/_shared/src/prisma/db.ts.hbs +++ b/templates/create/_shared/src/prisma/db.ts.hbs @@ -2,8 +2,8 @@ import postgres from "@prisma/orm-postgres/runtime"; import service from "../../service.ts"; -import type { Contract } from "./contract.d.ts"; -import contractJson from "./contract.json" with { type: "json" }; +import type { Contract } from "./{{#if (eq authoring "typescript")}}generated/{{/if}}contract.d.ts"; +import contractJson from "./{{#if (eq authoring "typescript")}}generated/{{/if}}contract.json" with { type: "json" }; function loadComposerDatabase() { try { @@ -23,8 +23,8 @@ export const db = import mongo from "@prisma/orm-mongo/runtime"; import service from "../../service.ts"; -import type { Contract } from "./contract.d.ts"; -import contractJson from "./contract.json" with { type: "json" }; +import type { Contract } from "./{{#if (eq authoring "typescript")}}generated/{{/if}}contract.d.ts"; +import contractJson from "./{{#if (eq authoring "typescript")}}generated/{{/if}}contract.json" with { type: "json" }; function getDatabaseUrl(): string { try { diff --git a/tests/e2e/create-prisma.e2e.test.ts b/tests/e2e/create-prisma.e2e.test.ts index 105f472..ee48dbb 100644 --- a/tests/e2e/create-prisma.e2e.test.ts +++ b/tests/e2e/create-prisma.e2e.test.ts @@ -138,4 +138,49 @@ describe("create-prisma e2e", () => { }, TEST_TIMEOUT, ); + + test( + "builds a Next.js app with a TypeScript-authored contract", + async () => { + const rootDir = await mkdtemp(path.join(tmpdir(), "create-prisma-next-typescript-e2e-")); + tempRoots.push(rootDir); + const previousCwd = process.cwd(); + process.chdir(rootDir); + try { + await runCreateCommand({ + name: "next-typescript-app", + template: "next", + provider: "postgres", + authoring: "typescript", + packageManager: "bun", + deploy: false, + yes: true, + }); + } finally { + process.chdir(previousCwd); + } + + const projectDir = path.join(rootDir, "next-typescript-app"); + const composerSource = await readFile( + path.join(projectDir, "src/prisma/composer.ts"), + "utf8", + ); + const dbSource = await readFile(path.join(projectDir, "src/prisma/db.ts"), "utf8"); + + expect(composerSource).toContain( + 'import type { Contract } from "./generated/contract.d.ts";', + ); + expect(dbSource).toContain('import type { Contract } from "./generated/contract.d.ts";'); + expect(await pathExists(path.join(projectDir, "src/prisma/generated/contract.json"))).toBe( + true, + ); + expect(await pathExists(path.join(projectDir, "src/prisma/generated/contract.d.ts"))).toBe( + true, + ); + + await runCommand(projectDir, ["bun", "run", "build"]); + await runCommand(projectDir, ["bunx", "tsc", "--noEmit"]); + }, + TEST_TIMEOUT, + ); }); diff --git a/tests/install.test.ts b/tests/install.test.ts index 1aa1d5c..5409b8e 100644 --- a/tests/install.test.ts +++ b/tests/install.test.ts @@ -124,6 +124,7 @@ describe("generated templates", () => { const packageJson = await readPackageJson(projectDir); const moduleSource = await readFile(path.join(projectDir, "module.ts"), "utf8"); const serviceSource = await readFile(path.join(projectDir, "service.ts"), "utf8"); + const dbSource = await readFile(path.join(projectDir, "src/prisma/db.ts"), "utf8"); const prismaConfig = await readFile( path.join(projectDir, "prisma.config.ts"), "utf8", @@ -135,9 +136,44 @@ describe("generated templates", () => { expect(serviceSource).toContain("compute({"); if (provider === "postgres") { expect(moduleSource).toContain("pnPostgres({"); + const composerSource = await readFile( + path.join(projectDir, "src/prisma/composer.ts"), + "utf8", + ); + if (authoring === "typescript") { + expect(composerSource).toContain( + 'import type { Contract } from "./generated/contract.d.ts";', + ); + expect(composerSource).toContain( + 'import contractJson from "./generated/contract.json"', + ); + } else { + expect(composerSource).toContain( + 'import type { Contract } from "./contract.d.ts";', + ); + expect(composerSource).toContain("pnContract(contractJson)"); + } } else { expect(moduleSource).toContain('envSecret("MONGODB_URL")'); } + if (authoring === "typescript") { + expect(dbSource).toContain( + 'import type { Contract } from "./generated/contract.d.ts";', + ); + expect(dbSource).toContain('import contractJson from "./generated/contract.json"'); + } else { + expect(dbSource).toContain('import type { Contract } from "./contract.d.ts";'); + expect(dbSource).toContain("contractJson,"); + } + const prismaNextConfig = await readFile( + path.join(projectDir, "prisma-next.config.ts"), + "utf8", + ); + if (authoring === "typescript") { + expect(prismaNextConfig).toContain('output: "./src/prisma/generated"'); + } else { + expect(prismaNextConfig).not.toContain("output:"); + } expect(await pathExists(path.join(projectDir, "deno.json"))).toBe(false); if (packageManager === "pnpm") { expect(packageJson.pnpm).toBeUndefined(); From 8805e9bb07a7172d09568d63dc79c513145603bf Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Fri, 14 Aug 2026 14:40:24 +0530 Subject: [PATCH 04/19] fix: pin the Prisma platform CLI prerelease Use the aligned RC2 package for init, authentication, and Composer commands so package-manager tag caches cannot resolve an older CLI without the ORM command. Signed-off-by: Aman Varshney --- README.md | 2 +- src/constants/dependencies.ts | 4 ++++ src/tasks/deploy-with-composer.ts | 7 +++---- src/tasks/install.ts | 10 ++++++---- src/tasks/setup-prisma.ts | 4 ++-- tests/dependencies.test.ts | 1 + tests/install.test.ts | 10 +++++----- 7 files changed, 22 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 58f9760..e0b386e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ yarn dlx create-prisma@next my-app bunx create-prisma@next my-app ``` -The CLI initializes Prisma Next with `@prisma/cli@next`, installs dependencies, emits the contract, and generates a deployable Composer app. PostgreSQL projects use Composer's native Prisma Postgres provider, including migrations and a typed runtime client. +The CLI initializes Prisma Next with the aligned `@prisma/cli` prerelease, installs dependencies, emits the contract, and generates a deployable Composer app. PostgreSQL projects use Composer's native Prisma Postgres provider, including migrations and a typed runtime client. The only Composer prompt is: diff --git a/src/constants/dependencies.ts b/src/constants/dependencies.ts index cda03d8..d45a90f 100644 --- a/src/constants/dependencies.ts +++ b/src/constants/dependencies.ts @@ -3,6 +3,7 @@ import type { CreateTemplate, PackageManager } from "../types"; export const dependencyVersionMap = { "@astrojs/node": "^10.0.2", "@elysiajs/node": "^1.4.5", + "@prisma/cli": "8.0.0-rc.2", "@prisma/cli-engine": "8.0.0-rc.2", "@prisma/composer": "0.6.0-dev.18", "@prisma/composer-prisma-cloud": "0.6.0-dev.18", @@ -23,6 +24,9 @@ export const dependencyVersionMap = { typescript: "^5.9.3", } as const; +export const PRISMA_PLATFORM_CLI_PACKAGE = + `@prisma/cli@${dependencyVersionMap["@prisma/cli"]}` as const; + export type AvailableDependency = keyof typeof dependencyVersionMap; export type CreateTemplateDependencyTarget = { diff --git a/src/tasks/deploy-with-composer.ts b/src/tasks/deploy-with-composer.ts index 29109c7..fe2acf2 100644 --- a/src/tasks/deploy-with-composer.ts +++ b/src/tasks/deploy-with-composer.ts @@ -1,6 +1,7 @@ import { log, spinner } from "@clack/prompts"; import { execa } from "execa"; +import { PRISMA_PLATFORM_CLI_PACKAGE } from "../constants/dependencies"; import type { PackageManager } from "../types"; import { getPackageExecutionArgs, @@ -9,8 +10,6 @@ import { getRunScriptCommand, } from "../utils/package-manager"; -const PRISMA_CLI_PACKAGE = "@prisma/cli@next"; - type PrismaCliEnvelope = { ok: boolean; result?: unknown; @@ -54,7 +53,7 @@ export function parsePrismaCliEnvelope(output: string): PrismaCliEnvelope { } function getPrismaCliArgs(packageManager: PackageManager, args: string[]) { - return getPackageExecutionArgs(packageManager, [PRISMA_CLI_PACKAGE, ...args]); + return getPackageExecutionArgs(packageManager, [PRISMA_PLATFORM_CLI_PACKAGE, ...args]); } async function isAuthenticated(packageManager: PackageManager, projectDir: string) { @@ -83,7 +82,7 @@ async function ensureAuthentication(packageManager: PackageManager, projectDir: if (await isAuthenticated(packageManager, projectDir)) return; const loginCommand = getPackageExecutionCommand(packageManager, [ - PRISMA_CLI_PACKAGE, + PRISMA_PLATFORM_CLI_PACKAGE, "auth", "login", ]); diff --git a/src/tasks/install.ts b/src/tasks/install.ts index e78ced4..94b58af 100644 --- a/src/tasks/install.ts +++ b/src/tasks/install.ts @@ -2,7 +2,11 @@ import { execa } from "execa"; import fs from "fs-extra"; import path from "node:path"; -import { getCreateTemplateDependencies, getDependencyVersion } from "../constants/dependencies"; +import { + getCreateTemplateDependencies, + getDependencyVersion, + PRISMA_PLATFORM_CLI_PACKAGE, +} from "../constants/dependencies"; import { getDbPackages } from "../constants/db-packages"; import type { AuthoringStyle, CreateTemplate, DatabaseProvider, PackageManager } from "../types"; import { @@ -11,8 +15,6 @@ import { getRunScriptCommand, } from "../utils/package-manager"; -const PRISMA_CLI_PACKAGE = "@prisma/cli@next"; - function getPrismaNextScriptMap(packageManager: PackageManager): Record { return { "contract:emit": "prisma-next contract emit", @@ -30,7 +32,7 @@ function getPrismaNextScriptMap(packageManager: PackageManager): Record { const composerCommand = (subcommand: string, extraArgs: string[] = []) => getPackageExecutionCommand(packageManager, [ - PRISMA_CLI_PACKAGE, + PRISMA_PLATFORM_CLI_PACKAGE, "composer", subcommand, "module.ts", diff --git a/src/tasks/setup-prisma.ts b/src/tasks/setup-prisma.ts index ff26595..62b6459 100644 --- a/src/tasks/setup-prisma.ts +++ b/src/tasks/setup-prisma.ts @@ -3,6 +3,7 @@ import { execa } from "execa"; import fs from "fs-extra"; import path from "node:path"; +import { PRISMA_PLATFORM_CLI_PACKAGE } from "../constants/dependencies"; import { scaffoldCreateSharedTemplates } from "../templates/render-create-template"; import { AuthoringStyleSchema, @@ -25,7 +26,6 @@ import { import { deployWithComposer } from "./deploy-with-composer"; import { installProjectDependencies, writePrismaDependencies } from "./install"; -const PRISMA_CLI_PACKAGE = "@prisma/cli@next"; const DEFAULT_DATABASE_PROVIDER: DatabaseProvider = "postgres"; const DEFAULT_AUTHORING: AuthoringStyle = "psl"; @@ -177,7 +177,7 @@ function getInitTarget(provider: DatabaseProvider): "postgres" | "mongodb" { } function getPrismaCliInvocation(packageManager: PackageManager, args: string[]) { - return getPackageExecutionArgs(packageManager, [PRISMA_CLI_PACKAGE, ...args]); + return getPackageExecutionArgs(packageManager, [PRISMA_PLATFORM_CLI_PACKAGE, ...args]); } async function runPrismaInit(context: PrismaSetupContext, projectDir: string): Promise { diff --git a/tests/dependencies.test.ts b/tests/dependencies.test.ts index e9e07ce..d8efbf8 100644 --- a/tests/dependencies.test.ts +++ b/tests/dependencies.test.ts @@ -6,6 +6,7 @@ describe("Prisma Next dependency versions", () => { test("uses the aligned Prisma Next and Composer releases", () => { expect(getDependencyVersion("prisma-next")).toBe("8.0.0-rc.1"); expect(getDependencyVersion("@prisma/orm-postgres")).toBe("8.0.0-rc.1"); + expect(getDependencyVersion("@prisma/cli")).toBe("8.0.0-rc.2"); expect(getDependencyVersion("@prisma/cli-engine")).toBe("8.0.0-rc.2"); expect(getDependencyVersion("@prisma/composer")).toBe("0.6.0-dev.18"); expect(getDependencyVersion("@prisma/composer-prisma-cloud")).toBe("0.6.0-dev.18"); diff --git a/tests/install.test.ts b/tests/install.test.ts index 5409b8e..16f0794 100644 --- a/tests/install.test.ts +++ b/tests/install.test.ts @@ -3,7 +3,7 @@ import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; -import { dependencyVersionMap } from "../src/constants/dependencies"; +import { dependencyVersionMap, PRISMA_PLATFORM_CLI_PACKAGE } from "../src/constants/dependencies"; import { scaffoldCreateTemplate } from "../src/templates/render-create-template"; import { getComposerScriptMap, @@ -80,16 +80,16 @@ describe("writePrismaDependencies", () => { describe("Composer package-manager commands", () => { test("uses each selected package manager for Prisma CLI execution", () => { expect(getComposerScriptMap("npm")["composer:deploy"]).toBe( - "npx --yes @prisma/cli@next composer deploy module.ts", + `npx --yes ${PRISMA_PLATFORM_CLI_PACKAGE} composer deploy module.ts`, ); expect(getComposerScriptMap("pnpm")["composer:deploy"]).toBe( - "pnpm dlx @prisma/cli@next composer deploy module.ts", + `pnpm dlx ${PRISMA_PLATFORM_CLI_PACKAGE} composer deploy module.ts`, ); expect(getComposerScriptMap("yarn")["composer:deploy"]).toBe( - "yarn dlx @prisma/cli@next composer deploy module.ts", + `yarn dlx ${PRISMA_PLATFORM_CLI_PACKAGE} composer deploy module.ts`, ); expect(getComposerScriptMap("bun")["composer:deploy"]).toBe( - "bunx @prisma/cli@next composer deploy module.ts", + `bunx ${PRISMA_PLATFORM_CLI_PACKAGE} composer deploy module.ts`, ); }); From df3359efd07fe3553abb6b82a343ca6d69b7db64 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Fri, 14 Aug 2026 15:11:57 +0530 Subject: [PATCH 05/19] fix: show deployed app URL --- package.json | 2 +- src/tasks/deploy-with-composer.ts | 20 ++++++++++++++++++-- tests/deploy-with-composer.test.ts | 27 +++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 tests/deploy-with-composer.test.ts diff --git a/package.json b/package.json index 0de1faf..a1195bb 100644 --- a/package.json +++ b/package.json @@ -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/dependencies.test.ts ./tests/install.test.ts ./tests/node-version.test.ts ./tests/setup-prisma.test.ts ./tests/telemetry.test.ts", + "test:unit": "bun test ./tests/dependencies.test.ts ./tests/deploy-with-composer.test.ts ./tests/install.test.ts ./tests/node-version.test.ts ./tests/setup-prisma.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/src/tasks/deploy-with-composer.ts b/src/tasks/deploy-with-composer.ts index fe2acf2..bc5a9c7 100644 --- a/src/tasks/deploy-with-composer.ts +++ b/src/tasks/deploy-with-composer.ts @@ -52,6 +52,13 @@ export function parsePrismaCliEnvelope(output: string): PrismaCliEnvelope { throw new Error("Prisma CLI returned output that is not a valid result envelope."); } +export function extractDeploymentUrl(output: string): string | undefined { + return output + .match(/https:\/\/[a-z0-9.-]+\.prisma\.build\/?/gi) + ?.at(-1) + ?.replace(/\/$/, ""); +} + function getPrismaCliArgs(packageManager: PackageManager, args: string[]) { return getPackageExecutionArgs(packageManager, [PRISMA_PLATFORM_CLI_PACKAGE, ...args]); } @@ -117,14 +124,23 @@ export async function deployWithComposer(options: { progress?.start("Deploying to Prisma..."); const command = getRunScriptArgs(options.packageManager, "deploy"); - await execa(command.command, command.args, { + const result = await execa(command.command, command.args, { cwd: options.projectDir, env: process.env, stdio: options.verbose ? "inherit" : "pipe", }); progress?.stop("Deployed to Prisma."); - if (options.verbose) log.success("Deployed to Prisma."); + if (options.verbose) { + log.success("Deployed to Prisma."); + } else { + const deploymentUrl = extractDeploymentUrl( + [result.stdout, result.stderr] + .filter((value): value is string => typeof value === "string") + .join("\n"), + ); + if (deploymentUrl) log.info(`App: ${deploymentUrl}`); + } return true; } catch (error) { progress?.stop("Deployment failed."); diff --git a/tests/deploy-with-composer.test.ts b/tests/deploy-with-composer.test.ts new file mode 100644 index 0000000..7ffd8ce --- /dev/null +++ b/tests/deploy-with-composer.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, test } from "bun:test"; + +import { extractDeploymentUrl } from "../src/tasks/deploy-with-composer"; + +describe("extractDeploymentUrl", () => { + test("returns the deployed Prisma Compute URL", () => { + expect( + extractDeploymentUrl(` +app compute-service cps_abc123 + https://abc123.ewr.prisma.build +Done: 22 succeeded +`), + ).toBe("https://abc123.ewr.prisma.build"); + }); + + test("uses the final deployed URL and removes a trailing slash", () => { + expect( + extractDeploymentUrl( + "Previous: https://old.ewr.prisma.build\nCurrent: https://new.fra.prisma.build/", + ), + ).toBe("https://new.fra.prisma.build"); + }); + + test("returns undefined when deploy output has no Compute URL", () => { + expect(extractDeploymentUrl("Done: 22 succeeded")).toBeUndefined(); + }); +}); From 73a0985370c652fac4a1c870f1327d598f7abf00 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Fri, 14 Aug 2026 15:28:09 +0530 Subject: [PATCH 06/19] fix: expose Elysia on Compute --- templates/create/elysia/src/index.ts.hbs | 2 +- tests/install.test.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/templates/create/elysia/src/index.ts.hbs b/templates/create/elysia/src/index.ts.hbs index 0e1d87d..d326ff3 100644 --- a/templates/create/elysia/src/index.ts.hbs +++ b/templates/create/elysia/src/index.ts.hbs @@ -32,6 +32,6 @@ const app = new Elysia({ adapter: node() }) return users; }) - .listen(port); + .listen({ port, hostname: "0.0.0.0" }); console.log(`Server running at http://localhost:${app.server?.port ?? port}`); diff --git a/tests/install.test.ts b/tests/install.test.ts index 16f0794..1b5f70b 100644 --- a/tests/install.test.ts +++ b/tests/install.test.ts @@ -134,6 +134,10 @@ describe("generated templates", () => { expect(packageJson.dependencies).toHaveProperty("@prisma/composer"); expect(prismaConfig).toContain('configPath: "./prisma-composer.config.ts"'); expect(serviceSource).toContain("compute({"); + if (template === "elysia") { + const serverSource = await readFile(path.join(projectDir, "src/index.ts"), "utf8"); + expect(serverSource).toContain('.listen({ port, hostname: "0.0.0.0" })'); + } if (provider === "postgres") { expect(moduleSource).toContain("pnPostgres({"); const composerSource = await readFile( From 2a3ec87fa51d98590306ed5f309f0a565c308b17 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Fri, 14 Aug 2026 15:37:23 +0530 Subject: [PATCH 07/19] fix: select the Elysia runtime adapter --- templates/create/elysia/src/index.ts.hbs | 2 +- tests/install.test.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/create/elysia/src/index.ts.hbs b/templates/create/elysia/src/index.ts.hbs index d326ff3..b0443c2 100644 --- a/templates/create/elysia/src/index.ts.hbs +++ b/templates/create/elysia/src/index.ts.hbs @@ -11,7 +11,7 @@ const parsedPort = rawPort.length > 0 ? Number(rawPort) : Number.NaN; const port = Number.isInteger(parsedPort) && parsedPort >= 0 && parsedPort <= 65535 ? parsedPort : 3000; -const app = new Elysia({ adapter: node() }) +const app = new Elysia({ adapter: "Bun" in globalThis ? undefined : node() }) .get("/", () => { return { message: "hello from create-prisma + elysia", diff --git a/tests/install.test.ts b/tests/install.test.ts index 1b5f70b..cb9013c 100644 --- a/tests/install.test.ts +++ b/tests/install.test.ts @@ -136,6 +136,7 @@ describe("generated templates", () => { expect(serviceSource).toContain("compute({"); if (template === "elysia") { const serverSource = await readFile(path.join(projectDir, "src/index.ts"), "utf8"); + expect(serverSource).toContain('adapter: "Bun" in globalThis ? undefined : node()'); expect(serverSource).toContain('.listen({ port, hostname: "0.0.0.0" })'); } if (provider === "postgres") { From aaa88dcc7c5dcf1fa45e424a9f5bd435d622884b Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Fri, 14 Aug 2026 16:13:39 +0530 Subject: [PATCH 08/19] fix: address deploy matrix failures --- src/commands/create.ts | 2 ++ src/constants/dependencies.ts | 2 +- .../create/_shared/pnpm-workspace.yaml.hbs | 6 ++++++ templates/create/nest/package.json.hbs | 2 +- .../create/nest/src/users.controller.ts.hbs | 4 ++-- .../create/nest/src/users.service.ts.hbs | 4 ++-- tests/install.test.ts | 21 +++++++++++++++++++ 7 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/commands/create.ts b/src/commands/create.ts index ce8ad30..1634d3a 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -206,6 +206,7 @@ export async function runCreateCommand(rawInput: CreateCommandInput = {}): Promi failureStage = "unknown"; const executionResult = await executeCreateContext(context); if (!executionResult.ok) { + process.exitCode = 1; if (executionResult.error) { cancel( `Create command failed: ${ @@ -232,6 +233,7 @@ export async function runCreateCommand(rawInput: CreateCommandInput = {}): Promi durationMs: Date.now() - startedAt, }); } catch (error) { + process.exitCode = 1; cancel(`Create command failed: ${error instanceof Error ? error.message : String(error)}`); await trackCreateFailed({ input, diff --git a/src/constants/dependencies.ts b/src/constants/dependencies.ts index d45a90f..f03eab7 100644 --- a/src/constants/dependencies.ts +++ b/src/constants/dependencies.ts @@ -50,7 +50,7 @@ export function getCreateTemplateDependencies( template: CreateTemplate, _packageManager: PackageManager, ): CreateTemplateDependencyTarget[] { - const dependencies = ["@prisma/composer", "@prisma/composer-prisma-cloud", "alchemy"]; + const dependencies = ["@prisma/composer", "@prisma/composer-prisma-cloud"]; const devDependencies = ["@prisma/cli-engine"]; if (usesEsbuild(template)) { diff --git a/templates/create/_shared/pnpm-workspace.yaml.hbs b/templates/create/_shared/pnpm-workspace.yaml.hbs index c728565..514ed31 100644 --- a/templates/create/_shared/pnpm-workspace.yaml.hbs +++ b/templates/create/_shared/pnpm-workspace.yaml.hbs @@ -2,6 +2,12 @@ allowBuilds: esbuild: true msgpackr-extract: true +{{#if (eq template "next")}} + sharp: true + unrs-resolver: true +{{else if (eq template "astro")}} + sharp: true +{{/if}} workerd: true minimumReleaseAgeExclude: - "@prisma/*" diff --git a/templates/create/nest/package.json.hbs b/templates/create/nest/package.json.hbs index b408920..21388c4 100644 --- a/templates/create/nest/package.json.hbs +++ b/templates/create/nest/package.json.hbs @@ -7,7 +7,7 @@ "type": "module", "scripts": { "dev": "{{runtimeScript packageManager "dev" "src/main.ts" "dist/main.js"}}", - "build": "esbuild src/main.ts --bundle --platform=node --format=esm --outfile=dist/server.mjs --banner:js=\"import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);\" --external:@nestjs/websockets/* --external:@nestjs/microservices --external:@nestjs/microservices/* --external:@nestjs/platform-socket.io --external:class-transformer --external:class-validator", + "build": "esbuild src/main.ts --bundle --platform=node --format=esm --outfile=dist/server.mjs --banner:js=\"import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);\" --external:'@nestjs/websockets/*' --external:@nestjs/microservices --external:'@nestjs/microservices/*' --external:@nestjs/platform-socket.io --external:class-transformer --external:class-validator", "start": "node dist/server.mjs" }, "dependencies": { diff --git a/templates/create/nest/src/users.controller.ts.hbs b/templates/create/nest/src/users.controller.ts.hbs index 1d9abc2..f799b28 100644 --- a/templates/create/nest/src/users.controller.ts.hbs +++ b/templates/create/nest/src/users.controller.ts.hbs @@ -1,10 +1,10 @@ -import { Controller, Get } from "@nestjs/common"; +import { Controller, Get, Inject } from "@nestjs/common"; import { UsersService } from "./users.service"; @Controller("users") export class UsersController { - constructor(private readonly usersService: UsersService) {} + constructor(@Inject(UsersService) private readonly usersService: UsersService) {} @Get() findAll() { diff --git a/templates/create/nest/src/users.service.ts.hbs b/templates/create/nest/src/users.service.ts.hbs index 25ffa77..60c873d 100644 --- a/templates/create/nest/src/users.service.ts.hbs +++ b/templates/create/nest/src/users.service.ts.hbs @@ -1,10 +1,10 @@ -import { Injectable } from "@nestjs/common"; +import { Inject, Injectable } from "@nestjs/common"; import { PrismaService } from "./prisma.service"; @Injectable() export class UsersService { - constructor(private readonly prisma: PrismaService) {} + constructor(@Inject(PrismaService) private readonly prisma: PrismaService) {} async findAll() { return this.prisma.listUsers(10); diff --git a/tests/install.test.ts b/tests/install.test.ts index cb9013c..2a52939 100644 --- a/tests/install.test.ts +++ b/tests/install.test.ts @@ -132,6 +132,7 @@ describe("generated templates", () => { expect(packageJson.scripts?.deploy).toBeDefined(); expect(packageJson.dependencies).toHaveProperty("@prisma/composer"); + expect(packageJson.dependencies?.alchemy).toBeUndefined(); expect(prismaConfig).toContain('configPath: "./prisma-composer.config.ts"'); expect(serviceSource).toContain("compute({"); if (template === "elysia") { @@ -139,6 +140,19 @@ describe("generated templates", () => { expect(serverSource).toContain('adapter: "Bun" in globalThis ? undefined : node()'); expect(serverSource).toContain('.listen({ port, hostname: "0.0.0.0" })'); } + if (template === "nest") { + expect(packageJson.scripts?.build).toContain("--external:'@nestjs/websockets/*'"); + const usersServiceSource = await readFile( + path.join(projectDir, "src/users.service.ts"), + "utf8", + ); + const usersControllerSource = await readFile( + path.join(projectDir, "src/users.controller.ts"), + "utf8", + ); + expect(usersServiceSource).toContain("@Inject(PrismaService)"); + expect(usersControllerSource).toContain("@Inject(UsersService)"); + } if (provider === "postgres") { expect(moduleSource).toContain("pnPostgres({"); const composerSource = await readFile( @@ -182,11 +196,18 @@ describe("generated templates", () => { expect(await pathExists(path.join(projectDir, "deno.json"))).toBe(false); if (packageManager === "pnpm") { expect(packageJson.pnpm).toBeUndefined(); + const frameworkBuildAllowances = + template === "next" + ? [" sharp: true", " unrs-resolver: true"] + : template === "astro" + ? [" sharp: true"] + : []; expect(await readFile(path.join(projectDir, "pnpm-workspace.yaml"), "utf8")).toBe( [ "allowBuilds:", " esbuild: true", " msgpackr-extract: true", + ...frameworkBuildAllowances, " workerd: true", "minimumReleaseAgeExclude:", ' - "@prisma/*"', From 4d0844c9179685dfb3933196a2d883fbba814c87 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Fri, 14 Aug 2026 16:25:36 +0530 Subject: [PATCH 09/19] fix: align Composer framework dependencies --- src/cli.ts | 10 ++++- src/constants/dependencies.ts | 2 +- templates/create/astro/package.json.hbs | 2 +- templates/create/nuxt/package.json.hbs | 2 +- templates/create/svelte/.npmrc | 1 - templates/create/svelte/package.json.hbs | 4 +- .../create/tanstack-start/package.json.hbs | 6 +-- tests/e2e/create-prisma.e2e.test.ts | 41 ++++++++++++++++++- tests/install.test.ts | 2 +- 9 files changed, 58 insertions(+), 12 deletions(-) delete mode 100644 templates/create/svelte/.npmrc diff --git a/src/cli.ts b/src/cli.ts index 77178b4..68206ca 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,3 +1,11 @@ import { createCreatePrismaCli } from "./index"; -createCreatePrismaCli().run(); +createCreatePrismaCli().run({ + process: { + exit(code): never { + const commandExitCode = + typeof process.exitCode === "number" && process.exitCode !== 0 ? process.exitCode : code; + process.exit(commandExitCode); + }, + }, +}); diff --git a/src/constants/dependencies.ts b/src/constants/dependencies.ts index f03eab7..d45a90f 100644 --- a/src/constants/dependencies.ts +++ b/src/constants/dependencies.ts @@ -50,7 +50,7 @@ export function getCreateTemplateDependencies( template: CreateTemplate, _packageManager: PackageManager, ): CreateTemplateDependencyTarget[] { - const dependencies = ["@prisma/composer", "@prisma/composer-prisma-cloud"]; + const dependencies = ["@prisma/composer", "@prisma/composer-prisma-cloud", "alchemy"]; const devDependencies = ["@prisma/cli-engine"]; if (usesEsbuild(template)) { diff --git a/templates/create/astro/package.json.hbs b/templates/create/astro/package.json.hbs index 67aa970..355ac7c 100644 --- a/templates/create/astro/package.json.hbs +++ b/templates/create/astro/package.json.hbs @@ -18,6 +18,6 @@ "@types/node": "^24.3.0", "tsx": "^4.7.1", "typescript": "^5.9.3", - "vite": "^7.3.3" + "vite": "^8.1.2" } } diff --git a/templates/create/nuxt/package.json.hbs b/templates/create/nuxt/package.json.hbs index 1bbfe3a..4935fda 100644 --- a/templates/create/nuxt/package.json.hbs +++ b/templates/create/nuxt/package.json.hbs @@ -23,7 +23,7 @@ "@types/node": "^24.3.0", "tsx": "^4.7.1", "typescript": "^5.9.3", - "vite": "^7.3.3", + "vite": "^8.1.2", "vue-tsc": "^3.3.9" } } diff --git a/templates/create/svelte/.npmrc b/templates/create/svelte/.npmrc deleted file mode 100644 index b6f27f1..0000000 --- a/templates/create/svelte/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true diff --git a/templates/create/svelte/package.json.hbs b/templates/create/svelte/package.json.hbs index c707b94..a153eb2 100644 --- a/templates/create/svelte/package.json.hbs +++ b/templates/create/svelte/package.json.hbs @@ -16,12 +16,12 @@ }, "devDependencies": { "@sveltejs/kit": "^2.50.2", - "@sveltejs/vite-plugin-svelte": "^6.2.4", + "@sveltejs/vite-plugin-svelte": "^7.3.0", "@types/node": "^24.3.0", "svelte": "^5.51.0", "svelte-check": "^4.4.2", "tsx": "^4.7.1", "typescript": "^5.9.3", - "vite": "^7.3.3" + "vite": "^8.1.2" } } diff --git a/templates/create/tanstack-start/package.json.hbs b/templates/create/tanstack-start/package.json.hbs index 5b2b7e7..ac89f40 100644 --- a/templates/create/tanstack-start/package.json.hbs +++ b/templates/create/tanstack-start/package.json.hbs @@ -23,10 +23,10 @@ "@types/node": "^24.3.0", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", - "@vitejs/plugin-react": "^5.1.4", + "@vitejs/plugin-react": "^5.2.0", "tsx": "^4.7.1", "typescript": "^5.9.3", - "vite": "^7.3.3", - "vite-tsconfig-paths": "^5.1.4" + "vite": "^8.1.2", + "vite-tsconfig-paths": "^6.1.1" } } diff --git a/tests/e2e/create-prisma.e2e.test.ts b/tests/e2e/create-prisma.e2e.test.ts index ee48dbb..434c5e2 100644 --- a/tests/e2e/create-prisma.e2e.test.ts +++ b/tests/e2e/create-prisma.e2e.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { access, mkdtemp, readFile, rm } from "node:fs/promises"; +import { access, mkdir, mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; @@ -97,6 +97,45 @@ afterEach(async () => { }); describe("create-prisma e2e", () => { + test("returns a non-zero exit code when project setup fails", async () => { + const rootDir = await mkdtemp(path.join(tmpdir(), "create-prisma-exit-code-e2e-")); + tempRoots.push(rootDir); + const emptyBinDir = path.join(rootDir, "empty-bin"); + await mkdir(emptyBinDir); + + const child = Bun.spawn({ + cmd: [ + process.execPath, + path.join(import.meta.dir, "../../src/cli.ts"), + "create", + "failed-app", + "--template", + "minimal", + "--provider", + "postgres", + "--authoring", + "psl", + "--package-manager", + "npm", + "--no-deploy", + "--yes", + ], + cwd: rootDir, + env: { + ...Bun.env, + PATH: emptyBinDir, + CI: "1", + CREATE_PRISMA_DISABLE_TELEMETRY: "1", + }, + stdout: "pipe", + stderr: "pipe", + }); + + const exitCode = await child.exited; + expect(exitCode).toBe(1); + expect(await pathExists(path.join(rootDir, "failed-app", "package.json"))).toBe(true); + }); + test( "generates, builds, and runs a Composer-backed Prisma Postgres app", async () => { diff --git a/tests/install.test.ts b/tests/install.test.ts index 2a52939..c55a6d5 100644 --- a/tests/install.test.ts +++ b/tests/install.test.ts @@ -132,7 +132,7 @@ describe("generated templates", () => { expect(packageJson.scripts?.deploy).toBeDefined(); expect(packageJson.dependencies).toHaveProperty("@prisma/composer"); - expect(packageJson.dependencies?.alchemy).toBeUndefined(); + expect(packageJson.dependencies).toHaveProperty("alchemy"); expect(prismaConfig).toContain('configPath: "./prisma-composer.config.ts"'); expect(serviceSource).toContain("compute({"); if (template === "elysia") { From e7bcdcae21c62e4b2e46c4b707fcf2162f7aab02 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Fri, 14 Aug 2026 17:47:49 +0530 Subject: [PATCH 10/19] fix: bundle SvelteKit runtime dependencies Keep SvelteKit on adapter-node while asking Vite to inline the application runtime packages required by the self-contained Composer build artifact. Signed-off-by: Aman Varshney --- templates/create/svelte/vite.config.ts | 3 +++ tests/install.test.ts | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/templates/create/svelte/vite.config.ts b/templates/create/svelte/vite.config.ts index 80864b9..c9e8522 100644 --- a/templates/create/svelte/vite.config.ts +++ b/templates/create/svelte/vite.config.ts @@ -3,4 +3,7 @@ import { defineConfig } from "vite"; export default defineConfig({ plugins: [sveltekit()], + ssr: { + noExternal: [/^@prisma\//, "arktype", "dotenv", "mongodb"], + }, }); diff --git a/tests/install.test.ts b/tests/install.test.ts index c55a6d5..fb74df0 100644 --- a/tests/install.test.ts +++ b/tests/install.test.ts @@ -153,6 +153,12 @@ describe("generated templates", () => { expect(usersServiceSource).toContain("@Inject(PrismaService)"); expect(usersControllerSource).toContain("@Inject(UsersService)"); } + if (template === "svelte") { + const viteConfig = await readFile(path.join(projectDir, "vite.config.ts"), "utf8"); + expect(viteConfig).toContain( + 'noExternal: [/^@prisma\\//, "arktype", "dotenv", "mongodb"]', + ); + } if (provider === "postgres") { expect(moduleSource).toContain("pnPostgres({"); const composerSource = await readFile( From 7dd06bb1ecc6a57c07440976691c3802a866c887 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Fri, 14 Aug 2026 18:21:58 +0530 Subject: [PATCH 11/19] fix: inline all SvelteKit SSR dependencies Use Vite noExternal for the full server dependency graph so the adapter-node build is self-contained for Composer. Signed-off-by: Aman Varshney --- templates/create/svelte/vite.config.ts | 2 +- tests/install.test.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/templates/create/svelte/vite.config.ts b/templates/create/svelte/vite.config.ts index c9e8522..cd00276 100644 --- a/templates/create/svelte/vite.config.ts +++ b/templates/create/svelte/vite.config.ts @@ -4,6 +4,6 @@ import { defineConfig } from "vite"; export default defineConfig({ plugins: [sveltekit()], ssr: { - noExternal: [/^@prisma\//, "arktype", "dotenv", "mongodb"], + noExternal: true, }, }); diff --git a/tests/install.test.ts b/tests/install.test.ts index fb74df0..33ca213 100644 --- a/tests/install.test.ts +++ b/tests/install.test.ts @@ -155,9 +155,7 @@ describe("generated templates", () => { } if (template === "svelte") { const viteConfig = await readFile(path.join(projectDir, "vite.config.ts"), "utf8"); - expect(viteConfig).toContain( - 'noExternal: [/^@prisma\\//, "arktype", "dotenv", "mongodb"]', - ); + expect(viteConfig).toContain("noExternal: true"); } if (provider === "postgres") { expect(moduleSource).toContain("pnPostgres({"); From fb04dd099e35523407eb18e20a08ac621ef85906 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Mon, 17 Aug 2026 16:50:01 +0530 Subject: [PATCH 12/19] fix: default immediate deployment to yes Signed-off-by: Aman Varshney --- src/tasks/setup-prisma.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasks/setup-prisma.ts b/src/tasks/setup-prisma.ts index 62b6459..d453bb8 100644 --- a/src/tasks/setup-prisma.ts +++ b/src/tasks/setup-prisma.ts @@ -117,7 +117,7 @@ async function promptForPackageManager( async function promptForDeployment(): Promise { const shouldDeploy = await confirm({ message: "Deploy to Prisma now?", - initialValue: false, + initialValue: true, }); if (isCancel(shouldDeploy)) { cancel("Operation cancelled."); From 2ed19458dc562de7f9f4c9c81fd70bfa7ada4b4d Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Tue, 18 Aug 2026 17:17:00 +0530 Subject: [PATCH 13/19] chore: call the release Prisma 8 Signed-off-by: Aman Varshney --- README.md | 6 +++--- package.json | 2 +- src/commands/create.ts | 10 +++++----- src/tasks/setup-prisma.ts | 12 ++++++------ src/types.ts | 2 +- src/ui/branding.ts | 4 ++-- templates/create/_shared/README.md.hbs | 4 ++-- templates/create/astro/src/pages/index.astro.hbs | 4 ++-- templates/create/next/src/app/page.tsx.hbs | 4 ++-- templates/create/nuxt/app/pages/index.vue.hbs | 4 ++-- templates/create/svelte/src/routes/+page.svelte.hbs | 4 ++-- .../create/tanstack-start/src/routes/__root.tsx.hbs | 4 ++-- .../create/tanstack-start/src/routes/index.tsx.hbs | 4 ++-- tests/dependencies.test.ts | 4 ++-- tests/install.test.ts | 2 +- tests/node-version.test.ts | 2 +- 16 files changed, 36 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index e0b386e..efc082d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # create-prisma -Create a Prisma Next app with Prisma Composer built in. +Create a Prisma 8 app with Prisma Composer built in. ## Quick start @@ -13,7 +13,7 @@ yarn dlx create-prisma@next my-app bunx create-prisma@next my-app ``` -The CLI initializes Prisma Next with the aligned `@prisma/cli` prerelease, installs dependencies, emits the contract, and generates a deployable Composer app. PostgreSQL projects use Composer's native Prisma Postgres provider, including migrations and a typed runtime client. +The CLI initializes Prisma 8 with the aligned `@prisma/cli` prerelease, installs dependencies, emits the contract, and generates a deployable Composer app. PostgreSQL projects use Composer's native Prisma Postgres provider, including migrations and a typed runtime client. The only Composer prompt is: @@ -49,7 +49,7 @@ PostgreSQL and MongoDB are supported with PSL or TypeScript contract authoring. - `--force` - `--verbose` -This branch intentionally targets Prisma Next only. It does not generate a Prisma 7 compatibility path. +This branch intentionally targets Prisma 8 only. It does not generate a Prisma 7 compatibility path. ## Development diff --git a/package.json b/package.json index a1195bb..a6a2693 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "create-prisma", "version": "0.4.2", "private": false, - "description": "Create Prisma Next projects with first-party templates and great DX.", + "description": "Create Prisma 8 projects with first-party templates and great DX.", "homepage": "https://github.com/prisma/create-prisma", "bugs": { "url": "https://github.com/prisma/create-prisma/issues" diff --git a/src/commands/create.ts b/src/commands/create.ts index 1634d3a..27dc4f8 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -101,7 +101,7 @@ async function promptForCreateTemplate(): Promise { { value: "minimal", label: "Minimal", - hint: "Script-first Prisma Next starter with no web framework", + hint: "Script-first Prisma 8 starter with no web framework", }, { value: "hono", @@ -310,7 +310,7 @@ async function executeCreateContext( context: CreatePromptContext, ): Promise { const createSpinner = context.prismaSetupContext.verbose ? undefined : spinner(); - createSpinner?.start("Creating Prisma Next project..."); + createSpinner?.start("Creating Prisma 8 project..."); try { if (context.prismaSetupContext.verbose) { @@ -330,7 +330,7 @@ async function executeCreateContext( log.success("Starter files scaffolded."); } } catch (error) { - createSpinner?.stop("Could not create Prisma Next project."); + createSpinner?.stop("Could not create Prisma 8 project."); return { ok: false, stage: "scaffold_template", @@ -345,7 +345,7 @@ async function executeCreateContext( projectDir: context.targetDirectory, }); } catch (error) { - createSpinner?.stop("Could not create Prisma Next project."); + createSpinner?.stop("Could not create Prisma 8 project."); return { ok: false, stage: "scaffold_template", @@ -391,7 +391,7 @@ async function executeCreateContext( }; } } catch (error) { - createSpinner?.stop("Could not create Prisma Next project."); + createSpinner?.stop("Could not create Prisma 8 project."); return { ok: false, stage: "prisma_setup", diff --git a/src/tasks/setup-prisma.ts b/src/tasks/setup-prisma.ts index d453bb8..03e9db9 100644 --- a/src/tasks/setup-prisma.ts +++ b/src/tasks/setup-prisma.ts @@ -299,10 +299,10 @@ export async function executePrismaSetupContext( const template = options.template ?? "minimal"; const progress = context.verbose ? undefined : (options.progressSpinner ?? spinner()); const ownsProgress = progress !== undefined && !options.progressSpinner; - if (ownsProgress) progress.start("Creating Prisma Next project..."); + if (ownsProgress) progress.start("Creating Prisma 8 project..."); try { - progress?.message("Preparing Prisma Next project files..."); + progress?.message("Preparing Prisma 8 project files..."); await runPrismaInit(context, projectDir); await scaffoldCreateSharedTemplates({ @@ -329,11 +329,11 @@ export async function executePrismaSetupContext( verbose: context.verbose, }); - progress?.message("Generating Prisma Next contract artifacts..."); + progress?.message("Generating Prisma 8 contract artifacts..."); await emitContract(context, projectDir); - progress?.stop("Prisma Next project ready."); + progress?.stop("Prisma 8 project ready."); } catch (error) { - progress?.stop("Could not create Prisma Next project."); + progress?.stop("Could not create Prisma 8 project."); cancel(getCommandErrorMessage(error)); return false; } @@ -349,6 +349,6 @@ export async function executePrismaSetupContext( if (options.createdProjectPath) note(path.resolve(options.createdProjectPath), "Project path"); note(formatNextSteps(buildNextSteps(context, options)), "Next steps"); - outro(context.shouldDeploy ? "Prisma Next app deployed." : "Prisma Next setup complete."); + outro(context.shouldDeploy ? "Prisma 8 app deployed." : "Prisma 8 setup complete."); return true; } diff --git a/src/types.ts b/src/types.ts index 27be8db..ea50fb4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -49,7 +49,7 @@ export const CommonCommandOptionsSchema = z.object({ export const PrismaSetupOptionsSchema = z.object({ provider: DatabaseProviderSchema.optional().describe( - "Prisma Next database target: PostgreSQL relational models or MongoDB document models", + "Prisma 8 database target: PostgreSQL relational models or MongoDB document models", ), authoring: AuthoringStyleSchema.optional().describe("Contract authoring style"), packageManager: PackageManagerSchema.optional().describe( diff --git a/src/ui/branding.ts b/src/ui/branding.ts index 0b59a04..4c9a835 100644 --- a/src/ui/branding.ts +++ b/src/ui/branding.ts @@ -3,8 +3,8 @@ import { styleText } from "node:util"; const prismaMark = styleText(["bold", "cyanBright"], "◭"); const createLabel = styleText(["bold", "cyanBright"], "Create"); const prismaLabel = styleText(["bold", "magentaBright"], "Prisma"); -const nextLabel = styleText(["bold", "blueBright"], "Next"); -const prismaTitle = `${prismaMark} ${createLabel} ${prismaLabel} ${nextLabel}`; +const versionLabel = styleText(["bold", "blueBright"], "8"); +const prismaTitle = `${prismaMark} ${createLabel} ${prismaLabel} ${versionLabel}`; export function getCreatePrismaIntro(): string { return prismaTitle; diff --git a/templates/create/_shared/README.md.hbs b/templates/create/_shared/README.md.hbs index 0225df7..884437b 100644 --- a/templates/create/_shared/README.md.hbs +++ b/templates/create/_shared/README.md.hbs @@ -1,6 +1,6 @@ # {{projectName}} -A minimal {{template}} app with Prisma Next and Prisma Composer. +A minimal {{template}} app with Prisma 8 and Prisma Composer. ## Run locally @@ -25,7 +25,7 @@ MongoDB is not provisioned by Composer. Set `MONGODB_URL` before running Compose ## Prisma - Contract: `src/prisma/contract{{#if (eq authoring "typescript")}}.ts{{else}}.prisma{{/if}}` -- Prisma Next config: `prisma-next.config.ts` +- Prisma 8 config: `prisma-next.config.ts` - Universal CLI config: `prisma.config.ts` - Composer app: `module.ts` and `service.ts` diff --git a/templates/create/astro/src/pages/index.astro.hbs b/templates/create/astro/src/pages/index.astro.hbs index 7cf4061..37c47f5 100644 --- a/templates/create/astro/src/pages/index.astro.hbs +++ b/templates/create/astro/src/pages/index.astro.hbs @@ -22,11 +22,11 @@ const users = await listUsers(10).catch(() => undefined);
-

Astro + Prisma Next

+

Astro + Prisma 8

Users from your database, loaded on the server.

- This page reads from src/pages/index.astro using the Prisma Next helper in + This page reads from src/pages/index.astro using the Prisma 8 helper in src/prisma/users.ts. An Astro API route is also scaffolded in src/pages/api/users.ts.

diff --git a/templates/create/next/src/app/page.tsx.hbs b/templates/create/next/src/app/page.tsx.hbs index ecd0c21..dcc6e67 100644 --- a/templates/create/next/src/app/page.tsx.hbs +++ b/templates/create/next/src/app/page.tsx.hbs @@ -12,11 +12,11 @@ export default async function Home() { return (
-

Next.js + Prisma Next

+

Next.js + Prisma 8

Users from your database, loaded on the server.

- This page reads from src/app/page.tsx using the Prisma Next helper in{" "} + This page reads from src/app/page.tsx using the Prisma 8 helper in{" "} src/prisma/users.ts.

diff --git a/templates/create/nuxt/app/pages/index.vue.hbs b/templates/create/nuxt/app/pages/index.vue.hbs index d3aa554..248faf8 100644 --- a/templates/create/nuxt/app/pages/index.vue.hbs +++ b/templates/create/nuxt/app/pages/index.vue.hbs @@ -32,12 +32,12 @@ function formatCreatedAt(value: string): string {