Files
wehub-resource-sync e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:40:13 +08:00

143 lines
7.0 KiB
Markdown

# `"use generative"` — build selection design
How one **bare import** of a generative module resolves to the server build in
route handlers and the client build in the browser, with `server-only` intact and
no per-app setup. Validated end-to-end on Next 16.2.6 + Turbopack (May 2026).
## The two consumers
A generative module forks into a **client build** (schema + `render`, `"use
client"`) and a **server build** (schema + `execute`, `import "server-only"`).
Two consumers import it:
- the **route handler** (App Router) — needs `execute`;
- a **client component** (tool-UI registration) — needs `render` + schema, and
its **SSR pass must resolve to the same build as the browser** or React throws
a hydration mismatch (#418).
## Verified layer map
| layer | `browser` cond | `react-server` cond |
| --- | --- | --- |
| RSC server component (`rsc`) | OFF | **ON** |
| App Router route handler | OFF | **ON** |
| client component, SSR pass (`ssr`) | OFF | OFF |
| client component, browser (`app-pages-browser`) | **ON** | OFF |
The `react-server` condition is **ON in exactly the layers that need the server
build** (RSC + route handlers) and **OFF where the client build is needed** (SSR
+ browser) — and SSR matches the browser, so there is no hydration mismatch. This
is also why `server-only` (`{"react-server":"./empty.js","default":"./index.js"}`,
where the default throws) is safe only in react-server layers.
## How selection happens (no `imports` field)
The loader rewrites a **bare** generative import into a facade that delegates
selection to a `react-server`-conditioned package subpath, passing the module's
own path through a **Turbopack import attribute** (a `?path=` query cannot ride a
package specifier; `with {}` can). The subpath carries a **per-module token** so
each generative module gets its own indirection identity (see "Per-module
identity" below):
```js
// facade — emitted in place of a bare import of the generative module
import toolkit from "@assistant-ui/next/bundler-redirect/<token>"
with { turbopackLoader: "@assistant-ui/next/loader",
turbopackLoaderOptions: "{\"path\":\"/abs/docs-toolkit.tsx\"}" };
export default toolkit;
```
`@assistant-ui/next/bundler-redirect/<token>` resolves through the package's
`"./bundler-redirect/*"` export by condition to one of two indirection modules,
which the loader (applied via the attribute) replaces with a re-export of the
concrete build — using a **relative** specifier computed from the indirection's
own path to the originating module (Turbopack won't resolve an absolute
specifier; a relative one is correct for both workspace symlinks and installed
packages):
```js
// react-server → bundler-redirect.server.js?aui=<token> ⇒ emitted:
export { default } from "../../../app/lib/docs-toolkit.tsx?generative-env=server";
// default → bundler-redirect.client.js?aui=<token> ⇒ emitted:
export { default } from "../../../app/lib/docs-toolkit.tsx?generative-env=client";
```
## Per-module identity
Turbopack keys runtime modules by **resolved path (+ query)**, ignoring loader
options and import attributes. A shared bare `@assistant-ui/next/bundler-redirect`
would therefore resolve every facade to the one `bundler-redirect.client.js` file
under a single key — so two generative modules register two indirection bodies
there and the last write wins, collapsing all imports onto whichever compiled
last (downstream: two `Tools` providers register the same toolkit → `tool …
already exists`). This only bites under `next dev`; a production `next build`
inlines the indirection per import site, so it resolves correctly.
So each facade imports `…/bundler-redirect/<token>`, where `<token>` is the
module's path encoded `base64url` (path-derived, so unique with no collisions;
it's already in the loader options, so it leaks nothing new). The wildcard export
```json
"./bundler-redirect/*": {
"react-server": "./dist/bundler-redirect.server.js?aui=*",
"default": "./dist/bundler-redirect.client.js?aui=*"
}
```
threads the token back as a `?aui=` **query on the resolved file**: both tokens
hit the same physical file, but the query makes each a distinct module key —
unique identity without per-module files, still selected by the static
`react-server` condition. The query is inert (`indirectionVariant` keys off the
basename; the path still arrives via the import attribute).
The `?generative-env=server|client` query then hits the loader again and compiles the
concrete build. Net resolution from one bare import:
- route handler / RSC (`react-server` ON) → **server build** (execute + `server-only`);
- SSR + browser (`react-server` OFF) → **client build** (render);
- everything is **static** (no top-level await), and the server build is only ever
resolved in react-server layers, so `server-only` never enters the SSR/client
graph and secrets never reach the browser.
## Verified (docs app)
- Browser bundle: no `server-only`, none of the server `execute` functions — **no leak**.
- Server bundle: the `execute` functions are present — server build wired.
- Page load: no hydration mismatch (clean browser console).
## Loader dispatch
1. resolution is a package **indirection** module → re-export the chosen concrete
build (path from loader options, made relative);
2. `?generative-env=client|server` query → compile that build;
3. bare generative module → emit the facade;
4. anything else → passthrough.
## Rejected alternatives
- **`browser` loader condition** to fork directly: the SSR pass is `{not:browser}`,
so a client component's SSR gets the server build → #418 + `server-only` build
error. (`browser` is fine for a *pure-server* consumer, not a client-consumed
module.)
- **Runtime `isServer` facade** (`if (isServer) import(server)`): can't prune, and
`server-only`'s build-time check follows the dynamic `import()` into the
client/SSR graph and errors.
- **A single shared `/bundler-redirect` subpath** for every module: collapses all
generative imports onto one resolved module key — see "Per-module identity".
- **A query on the package specifier** (`…/bundler-redirect?aui=<token>`): still
fails to resolve in Next 16.2.7 (`Can't resolve '@pkg/bundler-redirect'`), same
as the old `?path=` attempt. A query only survives on a *resolved file* path,
which is why the per-module token rides the wildcard-export **target** query
(`…client.js?aui=*`) rather than the request.
- **Per-module physical indirection files** (the other route to distinct resolved
paths): the set of generative modules isn't known when the package is built, so
pre-shipping a file per module is impossible — the wildcard-target query gives
distinct keys off one real file instead.
- **package.json `imports` field**: works (resolve-time `react-server`), but
requires a per-app entry; the facade removes that.
## Bundler support
Turbopack only — the facade uses Turbopack import attributes (`turbopackLoader`).
Under webpack, import the concrete builds explicitly (`?generative-env=server|client`).