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..efc082d 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 8 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 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. -```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 8 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..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" @@ -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/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", @@ -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/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/commands/create.ts b/src/commands/create.ts index 5bf2f54..27dc4f8 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"; @@ -100,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", @@ -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"; @@ -199,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: ${ @@ -225,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, @@ -301,14 +310,14 @@ 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) { log.step(`Scaffolding ${context.template} starter.`); } - await scaffoldCreateTemplate({ + await scaffoldCreateFrameworkTemplate({ projectDir: context.targetDirectory, projectName: context.projectPackageName, template: context.template, @@ -321,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", @@ -334,10 +343,9 @@ 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."); + createSpinner?.stop("Could not create Prisma 8 project."); return { ok: false, stage: "scaffold_template", @@ -369,6 +377,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, @@ -381,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/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..826eb3a 100644 --- a/src/constants/dependencies.ts +++ b/src/constants/dependencies.ts @@ -1,50 +1,27 @@ 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": "0.2.0", + "@prisma/composer": "0.9.0", + "@prisma/composer-prisma-cloud": "0.9.0", + "@prisma/orm-mongo": "8.0.0-rc.3", + "@prisma/orm-postgres": "8.0.0-rc.3", + "@sveltejs/adapter-node": "^5.3.2", "@types/node": "^25.6.2", - dotenv: "^17.4.2", + alchemy: "2.0.0-beta.67", + arktype: "^2.2.3", + 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", 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 const PRISMA_PLATFORM_CLI_PACKAGE = "prisma@next"; export type AvailableDependency = keyof typeof dependencyVersionMap; @@ -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..bc5a9c7 --- /dev/null +++ b/src/tasks/deploy-with-composer.ts @@ -0,0 +1,150 @@ +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, + getPackageExecutionCommand, + getRunScriptArgs, + getRunScriptCommand, +} from "../utils/package-manager"; + +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."); +} + +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]); +} + +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_PLATFORM_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"); + 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."); + } 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."); + log.error(`Deploy failed: ${getErrorMessage(error)}`); + return false; + } +} diff --git a/src/tasks/install.ts b/src/tasks/install.ts index 31eee70..47a1dd5 100644 --- a/src/tasks/install.ts +++ b/src/tasks/install.ts @@ -3,59 +3,57 @@ import fs from "fs-extra"; import path from "node:path"; import { - DEFAULT_PRISMA_NEXT_SPEC, getCreateTemplateDependencies, getDependencyVersion, - type ResolvedPrismaNextSpec, + PRISMA_PLATFORM_CLI_PACKAGE, } 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()}`; +import { + getInstallArgs, + getPackageExecutionCommand, + getRunScriptCommand, +} from "../utils/package-manager"; - 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; - } +function getPrismaScriptMap(packageManager: PackageManager): Record { + const prismaCommand = (...args: string[]) => + getPackageExecutionCommand(packageManager, [PRISMA_PLATFORM_CLI_PACKAGE, ...args]); - if (packageManager === "bun") { - const prismaNextCli = "bun prisma-next"; + return { + "contract:emit": prismaCommand("contract", "emit"), + "db:init": prismaCommand("db", "init"), + "db:update": prismaCommand("db", "update"), + "db:verify": prismaCommand("db", "verify"), + "migration:plan": prismaCommand("migration", "plan"), + migrate: prismaCommand("migrate"), + "migration:status": prismaCommand("migration", "status"), + "migration:show": prismaCommand("migration", "show"), + }; +} - 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; - } +export function getComposerScriptMap(packageManager: PackageManager): Record { + const composerCommand = (subcommand: string, extraArgs: string[] = []) => + getPackageExecutionCommand(packageManager, [ + PRISMA_PLATFORM_CLI_PACKAGE, + "composer", + subcommand, + "module.ts", + ...extraArgs, + ]); 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", - "migration:plan": "prisma-next migration plan", - migrate: "prisma-next migrate", - "migration:status": "prisma-next migration status", - "migration:show": "prisma-next migration show", - } as const; + "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 +64,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 +71,6 @@ export async function addPackageDependency(opts: { scripts?: Record; scriptMode?: "if-missing"; projectDir: string; - prismaNextSpec?: ResolvedPrismaNextSpec; }): Promise { const { dependencies = [], @@ -128,7 +79,6 @@ export async function addPackageDependency(opts: { scripts = {}, scriptMode, projectDir, - prismaNextSpec = DEFAULT_PRISMA_NEXT_SPEC, } = opts; const pkgJsonPath = path.join(projectDir, "package.json"); @@ -139,78 +89,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)]; + if (provider === "mongo") dependencies.push("arktype", "mongodb"); await addPackageDependency({ dependencies, - devDependencies, - scripts: prismaScriptMap, + devDependencies: ["@prisma/cli-engine", "@types/node"], + scripts: getPrismaScriptMap(packageManager), projectDir, - prismaNextSpec, }); } @@ -218,37 +144,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 +186,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..49e84f5 100644 --- a/src/tasks/setup-prisma.ts +++ b/src/tasks/setup-prisma.ts @@ -3,324 +3,67 @@ 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 { PRISMA_PLATFORM_CLI_PACKAGE } 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, getRunScriptCommand, } from "../utils/package-manager"; +import { deployWithComposer } from "./deploy-with-composer"; +import { installProjectDependencies, writePrismaDependencies } from "./install"; + +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; + shouldDeploy: boolean; }; -type FinalizePrismaOptions = { - provider: DatabaseProvider; - databaseUrl?: string; - claimUrl?: string; - projectDir?: string; -}; - -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 +72,276 @@ 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", +async function promptForDeployment(): Promise { + const shouldDeploy = await confirm({ + message: "Deploy to Prisma now?", initialValue: true, }); - - 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 getPrismaCliInvocation(packageManager: PackageManager, args: string[]) { + return getPackageExecutionArgs(packageManager, [PRISMA_PLATFORM_CLI_PACKAGE, ...args]); } -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(" "); -} - -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 invocation = getPrismaCliInvocation(context.packageManager, ["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([invocation.command, ...invocation.args].join(" ")); } + await execa(invocation.command, invocation.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 8 project..."); - const didWriteDependencies = await writeDependenciesForContext(context, projectDir); - if (!didWriteDependencies) { - stopProgressOnFailure(); - return false; - } + try { + progress?.message("Preparing Prisma 8 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 8 contract artifacts..."); + await emitContract(context, projectDir); + progress?.stop("Prisma 8 project ready."); + } catch (error) { + progress?.stop("Could not create Prisma 8 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 8 app deployed." : "Prisma 8 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..cc0126f 100644 --- a/src/templates/shared.ts +++ b/src/templates/shared.ts @@ -10,19 +10,6 @@ import { getRuntimeScriptCommand, getRunScriptCommand, } 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( @@ -35,10 +22,6 @@ Handlebars.registerHelper( (packageManager: PackageManager | undefined) => getPackageManagerManifestValue(packageManager) ?? "", ); -Handlebars.registerHelper( - "requiresDotenvConfigImport", - (packageManager: PackageManager | undefined) => requiresDotenvConfigImport(packageManager), -); Handlebars.registerHelper( "runtimeScript", ( @@ -46,17 +29,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..ea50fb4 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"), @@ -51,27 +49,13 @@ 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( "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/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/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 deleted file mode 100644 index edd7e0b..0000000 --- a/src/utils/runtime.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { PackageManager } from "../types"; - -export function usesNodeStyleRuntime(packageManager: PackageManager | undefined): boolean { - return packageManager !== undefined && packageManager !== "bun" && packageManager !== "deno"; -} - -export function requiresDotenvConfigImport(packageManager: PackageManager | undefined): boolean { - return usesNodeStyleRuntime(packageManager); -} - -export function requiresPrismaConfigDotenvImport( - packageManager: PackageManager | undefined, -): boolean { - return packageManager !== "deno"; -} 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/README.md.hbs b/templates/create/_shared/README.md.hbs new file mode 100644 index 0000000..17aa9dd --- /dev/null +++ b/templates/create/_shared/README.md.hbs @@ -0,0 +1,40 @@ +# {{projectName}} + +A minimal {{template}} app with Prisma 8 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. + +The starter users are inserted idempotently from `src/prisma/seed.ts` on the first database query through the Composer service binding. + +{{#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 8 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..5236efa --- /dev/null +++ b/templates/create/_shared/pnpm-workspace.yaml.hbs @@ -0,0 +1,16 @@ +{{#if (eq packageManager "pnpm")}} +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/*" +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-next.config.ts.hbs b/templates/create/_shared/prisma-next.config.ts.hbs new file mode 100644 index 0000000..61f8d14 --- /dev/null +++ b/templates/create/_shared/prisma-next.config.ts.hbs @@ -0,0 +1,11 @@ +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/prisma.config.ts.hbs b/templates/create/_shared/prisma.config.ts.hbs new file mode 100644 index 0000000..e6ee943 --- /dev/null +++ b/templates/create/_shared/prisma.config.ts.hbs @@ -0,0 +1,10 @@ +import { definePrismaConfig } from "@prisma/cli-engine"; + +import orm from "./prisma-next.config.ts"; + +export default definePrismaConfig({ + 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..71ee7fe --- /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 "./{{#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 new file mode 100644 index 0000000..eb9ccf3 --- /dev/null +++ b/templates/create/_shared/src/prisma/db.ts.hbs @@ -0,0 +1,51 @@ +{{#if (eq provider "postgres")}} +import postgres from "@prisma/orm-postgres/runtime"; + +import service from "../../service.ts"; +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 { + return service.load().database.client; + } catch { + return undefined; + } +} + +export const db = + loadComposerDatabase() ?? + (process.env.DATABASE_URL + ? postgres({ contractJson, url: process.env.DATABASE_URL }) + : postgres({ contractJson })); +{{else}} +import mongo from "@prisma/orm-mongo/runtime"; + +import service from "../../service.ts"; +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 | undefined { + try { + return service.input().databaseUrl.expose(); + } catch { + return process.env.DATABASE_URL; + } +} + +const databaseUrl = getDatabaseUrl(); + +export const db = databaseUrl + ? mongo({ contractJson, url: databaseUrl }) + : mongo({ contractJson }); +{{/if}} + +let connection: Promise | undefined; + +export function connectDatabase(): Promise { + connection ??= db.connect().then(() => undefined).catch((error: unknown) => { + connection = undefined; + throw error; + }); + return connection; +} diff --git a/templates/create/_shared/src/prisma/seed.ts.hbs b/templates/create/_shared/src/prisma/seed.ts.hbs index dba7fcf..8ad1eb9 100644 --- a/templates/create/_shared/src/prisma/seed.ts.hbs +++ b/templates/create/_shared/src/prisma/seed.ts.hbs @@ -1,42 +1,36 @@ -{{#if (requiresDotenvConfigImport packageManager)}} -import "dotenv/config"; -{{/if}} -import { db } from "./db{{#if (eq packageManager "deno")}}.ts{{/if}}"; +import { connectDatabase, db } from "./db.ts"; -const seedUsers = [ +const users = [ { email: "alice@prisma.io", username: "alice", name: "Alice" }, { email: "bob@prisma.io", username: "bob", name: "Bob" }, { email: "carol@prisma.io", username: "carol", name: "Carol" }, ]; -let createdCount = 0; +let pendingSeed: Promise | undefined; + +async function runSeed(): Promise { + await connectDatabase(); {{#if (eq provider "mongo")}} -try { - for (const user of seedUsers) { + for (const user of users) { const existingUser = await db.orm.users.where({ email: user.email }).first(); - if (!existingUser) { - await db.orm.users.create(user); - createdCount += 1; - } + if (!existingUser) await db.orm.users.create(user); } - - console.log(`Seeded ${createdCount} user${createdCount === 1 ? "" : "s"}.`); -} finally { - await db.close(); -} {{else}} -try { - for (const user of seedUsers) { - const existingUser = await db.orm.public.User.where({ email: user.email }).first(); - if (!existingUser) { - await db.orm.public.User.create(user); - createdCount += 1; - } + for (const user of users) { + await db.orm.public.User.upsert({ + create: user, + update: {}, + conflictOn: { email: user.email }, + }); } +{{/if}} +} - console.log(`Seeded ${createdCount} user${createdCount === 1 ? "" : "s"}.`); -} finally { - await db.close(); +export function seed(): Promise { + pendingSeed ??= runSeed().catch((error: unknown) => { + pendingSeed = undefined; + throw error; + }); + return pendingSeed; } -{{/if}} diff --git a/templates/create/_shared/src/prisma/users.ts.hbs b/templates/create/_shared/src/prisma/users.ts.hbs index 5ec37bd..470b7f2 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"; +import { seed } from "./seed.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) { - + await seed(); {{#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/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/astro/src/pages/index.astro.hbs b/templates/create/astro/src/pages/index.astro.hbs index 7cf4061..cb6cec7 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.

@@ -44,7 +44,7 @@ const users = await listUsers(10).catch(() => undefined); then refresh.

) : users.length === 0 ? ( -

No users yet. Run db:seed after applying your first migration.

+

No users found.

) : (
    {users.map((user) => ( 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..2e22f31 100644 --- a/templates/create/elysia/src/index.ts.hbs +++ b/templates/create/elysia/src/index.ts.hbs @@ -1,20 +1,14 @@ -{{#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: "Bun" in globalThis ? undefined : node() }) .get("/", () => { return { message: "hello from create-prisma + elysia", @@ -35,13 +29,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); + .listen({ port, hostname: "0.0.0.0" }); 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..0918fbe 100644 --- a/templates/create/hono/src/index.ts.hbs +++ b/templates/create/hono/src/index.ts.hbs @@ -1,10 +1,7 @@ -{{#if (requiresDotenvConfigImport packageManager)}} -import "dotenv/config"; -{{/if}} 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 +24,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..fe17f17 100644 --- a/templates/create/minimal/src/index.ts.hbs +++ b/templates/create/minimal/src/index.ts.hbs @@ -1,43 +1,19 @@ -import { db, listUsers } from "./prisma/users{{#if (eq packageManager "deno")}}.ts{{/if}}"; +import { createServer } from "node:http"; -const sampleUser = { - email: "first.user@prisma.io", - username: "first-user", - name: "First User", -}; +import { listUsers } from "./prisma/users"; -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 port = Number(process.env.PORT ?? 3000); - 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); +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." })); } - - const user = await db.orm.public.User.where({ email: sampleUser.email }).first(); -{{/if}} - const users = await listUsers(); - - 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}} -} +}).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..21388c4 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..9ffc8f2 100644 --- a/templates/create/nest/src/main.ts.hbs +++ b/templates/create/nest/src/main.ts.hbs @@ -1,15 +1,12 @@ import "reflect-metadata"; -{{#if (requiresDotenvConfigImport packageManager)}} -import "dotenv/config"; -{{/if}} 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 +16,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..f799b28 100644 --- a/templates/create/nest/src/users.controller.ts.hbs +++ b/templates/create/nest/src/users.controller.ts.hbs @@ -1,14 +1,13 @@ -import { Controller, Get } from "@nestjs/common"; +import { Controller, Get, Inject } from "@nestjs/common"; -import { UsersService } from "./users.service{{#if (eq packageManager "deno")}}.ts{{/if}}"; +import { UsersService } from "./users.service"; @Controller("users") export class UsersController { - constructor(private readonly usersService: UsersService) {} + constructor(@Inject(UsersService) private readonly usersService: UsersService) {} @Get() findAll() { 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..60c873d 100644 --- a/templates/create/nest/src/users.service.ts.hbs +++ b/templates/create/nest/src/users.service.ts.hbs @@ -1,13 +1,12 @@ -import { Injectable } from "@nestjs/common"; +import { Inject, Injectable } from "@nestjs/common"; -import { PrismaService } from "./prisma.service{{#if (eq packageManager "deno")}}.ts{{/if}}"; +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/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..1562e13 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() { @@ -15,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.

    @@ -36,7 +33,7 @@ export default async function Home() { then refresh.

    ) : users.length === 0 ? ( -

    No users yet. Run db:seed after applying your first migration.

    +

    No users found.

    ) : (
      {users.map((user) => ( 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/app/pages/index.vue.hbs b/templates/create/nuxt/app/pages/index.vue.hbs index d3aa554..0d719a9 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 {