Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ skills-lock.json
.next/
next-env.d.ts

# TanStack Start / Nitro build output
.output/

# Deploy working dir — assembled artifacts staged per graph address (ADR-0005)
.prisma-composer/
wip/
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ have.

| Guide | Covers |
| --- | --- |
| [Getting started](docs/guides/getting-started.md) | Your first app end to end; porting an existing Node or Next.js app |
| [Getting started](docs/guides/getting-started.md) | Your first app end to end; porting an existing Node, TanStack Start, or Next.js app |
| [Building an app](docs/guides/building-an-app.md) | Contracts, databases (plain + Prisma Next-typed with migrations), reusable Modules, cron/storage/streams, config, secrets |
| [Testing](docs/guides/testing.md) | Unit tests with `mockService`, integration tests with `bootstrapService` |
| [Deploying and operating](docs/guides/deploying.md) | Stages, destroy, CI, how apps behave in production |
Expand All @@ -142,6 +142,7 @@ Complete, deployable apps under [`examples/`](examples/):
| Example | Demonstrates |
| --- | --- |
| [pn-widgets](examples/pn-widgets/) | The minimal app: one service + one Prisma Next-typed Postgres |
| [tanstack-start](examples/tanstack-start/) | TanStack Start SSR and static assets from Nitro's multi-file output |
| [storefront-auth](examples/storefront-auth/) | Next.js frontend + API service, a reusable Module owning its database, secrets |
| [store](examples/store/) | Four modules, typed databases with migrations, the shared cron module |
| [cron](examples/cron/) | Scheduled jobs: `defineSchedule` + `serveSchedule` + the cron module |
Expand Down
12 changes: 12 additions & 0 deletions architecture.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,18 @@
"layer": "authoring",
"plane": "control"
},
{
"glob": "packages/0-framework/2-authoring/tanstack-start/src/index.ts",
"domain": "framework",
"layer": "authoring",
"plane": "shared"
},
{
"glob": "packages/0-framework/2-authoring/tanstack-start/src/control.ts",
"domain": "framework",
"layer": "authoring",
"plane": "control"
},
{
"glob": "packages/0-framework/3-tooling/assemble/src/**",
"domain": "framework",
Expand Down
1 change: 1 addition & 0 deletions biome.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"!**/.prisma",
"!**/.alchemy",
"!**/*.d.ts",
"!**/routeTree.gen.ts",
"!**/fixtures/**/emitted",
"!**/examples/**/migrations",
"!examples/pn-widgets/contract.json",
Expand Down
19 changes: 19 additions & 0 deletions docs/guides/building-an-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,25 @@ ships. Two things to know:

Without `dir` you get the single-file form above, unchanged.

**`tanstack-start` — a TanStack Start app.** Point the dedicated adapter at
the app root, the folder where Nitro writes `.output`:

```ts
build: tanstackStart({
module: import.meta.url,
appDir: '..',
})
```

Run `vite build` before deploy and register `tanstackStartBuild()` from
`@prisma/composer/tanstack-start/control` in `prisma-composer.config.ts`. The
assembler validates `.output/nitro.json`, requires Nitro's `node-server`
preset, reads the manifest's `serverEntry`, and copies the complete `.output`
tree verbatim. That keeps the entry's sibling chunks and client/public assets
in their emitted layout without hardcoding the server path. The complete,
tested shape is in
[`examples/tanstack-start`](../../examples/tanstack-start/).

**`nextjs` — a Next.js app.** `next build` with `output: 'standalone'` is the
whole build; the adapter just needs to know where the app lives:

Expand Down
17 changes: 17 additions & 0 deletions docs/guides/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,23 @@ your built server file, then make three changes to the server itself:
Your build must produce a self-contained entry file — keep your own build if
it already does.

**A TanStack Start app.** Keep its Nitro `node-server` build and point the
dedicated adapter at the app root:

```ts
build: tanstackStart({
module: import.meta.url,
appDir: '..',
})
```

Run `vite build` before deploy and include `tanstackStartBuild()` from
`@prisma/composer/tanstack-start/control` in the deploy config. Composer
validates Nitro's manifest, reads its declared server entry, and copies the
complete output, including server chunks, the client bundle, and `public/`.
See the complete [`examples/tanstack-start`](../../examples/tanstack-start/)
app.

**A Next.js app.** Use the `nextjs` build adapter instead of `node`; `next
build` with `output: 'standalone'` is the whole build:

Expand Down
6 changes: 6 additions & 0 deletions examples/tanstack-start/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { module } from '@prisma/composer';
import webService from './src/service.ts';

export default module('tanstack-start-example', ({ provision }) => {
provision(webService);
});
32 changes: 32 additions & 0 deletions examples/tanstack-start/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@prisma/example-tanstack-start",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build",
"dev": "vite --port 3000",
"typecheck": "tsc --noEmit",
"test": "bun test tests",
"deploy": "pnpm turbo run build --filter @prisma/example-tanstack-start... && ( set -a; . \"${PRISMA_DEPLOY_ENV:-../../.env}\"; set +a; bun node_modules/.bin/prisma-composer deploy module.ts ${TANSTACK_START_STACK_NAME:+--name \"$TANSTACK_START_STACK_NAME\"} ${TANSTACK_START_STAGE:+--stage \"$TANSTACK_START_STAGE\"} )",
"destroy": "( set -a; . \"${PRISMA_DEPLOY_ENV:-../../.env}\"; set +a; bun node_modules/.bin/prisma-composer destroy module.ts --production ${TANSTACK_START_STACK_NAME:+--name \"$TANSTACK_START_STACK_NAME\"} )",
"destroy:stage": "( set -a; . \"${PRISMA_DEPLOY_ENV:-../../.env}\"; set +a; bun node_modules/.bin/prisma-composer destroy module.ts --stage \"${TANSTACK_START_STAGE:?TANSTACK_START_STAGE is required}\" ${TANSTACK_START_STACK_NAME:+--name \"$TANSTACK_START_STACK_NAME\"} )"
},
"dependencies": {
"@prisma/composer": "workspace:0.1.0",
"@prisma/composer-prisma-cloud": "workspace:0.1.0",
"@tanstack/react-router": "1.170.18",
"@tanstack/react-start": "1.168.28",
"nitro": "npm:nitro-nightly@3.0.1-20260717-080150-bfc2f5ef",
"react": "19.2.7",
"react-dom": "19.2.7"
},
"devDependencies": {
"@types/bun": "^1.3.13",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "6.0.3",
"typescript": "^6.0.3",
"vite": "8.1.2"
}
}
8 changes: 8 additions & 0 deletions examples/tanstack-start/prisma-composer.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from '@prisma/composer/config';
import { tanstackStartBuild } from '@prisma/composer/tanstack-start/control';
import { prismaCloud, prismaState } from '@prisma/composer-prisma-cloud/control';

export default defineConfig({
extensions: [prismaCloud(), tanstackStartBuild()],
state: () => prismaState(),
});
1 change: 1 addition & 0 deletions examples/tanstack-start/public/composer.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
tanstack-start-composer-public-asset
68 changes: 68 additions & 0 deletions examples/tanstack-start/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/* eslint-disable */

// @ts-nocheck

// noinspection JSUnusedGlobalSymbols

// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.

import { Route as rootRouteImport } from './routes/__root'
import { Route as IndexRouteImport } from './routes/index'

const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any)

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/'
fileRoutesByTo: FileRoutesByTo
to: '/'
id: '__root__' | '/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
}

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
}
}

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()

import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
}
}
12 changes: 12 additions & 0 deletions examples/tanstack-start/src/router.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createRouter } from '@tanstack/react-router';
import { routeTree } from './routeTree.gen.ts';

export function getRouter() {
return createRouter({ routeTree });
}

declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof getRouter>;
}
}
28 changes: 28 additions & 0 deletions examples/tanstack-start/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createRootRoute, HeadContent, Outlet, Scripts } from '@tanstack/react-router';
import type { ReactNode } from 'react';

export const Route = createRootRoute({
head: () => ({
meta: [
{ charSet: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ title: 'TanStack Start on Prisma Composer' },
],
}),
component: Outlet,
shellComponent: RootDocument,
});

function RootDocument({ children }: { children: ReactNode }) {
return (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
);
}
13 changes: 13 additions & 0 deletions examples/tanstack-start/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { createFileRoute } from '@tanstack/react-router';

export const Route = createFileRoute('/')({ component: Home });

function Home() {
return (
<main>
<h1>TanStack Start on Prisma Composer</h1>
<p>Rendered by the Nitro server from Composer's directory build adapter.</p>
<a href="/composer.txt">Public asset</a>
</main>
);
}
11 changes: 11 additions & 0 deletions examples/tanstack-start/src/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import tanstackStart from '@prisma/composer/tanstack-start';
import { compute } from '@prisma/composer-prisma-cloud';

export default compute({
name: 'web',
deps: {},
build: tanstackStart({
module: import.meta.url,
appDir: '..',
}),
});
93 changes: 93 additions & 0 deletions examples/tanstack-start/tests/tanstack-start.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { afterEach, describe, expect, test } from 'bun:test';
import * as fs from 'node:fs';
import * as net from 'node:net';
import * as os from 'node:os';
import * as path from 'node:path';
import { assembleTanStackStart } from '@prisma/composer/tanstack-start/control';
import webService from '../src/service.ts';

const tmpDirs: string[] = [];
const processes: ReturnType<typeof Bun.spawn>[] = [];

afterEach(async () => {
for (const process of processes.splice(0)) {
process.kill();
await process.exited;
}
for (const dir of tmpDirs.splice(0)) fs.rmSync(dir, { recursive: true, force: true });
});

async function freePort(): Promise<number> {
const server = net.createServer();
await new Promise<void>((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', resolve);
});
const address = server.address();
await new Promise<void>((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
);
if (address === null || typeof address === 'string')
throw new Error('failed to reserve a TCP port');
return address.port;
}

async function fetchWhenReady(url: string): Promise<Response> {
const deadline = Date.now() + 10_000;
while (Date.now() < deadline) {
try {
const response = await fetch(url);
if (response.ok) return response;
} catch {
// Nitro has not started listening yet.
}
await Bun.sleep(50);
}
throw new Error(`Nitro did not serve ${url} within 10 seconds`);
}

describe('TanStack Start directory build', () => {
test('assembles the complete Nitro output and selects its server entry', async () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'composer-tanstack-start-'));
tmpDirs.push(cwd);

const artifact = await assembleTanStackStart({
build: webService.build,
address: 'web',
cwd,
});

expect(artifact.entry).toBe('bundle/server/index.mjs');
expect(fs.existsSync(path.join(artifact.dir, 'main.mjs'))).toBe(true);
expect(fs.existsSync(path.join(artifact.dir, 'bundle', 'nitro.json'))).toBe(true);
expect(
fs.readFileSync(path.join(artifact.dir, 'bundle', 'public', 'composer.txt'), 'utf8'),
).toBe('tanstack-start-composer-public-asset\n');
}, 20_000);

test('the built Nitro server renders SSR and serves its public tree', async () => {
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'composer-tanstack-start-'));
tmpDirs.push(cwd);
const artifact = await assembleTanStackStart({
build: webService.build,
address: 'web',
cwd,
});

const port = await freePort();
const child = Bun.spawn([process.execPath, path.join(artifact.dir, artifact.entry)], {
cwd: artifact.dir,
env: { ...process.env, HOST: '127.0.0.1', PORT: String(port) },
stdout: 'ignore',
stderr: 'inherit',
});
processes.push(child);

const origin = `http://127.0.0.1:${port}`;
const html = await (await fetchWhenReady(origin)).text();
expect(html).toContain('TanStack Start on Prisma Composer');

const asset = await (await fetchWhenReady(`${origin}/composer.txt`)).text();
expect(asset).toBe('tanstack-start-composer-public-asset\n');
}, 20_000);
});
9 changes: 9 additions & 0 deletions examples/tanstack-start/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"jsx": "react-jsx",
"types": ["bun", "vite/client"]
},
"include": ["module.ts", "prisma-composer.config.ts", "vite.config.ts", "src", "tests"]
}
Loading
Loading