@uniflowed/vite 0.0.0-alpha.6 → 0.0.0-alpha.7
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 +349 -118
- package/index.js +177 -8
- package/internal/events.js +20 -0
- package/internal/routes.js +112 -22
- package/internal/serve.js +386 -0
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -30,6 +30,7 @@ import path from "node:path";
|
|
|
30
30
|
import mdx from "@mdx-js/rollup";
|
|
31
31
|
import rehypeSlug from "rehype-slug";
|
|
32
32
|
|
|
33
|
+
import { reportRenderError } from "./internal/events.js";
|
|
33
34
|
import { highlightPlugin } from "./internal/highlight.js";
|
|
34
35
|
import remarkFrontmatter from "remark-frontmatter";
|
|
35
36
|
import remarkGfm from "remark-gfm";
|
|
@@ -43,6 +44,7 @@ import {
|
|
|
43
44
|
refreshRuntimeSource,
|
|
44
45
|
} from "./internal/refresh.js";
|
|
45
46
|
import {
|
|
47
|
+
RESERVED,
|
|
46
48
|
VIRTUAL,
|
|
47
49
|
clientModuleSource,
|
|
48
50
|
routesModuleSource,
|
|
@@ -113,6 +115,23 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
113
115
|
* hand, which is the part that goes wrong.
|
|
114
116
|
*/
|
|
115
117
|
const styles = new Map();
|
|
118
|
+
/**
|
|
119
|
+
* The React Compiler findings already reported, so each is said once.
|
|
120
|
+
*
|
|
121
|
+
* `uf build` runs Vite twice — once for the browser bundle and once for the
|
|
122
|
+
* server one — over the same modules, so every finding was made twice and
|
|
123
|
+
* printed twice. An entry records which environment reported a module's
|
|
124
|
+
* findings first: a re-transform in *that* environment (a dev server, after
|
|
125
|
+
* an edit) clears it and reports again, and the other environment's pass over
|
|
126
|
+
* the same module stays quiet. Keying on the environment rather than on a
|
|
127
|
+
* flag is what keeps the second half true without making the first half
|
|
128
|
+
* false.
|
|
129
|
+
*
|
|
130
|
+
* @type {Map<string, { environment: string, signatures: Set<string> }>}
|
|
131
|
+
*/
|
|
132
|
+
const reported = new Map();
|
|
133
|
+
/** Findings held back as a dependency's, waiting to be counted out loud. */
|
|
134
|
+
let suppressed = [];
|
|
116
135
|
|
|
117
136
|
const ensureService = () => {
|
|
118
137
|
service ??= new TransformService({ command, root });
|
|
@@ -194,9 +213,14 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
194
213
|
sourceMap: true,
|
|
195
214
|
});
|
|
196
215
|
if (out == null) return null;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
216
|
+
reportDiagnostics(this, {
|
|
217
|
+
id: cleanId(id),
|
|
218
|
+
root,
|
|
219
|
+
diagnostics: out.diagnostics,
|
|
220
|
+
environment: ssr ? "ssr" : "client",
|
|
221
|
+
reported,
|
|
222
|
+
suppressed,
|
|
223
|
+
});
|
|
200
224
|
const map = out.map == null ? null : JSON.parse(out.map);
|
|
201
225
|
// StyleX. `uf transform` compiled the module's `stylex.create` calls into
|
|
202
226
|
// class names and handed back the rules they declared; the rules become a
|
|
@@ -207,18 +231,34 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
207
231
|
// business: Vite already injects a stylesheet in dev, extracts it in a
|
|
208
232
|
// build, code-splits it per chunk, and replaces it over HMR. A module
|
|
209
233
|
// whose styles are gone stops importing it, and Vite notices.
|
|
234
|
+
const styled = out.css != null && out.css !== "";
|
|
210
235
|
let output = out.code;
|
|
211
|
-
if (
|
|
236
|
+
if (styled) {
|
|
212
237
|
const styleId = `${STYLE_PREFIX}${cleanId(id)}.css`;
|
|
213
238
|
styles.set(styleId, out.css);
|
|
214
239
|
output = `import ${JSON.stringify(styleId)};\n${output}`;
|
|
215
240
|
}
|
|
216
|
-
|
|
241
|
+
// A module that compiled a stylesheet has a side effect, whatever its
|
|
242
|
+
// package says. `@uniflowed/stylex` declares `sideEffects: false` and is
|
|
243
|
+
// right about its source: `tokens.stylex.js` only exports a token set.
|
|
244
|
+
// What it exports after this transform is a token set *and* a `:root`
|
|
245
|
+
// block, and the page that imports `ufTokens` no longer names it at
|
|
246
|
+
// runtime — the compiler turned every read into the `var(--…)` it minted.
|
|
247
|
+
// So the import was unused, a side-effect-free module with no used
|
|
248
|
+
// exports was dropped, and the custom properties every one of those
|
|
249
|
+
// `var()`s resolves against went with it: rules that referred to nothing.
|
|
250
|
+
// Declaring the side effect here rather than editing the package is
|
|
251
|
+
// deliberate — the side effect is one this plugin added, so it is this
|
|
252
|
+
// plugin's to admit to. See ubugeeei-prod/uf#306.
|
|
253
|
+
const moduleSideEffects = styled ? true : undefined;
|
|
254
|
+
if (!refresh) return { code: output, map, moduleSideEffects };
|
|
217
255
|
const relative = path.relative(root, cleanId(id)).split(path.sep).join("/");
|
|
218
|
-
return addRefreshWrapper(output, map, relative);
|
|
256
|
+
return { ...addRefreshWrapper(output, map, relative), moduleSideEffects };
|
|
219
257
|
},
|
|
220
258
|
|
|
221
259
|
buildEnd() {
|
|
260
|
+
summariseSuppressed(this, suppressed);
|
|
261
|
+
suppressed = [];
|
|
222
262
|
// A dev server keeps its service for the whole session; a build is
|
|
223
263
|
// done with it here.
|
|
224
264
|
if (server == null) {
|
|
@@ -246,9 +286,19 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
246
286
|
service = null;
|
|
247
287
|
});
|
|
248
288
|
|
|
249
|
-
// A
|
|
289
|
+
// A reserved file appearing or disappearing changes the route table,
|
|
250
290
|
// which lives in a virtual module the watcher knows nothing about.
|
|
251
|
-
|
|
291
|
+
//
|
|
292
|
+
// Built from `RESERVED` rather than written out. It used to be the
|
|
293
|
+
// literal `(page|layout|middleware|not-found)`, which is a fourth
|
|
294
|
+
// spelling of a grammar that already has three, and it was already
|
|
295
|
+
// missing `route` — so adding a route handler to a running dev server
|
|
296
|
+
// did not rebuild the table and the handler stayed invisible until a
|
|
297
|
+
// restart. A list that has to match another list has to be that list.
|
|
298
|
+
const stems = Object.values(RESERVED)
|
|
299
|
+
.map((stem) => stem.replaceAll(".", "\\."))
|
|
300
|
+
.join("|");
|
|
301
|
+
const reserved = new RegExp(`/(${stems})(\\.[a-z]+)?\\.(js|jsx|mdx)$`);
|
|
252
302
|
const onRouteFile = (file) => {
|
|
253
303
|
if (!reserved.test(file) || !file.startsWith(appRoot)) return;
|
|
254
304
|
const routes = devServer.moduleGraph.getModuleById(resolved(VIRTUAL.routes));
|
|
@@ -272,6 +322,7 @@ function flowPlugin({ routerRoot, appEntry, command }) {
|
|
|
272
322
|
styles: [],
|
|
273
323
|
preloads: [],
|
|
274
324
|
});
|
|
325
|
+
if (result.error != null) reportRenderError(devServer, url, result.error);
|
|
275
326
|
const html = await devServer.transformIndexHtml(url, result.html);
|
|
276
327
|
response.statusCode = result.status;
|
|
277
328
|
response.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
@@ -344,6 +395,124 @@ function wantsDocument(request) {
|
|
|
344
395
|
return !/\.[a-z0-9]+$/i.test(pathname);
|
|
345
396
|
}
|
|
346
397
|
|
|
398
|
+
/** The name of the environment variable that turns every finding back on. */
|
|
399
|
+
const ALL_DIAGNOSTICS = "UF_REACT_COMPILER_DIAGNOSTICS";
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Report what the React Compiler said about one module.
|
|
403
|
+
*
|
|
404
|
+
* Every finding used to be printed as `a function: <message>` — no file, no
|
|
405
|
+
* line, no column, and the fallback string doing all the work because the
|
|
406
|
+
* compiler names an inner function about as often as not. The transform hook
|
|
407
|
+
* knows the module and the compiler gives a position for most findings, so
|
|
408
|
+
* both go into the message: Vite prints a plugin warning's `message` and
|
|
409
|
+
* nothing else, so a location that is not in the string is a location the
|
|
410
|
+
* reader never sees. `id` and `loc` go along for anything reading the log
|
|
411
|
+
* object rather than the line. See ubugeeei-prod/uf#307.
|
|
412
|
+
*
|
|
413
|
+
* A dependency's findings are held back. A React Compiler bailout inside
|
|
414
|
+
* `@uniflowed/form` is not something the person running the build can fix, and
|
|
415
|
+
* a channel carrying forty of them on every build is a channel people stop
|
|
416
|
+
* reading — which costs them the one finding that was theirs. They are counted
|
|
417
|
+
* and said once instead, and `UF_REACT_COMPILER_DIAGNOSTICS=all` prints every
|
|
418
|
+
* one for whoever is fixing the dependency.
|
|
419
|
+
*/
|
|
420
|
+
function reportDiagnostics(context, { id, root, diagnostics, environment, reported, suppressed }) {
|
|
421
|
+
if (diagnostics.length === 0) return;
|
|
422
|
+
// A module transformed again by the environment that first reported it has
|
|
423
|
+
// been edited; anything else is the second bundle passing over the same file.
|
|
424
|
+
const previous = reported.get(id);
|
|
425
|
+
const ledger =
|
|
426
|
+
previous != null && previous.environment !== environment
|
|
427
|
+
? previous
|
|
428
|
+
: { environment, signatures: new Set() };
|
|
429
|
+
reported.set(id, ledger);
|
|
430
|
+
|
|
431
|
+
const file = relativeId(root, id);
|
|
432
|
+
const mine = isProjectModule(root, id) || process.env[ALL_DIAGNOSTICS] === "all";
|
|
433
|
+
for (const diagnostic of diagnostics) {
|
|
434
|
+
// Everything a reader would be shown, so two findings that would print as
|
|
435
|
+
// the same line collapse into one. The compiler reports "Cannot access refs
|
|
436
|
+
// during render" once per pass that noticed it — three times for one `ref`
|
|
437
|
+
// — and three identical lines are not three things to fix.
|
|
438
|
+
const signature = `${diagnostic.kind}\0${diagnostic.line}\0${diagnostic.column}\0${diagnostic.message}`;
|
|
439
|
+
if (ledger.signatures.has(signature)) continue;
|
|
440
|
+
ledger.signatures.add(signature);
|
|
441
|
+
if (!mine) {
|
|
442
|
+
suppressed.push(file);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
// Two conventions, both honoured. uf's own frames count columns from one
|
|
446
|
+
// (`uf_term::diagnostic`) and so does every editor a reader will paste
|
|
447
|
+
// `file:line:column` into; Rollup's `loc.column` counts from zero, which is
|
|
448
|
+
// what the compiler already gave us. The string gets the reader's number
|
|
449
|
+
// and the log object gets Rollup's.
|
|
450
|
+
const at = diagnostic.line == null ? "" : `:${diagnostic.line}:${(diagnostic.column ?? 0) + 1}`;
|
|
451
|
+
const who = diagnostic.function == null ? "" : ` (in ${diagnostic.function})`;
|
|
452
|
+
context.warn?.({
|
|
453
|
+
message: `${file}${at}: ${diagnostic.message}${who}`,
|
|
454
|
+
id,
|
|
455
|
+
loc:
|
|
456
|
+
diagnostic.line == null
|
|
457
|
+
? undefined
|
|
458
|
+
: { file: id, line: diagnostic.line, column: diagnostic.column ?? 0 },
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Say how many findings were a dependency's, and whose.
|
|
465
|
+
*
|
|
466
|
+
* Held back is not the same as hidden: a build that quietly drops forty
|
|
467
|
+
* findings is a build that has decided for the reader that uf has no bugs. One
|
|
468
|
+
* line names the packages and how to see the rest.
|
|
469
|
+
*/
|
|
470
|
+
function summariseSuppressed(context, suppressed) {
|
|
471
|
+
if (suppressed.length === 0) return;
|
|
472
|
+
const packages = [...new Set(suppressed.map(packageOf))].sort();
|
|
473
|
+
const count = suppressed.length;
|
|
474
|
+
context.warn?.(
|
|
475
|
+
`${count} React Compiler ${count === 1 ? "finding" : "findings"} in ` +
|
|
476
|
+
`${packages.join(", ")} — not this application's to fix; ` +
|
|
477
|
+
`set ${ALL_DIAGNOSTICS}=all to see them`,
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Whether a finding about this module is the application author's to act on.
|
|
483
|
+
*
|
|
484
|
+
* Inside the project root *and* outside `node_modules`, rather than
|
|
485
|
+
* `node_modules` alone: uf's own packages reach an application through a
|
|
486
|
+
* workspace link in this repository and through `node_modules` everywhere
|
|
487
|
+
* else, and they are no more the reader's code in one case than the other.
|
|
488
|
+
*/
|
|
489
|
+
function isProjectModule(root, id) {
|
|
490
|
+
const relative = path.relative(root, id);
|
|
491
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) return false;
|
|
492
|
+
return !relative.split(path.sep).includes("node_modules");
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** A module's path as a reader would write it: relative, with forward slashes. */
|
|
496
|
+
function relativeId(root, id) {
|
|
497
|
+
const relative = path.relative(root, id);
|
|
498
|
+
return relative === "" ? id : relative.split(path.sep).join("/");
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** The package a module belongs to, for the one line that names them. */
|
|
502
|
+
function packageOf(file) {
|
|
503
|
+
const parts = file.split("/");
|
|
504
|
+
const at = parts.lastIndexOf("node_modules");
|
|
505
|
+
if (at !== -1) {
|
|
506
|
+
const scoped = parts[at + 1]?.startsWith("@");
|
|
507
|
+
return parts.slice(at + 1, at + (scoped ? 3 : 2)).join("/");
|
|
508
|
+
}
|
|
509
|
+
// No `node_modules` in the path: a workspace link, resolved to a checkout.
|
|
510
|
+
// The directory the module hangs off is the closest thing to a package name
|
|
511
|
+
// that is true without reading its `package.json` from a warning path.
|
|
512
|
+
const up = parts.lastIndexOf("packages");
|
|
513
|
+
return up === -1 ? parts.slice(0, -1).join("/") || file : parts.slice(up, up + 2).join("/");
|
|
514
|
+
}
|
|
515
|
+
|
|
347
516
|
function cleanId(id) {
|
|
348
517
|
const at = id.indexOf("?");
|
|
349
518
|
return at === -1 ? id : id.slice(0, at);
|
package/internal/events.js
CHANGED
|
@@ -51,6 +51,26 @@ export function stripAnsi(text) {
|
|
|
51
51
|
return text.replace(ANSI, "");
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Report a page that rendered its error boundary instead of itself.
|
|
56
|
+
*
|
|
57
|
+
* `uf dev` has two renderers — the plugin's middleware and the driver's — and
|
|
58
|
+
* this is the one place either of them says so, because a message written
|
|
59
|
+
* twice is a message that ends up saying two things. The document the browser
|
|
60
|
+
* gets is the application's error page, which is what a visitor would see;
|
|
61
|
+
* the exception belongs in the terminal, which is uf's.
|
|
62
|
+
*
|
|
63
|
+
* The stack is mapped back onto the Flow source first, so the frames name the
|
|
64
|
+
* file that was written rather than the one that was compiled.
|
|
65
|
+
*/
|
|
66
|
+
export function reportRenderError(server, url, error) {
|
|
67
|
+
if (error instanceof Error) {
|
|
68
|
+
server.ssrFixStacktrace(error);
|
|
69
|
+
}
|
|
70
|
+
const detail = error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
71
|
+
server.config.logger.error(`${url} rendered its error boundary\n${stripAnsi(detail)}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
54
74
|
/**
|
|
55
75
|
* Describe an error for the channel: message, and a location when Babel or
|
|
56
76
|
* Rolldown attached one.
|
package/internal/routes.js
CHANGED
|
@@ -23,6 +23,7 @@ export const RESERVED = Object.freeze({
|
|
|
23
23
|
page: "_uf.page",
|
|
24
24
|
middleware: "_uf.middleware",
|
|
25
25
|
notFound: "_uf.not-found",
|
|
26
|
+
error: "_uf.error",
|
|
26
27
|
route: "_uf.route",
|
|
27
28
|
});
|
|
28
29
|
|
|
@@ -56,6 +57,38 @@ const MAX_DEPTH = 32;
|
|
|
56
57
|
* @property {string} module absolute path of the handler module
|
|
57
58
|
*/
|
|
58
59
|
|
|
60
|
+
/**
|
|
61
|
+
* One not-found boundary — the page a path under `path` gets when nothing
|
|
62
|
+
* there matched.
|
|
63
|
+
*
|
|
64
|
+
* A `_uf.not-found.js` is a segment file like `_uf.layout.js`, so a directory
|
|
65
|
+
* declares the 404 for everything beneath it and the resolver takes the
|
|
66
|
+
* nearest one above the path. `layouts` are the layouts in scope *at that
|
|
67
|
+
* directory*, which is what wraps the boundary when it renders.
|
|
68
|
+
*
|
|
69
|
+
* @typedef {object} NotFoundBoundary
|
|
70
|
+
* @property {string} path route path of the directory that declares it
|
|
71
|
+
* @property {string} page absolute path of the page module
|
|
72
|
+
* @property {ReadonlyArray<string>} layouts absolute paths, root first
|
|
73
|
+
* @property {boolean} mdx whether the page is MDX content
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* One error boundary — what renders in place of the subtree under `path` when
|
|
78
|
+
* something in it throws.
|
|
79
|
+
*
|
|
80
|
+
* The same nearest-ancestor shape as a not-found boundary, and deliberately
|
|
81
|
+
* not the same extensions: an error module is handed an error and a `reset`,
|
|
82
|
+
* which is a component's contract. `.mdx` compiles to a component that takes
|
|
83
|
+
* no such thing, so a `_uf.error.mdx` would be a file the router loads and can
|
|
84
|
+
* never hand its arguments to.
|
|
85
|
+
*
|
|
86
|
+
* @typedef {object} ErrorBoundary
|
|
87
|
+
* @property {string} path route path of the directory that declares it
|
|
88
|
+
* @property {string} module absolute path of the error module
|
|
89
|
+
* @property {ReadonlyArray<string>} layouts absolute paths, root first
|
|
90
|
+
*/
|
|
91
|
+
|
|
59
92
|
/**
|
|
60
93
|
* Scan `appRoot` for routes.
|
|
61
94
|
*
|
|
@@ -64,13 +97,14 @@ const MAX_DEPTH = 32;
|
|
|
64
97
|
* library project has no router root, and that is not a mistake.
|
|
65
98
|
*
|
|
66
99
|
* @param {string} appRoot absolute path of the router root (`app/`)
|
|
67
|
-
* @returns {Route[]}
|
|
100
|
+
* @returns {{routes: Route[], handlers: Handler[], notFound: NotFoundBoundary[], errors: ErrorBoundary[]}}
|
|
68
101
|
*/
|
|
69
102
|
export function scanRoutes(appRoot) {
|
|
70
103
|
const routes = [];
|
|
71
104
|
const handlers = [];
|
|
72
|
-
|
|
73
|
-
|
|
105
|
+
const notFound = [];
|
|
106
|
+
const errors = [];
|
|
107
|
+
if (!isDirectory(appRoot)) return { routes, handlers, notFound, errors };
|
|
74
108
|
|
|
75
109
|
const walk = (directory, segments, layouts, middleware, depth) => {
|
|
76
110
|
if (depth > MAX_DEPTH) return;
|
|
@@ -105,9 +139,29 @@ export function scanRoutes(appRoot) {
|
|
|
105
139
|
handlers.push({ path: routePath, pattern, params, module: handler });
|
|
106
140
|
}
|
|
107
141
|
|
|
108
|
-
if (depth === 0)
|
|
109
|
-
|
|
110
|
-
|
|
142
|
+
// At every depth, not only the root. This read `if (depth === 0)`, so
|
|
143
|
+
// `app/guide/_uf.not-found.js` was never looked for and a reader who
|
|
144
|
+
// followed a stale link into the manual was answered by the site's root
|
|
145
|
+
// 404, outside the manual's own layout. See ubugeeei-prod/uf#263.
|
|
146
|
+
const ownNotFound = findModule(directory, RESERVED.notFound, PAGE_EXTENSIONS);
|
|
147
|
+
if (ownNotFound) {
|
|
148
|
+
notFound.push({
|
|
149
|
+
path: routeFromSegments(segments).path,
|
|
150
|
+
page: ownNotFound,
|
|
151
|
+
layouts: nextLayouts,
|
|
152
|
+
mdx: ownNotFound.endsWith(".mdx"),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// `errors` is the boundaries a project declares, not failures that
|
|
157
|
+
// happened: one entry per directory holding an `_uf.error.js`.
|
|
158
|
+
const ownError = findModule(directory, RESERVED.error, MODULE_EXTENSIONS);
|
|
159
|
+
if (ownError) {
|
|
160
|
+
errors.push({
|
|
161
|
+
path: routeFromSegments(segments).path,
|
|
162
|
+
module: ownError,
|
|
163
|
+
layouts: nextLayouts,
|
|
164
|
+
});
|
|
111
165
|
}
|
|
112
166
|
|
|
113
167
|
for (const entry of entries) {
|
|
@@ -129,7 +183,20 @@ export function scanRoutes(appRoot) {
|
|
|
129
183
|
const byPath = (a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
130
184
|
routes.sort(byPath);
|
|
131
185
|
handlers.sort(byPath);
|
|
132
|
-
|
|
186
|
+
// Sorted by path, not by which is nearest: the resolver picks the longest
|
|
187
|
+
// path that covers the URL, so it does not depend on this order, and sorting
|
|
188
|
+
// by nearness would hide that.
|
|
189
|
+
//
|
|
190
|
+
// Two boundaries can share a path, because a `(group)` directory is not a URL
|
|
191
|
+
// segment — `app/_uf.not-found.js` and `app/(marketing)/_uf.not-found.js` are
|
|
192
|
+
// both at `/`, and the URL cannot say which tree it is in. The sort is stable
|
|
193
|
+
// and `walk` records a directory's own boundary before descending, so the
|
|
194
|
+
// shallower file wins, which is the one that is the site's own 404 rather
|
|
195
|
+
// than one section's idea of it. Letting each group own a boundary needs the
|
|
196
|
+
// parallel-route trees uf does not have yet; see ubugeeei-prod/uf#267.
|
|
197
|
+
notFound.sort(byPath);
|
|
198
|
+
errors.sort(byPath);
|
|
199
|
+
return { routes, handlers, notFound, errors };
|
|
133
200
|
}
|
|
134
201
|
|
|
135
202
|
function isDirectory(candidate) {
|
|
@@ -195,7 +262,7 @@ export const VIRTUAL = Object.freeze({
|
|
|
195
262
|
* Layouts are deduplicated into one table so a layout shared by fifty routes
|
|
196
263
|
* is one dynamic import, not fifty.
|
|
197
264
|
*
|
|
198
|
-
* @param {{routes: Route[], notFound
|
|
265
|
+
* @param {{routes: Route[], handlers?: Handler[], notFound?: NotFoundBoundary[], errors?: ErrorBoundary[]}} table
|
|
199
266
|
*/
|
|
200
267
|
export function routesModuleSource(table) {
|
|
201
268
|
const layoutIds = new Map();
|
|
@@ -222,14 +289,32 @@ export function routesModuleSource(table) {
|
|
|
222
289
|
}`;
|
|
223
290
|
});
|
|
224
291
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
:
|
|
292
|
+
// A list, because a not-found is a segment file: every directory may declare
|
|
293
|
+
// one and the router takes the nearest above the path. `layoutId` is the
|
|
294
|
+
// same table the routes use, so a boundary that shares a layout with a page
|
|
295
|
+
// shares its dynamic import too.
|
|
296
|
+
const notFoundEntries = (table.notFound ?? []).map(
|
|
297
|
+
(boundary) => ` {
|
|
298
|
+
path: ${JSON.stringify(boundary.path)},
|
|
299
|
+
mdx: ${boundary.mdx},
|
|
300
|
+
file: ${JSON.stringify(boundary.page)},
|
|
301
|
+
page: () => import(${JSON.stringify(boundary.page)}),
|
|
302
|
+
layouts: [${boundary.layouts.map(layoutId).join(", ")}],
|
|
303
|
+
}`,
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
// An error boundary is loaded with the route it guards rather than when it
|
|
307
|
+
// is needed: React decides to render a boundary's fallback synchronously,
|
|
308
|
+
// during the render that threw, so a module that still has to be imported is
|
|
309
|
+
// a module that is not there when the only chance to use it arrives.
|
|
310
|
+
const errorEntries = (table.errors ?? []).map(
|
|
311
|
+
(boundary) => ` {
|
|
312
|
+
path: ${JSON.stringify(boundary.path)},
|
|
313
|
+
file: ${JSON.stringify(boundary.module)},
|
|
314
|
+
module: () => import(${JSON.stringify(boundary.module)}),
|
|
315
|
+
layouts: [${boundary.layouts.map(layoutId).join(", ")}],
|
|
316
|
+
}`,
|
|
317
|
+
);
|
|
233
318
|
|
|
234
319
|
// Handlers are a separate table because nothing on the client wants them:
|
|
235
320
|
// a route handler answers a request, so shipping its module to the browser
|
|
@@ -250,7 +335,12 @@ ${entries.join(",\n")}
|
|
|
250
335
|
export const handlers = [
|
|
251
336
|
${handlerEntries.join(",\n")}
|
|
252
337
|
];
|
|
253
|
-
export const notFound =
|
|
338
|
+
export const notFound = [
|
|
339
|
+
${notFoundEntries.join(",\n")}
|
|
340
|
+
];
|
|
341
|
+
export const errors = [
|
|
342
|
+
${errorEntries.join(",\n")}
|
|
343
|
+
];
|
|
254
344
|
export default routes;
|
|
255
345
|
`;
|
|
256
346
|
}
|
|
@@ -264,9 +354,9 @@ export default routes;
|
|
|
264
354
|
*/
|
|
265
355
|
export function clientModuleSource(appEntry) {
|
|
266
356
|
return `import { hydrate } from "@uniflowed/router/client";
|
|
267
|
-
import { routes, notFound } from ${JSON.stringify(VIRTUAL.routes)};
|
|
357
|
+
import { routes, notFound, errors } from ${JSON.stringify(VIRTUAL.routes)};
|
|
268
358
|
import App from ${JSON.stringify(appEntry)};
|
|
269
|
-
hydrate({ App, routes, notFound });
|
|
359
|
+
hydrate({ App, routes, notFound, errors });
|
|
270
360
|
`;
|
|
271
361
|
}
|
|
272
362
|
|
|
@@ -275,10 +365,10 @@ hydrate({ App, routes, notFound });
|
|
|
275
365
|
*/
|
|
276
366
|
export function serverModuleSource(appEntry) {
|
|
277
367
|
return `import { createDispatcher, createRenderer } from "@uniflowed/router/server";
|
|
278
|
-
import { routes, handlers, notFound } from ${JSON.stringify(VIRTUAL.routes)};
|
|
368
|
+
import { routes, handlers, notFound, errors } from ${JSON.stringify(VIRTUAL.routes)};
|
|
279
369
|
import App from ${JSON.stringify(appEntry)};
|
|
280
|
-
export { routes, handlers, notFound };
|
|
281
|
-
export const render = createRenderer({ App, routes, notFound });
|
|
370
|
+
export { routes, handlers, notFound, errors };
|
|
371
|
+
export const render = createRenderer({ App, routes, notFound, errors });
|
|
282
372
|
export const dispatch = createDispatcher({ handlers });
|
|
283
373
|
`;
|
|
284
374
|
}
|