Skip to content

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

Merged
wmadden merged 26 commits into
mainfrom
tml-2298-react-router-example
May 1, 2026
Merged

feat(examples): add React Router framework example#368
wmadden merged 26 commits into
mainfrom
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

Summary by CodeRabbit

  • New Features

    • Added a React Router v7 example app showcasing server-side loaders/actions, a DB-backed user list, HMR-safe DB client lifecycle, and emitted DB contract/types for Postgres.
  • Documentation

    • Added a comprehensive quickstart, workflow notes, scripts, and guidance to switch contract authoring surfaces.
  • Tests

    • Added smoke and offline e2e tests validating contract emit, runtime integration, and contract re-emission.
  • Chores

    • Added example env sample, .gitignore, and project configs (TS, Vite, Vitest, Turbo, package manifest).

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 7bae2a82-86c2-4422-839d-3346b3831fe9

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
📝 Walkthrough

Walkthrough

Adds a new React Router v7 example (examples/react-router-demo) demonstrating Prisma Next + Postgres integration with contract emit, HMR-safe runtime, build/test configs, and end-to-end smoke/offline tests. Adds env, gitignore, prisma contract sources (TS + .prisma + generated JSON/.d.ts), runtime DB helper, routes, Vite/Vitest/Turbo/tsconfig, and docs.

Changes

Cohort / File(s) Summary
Project metadata & env
examples/react-router-demo/.env.example, examples/react-router-demo/.gitignore, examples/react-router-demo/package.json, examples/react-router-demo/biome.jsonc, examples/react-router-demo/tsconfig.json, examples/react-router-demo/turbo.json
Add example-level env example, gitignore, package manifest, Biome/TS/Turbo configs and scripts for dev/build/test/typecheck/lint.
Prisma contract sources & generated artifacts
examples/react-router-demo/prisma/contract.prisma, examples/react-router-demo/prisma/contract.ts, examples/react-router-demo/src/prisma/contract.json, examples/react-router-demo/src/prisma/contract.d.ts
Add PSL and TS contract sources and generated contract.json/.d.ts representing User/Post models and Postgres-specific capabilities.
Runtime DB helper & server integration
examples/react-router-demo/app/lib/db.server.ts
New lazy, module-cached Postgres pool + Prisma Next runtime initializer reading DATABASE_URL, handling pool errors, exposing getDb() and performing HMR disposal to end pool and clear cache.
App routes & UI
examples/react-router-demo/app/root.tsx, examples/react-router-demo/app/routes.ts, examples/react-router-demo/app/routes/users.tsx
Add app root component, route manifest, and users route with loader/action that query/insert user rows and a simple users list + create form UI.
Build & dev integrations
examples/react-router-demo/prisma-next.config.ts, examples/react-router-demo/vite.config.ts, examples/react-router-demo/react-router.config.ts, examples/react-router-demo/vitest.config.ts
Prisma Next config supporting PSL or TS contract source, Vite config using prisma contract-emit + react-router plugin + tsconfig paths, react-router SSR config, and Vitest config tuned for smoke tests.
Tests
examples/react-router-demo/test/react-router.smoke.e2e.test.ts, examples/react-router-demo/test/offline-emit.e2e.test.ts
Add smoke E2E that exercises dev server, contract emit on change, DB seeding via SQL and app POST, runtime reload; and offline emit test verifying contract emit without DATABASE_URL.
Docs & README
examples/react-router-demo/README.md, packages/1-framework/.../vite-plugin-contract-emit/README.md
Add example README describing quickstart, emit behavior, HMR disposal note; update plugin README examples section to include the new React Router example.

Sequence Diagram(s)

sequenceDiagram
  participant Dev as Developer (pnpm dev)
  participant Vite as Vite Dev Server
  participant Plugin as `@prisma-next/vite-plugin-contract-emit`
  participant DB as Postgres (prisma dev db / pg.Pool)
  participant App as SSR App (loader/action)
  participant Browser as Browser

  Dev->>Vite: start dev server
  Vite->>Plugin: init + watch contract sources
  Plugin->>Vite: emit contract.json / contract.d.ts
  Plugin->>DB: (optional) uses DATABASE_URL to validate/emit
  Browser->>Vite: request page (SSR)
  Vite->>App: load SSR module (imports app/lib/db.server.ts)
  App->>DB: getDb() -> pg.Pool -> Prisma runtime queries
  App->>Browser: HTML response with UI
  Browser->>App: submit form -> action
  App->>DB: action inserts user
  Dev->>Plugin: edit contract.prisma / contract.ts
  Plugin->>Vite: re-emit contract.json
  Vite->>App: HMR dispose/load updated modules (import.meta.hot.dispose)
  App->>DB: reopen pool / reinitialize runtime with new contract
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • TML-1817: Vite plugin #109 — Related React Router example integration using @prisma-next/vite-plugin-contract-emit in vite config and relying on contract emit behavior.

Suggested reviewers

  • SevInf
  • igalklebanov

Poem

"I hopped through code and fields of green,
Emitting contracts crisp and clean.
A pool I tend, then nuzzle logs,
HMR hugs, no tangled clogs.
Two users, one carrot cake — celebrate the scene! 🥕🐇"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(examples): add React Router framework example' accurately and directly describes the main change—addition of a React Router framework mode example to the examples directory.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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/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: 58330d0

Comment thread examples/react-router-demo/app/lib/db.server.ts
Comment thread examples/react-router-demo/app/routes/users.tsx Outdated
Comment thread examples/react-router-demo/prisma/contract.prisma
Comment thread examples/react-router-demo/test/react-router.smoke.e2e.test.ts Outdated
Base automatically changed from tml-2294-vite-support-matrix to main April 29, 2026 08:40
@jkomyno jkomyno force-pushed the tml-2298-react-router-example branch from 08e5bc1 to 6ff5625 Compare April 29, 2026 08:44
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
examples/react-router-demo/prisma-next.config.ts (1)

11-29: Validate PRISMA_NEXT_CONTRACT_SOURCE instead of silently defaulting on typos.

Right now, any unexpected value (e.g. TS, typescript) falls through to PSL mode without signal, which can make CI/debugging noisy.

Suggested hardening
-const useTs = process.env['PRISMA_NEXT_CONTRACT_SOURCE'] === 'ts';
+const contractSource = process.env['PRISMA_NEXT_CONTRACT_SOURCE'] ?? 'psl';
+if (contractSource !== 'psl' && contractSource !== 'ts') {
+  throw new Error("PRISMA_NEXT_CONTRACT_SOURCE must be 'ts' or 'psl'.");
+}
+const useTs = contractSource === 'ts';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react-router-demo/prisma-next.config.ts` around lines 11 - 29,
Validate the PRISMA_NEXT_CONTRACT_SOURCE value instead of silently treating
anything other than the exact 'ts' as PSL: add explicit validation of
process.env['PRISMA_NEXT_CONTRACT_SOURCE'] before computing useTs and if the
value is non-empty and not exactly 'ts' or 'psl' throw a clear Error (or
process.exit) so CI fails fast; update the logic around useTs/contract (symbols:
PRISMA_NEXT_CONTRACT_SOURCE, useTs, typescriptContract, prismaContract,
defineConfig) to only allow 'ts' to select typescriptContract, 'psl' to select
prismaContract, and surface an error for any other value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/react-router-demo/.env.example`:
- Line 4: Remove the surrounding double quotes from the DATABASE_URL value so
the environment variable is unquoted (change
DATABASE_URL="postgres://postgres:postgres@127.0.0.1:5432/postgres" to
DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/postgres) to resolve
the dotenv-linter QuoteCharacter warning; update the .env example entry for
DATABASE_URL accordingly.

In `@examples/react-router-demo/app/routes/users.tsx`:
- Around line 7-8: The query building for `plan` is missing an explicit ORDER BY
which makes the `limit(20)` nondeterministic; update the call chain on
`db.sql.user.select(...)` (the `plan` construction) to include an ordering
(e.g., order by `createdAt` DESC) before `.limit(20).build()` so that the
subsequent `rows = await db.runtime().execute(plan)` returns a deterministic set
(most recent users).

In `@examples/react-router-demo/README.md`:
- Line 3: The README currently includes a transient planning reference ("Closes
[April VP3](../../docs/planning/april-milestone.md)") which should be removed;
edit the README to delete that clause and any links to milestone/planning docs
and replace it with a stable, timeless sentence such as a short statement about
the example’s purpose (e.g., "Minimal React Router v7 Framework Mode example
demonstrating Prisma Next's Vite plugin re-emits contract artifacts on save").
Ensure you remove only the planning/milestone reference and leave the rest of
the descriptive text intact.

In `@examples/react-router-demo/test/react-router.smoke.e2e.test.ts`:
- Line 88: Replace the hardcoded 3_000ms cleanup timeout passed to
waitForFileMtimeChange(contractJsonPath, preRevertMtime, 3_000) with the shared
timeout helper from `@prisma-next/test-utils` (e.g., CLEANUP_TIMEOUT or
timeouts.CLEANUP_TIMEOUT). Update the import at the top to pull the helper
(import { CLEANUP_TIMEOUT } from '@prisma-next/test-utils') and use that symbol
in the call to waitForFileMtimeChange so the test uses the centralized CI
scaling timeout instead of a literal.

In `@packages/1-framework/3-tooling/vite-plugin-contract-emit/README.md`:
- Line 132: The README currently links to the transient planning artifact
`projects/vite-vp3-auto-emit/`; replace that `projects/...` link with a durable
reference (for example point to the actual example directory
`examples/react-router-demo` or to a stable docs page/archived doc URL) so
package docs don't reference transient project artifacts—update the line that
mentions `examples/react-router-demo` and remove/replace the
`projects/vite-vp3-auto-emit/` fragment accordingly.

---

Nitpick comments:
In `@examples/react-router-demo/prisma-next.config.ts`:
- Around line 11-29: Validate the PRISMA_NEXT_CONTRACT_SOURCE value instead of
silently treating anything other than the exact 'ts' as PSL: add explicit
validation of process.env['PRISMA_NEXT_CONTRACT_SOURCE'] before computing useTs
and if the value is non-empty and not exactly 'ts' or 'psl' throw a clear Error
(or process.exit) so CI fails fast; update the logic around useTs/contract
(symbols: PRISMA_NEXT_CONTRACT_SOURCE, useTs, typescriptContract,
prismaContract, defineConfig) to only allow 'ts' to select typescriptContract,
'psl' to select prismaContract, and surface an error for any other value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: b02f4050-cfed-4154-9a75-cc44f047655f

📥 Commits

Reviewing files that changed from the base of the PR and between 13b3a30 and 6ff5625.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (21)
  • examples/react-router-demo/.env.example
  • examples/react-router-demo/.gitignore
  • examples/react-router-demo/README.md
  • examples/react-router-demo/app/lib/db.server.ts
  • examples/react-router-demo/app/root.tsx
  • examples/react-router-demo/app/routes.ts
  • examples/react-router-demo/app/routes/users.tsx
  • examples/react-router-demo/biome.jsonc
  • examples/react-router-demo/package.json
  • examples/react-router-demo/prisma-next.config.ts
  • examples/react-router-demo/prisma/contract.ts
  • examples/react-router-demo/prisma/schema.prisma
  • examples/react-router-demo/react-router.config.ts
  • examples/react-router-demo/src/prisma/contract.d.ts
  • examples/react-router-demo/src/prisma/contract.json
  • examples/react-router-demo/test/react-router.smoke.e2e.test.ts
  • examples/react-router-demo/tsconfig.json
  • examples/react-router-demo/turbo.json
  • examples/react-router-demo/vite.config.ts
  • examples/react-router-demo/vitest.config.ts
  • packages/1-framework/3-tooling/vite-plugin-contract-emit/README.md

Comment thread examples/react-router-demo/.env.example Outdated
Comment thread examples/react-router-demo/app/routes/users.tsx Outdated
Comment thread examples/react-router-demo/README.md Outdated
Comment thread examples/react-router-demo/test/react-router.smoke.e2e.test.ts Outdated
Comment thread packages/1-framework/3-tooling/vite-plugin-contract-emit/README.md Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
examples/react-router-demo/test/react-router.smoke.e2e.test.ts (1)

136-141: ⚡ Quick win

This startup "emit" check is satisfied by a stale committed artifact.

Because originalMtime is null, this returns true as soon as src/prisma/contract.json already exists. In this example that file is committed, so Lines 136-141 do not prove Vite emitted anything for the current server instance. Capture the pre-boot mtime first, or remove the artifact before boot, and wait for an actual change.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react-router-demo/test/react-router.smoke.e2e.test.ts` around lines
136 - 141, The startup emit check uses waitForFileMtimeChange(contractJsonPath,
null, ...) so it returns true if the committed src/prisma/contract.json already
exists; instead capture the pre-boot mtime (call
waitForFileMtimeChange(contractJsonPath) or read mtime) before starting the
server and pass that as originalMtime to waitForFileMtimeChange, or delete the
committed artifact before boot and then call
waitForFileMtimeChange(contractJsonPath, null, ...); update the code around the
initialEmit/contractJsonPath usage to use the captured pre-boot mtime (or ensure
the file is removed) so the test waits for an actual emit rather than a stale
file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/react-router-demo/prisma-next.config.ts`:
- Around line 13-30: The config is missing an explicit extensions property;
update the defineConfig call to include extensions alongside family, target,
adapter, driver so the config satisfies the typed contract (use the same
familyId/targetId values used for family and target), e.g. add an extensions: []
entry (or appropriate array) to the exported object so that defineConfig({
family: sql, target: postgres, driver: postgresDriver, adapter: postgresAdapter,
extensions: ..., contract: ..., db: ... }) includes a concrete extensions field
referenced by the types.
- Around line 9-20: The file currently statically imports `contract` which
forces the TS module to be resolved even when PRISMA_NEXT_CONTRACT_SOURCE !==
'ts'; move the import behind the `useTs` branch (lazy/dynamic import) or split
into two separate config paths so `contract` is only imported when `useTs` is
true (refer to the `useTs` variable and the
`typescriptContract`/`prismaContract` branches in `defineConfig`); also add the
required `extensions` property to the exported config object with entries whose
`familyId` and `targetId` match the `family` and `target` values used in this
config.

In `@examples/react-router-demo/test/react-router.smoke.e2e.test.ts`:
- Around line 45-60: The mtime poller waitForFileMtimeChange uses existsSync +
stat which is TOCTOU-prone; remove the two-step check and wrap the stat call in
a try/catch, treating ENOENT as a transient condition (continue polling) instead
of failing, and only return true when stat succeeds and mtimeMs is greater than
originalMtime (or originalMtime is null); ensure other errors are rethrown or
logged as appropriate so the test still fails on unexpected I/O errors.

---

Nitpick comments:
In `@examples/react-router-demo/test/react-router.smoke.e2e.test.ts`:
- Around line 136-141: The startup emit check uses
waitForFileMtimeChange(contractJsonPath, null, ...) so it returns true if the
committed src/prisma/contract.json already exists; instead capture the pre-boot
mtime (call waitForFileMtimeChange(contractJsonPath) or read mtime) before
starting the server and pass that as originalMtime to waitForFileMtimeChange, or
delete the committed artifact before boot and then call
waitForFileMtimeChange(contractJsonPath, null, ...); update the code around the
initialEmit/contractJsonPath usage to use the captured pre-boot mtime (or ensure
the file is removed) so the test waits for an actual emit rather than a stale
file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 98c06de2-dd0f-4325-8080-1b2f238051fe

📥 Commits

Reviewing files that changed from the base of the PR and between 6ff5625 and 99c298c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (10)
  • examples/react-router-demo/.env.example
  • examples/react-router-demo/README.md
  • examples/react-router-demo/app/lib/db.server.ts
  • examples/react-router-demo/app/routes/users.tsx
  • examples/react-router-demo/package.json
  • examples/react-router-demo/prisma-next.config.ts
  • examples/react-router-demo/prisma/contract.prisma
  • examples/react-router-demo/test/react-router.smoke.e2e.test.ts
  • examples/react-router-demo/tsconfig.json
  • packages/1-framework/3-tooling/vite-plugin-contract-emit/README.md
✅ Files skipped from review due to trivial changes (5)
  • examples/react-router-demo/.env.example
  • examples/react-router-demo/tsconfig.json
  • examples/react-router-demo/README.md
  • examples/react-router-demo/package.json
  • examples/react-router-demo/prisma/contract.prisma
🚧 Files skipped from review as they are similar to previous changes (2)
  • examples/react-router-demo/app/lib/db.server.ts
  • packages/1-framework/3-tooling/vite-plugin-contract-emit/README.md

Comment on lines +9 to +20
import { contract } from './prisma/contract';

const useTs = process.env['PRISMA_NEXT_CONTRACT_SOURCE'] === 'ts';

export default defineConfig({
family: sql,
target: postgres,
driver: postgresDriver,
adapter: postgresAdapter,
contract: useTs
? typescriptContract(contract, 'src/prisma/contract.json')
: prismaContract('./prisma/contract.prisma', {
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In ECMAScript modules, are top-level static imports resolved and evaluated even when the imported binding is only used inside a conditional branch that never runs?

💡 Result:

Yes, in ECMAScript modules, top-level static imports are resolved, linked, and evaluated regardless of whether the imported binding is used inside a conditional branch that never runs. The module evaluation process is independent of runtime usage in the importing module.

Citations:


Move the TS contract import behind the TS branch or split configs by source mode.

Line 9 statically imports ./prisma/contract, which is resolved and evaluated regardless of the PRISMA_NEXT_CONTRACT_SOURCE condition. This creates a hard PSL dependency on the TS authoring module and undermines the source toggle. Either move the TS import into the useTs branch or maintain separate configs.

Additionally, add the missing extensions property required by the config schema (must specify extensions with matching familyId and targetId).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react-router-demo/prisma-next.config.ts` around lines 9 - 20, The
file currently statically imports `contract` which forces the TS module to be
resolved even when PRISMA_NEXT_CONTRACT_SOURCE !== 'ts'; move the import behind
the `useTs` branch (lazy/dynamic import) or split into two separate config paths
so `contract` is only imported when `useTs` is true (refer to the `useTs`
variable and the `typescriptContract`/`prismaContract` branches in
`defineConfig`); also add the required `extensions` property to the exported
config object with entries whose `familyId` and `targetId` match the `family`
and `target` values used in this config.

Comment on lines +13 to +30
export default defineConfig({
family: sql,
target: postgres,
driver: postgresDriver,
adapter: postgresAdapter,
contract: useTs
? typescriptContract(contract, 'src/prisma/contract.json')
: prismaContract('./prisma/contract.prisma', {
output: 'src/prisma/contract.json',
target: postgres,
}),
db: {
// Left undefined when DATABASE_URL is not set so emit-only flows
// (`prisma-next contract emit`, CI typegen) work in fresh checkouts.
// Commands that need a connection surface their own error pointing at
// `db.connection` or `--db <url>`.
connection: process.env['DATABASE_URL'],
},
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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add extensions explicitly to the config.

This repo expects prisma-next.config.ts to wire family, target, adapter, driver, and extensions together as one typed unit. Leaving extensions implicit here skips that part of the contract; set it explicitly, even if the example has no extensions yet.

♻️ Proposed fix
 export default defineConfig({
   family: sql,
   target: postgres,
   driver: postgresDriver,
   adapter: postgresAdapter,
+  extensions: [],
   contract: useTs
     ? typescriptContract(contract, 'src/prisma/contract.json')
     : prismaContract('./prisma/contract.prisma', {
         output: 'src/prisma/contract.json',
         target: postgres,

As per coding guidelines: "**/prisma-next.config.ts: Config files must use defineConfig from '@prisma-next/cli/config-types' and specify family, target, adapter, driver, and extensions with matching familyId and targetId".

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default defineConfig({
family: sql,
target: postgres,
driver: postgresDriver,
adapter: postgresAdapter,
contract: useTs
? typescriptContract(contract, 'src/prisma/contract.json')
: prismaContract('./prisma/contract.prisma', {
output: 'src/prisma/contract.json',
target: postgres,
}),
db: {
// Left undefined when DATABASE_URL is not set so emit-only flows
// (`prisma-next contract emit`, CI typegen) work in fresh checkouts.
// Commands that need a connection surface their own error pointing at
// `db.connection` or `--db <url>`.
connection: process.env['DATABASE_URL'],
},
export default defineConfig({
family: sql,
target: postgres,
driver: postgresDriver,
adapter: postgresAdapter,
extensions: [],
contract: useTs
? typescriptContract(contract, 'src/prisma/contract.json')
: prismaContract('./prisma/contract.prisma', {
output: 'src/prisma/contract.json',
target: postgres,
}),
db: {
// Left undefined when DATABASE_URL is not set so emit-only flows
// (`prisma-next contract emit`, CI typegen) work in fresh checkouts.
// Commands that need a connection surface their own error pointing at
// `db.connection` or `--db <url>`.
connection: process.env['DATABASE_URL'],
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react-router-demo/prisma-next.config.ts` around lines 13 - 30, The
config is missing an explicit extensions property; update the defineConfig call
to include extensions alongside family, target, adapter, driver so the config
satisfies the typed contract (use the same familyId/targetId values used for
family and target), e.g. add an extensions: [] entry (or appropriate array) to
the exported object so that defineConfig({ family: sql, target: postgres,
driver: postgresDriver, adapter: postgresAdapter, extensions: ..., contract:
..., db: ... }) includes a concrete extensions field referenced by the types.

Comment thread examples/react-router-demo/test/react-router.smoke.e2e.test.ts
@cursor cursor Bot force-pushed the tml-2298-react-router-example branch from 32828b0 to 2c2f7eb Compare May 1, 2026 08:24
jkomyno and others added 19 commits May 1, 2026 08:25
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>
- 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.
Two small follow-ups from local PR review.

README:
- The previous link to projects/vite-vp3-auto-emit/tickets/apr-vp3-07-hmr-safe-runtime-helper.md
  pointed at a file that doesn't exist anywhere in the repo (it was a local
  working-tree-only path on the author's machine). Replace the link with a
  prose reference to the Linear ticket so the README doesn't ship a 404.

Smoke test:
- Strengthen the comment above TEST_SCHEMA_SQL. Explain why this smoke test
  uses raw DDL instead of the control client's `dbInit`: the test's job is to
  validate VP3 (auto-emit + serve through framework runtime), and the dbInit
  path is exercised by `test/integration/test/cli.db-init.e2e.test.ts`.
  Inlining the DDL keeps the test readable top-to-bottom and bounds the flake
  surface to VP3 itself.

No code-behavior changes. No new tests. No new dependencies.
…reate

The action discards the result and redirects, so requesting `id`/`email`
back is dead code. Drop the `.returning(...)` chain to keep the example
honest about which DSL surfaces are actually exercised.
The previous `max: 1` was hard-coded into `db.server.ts` for a constraint
that only exists in the smoke test (PGlite rejects concurrent
connections). Anyone copying this example as a production reference would
inherit a 1-connection pool by accident.

Move the cap behind the `PRISMA_NEXT_DEMO_PG_POOL_MAX` env var. The smoke
test sets it to `'1'`; the production path leaves it unset so the pg
default applies.
Throwing on missing DATABASE_URL at config-load time made
\`prisma-next contract emit\` (and CI typegen) fail in fresh checkouts
even though emit doesn't touch the database. The CLI already produces a
command-aware error for connection-requiring commands, pointing at
\`db.connection\` or \`--db <url>\` — duplicating it here just blocked
the legitimate offline path.

Pass \`process.env['DATABASE_URL']\` through as-is and let the CLI
surface its own error when it actually needs a connection.
…re-emit

Use Vite ssrLoadModule to pull a fresh getDb() after the PSL edit, then
build a SELECT plan that references the newly emitted nickname column.
Without HMR cache invalidation the SQL builder synchronously throws
"Column nickname not found in scope", so this closes the AC11 gap where
the prior follow-up GET only exercised pre-existing fields.
Rename PRISMA_NEXT_DEMO_PG_POOL_MAX to REACT_ROUTER_DEMO_PG_POOL_MAX so
the variable name matches this examples package and cannot be confused
with the sibling examples/prisma-next-demo. The smoke test is the only
caller; no other readers depend on the old name.
…rd getDb on missing DATABASE_URL

- Promote the dispose handler to async and await cached.pool.end() so the
  next getDb() never opens a new pool while the old one is still draining
  (Vite supports Promise-returning dispose handlers). Capture the pool in
  a local before clearing the cache so a concurrent caller cannot observe
  the half-disposed cached entry.
- Throw a clear error from getDb() when DATABASE_URL is unset rather than
  silently falling back to libpq env-var defaults. The check is inside
  getDb(), not at module load, so emit-only flows (CI typegen, fresh
  checkouts without DATABASE_URL) keep working.
- Add a TODO(APR-VP3-07) marker near the dispose so the trail to the
  follow-up hash-keyed dev helper is discoverable from the call site.
Add react-router.config.ts, vite.config.ts, and vitest.config.ts to the
tsconfig include list so pnpm typecheck catches type errors in them.
The repo already includes prisma-next.config.ts; this brings the three
sibling tooling configs in line.
…oke test

Switch the smoke test to import path helpers from pathe (per the repo
rule .cursor/rules/use-pathe-for-paths.mdc) and replace the synchronous
readFileSync/writeFileSync calls with the asynchronous readFile/writeFile
from node:fs/promises. The test already used stat from fs/promises, so
this also removes the asymmetry inside an async hook.

Adds pathe to devDependencies; refreshes pnpm-lock.yaml.
…m, document deliberate validation omission

- Reword the README "Switching authoring surfaces" section so the TS line
  describes the actual mechanism: the plugin walks the prisma-next.config
  module graph and watches every imported file (including prisma/contract.ts),
  rather than declaring an explicit watch path.
- Add a comment above the users action explaining that input validation is
  deliberately omitted because this example is a validation harness for
  the Vite plugin auto-emit flow, not a production starter.
…ke teardown

Capture byte-equal baselines for prisma/contract.prisma and
src/prisma/contract.json in beforeEach, then assert in afterEach (after
the existing revert + wait-for-re-emit + server.close + dev.close
sequence) that both files match the baseline byte-for-byte.

Closes the AC10 gap: prior coverage relied on teardown sequencing alone,
so a regression in any of those steps would leave the working tree dirty
without test failure. The assertion now turns "tree clean" into the
explicit invariant.
…ABASE_URL)

Add an e2e test that spawns the example workspace binary at
node_modules/.bin/prisma-next contract emit with DATABASE_URL stripped
from the child env (along with the libpq fallback PG* vars), asserts the
process exits cleanly, and asserts the expected artifacts exist on disk.

Locks in AC9: prisma-next.config.ts was specifically modified in
6d11148 to keep db.connection undefined when DATABASE_URL is missing
rather than throwing at config-load time. Without this test, a future
regression of that path would only surface as a CI failure outside this
example. Captures byte-equal baselines for contract.json and
contract.d.ts and restores them in afterEach so the working tree stays
clean even if a future emitter change makes the rewrite non-idempotent.
@cursor cursor Bot force-pushed the tml-2298-react-router-example branch from 2c2f7eb to 7b995de Compare May 1, 2026 08:28
wmadden added 2 commits May 1, 2026 08:31
…ract types with main

After rebasing onto main:

- The `prisma_contract.marker` DDL now requires an `invariants text[] not null
  default '{}'` column (added in the invariant-aware-routing M1 work on main).
  The smoke test bootstraps the marker via inlined raw DDL rather than going
  through the control client's `dbInit`, so the new column had to be added
  here too — otherwise the runtime's marker upsert fails with `column
  "invariants" does not exist` on the action's INSERT path.

- `PgAdapterQueryOps` now takes the `CodecTypes` argument as a type parameter
  (`PgAdapterQueryOps<CodecTypes>`). The committed `contract.d.ts` regenerated
  to match the new shape on first emit; committing the regenerated file so it
  stays byte-equal with the emitter's output.
The previous TODO referenced `APR-VP3-07`, an internal milestone ID that was
not tracked anywhere durable from inside the repo. Linear ticket TML-2368
("Replace example-local HMR runtime cache with a hash-keyed dev helper shared
across frameworks", project [PN] Rough Edges) now owns the follow-up; pointing
the call-site comment at it so the trail is discoverable from `db.server.ts`
without prose archaeology.

https://linear.app/prisma-company/issue/TML-2368
wmadden added 5 commits May 1, 2026 08:59
…oller

The plugin publishes contract artifacts atomically via temp-write + rename
(publishContractArtifactPair in @prisma-next/cli/control-api). Between the
unlink/rename window the target path briefly does not exist, so an existsSync
+ stat pair is TOCTOU-prone — stat() can throw ENOENT after existsSync
returned true. Today this is unlikely to bite (atomic rename in practice),
but the smoke test runs in CI and the cost of defending against it is small.

Replace the existsSync guard with a single stat call wrapped in try/catch
that swallows ENOENT and rethrows everything else:

- in waitForFileMtimeChange, ENOENT means "not yet" — keep polling.
- in afterEach's pre-revert mtime read, ENOENT means "no prior artifact to
  compare against" — same semantics as the previous existsSync short-circuit
  (preRevertMtime stays null).

Drops the existsSync import. No production-code changes; smoke and
offline-emit tests still pass.

Addresses CodeRabbit thread on test/react-router.smoke.e2e.test.ts:61.
…n decisions

Two CodeRabbit threads on prisma-next.config.ts surfaced concerns the
PR is deliberately not addressing. The reasoning was scattered across
the local review and the PR walkthrough; lifting it to call-site
comments so future readers (human or bot) don't re-litigate:

1. Static import of ./prisma/contract is intentional. ESM evaluates
   it even when PRISMA_NEXT_CONTRACT_SOURCE != 'ts', but contract.ts
   is a pure defineContract call (no I/O). The two structural fixes
   were considered and rejected: typescriptContractFromPath (m1 review
   D05, conservative scope) and splitting into two configs (the older
   pattern this example replaces).

2. extensionPacks is optional and the example has no extensions to
   register. The schema property is `extensionPacks` — `validateConfig`
   actively throws if it sees `extensions`, so suggestions to add
   `extensions: []` would break config load.

No behaviour change.
… env var

Throw a clear error when PRISMA_NEXT_CONTRACT_SOURCE is set to a value
other than `ts` or `psl` (e.g. `TS`, `typescript`, typos), instead of
silently falling through to the PSL default. Unset/empty still defaults
to PSL so emit-only flows in fresh checkouts continue to work.
…e in smoke test

Pass the pre-boot mtime of `contract.json` to `waitForFileMtimeChange`
instead of `null` so the startup re-emit assertion actually waits for a
fresh emit on this server instance. Previously, with `originalMtime:
null`, the helper returned true the moment the committed artifact existed
on disk, which always succeeded regardless of whether the Vite plugin
ran. Capture mtime before `createServer()` (the call that runs the
plugin's `configureServer` post-hook and its initial emit) so the
poll waits for a *post-boot* mtime even when the committed artifact is
already on disk. Treat ENOENT as a null baseline so future changes that
stop committing the artifact still work.
@wmadden wmadden merged commit 1b7128d into main May 1, 2026
16 checks passed
@wmadden wmadden deleted the tml-2298-react-router-example branch May 1, 2026 11:42
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