Skip to content

feat(examples): add React Router framework example#368

Open
jkomyno wants to merge 3 commits intotml-2294-vite-support-matrixfrom
tml-2298-react-router-example
Open

feat(examples): add React Router framework example#368
jkomyno wants to merge 3 commits intotml-2294-vite-support-matrixfrom
tml-2298-react-router-example

Conversation

@jkomyno
Copy link
Copy Markdown
Contributor

@jkomyno jkomyno commented Apr 22, 2026

Summary

  • add examples/react-router-demo — a React Router v7.14 Framework Mode example with one index route exposing both a loader (SELECT) and an action (INSERT + redirect) that run through @prisma-next/postgres on the server via app/lib/db.server.ts (getDb() pattern, mirroring examples/retail-store/src/db-singleton.ts)
  • prove the April VP3 stop condition inside a real framework: a smoke test boots Vite programmatically against a PGlite database (via @prisma-next/test-utils.createDevDatabase), POSTs the action, GETs the loader, edits prisma/schema.prisma mid-flight, and asserts the contract re-emits with the new column without a manual command
  • collapse the PSL/TS authoring toggle into a single prisma-next.config.ts gated on PRISMA_NEXT_CONTRACT_SOURCE=ts|psl — no sibling .ts-contract.ts file, no --config flag
  • update @prisma-next/vite-plugin-contract-emit's README to point at both prisma-next-demo and react-router-demo

Plan-vs-reality deltas

  • Pool size: @prisma-next/postgres exposes poolOptions but only for timeouts. Cap via an externally-constructed new Pool({ connectionString, max: 1 }) passed as pg:, which is public API. max: 1 is required so the app cohabits with @prisma/dev's single-connection PGlite.
  • Writes: used db.sql.user.insert({ email }).returning(...) (the same pattern as examples/prisma-next-demo/src/queries/dml-operations.ts) rather than the ORM create() path, which requires explicit id/createdAt with branded-type casts.
  • Index-route POST: the smoke test POSTs to /?index per React Router's convention for disambiguating index routes from their parent layout.
  • No Vite 7/8 matrix for this example: covered at the plugin level by APR-VP3-05 (PR test(vite): narrow and validate Vite 7/8 support #367).
  • TS re-emit is a manual README repro: already covered at the plugin level by test/integration/test/vite-plugin.hmr.e2e.test.ts. Running it a second time through React Router would double flake surface without adding signal.

Testing

pnpm install
pnpm -C examples/react-router-demo typecheck     # react-router typegen + tsc --noEmit
pnpm -C examples/react-router-demo lint          # biome check
pnpm -C examples/react-router-demo test          # smoke e2e against PGlite
pnpm lint:deps                                    # import-layering check

All four green locally.

Tracking

  • Linear: TML-2298
  • Parent PR: test(vite): narrow and validate Vite 7/8 support #367
  • Project: projects/vite-vp3-auto-emit/ — ticket apr-vp3-06-react-router-example.md, milestone 3
  • Follow-up: APR-VP3-07 replaces the plain getDb() cache with a hash-keyed dev helper so HMR doesn't leave a stale runtime after re-emit

Closes April milestone VP3 by demonstrating invisible contract emission
inside a Vite-based framework. Part of TML-2298 / the vite-vp3-auto-emit
project.

The example is a minimal React Router v7.14 Framework Mode app with one
index route exposing both a loader (SELECT users) and an action (INSERT
user + redirect). The Prisma Next runtime lives in `app/lib/db.server.ts`
behind a `getDb()` function that stashes the client on globalThis, mirroring
the prior art in `examples/retail-store/src/db-singleton.ts`. Pool size is
capped at 1 so the smoke test stays compatible with @prisma/dev's
single-connection PGlite harness.

A single `prisma-next.config.ts` supports both PSL and TS authoring, gated
on `PRISMA_NEXT_CONTRACT_SOURCE`. No sibling `.ts-contract.ts` file, no
`--config` flag — one env flip at dev-server startup.

The smoke test (`test/react-router.smoke.e2e.test.ts`) stands up a PGlite
database via `@prisma-next/test-utils`, boots Vite programmatically via
`createServer`, POSTs to the action, GETs the loader, edits
`prisma/schema.prisma` mid-flight, and asserts the contract re-emits with
the new column. TS re-emit is already covered at the plugin level
(`test/integration/test/vite-plugin.hmr.e2e.test.ts`) and is documented as
a manual README repro to avoid doubling flake surface.

Vite 7/8 compat is covered by APR-VP3-05's plugin matrix, not by this
example's CI job, for the same reason.

Updates the Vite plugin README to reference the new example alongside
`prisma-next-demo`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 4bf63a40-a884-42b0-80d0-bf10f9405c19

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2298-react-router-example

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 22, 2026

Open in StackBlitz

@prisma-next/mongo-runtime

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-runtime@368

@prisma-next/family-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/family-mongo@368

@prisma-next/sql-runtime

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-runtime@368

@prisma-next/family-sql

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/family-sql@368

@prisma-next/middleware-telemetry

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/middleware-telemetry@368

@prisma-next/mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo@368

@prisma-next/extension-paradedb

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-paradedb@368

@prisma-next/extension-pgvector

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-pgvector@368

@prisma-next/postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/postgres@368

@prisma-next/sql-orm-client

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-orm-client@368

@prisma-next/sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sqlite@368

@prisma-next/target-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-mongo@368

@prisma-next/adapter-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-mongo@368

@prisma-next/driver-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-mongo@368

@prisma-next/contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/contract@368

@prisma-next/utils

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/utils@368

@prisma-next/config

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/config@368

@prisma-next/errors

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/errors@368

@prisma-next/framework-components

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/framework-components@368

@prisma-next/operations

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/operations@368

@prisma-next/ts-render

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/ts-render@368

@prisma-next/contract-authoring

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/contract-authoring@368

@prisma-next/ids

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/ids@368

@prisma-next/psl-parser

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/psl-parser@368

@prisma-next/psl-printer

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/psl-printer@368

@prisma-next/cli

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/cli@368

@prisma-next/emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/emitter@368

@prisma-next/migration-tools

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/migration-tools@368

prisma-next

npm i https://pkg.pr.new/prisma/prisma-next@368

@prisma-next/vite-plugin-contract-emit

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/vite-plugin-contract-emit@368

@prisma-next/runtime-executor

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/runtime-executor@368

@prisma-next/mongo-codec

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-codec@368

@prisma-next/mongo-contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-contract@368

@prisma-next/mongo-value

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-value@368

@prisma-next/mongo-contract-psl

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-contract-psl@368

@prisma-next/mongo-contract-ts

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-contract-ts@368

@prisma-next/mongo-emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-emitter@368

@prisma-next/mongo-schema-ir

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-schema-ir@368

@prisma-next/mongo-query-ast

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-query-ast@368

@prisma-next/mongo-orm

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-orm@368

@prisma-next/mongo-query-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-query-builder@368

@prisma-next/mongo-lowering

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-lowering@368

@prisma-next/mongo-wire

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-wire@368

@prisma-next/sql-contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract@368

@prisma-next/sql-errors

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-errors@368

@prisma-next/sql-operations

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-operations@368

@prisma-next/sql-schema-ir

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-schema-ir@368

@prisma-next/sql-contract-psl

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-psl@368

@prisma-next/sql-contract-ts

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-ts@368

@prisma-next/sql-contract-emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-emitter@368

@prisma-next/sql-lane-query-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-lane-query-builder@368

@prisma-next/sql-relational-core

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-relational-core@368

@prisma-next/sql-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-builder@368

@prisma-next/target-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-postgres@368

@prisma-next/target-sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-sqlite@368

@prisma-next/adapter-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-postgres@368

@prisma-next/adapter-sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-sqlite@368

@prisma-next/driver-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-postgres@368

@prisma-next/driver-sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-sqlite@368

commit: a4c5a1b

});
}
return cached;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't use the postgres facade for this?

Comment on lines +19 to +20
const plan = db.sql.user.insert({ email }).returning('id', 'email').build();
await db.runtime().execute(plan);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not the ORM client?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contract.prisma

Comment on lines +13 to +38
// Prisma Next's marker table plus our own model tables. Kept inline so the test
// reads top-to-bottom without chasing a fixture file.
const TEST_SCHEMA_SQL = `
create schema if not exists prisma_contract;
create table if not exists prisma_contract.marker (
id smallint primary key default 1,
core_hash text not null default '',
profile_hash text not null default '',
contract_json jsonb,
canonical_version int,
updated_at timestamptz not null default now(),
app_tag text,
meta jsonb not null default '{}'
);
create table if not exists "user" (
id uuid primary key default gen_random_uuid(),
email text not null,
"createdAt" timestamptz not null default now()
);
create table if not exists "post" (
id uuid primary key default gen_random_uuid(),
title text not null,
"userId" uuid not null references "user"(id),
"createdAt" timestamptz not null default now()
);
`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you using raw sql instead of the programmatic Prisma Next api (control client)?

jkomyno added 2 commits April 22, 2026 17:47
- db.server.ts: register an import.meta.hot.dispose handler so HMR after
  a contract re-emit rebuilds the runtime from the fresh contractJson
  instead of reusing the stale cached client. APR-VP3-07 will replace
  this with hash-keyed caching; this is minimum hygiene until then.

- smoke test teardown: revert prisma/schema.prisma *before* closing the
  Vite server and wait for the plugin to re-emit the clean contract.
  Previously the schema was reverted but the server was torn down before
  the watcher drained, leaving src/prisma/contract.json reflecting the
  mid-test edit and dirtying the working tree.

- smoke test: type JSON.parse result as `unknown` and narrow via
  toMatchObject (matches CLAUDE.md "never use any"); assert that the
  PSL replace() actually mutated the file so a future schema reformat
  doesn't silently no-op the test; drop the unused `_` param on the
  loader.

- vitest.config.ts: align with examples/prisma-next-demo — environment,
  pool, maxWorkers, isolate; use the @prisma-next/test-utils timeouts
  catalog helper instead of hard-coded 60_000ms.
…nd prisma/

The root turbo.json declares `test`/`typecheck`/`lint` inputs as
`src/**` + `test/**`, which matches the shape used by every other
workspace in the repo. React Router Framework Mode keeps route modules
under `app/**` and contract authoring under `prisma/**`, so without a
local override turbo would cache this example's test result across
changes to the loader, action, or db.server.ts. CI fresh checkouts are
unaffected, but local `pnpm test` at repo root could replay stale
success.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants