@rivetjs/router 0.0.0-stage → 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 (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -2
  3. package/adapters/assemble.d.ts +32 -0
  4. package/adapters/assemble.js +32 -0
  5. package/adapters/build.d.ts +24 -0
  6. package/adapters/build.js +43 -0
  7. package/adapters/bun.d.ts +18 -0
  8. package/adapters/bun.js +44 -0
  9. package/adapters/cloudflare.d.ts +5 -0
  10. package/adapters/cloudflare.js +8 -0
  11. package/adapters/cluster.d.ts +7 -0
  12. package/adapters/cluster.js +130 -0
  13. package/adapters/csr.d.ts +12 -0
  14. package/adapters/csr.js +11 -0
  15. package/adapters/deno.d.ts +13 -0
  16. package/adapters/deno.js +35 -0
  17. package/adapters/fetch.d.ts +55 -0
  18. package/adapters/fetch.js +72 -0
  19. package/adapters/files.d.ts +20 -0
  20. package/adapters/files.js +155 -0
  21. package/adapters/idle-memory.d.ts +23 -0
  22. package/adapters/idle-memory.js +91 -0
  23. package/adapters/netlify.d.ts +13 -0
  24. package/adapters/netlify.js +15 -0
  25. package/adapters/node.d.ts +180 -0
  26. package/adapters/node.js +986 -0
  27. package/adapters/pack.d.ts +39 -0
  28. package/adapters/pack.js +360 -0
  29. package/adapters/server-target.d.ts +17 -0
  30. package/adapters/server-target.js +8 -0
  31. package/adapters/static.d.ts +84 -0
  32. package/adapters/static.js +231 -0
  33. package/adapters/target-build.d.ts +95 -0
  34. package/adapters/target-build.js +118 -0
  35. package/adapters/vercel-edge.d.ts +7 -0
  36. package/adapters/vercel-edge.js +9 -0
  37. package/adapters/vercel.d.ts +35 -0
  38. package/adapters/vercel.js +45 -0
  39. package/build/codegen.d.ts +97 -0
  40. package/build/codegen.js +1037 -0
  41. package/build/dev-errors.d.ts +23 -0
  42. package/build/dev-errors.js +247 -0
  43. package/build/lazy-rows.d.ts +19 -0
  44. package/build/lazy-rows.js +59 -0
  45. package/build/migrate.d.ts +31 -0
  46. package/build/migrate.js +385 -0
  47. package/build/reach.d.ts +60 -0
  48. package/build/reach.js +302 -0
  49. package/build/route-css.d.ts +54 -0
  50. package/build/route-css.js +216 -0
  51. package/build/routes.d.ts +55 -0
  52. package/build/routes.js +219 -0
  53. package/build/sync.d.ts +20 -0
  54. package/build/sync.js +16 -0
  55. package/build/vite.d.ts +208 -0
  56. package/build/vite.js +861 -0
  57. package/cli/rivet.d.ts +2 -0
  58. package/cli/rivet.js +101 -0
  59. package/client/active.d.ts +6 -0
  60. package/client/active.js +22 -0
  61. package/client/announce.d.ts +2 -0
  62. package/client/announce.js +21 -0
  63. package/client/dev.d.ts +41 -0
  64. package/client/dev.js +91 -0
  65. package/client/index.d.ts +156 -0
  66. package/client/index.js +714 -0
  67. package/client/layouts.d.ts +110 -0
  68. package/client/layouts.js +310 -0
  69. package/client/leave.d.ts +40 -0
  70. package/client/leave.js +75 -0
  71. package/client/link.d.ts +48 -0
  72. package/client/link.jsx +65 -0
  73. package/client/live.d.ts +57 -0
  74. package/client/live.js +170 -0
  75. package/client/navigate.d.ts +71 -0
  76. package/client/navigate.js +120 -0
  77. package/client/navigation.d.ts +28 -0
  78. package/client/navigation.js +14 -0
  79. package/client/params.d.ts +16 -0
  80. package/client/params.js +28 -0
  81. package/client/route-context.d.ts +32 -0
  82. package/client/route-context.js +21 -0
  83. package/client/scroll.d.ts +16 -0
  84. package/client/scroll.js +80 -0
  85. package/client/search.d.ts +8 -0
  86. package/client/search.js +17 -0
  87. package/client/tailwind.css +8 -0
  88. package/client/transition-helpers.d.ts +28 -0
  89. package/client/transition-helpers.js +63 -0
  90. package/client/transitions.css +90 -0
  91. package/client/transitions.d.ts +21 -0
  92. package/client/transitions.js +156 -0
  93. package/index.d.ts +135 -0
  94. package/index.js +8 -0
  95. package/package.json +156 -4
  96. package/server/app.d.ts +175 -0
  97. package/server/app.js +335 -0
  98. package/server/body.d.ts +20 -0
  99. package/server/body.js +8 -0
  100. package/server/deferred.d.ts +12 -0
  101. package/server/deferred.js +60 -0
  102. package/server/early-hints.d.ts +34 -0
  103. package/server/early-hints.js +63 -0
  104. package/server/handler.d.ts +240 -0
  105. package/server/handler.js +661 -0
  106. package/server/index.d.ts +9 -0
  107. package/server/index.js +6 -0
  108. package/server/inline-styles.d.ts +21 -0
  109. package/server/inline-styles.js +28 -0
  110. package/server/module.d.ts +14 -0
  111. package/server/module.js +96 -0
  112. package/server/render.d.ts +96 -0
  113. package/server/render.js +492 -0
  114. package/server/route-cache.d.ts +245 -0
  115. package/server/route-cache.js +813 -0
  116. package/server/security.d.ts +18 -0
  117. package/server/security.js +47 -0
  118. package/server/shell-build.d.ts +3 -0
  119. package/server/shell-build.js +2 -0
  120. package/server/shell-cache.d.ts +8 -0
  121. package/server/shell-cache.js +836 -0
  122. package/server/strict-csp.d.ts +7 -0
  123. package/server/strict-csp.js +18 -0
  124. package/server-only.d.ts +1 -0
  125. package/server-only.js +3 -0
  126. package/shared/actions.d.ts +10 -0
  127. package/shared/actions.js +18 -0
  128. package/shared/base.d.ts +8 -0
  129. package/shared/base.js +20 -0
  130. package/shared/cookie.d.ts +7 -0
  131. package/shared/cookie.js +32 -0
  132. package/shared/csp-directives.d.ts +9 -0
  133. package/shared/csp-directives.js +85 -0
  134. package/shared/csp-hash.d.ts +39 -0
  135. package/shared/csp-hash.js +121 -0
  136. package/shared/csp.d.ts +26 -0
  137. package/shared/csp.js +13 -0
  138. package/shared/data.d.ts +175 -0
  139. package/shared/data.js +22 -0
  140. package/shared/errors.d.ts +31 -0
  141. package/shared/errors.js +51 -0
  142. package/shared/head.d.ts +57 -0
  143. package/shared/head.js +242 -0
  144. package/shared/nonce.d.ts +2 -0
  145. package/shared/nonce.js +23 -0
  146. package/shared/prefetch-fallback.min.d.ts +1 -0
  147. package/shared/prefetch-fallback.min.js +2 -0
  148. package/shared/prefetch.d.ts +54 -0
  149. package/shared/prefetch.js +74 -0
  150. package/shared/redirect.d.ts +6 -0
  151. package/shared/redirect.js +25 -0
  152. package/shared/server-exports.d.ts +78 -0
  153. package/shared/server-exports.js +77 -0
  154. package/shared/slash.d.ts +8 -0
  155. package/shared/slash.js +24 -0
package/build/vite.js ADDED
@@ -0,0 +1,861 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ // Vite plugin: scan routes, write `.rivet/` codegen, compile it with the Rivet plugin (resume mode
10
+ // for server/static apps), default the build inputs, and serve requests in dev.
11
+ import { lstat, mkdir, readdir, readFile, realpath, rm, writeFile } from "node:fs/promises";
12
+ import { dirname, join, relative, resolve, sep } from "node:path";
13
+ import { fileURLToPath, pathToFileURL } from "node:url";
14
+ import { rivet, _factsResolve, declaredServerOnly, isServerOnly, serverOnlyStub, } from "@rivetjs/compiler/vite";
15
+ import { updateEditorManifest } from "@rivetjs/compiler/editor";
16
+ import { scanRoutes, _serverExportsOf } from "./routes.js";
17
+ import { generate } from "./codegen.js";
18
+ import { normalizeBase } from "../shared/base.js";
19
+ import { slashPath } from "../shared/slash.js";
20
+ import { DATA_PARAM } from "../shared/data.js";
21
+ import { compileRules } from "../server/route-cache.js";
22
+ /** Build-time helper for other Vite plugins (docs-kit example frames): one merged sheet per stylesheet list. */
23
+ export { mergeRouteCSS } from "./route-css.js";
24
+ import { bundleRouteCSS, devRouteCSS, INLINE_CSS, inlineCSS, mergeRouteCSS, ROUTE_CSS } from "./route-css.js";
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";
29
+ import { adapterOptions, targetBuildConfig, targetEntryPlugin } from "../adapters/target-build.js";
30
+ /** The router's own base-path module (its `BASE` reads Vite's `BASE_URL`). */
31
+ const BASE_MODULE = fileURLToPath(new URL("../shared/base.js", import.meta.url));
32
+ /** The router's trailing-slash module (its `TRAILING_SLASH` constant is rewritten when the option is set). */
33
+ const SLASH_MODULE = fileURLToPath(new URL("../shared/slash.js", import.meta.url));
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));
41
+ const posix = (path) => path.split(sep).join("/");
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];
49
+ /** Write generated files whose contents changed; returns whether anything changed. */
50
+ async function sync(dir, files) {
51
+ await mkdir(dir, { recursive: true });
52
+ let changed = false;
53
+ for (const [name, contents] of Object.entries(files)) {
54
+ const path = join(dir, name);
55
+ const current = await readFile(path, "utf8").catch(() => null);
56
+ if (current === contents)
57
+ continue;
58
+ await writeFile(path, contents);
59
+ changed = true;
60
+ }
61
+ return changed;
62
+ }
63
+ const SOURCE = /\.[cm]?[jt]sx?$/;
64
+ const FEATURES = [
65
+ ["searchParams", /\b(?:useSearchParams|SearchParams)\b/],
66
+ ["navigate", /\b(?:navigate|invalidate|revalidate|setSearchParams)\b/],
67
+ ["params", /\buseParams\b/],
68
+ ["activeLinks", /<Link\b[^>]*?\sactive\b/],
69
+ ["prefetch", /\bprefetch\s*=\s*\{?\s*["'](?:document|prerender)["']/],
70
+ ];
71
+ /** Every `RouterUsage` flag (FEATURES + `forms`). */
72
+ const USAGE = [...FEATURES.map(([name]) => name), "forms"];
73
+ const none = () => ({ searchParams: false, navigate: false, forms: false, params: false, activeLinks: false, prefetch: false });
74
+ /** Features one source text uses: names in a file importing `@rivetjs/router` (`URLSearchParams` does not count). */
75
+ function featuresOf(text, into) {
76
+ // Forms (enhanced submits follow redirects / revalidate through the router): `@rivetjs/forms`, or ui's `Form`.
77
+ if (text.includes("@rivetjs/forms") || (text.includes("@rivetjs/ui") && /\bForm\b/.test(text)))
78
+ into.forms = into.navigate = true;
79
+ if (!text.includes("@rivetjs/router"))
80
+ return;
81
+ for (const [name, pattern] of FEATURES)
82
+ if (pattern.test(text))
83
+ into[name] = true;
84
+ }
85
+ /** Never sources: dot entries (`.rivet`, `.git`), `node_modules`, build output (`dist`, `dist-<lane>`, `dist.x`). */
86
+ const OUTPUT = /^(?:\.|node_modules$|dist(?:$|[-_.]))/;
87
+ async function scan(dir, into, seen) {
88
+ if (seen.has(dir))
89
+ return;
90
+ seen.add(dir);
91
+ // Concurrent builds may create or remove files mid-walk: a vanished entry is skipped (ENOENT).
92
+ for (const entry of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
93
+ const path = join(dir, entry.name);
94
+ if (entry.isDirectory()) {
95
+ if (!OUTPUT.test(entry.name))
96
+ await scan(path, into, seen);
97
+ }
98
+ else if (!entry.name.startsWith(".") && SOURCE.test(entry.name)) {
99
+ const text = await readFile(path, "utf8").catch((error) => {
100
+ if (error.code === "ENOENT" || error.code === "EISDIR")
101
+ return "";
102
+ throw error;
103
+ });
104
+ featuresOf(text, into);
105
+ }
106
+ }
107
+ }
108
+ /**
109
+ * Which router features the project's sources use: every source under `root` (skipping
110
+ * `node_modules`, `dist` and dot directories) plus the sources of linked (workspace / `npm link`)
111
+ * dependencies of `root/package.json`, except `@rivetjs/*` packages. Text based: a name that appears
112
+ * in a file importing `@rivetjs/router` counts (false positives only cost bytes); published
113
+ * dependencies are not scanned (set `rivetRouter({ navigate, params, searchParams })`).
114
+ */
115
+ export async function routerUsage(root, options = {}) {
116
+ const usage = none(),
117
+ // `exclude`: absolute directories never walked (the Vite build `outDir`, the generated `.rivet/`).
118
+ seen = new Set((options.exclude ?? []).map((dir) => resolve(root, dir)).filter((dir) => dir !== resolve(root)));
119
+ await scan(root, usage, seen);
120
+ const pkg = JSON.parse(await readFile(join(root, "package.json"), "utf8").catch(() => "{}"));
121
+ for (const name of Object.keys({ ...pkg.dependencies, ...pkg.devDependencies })) {
122
+ if (name.startsWith("@rivetjs/"))
123
+ continue;
124
+ for (let dir = root;; dir = dirname(dir)) {
125
+ const path = join(dir, "node_modules", name);
126
+ const link = await lstat(path).catch(() => null);
127
+ if (link?.isSymbolicLink())
128
+ await scan(await realpath(path), usage, seen);
129
+ if (link || dir === dirname(dir))
130
+ break;
131
+ }
132
+ }
133
+ return usage;
134
+ }
135
+ /** Regenerate `.rivet/` for a project root; returns the resume modules and whether files changed. */
136
+ export async function generateRoutes(root, options = {}) {
137
+ const routesDir = posix(options.routes ?? "src/routes");
138
+ const outDir = posix(options.outDir ?? ".rivet");
139
+ const table = await scanRoutes(resolve(root, routesDir));
140
+ const generated = generate(table, {
141
+ routesDir,
142
+ outDir,
143
+ document: posix(options.document ?? "src/document.ts"),
144
+ mode: options.mode ?? "server",
145
+ ...(options.head ? { head: options.head } : {}),
146
+ ...(options.transitions ? { transitions: options.transitions } : {}),
147
+ ...(options.announce === false ? { announce: false } : {}),
148
+ ...(options.base ? { base: options.base } : {}),
149
+ ...(options.searchParams ? { searchParams: true } : {}),
150
+ ...(options.navigate ? { navigate: true } : {}),
151
+ ...(options.forms ? { forms: true } : {}),
152
+ ...(options.params ? { params: true } : {}),
153
+ ...(options.activeLinks ? { activeLinks: true } : {}),
154
+ ...(options.prefetch ? { prefetch: options.prefetch } : {}),
155
+ ...(options.prefetchEagerness ? { prefetchEagerness: options.prefetchEagerness } : {}),
156
+ ...(options.prefetchPaths === false ? { prefetchPaths: false } : {}),
157
+ ...(options.trailingSlash && options.trailingSlash !== "ignore" ? { trailingSlash: options.trailingSlash } : {}),
158
+ ...(options.preload?.background !== undefined ? { preloadBackground: options.preload.background } : {}),
159
+ ...(options.routeRules && Object.keys(options.routeRules).length ? { routeRules: options.routeRules } : {}),
160
+ ...(options.clientRouter !== undefined ? { clientRouter: options.clientRouter } : {}),
161
+ ...(options.documents?.length ? { documents: options.documents } : {}),
162
+ ...(options.derived ? { derived: true } : {}),
163
+ });
164
+ const changed = await sync(resolve(root, outDir), generated.files);
165
+ // No router contexts: the compiler's default resume entry (drop a stale generated one).
166
+ const resumeEntry = "entry.resume.ts" in generated.files;
167
+ if (!resumeEntry)
168
+ await rm(resolve(root, outDir, "entry.resume.ts"), { force: true });
169
+ // Editors: route ids/patterns/files for links (go to route, unknown-route warnings). Best effort.
170
+ try {
171
+ const routes = table.routes.filter((route) => route.page).map((route) => ({ id: route.id, pattern: route.pattern, file: routesDir + "/" + route.page }));
172
+ updateEditorManifest(root, { routesDir, routes });
173
+ }
174
+ catch { }
175
+ return { table, resumeModules: generated.resumeModules, changed, resumeEntry };
176
+ }
177
+ /** Routes-relative modules that declare server exports (in-file pages/layouts, plain `.ts` endpoints). */
178
+ function serverFiles(table) {
179
+ const files = new Set();
180
+ for (const route of table.routes)
181
+ for (const module of [route, ...route.layouts]) {
182
+ if (module.inline && module.page)
183
+ files.add(module.page);
184
+ // Live pages (`export const live = true`) as `live:<page>`: toggling the flag regenerates the App.
185
+ if (module.live && module.page)
186
+ files.add("live:" + module.page);
187
+ if (module.server && !/\.server\.[cm]?[jt]s$/.test(module.server))
188
+ files.add(module.server);
189
+ }
190
+ return files;
191
+ }
192
+ /** Root-relative page/layout/special modules of a table: compiled with `serverExports`. */
193
+ function routeModules(table, routesDir) {
194
+ const files = new Set();
195
+ for (const route of table.routes)
196
+ for (const module of [route, ...route.layouts]) {
197
+ if (module.page)
198
+ files.add(module.page);
199
+ // Plain `.ts` endpoints too (T7): a page importing a constant from one gets its handlers split out.
200
+ if (module.server && !/\.server\.[cm]?[jt]s$/.test(module.server))
201
+ files.add(module.server);
202
+ }
203
+ return [...files].map((file) => posix(join(routesDir, file)));
204
+ }
205
+ /**
206
+ * File routes for Vite: the routing plugin plus the Rivet compiler configured for the generated app.
207
+ * `vite build` builds the browser side into `dist/client`; `vite build --ssr` the server entry into
208
+ * `dist/server` (both overridable through `build`). `vite` serves pages through the same handler.
209
+ * In `csr` mode the client build also writes `index.html` and there is no server build.
210
+ */
211
+ const STATIC_DATA = /^([^?#]*\/)_data\.json(?:[?#]|$)/;
212
+ /**
213
+ * Dev server, static mode: a `<page>/_data.json` request (the file the static adapter prerenders
214
+ * beside each page, `client/index.ts` `dataURL(url, "static")`) becomes that page's data request,
215
+ * as `adapters/static.ts` `prerender` renders it: the page path in its canonical form, no query.
216
+ */
217
+ function staticData(req, slash) {
218
+ const hit = STATIC_DATA.exec(req.url ?? "");
219
+ if (!hit || (req.method !== "GET" && req.method !== "HEAD"))
220
+ return;
221
+ const page = hit[1] === "/" ? "/" : hit[1].slice(0, -1);
222
+ req.url = slashPath(page, slash) + "?" + DATA_PARAM + "=data";
223
+ req.headers.accept = "application/json";
224
+ }
225
+ /** The router's Vite plugins: file routes, typed routes, server code split, data requests and build adapters. */
226
+ export function rivetRouter(options = {}) {
227
+ const routesDir = options.routes ?? "src/routes";
228
+ const outDir = options.outDir ?? ".rivet";
229
+ const styles = options.styles ?? [];
230
+ const mode = options.mode ?? "server";
231
+ const documentPath = options.document ?? "src/document.ts";
232
+ const codegen = {
233
+ routes: routesDir,
234
+ outDir,
235
+ document: documentPath,
236
+ mode,
237
+ transitions: options.transitions,
238
+ announce: options.announce,
239
+ ...(options.trailingSlash ? { trailingSlash: options.trailingSlash } : {}),
240
+ ...(options.prefetchEagerness ? { prefetchEagerness: options.prefetchEagerness } : {}),
241
+ ...(options.prefetchPaths === false ? { prefetchPaths: false } : {}),
242
+ ...(options.preload ? { preload: options.preload } : {}),
243
+ ...(options.routeRules ? { routeRules: options.routeRules } : {}),
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 } : {}),
247
+ };
248
+ if (options.routeRules !== undefined) {
249
+ // Checked at config time (same checks as at server start, except the origin `isr` needs: a server option), and
250
+ // kept to JSON (the server entry inlines it).
251
+ compileRules(options.routeRules, true);
252
+ const json = (value) => typeof value === "string" ||
253
+ typeof value === "boolean" ||
254
+ (typeof value === "number" && Number.isFinite(value)) ||
255
+ (Array.isArray(value)
256
+ ? value.every(json)
257
+ : typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype && Object.values(value).every(json));
258
+ if (!json(options.routeRules))
259
+ throw new Error("rivetRouter: routeRules must be JSON data (numbers, strings, booleans; use isr: true for no expiry)");
260
+ }
261
+ const background = options.preload?.background;
262
+ if (background !== undefined && !(Number.isFinite(background) && background >= 0))
263
+ throw new Error(`rivetRouter: preload.background must be a non-negative number of bytes (got ${JSON.stringify(background)})`);
264
+ if (options.prefetch !== undefined && typeof options.prefetch !== "boolean" && options.prefetch !== "document" && options.prefetch !== "prerender")
265
+ throw new Error(`rivetRouter: prefetch must be a boolean, "document" or "prerender" (got ${JSON.stringify(options.prefetch)})`);
266
+ if (options.prefetchEagerness !== undefined && !["conservative", "moderate", "eager"].includes(options.prefetchEagerness))
267
+ throw new Error(`rivetRouter: prefetchEagerness must be "conservative", "moderate" or "eager" (got ${JSON.stringify(options.prefetchEagerness)})`);
268
+ if (options.prefetchPaths !== undefined && typeof options.prefetchPaths !== "boolean")
269
+ throw new Error(`rivetRouter: prefetchPaths must be a boolean (got ${JSON.stringify(options.prefetchPaths)})`);
270
+ const inlineOption = options.inlineStylesheets;
271
+ if (inlineOption !== undefined &&
272
+ !(typeof inlineOption === "string"
273
+ ? ["auto", "always", "never"].includes(inlineOption)
274
+ : typeof inlineOption === "object" &&
275
+ inlineOption !== null &&
276
+ Object.keys(inlineOption).every((key) => key === "maxTotalBrotli") &&
277
+ (inlineOption.maxTotalBrotli === undefined || (Number.isSafeInteger(inlineOption.maxTotalBrotli) && inlineOption.maxTotalBrotli >= 0))))
278
+ throw new Error(`rivetRouter: inlineStylesheets must be "auto", "always", "never" or { maxTotalBrotli: <bytes> } (got ${JSON.stringify(inlineOption)})`);
279
+ const serverOnly = serverOnlyOption(options.serverOnly);
280
+ const slash = options.trailingSlash ?? "ignore";
281
+ const shellBuild = Object.values(options.routeRules ?? {}).some((rule) => Boolean(rule?.shell));
282
+ if (slash !== "ignore" && slash !== "never" && slash !== "always")
283
+ throw new Error(`rivetRouter: trailingSlash must be "ignore", "never" or "always" (got ${JSON.stringify(slash)})`);
284
+ // Filled in `config`, which runs before the compiler plugin reads it in `configResolved`.
285
+ // Page chunks prefetch on navigation intent, not at idle (the App chunk loads eagerly for clicks).
286
+ // The generated App prefetches the hovered route's page itself: page chunks are not chunk lazies (`lazyOwned`).
287
+ const resume = {
288
+ lazy: "intent",
289
+ ...options.resume,
290
+ lazyOwned: [...(options.resume?.lazyOwned ?? []), posix(join(outDir, "app.tsx"))],
291
+ modules: [...(options.resume?.modules ?? [])],
292
+ dev: options.dev?.resume !== false,
293
+ // Every app component module a route imports resumes without being listed in `resume.modules`.
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"))] } : {}),
300
+ };
301
+ // The generated resume entry (when there is one) provides the router's contexts (navigation state,
302
+ // search params) to resumed regions.
303
+ const ownEntry = resume.entry;
304
+ // Dev resumed pages read it live (the compiler's dev manifest): kept in step on regeneration too.
305
+ const entryFor = (generated) => {
306
+ if (ownEntry)
307
+ resume.entry = ownEntry;
308
+ else if (generated)
309
+ resume.entry = posix(join(outDir, "entry.resume.ts"));
310
+ else
311
+ delete resume.entry;
312
+ };
313
+ let root = process.cwd();
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
+ };
342
+ const devtools = options.devtools ?? true;
343
+ const css = options.css ? { css: options.css } : {};
344
+ // Filled in `config` and on route changes: in-file server exports leave the browser build.
345
+ const serverExports = { modules: [] };
346
+ let inFile = new Set(); // routes-relative files with server exports + `live:<page>` (dev change detection)
347
+ // Dev regenerations run one at a time (two writing `.rivet/` at once race); closing the server
348
+ // waits for the running one and drops queued ones.
349
+ let regenerations = Promise.resolve();
350
+ let closed = false;
351
+ const icons = options.staticIcons === undefined ? {} : { staticIcons: options.staticIcons };
352
+ const compilerOptions = mode === "csr" ? { devtools, serverExports, ...css, ...icons } : { resume, devtools, serverExports, ...css, ...icons };
353
+ const bridges = options.bridges?.length ? options.bridges : undefined;
354
+ if (bridges && mode === "csr")
355
+ throw new Error("rivetRouter: framework bridges need server or static mode (islands are server-rendered)");
356
+ const plugins = options.images || bridges
357
+ ? rivet({ ...compilerOptions, images: options.images, bridges })
358
+ : [rivet(compilerOptions)];
359
+ // With bridges the framework plugins come first: the compiler is the plugin named `vite-plugin-rivet`.
360
+ const compiler = plugins.find((plugin) => plugin.name === "vite-plugin-rivet");
361
+ const clientEntry = () => resolve(root, outDir, "entry.client.tsx");
362
+ const adapter = adapterOptions(options.adapter);
363
+ if (adapter && mode === "csr")
364
+ throw new Error("rivetRouter: CSR apps have no server build (adapter)");
365
+ // i18n client builds: the locale suffix of Vite's base (`/<locale>`), and the app base without it.
366
+ let suffix = "", appBase = "";
367
+ // i18n client builds: RegExp source of the app paths this build's locale serves (other locales' URLs load documents).
368
+ let localePaths = null;
369
+ // i18n dev server: every locale's path pattern (one client for all locales; see the base.ts transform).
370
+ let devLocalePaths = null;
371
+ // Z5: the client build's output (captured before Vite rewrites dynamic imports) and, per importing
372
+ // chunk, the files its static graph already loaded.
373
+ let clientBundle;
374
+ const loadedBy = new Map();
375
+ const preloadDeps = (file, deps, { hostId }) => {
376
+ if (!clientBundle)
377
+ return deps;
378
+ let loaded = loadedBy.get(hostId);
379
+ if (!loaded) {
380
+ loaded = new Set();
381
+ for (const stack = [hostId]; stack.length;) {
382
+ const next = stack.pop();
383
+ if (loaded.has(next))
384
+ continue;
385
+ loaded.add(next);
386
+ const chunk = clientBundle[next];
387
+ if (chunk?.type === "chunk")
388
+ stack.push(...chunk.imports);
389
+ }
390
+ loadedBy.set(hostId, loaded);
391
+ }
392
+ const rest = deps.filter((dep) => !loaded.has(dep));
393
+ // Only the imported chunk itself left: `import()` fetches it anyway (a preload list would repeat its URL).
394
+ return rest.length === 1 && rest[0] === file ? [] : rest;
395
+ };
396
+ const router = {
397
+ name: "vite-plugin-rivet-router",
398
+ enforce: "pre",
399
+ async config(config, env) {
400
+ root = resolve(config.root ?? process.cwd());
401
+ // `trailingSlash`: the server bundle must carry its own copy of the router (its policy constant
402
+ // is rewritten in this build), even where the package would otherwise be external.
403
+ if (slash !== "ignore") {
404
+ const kept = config.ssr?.noExternal;
405
+ if (kept !== true)
406
+ (config.ssr ??= {}).noExternal = [...(kept === undefined ? [] : Array.isArray(kept) ? kept : [kept]), "@rivetjs/router"];
407
+ }
408
+ const names = new Set();
409
+ let i18nLocale = null;
410
+ localePaths = null;
411
+ devLocalePaths = null;
412
+ const scan = (list) => {
413
+ if (Array.isArray(list))
414
+ list.forEach(scan);
415
+ else if (list && typeof list === "object" && "name" in list) {
416
+ const name = String(list.name);
417
+ names.add(name);
418
+ if (name === "vite-plugin-rivet-i18n") {
419
+ const api = list.api;
420
+ i18nLocale = api?.locale ?? null;
421
+ localePaths = api?.clientPaths ?? null;
422
+ if (env.command === "serve")
423
+ devLocalePaths = api?.devPaths ?? null;
424
+ }
425
+ }
426
+ };
427
+ scan(config.plugins);
428
+ // `@rivetjs/i18n/vite` client builds (one per locale, its config hook runs first) serve a
429
+ // locale's assets under `<base><locale>/`; routes and `BASE` keep the app base.
430
+ suffix = i18nLocale && env.command === "build" && !config.build?.ssr && !env.isSsrBuild ? "/" + i18nLocale : "";
431
+ // One base for assets (Vite) and routes (codegen).
432
+ if (options.base !== undefined)
433
+ config.base = normalizeBase(options.base) + suffix + "/";
434
+ appBase = normalizeBase(config.base);
435
+ if (suffix && appBase.endsWith(suffix))
436
+ appBase = appBase.slice(0, -suffix.length);
437
+ codegen.base = appBase;
438
+ // Kit's plugin (`kit()` from @rivetjs/kit/vite) turns component head on unless `head: "early"`.
439
+ const head = options.head ?? (names.has("vite-plugin-rivet-kit") ? "components" : "early");
440
+ if (head === "components" && mode !== "csr")
441
+ codegen.head = "components";
442
+ const usage = await routerUsage(root, { exclude: [config.build?.outDir ?? "dist", outDir] });
443
+ for (const name of USAGE)
444
+ codegen[name] = options[name] ?? usage[name];
445
+ const generated = await generateRoutes(root, codegen);
446
+ entryFor(generated.resumeEntry);
447
+ const { resumeModules } = generated;
448
+ table = generated.table;
449
+ serverExports.modules.splice(0, serverExports.modules.length, ...routeModules(table, routesDir));
450
+ inFile = serverFiles(table);
451
+ const extra = options.resume?.modules ?? [];
452
+ resume.modules.splice(0, resume.modules.length, ...new Set([...resumeModules, ...extra]));
453
+ const ssr = config.build?.ssr;
454
+ const input = config.build?.rollupOptions?.input;
455
+ if (ssr && mode === "csr")
456
+ throw new Error("rivetRouter: CSR apps have no server build");
457
+ const serverOut = config.build?.outDir ?? "dist/server";
458
+ if (ssr === true && adapter)
459
+ return targetBuildConfig(adapter, resolve(root, outDir, "entry.server.tsx"), serverOut);
460
+ if (ssr === true)
461
+ return { build: { ssr: resolve(root, outDir, "entry.server.tsx"), outDir: serverOut } };
462
+ if (ssr)
463
+ return;
464
+ // Keyed by file name, so stylesheets land flat in `assets/` (`styles-<hash>.css`).
465
+ const stylesInput = Object.fromEntries(styles.map((path) => [
466
+ posix(path)
467
+ .split("/")
468
+ .pop()
469
+ .replace(/\.[^.]+$/, ""),
470
+ resolve(root, path),
471
+ ]));
472
+ // Resumed pages have no application entry, only stylesheets (the compiler emits the resume
473
+ // runtime and symbol chunks itself). CSR apps start from the generated client entry.
474
+ const defaultInput = mode === "csr" ? { app: clientEntry(), ...stylesInput } : styles.length ? stylesInput : { "rivet-empty": EMPTY };
475
+ return {
476
+ // Pages come from the app handler (or the CSR shell), not an index.html fallback.
477
+ appType: config.appType ?? "custom",
478
+ build: {
479
+ outDir: config.build?.outDir ?? "dist/client",
480
+ manifest: config.build?.manifest ?? true,
481
+ // Z5: a dynamic import (route page, lazy component) preloads only what its importer has not
482
+ // loaded statically: the App chunk lists no shared runtime chunks per route (routes-500 −26 KB min).
483
+ ...(config.build?.modulePreload === undefined ? { modulePreload: { resolveDependencies: preloadDeps } } : {}),
484
+ ...(input === undefined ? { rollupOptions: { input: defaultInput } } : {}),
485
+ },
486
+ };
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
+ },
512
+ resolveId(id, importer, resolveOptions) {
513
+ if (id === EMPTY || id.startsWith(DOCUMENT_PAGE) || id.startsWith(STAND_IN))
514
+ return id;
515
+ // `x.server.ts` modules hold secrets and server code: never part of a browser graph.
516
+ if (!importer || resolveOptions?.ssr || this.environment?.config?.consumer === "server")
517
+ return null;
518
+ // The compiler's resume grouping resolves every specifier its facts list (type-only imports too): not an import.
519
+ if (_factsResolve(id, importer))
520
+ return null;
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);
532
+ this.error({
533
+ message: `[rivet:router] ${id} is server-only and cannot be imported by browser code (imported from ${posix(importer)})` +
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",
536
+ id: importer,
537
+ plugin: "vite-plugin-rivet-router",
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
+ }
555
+ return null;
556
+ },
557
+ // i18n client build: `BASE` is the app base, not the locale's asset base (`BASE_URL`). Only this
558
+ // build's copy of shared/base.ts changes, so other apps' bundles stay byte-identical.
559
+ // `trailingSlash`: the policy constant (client and server bundles); unset, the module stays as is.
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
+ }
566
+ if (slash !== "ignore" && id.split("?")[0] === SLASH_MODULE) {
567
+ if (!code.includes(SLASH_DEFAULT))
568
+ this.error("rivetRouter: shared/slash.ts no longer declares TRAILING_SLASH as expected");
569
+ return { code: code.replace(SLASH_DEFAULT, JSON.stringify(slash) + " as TrailingSlash"), map: null };
570
+ }
571
+ // i18n dev: one client serves every locale, so `underBase` also requires the target's locale to be
572
+ // the page's (read once at load): another locale's URL loads its document, as in a build.
573
+ if (devLocalePaths && !transformOptions?.ssr && id.split("?")[0] === BASE_MODULE) {
574
+ const decl = "export function underBase(";
575
+ if (!code.includes(decl))
576
+ this.error("rivetRouter: shared/base.ts no longer declares underBase as expected");
577
+ return {
578
+ code: code.replace(decl, "function _inBase(") +
579
+ `\nconst _LOCALES = ${JSON.stringify(devLocalePaths)}.map((source) => new RegExp(source, "i"));\n` +
580
+ 'const _localeOf = (pathname, base) => { const path = base ? pathname.slice(base.length) || "/" : pathname; return _LOCALES.findIndex((re) => re.test(path)); };\n' +
581
+ 'const _PAGE = typeof location === "undefined" ? -1 : _localeOf(location.pathname, BASE);\n' +
582
+ "export function underBase(pathname, base = BASE) {\n" +
583
+ " return _inBase(pathname, base) && (_PAGE < 0 || _localeOf(pathname, base) === _PAGE);\n}\n",
584
+ map: null,
585
+ };
586
+ }
587
+ if (!suffix || id.split("?")[0] !== BASE_MODULE)
588
+ return null;
589
+ const read = /\(import\.meta(?: as [^)]*)?\)\.env\?\.BASE_URL|import\.meta\.env\?\.BASE_URL/;
590
+ if (!read.test(code))
591
+ this.error("rivetRouter: shared/base.ts no longer reads import.meta.env.BASE_URL as expected");
592
+ let out = code.replace(read, JSON.stringify(appBase + "/"));
593
+ // Another locale's URL is another client build (its inlined messages, its chunks): the
594
+ // browser loads its document; the client router and navigate() never switch locale in place.
595
+ if (localePaths) {
596
+ const decl = "export function underBase(";
597
+ if (!out.includes(decl))
598
+ this.error("rivetRouter: shared/base.ts no longer declares underBase as expected");
599
+ out =
600
+ out.replace(decl, "function _inBase(") +
601
+ `\nconst _LOCALE = new RegExp(${JSON.stringify(localePaths)}, "i");\n` +
602
+ "export function underBase(pathname, base = BASE) {\n" +
603
+ ' return _inBase(pathname, base) && _LOCALE.test(base ? pathname.slice(base.length) || "/" : pathname);\n}\n';
604
+ }
605
+ return { code: out, map: null };
606
+ },
607
+ async generateBundle(_options, bundle) {
608
+ for (const [name, output] of Object.entries(bundle))
609
+ if (output.type === "chunk" && output.facadeModuleId === EMPTY)
610
+ delete bundle[name];
611
+ if (this.environment?.config?.consumer === "server")
612
+ return;
613
+ clientBundle = bundle;
614
+ loadedBy.clear();
615
+ if (mode !== "csr") {
616
+ // Route modules' CSS (component stylesheets, css`` sheets), linked per route by the handler.
617
+ // Several stylesheets of one route are merged into one route sheet (`mergeRouteCSS`).
618
+ const base = this.environment?.config?.base ?? "/";
619
+ const { routes: css, assets } = mergeRouteCSS(bundle, bundleRouteCSS(bundle, table, resolve(root, routesDir), base), base);
620
+ for (const asset of assets)
621
+ this.emitFile({ type: "asset", ...asset });
622
+ if (Object.keys(css).length)
623
+ this.emitFile({ type: "asset", fileName: ROUTE_CSS, source: JSON.stringify(css, null, 2) + "\n" });
624
+ // Sheets the handler may write inline (`inlineStylesheets`; decided per page, `server/inline-styles.ts`).
625
+ const limit = this.environment?.config?.build.assetsInlineLimit;
626
+ const inline = inlineCSS(bundle, assets, base, options.inlineStylesheets ?? "auto", typeof limit === "number" ? limit : 4096);
627
+ if (inline)
628
+ this.emitFile({ type: "asset", fileName: INLINE_CSS, source: JSON.stringify(inline) + "\n" });
629
+ return;
630
+ }
631
+ const base = this.environment?.config?.base ?? "/";
632
+ const entry = Object.values(bundle).find((output) => output.type === "chunk" && output.facadeModuleId === clientEntry());
633
+ if (!entry || entry.type !== "chunk")
634
+ return;
635
+ const css = new Set(entry.viteMetadata?.importedCss ?? []);
636
+ const sources = new Set(styles.map((path) => posix(path)));
637
+ for (const output of Object.values(bundle))
638
+ if (output.type === "asset" && output.fileName.endsWith(".css") && output.originalFileNames.some((name) => sources.has(posix(name))))
639
+ css.add(output.fileName);
640
+ const { shellHTML } = await import("../adapters/csr.js");
641
+ const document = await loadDocument(resolve(root, documentPath));
642
+ const source = shellHTML(document, { script: base + entry.fileName, styles: [...css].map((file) => base + file) });
643
+ this.emitFile({ type: "asset", fileName: "index.html", source });
644
+ },
645
+ async closeServer() {
646
+ closed = true;
647
+ await regenerations;
648
+ },
649
+ configureServer(server) {
650
+ const routesPath = resolve(root, routesDir);
651
+ const regenerate = async (file) => (resolve(file).startsWith(routesPath + sep) ? refresh() : false);
652
+ closed = false;
653
+ const refresh = () => {
654
+ const run = regenerations.then(async () => {
655
+ if (closed)
656
+ return false;
657
+ const { changed, table: next, resumeEntry } = await generateRoutes(root, codegen);
658
+ entryFor(resumeEntry);
659
+ table = next;
660
+ serverExports.modules.splice(0, serverExports.modules.length, ...routeModules(table, routesDir));
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;
665
+ if (changed && !closed)
666
+ server.ws.send({ type: "full-reload" });
667
+ return changed;
668
+ });
669
+ regenerations = run.catch(() => { });
670
+ return run;
671
+ };
672
+ // A failed regeneration is logged (an unhandled rejection would end the dev server's process).
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))));
674
+ server.watcher.on("add", logged(regenerate));
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
+ };
722
+ // A source starts using a router feature (search params, navigate(), useParams()): generate it
723
+ // from now on (regenerate + full reload). A dev server never turns one off again.
724
+ const features = async (file) => {
725
+ if (!SOURCE.test(file) || /(?:^|[\\/])(?:node_modules|dist(?:[-_.][^\\/]*)?|\.[^\\/]*)[\\/]/.test(relative(root, file)))
726
+ return false;
727
+ const found = none();
728
+ featuresOf(await readFile(file, "utf8").catch(() => ""), found);
729
+ const added = USAGE.filter((name) => found[name] && !codegen[name] && options[name] === undefined);
730
+ if (!added.length)
731
+ return false;
732
+ for (const name of added)
733
+ codegen[name] = true;
734
+ await refresh();
735
+ return true;
736
+ };
737
+ server.watcher.on("add", logged(features));
738
+ // An edit can add or remove in-file server exports: only then does the route table change
739
+ // (regenerated without touching other edits). Server-only edits (`*.server.ts`, in-file
740
+ // loaders) refetch the route data and re-mount the page (`rivet:data`).
741
+ server.watcher.on("change", logged(async (file) => {
742
+ if (await features(file))
743
+ return;
744
+ const path = posix(file);
745
+ if (/\.server\.[cm]?[jt]sx?$/.test(path))
746
+ return server.ws.send({ type: "custom", event: "rivet:data" });
747
+ if (!resolve(file).startsWith(routesPath + sep))
748
+ return;
749
+ const name = posix(relative(routesPath, resolve(file)));
750
+ const names = (await _serverExportsOf(routesPath, [name])).get(name) ?? [];
751
+ const has = names.some((n) => n !== "live");
752
+ if ((has !== inFile.has(name) || names.includes("live") !== inFile.has("live:" + name)) && (await regenerate(file)))
753
+ return;
754
+ if (has)
755
+ server.ws.send({ type: "custom", event: "rivet:data" });
756
+ }));
757
+ const devStyles = styles.map((path) => "/" + posix(path));
758
+ // i18n (`@rivetjs/i18n/vite`): dev pages render their locale's variant, and a non-default
759
+ // locale loads the dev entry with `?rivet-locale=` (its client modules compile for that locale).
760
+ const i18n = server.config.plugins.find((p) => p.name === "vite-plugin-rivet-i18n")?.api?.config;
761
+ const devEntry = server.config.base + posix(join(outDir, "entry.dev.tsx"));
762
+ const devMount = server.config.base + posix(join(outDir, "dev-mount.tsx"));
763
+ // Dev errors: server failures reach the overlay and a dev error page; browser ones are forwarded.
764
+ listenClientErrors(server);
765
+ // Dev SSR records like production (resume recorder, dev manifest): serialization errors surface.
766
+ const devResume = compiler.api.devResume;
767
+ const onError = devReport(server, () => table, routesPath, (id) => devResume.describe(id));
768
+ // Pages resume like production; opted out, or a non-default i18n locale (its client graph
769
+ // compiles per locale, chunks do not yet): client-render without the recorded state.
770
+ const devMode = (locale) => !resume.dev || (i18n && locale && locale !== i18n.defaultLocale) ? "check" : "resume";
771
+ const listeners = new WeakMap();
772
+ // After Vite's own middlewares (modules, assets): pages and data go through the app handler.
773
+ return () => {
774
+ server.middlewares.use(async (req, res, next) => {
775
+ try {
776
+ if (mode === "csr") {
777
+ if (req.method !== "GET" && req.method !== "HEAD")
778
+ return next();
779
+ const { shellHTML } = await import("../adapters/csr.js");
780
+ const document = (await server.ssrLoadModule("/" + posix(documentPath))).default;
781
+ const shell = shellHTML(document, { script: "/" + posix(join(outDir, "entry.client.tsx")), styles: devStyles });
782
+ const html = await server.transformIndexHtml(req.url ?? "/", shell);
783
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-cache" });
784
+ res.end(req.method === "HEAD" ? undefined : html);
785
+ return;
786
+ }
787
+ // The entry re-exports createApp: handler and app load through one SSR graph (one runtime).
788
+ const entry = (await server.ssrLoadModule("/" + posix(join(outDir, "entry.server.tsx"))));
789
+ // One app per entry namespace: Vite hands out a new one once an edit invalidates the graph.
790
+ let listener = listeners.get(entry);
791
+ if (!listener) {
792
+ const { createListener } = await import("../adapters/node.js");
793
+ const handler = entry.createApp({
794
+ entry,
795
+ styles: devStyles,
796
+ routeStyles: (id) => devRouteCSS(server, table, resolve(root, routesDir), id, new Set(devStyles)),
797
+ dev: true,
798
+ // Read per page: the dev manifest grows as Vite compiles modules.
799
+ devResume: {
800
+ get manifest() {
801
+ return devResume.manifest();
802
+ },
803
+ mode: devMode,
804
+ },
805
+ // The DevTools agent (when on) runs first, before any framework module evaluates.
806
+ devScripts: (locale) => [
807
+ ...compiler.api.devtoolsScripts(),
808
+ server.config.base + "@vite/client",
809
+ // Resumed: the entry that switches to client rendering on a hot update; else the mount itself.
810
+ devMode(locale) === "resume"
811
+ ? devEntry
812
+ : i18n && locale && locale !== i18n.defaultLocale
813
+ ? devMount + "?rivet-locale=" + encodeURIComponent(locale)
814
+ : devMount,
815
+ ],
816
+ ...(i18n ? { i18n: { locales: i18n.locales } } : {}),
817
+ onError,
818
+ });
819
+ listener = createListener({ handler: devHandler(handler, server, () => table, devReach), compress: false }).listener;
820
+ listeners.set(entry, listener);
821
+ }
822
+ // Vite's base middleware strips the base from `req.url`: route patterns carry it.
823
+ const original = req.originalUrl;
824
+ if (codegen.base && original)
825
+ req.url = original;
826
+ // Static mode: client navigation reads prerendered `<page>/_data.json` files, which only the
827
+ // build writes. Dev answers them from the data endpoint, as the static adapter renders them.
828
+ if (mode === "static")
829
+ staticData(req, slash);
830
+ listener(req, res);
831
+ }
832
+ catch (error) {
833
+ // Never reject (connect does not await middlewares; an unhandled rejection ends the dev server).
834
+ try {
835
+ if (error instanceof Error)
836
+ server.ssrFixStacktrace(error);
837
+ }
838
+ catch {
839
+ /* Keep the original stack. */
840
+ }
841
+ next(error);
842
+ }
843
+ });
844
+ };
845
+ },
846
+ };
847
+ return [
848
+ router,
849
+ ...plugins,
850
+ // Measurement only (`RIVET_LAZY_ROWS=swap`, dev / lazy-rows tools): inert otherwise.
851
+ lazyRowsHarness(() => resolve(root, outDir, "entry.server.tsx")),
852
+ ...(adapter ? [targetEntryPlugin(adapter, { serverEntry: () => resolve(root, outDir, "entry.server.tsx"), mode: () => mode })] : []),
853
+ ];
854
+ }
855
+ /** Import the document shell module at build time (Node strips its types). */
856
+ async function loadDocument(path) {
857
+ const module = (await import(__rewriteRelativeImportExtension(pathToFileURL(path).href, true)));
858
+ if (typeof module.default !== "function")
859
+ throw new Error(`${path}: default export must be a DocumentShell function`);
860
+ return module.default;
861
+ }