@uniflowed/vite 0.0.0-alpha.4 → 0.0.0-alpha.40
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 +2265 -188
- package/index.js +1124 -52
- package/internal/a11y-runtime.js +234 -0
- package/internal/a11y.js +102 -0
- package/internal/assets.js +644 -0
- package/internal/barrel-imports.js +459 -0
- package/internal/compile-assets.js +67 -0
- package/internal/config.js +39 -6
- package/internal/devtools.js +117 -0
- package/internal/diagnostics.js +369 -0
- package/internal/events.js +20 -0
- package/internal/flight.js +772 -0
- package/internal/flow-keywords.js +1 -1
- package/internal/frontmatter.js +33 -0
- package/internal/http.js +98 -0
- package/internal/module-graph.js +134 -0
- package/internal/openapi.js +218 -0
- package/internal/routes.js +1581 -72
- package/internal/rsc.js +475 -0
- package/internal/serve.js +740 -0
- package/internal/server-components.js +109 -0
- package/internal/worker-builtins.js +170 -0
- package/package.json +26 -6
package/internal/rsc.js
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
// @noflow
|
|
2
|
+
//
|
|
3
|
+
// Plain JavaScript: executed by the host that runs Vite, before any transform.
|
|
4
|
+
//
|
|
5
|
+
// The server/client split, as the bundler applies it.
|
|
6
|
+
//
|
|
7
|
+
// `crates/uf_rsc` decides which modules a `"use client"` boundary is reachable
|
|
8
|
+
// from. That answer used to reach nothing: `virtual:uf/routes` emitted
|
|
9
|
+
// `page: () => import(<file>)` for every route, `virtual:uf/client` imported
|
|
10
|
+
// that table, and so every page in the application was a chunk of the *client*
|
|
11
|
+
// bundle whether or not a browser had anything to do with it.
|
|
12
|
+
//
|
|
13
|
+
// This module is the first thing that reads the answer. What is done with it
|
|
14
|
+
// is in `routesModuleSource`, including the one thing a dropped route keeps.
|
|
15
|
+
//
|
|
16
|
+
// # Why the unit is a route and not a module
|
|
17
|
+
//
|
|
18
|
+
// Dropping a single Server Component from the client bundle is what Next.js
|
|
19
|
+
// does, and it works there because the browser is handed a Flight payload
|
|
20
|
+
// describing the tree the server rendered. uf's payload
|
|
21
|
+
// (`packages/router/internal/payload.js`) carries the route's *data* and not
|
|
22
|
+
// its tree — that half needs a second React module graph, see
|
|
23
|
+
// ubugeeei-prod/uf#519 — so `packages/router/client.js` still hydrates by
|
|
24
|
+
// re-rendering the matched tree from the same modules the server rendered it
|
|
25
|
+
// from, and a module missing from the client bundle is a module React cannot
|
|
26
|
+
// hydrate. What *can* be dropped is a
|
|
27
|
+
// route the browser never renders at all — one where no client boundary is
|
|
28
|
+
// reachable from the page, its layouts, its loading fallbacks or the
|
|
29
|
+
// boundaries that cover it. Nothing under it is ever re-rendered in the
|
|
30
|
+
// browser, so nothing under it has to be shipped. See ubugeeei-prod/uf#350.
|
|
31
|
+
//
|
|
32
|
+
// # Why "unknown" means "ship it"
|
|
33
|
+
//
|
|
34
|
+
// The analysis scans `.js`. A page written as `.mdx`, a `.jsx` module, a file
|
|
35
|
+
// past the scanner's size limit: none of them is in the manifest, and the
|
|
36
|
+
// honest reading of a module the analysis never saw is that it might reach a
|
|
37
|
+
// boundary. Every unknown answers `true`, so the split can only ever remove a
|
|
38
|
+
// route uf has positively decided needs no browser — and a manifest that is
|
|
39
|
+
// missing, unreadable, or written by an older uf removes nothing at all.
|
|
40
|
+
|
|
41
|
+
import { readFileSync, statSync } from "node:fs";
|
|
42
|
+
import path from "node:path";
|
|
43
|
+
|
|
44
|
+
/** Environment variable naming the manifest, set by `uf build` and `uf dev`. */
|
|
45
|
+
export const RSC_MANIFEST_ENV = "UF_RSC_MANIFEST";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The manifest schema this understands.
|
|
49
|
+
*
|
|
50
|
+
* Version 3 lets client boundaries name package specifiers as well as project
|
|
51
|
+
* paths. An older manifest is therefore refused rather than read
|
|
52
|
+
* optimistically: it cannot know a route imports a package client module, and
|
|
53
|
+
* quietly dropping that route would be the worst possible answer.
|
|
54
|
+
*/
|
|
55
|
+
const SUPPORTED_VERSION = 3;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Read the RSC manifest, or `null` when there is nothing usable to read.
|
|
59
|
+
*
|
|
60
|
+
* Never throws. The split is an optimisation over a build that is already
|
|
61
|
+
* correct without it, so no failure here may be a failure of the build.
|
|
62
|
+
*
|
|
63
|
+
* @param {string | undefined} file absolute path, from the environment
|
|
64
|
+
*/
|
|
65
|
+
export function readRscManifest(file) {
|
|
66
|
+
if (file == null || file === "") return null;
|
|
67
|
+
let parsed;
|
|
68
|
+
try {
|
|
69
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
if (parsed == null || typeof parsed !== "object") return null;
|
|
74
|
+
if (parsed.version !== SUPPORTED_VERSION || !Array.isArray(parsed.modules)) return null;
|
|
75
|
+
return parsed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Which modules the browser has to be able to evaluate, keyed by project path.
|
|
80
|
+
*
|
|
81
|
+
* A `"use client"` module is a client bundle root by definition, and
|
|
82
|
+
* `proximity` never says so about it — it is the far side of the boundary
|
|
83
|
+
* rather than a module above one — so both halves are asked. This mirrors
|
|
84
|
+
* `RscModule::requires_client_bundle` in `crates/uf_rsc/src/graph.rs`.
|
|
85
|
+
*/
|
|
86
|
+
function clientModules(manifest) {
|
|
87
|
+
const modules = new Map();
|
|
88
|
+
for (const module of manifest.modules) {
|
|
89
|
+
if (module == null || typeof module.path !== "string") continue;
|
|
90
|
+
modules.set(
|
|
91
|
+
module.path,
|
|
92
|
+
module.environment === "client" || module.proximity === "reaches-boundary",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
for (const boundary of manifest.clientBoundaries ?? []) {
|
|
96
|
+
if (boundary == null || typeof boundary.importer !== "string") continue;
|
|
97
|
+
modules.set(boundary.importer, true);
|
|
98
|
+
}
|
|
99
|
+
for (const root of manifest.clientBundleRoots ?? []) {
|
|
100
|
+
if (root == null || root.kind !== "module" || typeof root.path !== "string") continue;
|
|
101
|
+
modules.set(root.path, true);
|
|
102
|
+
}
|
|
103
|
+
return modules;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Whether `boundary`'s route path covers `route`'s.
|
|
108
|
+
*
|
|
109
|
+
* Deliberately "covers" and not "is nearest to". `packages/router` picks the
|
|
110
|
+
* nearest not-found and error boundary above a path at render time; asking the
|
|
111
|
+
* same question here would be a second implementation of that rule, and the
|
|
112
|
+
* two would disagree the first time either moved. Every boundary that could
|
|
113
|
+
* apply is counted instead, which can only decide that more routes need the
|
|
114
|
+
* browser than strictly do.
|
|
115
|
+
*/
|
|
116
|
+
function covers(boundaryPath, routePath) {
|
|
117
|
+
return (
|
|
118
|
+
boundaryPath === "/" || routePath === boundaryPath || routePath.startsWith(`${boundaryPath}/`)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Build the predicate `routesModuleSource` asks about each route.
|
|
124
|
+
*
|
|
125
|
+
* Returns `(route) => boolean`: true when the route's page module belongs in
|
|
126
|
+
* the client bundle. With no manifest every route answers true, which is the
|
|
127
|
+
* whole table and exactly what the build emitted before this existed.
|
|
128
|
+
*
|
|
129
|
+
* @param {object | null} manifest from {@link readRscManifest}
|
|
130
|
+
* @param {string} root absolute project root
|
|
131
|
+
* @param {{notFound?: Array<object>, errors?: Array<object>}} [boundaries]
|
|
132
|
+
* the scanned table, so a boundary that needs the browser keeps the routes
|
|
133
|
+
* it covers in the client bundle
|
|
134
|
+
*/
|
|
135
|
+
export function clientRouteFilter(manifest, root, boundaries = {}) {
|
|
136
|
+
if (manifest == null) return () => true;
|
|
137
|
+
const modules = clientModules(manifest);
|
|
138
|
+
|
|
139
|
+
const needed = (file) => {
|
|
140
|
+
if (typeof file !== "string") return true;
|
|
141
|
+
const relative = path.relative(root, file).split(path.sep).join("/");
|
|
142
|
+
const answer = modules.get(relative);
|
|
143
|
+
return answer === undefined ? true : answer;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const notFound = boundaries.notFound ?? [];
|
|
147
|
+
const errors = boundaries.errors ?? [];
|
|
148
|
+
const needsSlot = slotPredicate(needed);
|
|
149
|
+
|
|
150
|
+
return (route) => {
|
|
151
|
+
if (needed(route.page)) return true;
|
|
152
|
+
if (route.layouts.some(needed)) return true;
|
|
153
|
+
if ((route.loading ?? []).some((entry) => needed(entry.module))) return true;
|
|
154
|
+
if ((route.templates ?? []).some((entry) => needed(entry.module))) return true;
|
|
155
|
+
// A slot renders inside this route, so a `"use client"` anywhere in one is
|
|
156
|
+
// this route's reason to ship. Without this line a page whose only
|
|
157
|
+
// interactive part is in a slot would be dropped from the client bundle
|
|
158
|
+
// and served as a document — rendered correctly and never hydrated, which
|
|
159
|
+
// is the quietest way a feature can be half-implemented.
|
|
160
|
+
if ((route.slots ?? []).some(needsSlot)) return true;
|
|
161
|
+
// A boundary with no module of its own is the record the scan synthesises
|
|
162
|
+
// at the router root, and what renders there is the framework's own page —
|
|
163
|
+
// already in `@uniflowed/router`, reaching nothing this project wrote. It
|
|
164
|
+
// is skipped rather than left to `needed`, whose answer for a value that is
|
|
165
|
+
// not a file is "assume it is needed": that answer is right for a path the
|
|
166
|
+
// manifest has never heard of and wrong for the absence of a path, and
|
|
167
|
+
// taking it here would have kept every page of every project in the client
|
|
168
|
+
// bundle. See ubugeeei-prod/uf#351.
|
|
169
|
+
for (const boundary of notFound) {
|
|
170
|
+
if (boundary.page == null) continue;
|
|
171
|
+
if (covers(boundary.path, route.path) && needed(boundary.page)) return true;
|
|
172
|
+
}
|
|
173
|
+
for (const boundary of errors) {
|
|
174
|
+
if (boundary.module == null) continue;
|
|
175
|
+
if (covers(boundary.path, route.path) && needed(boundary.module)) return true;
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Whether anything in a slot tree has to reach the browser.
|
|
183
|
+
*
|
|
184
|
+
* Recursive because slots nest: a slot's own layout may declare slots of its
|
|
185
|
+
* own, and a `"use client"` at any depth is still inside the page this route
|
|
186
|
+
* renders.
|
|
187
|
+
*
|
|
188
|
+
* @param {ReadonlyArray<{
|
|
189
|
+
* defaultPage: ?string,
|
|
190
|
+
* routes: ReadonlyArray<{
|
|
191
|
+
* page: string,
|
|
192
|
+
* layouts: ReadonlyArray<string>,
|
|
193
|
+
* loading?: ReadonlyArray<{module: string}>,
|
|
194
|
+
* templates?: ReadonlyArray<{module: string}>,
|
|
195
|
+
* slots: ReadonlyArray<*>,
|
|
196
|
+
* }>,
|
|
197
|
+
* @param {(file: ?string) => boolean} needed
|
|
198
|
+
* @returns {(slot: *) => boolean}
|
|
199
|
+
*/
|
|
200
|
+
function slotPredicate(needed) {
|
|
201
|
+
const cache = new WeakMap();
|
|
202
|
+
const needsSlot = (slot) => {
|
|
203
|
+
const cached = cache.get(slot);
|
|
204
|
+
if (cached !== undefined) return cached;
|
|
205
|
+
// A slot that intercepts anything needs the client router on every page it
|
|
206
|
+
// is rendered on, whatever its modules are. An intercepting route renders
|
|
207
|
+
// only for a client navigation that starts on such a page, and a page
|
|
208
|
+
// served as a document with no router hydrated on it is a page no
|
|
209
|
+
// navigation starts from — so dropping it would leave the interception a
|
|
210
|
+
// file nothing ever renders, which is the silence #267 is about.
|
|
211
|
+
let answer = (slot.intercepts ?? []).length > 0;
|
|
212
|
+
if (slot.defaultPage != null && needed(slot.defaultPage)) {
|
|
213
|
+
answer = true;
|
|
214
|
+
}
|
|
215
|
+
for (const route of slot.routes) {
|
|
216
|
+
if (answer) break;
|
|
217
|
+
if (
|
|
218
|
+
needed(route.page) ||
|
|
219
|
+
route.layouts.some(needed) ||
|
|
220
|
+
(route.loading ?? []).some((entry) => needed(entry.module)) ||
|
|
221
|
+
(route.templates ?? []).some((entry) => needed(entry.module)) ||
|
|
222
|
+
route.slots.some(needsSlot)
|
|
223
|
+
) {
|
|
224
|
+
answer = true;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
cache.set(slot, answer);
|
|
228
|
+
return answer;
|
|
229
|
+
};
|
|
230
|
+
return needsSlot;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// Server actions
|
|
235
|
+
//
|
|
236
|
+
// The other half of the split, and the one the manifest was already carrying
|
|
237
|
+
// an answer for. `serverActions` in the manifest is every action `uf_rsc`
|
|
238
|
+
// decided is a *callable endpoint* — an action some module that can hand it
|
|
239
|
+
// across a client boundary reaches — with the keyed id
|
|
240
|
+
// `crates/uf_rsc/src/action.rs` derived for it. An action nothing exposes is
|
|
241
|
+
// tracked in the registry and never written here, so a table built out of this
|
|
242
|
+
// file cannot contain a row that was not meant to be dialable.
|
|
243
|
+
//
|
|
244
|
+
// Two tables come out of it, for the two graphs:
|
|
245
|
+
//
|
|
246
|
+
// * `serverActionModules` is the browser's. It says, for each `"use server"`
|
|
247
|
+
// file, which exports become `createServerReference` calls — and the plugin
|
|
248
|
+
// answers that source *instead of the file*, so the module's body never
|
|
249
|
+
// enters the client graph and neither does anything only it imported.
|
|
250
|
+
// * `serverActionTable` is the server's. It is what `virtual:uf/actions`
|
|
251
|
+
// emits and what `createActionDispatcher` dials into.
|
|
252
|
+
//
|
|
253
|
+
// Both are keyed on the id and nothing else. No request-derived value ever
|
|
254
|
+
// becomes a path, a specifier or an export name here or downstream; see the
|
|
255
|
+
// header of `packages/router/internal/action-endpoint.js`.
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The request header carrying an action id, lowercased as Node delivers it.
|
|
260
|
+
*
|
|
261
|
+
* A second spelling of `ACTION_HEADER` in
|
|
262
|
+
* `packages/router/internal/action-wire.js`, and it has to be one: this module
|
|
263
|
+
* is plain JavaScript the Vite host imports before any transform, and that one
|
|
264
|
+
* is Flow, which Node cannot import at all. `RSC_MANIFEST_ENV` above is the
|
|
265
|
+
* same situation with `crates/uf_rsc/src/manifest.rs`. What keeps a second
|
|
266
|
+
* spelling from becoming a second answer is
|
|
267
|
+
* `tests/library/server-actions.test.js`, which reads both and compares them.
|
|
268
|
+
*/
|
|
269
|
+
export const ACTION_HEADER = "uf-action";
|
|
270
|
+
|
|
271
|
+
/** The id an action row must carry: 64 lowercase hexadecimal characters. */
|
|
272
|
+
function isActionId(value) {
|
|
273
|
+
if (typeof value !== "string" || value.length !== 64) return false;
|
|
274
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
275
|
+
const code = value.charCodeAt(index);
|
|
276
|
+
const digit = code >= 0x30 && code <= 0x39;
|
|
277
|
+
const lower = code >= 0x61 && code <= 0x66;
|
|
278
|
+
if (!digit && !lower) return false;
|
|
279
|
+
}
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Whether a name can be written as `export const <name>`.
|
|
285
|
+
*
|
|
286
|
+
* The scanner only ever produces identifiers, so this refuses nothing a real
|
|
287
|
+
* project has. It is here because the alternative to refusing is emitting a
|
|
288
|
+
* module that does not parse, and a generated file that does not parse fails a
|
|
289
|
+
* build somewhere far from the module that caused it. `default` is handled by
|
|
290
|
+
* the caller, which writes `export default`.
|
|
291
|
+
*/
|
|
292
|
+
function isExportableName(name) {
|
|
293
|
+
if (typeof name !== "string" || name.length === 0) return false;
|
|
294
|
+
const first = name.charCodeAt(0);
|
|
295
|
+
const startish = (code) =>
|
|
296
|
+
(code >= 0x41 && code <= 0x5a) ||
|
|
297
|
+
(code >= 0x61 && code <= 0x7a) ||
|
|
298
|
+
code === 0x24 ||
|
|
299
|
+
code === 0x5f;
|
|
300
|
+
if (!startish(first)) return false;
|
|
301
|
+
for (let index = 1; index < name.length; index += 1) {
|
|
302
|
+
const code = name.charCodeAt(index);
|
|
303
|
+
if (!startish(code) && !(code >= 0x30 && code <= 0x39)) return false;
|
|
304
|
+
}
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Every callable action of the manifest, in the manifest's own order. */
|
|
309
|
+
function callableActions(manifest) {
|
|
310
|
+
if (manifest == null || !Array.isArray(manifest.serverActions)) return [];
|
|
311
|
+
return manifest.serverActions.filter(
|
|
312
|
+
(action) =>
|
|
313
|
+
action != null &&
|
|
314
|
+
isActionId(action.id) &&
|
|
315
|
+
typeof action.module === "string" &&
|
|
316
|
+
action.module !== "" &&
|
|
317
|
+
// An inline `"use server"` closure has no export name to import, so it
|
|
318
|
+
// has no reference in the client bundle and no row in the server's
|
|
319
|
+
// table. It is in the manifest, and reaching it needs the payload
|
|
320
|
+
// ubugeeei-prod/uf#252 is about.
|
|
321
|
+
action.kind === "module-export" &&
|
|
322
|
+
isExportableName(action.export === "default" ? "default_" : action.export),
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* The absolute path of a module the manifest names, or `null`.
|
|
328
|
+
*
|
|
329
|
+
* The manifest's paths are project-relative with forward slashes and were
|
|
330
|
+
* written by a walk that already refused anything outside the root; joined
|
|
331
|
+
* here and checked again, because a path that escapes the project is a path
|
|
332
|
+
* this plugin would otherwise hand to Rollup as a module to emit.
|
|
333
|
+
*/
|
|
334
|
+
function moduleFile(root, relative) {
|
|
335
|
+
const joined = path.resolve(root, relative);
|
|
336
|
+
const inside = path.relative(root, joined);
|
|
337
|
+
if (inside === "" || inside.startsWith("..") || path.isAbsolute(inside)) return null;
|
|
338
|
+
return joined;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Which exports of each `"use server"` file become references in the browser.
|
|
343
|
+
*
|
|
344
|
+
* Keyed by absolute path, because that is what Vite's `load` hook is given.
|
|
345
|
+
* A file with no callable action is absent rather than present-and-empty: the
|
|
346
|
+
* plugin substitutes a module only for a key it finds, and substituting an
|
|
347
|
+
* empty module for a file something imports would be a build error in place of
|
|
348
|
+
* a working import.
|
|
349
|
+
*
|
|
350
|
+
* @param {object | null} manifest from {@link readRscManifest}
|
|
351
|
+
* @param {string} root absolute project root
|
|
352
|
+
* @returns {Map<string, Array<{id: string, module: string, export: string}>>}
|
|
353
|
+
*/
|
|
354
|
+
export function serverActionModules(manifest, root) {
|
|
355
|
+
const modules = new Map();
|
|
356
|
+
for (const action of callableActions(manifest)) {
|
|
357
|
+
const file = moduleFile(root, action.module);
|
|
358
|
+
if (file == null) continue;
|
|
359
|
+
const rows = modules.get(file);
|
|
360
|
+
const row = { id: action.id, module: action.module, export: action.export };
|
|
361
|
+
if (rows === undefined) modules.set(file, [row]);
|
|
362
|
+
else rows.push(row);
|
|
363
|
+
}
|
|
364
|
+
return modules;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Every callable action, as the server's dispatcher table.
|
|
369
|
+
*
|
|
370
|
+
* @param {object | null} manifest from {@link readRscManifest}
|
|
371
|
+
* @param {string} root absolute project root
|
|
372
|
+
* @returns {Array<{id: string, module: string, export: string, file: string}>}
|
|
373
|
+
*/
|
|
374
|
+
export function serverActionTable(manifest, root) {
|
|
375
|
+
const rows = [];
|
|
376
|
+
for (const action of callableActions(manifest)) {
|
|
377
|
+
const file = moduleFile(root, action.module);
|
|
378
|
+
if (file == null) continue;
|
|
379
|
+
rows.push({ id: action.id, module: action.module, export: action.export, file });
|
|
380
|
+
}
|
|
381
|
+
return rows;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* The client bundle's stand-in for one `"use server"` module.
|
|
386
|
+
*
|
|
387
|
+
* What the browser gets in place of the file: one `createServerReference` per
|
|
388
|
+
* callable export, an id each, and nothing the module itself imported. This is
|
|
389
|
+
* the whole of how a database handle reached only through an action stays on
|
|
390
|
+
* the server — `crates/uf_rsc/src/graph/build.rs` colours the module server for
|
|
391
|
+
* the same reason, so that the analysis and the bundle agree about it.
|
|
392
|
+
*
|
|
393
|
+
* @param {Array<{id: string, module: string, export: string}>} actions
|
|
394
|
+
*/
|
|
395
|
+
export function actionReferenceSource(actions) {
|
|
396
|
+
const lines = ['import { createServerReference } from "@uniflowed/router/action";', ""];
|
|
397
|
+
for (const action of actions) {
|
|
398
|
+
const reference = `createServerReference(${JSON.stringify(action.id)}, ${JSON.stringify(
|
|
399
|
+
`${action.module}#${action.export}`,
|
|
400
|
+
)})`;
|
|
401
|
+
lines.push(
|
|
402
|
+
action.export === "default"
|
|
403
|
+
? `export default ${reference};`
|
|
404
|
+
: `export const ${action.export} = ${reference};`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
return `${lines.join("\n")}\n`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* The source of `virtual:uf/actions`: the table the endpoint dials into.
|
|
412
|
+
*
|
|
413
|
+
* One `import()` thunk per file rather than one per action, so a module with
|
|
414
|
+
* four actions is one chunk of the server bundle and not four. Lazy for the
|
|
415
|
+
* reason the handler table is: an action module is loaded when an action in it
|
|
416
|
+
* is called, and a project's actions are not something every request should
|
|
417
|
+
* pay to import.
|
|
418
|
+
*
|
|
419
|
+
* With no manifest the table is empty and every action call is a `404` — the
|
|
420
|
+
* same answer a project driving Vite itself gets for the route split, and for
|
|
421
|
+
* the same reason: uf will not guess at an analysis it was not given.
|
|
422
|
+
*
|
|
423
|
+
* @param {Array<{id: string, module: string, export: string, file: string}>} actions
|
|
424
|
+
*/
|
|
425
|
+
export function actionsModuleSource(actions) {
|
|
426
|
+
const loaders = new Map();
|
|
427
|
+
const declarations = [];
|
|
428
|
+
const loaderId = (file) => {
|
|
429
|
+
let id = loaders.get(file);
|
|
430
|
+
if (id === undefined) {
|
|
431
|
+
id = `load${loaders.size}`;
|
|
432
|
+
loaders.set(file, id);
|
|
433
|
+
declarations.push(`const ${id} = () => import(${JSON.stringify(file)});`);
|
|
434
|
+
}
|
|
435
|
+
return id;
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const entries = actions.map(
|
|
439
|
+
(action) => ` {
|
|
440
|
+
id: ${JSON.stringify(action.id)},
|
|
441
|
+
module: ${JSON.stringify(action.module)},
|
|
442
|
+
export: ${JSON.stringify(action.export)},
|
|
443
|
+
load: ${loaderId(action.file)},
|
|
444
|
+
}`,
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
return `${declarations.join("\n")}
|
|
448
|
+
export const actions = [
|
|
449
|
+
${entries.join(",\n")}
|
|
450
|
+
];
|
|
451
|
+
export default actions;
|
|
452
|
+
`;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* A cheap identity for the manifest file, so a reader can tell it has changed.
|
|
457
|
+
*
|
|
458
|
+
* The plugin's `load` hook runs for every module in the graph and cannot parse
|
|
459
|
+
* the manifest each time. Size and modification time together are what
|
|
460
|
+
* `.uf/cache/transform` already keys on for the binary that wrote it, and the
|
|
461
|
+
* same reasoning applies: a file that differs in neither is the file that was
|
|
462
|
+
* read. `uf dev` also clears the cache outright when its watcher sees the
|
|
463
|
+
* manifest change, so this is the build's answer rather than the only one.
|
|
464
|
+
*
|
|
465
|
+
* @param {string | undefined} file
|
|
466
|
+
*/
|
|
467
|
+
export function rscManifestKey(file) {
|
|
468
|
+
if (file == null || file === "") return "";
|
|
469
|
+
try {
|
|
470
|
+
const stats = statSync(file);
|
|
471
|
+
return `${String(stats.size)}:${String(stats.mtimeMs)}`;
|
|
472
|
+
} catch {
|
|
473
|
+
return "";
|
|
474
|
+
}
|
|
475
|
+
}
|