@uniflowed/vite 0.0.0-alpha.8 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,803 @@
1
+ // @noflow
2
+ //
3
+ // Plain JavaScript: executed by the host that runs Vite, before any transform.
4
+ //
5
+ // React Server Components, as the bundler applies them (ubugeeei-prod/uf#519,
6
+ // ubugeeei-prod/uf#252).
7
+ //
8
+ // # A second module graph
9
+ //
10
+ // React's Flight renderer only runs where `react` is the build with no
11
+ // `useState` in it — the one a package exports under the `react-server`
12
+ // condition — and the HTML renderer and the browser only run where it is not.
13
+ // No single module graph can hold both, so a uf application is three:
14
+ //
15
+ // * **`rsc`**, a Vite environment of its own, resolved under `react-server`.
16
+ // It holds the route table, every page, layout and loader, and the Flight
17
+ // renderer (`@uniflowed/router/rsc`). A module that opens with the use
18
+ // client directive is not evaluated here: it is replaced by a client
19
+ // reference per export, naming the chunk the browser loads it from.
20
+ // * **`ssr`**, Vite's own server environment. It holds the HTML renderer, the
21
+ // route handlers, the middleware, the action table — and the *server copy*
22
+ // of every client module, which is what renders a client component into
23
+ // HTML. It reaches the rsc graph through one module, the bridge.
24
+ // * **`client`**, the browser's. Its entry hydrates from the payload the
25
+ // document carries, and it holds no page, layout or loader — only the
26
+ // client modules, each an entry of its own, loaded when a payload names it.
27
+ //
28
+ // Vite is still the whole bundler: every graph is an environment, every
29
+ // resolution is Vite's, and uf adds a transform, four virtual modules and the
30
+ // order the builds run in. `docs/architecture.md` has the picture.
31
+ //
32
+ // # A build is three passes, in this order
33
+ //
34
+ // 1. **rsc** records every client module it replaced, and leaves the manifest
35
+ // those references read as an import, because no chunk URL exists yet.
36
+ // 2. **client** builds the entry and one entry per recorded client module, with
37
+ // their export names kept, so each chunk still has the export a reference
38
+ // asks for.
39
+ // 3. **ssr** bundles the rsc output in through the bridge, and resolves the
40
+ // manifest the rsc output left open to the client build's chunk URLs.
41
+ //
42
+ // # Stylesheets come from the rsc graph
43
+ //
44
+ // A layout's stylesheet is imported by the layout, and the layout is in the rsc
45
+ // graph now, so the client build never sees it. The rsc build emits its assets,
46
+ // the driver copies them beside the client's, and the document links every
47
+ // stylesheet the rsc build emitted — the rule `assetsFromManifest` already
48
+ // applies to the client build, which links a route's stylesheet on every page.
49
+ // In development the rsc environment's module graph is read instead, once the
50
+ // route's modules have been imported.
51
+
52
+ import { clientInstrumentationSource } from "./instrumentation.js";
53
+
54
+ import path from "node:path";
55
+ import { relayDependencies } from "./relay.js";
56
+
57
+ import {
58
+ createRunnableDevEnvironment,
59
+ defaultServerConditions,
60
+ isCSSRequest,
61
+ parseAst,
62
+ } from "vite";
63
+
64
+ /** The environment the Flight renderer runs in. */
65
+ export const RSC_ENVIRONMENT = "rsc";
66
+
67
+ /** The virtual modules this file generates; `./routes.js`'s `VIRTUAL` has the rest. */
68
+ export const FLIGHT_VIRTUAL = Object.freeze({
69
+ /** The rsc graph's entry: the Flight renderer over the whole route table. */
70
+ entry: "virtual:uf/rsc",
71
+ /** The ssr graph's one door into the rsc graph. */
72
+ bridge: "virtual:uf/rsc-bridge",
73
+ /** The chunk URL of every client module, for the references a build writes. */
74
+ manifest: "virtual:uf/client-manifest",
75
+ /** The server copy of every client module, keyed by that URL. */
76
+ references: "virtual:uf/client-references",
77
+ /** `react/compiler-runtime`, as the rsc graph gets it; see `compilerRuntimeSource`. */
78
+ compilerRuntime: "virtual:uf/rsc-compiler-runtime",
79
+ });
80
+
81
+ /**
82
+ * `react/compiler-runtime` in the rsc graph.
83
+ *
84
+ * Every Flow module goes through the React Compiler, and what it emits calls
85
+ * `c(size)` from `react/compiler-runtime` for a component's memo cache. React
86
+ * 19.3's runtime reads that cache through `react`'s *client* internals, and the
87
+ * `react` a graph resolved under `react-server` has only server internals — so
88
+ * every compiled server component threw `Cannot read properties of undefined
89
+ * (reading 'H')` before it rendered a byte.
90
+ *
91
+ * A server component renders once per request and never again, so there is
92
+ * nothing for a cache to remember: this is the cache React's Flight renderer
93
+ * itself hands `useMemoCache`, every slot the sentinel the compiled code tests
94
+ * for, fresh on each call.
95
+ */
96
+ export function compilerRuntimeSource() {
97
+ return `const sentinel = Symbol.for("react.memo_cache_sentinel");
98
+ export function c(size) {
99
+ const cache = new Array(size);
100
+ for (let index = 0; index < size; index += 1) cache[index] = sentinel;
101
+ return cache;
102
+ }
103
+ `;
104
+ }
105
+
106
+ /**
107
+ * The global a development server leaves for the ssr graph to reach the rsc
108
+ * graph by.
109
+ *
110
+ * A function that imports the rsc entry through the rsc environment's module
111
+ * runner each time it is called, so an edit to a server component is in the
112
+ * next render the way an edit to anything else is.
113
+ */
114
+ export const DEV_RSC_HOOK = "uf.dev.rsc";
115
+
116
+ /**
117
+ * The last segment of a route's payload URL.
118
+ *
119
+ * A third spelling, beside `packages/router/internal/flight.js` and
120
+ * `packages/server/internal/flight.js`, because this file is plain JavaScript
121
+ * that Vite imports before any Flow transform exists. `packages/server/flight.test.js`
122
+ * holds all three to one answer.
123
+ */
124
+ export const FLIGHT_SEGMENT = "__uf.flight";
125
+
126
+ /** The header a browser sends when a Flight payload should render an interception. */
127
+ export const INTERCEPTED_FROM_HEADER = "uf-intercepted-from";
128
+
129
+ /** The document a payload path is for, or `null` for any other path. */
130
+ export function flightDocumentPath(pathname) {
131
+ const suffix = `/${FLIGHT_SEGMENT}`;
132
+ if (!pathname.endsWith(suffix)) return null;
133
+ const document = pathname.slice(0, -suffix.length);
134
+ return document === "" ? "/" : document;
135
+ }
136
+
137
+ /**
138
+ * The directive, spelled without quotes.
139
+ *
140
+ * `uf lint`'s `server/no-server-only-import-in-client` decides a file is a
141
+ * client module by finding the directive in quotes on any line of code, and
142
+ * this file, which imports `node:path`, is not one.
143
+ */
144
+ const USE_CLIENT = `use client`;
145
+
146
+ /**
147
+ * Whether an application renders through React Server Components.
148
+ *
149
+ * On unless `app.rsc` is `false`, read as `!== false` for the reason every
150
+ * default-on flag in `../index.js` is. A single-page build (`modes: ["csr"]`)
151
+ * renders nothing on a server, so it has no payload to render, and a native
152
+ * target has no document to write one into.
153
+ */
154
+ export function rendersFlight(app, { mount, routeTarget }) {
155
+ return app?.rsc !== false && mount === "hydrate" && routeTarget === "web";
156
+ }
157
+
158
+ /**
159
+ * The rsc environment, as `config()` declares it.
160
+ *
161
+ * `noExternal: true` because every module has to be resolved under
162
+ * `react-server`: a dependency left to Node would be resolved by Node, under
163
+ * the default conditions, and would import the `react` with `useState` in it.
164
+ * `optimizeDeps` names React and the Flight server because both are CommonJS
165
+ * and the module runner runs ES modules; uf's own packages are excluded for the
166
+ * reason the client excludes them — they ship Flow.
167
+ *
168
+ * `process.env.NODE_ENV` is fixed in a build. A server bundle reads it at run
169
+ * time otherwise, and a server started without it runs React's development
170
+ * build, whose payload carries every server component's source location and
171
+ * every error's stack — to the browser.
172
+ *
173
+ * @param {{ production: boolean, exclude: Array<string> }} options
174
+ */
175
+ export function rscEnvironment({ production, exclude, root = process.cwd() }) {
176
+ return {
177
+ consumer: "server",
178
+ resolve: {
179
+ conditions: ["react-server", ...defaultServerConditions],
180
+ externalConditions: ["react-server", ...defaultServerConditions],
181
+ noExternal: true,
182
+ },
183
+ optimizeDeps: {
184
+ include: [
185
+ "react",
186
+ "react/jsx-runtime",
187
+ "react/jsx-dev-runtime",
188
+ "react-server-dom-parcel/server",
189
+ ...relayDependencies(root, true),
190
+ ],
191
+ exclude,
192
+ },
193
+ define: production ? { "process.env.NODE_ENV": JSON.stringify("production") } : {},
194
+ dev: {
195
+ createEnvironment(name, config) {
196
+ return createRunnableDevEnvironment(name, config);
197
+ },
198
+ },
199
+ build: {
200
+ // Its stylesheets and images, which only this graph imports; see the
201
+ // header. The manifest is how the driver finds the stylesheets.
202
+ emitAssets: true,
203
+ manifest: true,
204
+ },
205
+ };
206
+ }
207
+
208
+ /**
209
+ * What the browser's graph pre-bundles for an application React Server
210
+ * Components render: React's Flight client, spelled the way the router's
211
+ * `internal/flight-browser.js` imports it, because the optimizer finds a
212
+ * pre-bundled dependency by the specifier that imports it.
213
+ *
214
+ * Named rather than left to be discovered, because in a project that installs
215
+ * the router nothing discovers it (ubugeeei-prod/uf#1126). The package is
216
+ * CommonJS. `uf:flow` excludes every `@uniflowed/*` package from the optimizer,
217
+ * since they ship Flow, and Vite pre-bundles a dependency it first meets while
218
+ * serving only when the module importing it is outside `node_modules`. An
219
+ * installed router is inside it, so the browser was sent the CommonJS file and
220
+ * hydration stopped at "does not provide an export named 'createFromFetch'". A
221
+ * linked router, the layout of this repository, is outside it, which is why
222
+ * nothing here failed.
223
+ *
224
+ * It resolves from the project, which installs `react-server-dom-parcel` as the
225
+ * router's peer.
226
+ */
227
+ export const FLIGHT_BROWSER_DEPENDENCIES = Object.freeze([
228
+ "react-server-dom-parcel/client.browser",
229
+ ]);
230
+
231
+ /**
232
+ * The state the plugins and the driver share for one application.
233
+ *
234
+ * `clientModules` is filled by the rsc graph's transform and read by the client
235
+ * build, which makes each of them an entry. `chunkUrls` is filled by the driver
236
+ * from the client build's manifest, and is what the ssr build resolves every
237
+ * reference to. `rscOutput` is the rsc build's entry file, which the ssr build
238
+ * bundles in; `null` under `uf dev`, where the rsc graph runs in-process.
239
+ *
240
+ * @param {{ root: string }} options
241
+ */
242
+ export function createFlightState({ root }) {
243
+ return {
244
+ root,
245
+ base: "/",
246
+ production: false,
247
+ clientModules: new Set(),
248
+ chunkUrls: new Map(),
249
+ rscOutput: null,
250
+ // The build's public id, set by the driver before it builds the rsc graph.
251
+ deployment: null,
252
+ };
253
+ }
254
+
255
+ /**
256
+ * The plugin that replaces a client module with references, in the rsc graph.
257
+ *
258
+ * After `uf:flow`, which is a `pre` plugin, so a Flow module is JavaScript by
259
+ * the time its exports are read — and after Vite's own transforms, so a `.jsx`
260
+ * module is too. The module is parsed with Vite's parser rather than scanned:
261
+ * the directive only counts as the first statement of the module, and an
262
+ * export list is not something a regular expression reads correctly.
263
+ *
264
+ * In development a reference names the URL Vite serves the module at, which is
265
+ * the URL the browser already imports it by from any other client module, so
266
+ * both reach one instance. In a build it names the client manifest, which the
267
+ * ssr build resolves once the client build has written the chunks.
268
+ *
269
+ * @param {ReturnType<typeof createFlightState>} state
270
+ */
271
+ export function clientReferencePlugin(state) {
272
+ return {
273
+ name: "uf:rsc-references",
274
+ applyToEnvironment(environment) {
275
+ return environment.name === RSC_ENVIRONMENT;
276
+ },
277
+ transform(code, id) {
278
+ const file = cleanId(id);
279
+ let program = null;
280
+ if (
281
+ code.includes(USE_CLIENT) &&
282
+ !id.startsWith("\0") &&
283
+ !isCSSRequest(file) &&
284
+ SCRIPT.test(file)
285
+ ) {
286
+ try {
287
+ program = parseAst(code);
288
+ } catch {
289
+ program = null;
290
+ }
291
+ }
292
+ if (program == null || !opensWithUseClient(program)) {
293
+ // Forgotten as well as not recorded: a module whose directive was
294
+ // removed under `uf dev` is a server module from that edit on, so
295
+ // `hotUpdate` reloads the page for its next edit instead of leaving it
296
+ // to Fast Refresh, which has nothing of it in the browser to replace.
297
+ state.clientModules.delete(file);
298
+ return null;
299
+ }
300
+ state.clientModules.add(file);
301
+ const names = clientExportNames(program, projectPath(state.root, file));
302
+ const lines = [`import { createClientReference } from "react-server-dom-parcel/server";`];
303
+ if (state.production) {
304
+ lines.push(`import { clientUrl } from ${JSON.stringify(FLIGHT_VIRTUAL.manifest)};`);
305
+ lines.push(`const url = clientUrl(${JSON.stringify(file)});`);
306
+ } else {
307
+ lines.push(`const url = ${JSON.stringify(devUrlOf(state.root, state.base, file))};`);
308
+ }
309
+ names.forEach((name, index) => {
310
+ lines.push(
311
+ `const reference${index} = createClientReference(url, ${JSON.stringify(name)}, [url]);`,
312
+ );
313
+ lines.push(`export { reference${index} as ${JSON.stringify(name)} };`);
314
+ });
315
+ return { code: `${lines.join("\n")}\n`, map: null };
316
+ },
317
+ };
318
+ }
319
+
320
+ /**
321
+ * The plugin that gives a file of a `@uniflowed/*` package one URL in the
322
+ * browser under `uf dev`: its path, with no `?v=`.
323
+ *
324
+ * A client reference names the URL `devUrlOf` gives its file, and the browser
325
+ * imports that URL when a payload names it. Vite gave the same file a second
326
+ * URL when a client module imported it: a file in `node_modules` of a package
327
+ * the dependency optimizer excludes — every `@uniflowed/*` package, because
328
+ * they ship Flow — carries `?v=` and the optimizer's hash. A browser keys a
329
+ * module by its URL, so those were two modules. `Dialog.Root` rendered by a
330
+ * server component and `Dialog.Trigger` rendered by a client component held
331
+ * two `DialogContext`s, and hydration threw "Dialog.Trigger must be rendered
332
+ * inside a Dialog.Root". Only in a project that installed its packages, since a
333
+ * linked package is not in `node_modules` and gets no query: every project but
334
+ * this repository.
335
+ *
336
+ * The query only lets the browser cache the file without asking, so dropping it
337
+ * costs a revalidation. `pre`, so it can ask Vite's resolver first and drop
338
+ * what that added; only in the browser's graph, because the rsc graph records a
339
+ * client module by its path already and the ssr graph has no optimizer.
340
+ */
341
+ export function clientModuleUrlPlugin() {
342
+ return {
343
+ name: "uf:rsc-client-urls",
344
+ apply: "serve",
345
+ enforce: "pre",
346
+ applyToEnvironment(environment) {
347
+ return environment.name === "client";
348
+ },
349
+ async resolveId(id, importer, options) {
350
+ if (!reachesUniflowedPackage(id, importer)) return null;
351
+ const resolved = await this.resolve(id, importer, { ...options, skipSelf: true });
352
+ if (resolved == null) return null;
353
+ const unversioned = withoutVersion(resolved.id);
354
+ return unversioned === resolved.id ? resolved : { ...resolved, id: unversioned };
355
+ },
356
+ };
357
+ }
358
+
359
+ /** Where every `@uniflowed/*` package is, installed, whatever manages `node_modules`. */
360
+ const UNIFLOWED_FILES = "/node_modules/@uniflowed/";
361
+
362
+ /**
363
+ * Whether an import can resolve to a file of an installed `@uniflowed/*`
364
+ * package: a bare import of one, a path or URL into one, or a relative import
365
+ * from inside one. Everything else is left to Vite without a second resolution.
366
+ */
367
+ function reachesUniflowedPackage(id, importer) {
368
+ if (id.startsWith("\0")) return false;
369
+ if (id.startsWith("@uniflowed/") || id.includes(UNIFLOWED_FILES)) return true;
370
+ return /^\.\.?\//.test(id) && typeof importer === "string" && importer.includes(UNIFLOWED_FILES);
371
+ }
372
+
373
+ /** `id` without the optimizer's `v=`, for a file of an installed `@uniflowed/*` package. */
374
+ function withoutVersion(id) {
375
+ const at = id.indexOf("?");
376
+ if (at === -1 || !id.slice(0, at).includes(UNIFLOWED_FILES)) return id;
377
+ const kept = id
378
+ .slice(at + 1)
379
+ .split("&")
380
+ .filter((parameter) => !/^v=[\w.-]*$/.test(parameter));
381
+ return kept.length === 0 ? id.slice(0, at) : `${id.slice(0, at)}?${kept.join("&")}`;
382
+ }
383
+
384
+ /** The module kinds a reference can stand in for. */
385
+ const SCRIPT = /\.(?:[cm]?js|jsx|mdx)$/;
386
+
387
+ /**
388
+ * Whether a module's directive prologue holds the use client directive.
389
+ *
390
+ * The prologue rather than the first statement, because `"use strict"` may
391
+ * come before it and is still a directive.
392
+ */
393
+ export function opensWithUseClient(program) {
394
+ for (const statement of program.body) {
395
+ if (statement.type !== "ExpressionStatement" || typeof statement.directive !== "string") {
396
+ return false;
397
+ }
398
+ if (statement.directive === USE_CLIENT) return true;
399
+ }
400
+ return false;
401
+ }
402
+
403
+ /**
404
+ * Every name a client module exports, which is every reference it becomes.
405
+ *
406
+ * `export *` is refused rather than followed: a reference is one per export,
407
+ * and the names behind a star are another module's, which this transform would
408
+ * have to resolve and parse before it could write this one.
409
+ *
410
+ * @param {object} program
411
+ * @param {string} file the module, as the error names it
412
+ */
413
+ export function clientExportNames(program, file) {
414
+ const names = [];
415
+ for (const node of program.body) {
416
+ if (node.type === "ExportDefaultDeclaration") {
417
+ names.push("default");
418
+ } else if (node.type === "ExportNamedDeclaration") {
419
+ const declaration = node.declaration;
420
+ if (declaration != null) {
421
+ if (declaration.type === "VariableDeclaration") {
422
+ for (const declarator of declaration.declarations) bindingNames(declarator.id, names);
423
+ } else if (declaration.id != null) {
424
+ names.push(declaration.id.name);
425
+ }
426
+ }
427
+ for (const specifier of node.specifiers ?? []) names.push(exportedName(specifier.exported));
428
+ } else if (node.type === "ExportAllDeclaration") {
429
+ if (node.exported != null) {
430
+ names.push(exportedName(node.exported));
431
+ continue;
432
+ }
433
+ throw new Error(
434
+ `uf: ${file} is a client module and re-exports everything from ` +
435
+ `${JSON.stringify(node.source.value)}. A client module becomes one reference per ` +
436
+ "export, so each export has to be named: write `export { A, B } from " +
437
+ `${JSON.stringify(node.source.value)}\` instead.`,
438
+ );
439
+ }
440
+ }
441
+ return [...new Set(names)];
442
+ }
443
+
444
+ function exportedName(node) {
445
+ return node.type === "Identifier" ? node.name : String(node.value);
446
+ }
447
+
448
+ function bindingNames(pattern, names) {
449
+ if (pattern == null) return;
450
+ switch (pattern.type) {
451
+ case "Identifier":
452
+ names.push(pattern.name);
453
+ break;
454
+ case "ObjectPattern":
455
+ for (const property of pattern.properties) {
456
+ bindingNames(property.type === "RestElement" ? property.argument : property.value, names);
457
+ }
458
+ break;
459
+ case "ArrayPattern":
460
+ for (const element of pattern.elements) bindingNames(element, names);
461
+ break;
462
+ case "RestElement":
463
+ bindingNames(pattern.argument, names);
464
+ break;
465
+ case "AssignmentPattern":
466
+ bindingNames(pattern.left, names);
467
+ break;
468
+ default:
469
+ break;
470
+ }
471
+ }
472
+
473
+ /**
474
+ * The URL Vite serves `file` at in development.
475
+ *
476
+ * Root-relative for a file under the project, and `/@fs/` for one outside it —
477
+ * a workspace package, which Vite resolves to its real path — which is the URL
478
+ * any client module importing it is rewritten to as well.
479
+ */
480
+ export function devUrlOf(root, base, file) {
481
+ const relative = path.relative(root, file);
482
+ const inside = relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
483
+ const forward = file.split(path.sep).join("/");
484
+ // `/@fs/` and then the path. On Windows the path starts at its drive letter,
485
+ // `C:/work/button.js`, with no slash of its own to follow the prefix.
486
+ const pathname = inside
487
+ ? `/${relative.split(path.sep).join("/")}`
488
+ : `/@fs${forward.startsWith("/") ? "" : "/"}${forward}`;
489
+ return `${base.replace(/\/$/, "")}${pathname}`;
490
+ }
491
+
492
+ /**
493
+ * The file a `/@fs/` URL's path names: the inverse of [`devUrlOf`] for a file
494
+ * outside the project, read the way Vite reads that prefix. A POSIX path gets
495
+ * its leading slash back; a Windows path starts at its drive letter.
496
+ *
497
+ * Also the body of the loader [`devReferencesSource`] generates, which is why
498
+ * it closes over nothing.
499
+ */
500
+ export function fsFileOf(pathname) {
501
+ const rest = pathname.slice("/@fs/".length);
502
+ return /^[A-Za-z]:\//.test(rest) ? rest : `/${rest}`;
503
+ }
504
+
505
+ /**
506
+ * `virtual:uf/rsc`: the Flight renderer over the rsc graph's route table.
507
+ *
508
+ * `deployment` is the build's public id, baked in so every payload says which
509
+ * build rendered it — a prerendered payload is a file, and nothing that serves
510
+ * a file can say so for it. `null` under `uf dev`.
511
+ */
512
+ export function rscEntrySource(routesId, routing = {}, deployment = null) {
513
+ const settings = {
514
+ basePath: routing.basePath ?? "",
515
+ trailingSlash: routing.trailingSlash ?? "ignore",
516
+ };
517
+ return `import { createFlightRenderer, installRouting } from "@uniflowed/router/rsc";
518
+ import { routes, notFound, errors } from ${JSON.stringify(routesId)};
519
+ installRouting(${JSON.stringify(settings)});
520
+ export { routes, notFound, errors };
521
+ export const renderFlight = createFlightRenderer({ routes, notFound, errors, deployment: ${JSON.stringify(
522
+ deployment ?? null,
523
+ )} });
524
+ `;
525
+ }
526
+
527
+ /**
528
+ * `virtual:uf/rsc-bridge` under `uf dev`: the rsc graph, through its runner.
529
+ *
530
+ * `renderFlight` imports the rsc entry on every call rather than once, which
531
+ * costs a map lookup when nothing changed and is what puts an edited server
532
+ * component in the next render. The route table is read once, because nothing
533
+ * under `uf dev` reads it — the driver's build is its reader.
534
+ */
535
+ export function devBridgeSource() {
536
+ return `const load = globalThis[Symbol.for(${JSON.stringify(DEV_RSC_HOOK)})];
537
+ if (typeof load !== "function") {
538
+ throw new Error(
539
+ "uf: the rsc environment is not running, so there is nothing to render a route with. " +
540
+ "This module is served by uf dev, which starts that environment first.",
541
+ );
542
+ }
543
+ export async function renderFlight(url, options) {
544
+ return (await load()).renderFlight(url, options);
545
+ }
546
+ export const { routes, notFound, errors } = await load();
547
+ `;
548
+ }
549
+
550
+ /** `virtual:uf/rsc-bridge` in a build: the rsc build's output, bundled in. */
551
+ export function builtBridgeSource(rscOutput) {
552
+ return `export { renderFlight, routes, notFound, errors } from ${JSON.stringify(rscOutput)};\n`;
553
+ }
554
+
555
+ /**
556
+ * `virtual:uf/client-manifest` in the ssr build: every client module's chunk.
557
+ *
558
+ * Keyed by absolute path, which is what the rsc build's references were written
559
+ * with. The paths stay in the server bundle; what reaches a payload, and so a
560
+ * browser, is the URL.
561
+ *
562
+ * @param {Map<string, string>} chunkUrls
563
+ */
564
+ export function clientManifestSource(chunkUrls) {
565
+ return `const urls = new Map(${JSON.stringify([...chunkUrls])});
566
+ export function clientUrl(file) {
567
+ const url = urls.get(file);
568
+ if (url == null) {
569
+ throw new Error(
570
+ "uf: the client build wrote no chunk for " + file + ", which a server component " +
571
+ "renders as a client component. Its passes disagree; run uf build again.",
572
+ );
573
+ }
574
+ return url;
575
+ }
576
+ `;
577
+ }
578
+
579
+ /** `virtual:uf/client-references` under `uf dev`: the module at a dev URL. */
580
+ export function devReferencesSource(root, base) {
581
+ return `const root = ${JSON.stringify(root)};
582
+ const base = ${JSON.stringify(base)};
583
+ const fsFileOf = ${fsFileOf.toString()};
584
+ export function loadClientModule(url) {
585
+ const pathname = url.startsWith(base) ? url.slice(base.length - 1) : url;
586
+ const file = pathname.startsWith("/@fs/") ? fsFileOf(pathname) : root + decodeURI(pathname);
587
+ return import(/* @vite-ignore */ file);
588
+ }
589
+ `;
590
+ }
591
+
592
+ /**
593
+ * `virtual:uf/client-references` in a build: the server copy at a chunk URL.
594
+ *
595
+ * One `import()` per client module, so a server bundle loads a client
596
+ * component's server copy the first time a payload names it and not before.
597
+ *
598
+ * @param {Map<string, string>} chunkUrls
599
+ */
600
+ export function builtReferencesSource(chunkUrls) {
601
+ const imports = [];
602
+ const entries = [];
603
+ let staticIndex = 0;
604
+ for (const [file, url] of chunkUrls) {
605
+ if (serverBundleAlreadyImportsClientReference(file)) {
606
+ const name = `staticReference${staticIndex}`;
607
+ staticIndex += 1;
608
+ imports.push(`import * as ${name} from ${JSON.stringify(file)};`);
609
+ entries.push(` [${JSON.stringify(url)}, () => Promise.resolve(${name})],`);
610
+ } else {
611
+ entries.push(` [${JSON.stringify(url)}, () => import(${JSON.stringify(file)})],`);
612
+ }
613
+ }
614
+ const prelude = imports.length === 0 ? "" : `${imports.join("\n")}\n`;
615
+ return `${prelude}const table = new Map([
616
+ ${entries.join("\n")}
617
+ ]);
618
+ export function loadClientModule(url) {
619
+ const load = table.get(url);
620
+ if (load == null) {
621
+ return Promise.reject(
622
+ new Error("uf: a payload named the client chunk " + url + ", and this server has no copy of it"),
623
+ );
624
+ }
625
+ return load();
626
+ }
627
+ `;
628
+ }
629
+
630
+ /**
631
+ * Router internals that are client modules and also already in the server
632
+ * bundle through the router's own synchronous imports.
633
+ *
634
+ * They still need entries in the client-reference table, because a Flight
635
+ * payload can name their chunks. Loading them with `import()`, however, makes
636
+ * Rollup print INEFFECTIVE_DYNAMIC_IMPORT: the dynamic import cannot split a
637
+ * module the server chunk has already imported. Return the namespace the
638
+ * server loaded anyway.
639
+ */
640
+ function serverBundleAlreadyImportsClientReference(file) {
641
+ const normalized = file.split(path.sep).join("/");
642
+ return ROUTER_CLIENT_REFERENCES_IN_SERVER.some((suffix) => normalized.endsWith(suffix));
643
+ }
644
+
645
+ const ROUTER_CLIENT_REFERENCES_IN_SERVER = Object.freeze([
646
+ "/@uniflowed/router/internal/boundaries.js",
647
+ "/@uniflowed/router/internal/error-view.js",
648
+ "/packages/router/internal/boundaries.js",
649
+ "/packages/router/internal/error-view.js",
650
+ ]);
651
+
652
+ /**
653
+ * `virtual:uf/client` for an application React Server Components render.
654
+ *
655
+ * No route table: the browser resolves no route and imports no page. What it
656
+ * has is the application root and the payload the document carries, which
657
+ * `hydrateFlight` reads. That function comes from `@uniflowed/router/rsc/client`,
658
+ * not from `@uniflowed/router/client`, the entry an application rendered from
659
+ * its modules starts from. So only this kind of application has React's Flight
660
+ * client in its bundle: `react-server-dom-parcel` is an optional peer of the
661
+ * router, and a project on React 19.2 does not install it (ubugeeei-prod/uf#992).
662
+ * Strict Mode and navigation are generated constants for the reasons
663
+ * `clientModuleSource` in `./routes.js` gives.
664
+ */
665
+ export function flightClientSource(appEntry, options = {}) {
666
+ const strictMode = options.strictMode === true ? ", strictMode: true" : "";
667
+ const navigation = options.navigation === "document" ? ', navigation: "document"' : "";
668
+ // `routes.js`'s `routingArgumentSource`, spelled here too: that module
669
+ // imports this one, and a default project's entry has to stay the module it
670
+ // was, so nothing is written for the root and the default policy.
671
+ const basePath = options.routing?.basePath ?? "";
672
+ const trailingSlash = options.routing?.trailingSlash ?? "ignore";
673
+ const routing =
674
+ (basePath === "" ? "" : `, basePath: ${JSON.stringify(basePath)}`) +
675
+ (trailingSlash === "ignore" ? "" : `, trailingSlash: ${JSON.stringify(trailingSlash)}`);
676
+ // `app.rendering.staleTime`, in seconds, and nothing for the default `0`.
677
+ const staleTime =
678
+ options.staleTime > 0 ? `, staleTime: ${JSON.stringify(options.staleTime)}` : "";
679
+ return `import { hydrateFlight } from "@uniflowed/router/rsc/client";
680
+ import App from ${JSON.stringify(appEntry)};
681
+ ${clientInstrumentationSource(options.instrumentation)}hydrateFlight({ App${strictMode}${navigation}${staleTime}${routing} });
682
+ `;
683
+ }
684
+
685
+ /**
686
+ * `virtual:uf/server` for an application React Server Components render.
687
+ *
688
+ * The exports and their order are `serverModuleSource`'s in `./routes.js`, and
689
+ * that comment is the argument for them. Two things differ. The renderer is
690
+ * `createDocumentRenderer` from `@uniflowed/router/rsc/ssr`, an entry of its own
691
+ * for the reason `flightClientSource` gives. It renders the payload the rsc graph
692
+ * writes rather than the route's modules, and adds `flight` for a browser that
693
+ * is navigating. And `routes`, `notFound` and `errors` come through the bridge,
694
+ * because the page modules they import are the rsc graph's: the driver reads a
695
+ * page's `generateStaticParams` from the graph that renders it.
696
+ */
697
+ export function flightServerSource(
698
+ appEntry,
699
+ routesId,
700
+ actionsId,
701
+ routing = { redirects: [], rewrites: [], headers: [], basePath: "", trailingSlash: "ignore" },
702
+ instrumentation = null,
703
+ ) {
704
+ return `import {
705
+ createActionDispatcher,
706
+ createInstrumentation,
707
+ instrumentRender,
708
+ traceRequestPhase,
709
+ createDispatcher,
710
+ createMiddlewareRunner,
711
+ installRouting,
712
+ } from "@uniflowed/router/server";
713
+ import { createDocumentRenderer } from "@uniflowed/router/rsc/ssr";
714
+ import { handlers, middleware } from ${JSON.stringify(routesId)};
715
+ import { actions } from ${JSON.stringify(actionsId)};
716
+ import { renderFlight, routes, notFound, errors } from ${JSON.stringify(FLIGHT_VIRTUAL.bridge)};
717
+ import { loadClientModule } from ${JSON.stringify(FLIGHT_VIRTUAL.references)};
718
+ import App from ${JSON.stringify(appEntry)};
719
+ export const routing = ${JSON.stringify(routing)};
720
+ installRouting(routing);
721
+ export { routes, handlers, middleware, notFound, errors };
722
+ ${instrumentation == null ? "" : `import * as hooks from ${JSON.stringify(instrumentation)};`}
723
+ export const { beginRequest } = createInstrumentation(${instrumentation == null ? "" : "hooks"});
724
+ const renderer = createDocumentRenderer({ App, renderFlight, loadClientModule });
725
+ export const render = (url, assets, options = {}) => instrumentRender(
726
+ (onError) => renderer.render(url, assets, { ...options, onError }), options.onError,
727
+ );
728
+ export const prerender = renderer.prerender;
729
+ export const resume = (url, assets, shell, options = {}) => instrumentRender(
730
+ (onError) => renderer.resume(url, assets, shell, { ...options, onError }), options.onError,
731
+ );
732
+ export const flight = (url, options = {}) => instrumentRender(
733
+ (onError) => renderer.flight(url, { ...options, onError }), options.onError,
734
+ );
735
+ export { shellDocument } from "@uniflowed/router/server";
736
+ const dispatchRoute = createDispatcher({ handlers });
737
+ export const dispatch = (request) => traceRequestPhase("route", () => dispatchRoute(request));
738
+ export const callAction = createActionDispatcher({ actions });
739
+ const guard = createMiddlewareRunner({ middleware });
740
+ export const runMiddleware = (request) => traceRequestPhase("middleware", () => guard(request));
741
+ `;
742
+ }
743
+
744
+ /**
745
+ * The stylesheets the rsc graph has imported so far, as development URLs.
746
+ *
747
+ * Read after the route's modules have been imported — `createDocumentRenderer`
748
+ * reads a document's assets once the payload's route has resolved — so a
749
+ * layout's stylesheet is in the graph by the time its document's head is
750
+ * written. Every stylesheet the graph holds, in the order it met them, which is
751
+ * the development version of the build's rule and cascades the same way.
752
+ */
753
+ export function devStylesheets(server) {
754
+ const environment = server.environments?.[RSC_ENVIRONMENT];
755
+ if (environment == null) return [];
756
+ const { root, base } = server.config;
757
+ const urls = [];
758
+ for (const [id, module] of environment.moduleGraph.idToModuleMap) {
759
+ if (id.includes("?") || !isCSSRequest(cleanId(id))) continue;
760
+ // From the file rather than the graph's own `url`, which is not the URL the
761
+ // browser can fetch for a stylesheet outside the project — a workspace
762
+ // package's, which Vite serves under `/@fs/` — and the same rule a client
763
+ // reference's URL follows. A stylesheet with no file is a virtual one.
764
+ urls.push(
765
+ typeof module.file === "string" && path.isAbsolute(module.file)
766
+ ? devUrlOf(root, base, module.file)
767
+ : `${base.replace(/\/$/, "")}/@id/${id.replace(/\0/g, "__x00__")}`,
768
+ );
769
+ }
770
+ return urls;
771
+ }
772
+
773
+ /**
774
+ * `head` with a stylesheet link for each of `hrefs`, before `</head>` when the
775
+ * head is closed and at its end when it is not.
776
+ *
777
+ * The document opening a development server transforms may stop inside the
778
+ * head, which is why "at its end" is an answer rather than an error.
779
+ */
780
+ export function linkStylesheets(head, hrefs) {
781
+ if (hrefs.length === 0) return head;
782
+ const links = hrefs
783
+ .map(
784
+ (href) =>
785
+ `<link rel="stylesheet" href="${href.replace(/&/g, "&amp;").replace(/"/g, "&quot;")}">`,
786
+ )
787
+ .join("");
788
+ const close = head.search(/<\/head>/i);
789
+ return close === -1 ? `${head}${links}` : `${head.slice(0, close)}${links}${head.slice(close)}`;
790
+ }
791
+
792
+ /** A module's path as an error names it: project-relative when it can be. */
793
+ function projectPath(root, file) {
794
+ const relative = path.relative(root, file);
795
+ return relative.startsWith("..") || path.isAbsolute(relative)
796
+ ? file
797
+ : relative.split(path.sep).join("/");
798
+ }
799
+
800
+ function cleanId(id) {
801
+ const at = id.indexOf("?");
802
+ return at === -1 ? id : id.slice(0, at);
803
+ }