@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.
- package/adapters/files.d.ts +13 -0
- package/adapters/files.js +58 -6
- package/adapters/node.d.ts +22 -5
- package/adapters/node.js +113 -67
- package/adapters/static.js +1 -1
- package/adapters/target-build.d.ts +5 -2
- package/adapters/vercel.d.ts +4 -2
- package/adapters/vercel.js +1 -1
- package/build/codegen.d.ts +13 -0
- package/build/codegen.js +152 -35
- package/build/dev-errors.d.ts +6 -2
- package/build/dev-errors.js +15 -4
- package/build/lazy-rows.d.ts +19 -0
- package/build/lazy-rows.js +59 -0
- package/build/migrate.d.ts +20 -4
- package/build/migrate.js +300 -61
- package/build/reach.d.ts +60 -0
- package/build/reach.js +302 -0
- package/build/routes.d.ts +9 -4
- package/build/routes.js +30 -13
- package/build/vite.d.ts +13 -0
- package/build/vite.js +165 -7
- package/cli/rivet.js +3 -2
- package/client/dev.js +17 -0
- package/client/index.d.ts +4 -0
- package/client/index.js +34 -0
- package/client/route-context.d.ts +1 -1
- package/index.d.ts +22 -25
- package/index.js +2 -1
- package/package.json +4 -4
- package/server/app.d.ts +7 -1
- package/server/app.js +38 -5
- package/server/deferred.d.ts +12 -0
- package/server/deferred.js +60 -0
- package/server/handler.d.ts +56 -12
- package/server/handler.js +46 -8
- package/server/index.d.ts +1 -1
- package/server/module.d.ts +7 -7
- package/server/module.js +52 -34
- package/server/render.d.ts +2 -0
- package/server/render.js +3 -0
- package/server/route-cache.d.ts +74 -4
- package/server/route-cache.js +77 -6
- package/server/shell-build.d.ts +3 -0
- package/server/shell-build.js +2 -0
- package/server/shell-cache.d.ts +8 -0
- package/server/shell-cache.js +836 -0
- package/shared/server-exports.d.ts +78 -0
- 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={${
|
|
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
|
|
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
|
-
...(
|
|
350
|
-
? [`import { signal, useContext } from "@rivetjs/reactivity";`]
|
|
351
|
-
:
|
|
352
|
-
? [`import { signal } from "@rivetjs/reactivity";`]
|
|
353
|
-
:
|
|
354
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
393
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
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
|
-
|
|
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 (`
|
|
537
|
-
// sibling `*.server.ts` when there is one. Sibling-only routes load the sibling
|
|
538
|
-
let
|
|
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
|
-
|
|
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},
|
|
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 =
|
|
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 `
|
|
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) => `
|
|
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) => `
|
|
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 = ["
|
|
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";`] : []),
|
package/build/dev-errors.d.ts
CHANGED
|
@@ -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
|
|
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 {};
|
package/build/dev-errors.js
CHANGED
|
@@ -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) =>
|
|
137
|
-
|
|
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
|
+
}
|
package/build/migrate.d.ts
CHANGED
|
@@ -1,15 +1,31 @@
|
|
|
1
|
-
|
|
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
|
|
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
|
|
12
|
-
* source module under `projectDir`. Nothing is
|
|
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>;
|