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

7.0 KiB

"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):

// 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):

// 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

"./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).