@uniflowed/vite 0.0.0-alpha.34 → 0.0.0-alpha.37

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