@uniflowed/vite 0.0.0-alpha.12 → 0.0.0-alpha.14
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 +329 -145
- package/index.js +266 -51
- package/internal/diagnostics.js +366 -0
- package/internal/events.js +5 -5
- package/internal/routes.js +295 -27
- package/internal/rsc.js +256 -1
- package/internal/serve.js +46 -3
- package/package.json +11 -3
package/internal/routes.js
CHANGED
|
@@ -20,6 +20,7 @@ import path from "node:path";
|
|
|
20
20
|
/** The file names the router reserves inside the router root. */
|
|
21
21
|
export const RESERVED = Object.freeze({
|
|
22
22
|
layout: "_uf.layout",
|
|
23
|
+
template: "_uf.template",
|
|
23
24
|
page: "_uf.page",
|
|
24
25
|
middleware: "_uf.middleware",
|
|
25
26
|
notFound: "_uf.not-found",
|
|
@@ -28,6 +29,31 @@ export const RESERVED = Object.freeze({
|
|
|
28
29
|
route: "_uf.route",
|
|
29
30
|
});
|
|
30
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Directory names uf reserves inside the router root without serving them.
|
|
34
|
+
*
|
|
35
|
+
* One spelling each, and they are here so `crates/uf_router/tests/
|
|
36
|
+
* reserved_names.rs` can hold this router and `uf_router::RouteSegment` to the
|
|
37
|
+
* same list — the way it already holds the two to the same `_uf.*` roles. A
|
|
38
|
+
* spelling one router refuses and the other serves as a URL is exactly the
|
|
39
|
+
* disagreement that made this necessary.
|
|
40
|
+
*
|
|
41
|
+
* `@team` is Next.js's parallel-route slot and `(.)photo` its intercepting
|
|
42
|
+
* route. uf has neither, and until #267 both fell through to "a literal URL
|
|
43
|
+
* segment": `@team` became `/@team`, `(.)photo` became `/(.)photo` — the test
|
|
44
|
+
* for a `(group)` is that the segment *ends* in `)` — and the generated
|
|
45
|
+
* `RoutePath` union contained them, so `route("/@team", …)` type checked. A
|
|
46
|
+
* convention served as nonsense is worse than one that is refused, because the
|
|
47
|
+
* project looks like it works.
|
|
48
|
+
*/
|
|
49
|
+
export const UNSUPPORTED_SEGMENTS = Object.freeze([
|
|
50
|
+
"@team",
|
|
51
|
+
"(.)photo",
|
|
52
|
+
"(..)photo",
|
|
53
|
+
"(...)photo",
|
|
54
|
+
"(..)(..)photo",
|
|
55
|
+
]);
|
|
56
|
+
|
|
31
57
|
/** Extensions a page or layout may use; `.mdx` is a page written as content. */
|
|
32
58
|
const PAGE_EXTENSIONS = [".js", ".jsx", ".mdx"];
|
|
33
59
|
const MODULE_EXTENSIONS = [".js", ".jsx"];
|
|
@@ -47,6 +73,8 @@ const MAX_DEPTH = 32;
|
|
|
47
73
|
* @property {ReadonlyArray<{above: number, module: string}>} loading the
|
|
48
74
|
* `<Suspense>` boundaries in scope, root first; `above` is how many of
|
|
49
75
|
* `layouts` are outside each one
|
|
76
|
+
* @property {ReadonlyArray<{above: number, module: string}>} templates the
|
|
77
|
+
* `_uf.template.js` wrappers in scope, root first, with the same `above`
|
|
50
78
|
* @property {boolean} mdx whether the page is MDX content
|
|
51
79
|
*/
|
|
52
80
|
|
|
@@ -86,7 +114,9 @@ const MAX_DEPTH = 32;
|
|
|
86
114
|
*
|
|
87
115
|
* @typedef {object} NotFoundBoundary
|
|
88
116
|
* @property {string} path route path of the directory that declares it
|
|
89
|
-
* @property {string} page absolute path of the page module
|
|
117
|
+
* @property {?string} page absolute path of the page module, or `null` for the
|
|
118
|
+
* record the scan synthesises at the router root when a project declares
|
|
119
|
+
* none — see `scanRoutes`
|
|
90
120
|
* @property {ReadonlyArray<string>} layouts absolute paths, root first
|
|
91
121
|
* @property {boolean} mdx whether the page is MDX content
|
|
92
122
|
*/
|
|
@@ -103,7 +133,8 @@ const MAX_DEPTH = 32;
|
|
|
103
133
|
*
|
|
104
134
|
* @typedef {object} ErrorBoundary
|
|
105
135
|
* @property {string} path route path of the directory that declares it
|
|
106
|
-
* @property {string} module absolute path of the error module
|
|
136
|
+
* @property {?string} module absolute path of the error module, or `null` for
|
|
137
|
+
* the synthesised root record
|
|
107
138
|
* @property {ReadonlyArray<string>} layouts absolute paths, root first
|
|
108
139
|
*/
|
|
109
140
|
|
|
@@ -136,6 +167,10 @@ const MAX_DEPTH = 32;
|
|
|
136
167
|
* Directories that do not exist yield an empty table rather than an error: a
|
|
137
168
|
* library project has no router root, and that is not a mistake.
|
|
138
169
|
*
|
|
170
|
+
* Throws for a directory named the way a parallel route or an intercepting
|
|
171
|
+
* route is spelled: uf has neither, and both used to become literal URL
|
|
172
|
+
* segments. See {@link UNSUPPORTED_SEGMENTS}.
|
|
173
|
+
*
|
|
139
174
|
* @param {string} appRoot absolute path of the router root (`app/`)
|
|
140
175
|
* @returns {{
|
|
141
176
|
* routes: Route[],
|
|
@@ -153,7 +188,11 @@ export function scanRoutes(appRoot) {
|
|
|
153
188
|
const errors = [];
|
|
154
189
|
if (!isDirectory(appRoot)) return { routes, handlers, middleware, notFound, errors };
|
|
155
190
|
|
|
156
|
-
|
|
191
|
+
// The layouts in scope at the router root, kept because the two synthesised
|
|
192
|
+
// records below are made of them. See the note beside them.
|
|
193
|
+
let rootLayouts = [];
|
|
194
|
+
|
|
195
|
+
const walk = (directory, segments, layouts, loading, templates, depth) => {
|
|
157
196
|
if (depth > MAX_DEPTH) return;
|
|
158
197
|
const entries = readdirSync(directory, { withFileTypes: true }).sort((a, b) =>
|
|
159
198
|
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
|
@@ -161,6 +200,9 @@ export function scanRoutes(appRoot) {
|
|
|
161
200
|
|
|
162
201
|
const ownLayout = findModule(directory, RESERVED.layout, MODULE_EXTENSIONS);
|
|
163
202
|
const nextLayouts = ownLayout ? [...layouts, ownLayout] : layouts;
|
|
203
|
+
if (depth === 0) {
|
|
204
|
+
rootLayouts = nextLayouts;
|
|
205
|
+
}
|
|
164
206
|
|
|
165
207
|
// Inside this directory's own layout, which is where Next.js puts it and
|
|
166
208
|
// the only placement that makes sense: the fallback is what shows *within*
|
|
@@ -173,6 +215,17 @@ export function scanRoutes(appRoot) {
|
|
|
173
215
|
? [...loading, { above: nextLayouts.length, module: ownLoading }]
|
|
174
216
|
: loading;
|
|
175
217
|
|
|
218
|
+
// A template accumulates the way a layout does, and is placed the way a
|
|
219
|
+
// loading file is: inside its own segment's layout and outside everything
|
|
220
|
+
// below, so `nextLayouts.length` is taken after the own layout is added.
|
|
221
|
+
// Every template above a route is on that route, one inside the next, for
|
|
222
|
+
// the reason every layout is — the difference between the two is a `key`,
|
|
223
|
+
// not a shape.
|
|
224
|
+
const ownTemplate = findModule(directory, RESERVED.template, MODULE_EXTENSIONS);
|
|
225
|
+
const nextTemplates = ownTemplate
|
|
226
|
+
? [...templates, { above: nextLayouts.length, module: ownTemplate }]
|
|
227
|
+
: templates;
|
|
228
|
+
|
|
176
229
|
// A middleware guards this directory and everything below it, whether or
|
|
177
230
|
// not this directory is itself a route: `app/dashboard/_uf.middleware.js`
|
|
178
231
|
// with no `_uf.page.js` beside it still guards `/dashboard/settings`.
|
|
@@ -191,6 +244,7 @@ export function scanRoutes(appRoot) {
|
|
|
191
244
|
page,
|
|
192
245
|
layouts: nextLayouts,
|
|
193
246
|
loading: nextLoading,
|
|
247
|
+
templates: nextTemplates,
|
|
194
248
|
mdx: page.endsWith(".mdx"),
|
|
195
249
|
});
|
|
196
250
|
}
|
|
@@ -233,17 +287,49 @@ export function scanRoutes(appRoot) {
|
|
|
233
287
|
// A leading dot or underscore is private to the author: `_components/`
|
|
234
288
|
// beside a page is a place to put things, not a route.
|
|
235
289
|
if (entry.name.startsWith(".") || entry.name.startsWith("_")) continue;
|
|
290
|
+
// Checked before descending, and after the private-directory test for
|
|
291
|
+
// the same reason `uf_router` prunes them: `app/_drafts/@team/` is not a
|
|
292
|
+
// route uf would have served, so it is not one to refuse.
|
|
293
|
+
const refused = unsupportedSegmentReason(entry.name);
|
|
294
|
+
if (refused != null) {
|
|
295
|
+
throw new Error(`${path.join(directory, entry.name)}: ${refused}`);
|
|
296
|
+
}
|
|
236
297
|
walk(
|
|
237
298
|
path.join(directory, entry.name),
|
|
238
299
|
[...segments, entry.name],
|
|
239
300
|
nextLayouts,
|
|
240
301
|
nextLoading,
|
|
302
|
+
nextTemplates,
|
|
241
303
|
depth + 1,
|
|
242
304
|
);
|
|
243
305
|
}
|
|
244
306
|
};
|
|
245
307
|
|
|
246
|
-
walk(appRoot, [], [], [], 0);
|
|
308
|
+
walk(appRoot, [], [], [], [], 0);
|
|
309
|
+
|
|
310
|
+
// A boundary at the router root for a project that declared none, carrying
|
|
311
|
+
// the root's layouts and no module of its own.
|
|
312
|
+
//
|
|
313
|
+
// Without it the router had no record to answer an unmatched URL with, so it
|
|
314
|
+
// answered with the framework's page and `layouts: []` — and a site whose
|
|
315
|
+
// root layout owns the masthead, the stylesheet and often `<html>` itself
|
|
316
|
+
// replied to a stale link with a white page saying 404, with no way to leave
|
|
317
|
+
// it. That was never the nearest-ancestor rule failing: the rule had nothing
|
|
318
|
+
// to find. `uf create` scaffolds neither boundary, so this is the state every
|
|
319
|
+
// new project is in until it writes one. See ubugeeei-prod/uf#351.
|
|
320
|
+
//
|
|
321
|
+
// Only when nothing is at `/` already. A `(group)` directory is not a URL
|
|
322
|
+
// segment, so `app/(marketing)/_uf.not-found.js` is a boundary at `/` too and
|
|
323
|
+
// adding a second one there would put a second answer at a path the URL
|
|
324
|
+
// cannot choose between.
|
|
325
|
+
const atRoot = (boundaries) => boundaries.some((boundary) => boundary.path === "/");
|
|
326
|
+
if (!atRoot(notFound)) {
|
|
327
|
+
notFound.push({ path: "/", page: null, layouts: rootLayouts, mdx: false });
|
|
328
|
+
}
|
|
329
|
+
if (!atRoot(errors)) {
|
|
330
|
+
errors.push({ path: "/", module: null, layouts: rootLayouts });
|
|
331
|
+
}
|
|
332
|
+
|
|
247
333
|
const byPath = (a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
248
334
|
routes.sort(byPath);
|
|
249
335
|
handlers.sort(byPath);
|
|
@@ -287,27 +373,112 @@ function findModule(directory, stem, extensions) {
|
|
|
287
373
|
return null;
|
|
288
374
|
}
|
|
289
375
|
|
|
376
|
+
/**
|
|
377
|
+
* What one directory name means to the route path.
|
|
378
|
+
*
|
|
379
|
+
* Mirrors `uf_router::classify_route_segment`, which is the same six answers
|
|
380
|
+
* in the same order. The order is load-bearing in one place: an interception
|
|
381
|
+
* marker is a `(…)` *prefix* with a route after it, and a `(group)` is a
|
|
382
|
+
* segment that ends in `)`, so the interception test has to come first or
|
|
383
|
+
* every group would be read as one.
|
|
384
|
+
*
|
|
385
|
+
* @param {string} segment one directory name
|
|
386
|
+
* @returns {{kind: "group"}
|
|
387
|
+
* | {kind: "param", name: string}
|
|
388
|
+
* | {kind: "catchAll", name: string}
|
|
389
|
+
* | {kind: "literal", name: string}
|
|
390
|
+
* | {kind: "slot", name: string}
|
|
391
|
+
* | {kind: "interception", marker: string, route: string}}
|
|
392
|
+
*/
|
|
393
|
+
export function classifyRouteSegment(segment) {
|
|
394
|
+
if (segment.startsWith("@")) return { kind: "slot", name: segment.slice(1) };
|
|
395
|
+
const intercepted = interceptionMarker(segment);
|
|
396
|
+
if (intercepted != null) return { kind: "interception", ...intercepted };
|
|
397
|
+
if (segment.startsWith("(") && segment.endsWith(")")) return { kind: "group" };
|
|
398
|
+
if (segment.startsWith("[...") && segment.endsWith("]")) {
|
|
399
|
+
return { kind: "catchAll", name: segment.slice(4, -1) };
|
|
400
|
+
}
|
|
401
|
+
if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
402
|
+
return { kind: "param", name: segment.slice(1, -1) };
|
|
403
|
+
}
|
|
404
|
+
return { kind: "literal", name: segment };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* The `(.)`-style prefix of `segment` and the route after it, or `null`.
|
|
409
|
+
*
|
|
410
|
+
* One or more of `(.)`, `(..)` and `(...)` — every marker Next.js defines;
|
|
411
|
+
* `(..)(..)` is two of them rather than a fourth — followed by something for
|
|
412
|
+
* them to intercept. A marker with nothing after it names no route and is the
|
|
413
|
+
* `(group)` it has always been.
|
|
414
|
+
*/
|
|
415
|
+
function interceptionMarker(segment) {
|
|
416
|
+
let consumed = 0;
|
|
417
|
+
while (segment[consumed] === "(") {
|
|
418
|
+
const close = segment.indexOf(")", consumed);
|
|
419
|
+
if (close === -1) break;
|
|
420
|
+
const inner = segment.slice(consumed + 1, close);
|
|
421
|
+
if (inner.length === 0 || inner.length > 3 || /[^.]/.test(inner)) break;
|
|
422
|
+
consumed = close + 1;
|
|
423
|
+
}
|
|
424
|
+
if (consumed === 0 || consumed === segment.length) return null;
|
|
425
|
+
return { marker: segment.slice(0, consumed), route: segment.slice(consumed) };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Why uf refuses a directory named `segment`, or `null` when it serves it.
|
|
430
|
+
*
|
|
431
|
+
* The message is this router's own rather than `uf_router`'s, because the two
|
|
432
|
+
* are reached differently: the Rust one fails `uf build` and `uf dev` through
|
|
433
|
+
* the route manifest, and this one fails a project driving Vite itself. Both
|
|
434
|
+
* say the same two things — which feature the spelling belongs to, and that it
|
|
435
|
+
* is refused rather than served as a URL.
|
|
436
|
+
*/
|
|
437
|
+
export function unsupportedSegmentReason(segment) {
|
|
438
|
+
const classified = classifyRouteSegment(segment);
|
|
439
|
+
if (classified.kind === "slot") {
|
|
440
|
+
return (
|
|
441
|
+
`\`${segment}\` is a parallel-route slot, and uf does not have parallel routes — a route ` +
|
|
442
|
+
"here renders in one place, so there is nothing for a slot to render into. It is refused " +
|
|
443
|
+
`rather than served as the URL segment \`/${segment}\`, which is what it used to become. ` +
|
|
444
|
+
"Rename the directory; a URL segment that really starts with `@` has no spelling in this " +
|
|
445
|
+
"grammar, so capture it with a `[param]`. https://github.com/ubugeeei-prod/uf/issues/267"
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
if (classified.kind === "interception") {
|
|
449
|
+
return (
|
|
450
|
+
`\`${segment}\` is an intercepting route, and uf does not have interception — a navigation ` +
|
|
451
|
+
"carries where it is going and not where it came from, so nothing here could match " +
|
|
452
|
+
`\`${classified.route}\`. It is refused rather than served as the URL segment ` +
|
|
453
|
+
`\`/${segment}\`, which is what it used to become. Move the route to the path it belongs ` +
|
|
454
|
+
"at, or rename the directory. https://github.com/ubugeeei-prod/uf/issues/267"
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
|
|
290
460
|
/**
|
|
291
461
|
* Turn directory segments into a route path and its parameters.
|
|
292
462
|
*
|
|
293
463
|
* `(group)` segments organise files without appearing in the URL, `[name]`
|
|
294
|
-
* captures one segment, and `[...name]` captures the rest of the path.
|
|
464
|
+
* captures one segment, and `[...name]` captures the rest of the path. A slot
|
|
465
|
+
* or an interception never reaches here: {@link scanRoutes} refuses the
|
|
466
|
+
* directory before it walks into it.
|
|
295
467
|
*/
|
|
296
468
|
export function routeFromSegments(segments) {
|
|
297
469
|
const params = [];
|
|
298
470
|
const out = [];
|
|
299
471
|
for (const segment of segments) {
|
|
300
|
-
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
params.push({ name, catchAll: true });
|
|
304
|
-
out.push(`:${name}*`);
|
|
472
|
+
const classified = classifyRouteSegment(segment);
|
|
473
|
+
if (classified.kind === "group") continue;
|
|
474
|
+
if (classified.kind === "catchAll") {
|
|
475
|
+
params.push({ name: classified.name, catchAll: true });
|
|
476
|
+
out.push(`:${classified.name}*`);
|
|
305
477
|
continue;
|
|
306
478
|
}
|
|
307
|
-
if (
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
out.push(`:${name}`);
|
|
479
|
+
if (classified.kind === "param") {
|
|
480
|
+
params.push({ name: classified.name, catchAll: false });
|
|
481
|
+
out.push(`:${classified.name}`);
|
|
311
482
|
continue;
|
|
312
483
|
}
|
|
313
484
|
out.push(segment);
|
|
@@ -316,11 +487,21 @@ export function routeFromSegments(segments) {
|
|
|
316
487
|
return { path: routePath, pattern: routePath.replace(/:(\w+)\*/g, "*$1"), params };
|
|
317
488
|
}
|
|
318
489
|
|
|
319
|
-
/**
|
|
490
|
+
/**
|
|
491
|
+
* Virtual module ids the router plugin serves.
|
|
492
|
+
*
|
|
493
|
+
* `actions` is the one that does not come from this file's directory scan: it
|
|
494
|
+
* is generated from the RSC manifest by `internal/rsc.js`, because which
|
|
495
|
+
* `"use server"` exports are callable endpoints is an answer about the module
|
|
496
|
+
* graph and not about the filesystem. It is here because it is a virtual
|
|
497
|
+
* module id and this is where they are named, and because
|
|
498
|
+
* `serverModuleSource` below is the only thing that imports it.
|
|
499
|
+
*/
|
|
320
500
|
export const VIRTUAL = Object.freeze({
|
|
321
501
|
routes: "virtual:uf/routes",
|
|
322
502
|
client: "virtual:uf/client",
|
|
323
503
|
server: "virtual:uf/server",
|
|
504
|
+
actions: "virtual:uf/actions",
|
|
324
505
|
});
|
|
325
506
|
|
|
326
507
|
/**
|
|
@@ -366,6 +547,25 @@ export const VIRTUAL = Object.freeze({
|
|
|
366
547
|
* `virtual:uf/server` is generated with the default and always will be: the
|
|
367
548
|
* server renders every route, so its table is the complete one.
|
|
368
549
|
*
|
|
550
|
+
* # `relativeTo`, and the one string in this table a browser can read
|
|
551
|
+
*
|
|
552
|
+
* Every `import()` here is a specifier Vite resolves and rewrites to a chunk
|
|
553
|
+
* URL, so no absolute path survives the build — except `file`, which is a
|
|
554
|
+
* string. It is the route's source path, kept for diagnostics: the middleware
|
|
555
|
+
* table's is what names a module in an error, and `router.js`'s generated
|
|
556
|
+
* types are about the same files.
|
|
557
|
+
*
|
|
558
|
+
* The server's table can hold an absolute path; it is read on the machine that
|
|
559
|
+
* has those files. The browser's cannot, because that table is downloaded:
|
|
560
|
+
* uf's own manual shipped `/home/<user>/…/docs/app/guide/cache/_uf.page.mdx`
|
|
561
|
+
* for each of thirty-four routes to every visitor, which publishes the build
|
|
562
|
+
* machine's layout and its user's name for nothing — the browser has no
|
|
563
|
+
* filesystem to resolve them against and reads them only in a message.
|
|
564
|
+
*
|
|
565
|
+
* So the client call passes the project root and every `file` here is emitted
|
|
566
|
+
* relative to it. Diagnostics keep a path a person can act on — a shorter one
|
|
567
|
+
* — and a deploy stops describing the machine it was built on.
|
|
568
|
+
*
|
|
369
569
|
* @param {{
|
|
370
570
|
* routes: Route[],
|
|
371
571
|
* handlers?: Handler[],
|
|
@@ -373,10 +573,29 @@ export const VIRTUAL = Object.freeze({
|
|
|
373
573
|
* notFound?: NotFoundBoundary[],
|
|
374
574
|
* errors?: ErrorBoundary[],
|
|
375
575
|
* }} table
|
|
376
|
-
* @param {{
|
|
576
|
+
* @param {{
|
|
577
|
+
* shipsPage?: (route: Route) => boolean,
|
|
578
|
+
* relativeTo?: string,
|
|
579
|
+
* }} [options]
|
|
377
580
|
*/
|
|
378
581
|
export function routesModuleSource(table, options = {}) {
|
|
379
582
|
const shipsPage = options.shipsPage ?? (() => true);
|
|
583
|
+
const relativeTo = options.relativeTo ?? null;
|
|
584
|
+
/**
|
|
585
|
+
* A `file` as this table should state it.
|
|
586
|
+
*
|
|
587
|
+
* Relative even when that means leading `..` segments — a module outside the
|
|
588
|
+
* project root is rare and a `../` path still says where it is without
|
|
589
|
+
* saying where the machine is, which is the whole property. Separators are
|
|
590
|
+
* POSIX because this string is read wherever the bundle is opened rather
|
|
591
|
+
* than where it was written.
|
|
592
|
+
*/
|
|
593
|
+
const displayFile = (file) => {
|
|
594
|
+
if (relativeTo == null || file == null) {
|
|
595
|
+
return file;
|
|
596
|
+
}
|
|
597
|
+
return path.relative(relativeTo, file).split(path.sep).join("/");
|
|
598
|
+
};
|
|
380
599
|
const layoutIds = new Map();
|
|
381
600
|
const layoutImports = [];
|
|
382
601
|
const layoutId = (file) => {
|
|
@@ -412,32 +631,63 @@ export function routesModuleSource(table, options = {}) {
|
|
|
412
631
|
return id;
|
|
413
632
|
};
|
|
414
633
|
|
|
634
|
+
// Templates are deduplicated for the reason layouts are — one
|
|
635
|
+
// `app/_uf.template.js` wraps every route under it — and are lazy for the
|
|
636
|
+
// reason layouts are too: a template is part of the route's own tree rather
|
|
637
|
+
// than a fallback React has to have in hand at the moment something goes
|
|
638
|
+
// wrong, so it is awaited with the layouts before the first render.
|
|
639
|
+
const templateIds = new Map();
|
|
640
|
+
const templateImports = [];
|
|
641
|
+
const templateId = (file) => {
|
|
642
|
+
let id = templateIds.get(file);
|
|
643
|
+
if (id === undefined) {
|
|
644
|
+
id = `template${templateIds.size}`;
|
|
645
|
+
templateIds.set(file, id);
|
|
646
|
+
templateImports.push(`const ${id} = () => import(${JSON.stringify(file)});`);
|
|
647
|
+
}
|
|
648
|
+
return id;
|
|
649
|
+
};
|
|
650
|
+
|
|
415
651
|
const entries = table.routes.map((route) => {
|
|
416
652
|
if (!shipsPage(route)) {
|
|
417
653
|
return ` {
|
|
418
654
|
path: ${JSON.stringify(route.path)},
|
|
419
655
|
params: ${JSON.stringify(route.params)},
|
|
420
656
|
mdx: ${route.mdx},
|
|
421
|
-
file: ${JSON.stringify(route.page)},
|
|
657
|
+
file: ${JSON.stringify(displayFile(route.page))},
|
|
422
658
|
layouts: [],
|
|
423
659
|
loading: [],
|
|
660
|
+
templates: [],
|
|
424
661
|
}`;
|
|
425
662
|
}
|
|
426
663
|
const layouts = route.layouts.map(layoutId);
|
|
427
664
|
const loading = (route.loading ?? []).map(
|
|
428
665
|
(boundary) => `{ above: ${boundary.above}, module: ${loadingId(boundary.module)} }`,
|
|
429
666
|
);
|
|
667
|
+
const templates = (route.templates ?? []).map(
|
|
668
|
+
(entry) => `{ above: ${entry.above}, module: ${templateId(entry.module)} }`,
|
|
669
|
+
);
|
|
430
670
|
return ` {
|
|
431
671
|
path: ${JSON.stringify(route.path)},
|
|
432
672
|
params: ${JSON.stringify(route.params)},
|
|
433
673
|
mdx: ${route.mdx},
|
|
434
|
-
file: ${JSON.stringify(route.page)},
|
|
674
|
+
file: ${JSON.stringify(displayFile(route.page))},
|
|
435
675
|
page: () => import(${JSON.stringify(route.page)}),
|
|
436
676
|
layouts: [${layouts.join(", ")}],
|
|
437
677
|
loading: [${loading.join(", ")}],
|
|
678
|
+
templates: [${templates.join(", ")}],
|
|
438
679
|
}`;
|
|
439
680
|
});
|
|
440
681
|
|
|
682
|
+
// A boundary the scan synthesised has no module to import — the framework's
|
|
683
|
+
// own page renders in its place — so it emits `null` where a declared one
|
|
684
|
+
// emits a loader, and a name for `file` rather than a path nothing wrote.
|
|
685
|
+
// See the note in `scanRoutes` and ubugeeei-prod/uf#351.
|
|
686
|
+
const SYNTHESISED = JSON.stringify("@uniflowed/router");
|
|
687
|
+
const boundaryModule = (file) =>
|
|
688
|
+
file == null ? "null" : `() => import(${JSON.stringify(file)})`;
|
|
689
|
+
const boundaryFile = (file) => (file == null ? SYNTHESISED : JSON.stringify(displayFile(file)));
|
|
690
|
+
|
|
441
691
|
// A list, because a not-found is a segment file: every directory may declare
|
|
442
692
|
// one and the router takes the nearest above the path. `layoutId` is the
|
|
443
693
|
// same table the routes use, so a boundary that shares a layout with a page
|
|
@@ -446,8 +696,8 @@ export function routesModuleSource(table, options = {}) {
|
|
|
446
696
|
(boundary) => ` {
|
|
447
697
|
path: ${JSON.stringify(boundary.path)},
|
|
448
698
|
mdx: ${boundary.mdx},
|
|
449
|
-
file: ${
|
|
450
|
-
page:
|
|
699
|
+
file: ${boundaryFile(boundary.page)},
|
|
700
|
+
page: ${boundaryModule(boundary.page)},
|
|
451
701
|
layouts: [${boundary.layouts.map(layoutId).join(", ")}],
|
|
452
702
|
}`,
|
|
453
703
|
);
|
|
@@ -459,8 +709,8 @@ export function routesModuleSource(table, options = {}) {
|
|
|
459
709
|
const errorEntries = (table.errors ?? []).map(
|
|
460
710
|
(boundary) => ` {
|
|
461
711
|
path: ${JSON.stringify(boundary.path)},
|
|
462
|
-
file: ${
|
|
463
|
-
module:
|
|
712
|
+
file: ${boundaryFile(boundary.module)},
|
|
713
|
+
module: ${boundaryModule(boundary.module)},
|
|
464
714
|
layouts: [${boundary.layouts.map(layoutId).join(", ")}],
|
|
465
715
|
}`,
|
|
466
716
|
);
|
|
@@ -472,7 +722,7 @@ export function routesModuleSource(table, options = {}) {
|
|
|
472
722
|
(handler) => ` {
|
|
473
723
|
path: ${JSON.stringify(handler.path)},
|
|
474
724
|
params: ${JSON.stringify(handler.params)},
|
|
475
|
-
file: ${JSON.stringify(handler.module)},
|
|
725
|
+
file: ${JSON.stringify(displayFile(handler.module))},
|
|
476
726
|
load: () => import(${JSON.stringify(handler.module)}),
|
|
477
727
|
}`,
|
|
478
728
|
);
|
|
@@ -485,7 +735,7 @@ export function routesModuleSource(table, options = {}) {
|
|
|
485
735
|
const middlewareEntries = (table.middleware ?? []).map(
|
|
486
736
|
(entry) => ` {
|
|
487
737
|
path: ${JSON.stringify(entry.path)},
|
|
488
|
-
file: ${JSON.stringify(entry.module)},
|
|
738
|
+
file: ${JSON.stringify(displayFile(entry.module))},
|
|
489
739
|
load: () => import(${JSON.stringify(entry.module)}),
|
|
490
740
|
}`,
|
|
491
741
|
);
|
|
@@ -493,13 +743,18 @@ export function routesModuleSource(table, options = {}) {
|
|
|
493
743
|
// Last, because it is defined by what everything above did *not* import: a
|
|
494
744
|
// layout a kept route also uses is already in the graph as a lazy chunk, and
|
|
495
745
|
// importing it here as well would pull it into the entry chunk instead.
|
|
496
|
-
const carried = new Set([...layoutIds.keys(), ...loadingIds.keys()]);
|
|
746
|
+
const carried = new Set([...layoutIds.keys(), ...loadingIds.keys(), ...templateIds.keys()]);
|
|
497
747
|
const styleOnlyImports = [];
|
|
498
748
|
for (const route of table.routes) {
|
|
499
749
|
if (shipsPage(route)) {
|
|
500
750
|
continue;
|
|
501
751
|
}
|
|
502
|
-
const files = [
|
|
752
|
+
const files = [
|
|
753
|
+
route.page,
|
|
754
|
+
...route.layouts,
|
|
755
|
+
...(route.loading ?? []).map((it) => it.module),
|
|
756
|
+
...(route.templates ?? []).map((it) => it.module),
|
|
757
|
+
];
|
|
503
758
|
for (const file of files) {
|
|
504
759
|
if (carried.has(file)) {
|
|
505
760
|
continue;
|
|
@@ -509,7 +764,7 @@ export function routesModuleSource(table, options = {}) {
|
|
|
509
764
|
}
|
|
510
765
|
}
|
|
511
766
|
|
|
512
|
-
return `${[...styleOnlyImports, ...layoutImports, ...loadingImports].join("\n")}
|
|
767
|
+
return `${[...styleOnlyImports, ...layoutImports, ...loadingImports, ...templateImports].join("\n")}
|
|
513
768
|
export const routes = [
|
|
514
769
|
${entries.join(",\n")}
|
|
515
770
|
];
|
|
@@ -558,6 +813,16 @@ hydrate({ App, routes, notFound, errors });
|
|
|
558
813
|
* request — one decides whether the router is reached at all, the others
|
|
559
814
|
* decide what the router renders when it is.
|
|
560
815
|
*
|
|
816
|
+
* `callAction` goes between the two, and its position is the same argument
|
|
817
|
+
* made twice. Below `runMiddleware`, because an action call is a request to a
|
|
818
|
+
* path and the guard on that path is owed the same say over it as over the
|
|
819
|
+
* page — which is why the call is a `POST` to the page's own URL rather than
|
|
820
|
+
* to a reserved one. Above `dispatch`, because a request that names an action
|
|
821
|
+
* has named it: letting it fall through to a route handler that happens to sit
|
|
822
|
+
* at the same path would answer somebody's action with somebody else's
|
|
823
|
+
* function. It declines every request that carries no action id, so a project
|
|
824
|
+
* with no actions pays one `headers.get` per request and nothing else.
|
|
825
|
+
*
|
|
561
826
|
* `internal/serve.js` and `driver.js` call them in that order, and
|
|
562
827
|
* `packages/vite/index.js` does the same for a project driving Vite itself.
|
|
563
828
|
*
|
|
@@ -583,11 +848,13 @@ hydrate({ App, routes, notFound, errors });
|
|
|
583
848
|
*/
|
|
584
849
|
export function serverModuleSource(appEntry) {
|
|
585
850
|
return `import {
|
|
851
|
+
createActionDispatcher,
|
|
586
852
|
createDispatcher,
|
|
587
853
|
createMiddlewareRunner,
|
|
588
854
|
createRenderer,
|
|
589
855
|
} from "@uniflowed/router/server";
|
|
590
856
|
import { routes, handlers, middleware, notFound, errors } from ${JSON.stringify(VIRTUAL.routes)};
|
|
857
|
+
import { actions } from ${JSON.stringify(VIRTUAL.actions)};
|
|
591
858
|
import App from ${JSON.stringify(appEntry)};
|
|
592
859
|
export { routes, handlers, middleware, notFound, errors };
|
|
593
860
|
export { beginRequest } from "@uniflowed/router/server";
|
|
@@ -595,6 +862,7 @@ const renderer = createRenderer({ App, routes, notFound, errors });
|
|
|
595
862
|
export const render = renderer.render;
|
|
596
863
|
export const prerender = renderer.prerender;
|
|
597
864
|
export const dispatch = createDispatcher({ handlers });
|
|
865
|
+
export const callAction = createActionDispatcher({ actions });
|
|
598
866
|
export const runMiddleware = createMiddlewareRunner({ middleware });
|
|
599
867
|
`;
|
|
600
868
|
}
|