RSC Smoke Gate
A real Next.js App Router build that fails when a component isn't safe across the server/client boundary.
RSC Smoke Gate
@infinitibit_gmbh/ui is built to be consumed in React Server Components — Next.js App Router
is the canonical RSC consumer. axe, keyboard and visual all check rendered behavior; none
check the server/client module boundary. A component that uses a hook or browser API
without a "use client" directive compiles fine under Vite/Storybook, passes type-check, and
still breaks a consumer's next build. The RSC gate is the only one that catches that.
It builds a minimal Next.js App Router app (tests/rsc-smoke) whose server page crosses the
boundary — it imports the real Icon from the built package and renders a "use client"
island — and runs next build. App Router prerenders the page, so an RSC-unsafe import fails
the build. It runs inside pnpm gate, so it fires locally and in CI with no extra steps.
Use it
There's nothing to author per component — the gate is a build. To run it:
pnpm gate # full suite, incl. RSC smoke
pnpm verify:rsc # RSC gate only
verify:rsc builds @infinitibit_gmbh/ui (so Next imports the published dist, not source) then
runs next build on tests/rsc-smoke. Green means the package is server/client-boundary safe.
If a component reaches the server graph unsafely, the build fails and Next names the file and
the offending hook — for example:
You're importing a component that needs `useState`. This React Hook only works in a
Client Component. To fix, mark the file (or its parent) with the "use client" directive.
./app/page.tsx
Fix the component (add "use client" where it belongs, or stop importing a client-only module
into a server component) until the gate is green.
How it works
- A real
next build, not a simulation.tests/rsc-smoke/app/page.tsxis a server component that importsIconfrom@infinitibit_gmbh/ui/iconsand renders a local"use client"island (app/counter.tsx).next buildcompiles the RSC graph and statically prerenders the page — the only honest proof of RSC-safety, because the server/client split is a property of the RSC compiler, not of types or jsdom. - It tests the published artifact. Next resolves the
importexport condition, so it imports@infinitibit_gmbh/ui'sdist— what consumers actually get.verify:rscbuilds the library first. - Wired by inheritance.
verify:rscjoins thetestscript'sverify:*chain, sopnpm test→pnpm gate→ci.ymlpick it up with no workflow edit — the extension model from Testing & CI. Unlike visual, it needs no pinned container: a build resolves the boundary or it doesn't, identically everywhere. - A fixture, kept out of turbo.
tests/rsc-smokeis a workspace member (so Next deps install) but defines no turbo task, soturbo run *skips it and Next stays out of the bare gate's turbo runs. The fixture disables ESLint during its build (App Router requires default exports, which the library lint rules ban); type-checking stays on, and the RSC error is a compile error regardless.
See ADR 0013 for the full rationale (and why it builds the real package instead of linting for directives).
Maintain it
- Extend coverage by importing a new component into
tests/rsc-smoke/app/page.tsx(a server component). From the golden Button (#4), swap or joinIconthere — that's the whole wiring. - Keep the page a server component. Its lack of
"use client"is the point: it's what makes an unsafe import fail. Put new client-only demos behind a"use client"island likeapp/counter.tsx. - Pin moves deliberately. Next/React versions live in
tests/rsc-smoke/package.json; bump them on purpose, then re-runpnpm verify:rsc. - Build, not behavior. This gate proves the package builds in RSC; it doesn't assert runtime output. Pair it with the axe, keyboard and visual gates for rendered behavior.