@uniflowed/vite 0.0.0-alpha.1 → 0.0.0-alpha.10

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.
@@ -1,3 +1,5 @@
1
+ // @noflow
2
+ //
1
3
  // Plain JavaScript: executed by the host that runs Vite, before any transform.
2
4
  //
3
5
  // React Fast Refresh wiring for `uf dev`. The runtime itself is
@@ -1,3 +1,5 @@
1
+ // @noflow
2
+ //
1
3
  // Plain JavaScript: executed by the host that runs Vite, before any transform.
2
4
  //
3
5
  // The file-system router, as the build sees it.
@@ -21,6 +23,9 @@ export const RESERVED = Object.freeze({
21
23
  page: "_uf.page",
22
24
  middleware: "_uf.middleware",
23
25
  notFound: "_uf.not-found",
26
+ error: "_uf.error",
27
+ loading: "_uf.loading",
28
+ route: "_uf.route",
24
29
  });
25
30
 
26
31
  /** Extensions a page or layout may use; `.mdx` is a page written as content. */
@@ -39,10 +44,91 @@ const MAX_DEPTH = 32;
39
44
  * @property {ReadonlyArray<{name: string, catchAll: boolean}>} params
40
45
  * @property {string} page absolute path of the page module
41
46
  * @property {ReadonlyArray<string>} layouts absolute paths, root first
42
- * @property {ReadonlyArray<string>} middleware absolute paths, root first
47
+ * @property {ReadonlyArray<{above: number, module: string}>} loading the
48
+ * `<Suspense>` boundaries in scope, root first; `above` is how many of
49
+ * `layouts` are outside each one
43
50
  * @property {boolean} mdx whether the page is MDX content
44
51
  */
45
52
 
53
+ /**
54
+ * One middleware — everything under a directory, guarded before it answers.
55
+ *
56
+ * A flat table keyed by the directory's route path, rather than an array on
57
+ * every route the way layouts are accumulated. That was the first shape and it
58
+ * left two holes: `/dashboard/typo` matches no route, so a per-route array
59
+ * would have rendered the 404 with the guard skipped, and a route handler is
60
+ * in a table of its own, so guarding pages would have guarded half of them.
61
+ * The path is the matcher, so the path is what the table carries.
62
+ *
63
+ * @typedef {object} Middleware
64
+ * @property {string} path route path of the directory it guards, `/` at the root
65
+ * @property {string} module absolute path of the middleware module
66
+ */
67
+
68
+ /**
69
+ * One route handler — a path that answers a request instead of rendering.
70
+ *
71
+ * @typedef {object} Handler
72
+ * @property {string} path route path such as `/api/users/:id`
73
+ * @property {string} pattern the same path with `*` for catch-alls
74
+ * @property {ReadonlyArray<{name: string, catchAll: boolean}>} params
75
+ * @property {string} module absolute path of the handler module
76
+ */
77
+
78
+ /**
79
+ * One not-found boundary — the page a path under `path` gets when nothing
80
+ * there matched.
81
+ *
82
+ * A `_uf.not-found.js` is a segment file like `_uf.layout.js`, so a directory
83
+ * declares the 404 for everything beneath it and the resolver takes the
84
+ * nearest one above the path. `layouts` are the layouts in scope *at that
85
+ * directory*, which is what wraps the boundary when it renders.
86
+ *
87
+ * @typedef {object} NotFoundBoundary
88
+ * @property {string} path route path of the directory that declares it
89
+ * @property {string} page absolute path of the page module
90
+ * @property {ReadonlyArray<string>} layouts absolute paths, root first
91
+ * @property {boolean} mdx whether the page is MDX content
92
+ */
93
+
94
+ /**
95
+ * One error boundary — what renders in place of the subtree under `path` when
96
+ * something in it throws.
97
+ *
98
+ * The same nearest-ancestor shape as a not-found boundary, and deliberately
99
+ * not the same extensions: an error module is handed an error and a `reset`,
100
+ * which is a component's contract. `.mdx` compiles to a component that takes
101
+ * no such thing, so a `_uf.error.mdx` would be a file the router loads and can
102
+ * never hand its arguments to.
103
+ *
104
+ * @typedef {object} ErrorBoundary
105
+ * @property {string} path route path of the directory that declares it
106
+ * @property {string} module absolute path of the error module
107
+ * @property {ReadonlyArray<string>} layouts absolute paths, root first
108
+ */
109
+
110
+ /**
111
+ * One loading boundary — the fallback for the segment that declares it.
112
+ *
113
+ * Not the nearest-ancestor shape the other two boundaries have, and the
114
+ * difference is the whole of what a fallback is. A not-found or an error
115
+ * boundary is *chosen*: one of them renders, and the resolver picks the
116
+ * nearest above the path. Loading boundaries *nest*: `app/_uf.loading.js` and
117
+ * `app/docs/_uf.loading.js` are two `<Suspense>` elements on one route, one
118
+ * inside the other, and both are in the tree at once. So they accumulate down
119
+ * the walk the way layouts do rather than being matched afterwards, and each
120
+ * route carries the list that applies to it.
121
+ *
122
+ * `above` is the count of the route's `layouts` that sit outside the boundary
123
+ * — the layouts that render immediately, which is what "the shell around a
124
+ * slow page" means. It is the same number, spelled the same way, as
125
+ * `ResolvedRoute["errorBoundary"].above` in the router runtime.
126
+ *
127
+ * @typedef {object} LoadingBoundary
128
+ * @property {number} above how many of the route's layouts are outside it
129
+ * @property {string} module absolute path of the loading module
130
+ */
131
+
46
132
  /**
47
133
  * Scan `appRoot` for routes.
48
134
  *
@@ -51,23 +137,49 @@ const MAX_DEPTH = 32;
51
137
  * library project has no router root, and that is not a mistake.
52
138
  *
53
139
  * @param {string} appRoot absolute path of the router root (`app/`)
54
- * @returns {Route[]}
140
+ * @returns {{
141
+ * routes: Route[],
142
+ * handlers: Handler[],
143
+ * middleware: Middleware[],
144
+ * notFound: NotFoundBoundary[],
145
+ * errors: ErrorBoundary[],
146
+ * }}
55
147
  */
56
148
  export function scanRoutes(appRoot) {
57
149
  const routes = [];
58
- let notFound = null;
59
- if (!isDirectory(appRoot)) return { routes, notFound };
150
+ const handlers = [];
151
+ const middleware = [];
152
+ const notFound = [];
153
+ const errors = [];
154
+ if (!isDirectory(appRoot)) return { routes, handlers, middleware, notFound, errors };
60
155
 
61
- const walk = (directory, segments, layouts, middleware, depth) => {
156
+ const walk = (directory, segments, layouts, loading, depth) => {
62
157
  if (depth > MAX_DEPTH) return;
63
158
  const entries = readdirSync(directory, { withFileTypes: true }).sort((a, b) =>
64
159
  a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
65
160
  );
66
161
 
67
162
  const ownLayout = findModule(directory, RESERVED.layout, MODULE_EXTENSIONS);
68
- const ownMiddleware = findModule(directory, RESERVED.middleware, MODULE_EXTENSIONS);
69
163
  const nextLayouts = ownLayout ? [...layouts, ownLayout] : layouts;
70
- const nextMiddleware = ownMiddleware ? [...middleware, ownMiddleware] : middleware;
164
+
165
+ // Inside this directory's own layout, which is where Next.js puts it and
166
+ // the only placement that makes sense: the fallback is what shows *within*
167
+ // the frame this segment draws, so the frame has to be outside it.
168
+ // `nextLayouts.length` is therefore the count taken after the own layout is
169
+ // added, not before. A segment with a loading file and no layout of its own
170
+ // still gets a boundary — it just shares its parent's frame.
171
+ const ownLoading = findModule(directory, RESERVED.loading, MODULE_EXTENSIONS);
172
+ const nextLoading = ownLoading
173
+ ? [...loading, { above: nextLayouts.length, module: ownLoading }]
174
+ : loading;
175
+
176
+ // A middleware guards this directory and everything below it, whether or
177
+ // not this directory is itself a route: `app/dashboard/_uf.middleware.js`
178
+ // with no `_uf.page.js` beside it still guards `/dashboard/settings`.
179
+ const ownMiddleware = findModule(directory, RESERVED.middleware, MODULE_EXTENSIONS);
180
+ if (ownMiddleware) {
181
+ middleware.push({ path: routeFromSegments(segments).path, module: ownMiddleware });
182
+ }
71
183
 
72
184
  const page = findModule(directory, RESERVED.page, PAGE_EXTENSIONS);
73
185
  if (page) {
@@ -78,13 +190,42 @@ export function scanRoutes(appRoot) {
78
190
  params,
79
191
  page,
80
192
  layouts: nextLayouts,
81
- middleware: nextMiddleware,
193
+ loading: nextLoading,
82
194
  mdx: page.endsWith(".mdx"),
83
195
  });
84
196
  }
85
- if (depth === 0) {
86
- const own = findModule(directory, RESERVED.notFound, PAGE_EXTENSIONS);
87
- if (own) notFound = { page: own, layouts: nextLayouts, mdx: own.endsWith(".mdx") };
197
+ // A handler answers the request itself, so it takes no layouts and is not
198
+ // MDX. It may sit beside a page: `/feed` can render for a browser and
199
+ // `/feed.xml` answer for a reader, and both are the same directory tree.
200
+ const handler = findModule(directory, RESERVED.route, MODULE_EXTENSIONS);
201
+ if (handler) {
202
+ const { path: routePath, pattern, params } = routeFromSegments(segments);
203
+ handlers.push({ path: routePath, pattern, params, module: handler });
204
+ }
205
+
206
+ // At every depth, not only the root. This read `if (depth === 0)`, so
207
+ // `app/guide/_uf.not-found.js` was never looked for and a reader who
208
+ // followed a stale link into the manual was answered by the site's root
209
+ // 404, outside the manual's own layout. See ubugeeei-prod/uf#263.
210
+ const ownNotFound = findModule(directory, RESERVED.notFound, PAGE_EXTENSIONS);
211
+ if (ownNotFound) {
212
+ notFound.push({
213
+ path: routeFromSegments(segments).path,
214
+ page: ownNotFound,
215
+ layouts: nextLayouts,
216
+ mdx: ownNotFound.endsWith(".mdx"),
217
+ });
218
+ }
219
+
220
+ // `errors` is the boundaries a project declares, not failures that
221
+ // happened: one entry per directory holding an `_uf.error.js`.
222
+ const ownError = findModule(directory, RESERVED.error, MODULE_EXTENSIONS);
223
+ if (ownError) {
224
+ errors.push({
225
+ path: routeFromSegments(segments).path,
226
+ module: ownError,
227
+ layouts: nextLayouts,
228
+ });
88
229
  }
89
230
 
90
231
  for (const entry of entries) {
@@ -96,15 +237,34 @@ export function scanRoutes(appRoot) {
96
237
  path.join(directory, entry.name),
97
238
  [...segments, entry.name],
98
239
  nextLayouts,
99
- nextMiddleware,
240
+ nextLoading,
100
241
  depth + 1,
101
242
  );
102
243
  }
103
244
  };
104
245
 
105
246
  walk(appRoot, [], [], [], 0);
106
- routes.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
107
- return { routes, notFound };
247
+ const byPath = (a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
248
+ routes.sort(byPath);
249
+ handlers.sort(byPath);
250
+ // Sorted for a table that does not churn between builds, and for nothing
251
+ // else: `createMiddlewareRunner` re-orders the table root first, because
252
+ // what a chain of guards runs in is depth, not name.
253
+ middleware.sort(byPath);
254
+ // Sorted by path, not by which is nearest: the resolver picks the longest
255
+ // path that covers the URL, so it does not depend on this order, and sorting
256
+ // by nearness would hide that.
257
+ //
258
+ // Two boundaries can share a path, because a `(group)` directory is not a URL
259
+ // segment — `app/_uf.not-found.js` and `app/(marketing)/_uf.not-found.js` are
260
+ // both at `/`, and the URL cannot say which tree it is in. The sort is stable
261
+ // and `walk` records a directory's own boundary before descending, so the
262
+ // shallower file wins, which is the one that is the site's own 404 rather
263
+ // than one section's idea of it. Letting each group own a boundary needs the
264
+ // parallel-route trees uf does not have yet; see ubugeeei-prod/uf#267.
265
+ notFound.sort(byPath);
266
+ errors.sort(byPath);
267
+ return { routes, handlers, middleware, notFound, errors };
108
268
  }
109
269
 
110
270
  function isDirectory(candidate) {
@@ -168,11 +328,55 @@ export const VIRTUAL = Object.freeze({
168
328
  *
169
329
  * Each page and layout is a lazy `import()`, so a route is a chunk of its own.
170
330
  * Layouts are deduplicated into one table so a layout shared by fifty routes
171
- * is one dynamic import, not fifty.
331
+ * is one dynamic import, not fifty. Middleware needs no deduplication: it is
332
+ * already one entry per file, keyed by the path it guards.
333
+ *
334
+ * # The client's copy is not the server's
335
+ *
336
+ * `shipsPage` is how the server/client split reaches the bundle. A route it
337
+ * answers `false` for keeps its path and its parameters — the router still has
338
+ * to *match* the URL, so that a link into it can hand the navigation back to
339
+ * the browser — and loses its `page`, its `layouts` and its `loading`
340
+ * boundaries, which are the only `import()` calls in this table. Nothing in
341
+ * the browser can then reach the module through the router, so Rollup emits no
342
+ * chunk for it and none for anything only it reached.
343
+ *
344
+ * Omitted by leaving the key out rather than by writing `page: null`, because
345
+ * the two say different things to a bundler: a property whose value is an
346
+ * `import()` is a chunk whether or not anything reads it.
347
+ *
348
+ * # Except for its styles
349
+ *
350
+ * A route that ships no JavaScript still has to *look* right, and a uf build
351
+ * takes its stylesheets from the client graph: `assetsFromManifest` walks the
352
+ * client entry's imports and links the CSS it finds, so a module removed from
353
+ * that graph takes its rules out of every page in the site. That is a silent
354
+ * visual break, and it is worse than shipping the module.
355
+ *
356
+ * So each module a dropped route was the only reader of comes back at the top
357
+ * of this file as a bare `import <file>;` — a side-effect import, with no
358
+ * binding read from it. Its stylesheet is a side effect and survives; its
359
+ * components, its helpers and everything only they referenced are unused
360
+ * exports and do not. A layout a *kept* route still uses is left out of that
361
+ * list: it is already here as a lazy import, and a static one as well would
362
+ * pull it into the entry chunk.
172
363
  *
173
- * @param {{routes: Route[], notFound: object | null}} table
364
+ * The default answers `true` for every route, which is the whole table, no
365
+ * side-effect imports, and exactly what this emitted before the split existed.
366
+ * `virtual:uf/server` is generated with the default and always will be: the
367
+ * server renders every route, so its table is the complete one.
368
+ *
369
+ * @param {{
370
+ * routes: Route[],
371
+ * handlers?: Handler[],
372
+ * middleware?: Middleware[],
373
+ * notFound?: NotFoundBoundary[],
374
+ * errors?: ErrorBoundary[],
375
+ * }} table
376
+ * @param {{shipsPage?: (route: Route) => boolean}} [options]
174
377
  */
175
- export function routesModuleSource(table) {
378
+ export function routesModuleSource(table, options = {}) {
379
+ const shipsPage = options.shipsPage ?? (() => true);
176
380
  const layoutIds = new Map();
177
381
  const layoutImports = [];
178
382
  const layoutId = (file) => {
@@ -185,8 +389,44 @@ export function routesModuleSource(table) {
185
389
  return id;
186
390
  };
187
391
 
392
+ // Loading modules are deduplicated into a table of their own, for the reason
393
+ // layouts are: one `app/_uf.loading.js` is the fallback of every route under
394
+ // it, and fifty copies of the same `import()` would be fifty chunks of the
395
+ // same file.
396
+ //
397
+ // They are static imports rather than lazy ones, and that is not an
398
+ // oversight. React decides to show a fallback *synchronously*, during the
399
+ // render that suspended, so a fallback still waiting on its own `import()` is
400
+ // a fallback that is not there at the only moment it is wanted — the same
401
+ // reasoning as the error boundaries below, arrived at from the other
402
+ // direction. `resolveMatch` awaits them with the layouts, before it renders.
403
+ const loadingIds = new Map();
404
+ const loadingImports = [];
405
+ const loadingId = (file) => {
406
+ let id = loadingIds.get(file);
407
+ if (id === undefined) {
408
+ id = `loading${loadingIds.size}`;
409
+ loadingIds.set(file, id);
410
+ loadingImports.push(`const ${id} = () => import(${JSON.stringify(file)});`);
411
+ }
412
+ return id;
413
+ };
414
+
188
415
  const entries = table.routes.map((route) => {
416
+ if (!shipsPage(route)) {
417
+ return ` {
418
+ path: ${JSON.stringify(route.path)},
419
+ params: ${JSON.stringify(route.params)},
420
+ mdx: ${route.mdx},
421
+ file: ${JSON.stringify(route.page)},
422
+ layouts: [],
423
+ loading: [],
424
+ }`;
425
+ }
189
426
  const layouts = route.layouts.map(layoutId);
427
+ const loading = (route.loading ?? []).map(
428
+ (boundary) => `{ above: ${boundary.above}, module: ${loadingId(boundary.module)} }`,
429
+ );
190
430
  return ` {
191
431
  path: ${JSON.stringify(route.path)},
192
432
  params: ${JSON.stringify(route.params)},
@@ -194,23 +434,97 @@ export function routesModuleSource(table) {
194
434
  file: ${JSON.stringify(route.page)},
195
435
  page: () => import(${JSON.stringify(route.page)}),
196
436
  layouts: [${layouts.join(", ")}],
437
+ loading: [${loading.join(", ")}],
197
438
  }`;
198
439
  });
199
440
 
200
- const notFound = table.notFound
201
- ? `{
202
- mdx: ${table.notFound.mdx},
203
- file: ${JSON.stringify(table.notFound.page)},
204
- page: () => import(${JSON.stringify(table.notFound.page)}),
205
- layouts: [${table.notFound.layouts.map(layoutId).join(", ")}],
206
- }`
207
- : "null";
441
+ // A list, because a not-found is a segment file: every directory may declare
442
+ // one and the router takes the nearest above the path. `layoutId` is the
443
+ // same table the routes use, so a boundary that shares a layout with a page
444
+ // shares its dynamic import too.
445
+ const notFoundEntries = (table.notFound ?? []).map(
446
+ (boundary) => ` {
447
+ path: ${JSON.stringify(boundary.path)},
448
+ mdx: ${boundary.mdx},
449
+ file: ${JSON.stringify(boundary.page)},
450
+ page: () => import(${JSON.stringify(boundary.page)}),
451
+ layouts: [${boundary.layouts.map(layoutId).join(", ")}],
452
+ }`,
453
+ );
454
+
455
+ // An error boundary is loaded with the route it guards rather than when it
456
+ // is needed: React decides to render a boundary's fallback synchronously,
457
+ // during the render that threw, so a module that still has to be imported is
458
+ // a module that is not there when the only chance to use it arrives.
459
+ const errorEntries = (table.errors ?? []).map(
460
+ (boundary) => ` {
461
+ path: ${JSON.stringify(boundary.path)},
462
+ file: ${JSON.stringify(boundary.module)},
463
+ module: () => import(${JSON.stringify(boundary.module)}),
464
+ layouts: [${boundary.layouts.map(layoutId).join(", ")}],
465
+ }`,
466
+ );
467
+
468
+ // Handlers are a separate table because nothing on the client wants them:
469
+ // a route handler answers a request, so shipping its module to the browser
470
+ // would ship server code to the page.
471
+ const handlerEntries = (table.handlers ?? []).map(
472
+ (handler) => ` {
473
+ path: ${JSON.stringify(handler.path)},
474
+ params: ${JSON.stringify(handler.params)},
475
+ file: ${JSON.stringify(handler.module)},
476
+ load: () => import(${JSON.stringify(handler.module)}),
477
+ }`,
478
+ );
479
+
480
+ // Middleware is a table of its own for the same reason, and for a stronger
481
+ // one: it is where an application puts the check it does not want a user to
482
+ // read. `clientModuleSource` imports `routes`, `notFound` and `errors` and
483
+ // nothing else, so a middleware module is reachable from the server entry
484
+ // alone.
485
+ const middlewareEntries = (table.middleware ?? []).map(
486
+ (entry) => ` {
487
+ path: ${JSON.stringify(entry.path)},
488
+ file: ${JSON.stringify(entry.module)},
489
+ load: () => import(${JSON.stringify(entry.module)}),
490
+ }`,
491
+ );
492
+
493
+ // Last, because it is defined by what everything above did *not* import: a
494
+ // layout a kept route also uses is already in the graph as a lazy chunk, and
495
+ // importing it here as well would pull it into the entry chunk instead.
496
+ const carried = new Set([...layoutIds.keys(), ...loadingIds.keys()]);
497
+ const styleOnlyImports = [];
498
+ for (const route of table.routes) {
499
+ if (shipsPage(route)) {
500
+ continue;
501
+ }
502
+ const files = [route.page, ...route.layouts, ...(route.loading ?? []).map((it) => it.module)];
503
+ for (const file of files) {
504
+ if (carried.has(file)) {
505
+ continue;
506
+ }
507
+ carried.add(file);
508
+ styleOnlyImports.push(`import ${JSON.stringify(file)};`);
509
+ }
510
+ }
208
511
 
209
- return `${layoutImports.join("\n")}
512
+ return `${[...styleOnlyImports, ...layoutImports, ...loadingImports].join("\n")}
210
513
  export const routes = [
211
514
  ${entries.join(",\n")}
212
515
  ];
213
- export const notFound = ${notFound};
516
+ export const handlers = [
517
+ ${handlerEntries.join(",\n")}
518
+ ];
519
+ export const middleware = [
520
+ ${middlewareEntries.join(",\n")}
521
+ ];
522
+ export const notFound = [
523
+ ${notFoundEntries.join(",\n")}
524
+ ];
525
+ export const errors = [
526
+ ${errorEntries.join(",\n")}
527
+ ];
214
528
  export default routes;
215
529
  `;
216
530
  }
@@ -224,20 +538,63 @@ export default routes;
224
538
  */
225
539
  export function clientModuleSource(appEntry) {
226
540
  return `import { hydrate } from "@uniflowed/router/client";
227
- import { routes, notFound } from ${JSON.stringify(VIRTUAL.routes)};
541
+ import { routes, notFound, errors } from ${JSON.stringify(VIRTUAL.routes)};
228
542
  import App from ${JSON.stringify(appEntry)};
229
- hydrate({ App, routes, notFound });
543
+ hydrate({ App, routes, notFound, errors });
230
544
  `;
231
545
  }
232
546
 
233
547
  /**
234
- * The source of `virtual:uf/server`: render one URL to HTML.
548
+ * The source of `virtual:uf/server`: answer one request.
549
+ *
550
+ * Three exports, and the order a host calls them in is the whole of how the
551
+ * two halves of the table compose. `runMiddleware` first, because a middleware
552
+ * guards a *path* — it has to run for a page, for a route handler, and for a
553
+ * path under it that matches neither, so it belongs above route resolution
554
+ * rather than inside it. `notFound` and `errors` go the other way: they are
555
+ * boundaries chosen *during* a render, once resolution knows which route was
556
+ * asked for and whether it threw, which is why they are `createRenderer`'s
557
+ * arguments and not a step of their own. The two never compete for the same
558
+ * request — one decides whether the router is reached at all, the others
559
+ * decide what the router renders when it is.
560
+ *
561
+ * `internal/serve.js` and `driver.js` call them in that order, and
562
+ * `packages/vite/index.js` does the same for a project driving Vite itself.
563
+ *
564
+ * `render` and `prerender` are two exports rather than one with a flag, because
565
+ * a host is one or the other: a server streams, a build writes files. See the
566
+ * header of `packages/router/server.js` for why React needs both told apart.
567
+ *
568
+ * `beginRequest` is the fourth, and it is re-exported rather than imported by
569
+ * the host for a reason that is easy to get wrong: `@uniflowed/server` keeps
570
+ * the request in an `AsyncLocalStorage` held by *its module*, and a bundled
571
+ * application has its own copy of that module inlined. A host that imported
572
+ * `beginRequest` from its own `node_modules` would establish a request in a
573
+ * second storage, and every `cookies()` in the application would still be
574
+ * outside one. So the bundle hands the host the entry point that belongs to
575
+ * the bundle. `uf preview`, `uf start`, `uf dev` and the compiled binary all
576
+ * take it from here; see ubugeeei-prod/uf#389.
577
+ *
578
+ * Through `@uniflowed/router/server` rather than `@uniflowed/server/host`,
579
+ * because this source is resolved from the *project's* directory and a project
580
+ * depends on the router, not on the router's own dependency. It is also the
581
+ * shorter proof of the paragraph above: the copy the router dispatches and
582
+ * renders with is by construction the copy the host is handed.
235
583
  */
236
584
  export function serverModuleSource(appEntry) {
237
- return `import { createRenderer } from "@uniflowed/router/server";
238
- import { routes, notFound } from ${JSON.stringify(VIRTUAL.routes)};
585
+ return `import {
586
+ createDispatcher,
587
+ createMiddlewareRunner,
588
+ createRenderer,
589
+ } from "@uniflowed/router/server";
590
+ import { routes, handlers, middleware, notFound, errors } from ${JSON.stringify(VIRTUAL.routes)};
239
591
  import App from ${JSON.stringify(appEntry)};
240
- export { routes, notFound };
241
- export const render = createRenderer({ App, routes, notFound });
592
+ export { routes, handlers, middleware, notFound, errors };
593
+ export { beginRequest } from "@uniflowed/router/server";
594
+ const renderer = createRenderer({ App, routes, notFound, errors });
595
+ export const render = renderer.render;
596
+ export const prerender = renderer.prerender;
597
+ export const dispatch = createDispatcher({ handlers });
598
+ export const runMiddleware = createMiddlewareRunner({ middleware });
242
599
  `;
243
600
  }