@rivetjs/router 0.1.0-alpha.0 → 0.1.0-alpha.1

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.
Files changed (49) hide show
  1. package/adapters/files.d.ts +13 -0
  2. package/adapters/files.js +58 -6
  3. package/adapters/node.d.ts +22 -5
  4. package/adapters/node.js +113 -67
  5. package/adapters/static.js +1 -1
  6. package/adapters/target-build.d.ts +5 -2
  7. package/adapters/vercel.d.ts +4 -2
  8. package/adapters/vercel.js +1 -1
  9. package/build/codegen.d.ts +13 -0
  10. package/build/codegen.js +152 -35
  11. package/build/dev-errors.d.ts +6 -2
  12. package/build/dev-errors.js +15 -4
  13. package/build/lazy-rows.d.ts +19 -0
  14. package/build/lazy-rows.js +59 -0
  15. package/build/migrate.d.ts +20 -4
  16. package/build/migrate.js +300 -61
  17. package/build/reach.d.ts +60 -0
  18. package/build/reach.js +302 -0
  19. package/build/routes.d.ts +9 -4
  20. package/build/routes.js +30 -13
  21. package/build/vite.d.ts +13 -0
  22. package/build/vite.js +165 -7
  23. package/cli/rivet.js +3 -2
  24. package/client/dev.js +17 -0
  25. package/client/index.d.ts +4 -0
  26. package/client/index.js +34 -0
  27. package/client/route-context.d.ts +1 -1
  28. package/index.d.ts +22 -25
  29. package/index.js +2 -1
  30. package/package.json +4 -4
  31. package/server/app.d.ts +7 -1
  32. package/server/app.js +38 -5
  33. package/server/deferred.d.ts +12 -0
  34. package/server/deferred.js +60 -0
  35. package/server/handler.d.ts +56 -12
  36. package/server/handler.js +46 -8
  37. package/server/index.d.ts +1 -1
  38. package/server/module.d.ts +7 -7
  39. package/server/module.js +52 -34
  40. package/server/render.d.ts +2 -0
  41. package/server/render.js +3 -0
  42. package/server/route-cache.d.ts +74 -4
  43. package/server/route-cache.js +77 -6
  44. package/server/shell-build.d.ts +3 -0
  45. package/server/shell-build.js +2 -0
  46. package/server/shell-cache.d.ts +8 -0
  47. package/server/shell-cache.js +836 -0
  48. package/shared/server-exports.d.ts +78 -0
  49. package/shared/server-exports.js +77 -0
package/build/codegen.js CHANGED
@@ -122,6 +122,40 @@ function wrap(providers, inner, indent) {
122
122
  ...providers.map((_, n) => pad(providers.length - 1 - n) + "</Provider>"),
123
123
  ];
124
124
  }
125
+ /**
126
+ * Derived resume model (`CodegenOptions.derived`): `app.tsx`'s default export, the page's independent root boundary. Server:
127
+ * the App with the request's router (`ServerData` comes from the server entry). Browser (its adopt run): the browser
128
+ * router, the payload as `Revived`, and the contexts the server entry provides (navigation state, search params) as the
129
+ * browser's; `ready` loads the page modules of the URL first, so the adopt run renders the route synchronously.
130
+ */
131
+ function rootComponent(transitions, search) {
132
+ const providers = [
133
+ ...(transitions
134
+ ? [["{/* useNavigation(): the browser's navigation signal. */}", "<Provider token={Navigation} value={navigation}>"]]
135
+ : []),
136
+ ...(search ? [["{/* useSearchParams(): the browser's. */}", "<Provider token={SearchParams} value={searchParams}>"]] : []),
137
+ ];
138
+ return [
139
+ "",
140
+ "/** Derived resume model: the page root (independent boundary: its props are the route payload as data). */",
141
+ "export default function Root(props: { s: RootData }) {",
142
+ " if (import.meta.env.SSR) return <App router={serverRouter(useContext(ServerData)!.url)} />;",
143
+ " if (!adopting) ((adopting = true), queueMicrotask(() => (adopting = false)));",
144
+ " const s = props.s,",
145
+ " router = browserRouter(),",
146
+ " data = s.d.slice();",
147
+ " for (const i of s.n ?? []) data[i] = undefined;",
148
+ " const server = { data, url: untrack(router.location), ...(s.e ? { error: s.e } : {}), ...(s.a ? { action: s.a } : {}), ...(s.x ? { byURL: s.x } : {}), ...(s.k ? { deps: s.k } : {}) } as ServerRoutePayload;",
149
+ " return (",
150
+ " <Provider token={Revived} value={server}>",
151
+ ...wrap(providers, "<App router={router} />", " "),
152
+ " </Provider>",
153
+ " );",
154
+ "}",
155
+ "/** Before the adopt run: the page modules of the shown URL (a lazy page renders synchronously once loaded). */",
156
+ "Root.ready = (): Promise<unknown> | undefined => loadPage(new URL(globalThis.location.href));",
157
+ ];
158
+ }
125
159
  /** Generate the app, server entry, route table and route types for a scanned table. */
126
160
  export function generate(table, options) {
127
161
  const outDir = options.outDir ?? ".rivet";
@@ -152,6 +186,9 @@ export function generate(table, options) {
152
186
  const active = options.activeLinks === true;
153
187
  const slash = options.trailingSlash === "never" || options.trailingSlash === "always" ? options.trailingSlash : null;
154
188
  const swap = options.announce === false ? "swapRouteQuiet" : "swapRoute";
189
+ const derived = options.derived === true && mode !== "csr";
190
+ // Derived model: the Routers swap through `adoptSwap` (the adopt task's route renders are the page load, not a navigation).
191
+ const swapProp = derived ? "adoptSwap" : swap;
155
192
  const routesDir = options.routesDir.replace(/\/$/, "");
156
193
  const fromOut = (path) => {
157
194
  const relative = posix.relative(outDir, path);
@@ -246,7 +283,7 @@ export function generate(table, options) {
246
283
  const enterView = (key) => {
247
284
  const [k, i, at] = key.split(":").map(Number);
248
285
  const name = layouts.get(layoutModulesList[i].page);
249
- const inner = `<div data-rivet-outlet="${k + 1}"><Router router={_level(router, nest, ${k + 1}, t)} routes={routes${i}} swap={${swap}} hold={() => heldRoutes(${k + 1})} notFound={() => null} /></div>`;
286
+ const inner = `<div data-rivet-outlet="${k + 1}"><Router router={_level(router, nest, ${k + 1}, t)} routes={routes${i}} swap={${swapProp}} hold={() => heldRoutes(${k + 1})} notFound={() => null} /></div>`;
250
287
  const view = level(at, `<${name} data={${slot(i)}.d} params={${slot(i)}.p} url={${slot(i)}.u}>${inner}</${name}>`);
251
288
  return `_enter(${k}, result, url, (t) => ${params ? `<Provider token={RouteParamsContext} value={${slot(i)}.p}>${view}</Provider>` : view})`;
252
289
  };
@@ -309,7 +346,9 @@ export function generate(table, options) {
309
346
  const usesRow = pageRows || fails.length > 1 || enters.length > 1;
310
347
  // Server: the same chain on the handler's data, synchronously (`_ssrLevel`; undefined in the browser).
311
348
  const land = useNest ? `(result) => _landed(nest, NEST, ${k}, ROUTES${name}[n]![0], result, match)` : "(result) => result";
312
- const served = mode === "csr" ? "" : `_ssrLevel(work, (data) => loaded(${kit ? `applyRouteHead(data, ROUTES${name}[n]![1])` : "data"}), failed, ${land}) ?? `;
349
+ const ok = `(data) => loaded(${kit ? `applyRouteHead(data, ROUTES${name}[n]![1])` : "data"})`;
350
+ // Derived model: the browser's adopt run answers each level's first load from the revived payload (`Root`).
351
+ const served = mode === "csr" ? "" : `_ssrLevel(work, ${ok}, failed, ${land}) ?? ${derived ? `revived(work, ${ok}, failed, ${land}) ?? ` : ""}`;
313
352
  return [
314
353
  ` const routes${name} = {`,
315
354
  ` paths: PATHS${name},`,
@@ -329,6 +368,9 @@ export function generate(table, options) {
329
368
  // Endpoint routes (no page) before a dynamic page pattern, as [index in the client's match order, pattern]:
330
369
  // the page pattern alone would claim their URLs for client navigation (the server answers the endpoint).
331
370
  const shadowed = [];
371
+ // Document routes (server reach): their rows in the client's match table (top-level `PATHS` index, before the splices).
372
+ const documentFiles = new Set(options.documents ?? []);
373
+ const documentRows = pageRoutes.flatMap((route, n) => (documentFiles.has(route.page) ? [n] : []));
332
374
  table.routes.forEach((route, i) => {
333
375
  if (!route.page && table.routes.slice(i + 1).some((later) => later.page && /[:*]/.test(later.pattern) && overlaps(later.pattern, route.pattern)))
334
376
  shadowed.push([table.routes.slice(0, i).filter((before) => before.page).length + shadowed.length, at(route.pattern)]);
@@ -346,16 +388,25 @@ export function generate(table, options) {
346
388
  const app = [
347
389
  HEADER,
348
390
  `/// <reference types="vite/client" />`,
349
- ...(useNest
350
- ? [`import { signal, useContext } from "@rivetjs/reactivity";`]
351
- : pageRoutes.length && params
352
- ? [`import { signal } from "@rivetjs/reactivity";`]
353
- : []),
354
- `import { Router, defineRoute, lazyComponent${single && !go ? "" : ", clientEvent"}${kit || (pageRoutes.length && params) ? ", Provider" : ""} } from "@rivetjs/runtime/dom";`,
391
+ ...(derived
392
+ ? [`import { ${useNest || (pageRoutes.length && params) ? "signal, " : ""}createContext, untrack, useContext, type Context } from "@rivetjs/reactivity";`]
393
+ : useNest
394
+ ? [`import { signal, useContext } from "@rivetjs/reactivity";`]
395
+ : pageRoutes.length && params
396
+ ? [`import { signal } from "@rivetjs/reactivity";`]
397
+ : []),
398
+ `import { Router, defineRoute, lazyComponent${single && !go ? "" : ", clientEvent"}${kit || (pageRoutes.length && params) || derived ? ", Provider" : ""} } from "@rivetjs/runtime/dom";`,
355
399
  ...(kit ? [`import { HeadLevel, applyRouteHead } from "@rivetjs/kit";`] : []),
356
- `import type { BrowserRouter } from "@rivetjs/runtime/router/dom";`,
357
- `import { routeData, ${mode === "csr" ? "" : "_ssrLevel, "}${swap}, heldRoutes, preloadPage, ${single ? "" : "interceptClick, notePopstate, "}${go ? (live.length ? "_serveLive, _live, _livePage, _liveIntent, " + (liveTop ? "_liveRouter, _armLive, " : nested ? "_liveLevels, " : "") : "_serve, ") : ""}${pageRoutes.length && params ? "RouteParamsContext, " : ""}${mode === "csr" || single ? "" : "prefetchData, "}${single ? "" : "prefetchModule, "}loadModule${single ? "" : ", routableAnchor"}${useNest ? ", ServerData, _layouts, _landed" + (keeps ? ", _keepIds" : "") + (nested ? ", _level, _enter, _take, _sync" : "") + ", type LayoutTable" : ""}${transitions ? ", _routeModule" : ""}${active ? ", onNavigate, _markActive" : ""}${nested || active ? ", _arm" : ""}${shadowed.length ? ", _endpoint" : ""}${forms && !single ? ", _brand, _go" : ""} } from "@rivetjs/router/client";`,
358
- `import { isHttpError } from "@rivetjs/router";`,
400
+ ...(derived
401
+ ? [
402
+ `import { createRouter as browserRouter, type BrowserRouter } from "@rivetjs/runtime/router/dom";`,
403
+ `import { createRouter as serverRouter } from "@rivetjs/runtime/router/server";`,
404
+ ...(transitions ? [`import { Navigation, navigation } from "@rivetjs/router/navigation";`] : []),
405
+ ...(search ? [`import { SearchParams, searchParams } from "@rivetjs/router/search";`] : []),
406
+ ]
407
+ : [`import type { BrowserRouter } from "@rivetjs/runtime/router/dom";`]),
408
+ `import { routeData, ${mode === "csr" ? "" : "_ssrLevel, "}${swap}, heldRoutes, preloadPage, ${single ? "" : "interceptClick, notePopstate, "}${go ? (live.length ? "_serveLive, _live, _livePage, _liveIntent, " + (liveTop ? "_liveRouter, _armLive, " : nested ? "_liveLevels, " : "") : "_serve, ") : ""}${pageRoutes.length && params ? "RouteParamsContext, " : ""}${mode === "csr" || single ? "" : "prefetchData, "}${single ? "" : "prefetchModule, "}loadModule${single ? "" : ", routableAnchor"}${useNest || derived ? ", ServerData" : ""}${derived ? ", type ServerRoutePayload" : ""}${useNest ? ", _layouts, _landed" + (keeps ? ", _keepIds" : "") + (nested ? ", _level, _enter, _take, _sync" : "") + ", type LayoutTable" : ""}${transitions ? ", _routeModule" : ""}${active ? ", onNavigate, _markActive" : ""}${nested || active ? ", _arm" : ""}${shadowed.length || documentRows.length ? ", _endpoint" : ""}${forms && !single ? ", _brand, _go" : ""} } from "@rivetjs/router/client";`,
409
+ `import { ${derived ? "HttpError, " : ""}isHttpError } from "@rivetjs/router";`,
359
410
  "",
360
411
  "// Route modules as this file renders them (strict-safe whatever props they declare): data comes",
361
412
  "// from loaders (untyped here), layout props are accessors.",
@@ -386,11 +437,21 @@ export function generate(table, options) {
386
437
  `const PATHS${table.name}: readonly string[] = ${str(table.routes.map((route) => at(route.pattern)))};`,
387
438
  `const ROUTES${table.name}: readonly Row[] = ${tableRows(table)};`,
388
439
  ]),
389
- ...(shadowed.length
440
+ ...(shadowed.length || documentRows.length
390
441
  ? [
391
442
  "const modules: (readonly [string, () => unknown])[] = PATHS.map((path, n) => [path, () => preloadPage(page(ROUTES[n]![2]), PAGES[ROUTES[n]![2]]!)]);",
392
- "// Endpoints the server matches before a dynamic page pattern that also fits them: the browser loads them.",
393
- `for (const [at, path] of ${str(shadowed)} as const) modules.splice(at, 0, [path, _endpoint]);`,
443
+ ...(documentRows.length
444
+ ? [
445
+ "// Document routes (their browser graph reaches a server-only module): the browser loads their documents.",
446
+ `for (const n of ${str(documentRows)} as const) modules[n] = [PATHS[n]!, _endpoint];`,
447
+ ]
448
+ : []),
449
+ ...(shadowed.length
450
+ ? [
451
+ "// Endpoints the server matches before a dynamic page pattern that also fits them: the browser loads them.",
452
+ `for (const [at, path] of ${str(shadowed)} as const) modules.splice(at, 0, [path, _endpoint]);`,
453
+ ]
454
+ : []),
394
455
  ]
395
456
  : [
396
457
  "const modules: readonly (readonly [string, () => unknown])[] = PATHS.map((path, n) => [path, () => preloadPage(page(ROUTES[n]![2]), PAGES[ROUTES[n]![2]]!)]);",
@@ -421,6 +482,34 @@ export function generate(table, options) {
421
482
  ' if (import.meta.hot && typeof document !== "undefined" && !isHttpError(cause) && (cause as Error | undefined)?.name !== "AbortError") _devError(cause);',
422
483
  ' return { data: [], failure: cause ?? new Error("Route failed") };',
423
484
  "};",
485
+ ...(derived
486
+ ? [
487
+ "",
488
+ "// Derived resume model: `Root`'s props carry the route payload as data (the page's independent root boundary). In the",
489
+ "// browser its adopt run provides it as `Revived`; the Router levels' loads of that adopt (one synchronous task: the",
490
+ "// probe and adopt runs of every unit) answer from it (the server's chain on the same data, as `_ssrLevel`); once the",
491
+ "// task is over it is spent and every load (navigation, revalidation) fetches.",
492
+ "type RootData = { d: unknown[]; n?: readonly number[]; e?: { readonly status: number; readonly message: string }; a?: unknown; x?: readonly number[]; k?: unknown };",
493
+ "const Revived = createContext<ServerRoutePayload | null>(null);",
494
+ "const spent = new WeakMap<object, boolean>();",
495
+ "const revived = <R, T>(work: { get<V>(token: Context<V>): V }, ok: (data: ServerRoutePayload) => R, fail: (cause: unknown) => R, land: (result: R) => T): T | undefined => {",
496
+ " if (import.meta.env.SSR) return undefined;",
497
+ " const server = work.get(Revived);",
498
+ " if (!server || spent.get(server)) return undefined;",
499
+ " if (!spent.has(server)) (spent.set(server, false), queueMicrotask(() => spent.set(server, true)));",
500
+ " let result: R;",
501
+ " try {",
502
+ " result = server.error ? fail(new HttpError(server.error.status, server.error.message)) : ok(server);",
503
+ " } catch (cause) {",
504
+ " result = fail(cause);",
505
+ " }",
506
+ " return land(result);",
507
+ "};",
508
+ "// The adopt task's Router renders (probe + adopt runs of `Root`) are the page load: no scroll / focus / announce.",
509
+ "let adopting = false;",
510
+ `const adoptSwap = (apply: () => void, next: ParentNode): void => (adopting ? apply() : ${swap}(apply, next));`,
511
+ ]
512
+ : []),
424
513
  "",
425
514
  "/** File-routed application: layouts wrap pages; client navigation fetches route data from the server. */",
426
515
  "export function App({ router }: { router: AppRouter }) {",
@@ -428,7 +517,7 @@ export function generate(table, options) {
428
517
  ? [
429
518
  " // Layout props are App-level signals (resume state carries them): the request's data on the",
430
519
  " // server, then each navigation's; kept layouts keep theirs. Level keys drive the nested Routers.",
431
- " const server = useContext(ServerData);",
520
+ ` const server = useContext(ServerData)${derived ? " ?? useContext(Revived)" : ""};`,
432
521
  ` const nest = _layouts(server, router, NEST, ${nested ? levels + 1 : 0});`,
433
522
  ]
434
523
  : []),
@@ -519,32 +608,43 @@ export function generate(table, options) {
519
608
  ]),
520
609
  // The Router sits directly inside an element so the compiler makes it (and `routes`) a resumable
521
610
  // region; the root layout wraps it outside that region and stays mounted.
522
- ` const view = <div data-rivet-routes=""${go ? ' data-rivet-go=""' : ""}><Router router={${nested ? "_level(router, nest, 0, 0)" : liveTop ? "_liveRouter(router, live)" : "router"}} routes={routes} swap={${swap}} hold={heldRoutes} notFound={(url) => (${unmatched})} /></div>;`,
611
+ ...(derived ? [" // Derived model: built where it renders (inside the root layout's adopt run), not passed in ready-made."] : []),
612
+ ` const view = ${derived ? "() => " : ""}<div data-rivet-routes=""${go ? ' data-rivet-go=""' : ""}><Router router={${nested ? "_level(router, nest, 0, 0)" : liveTop ? "_liveRouter(router, live)" : "router"}} routes={routes} swap={${swapProp}} hold={heldRoutes} notFound={(url) => (${unmatched})} /></div>;`,
523
613
  !rootLayout
524
- ? " return view;"
614
+ ? ` return view${derived ? "()" : ""};`
525
615
  : rootLayoutServer
526
616
  ? " // Server error renders carry the root layout's data when its loader succeeded; else no root layout.\n" +
527
- ` return server?.error && !server.data.length ? view : ${level(0, `<${layouts.get(rootLayout)} data={layoutData} params={noParams} url={layoutURL}>{view}</${layouts.get(rootLayout)}>`)};`
528
- : ` return ${level(0, `<${layouts.get(rootLayout)} data={layoutData} params={noParams} url={layoutURL}>{view}</${layouts.get(rootLayout)}>`)};`,
617
+ ` return server?.error && !server.data.length ? view${derived ? "()" : ""} : ${level(0, `<${layouts.get(rootLayout)} data={layoutData} params={noParams} url={layoutURL}>{view${derived ? "()" : ""}}</${layouts.get(rootLayout)}>`)};`
618
+ : ` return ${level(0, `<${layouts.get(rootLayout)} data={layoutData} params={noParams} url={layoutURL}>{view${derived ? "()" : ""}}</${layouts.get(rootLayout)}>`)};`,
529
619
  "}",
620
+ ...(derived ? rootComponent(transitions !== null, search) : []),
530
621
  "",
531
622
  ].join("\n");
532
623
  // --- routes.ts: server route table (server modules load lazily, once per process in builds).
533
- const serverModule = (file) => (file ? `once(() => import(${str(spec(file))}))` : "undefined");
624
+ // Every server loader returns a normalized `ServerModule` (exports found by tag, `server/module.ts`).
625
+ let serverUsed = false;
626
+ const serverModule = (file) => {
627
+ if (!file)
628
+ return "undefined";
629
+ serverUsed = true;
630
+ return `once(async () => serverExports(await import(${str(spec(file))}), ${str(posix.join(routesDir, file))}, true))`;
631
+ };
534
632
  // Layouts are static imports of the app already; pages stay lazy.
535
633
  const layoutModules = new Map([...layouts.keys()].map((file, i) => [file, "L" + i]));
536
- // In-file server exports (`serverLoad`…): normalized from the page/layout module, merged with a
537
- // sibling `*.server.ts` when there is one. Sibling-only routes load the sibling as before.
538
- let inlineUsed = false;
634
+ // In-file server exports (`load(…)`, `onGet(…)`… calls): normalized from the page/layout module by tag,
635
+ // merged with a sibling `*.server.ts` when there is one. Sibling-only routes load the sibling raw.
636
+ let mergeUsed = false;
539
637
  const serverOf = (module, layout) => {
540
638
  if (!module.inline || !module.page)
541
639
  return serverModule(module.server);
542
- inlineUsed = true;
640
+ serverUsed = true;
641
+ if (module.server)
642
+ mergeUsed = true;
543
643
  const own = layout ? layoutModules.get(module.page) : `await import(${str(spec(module.page))})`;
544
644
  const files = str([posix.join(routesDir, module.page), module.server ? posix.join(routesDir, module.server) : ""]);
545
645
  return module.server
546
646
  ? `once(async () => mergeServer(${own}, await import(${str(spec(module.server))}), ${files}))`
547
- : `once(async () => serverExports(${own}, false, ${str(posix.join(routesDir, module.page))}))`;
647
+ : `once(async () => serverExports(${own}, ${str(posix.join(routesDir, module.page))}))`;
548
648
  };
549
649
  const routes = [
550
650
  HEADER,
@@ -584,20 +684,20 @@ export function generate(table, options) {
584
684
  "};",
585
685
  "",
586
686
  ].join("\n");
587
- const routesFile = inlineUsed
588
- ? routes.replace(`import type { AppRoute } from "@rivetjs/router/server";`, `import { mergeServer, serverExports, type AppRoute } from "@rivetjs/router/server";`)
687
+ const routesFile = serverUsed
688
+ ? routes.replace(`import type { AppRoute } from "@rivetjs/router/server";`, `import { ${mergeUsed ? "mergeServer, " : ""}serverExports, type AppRoute } from "@rivetjs/router/server";`)
589
689
  : routes;
590
690
  // --- entry.server.tsx: render one request inside the precomputed route data.
591
691
  const entry = [
592
692
  HEADER,
593
693
  ...(search ? [`import { signal } from "@rivetjs/reactivity";`] : []),
594
694
  `import { Provider } from "@rivetjs/runtime/server";`,
595
- `import { createRouter } from "@rivetjs/runtime/router/server";`,
695
+ ...(derived ? [`import { _rListen } from "@rivetjs/runtime/server-derived";`] : [`import { createRouter } from "@rivetjs/runtime/router/server";`]),
596
696
  `import { ServerData } from "@rivetjs/router/client";`,
597
697
  ...(transitions ? [`import { Navigation, navigation } from "@rivetjs/router/navigation";`] : []),
598
698
  ...(search ? [`import { SearchParams } from "@rivetjs/router/search";`] : []),
599
699
  `import type { PageInput } from "@rivetjs/router/server";`,
600
- `import { App } from "./app.tsx";`,
700
+ derived ? `import Root from "./app.tsx";` : `import { App } from "./app.tsx";`,
601
701
  `export { routes, universal } from "./routes.ts";`,
602
702
  '/** URL base path the route patterns carry (`""`: root); adapters serve files and prerender under it. */',
603
703
  `export const base = ${str(base)};`,
@@ -608,7 +708,7 @@ export function generate(table, options) {
608
708
  `import shell from ${str(fromOut(options.document))};`,
609
709
  `import { documentPrefetch } from "@rivetjs/router/prefetch";`,
610
710
  "/** Document prefetch (`rivetRouter({ prefetch })`): Speculation Rules + intent fallback in every page's head. */",
611
- `export const document = documentPrefetch(shell, ${str({ ...(typeof options.prefetch === "string" ? { default: options.prefetch } : {}), ...(single ? {} : { routed: true, ...(options.prefetchPaths === false ? {} : { routes: routePathRegExp(table.routes.map((route) => at(route.pattern))) }) }), ...(options.prefetchEagerness && options.prefetchEagerness !== "moderate" ? { eagerness: options.prefetchEagerness } : {}) })});`,
711
+ `export const document = documentPrefetch(shell, ${str({ ...(typeof options.prefetch === "string" ? { default: options.prefetch } : {}), ...(single ? {} : { routed: true, ...(options.prefetchPaths === false ? {} : { routes: routePathRegExp(table.routes.filter((route) => !route.page || !documentFiles.has(route.page)).map((route) => at(route.pattern))) }) }), ...(options.prefetchEagerness && options.prefetchEagerness !== "moderate" ? { eagerness: options.prefetchEagerness } : {}) })});`,
612
712
  ]
613
713
  : options.document
614
714
  ? [`export { default as document } from ${str(fromOut(options.document))};`]
@@ -639,6 +739,23 @@ export function generate(table, options) {
639
739
  ]
640
740
  : []),
641
741
  "",
742
+ ...(derived
743
+ ? [
744
+ "/** Derived resume model: link clicks anywhere in the document are `Root`'s (its App's document click listener). */",
745
+ "function Links() {",
746
+ ' _rListen("a[href]");',
747
+ " return null;",
748
+ "}",
749
+ "/** Derived resume model: the route payload `Root` carries as data (`undefined` loader results listed in `n`). */",
750
+ "const rootData = (input: PageInput) => {",
751
+ " const d: unknown[] = [],",
752
+ " n: number[] = [];",
753
+ " input.data.forEach((value, i) => (value === undefined ? ((d[i] = null), n.push(i)) : (d[i] = value)));",
754
+ " return { d, ...(n.length ? { n } : {}), ...(input.error ? { e: input.error } : {}), ...(input.action ? { a: input.action } : {}), ...(input.byURL ? { x: input.byURL } : {}), ...(input.deps ? { k: input.deps } : {}) };",
755
+ "};",
756
+ "",
757
+ ]
758
+ : []),
642
759
  "/** Page factory for one request; the app reads the handler's loader results through ServerData. */",
643
760
  "export function page(input: PageInput) {",
644
761
  " return () => (",
@@ -660,7 +777,7 @@ export function generate(table, options) {
660
777
  ],
661
778
  ]
662
779
  : []),
663
- ], "<App router={createRouter(input.url)} />", " "),
780
+ ], derived ? "<>\n <Links />\n <Root s={rootData(input)} />\n </>" : "<App router={createRouter(input.url)} />", " "),
664
781
  " </Provider>",
665
782
  " );",
666
783
  "}",
@@ -844,7 +961,7 @@ export function generate(table, options) {
844
961
  .join("/") +
845
962
  "`";
846
963
  const paths = [...new Set(pageRoutes.map((r) => pathOf(r.pattern)))];
847
- // Loader data per route id: the route module's `serverLoad` (in-file) or sibling loader.
964
+ // Loader data per route id: the route module's `load(…)` export (in-file or sibling), found by tag.
848
965
  const typeOf = (module, wrap, sibling) => {
849
966
  const parts = [
850
967
  ...(module.inline && module.page ? [wrap(`typeof import(${str(spec(module.page))})`)] : []),
@@ -854,7 +971,7 @@ export function generate(table, options) {
854
971
  };
855
972
  const firstOf = (id) => table.routes.find((r) => r.id === id);
856
973
  const data = ids.flatMap((id) => {
857
- const type = typeOf(firstOf(id), (m) => `InlineData<${m}>`, (m) => `ModuleData<${m}>`);
974
+ const type = typeOf(firstOf(id), (m) => `ModuleData<${m}>`, (m) => `ModuleData<${m}>`);
858
975
  return type ? [` ${str(id)}: ${type};`] : [];
859
976
  });
860
977
  // Form actions per route id (the page's own module: layouts have none).
@@ -878,11 +995,11 @@ export function generate(table, options) {
878
995
  return ` ${str(id)}: ${all.size ? "{ " + [...all].map((p) => `${p}${required(p) ? "" : "?"}: string`).join("; ") + " }" : "{}"};`;
879
996
  });
880
997
  const layoutData = [...layoutIds].flatMap(([id, module]) => {
881
- const type = typeOf(module, (m) => `InlineData<${m}>`, (m) => `ModuleData<${m}>`);
998
+ const type = typeOf(module, (m) => `ModuleData<${m}>`, (m) => `ModuleData<${m}>`);
882
999
  return type ? [` ${str(id)}: ${type};`] : [];
883
1000
  });
884
1001
  const lines = [...data, ...actions, ...layoutData].join("\n");
885
- const helpers = ["InlineData", "ModuleData", "ModuleActions"].filter((name) => lines.includes(name + "<"));
1002
+ const helpers = ["ModuleData", "ModuleActions"].filter((name) => lines.includes(name + "<"));
886
1003
  const types = [
887
1004
  HEADER,
888
1005
  ...(helpers.length ? [`import type { ${helpers.join(", ")} } from "@rivetjs/router";`] : []),
@@ -13,7 +13,11 @@ export declare function devReport(server: ViteDevServer, table: () => RouteTable
13
13
  export declare function listenClientErrors(server: ViteDevServer): void;
14
14
  /**
15
15
  * Wrap the dev app handler: a reported 5xx page gets the dev error page (no `_error` view) or the
16
- * overlay over the app's `_error` view; an unmatched URL without a `_404` lists the routes.
16
+ * overlay over the app's `_error` view; an unmatched URL without a `_404` lists the routes. `reach`: the page's server
17
+ * reach reports (RV3011, `build/reach.ts`): an error opens the overlay over the rendered page.
17
18
  */
18
- export declare function devHandler(handler: AppHandler, server: ViteDevServer, table: () => RouteTable): AppHandler;
19
+ export declare function devHandler(handler: AppHandler, server: ViteDevServer, table: () => RouteTable, reach?: (url: URL) => Promise<readonly {
20
+ readonly severity: "error" | "warning";
21
+ readonly message: string;
22
+ }[]>): AppHandler;
19
23
  export {};
@@ -133,8 +133,9 @@ const inject = (page, markup) => {
133
133
  return at < 0 ? page + markup : page.slice(0, at) + markup + page.slice(at);
134
134
  };
135
135
  /** Inline module script opening Vite's overlay on this page (the request's ws message reached only pages already open). */
136
- const overlayScript = (server, failure, nonce) => `<script type="module"${nonceAttr(nonce)}>import { ErrorOverlay } from ${scriptJSON(server.config.base + "@vite/client")};` +
137
- `document.body.appendChild(new ErrorOverlay(${scriptJSON(overlayError(failure))}));</script>`;
136
+ const overlayScript = (server, failure, nonce) => overlayOf(server, overlayError(failure), nonce);
137
+ const overlayOf = (server, error, nonce) => `<script type="module"${nonceAttr(nonce)}>import { ErrorOverlay } from ${scriptJSON(server.config.base + "@vite/client")};` +
138
+ `document.body.appendChild(new ErrorOverlay(${scriptJSON(error)}));</script>`;
138
139
  const STYLE = "body{margin:0;font:15px/1.5 system-ui,sans-serif;background:#fff;color:#1b1b1f}main{max-width:72rem;margin:0 auto;padding:2rem 1rem}" +
139
140
  "h1{font-size:1.4rem;margin:0 0 .5rem}code,pre{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}pre{overflow:auto;padding:1rem;border-radius:6px;background:#f4f4f6;white-space:pre-wrap}" +
140
141
  ".message{color:#b00020;font-weight:600}ul{padding-left:1.2rem}li{margin:.15rem 0}small{color:#6b6b76}" +
@@ -157,10 +158,16 @@ const routeList = (server, table) => {
157
158
  const comment = (text) => `<!-- ${text.replace(/--/g, "- -")} -->`;
158
159
  /**
159
160
  * Wrap the dev app handler: a reported 5xx page gets the dev error page (no `_error` view) or the
160
- * overlay over the app's `_error` view; an unmatched URL without a `_404` lists the routes.
161
+ * overlay over the app's `_error` view; an unmatched URL without a `_404` lists the routes. `reach`: the page's server
162
+ * reach reports (RV3011, `build/reach.ts`): an error opens the overlay over the rendered page.
161
163
  */
162
- export function devHandler(handler, server, table) {
164
+ export function devHandler(handler, server, table, reach) {
163
165
  return async (request, context) => {
166
+ // A page document (not a data request): its route's server reach errors first (the walk may regenerate the App).
167
+ const url = new URL(request.url);
168
+ const reachErrors = reach && request.method === "GET" && !url.searchParams.has("_rivet") && (request.headers.get("accept") ?? "text/html").includes("text/html")
169
+ ? (await reach(url)).filter((problem) => problem.severity === "error")
170
+ : [];
164
171
  const seen = [];
165
172
  const response = await failures.run(seen, () => handler(request, context));
166
173
  if (!(response.headers.get("content-type") ?? "").startsWith("text/html"))
@@ -191,6 +198,10 @@ export function devHandler(handler, server, table) {
191
198
  "<p><small>Development only: add a <code>_404</code> page to render your own.</small></p>";
192
199
  return html(response, devPage("404 · " + path, body, devClient(server, nonce)));
193
200
  }
201
+ if (reachErrors.length && response.status < 400 && overlayOn(server)) {
202
+ const error = { message: reachErrors.map((problem) => problem.message).join("\n\n"), stack: "", plugin: "vite-plugin-rivet-router" };
203
+ return html(response, inject(await response.text(), comment(error.message) + overlayOf(server, error, nonce)));
204
+ }
194
205
  return response.body ? lateFailures(response, seen, server, nonce) : response;
195
206
  };
196
207
  }
@@ -0,0 +1,19 @@
1
+ import type { Plugin } from "vite";
2
+ /** The measurement mode env var: `swap` here; `on` / `off` belong to the runtime (server lazy row records, LAZY-ROWS T4, read at render). */
3
+ export declare const LAZY_ROWS_ENV = "RIVET_LAZY_ROWS";
4
+ /** Set by the measurement tools for their build children: production builds honour `RIVET_LAZY_ROWS` only with it. */
5
+ export declare const LAZY_ROWS_HARNESS_ENV = "RIVET_LAZY_ROWS_HARNESS";
6
+ /** Whether the swap harness applies to a Vite run (`command`): dev server, or a harness build. `on` / `off` → null (runtime
7
+ * values, no build change). Throws on unknown values. */
8
+ export declare function lazyRowsMode(command: "serve" | "build", env?: Readonly<Record<string, string | undefined>>): "swap" | null;
9
+ /**
10
+ * Lazy rows proper (LAZY-ROWS T5, `on`): a build whose resume runtime materialises them needs
11
+ * `rivetRouter({ resume: { lazyRows: true } })`. The measurement tools (docs-kit `check-examples`,
12
+ * `scripts/lazy-rows/probe.ts`) build app configs they do not own: with `RIVET_LAZY_ROWS=on` AND `LAZY_ROWS_HARNESS_ENV`
13
+ * (which only they set for their build children) `rivetRouter` turns the option on for that build (LAZY-T5B).
14
+ */
15
+ export declare function lazyRowsBuild(env?: Readonly<Record<string, string | undefined>>): boolean;
16
+ /** The runtime module the server entry imports in swap mode (no package export: not public API). */
17
+ export declare function lazyRowsModule(): string;
18
+ /** Vite plugin: in swap mode the generated server entry (`entry`) installs the swap row hook before anything else. */
19
+ export declare function lazyRowsHarness(entry: () => string): Plugin;
@@ -0,0 +1,59 @@
1
+ // Lazy collection rows, T1 measurement harness (spec docs/design/specs/2026-10-19-lazy-collection-rows-design.md §3.4, R2):
2
+ // `RIVET_LAZY_ROWS=swap` renders every For row inside a resumable region unrecorded and re-creates the region in the
3
+ // browser on the first interaction (create + swap). A bytes / server-CPU baseline only — it must never ship (the event
4
+ // target is replaced under the pointer, inputs lose their value, focus moves), so it is no option of `rivet.config` /
5
+ // `rivetRouter()`: only this environment variable turns it on, and only in the dev server or in a build whose environment
6
+ // also carries `LAZY_ROWS_HARNESS_ENV` (docs-kit `check-examples` / `scripts/lazy-rows/probe.ts` set it for their
7
+ // build children). Everything it adds is server code (`@rivetjs/runtime` `server-lazy-swap.ts`, installed first by the
8
+ // generated server entry): browser bundles are unchanged.
9
+ import { createRequire } from "node:module";
10
+ import { dirname, join } from "node:path";
11
+ /** The measurement mode env var: `swap` here; `on` / `off` belong to the runtime (server lazy row records, LAZY-ROWS T4, read at render). */
12
+ export const LAZY_ROWS_ENV = "RIVET_LAZY_ROWS";
13
+ /** Set by the measurement tools for their build children: production builds honour `RIVET_LAZY_ROWS` only with it. */
14
+ export const LAZY_ROWS_HARNESS_ENV = "RIVET_LAZY_ROWS_HARNESS";
15
+ /** Whether the swap harness applies to a Vite run (`command`): dev server, or a harness build. `on` / `off` → null (runtime
16
+ * values, no build change). Throws on unknown values. */
17
+ export function lazyRowsMode(command, env = process.env) {
18
+ const value = env[LAZY_ROWS_ENV];
19
+ if (!value || value === "on" || value === "off")
20
+ return null;
21
+ if (value !== "swap")
22
+ throw new Error(`${LAZY_ROWS_ENV}=${value}: the only measurement mode is "swap" ("on" / "off": server lazy row records)`);
23
+ if (command === "serve" || env[LAZY_ROWS_HARNESS_ENV] === "1")
24
+ return "swap";
25
+ console.warn(`rivetRouter: ${LAZY_ROWS_ENV}=swap ignored: a measurement mode for the dev server and the lazy-rows tools only`);
26
+ return null;
27
+ }
28
+ /**
29
+ * Lazy rows proper (LAZY-ROWS T5, `on`): a build whose resume runtime materialises them needs
30
+ * `rivetRouter({ resume: { lazyRows: true } })`. The measurement tools (docs-kit `check-examples`,
31
+ * `scripts/lazy-rows/probe.ts`) build app configs they do not own: with `RIVET_LAZY_ROWS=on` AND `LAZY_ROWS_HARNESS_ENV`
32
+ * (which only they set for their build children) `rivetRouter` turns the option on for that build (LAZY-T5B).
33
+ */
34
+ export function lazyRowsBuild(env = process.env) {
35
+ return env[LAZY_ROWS_ENV] === "on" && env[LAZY_ROWS_HARNESS_ENV] === "1";
36
+ }
37
+ /** The runtime module the server entry imports in swap mode (no package export: not public API). */
38
+ export function lazyRowsModule() {
39
+ return join(dirname(createRequire(import.meta.url).resolve("@rivetjs/runtime/server-resume")), "server-lazy-swap.ts");
40
+ }
41
+ /** Vite plugin: in swap mode the generated server entry (`entry`) installs the swap row hook before anything else. */
42
+ export function lazyRowsHarness(entry) {
43
+ let active = false;
44
+ return {
45
+ name: "vite-plugin-rivet-lazy-rows",
46
+ enforce: "pre",
47
+ config(_config, env) {
48
+ active = lazyRowsMode(env.command) !== null;
49
+ if (active)
50
+ console.warn(`rivetRouter: ${LAZY_ROWS_ENV}=swap: For rows render unrecorded (measurement build, never ship it)`);
51
+ },
52
+ transform(code, id, options) {
53
+ if (!active || !options?.ssr || id.split("?")[0] !== entry())
54
+ return null;
55
+ // A call, not a bare import: the runtime package is `sideEffects: false`.
56
+ return { code: `import { _lazyRowsSwap } from ${JSON.stringify(lazyRowsModule())};\n_lazyRowsSwap();\n` + code, map: null };
57
+ },
58
+ };
59
+ }
@@ -1,15 +1,31 @@
1
- export declare const RENAMES: Readonly<Record<string, string>>;
1
+ import type TS from "typescript";
2
+ /** Wrapper and conventional export name per former magic name. */
3
+ export declare const WRAPPERS: Readonly<Record<string, {
4
+ readonly wrapper: string;
5
+ readonly name: string;
6
+ }>>;
7
+ /** Former sibling `*.server.ts` names → the in-file name they meant. */
8
+ export declare const SIBLING: Readonly<Record<string, string>>;
2
9
  export interface MigrateReport {
3
10
  /** Changed files (absolute) → new contents. */
4
11
  readonly changes: ReadonlyMap<string, string>;
5
- /** `file: old → new` per renamed export. */
12
+ /** `file: old → new = wrapper(…)` per rewritten export. */
6
13
  readonly renamed: readonly string[];
7
14
  /** Exports left alone, with the reason. */
8
15
  readonly skipped: readonly string[];
9
16
  }
10
17
  /**
11
- * Compute the migration for `*.server.ts` modules under `routesDir`; references are searched in every
12
- * source module under `projectDir`. Nothing is written (see `writeMigration`).
18
+ * Compute the migration for route modules under `routesDir` (pages, layouts, plain `.ts` endpoints,
19
+ * sibling `*.server.ts`); references are searched in every source module under `projectDir`. Nothing is
20
+ * written (see `writeMigration`).
13
21
  */
14
22
  export declare function migrateServerExports(projectDir: string, routesDir: string): Promise<MigrateReport>;
23
+ /** Migrate one module's text on its own (no other modules: references elsewhere are not renamed). */
24
+ export declare function migrateSource(ts: typeof TS, text: string, fileName: string): {
25
+ text: string;
26
+ renamed: readonly string[];
27
+ skipped: readonly string[];
28
+ };
29
+ /** The migration over in-memory `texts` (absolute paths); `targets`: the route modules to rewrite. */
30
+ export declare function migrateTexts(ts: typeof TS, texts: ReadonlyMap<string, string>, targets: readonly string[], root: string): MigrateReport;
15
31
  export declare function writeMigration(report: MigrateReport): Promise<void>;