@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/vite.js
CHANGED
|
@@ -11,7 +11,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
11
11
|
import { lstat, mkdir, readdir, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
12
12
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
13
13
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
14
|
-
import { rivet, _factsResolve, } from "@rivetjs/compiler/vite";
|
|
14
|
+
import { rivet, _factsResolve, declaredServerOnly, isServerOnly, serverOnlyStub, } from "@rivetjs/compiler/vite";
|
|
15
15
|
import { updateEditorManifest } from "@rivetjs/compiler/editor";
|
|
16
16
|
import { scanRoutes, _serverExportsOf } from "./routes.js";
|
|
17
17
|
import { generate } from "./codegen.js";
|
|
@@ -23,14 +23,29 @@ import { compileRules } from "../server/route-cache.js";
|
|
|
23
23
|
export { mergeRouteCSS } from "./route-css.js";
|
|
24
24
|
import { bundleRouteCSS, devRouteCSS, INLINE_CSS, inlineCSS, mergeRouteCSS, ROUTE_CSS } from "./route-css.js";
|
|
25
25
|
import { devHandler, devReport, listenClientErrors } from "./dev-errors.js";
|
|
26
|
+
import { lazyRowsBuild, lazyRowsHarness } from "./lazy-rows.js";
|
|
27
|
+
import { analyzeReach, serverOnlyOption, RV_REACH } from "./reach.js";
|
|
28
|
+
import { compileRoutes } from "@rivetjs/runtime/router/server";
|
|
26
29
|
import { adapterOptions, targetBuildConfig, targetEntryPlugin } from "../adapters/target-build.js";
|
|
27
30
|
/** The router's own base-path module (its `BASE` reads Vite's `BASE_URL`). */
|
|
28
31
|
const BASE_MODULE = fileURLToPath(new URL("../shared/base.js", import.meta.url));
|
|
29
32
|
/** The router's trailing-slash module (its `TRAILING_SLASH` constant is rewritten when the option is set). */
|
|
30
33
|
const SLASH_MODULE = fileURLToPath(new URL("../shared/slash.js", import.meta.url));
|
|
31
34
|
const SLASH_DEFAULT = '"ignore" as TrailingSlash';
|
|
35
|
+
/** The shell cache loader (`SHELL_BUILD`, rewritten to an `import()` when a route rule has `shell`). */
|
|
36
|
+
const SHELL_MODULE = fileURLToPath(new URL("../server/shell-build.js", import.meta.url));
|
|
37
|
+
/** `SHELL_BUILD`'s declaration in the source (`.ts`, with its type cast) and in the published `.js` (cast stripped). */
|
|
38
|
+
const SHELL_DEFAULT = /export const SHELL_BUILD = undefined(?: as [^;]+)?;/;
|
|
39
|
+
/** The shell cache module the rewritten loader imports (by file path: also what keeps it in the published package). */
|
|
40
|
+
const SHELL_CACHE = fileURLToPath(new URL("../server/shell-cache.js", import.meta.url));
|
|
32
41
|
const posix = (path) => path.split(sep).join("/");
|
|
33
42
|
const EMPTY = "\0rivet-router-empty";
|
|
43
|
+
/** Client build: a document route's page module as `.rivet/app.tsx` imports it (no browser code for the page). */
|
|
44
|
+
const DOCUMENT_PAGE = "\0rivet-document-page:";
|
|
45
|
+
/** Browser stand-in of a server-only module imported only from document routes' graphs (`serverOnlyStub`). */
|
|
46
|
+
const STAND_IN = "\0rivet-server-only:";
|
|
47
|
+
/** The file behind a module id (virtual compiler parts `\0rivet-resume-part:/abs/file.tsx?…` included), no query. */
|
|
48
|
+
const fileOf = (id) => (id.startsWith("\0") ? id.slice(id.indexOf(":") + 1) : id).split("?")[0];
|
|
34
49
|
/** Write generated files whose contents changed; returns whether anything changed. */
|
|
35
50
|
async function sync(dir, files) {
|
|
36
51
|
await mkdir(dir, { recursive: true });
|
|
@@ -143,6 +158,8 @@ export async function generateRoutes(root, options = {}) {
|
|
|
143
158
|
...(options.preload?.background !== undefined ? { preloadBackground: options.preload.background } : {}),
|
|
144
159
|
...(options.routeRules && Object.keys(options.routeRules).length ? { routeRules: options.routeRules } : {}),
|
|
145
160
|
...(options.clientRouter !== undefined ? { clientRouter: options.clientRouter } : {}),
|
|
161
|
+
...(options.documents?.length ? { documents: options.documents } : {}),
|
|
162
|
+
...(options.derived ? { derived: true } : {}),
|
|
146
163
|
});
|
|
147
164
|
const changed = await sync(resolve(root, outDir), generated.files);
|
|
148
165
|
// No router contexts: the compiler's default resume entry (drop a stale generated one).
|
|
@@ -225,6 +242,8 @@ export function rivetRouter(options = {}) {
|
|
|
225
242
|
...(options.preload ? { preload: options.preload } : {}),
|
|
226
243
|
...(options.routeRules ? { routeRules: options.routeRules } : {}),
|
|
227
244
|
...(options.clientRouter !== undefined ? { clientRouter: options.clientRouter } : {}),
|
|
245
|
+
// Derived resume model (M2 R5): the generated `Root` boundary carries the route payload as data.
|
|
246
|
+
...(options.resume?.model === "derived" && mode !== "csr" ? { derived: true } : {}),
|
|
228
247
|
};
|
|
229
248
|
if (options.routeRules !== undefined) {
|
|
230
249
|
// Checked at config time (same checks as at server start, except the origin `isr` needs: a server option), and
|
|
@@ -257,7 +276,9 @@ export function rivetRouter(options = {}) {
|
|
|
257
276
|
Object.keys(inlineOption).every((key) => key === "maxTotalBrotli") &&
|
|
258
277
|
(inlineOption.maxTotalBrotli === undefined || (Number.isSafeInteger(inlineOption.maxTotalBrotli) && inlineOption.maxTotalBrotli >= 0))))
|
|
259
278
|
throw new Error(`rivetRouter: inlineStylesheets must be "auto", "always", "never" or { maxTotalBrotli: <bytes> } (got ${JSON.stringify(inlineOption)})`);
|
|
279
|
+
const serverOnly = serverOnlyOption(options.serverOnly);
|
|
260
280
|
const slash = options.trailingSlash ?? "ignore";
|
|
281
|
+
const shellBuild = Object.values(options.routeRules ?? {}).some((rule) => Boolean(rule?.shell));
|
|
261
282
|
if (slash !== "ignore" && slash !== "never" && slash !== "always")
|
|
262
283
|
throw new Error(`rivetRouter: trailingSlash must be "ignore", "never" or "always" (got ${JSON.stringify(slash)})`);
|
|
263
284
|
// Filled in `config`, which runs before the compiler plugin reads it in `configResolved`.
|
|
@@ -271,6 +292,11 @@ export function rivetRouter(options = {}) {
|
|
|
271
292
|
dev: options.dev?.resume !== false,
|
|
272
293
|
// Every app component module a route imports resumes without being listed in `resume.modules`.
|
|
273
294
|
appModules: options.resume?.appModules ?? true,
|
|
295
|
+
// Lazy rows measurement builds (`RIVET_LAZY_ROWS=on` + the harness flag of the lazy-rows tools): the option on.
|
|
296
|
+
...(lazyRowsBuild() ? { lazyRows: true } : {}),
|
|
297
|
+
// Derived model (M2 R5): `.rivet/app.tsx`'s default export (`Root`) is an independent root boundary (its props =
|
|
298
|
+
// the route payload as data), like a route module's default export.
|
|
299
|
+
...(options.resume?.model === "derived" && mode !== "csr" ? { roots: [...(options.resume.roots ?? []), posix(join(outDir, "app.tsx"))] } : {}),
|
|
274
300
|
};
|
|
275
301
|
// The generated resume entry (when there is one) provides the router's contexts (navigation state,
|
|
276
302
|
// search params) to resumed regions.
|
|
@@ -286,6 +312,33 @@ export function rivetRouter(options = {}) {
|
|
|
286
312
|
};
|
|
287
313
|
let root = process.cwd();
|
|
288
314
|
let table = { routes: [] };
|
|
315
|
+
// Server reach (`build/reach.ts`): computed in `configResolved` (before any module compiles), re-run in dev after edits.
|
|
316
|
+
let reach = null, reachDirty = false, reachRun = Promise.resolve(), resolvedConfig = null, resolver = null, patterns = [...serverOnly.modules], building = false;
|
|
317
|
+
// Document routes' page files (absolute → routes-relative), client builds.
|
|
318
|
+
let documentPages = new Map();
|
|
319
|
+
/** Walk the routes' browser graphs; true when the document routes changed (codegen input). */
|
|
320
|
+
const analyze = async () => {
|
|
321
|
+
if (!resolvedConfig || mode === "csr")
|
|
322
|
+
return false;
|
|
323
|
+
resolver ??= resolvedConfig.createResolver();
|
|
324
|
+
const resolveFor = resolver;
|
|
325
|
+
reach = await analyzeReach({
|
|
326
|
+
root,
|
|
327
|
+
routesDir: resolve(root, routesDir),
|
|
328
|
+
table,
|
|
329
|
+
resolve: (spec, importer) => resolveFor(spec, importer, false, false),
|
|
330
|
+
serverOnly: patterns,
|
|
331
|
+
reach: serverOnly.reach,
|
|
332
|
+
routeModules: new Set(routeModules(table, routesDir).map((file) => resolve(root, file))),
|
|
333
|
+
config: resolvedConfig,
|
|
334
|
+
});
|
|
335
|
+
documentPages = new Map([...reach.documents.keys()].map((page) => [resolve(root, routesDir, page), page]));
|
|
336
|
+
const documents = [...reach.documents.keys()].sort();
|
|
337
|
+
if (documents.join("\0") === (codegen.documents ?? []).join("\0"))
|
|
338
|
+
return false;
|
|
339
|
+
codegen.documents = documents;
|
|
340
|
+
return true;
|
|
341
|
+
};
|
|
289
342
|
const devtools = options.devtools ?? true;
|
|
290
343
|
const css = options.css ? { css: options.css } : {};
|
|
291
344
|
// Filled in `config` and on route changes: in-file server exports leave the browser build.
|
|
@@ -432,8 +485,32 @@ export function rivetRouter(options = {}) {
|
|
|
432
485
|
},
|
|
433
486
|
};
|
|
434
487
|
},
|
|
488
|
+
// Server reach: every route's browser graph, before the first module compiles (codegen marks document routes).
|
|
489
|
+
async configResolved(config) {
|
|
490
|
+
resolvedConfig = config;
|
|
491
|
+
resolver = null;
|
|
492
|
+
building = config.command === "build";
|
|
493
|
+
patterns = [...serverOnly.modules, ...declaredServerOnly(config.plugins)];
|
|
494
|
+
if (mode === "csr")
|
|
495
|
+
return;
|
|
496
|
+
if (await analyze())
|
|
497
|
+
await generateRoutes(root, codegen);
|
|
498
|
+
},
|
|
499
|
+
buildStart() {
|
|
500
|
+
if (!building || !reach)
|
|
501
|
+
return;
|
|
502
|
+
const client = this.environment?.config?.consumer !== "server";
|
|
503
|
+
// Warnings once (client build); errors fail either build.
|
|
504
|
+
if (client)
|
|
505
|
+
for (const problem of reach.problems)
|
|
506
|
+
if (problem.severity === "warning")
|
|
507
|
+
this.warn(problem.message);
|
|
508
|
+
const errors = reach.problems.filter((problem) => problem.severity === "error");
|
|
509
|
+
if (errors.length)
|
|
510
|
+
this.error({ message: errors.map((problem) => problem.message).join("\n\n"), plugin: "vite-plugin-rivet-router", pluginCode: RV_REACH });
|
|
511
|
+
},
|
|
435
512
|
resolveId(id, importer, resolveOptions) {
|
|
436
|
-
if (id === EMPTY)
|
|
513
|
+
if (id === EMPTY || id.startsWith(DOCUMENT_PAGE) || id.startsWith(STAND_IN))
|
|
437
514
|
return id;
|
|
438
515
|
// `x.server.ts` modules hold secrets and server code: never part of a browser graph.
|
|
439
516
|
if (!importer || resolveOptions?.ssr || this.environment?.config?.consumer === "server")
|
|
@@ -441,21 +518,51 @@ export function rivetRouter(options = {}) {
|
|
|
441
518
|
// The compiler's resume grouping resolves every specifier its facts list (type-only imports too): not an import.
|
|
442
519
|
if (_factsResolve(id, importer))
|
|
443
520
|
return null;
|
|
444
|
-
|
|
521
|
+
const from = fileOf(importer);
|
|
522
|
+
// Client build: the App's import of a document route's page — the browser never renders it (links load its document).
|
|
523
|
+
if (building && documentPages.size && /^\.\.?\//.test(id) && from === resolve(root, outDir, "app.tsx")) {
|
|
524
|
+
const page = documentPages.get(resolve(dirname(from), id.split("?")[0]));
|
|
525
|
+
if (page)
|
|
526
|
+
return DOCUMENT_PAGE + page;
|
|
527
|
+
}
|
|
528
|
+
if (isServerOnly(patterns, id)) {
|
|
529
|
+
// Reached only from document routes' graphs (their resumed parts): a stand-in that throws on use, never the module.
|
|
530
|
+
if (reach?.documentOnly.has(from))
|
|
531
|
+
return STAND_IN + (reach.targets.get(from + "\0" + id) ?? id);
|
|
445
532
|
this.error({
|
|
446
533
|
message: `[rivet:router] ${id} is server-only and cannot be imported by browser code (imported from ${posix(importer)})` +
|
|
447
|
-
"\n fix: read it in a server export (`
|
|
448
|
-
"\n docs: https://rivetjs.
|
|
534
|
+
"\n fix: read it in a server export (`load(…)`, `actions(…)`, `onGet(…)`, …) and pass the data to the page; type-only imports (`import type`) are fine" +
|
|
535
|
+
"\n docs: https://rivetjs.severyn.pro/guides/security/server-only-code",
|
|
449
536
|
id: importer,
|
|
450
537
|
plugin: "vite-plugin-rivet-router",
|
|
451
538
|
});
|
|
539
|
+
}
|
|
540
|
+
return null;
|
|
541
|
+
},
|
|
542
|
+
load(id) {
|
|
543
|
+
if (id === EMPTY)
|
|
544
|
+
return "export {};\n";
|
|
545
|
+
if (id.startsWith(DOCUMENT_PAGE)) {
|
|
546
|
+
const page = id.slice(DOCUMENT_PAGE.length);
|
|
547
|
+
const message = `[rivet:router] ${posix(join(routesDir, page))} is a document route (its browser graph reaches a server-only module): its links load the document, the browser never renders it`;
|
|
548
|
+
return `export default function DocumentRoute() {\n throw new Error(${JSON.stringify(message)});\n}\n`;
|
|
549
|
+
}
|
|
550
|
+
if (id.startsWith(STAND_IN)) {
|
|
551
|
+
const key = id.slice(STAND_IN.length);
|
|
552
|
+
const shown = key.startsWith("/") ? posix(relative(root, key)) : key;
|
|
553
|
+
return serverOnlyStub(shown, reach?.chains.get(key) ?? [], reach?.names.get(key) ?? []);
|
|
554
|
+
}
|
|
452
555
|
return null;
|
|
453
556
|
},
|
|
454
|
-
load: (id) => (id === EMPTY ? "export {};\n" : null),
|
|
455
557
|
// i18n client build: `BASE` is the app base, not the locale's asset base (`BASE_URL`). Only this
|
|
456
558
|
// build's copy of shared/base.ts changes, so other apps' bundles stay byte-identical.
|
|
457
559
|
// `trailingSlash`: the policy constant (client and server bundles); unset, the module stays as is.
|
|
458
560
|
transform(code, id, transformOptions) {
|
|
561
|
+
if (shellBuild && id.split("?")[0] === SHELL_MODULE) {
|
|
562
|
+
if (!SHELL_DEFAULT.test(code))
|
|
563
|
+
this.error("rivetRouter: server/shell-build.ts no longer declares SHELL_BUILD as expected");
|
|
564
|
+
return { code: code.replace(SHELL_DEFAULT, `export const SHELL_BUILD = () => import(${JSON.stringify(SHELL_CACHE)});`), map: null };
|
|
565
|
+
}
|
|
459
566
|
if (slash !== "ignore" && id.split("?")[0] === SLASH_MODULE) {
|
|
460
567
|
if (!code.includes(SLASH_DEFAULT))
|
|
461
568
|
this.error("rivetRouter: shared/slash.ts no longer declares TRAILING_SLASH as expected");
|
|
@@ -552,6 +659,9 @@ export function rivetRouter(options = {}) {
|
|
|
552
659
|
table = next;
|
|
553
660
|
serverExports.modules.splice(0, serverExports.modules.length, ...routeModules(table, routesDir));
|
|
554
661
|
inFile = serverFiles(table);
|
|
662
|
+
// A new route table (or new document routes): the browser graphs are walked again on the next page request.
|
|
663
|
+
if (changed)
|
|
664
|
+
reachDirty = true;
|
|
555
665
|
if (changed && !closed)
|
|
556
666
|
server.ws.send({ type: "full-reload" });
|
|
557
667
|
return changed;
|
|
@@ -563,6 +673,52 @@ export function rivetRouter(options = {}) {
|
|
|
563
673
|
const logged = (fn) => (file) => void fn(file).catch((error) => server.config.logger.error("rivet router: " + (error instanceof Error ? (error.stack ?? error.message) : String(error))));
|
|
564
674
|
server.watcher.on("add", logged(regenerate));
|
|
565
675
|
server.watcher.on("unlink", logged(regenerate));
|
|
676
|
+
// Any source edit can change what a route's browser graph reaches (re-walked lazily, changed files only).
|
|
677
|
+
const stale = (file) => {
|
|
678
|
+
if (SOURCE.test(file))
|
|
679
|
+
reachDirty = true;
|
|
680
|
+
};
|
|
681
|
+
server.watcher.on("add", stale);
|
|
682
|
+
server.watcher.on("unlink", stale);
|
|
683
|
+
server.watcher.on("change", stale);
|
|
684
|
+
// Dev server reach: a page's RV3011 reports on its load (terminal once per report, overlay on every load of an
|
|
685
|
+
// error); new document routes regenerate the App (full reload).
|
|
686
|
+
const reported = new Set();
|
|
687
|
+
let routeMatch;
|
|
688
|
+
const devReach = async (url) => {
|
|
689
|
+
if (mode === "csr")
|
|
690
|
+
return [];
|
|
691
|
+
if (reachDirty) {
|
|
692
|
+
reachDirty = false;
|
|
693
|
+
const run = reachRun.then(async () => {
|
|
694
|
+
if (!closed && (await analyze()))
|
|
695
|
+
await refresh();
|
|
696
|
+
});
|
|
697
|
+
reachRun = run.catch(() => { });
|
|
698
|
+
await run;
|
|
699
|
+
}
|
|
700
|
+
if (!reach?.problems.length)
|
|
701
|
+
return [];
|
|
702
|
+
if (routeMatch?.table !== table) {
|
|
703
|
+
const at = (pattern) => (codegen.base ? (pattern === "/" ? codegen.base : codegen.base + pattern) : pattern);
|
|
704
|
+
const match = compileRoutes(table.routes.filter((route) => route.page).map((route) => ({ path: at(route.pattern), render: () => route })));
|
|
705
|
+
routeMatch = { table, match: (target) => match(target)?.route.render(undefined, null) };
|
|
706
|
+
}
|
|
707
|
+
const route = routeMatch.match(url);
|
|
708
|
+
if (!route)
|
|
709
|
+
return [];
|
|
710
|
+
const files = new Set([route.page, route.notFound, route.error, ...route.layouts.map((layout) => layout.page)].filter(Boolean));
|
|
711
|
+
const found = reach.problems.filter((problem) => problem.files.some((file) => files.has(file)));
|
|
712
|
+
for (const problem of found)
|
|
713
|
+
if (!reported.has(problem.message)) {
|
|
714
|
+
reported.add(problem.message);
|
|
715
|
+
if (problem.severity === "error")
|
|
716
|
+
server.config.logger.error(problem.message);
|
|
717
|
+
else
|
|
718
|
+
server.config.logger.warn(problem.message);
|
|
719
|
+
}
|
|
720
|
+
return found;
|
|
721
|
+
};
|
|
566
722
|
// A source starts using a router feature (search params, navigate(), useParams()): generate it
|
|
567
723
|
// from now on (regenerate + full reload). A dev server never turns one off again.
|
|
568
724
|
const features = async (file) => {
|
|
@@ -660,7 +816,7 @@ export function rivetRouter(options = {}) {
|
|
|
660
816
|
...(i18n ? { i18n: { locales: i18n.locales } } : {}),
|
|
661
817
|
onError,
|
|
662
818
|
});
|
|
663
|
-
listener = createListener({ handler: devHandler(handler, server, () => table), compress: false }).listener;
|
|
819
|
+
listener = createListener({ handler: devHandler(handler, server, () => table, devReach), compress: false }).listener;
|
|
664
820
|
listeners.set(entry, listener);
|
|
665
821
|
}
|
|
666
822
|
// Vite's base middleware strips the base from `req.url`: route patterns carry it.
|
|
@@ -691,6 +847,8 @@ export function rivetRouter(options = {}) {
|
|
|
691
847
|
return [
|
|
692
848
|
router,
|
|
693
849
|
...plugins,
|
|
850
|
+
// Measurement only (`RIVET_LAZY_ROWS=swap`, dev / lazy-rows tools): inert otherwise.
|
|
851
|
+
lazyRowsHarness(() => resolve(root, outDir, "entry.server.tsx")),
|
|
694
852
|
...(adapter ? [targetEntryPlugin(adapter, { serverEntry: () => resolve(root, outDir, "entry.server.tsx"), mode: () => mode })] : []),
|
|
695
853
|
];
|
|
696
854
|
}
|
package/cli/rivet.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// Write the app's generated `.rivet/` files (route types, generated app, editor manifest) from its
|
|
5
5
|
// Vite config without starting Vite. Run before `tsc` / `rivet-check` on a fresh clone.
|
|
6
6
|
// rivet migrate server-exports [--routes src/routes] [--dry-run]
|
|
7
|
-
//
|
|
8
|
-
//
|
|
7
|
+
// Rewrite magic route server export names (in-file serverLoad, onGet…; sibling load, GET…) to
|
|
8
|
+
// wrapper calls (`export const loader = load(…)`, `export const GET = onGet(…)`), references renamed
|
|
9
|
+
// scope-aware, the `@rivetjs/router` import added.
|
|
9
10
|
// rivet mcp
|
|
10
11
|
// Start the Rivet MCP server (`@rivetjs/mcp`, stdio) installed in the app; same as `npx @rivetjs/mcp`.
|
|
11
12
|
import { relative } from "node:path";
|
package/client/dev.js
CHANGED
|
@@ -7,6 +7,23 @@ import { untrack } from "@rivetjs/reactivity";
|
|
|
7
7
|
import { devtoolsHook } from "@rivetjs/reactivity/debug";
|
|
8
8
|
import * as client from "./index.js";
|
|
9
9
|
export * from "./index.js";
|
|
10
|
+
// A failed client navigation (`_recover`, index.ts) loads its document after Vite's error overlay showed the error: once
|
|
11
|
+
// the overlay closes, or at once when none opens within 1.5 s (overlay off, error not reported).
|
|
12
|
+
client._recover.defer = (load) => {
|
|
13
|
+
const open = () => document.querySelector("vite-error-overlay");
|
|
14
|
+
const until = Date.now() + 1500;
|
|
15
|
+
const wait = () => {
|
|
16
|
+
if (open()) {
|
|
17
|
+
const watch = new MutationObserver(() => open() || (watch.disconnect(), load()));
|
|
18
|
+
watch.observe(document.documentElement, { childList: true, subtree: true });
|
|
19
|
+
}
|
|
20
|
+
else if (Date.now() > until)
|
|
21
|
+
load();
|
|
22
|
+
else
|
|
23
|
+
setTimeout(wait, 100);
|
|
24
|
+
};
|
|
25
|
+
wait();
|
|
26
|
+
};
|
|
10
27
|
const hook = devtoolsHook();
|
|
11
28
|
let table = null;
|
|
12
29
|
let shown = null;
|
package/client/index.d.ts
CHANGED
|
@@ -77,6 +77,10 @@ export declare const _transitions: {
|
|
|
77
77
|
* returns runs once the new page is in the document. Returns an unsubscribe function.
|
|
78
78
|
*/
|
|
79
79
|
export declare function onNavigate(fn: (nav: NavigationInfo) => void | (() => void)): () => void;
|
|
80
|
+
/** @internal Dev (`dev.ts`): the error overlay shows a failed navigation's error before its document loads. */
|
|
81
|
+
export declare const _recover: {
|
|
82
|
+
defer?: (load: () => void) => void;
|
|
83
|
+
};
|
|
80
84
|
/** @internal The navigation in flight (read by the transitions module when it installs). */
|
|
81
85
|
export declare const _current: () => NavigationInfo | null;
|
|
82
86
|
/** @internal Run the `onNavigate` callbacks' after-swap functions. */
|
package/client/index.js
CHANGED
|
@@ -204,6 +204,8 @@ export function onNavigate(fn) {
|
|
|
204
204
|
return () => void listeners.delete(fn);
|
|
205
205
|
}
|
|
206
206
|
function started(nav) {
|
|
207
|
+
if (recovering === undefined)
|
|
208
|
+
((recovering = false), addEventListener("error", recover), addEventListener("unhandledrejection", recover));
|
|
207
209
|
// Warmed data for other URLs is no longer wanted: abort it (the target's own stays for its load).
|
|
208
210
|
dropPrefetch(key(nav.to));
|
|
209
211
|
keep = 0;
|
|
@@ -217,6 +219,34 @@ function started(nav) {
|
|
|
217
219
|
}
|
|
218
220
|
_transitions.start?.(nav);
|
|
219
221
|
}
|
|
222
|
+
/** @internal Dev (`dev.ts`): the error overlay shows a failed navigation's error before its document loads. */
|
|
223
|
+
export const _recover = {};
|
|
224
|
+
// Listening (`false`, from the first navigation) / loading the document (`true`); `undefined`: not listening yet.
|
|
225
|
+
let recovering;
|
|
226
|
+
// The URL a navigation just committed (`swapRoute`; until the next frame): its page renders after the commit.
|
|
227
|
+
let rendering;
|
|
228
|
+
/**
|
|
229
|
+
* A client navigation whose render or commit throws (an uncaught error after the history push: before the routes
|
|
230
|
+
* region committed the URL, or inside its swap) loads the URL's document instead: URL and page never diverge. `location.replace`: the
|
|
231
|
+
* history entry is already there. A failing document load does not re-enter: once per URL within 10 s (session storage).
|
|
232
|
+
*/
|
|
233
|
+
function recover() {
|
|
234
|
+
const href = location.href, at = Date.now();
|
|
235
|
+
if (recovering || (href !== rendering && (!current || href === committed)))
|
|
236
|
+
return;
|
|
237
|
+
try {
|
|
238
|
+
// `<13-digit ms><href>` of the last recovery.
|
|
239
|
+
const last = sessionStorage["rivet:recover"];
|
|
240
|
+
if (last?.slice(13) === href && at - +last.slice(0, 13) < 1e4)
|
|
241
|
+
return;
|
|
242
|
+
sessionStorage["rivet:recover"] = at + href;
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// No session storage: recover without the cross-document loop guard.
|
|
246
|
+
}
|
|
247
|
+
recovering = true;
|
|
248
|
+
(_recover.defer ?? ((load) => load()))(() => location.replace(href));
|
|
249
|
+
}
|
|
220
250
|
/** @internal The navigation in flight (read by the transitions module when it installs). */
|
|
221
251
|
export const _current = () => current;
|
|
222
252
|
/** @internal Run the `onNavigate` callbacks' after-swap functions. */
|
|
@@ -334,6 +364,10 @@ export function swapRoute(apply, next) {
|
|
|
334
364
|
enter((index = history.state?.i ?? index + 1));
|
|
335
365
|
}
|
|
336
366
|
committed = href;
|
|
367
|
+
// Until the next frame's first task (the routes region renders the new page after the swap, up to a frame later), an
|
|
368
|
+
// uncaught error is this navigation's (`recover`).
|
|
369
|
+
if (nav)
|
|
370
|
+
((rendering = href), requestAnimationFrame(() => setTimeout(() => rendering === href && (rendering = undefined))));
|
|
337
371
|
if (nav && _transitions.swap)
|
|
338
372
|
return _transitions.swap(apply, next, nav);
|
|
339
373
|
apply();
|
|
@@ -26,7 +26,7 @@ export declare const ServerData: Context<ServerRoutePayload | null>;
|
|
|
26
26
|
export declare function actionResult(name?: string): ActionResult | undefined;
|
|
27
27
|
/**
|
|
28
28
|
* Typed: `actionResult<"items/[id]">("save")` (route id, generated `RouteActions`) or
|
|
29
|
-
* `actionResult<typeof
|
|
29
|
+
* `actionResult<typeof forms>()` (`forms = actions(…)`). Pass the name as the second type argument to narrow the
|
|
30
30
|
* result to that action (`actionResult<"items/[id]", "save">("save")`).
|
|
31
31
|
*/
|
|
32
32
|
export declare function actionResult<A extends (keyof RouteActions & string) | Record<string, unknown>, N extends ActionName<A> = ActionName<A>>(name?: N): ActionResultOf<A, N> | undefined;
|
package/index.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { JSX } from "@rivetjs/runtime/jsx-runtime";
|
|
2
2
|
import type { HttpError as HttpErrorType } from "./shared/errors.js";
|
|
3
3
|
import type { ActionFailure } from "./shared/actions.js";
|
|
4
|
+
import type { Deferred, SERVER_EXPORT } from "./shared/server-exports.js";
|
|
5
|
+
export { load, guard, actions, entries, headers, onRequest, onGet, onPost, onPut, onPatch, onDelete, onOptions } from "./shared/server-exports.js";
|
|
6
|
+
export type { ServerExport, ServerKind, Deferred, LoadOptions } from "./shared/server-exports.js";
|
|
4
7
|
export { HttpError, redirect, deny, notFound, error, isHttpError, type RedirectOptions } from "./shared/errors.js";
|
|
5
8
|
export { safeRedirect } from "./shared/redirect.js";
|
|
6
9
|
export { parseCookies } from "./shared/cookie.js";
|
|
@@ -16,7 +19,7 @@ export interface RouteParams {
|
|
|
16
19
|
/** Route id → loader data (routes with a loader). Augmented by the generated `.rivet/routes.d.ts`. */
|
|
17
20
|
export interface RouteData {
|
|
18
21
|
}
|
|
19
|
-
/** Route id → its `
|
|
22
|
+
/** Route id → its `actions(…)` object type (routes with server code). Augmented by `.rivet/routes.d.ts`. */
|
|
20
23
|
export interface RouteActions {
|
|
21
24
|
}
|
|
22
25
|
/** Layout id (`_layout`, `items/_layout`) → the params it receives. Augmented by `.rivet/routes.d.ts`. */
|
|
@@ -42,42 +45,36 @@ export type RoutePath = RouteRegistry extends {
|
|
|
42
45
|
/** A page path with optional query and hash: `<Link to>`. */
|
|
43
46
|
export type LinkHref = RoutePath | `${RoutePath}?${string}` | `${RoutePath}#${string}`;
|
|
44
47
|
declare const unchecked: unique symbol;
|
|
45
|
-
/** `PageProps<Unchecked<typeof
|
|
48
|
+
/** `PageProps<Unchecked<typeof loader>>`: loader data without the serializability check. */
|
|
46
49
|
export interface Unchecked<T> {
|
|
47
50
|
readonly [unchecked]: T;
|
|
48
51
|
}
|
|
49
52
|
/** Loader data the page receives must survive the resume encoder: no functions, symbols or class instances. */
|
|
50
53
|
export interface NotSerializable<T> {
|
|
51
|
-
"rivet: loader data must be serializable (plain objects, arrays, primitives, Date, URL, URLSearchParams, RegExp, Map, Set, typed arrays, ArrayBuffer, DataView, Error — no functions, symbols or class instances); opt out with PageProps<Unchecked<typeof
|
|
54
|
+
"rivet: loader data must be serializable (plain objects, arrays, primitives, Date, URL, URLSearchParams, RegExp, Map, Set, typed arrays, ArrayBuffer, DataView, Error — no functions, symbols or class instances); opt out with PageProps<Unchecked<typeof loader>>": T;
|
|
52
55
|
}
|
|
53
56
|
type Leaf = string | number | boolean | bigint | null | undefined | Date | URL | URLSearchParams | RegExp | ArrayBuffer | ArrayBufferView;
|
|
54
57
|
type Depth = [never, 0, 1, 2, 3, 4, 5, 6, 7];
|
|
55
58
|
type IsError<T> = T extends Error ? ("stack" extends keyof T ? true : false) : false;
|
|
56
|
-
type Serializable<T, D extends number = 8> = [D] extends [never] ? true : T extends Leaf ? true : T extends symbol | ((...args: never[]) => unknown) ? false : IsError<T> extends true ? true : T extends ReadonlyMap<infer K, infer V> ? Serializable<K, Depth[D]> extends true ? Serializable<V, Depth[D]> : false : T extends ReadonlySet<infer V> ? Serializable<V, Depth[D]> : T extends readonly (infer E)[] ? Serializable<E, Depth[D]> : T extends object ? false extends {
|
|
59
|
+
type Serializable<T, D extends number = 8> = [D] extends [never] ? true : T extends Leaf ? true : T extends Deferred<infer V> ? Serializable<V, D> : T extends symbol | ((...args: never[]) => unknown) ? false : IsError<T> extends true ? true : T extends ReadonlyMap<infer K, infer V> ? Serializable<K, Depth[D]> extends true ? Serializable<V, Depth[D]> : false : T extends ReadonlySet<infer V> ? Serializable<V, Depth[D]> : T extends readonly (infer E)[] ? Serializable<E, Depth[D]> : T extends object ? false extends {
|
|
57
60
|
[K in keyof T]-?: Serializable<T[K], Depth[D]>;
|
|
58
61
|
}[keyof T] ? false : true : true;
|
|
59
|
-
type Returned<L> = L extends (...args: never[]) => infer R ? Awaited<R> : L;
|
|
62
|
+
type Returned<L> = L extends (...args: never[]) => infer R ? (R extends Deferred<infer V> ? Deferred<V> : Awaited<R>) : L;
|
|
60
63
|
/**
|
|
61
|
-
* Data type of a loader (`DataOf<typeof
|
|
64
|
+
* Data type of a loader (`DataOf<typeof loader>`, `loader = load(…)`), checked to be serializable; plain data types
|
|
62
65
|
* pass through unchanged (`DataOf<Item[]>` is `Item[]`).
|
|
63
66
|
*/
|
|
64
67
|
export type DataOf<T> = T extends Unchecked<infer L> ? Returned<L> : T extends (...args: never[]) => unknown ? Serializable<Returned<T>> extends true ? Returned<T> : NotSerializable<Returned<T>> : T;
|
|
65
|
-
/**
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
/** Form actions of a route module (`serverActions`, or a sibling's old `actions`); `{}` without. */
|
|
76
|
-
export type ModuleActions<M> = M extends {
|
|
77
|
-
serverActions: infer A;
|
|
78
|
-
} ? A : M extends {
|
|
79
|
-
actions: infer A;
|
|
80
|
-
} ? A : {};
|
|
68
|
+
/** The export of module type `M` tagged as server export kind `K` (by tag, any name); `never` without. */
|
|
69
|
+
type Tagged<M, K> = {
|
|
70
|
+
[N in keyof M]: M[N] extends {
|
|
71
|
+
readonly [SERVER_EXPORT]: K;
|
|
72
|
+
} ? M[N] : never;
|
|
73
|
+
}[keyof M];
|
|
74
|
+
/** Loader data of a route module (`typeof import("./page")`): its `load(…)` export, any name; `unknown` without. */
|
|
75
|
+
export type ModuleData<M> = [Tagged<M, "load">] extends [never] ? unknown : DataOf<Tagged<M, "load">>;
|
|
76
|
+
/** Form actions of a route module: its `actions(…)` export, any name; `{}` without. */
|
|
77
|
+
export type ModuleActions<M> = [Tagged<M, "actions">] extends [never] ? {} : Tagged<M, "actions">;
|
|
81
78
|
/** A string that is not an id of `RouteParams` / `LayoutParams`: `data` and `params` are unusable. */
|
|
82
79
|
export interface UnknownRoute<Id> {
|
|
83
80
|
"rivet: not a route or layout id of this app (see .rivet/routes.d.ts)": Id;
|
|
@@ -89,7 +86,7 @@ type LayoutDataOf<D> = D extends LayoutId ? (D extends keyof LayoutData ? Layout
|
|
|
89
86
|
type LayoutParamsOf<D, P> = D extends LayoutId ? LayoutParams[D] : IsLiteral<D> extends true ? UnknownRoute<D> : P;
|
|
90
87
|
/**
|
|
91
88
|
* Props every page component receives (layouts: `LayoutProps`). `D` is a route id (generated
|
|
92
|
-
* types: `PageProps<"items/[id]">` types data, params), the loader (`PageProps<typeof
|
|
89
|
+
* types: `PageProps<"items/[id]">` types data, params), the loader (`PageProps<typeof loader>`
|
|
93
90
|
* infers the data) or the data type.
|
|
94
91
|
*/
|
|
95
92
|
export interface PageProps<D = unknown, P extends Record<string, string> = Record<string, string>> {
|
|
@@ -125,11 +122,11 @@ type Outcome<R> = R extends HttpErrorType | Response ? never : R extends ActionF
|
|
|
125
122
|
readonly status?: undefined;
|
|
126
123
|
};
|
|
127
124
|
type ActionsOf<A> = A extends RouteId ? (A extends keyof RouteActions ? RouteActions[A] : {}) : A;
|
|
128
|
-
/** Action names of a route id (`RouteActions`) or an actions object type (`typeof
|
|
125
|
+
/** Action names of a route id (`RouteActions`) or an actions object type (`typeof forms`, `forms = actions(…)`). */
|
|
129
126
|
export type ActionName<A> = keyof ActionsOf<A> & string;
|
|
130
127
|
/**
|
|
131
128
|
* Typed `actionResult()`: per action name its return value (`redirect()`/`Response` excluded), or
|
|
132
|
-
* the data and status of `fail()`. `A` is a route id or `typeof
|
|
129
|
+
* the data and status of `fail()`. `A` is a route id or `typeof forms` (`forms = actions(…)`).
|
|
133
130
|
*/
|
|
134
131
|
export type ActionResultOf<A, N extends ActionName<A> = ActionName<A>> = {
|
|
135
132
|
[K in N]: {
|
package/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
// Route-module API: control-flow helpers and types for guards, loaders and actions.
|
|
1
|
+
// Route-module API: server export wrappers, control-flow helpers and types for guards, loaders and actions.
|
|
2
|
+
export { load, guard, actions, entries, headers, onRequest, onGet, onPost, onPut, onPatch, onDelete, onOptions } from "./shared/server-exports.js";
|
|
2
3
|
export { HttpError, redirect, deny, notFound, error, isHttpError } from "./shared/errors.js";
|
|
3
4
|
export { safeRedirect } from "./shared/redirect.js";
|
|
4
5
|
export { parseCookies } from "./shared/cookie.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rivetjs/router",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
4
|
"description": "File routing for Rivet: loaders, actions, guards, a web-standard request handler, Node / static / edge adapters.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -114,9 +114,9 @@
|
|
|
114
114
|
}
|
|
115
115
|
},
|
|
116
116
|
"dependencies": {
|
|
117
|
-
"@rivetjs/runtime": "0.1.0-alpha.
|
|
118
|
-
"@rivetjs/reactivity": "0.1.0-alpha.
|
|
119
|
-
"@rivetjs/compiler": "0.1.0-alpha.
|
|
117
|
+
"@rivetjs/runtime": "0.1.0-alpha.1",
|
|
118
|
+
"@rivetjs/reactivity": "0.1.0-alpha.1",
|
|
119
|
+
"@rivetjs/compiler": "0.1.0-alpha.1"
|
|
120
120
|
},
|
|
121
121
|
"engines": {
|
|
122
122
|
"node": ">=24"
|
package/server/app.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { ActionResult, Head, HeadSink, LoaderDeps } from "../shared/data.js
|
|
|
5
5
|
import { type CSPOptions } from "../shared/csp.js";
|
|
6
6
|
import { type InlineStyleManifest } from "./inline-styles.js";
|
|
7
7
|
import type { TrailingSlash } from "../shared/slash.js";
|
|
8
|
-
import { type AppHandler, type AppRoute, type BodyLimit, type ErrorInfo } from "./handler.js";
|
|
8
|
+
import { type AppHandler, type AppRoute, type BodyLimit, type ErrorInfo, type ShellPhase } from "./handler.js";
|
|
9
9
|
import { type DocumentShell } from "./render.js";
|
|
10
10
|
import type { EarlyHintsOptions } from "./early-hints.js";
|
|
11
11
|
import type { RouteCacheOptions, RouteRules } from "./route-cache.js";
|
|
@@ -28,6 +28,12 @@ export interface PageInput {
|
|
|
28
28
|
readonly byURL?: readonly number[];
|
|
29
29
|
/** Loaders' `event.depends()` keys (absent when none called it): resumed with the layout state for `invalidate(key)`. */
|
|
30
30
|
readonly deps?: LoaderDeps;
|
|
31
|
+
/** A `data` slot is a deferred loader's handle (`load(fn, { defer: true })`): the page streams (out of order) even without `stream`. */
|
|
32
|
+
readonly deferred?: true;
|
|
33
|
+
/** A deferred loader's redirect target, read at the document's end (client redirect). */
|
|
34
|
+
readonly moved?: () => string | undefined;
|
|
35
|
+
/** @internal Route rule `shell`: a shell fill / replay (always streams). */
|
|
36
|
+
readonly shell?: ShellPhase;
|
|
31
37
|
}
|
|
32
38
|
/** Shape of the generated `.rivet/entry.server.tsx` module. */
|
|
33
39
|
export interface GeneratedEntry {
|