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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ workspace. Choosing another workspace also updates the Prisma CLI's active works
- `elysia`
- `nest`
- `next`
- `turborepo` (Next.js in `apps/web`, shared Prisma package in `packages/database`)
- `svelte` (SvelteKit)
- `astro`
- `nuxt`
Expand Down
5 changes: 5 additions & 0 deletions src/commands/create-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ const promptForCreateTemplate = Effect.fn("Prompts.template")(function* (output:
hint: "Structured Node API with controllers and services",
},
{ value: "next", label: "Next.js", hint: "Full-stack React app with App Router" },
{
value: "turborepo",
label: "Monorepo (Turborepo)",
hint: "Next.js app with a shared Prisma database package",
},
{ value: "svelte", label: "SvelteKit", hint: "Full-stack Svelte 5 app with Vite" },
{ value: "astro", label: "Astro", hint: "Content-oriented web app with server routes" },
{ value: "nuxt", label: "Nuxt", hint: "Full-stack Vue app with Nitro server routes" },
Expand Down
12 changes: 11 additions & 1 deletion src/constants/dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const dependencyVersionMap = {
// stable Node or Bun ships yet.
"temporal-polyfill": "^1.0.4",
tsdown: "^0.22.14",
turbo: "2.10.12",
tsx: "^4.21.0",
typescript: "^5.9.3",
} as const;
Expand Down Expand Up @@ -79,12 +80,21 @@ export function getCreateTemplateDependencies(
if (template === "tanstack-start") {
devDependencies.push("nitro");
}
if (template === "turborepo") devDependencies.push("turbo");

return [
const targets: CreateTemplateDependencyTarget[] = [
{
packageJsonPath: "package.json",
dependencies,
devDependencies,
},
];
if (template === "turborepo") {
targets.push({
packageJsonPath: "packages/database/package.json",
dependencies: [],
devDependencies: ["typescript"],
});
}
return targets;
}
25 changes: 20 additions & 5 deletions src/tasks/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,18 +161,30 @@ export const writePrismaDependenciesEffect = Effect.fn("Dependencies.writePrisma
packageManager: PackageManager,
_authoring: AuthoringStyle,
projectDir = process.cwd(),
options: { skillsSync?: boolean } = {},
options: { skillsSync?: boolean; template?: CreateTemplate } = {},
) {
const dependencies = [getDbPackages(provider)];
if (provider === "postgres" && packageManager !== "deno") dependencies.push("temporal-polyfill");
if (provider === "mongo") dependencies.push("arktype", "mongodb");
const databaseDependencies = [getDbPackages(provider)];
if (provider === "postgres" && packageManager !== "deno") {
databaseDependencies.push("temporal-polyfill");
}
if (provider === "mongo") databaseDependencies.push("arktype", "mongodb");
const dependencies =
options.template === "turborepo"
? [getDbPackages(provider), ...(provider === "mongo" ? ["arktype"] : [])]
: [...databaseDependencies];
if (packageManager === "deno") dependencies.push("dotenv");
yield* addPackageDependencyEffect({
dependencies,
devDependencies: ["@types/node", "prisma"],
scripts: getPrismaScriptMap(packageManager, options.skillsSync ?? true),
projectDir,
});
if (options.template === "turborepo") {
yield* addPackageDependencyEffect({
dependencies: databaseDependencies,
projectDir: path.join(projectDir, "packages/database"),
});
}
});

export const writeCreateTemplateDependenciesEffect = Effect.fn("Dependencies.writeTemplate")(
Expand All @@ -189,7 +201,10 @@ export const writeCreateTemplateDependenciesEffect = Effect.fn("Dependencies.wri
dependencies: target.dependencies,
devDependencies: target.devDependencies,
customDependencies: target.customDependencies,
scripts: getComposerScriptMap(opts.packageManager),
scripts:
target.packageJsonPath === "package.json"
? getComposerScriptMap(opts.packageManager)
: undefined,
projectDir: path.join(projectDir, path.dirname(target.packageJsonPath)),
});
}
Expand Down
10 changes: 6 additions & 4 deletions src/tasks/prisma-setup/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { log } from "@clack/prompts";
import { Effect, FileSystem } from "effect";
import path from "node:path";

import type { AuthoringStyle, DatabaseProvider } from "../../types";
import { getCreatePrismaSourceDir } from "../../templates/render-create-template";
import type { AuthoringStyle, CreateTemplate, DatabaseProvider } from "../../types";
import { getLocalPackageBinaryArgs } from "../../utils/package-manager";
import { redactSecrets } from "../../utils/errors";
import { runPrismaJsonCommandEffect } from "../prisma-cli";
import type { PrismaSetupContext } from "./types";

const getContractPath = (authoring: AuthoringStyle) =>
`src/prisma/contract${authoring === "typescript" ? ".ts" : ".prisma"}`;
const getContractPath = (authoring: AuthoringStyle, template: CreateTemplate) =>
`${getCreatePrismaSourceDir(template)}/contract${authoring === "typescript" ? ".ts" : ".prisma"}`;

const getInitTarget = (provider: DatabaseProvider) =>
provider === "mongo" ? ("mongodb" as const) : ("postgres" as const);
Expand Down Expand Up @@ -43,6 +44,7 @@ export const runPrismaInit = Effect.fn("PrismaSetup.init")(function* (
context: PrismaSetupContext,
projectDir: string,
force = false,
template: CreateTemplate = "minimal",
) {
yield* runPrismaCli(context, projectDir, [
"orm",
Expand All @@ -54,7 +56,7 @@ export const runPrismaInit = Effect.fn("PrismaSetup.init")(function* (
"--authoring",
context.authoring,
"--schema-path",
getContractPath(context.authoring),
getContractPath(context.authoring, template),
"--skip-install",
]);
if (context.packageManager === "deno") {
Expand Down
4 changes: 2 additions & 2 deletions src/tasks/setup-prisma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const executePrismaSetupContextEffect = Effect.fn("PrismaSetup.execute")(
context.packageManager,
context.authoring,
projectDir,
{ skillsSync: context.skillAgents.length > 0 },
{ skillsSync: context.skillAgents.length > 0, template },
),
"configure_project",
"project_configuration_failed",
Expand All @@ -85,7 +85,7 @@ export const executePrismaSetupContextEffect = Effect.fn("PrismaSetup.execute")(

yield* Effect.sync(() => progress?.message("Preparing Prisma 8 project files..."));
yield* atCreateStage(
runPrismaInit(context, projectDir, options.force),
runPrismaInit(context, projectDir, options.force, template),
"initialize_prisma",
"prisma_init_failed",
);
Expand Down
27 changes: 27 additions & 0 deletions src/templates/render-create-template.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Effect } from "effect";
import path from "node:path";

import { applicationRuntime } from "../runtime";
import {
Expand All @@ -11,6 +12,13 @@ import {
} from "../types";
import { renderTemplateTreeEffect, resolveTemplatesDirEffect } from "./shared";

const DEFAULT_PRISMA_SOURCE_DIR = "src/prisma";
const TURBOREPO_PRISMA_SOURCE_DIR = "packages/database/src";

export function getCreatePrismaSourceDir(template: CreateTemplate): string {
return template === "turborepo" ? TURBOREPO_PRISMA_SOURCE_DIR : DEFAULT_PRISMA_SOURCE_DIR;
}

type CreateTemplateContext = {
projectName: string;
template: CreateTemplate;
Expand Down Expand Up @@ -59,6 +67,17 @@ export const scaffoldCreateSharedTemplatesEffect = Effect.fn("Templates.scaffold
templateRoot,
outputDir: options.projectDir,
context: createTemplateContext(options),
mapRelativeOutputPath(relativePath) {
if (options.template !== "turborepo") return relativePath;
const relativePrismaPath = path.relative(DEFAULT_PRISMA_SOURCE_DIR, relativePath);
if (
relativePrismaPath === ".." ||
relativePrismaPath.startsWith(`..${path.sep}`) ||
path.isAbsolute(relativePrismaPath)
)
return relativePath;
return path.join(TURBOREPO_PRISMA_SOURCE_DIR, relativePrismaPath);
},
});
});

Expand All @@ -81,6 +100,14 @@ export const scaffoldCreateFrameworkTemplateEffect = Effect.fn("Templates.scaffo
outputDir: options.projectDir,
context: createTemplateContext(options),
});
if (options.template === "turborepo") {
const nextTemplateRoot = yield* resolveTemplatesDirEffect("templates/create/next");
yield* renderTemplateTreeEffect({
templateRoot: nextTemplateRoot,
outputDir: path.join(options.projectDir, "apps/web"),
context: createTemplateContext(options),
});
}
},
);

Expand Down
14 changes: 12 additions & 2 deletions src/templates/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "../utils/package-manager";

Handlebars.registerHelper("eq", (left: unknown, right: unknown) => left === right);
Handlebars.registerHelper("or", (...args: unknown[]) => args.slice(0, -1).some(Boolean));
Handlebars.registerHelper(
"runScriptCommand",
(packageManager: PackageManager | undefined, scriptName: string) =>
Expand Down Expand Up @@ -92,17 +93,26 @@ export const renderTemplateFileEffect = Effect.fn("Templates.renderFile")(functi

export const renderTemplateTreeEffect = Effect.fn("Templates.renderTree")(function* <
TContext,
>(opts: { templateRoot: string; outputDir: string; context: TContext }) {
>(opts: {
templateRoot: string;
outputDir: string;
context: TContext;
mapRelativeOutputPath?: (relativePath: string) => string;
}) {
const fs = yield* FileSystem.FileSystem;
const entries = yield* fs.readDirectory(opts.templateRoot, { recursive: true });

for (const relativePath of entries) {
const templateFilePath = path.join(opts.templateRoot, relativePath);
const info = yield* fs.stat(templateFilePath);
if (info.type !== "File") continue;
const renderedRelativePath = stripHbsExtension(relativePath);
yield* renderTemplateFileEffect({
templateFilePath,
outputPath: path.join(opts.outputDir, stripHbsExtension(relativePath)),
outputPath: path.join(
opts.outputDir,
opts.mapRelativeOutputPath?.(renderedRelativePath) ?? renderedRelativePath,
),
context: opts.context,
});
}
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const createTemplates = [
"elysia",
"nest",
"next",
"turborepo",
"svelte",
"astro",
"nuxt",
Expand Down
7 changes: 6 additions & 1 deletion templates/create/_package-manager/pnpm-workspace.yaml.hbs
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
{{#if (eq packageManager "pnpm")}}
{{#if (eq template "turborepo")}}
packages:
- "apps/*"
- "packages/*"
{{/if}}
allowBuilds:
esbuild: true
msgpackr-extract: true
{{#if (eq template "next")}}
{{#if (or (eq template "next") (eq template "turborepo"))}}
sharp: true
unrs-resolver: true
{{else if (eq template "astro")}}
Expand Down
12 changes: 6 additions & 6 deletions templates/create/_shared/.gitattributes.hbs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{{#if (eq authoring "typescript")}}
src/prisma/generated/contract.json linguist-generated
src/prisma/generated/contract.d.ts linguist-generated
{{#if (eq template "turborepo")}}packages/database/src{{else}}src/prisma{{/if}}/generated/contract.json linguist-generated
{{#if (eq template "turborepo")}}packages/database/src{{else}}src/prisma{{/if}}/generated/contract.d.ts linguist-generated
{{else}}
src/prisma/contract.json linguist-generated
src/prisma/contract.d.ts linguist-generated
{{#if (eq template "turborepo")}}packages/database/src{{else}}src/prisma{{/if}}/contract.json linguist-generated
{{#if (eq template "turborepo")}}packages/database/src{{else}}src/prisma{{/if}}/contract.d.ts linguist-generated
{{/if}}
src/prisma/ops.json linguist-generated
src/prisma/migration.json linguist-generated
{{#if (eq template "turborepo")}}packages/database/src{{else}}src/prisma{{/if}}/ops.json linguist-generated
{{#if (eq template "turborepo")}}packages/database/src{{else}}src/prisma{{/if}}/migration.json linguist-generated
migrations/snapshots/**/contract.json linguist-generated
migrations/snapshots/**/contract.d.ts linguist-generated
44 changes: 44 additions & 0 deletions templates/create/_shared/README.md.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,50 @@ deno task contract:emit
```

Prisma Compute does not support Deno deployments yet.
{{else if (eq template "turborepo")}}
A Prisma 8 monorepo powered by Turborepo, Next.js, and Prisma Composer.

## Workspace layout

- `apps/web` — Next.js application
- `packages/database` — Prisma contract, generated artifacts, runtime client, and seed data
- `module.ts` and `service.ts` — Composer deployment topology

## Run locally

```bash
{{runScriptCommand packageManager "dev:composer"}}
```

This builds the workspace and starts it with Composer. PostgreSQL projects get a local Prisma Postgres database and apply the committed migrations automatically.

## Deploy

```bash
{{runScriptCommand packageManager "deploy"}}
```

The deploy script runs the Turborepo build, provisions Prisma Postgres when selected, applies migrations, and deploys the Next.js app to Prisma Compute.

The starter users are inserted idempotently from `packages/database/src/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: `packages/database/src/contract{{#if (eq authoring "typescript")}}.ts{{else}}.prisma{{/if}}`
- Prisma and Composer config: `prisma.config.ts`
- Composer app: `module.ts` and `service.ts`

After changing the contract, run:

```bash
{{runScriptCommand packageManager "contract:emit"}}
```

To run the workspace's development tasks directly, use `{{runScriptCommand packageManager "dev"}}`. This direct mode requires `{{#if (eq provider "postgres")}}DATABASE_URL{{else}}MONGODB_URL{{/if}}`.
{{else}}
A minimal {{template}} app with Prisma 8 and Prisma Composer.

Expand Down
2 changes: 1 addition & 1 deletion templates/create/_shared/module.ts.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { module } from "@prisma/composer";
{{#if (eq provider "postgres")}}
import { postgres } from "@prisma/composer-prisma-cloud/orm";

import { appContract } from "./src/prisma/composer.ts";
import { appContract } from "./{{#if (eq template "turborepo")}}packages/database/src{{else}}src/prisma{{/if}}/composer.ts";
{{else}}
import { envSecret } from "@prisma/composer-prisma-cloud";
{{/if}}
Expand Down
4 changes: 2 additions & 2 deletions templates/create/_shared/prisma-composer.config.ts.hbs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{{#unless (eq packageManager "deno")}}
import { defineConfig } from "@prisma/composer/config";
import { nodeBuild } from "@prisma/composer/node/control";
{{#if (eq template "next")}}
{{#if (or (eq template "next") (eq template "turborepo"))}}
import { nextjsBuild } from "@prisma/composer/nextjs/control";
{{/if}}
import { prismaCloud, prismaState } from "@prisma/composer-prisma-cloud/control";

export default defineConfig({
extensions: [prismaCloud({ region: "us-east-1" }), nodeBuild(){{#if (eq template "next")}}, nextjsBuild(){{/if}}],
extensions: [prismaCloud({ region: "us-east-1" }), nodeBuild(){{#if (or (eq template "next") (eq template "turborepo"))}}, nextjsBuild(){{/if}}],
state: prismaState(),
});
{{/unless}}
8 changes: 4 additions & 4 deletions templates/create/_shared/prisma.config.ts.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { defineConfig as ormConfig } from "@prisma/orm-{{#if (eq provider "postg

export default definePrismaConfig({
orm: ormConfig({
contract: "./src/prisma/contract{{#if (eq authoring "typescript")}}.ts{{else}}.prisma{{/if}}",
contract: "./{{#if (eq template "turborepo")}}packages/database/src{{else}}src/prisma{{/if}}/contract{{#if (eq authoring "typescript")}}.ts{{else}}.prisma{{/if}}",
{{#if (eq authoring "typescript")}}
output: "./src/prisma/generated",
output: "./{{#if (eq template "turborepo")}}packages/database/src{{else}}src/prisma{{/if}}/generated",
{{/if}}
db: {
connection: process.env.{{#if (eq provider "postgres")}}DATABASE_URL{{else}}MONGODB_URL{{/if}}!,
Expand All @@ -23,9 +23,9 @@ export default definePrismaConfig({
agents: [{{#each skillAgents}}"{{this}}"{{#unless @last}}, {{/unless}}{{/each}}],
},
orm: ormConfig({
contract: "./src/prisma/contract{{#if (eq authoring "typescript")}}.ts{{else}}.prisma{{/if}}",
contract: "./{{#if (eq template "turborepo")}}packages/database/src{{else}}src/prisma{{/if}}/contract{{#if (eq authoring "typescript")}}.ts{{else}}.prisma{{/if}}",
{{#if (eq authoring "typescript")}}
output: "./src/prisma/generated",
output: "./{{#if (eq template "turborepo")}}packages/database/src{{else}}src/prisma{{/if}}/generated",
{{/if}}
db: {
connection: process.env.{{#if (eq provider "postgres")}}DATABASE_URL{{else}}MONGODB_URL{{/if}}!,
Expand Down
Loading
Loading