@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.
- package/driver.js +537 -48
- package/index.js +327 -24
- package/internal/barrel-imports.js +459 -0
- package/internal/flight.js +736 -0
- package/internal/frontmatter.js +33 -0
- package/internal/http.js +21 -2
- package/internal/module-graph.js +134 -0
- package/internal/routes.js +55 -2
- package/internal/serve.js +206 -14
- package/internal/server-components.js +109 -0
- package/internal/worker-builtins.js +170 -0
- package/package.json +9 -7
|
@@ -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, "&").replace(/"/g, """)}">`,
|
|
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
|
+
}
|