@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
@@ -0,0 +1,385 @@
1
+ // `rivet migrate server-exports`: rewrite magic route server export names to wrapper calls (spec
2
+ // docs/design/specs/2026-10-20-server-exports-design.md). In-file names (`serverLoad`, `serverGuard`,
3
+ // `serverActions`, `serverEntries`, `serverHeaders`, `onRequest`, `onGet`…) in every route module and the
4
+ // sibling `*.server.ts` names (`load`, `guard`, `actions`, `entries`, `headers`, `GET`…):
5
+ // export async function serverLoad(event) { … } → export const loader = load(async (event) => { … });
6
+ // export const onGet = (event) => … → export const GET = onGet((event) => …);
7
+ // with `import { load, onGet } from "@rivetjs/router"` added. Renames are scope-aware (TypeScript
8
+ // language service): references in other modules (`typeof import(…).serverLoad`, `PageProps<typeof
9
+ // serverLoad>`) follow; unrelated locals of the same name do not.
10
+ import { readdir, readFile, writeFile } from "node:fs/promises";
11
+ import { join, relative, resolve, sep } from "node:path";
12
+ /** Wrapper and conventional export name per former magic name. */
13
+ export const WRAPPERS = {
14
+ serverLoad: { wrapper: "load", name: "loader" },
15
+ serverGuard: { wrapper: "guard", name: "access" },
16
+ serverActions: { wrapper: "actions", name: "forms" },
17
+ serverEntries: { wrapper: "entries", name: "paths" },
18
+ serverHeaders: { wrapper: "headers", name: "responseHeaders" },
19
+ onRequest: { wrapper: "onRequest", name: "middleware" },
20
+ onGet: { wrapper: "onGet", name: "GET" },
21
+ onPost: { wrapper: "onPost", name: "POST" },
22
+ onPut: { wrapper: "onPut", name: "PUT" },
23
+ onPatch: { wrapper: "onPatch", name: "PATCH" },
24
+ onDelete: { wrapper: "onDelete", name: "DELETE" },
25
+ onOptions: { wrapper: "onOptions", name: "OPTIONS" },
26
+ };
27
+ /** Former sibling `*.server.ts` names → the in-file name they meant. */
28
+ export const SIBLING = {
29
+ load: "serverLoad",
30
+ guard: "serverGuard",
31
+ actions: "serverActions",
32
+ entries: "serverEntries",
33
+ headers: "serverHeaders",
34
+ GET: "onGet",
35
+ POST: "onPost",
36
+ PUT: "onPut",
37
+ PATCH: "onPatch",
38
+ DELETE: "onDelete",
39
+ };
40
+ const SOURCE = /\.(m|c)?[jt]sx?$/;
41
+ const SERVER = /\.server\.(m|c)?[jt]s$/;
42
+ const IGNORED = /(^|[/\\])(node_modules|dist|\.rivet|\.[^/\\]+)([/\\]|$)/;
43
+ const NOT_ROUTE = /\.(test|spec)\.[^/\\]+$|\.d\.ts$/;
44
+ async function sources(dir) {
45
+ const entries = await readdir(dir, { recursive: true, withFileTypes: true });
46
+ return entries
47
+ .filter((e) => e.isFile() && SOURCE.test(e.name) && !e.name.endsWith(".d.ts"))
48
+ .map((e) => join(e.parentPath, e.name))
49
+ .filter((file) => !IGNORED.test(relative(dir, file)));
50
+ }
51
+ /**
52
+ * Compute the migration for route modules under `routesDir` (pages, layouts, plain `.ts` endpoints,
53
+ * sibling `*.server.ts`); references are searched in every source module under `projectDir`. Nothing is
54
+ * written (see `writeMigration`).
55
+ */
56
+ export async function migrateServerExports(projectDir, routesDir) {
57
+ const ts = (await import("typescript")).default;
58
+ const files = await sources(resolve(projectDir));
59
+ const texts = new Map();
60
+ for (const file of files)
61
+ texts.set(file, await readFile(file, "utf8"));
62
+ const routesRoot = resolve(projectDir, routesDir) + sep;
63
+ const targets = files.filter((file) => file.startsWith(routesRoot) && !NOT_ROUTE.test(file));
64
+ return migrateTexts(ts, texts, targets, resolve(projectDir));
65
+ }
66
+ /** Migrate one module's text on its own (no other modules: references elsewhere are not renamed). */
67
+ export function migrateSource(ts, text, fileName) {
68
+ const file = resolve("/", fileName);
69
+ const report = migrateTexts(ts, new Map([[file, text]]), [file], "/");
70
+ return { text: report.changes.get(file) ?? text, renamed: report.renamed, skipped: report.skipped };
71
+ }
72
+ /** The migration over in-memory `texts` (absolute paths); `targets`: the route modules to rewrite. */
73
+ export function migrateTexts(ts, texts, targets, root) {
74
+ const current = new Map(texts);
75
+ const files = [...texts.keys()];
76
+ const options = {
77
+ allowJs: true,
78
+ jsx: ts.JsxEmit.Preserve,
79
+ module: ts.ModuleKind.NodeNext,
80
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
81
+ target: ts.ScriptTarget.ES2022,
82
+ allowImportingTsExtensions: true,
83
+ noEmit: true,
84
+ };
85
+ const host = {
86
+ getScriptFileNames: () => files,
87
+ getScriptVersion: () => "1",
88
+ getScriptSnapshot: (file) => {
89
+ const text = texts.get(file) ?? ts.sys.readFile(file);
90
+ return text === undefined ? undefined : ts.ScriptSnapshot.fromString(text);
91
+ },
92
+ getCurrentDirectory: () => root,
93
+ getCompilationSettings: () => options,
94
+ getDefaultLibFileName: (o) => ts.getDefaultLibFilePath(o),
95
+ fileExists: (file) => texts.has(file) || ts.sys.fileExists(file),
96
+ readFile: (file) => texts.get(file) ?? ts.sys.readFile(file),
97
+ readDirectory: ts.sys.readDirectory,
98
+ directoryExists: ts.sys.directoryExists,
99
+ getDirectories: ts.sys.getDirectories,
100
+ };
101
+ const service = ts.createLanguageService(host, ts.createDocumentRegistry());
102
+ const edits = new Map();
103
+ const renamed = [];
104
+ const skipped = [];
105
+ const planned = new Map();
106
+ const edit = (file, start, end, text) => {
107
+ let list = edits.get(file);
108
+ if (!list)
109
+ edits.set(file, (list = []));
110
+ if (!list.some((e) => e.start === start && e.end === end))
111
+ list.push({ start, end, text });
112
+ };
113
+ const display = (file) => relative(root, file).split(sep).join("/");
114
+ // Phase 1: renames (project-wide, scope-aware) and the plan per target.
115
+ for (const file of targets) {
116
+ const source = service.getProgram().getSourceFile(file);
117
+ if (!source)
118
+ continue;
119
+ const sibling = SERVER.test(file);
120
+ const locals = topLevelNames(ts, source);
121
+ const plan = [];
122
+ for (const statement of source.statements) {
123
+ const exported = ts.canHaveModifiers(statement) && ts.getModifiers(statement)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
124
+ const ids = [];
125
+ if (exported && ts.isFunctionDeclaration(statement) && statement.name && !statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword))
126
+ ids.push(statement.name);
127
+ if (exported && ts.isVariableStatement(statement))
128
+ for (const d of statement.declarationList.declarations)
129
+ if (ts.isIdentifier(d.name))
130
+ ids.push(d.name);
131
+ const specifiers = ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause) ? statement.exportClause.elements : [];
132
+ for (const id of ids) {
133
+ const meant = magic(id.text, sibling);
134
+ if (!meant)
135
+ continue;
136
+ if (ts.isVariableStatement(statement) && statement.declarationList.declarations.length > 1) {
137
+ skipped.push(`${display(file)}: ${id.text} (declared with other variables: give it its own statement)`);
138
+ continue;
139
+ }
140
+ if (ts.isVariableStatement(statement) && !statement.declarationList.declarations[0].initializer) {
141
+ skipped.push(`${display(file)}: ${id.text} (no initializer)`);
142
+ continue;
143
+ }
144
+ const { wrapper, name } = WRAPPERS[meant];
145
+ const next = name === id.text ? name : free(name, locals);
146
+ if (!next) {
147
+ skipped.push(`${display(file)}: ${id.text} (\`${name}\` already exists)`);
148
+ continue;
149
+ }
150
+ locals.add(next);
151
+ const locations = service.findRenameLocations(file, id.getStart(source), false, false, { providePrefixAndSuffixTextForRename: true }) ?? [];
152
+ for (const location of locations)
153
+ edit(location.fileName, location.textSpan.start, location.textSpan.start + location.textSpan.length, (location.prefixText ?? "") + next + (location.suffixText ?? ""));
154
+ plan.push({ old: id.text, name: next, wrapper });
155
+ renamed.push(`${display(file)}: ${id.text} → ${next} = ${wrapper}(…)`);
156
+ }
157
+ for (const specifier of specifiers) {
158
+ const meant = magic(specifier.name.text, sibling);
159
+ if (!meant)
160
+ continue;
161
+ if (ts.isExportDeclaration(statement) && statement.moduleSpecifier) {
162
+ skipped.push(`${display(file)}: ${specifier.name.text} (re-exported: wrap it where it is declared, then re-export it from a *.server.ts module)`);
163
+ continue;
164
+ }
165
+ if (!specifier.propertyName || magic(specifier.propertyName.text, sibling)) {
166
+ skipped.push(`${display(file)}: ${specifier.name.text} (\`export { ${specifier.name.text} }\`: rename the local, then wrap it)`);
167
+ continue;
168
+ }
169
+ const { wrapper, name } = WRAPPERS[meant];
170
+ const next = name === specifier.name.text ? name : free(name, locals);
171
+ if (!next) {
172
+ skipped.push(`${display(file)}: ${specifier.name.text} (\`${name}\` already exists)`);
173
+ continue;
174
+ }
175
+ locals.add(next);
176
+ const locations = service.findRenameLocations(file, specifier.name.getStart(source), false, false, { providePrefixAndSuffixTextForRename: true }) ?? [];
177
+ for (const location of locations)
178
+ edit(location.fileName, location.textSpan.start, location.textSpan.start + location.textSpan.length, (location.prefixText ?? "") + next + (location.suffixText ?? ""));
179
+ plan.push({ old: specifier.name.text, name: next, wrapper });
180
+ renamed.push(`${display(file)}: ${specifier.name.text} → ${next} = ${wrapper}(${specifier.propertyName.text})`);
181
+ }
182
+ }
183
+ if (plan.length)
184
+ planned.set(file, plan);
185
+ }
186
+ for (const [file, list] of edits)
187
+ current.set(file, applyEdits(current.get(file) ?? "", list));
188
+ // Phase 2 (per target, parser only): wrap the renamed exports, add the wrapper import.
189
+ for (const [file, plan] of planned)
190
+ current.set(file, wrapExports(ts, current.get(file), file, plan));
191
+ const changes = new Map();
192
+ for (const [file, text] of current)
193
+ if (text !== texts.get(file))
194
+ changes.set(file, text);
195
+ return { changes, renamed, skipped };
196
+ }
197
+ /** The in-file name an export name stands for (sibling modules: their former names too). */
198
+ function magic(name, sibling) {
199
+ if (Object.hasOwn(WRAPPERS, name))
200
+ return name;
201
+ return sibling && Object.hasOwn(SIBLING, name) ? SIBLING[name] : undefined;
202
+ }
203
+ function free(name, taken) {
204
+ for (const candidate of [name, name + "Export"])
205
+ if (!taken.has(candidate))
206
+ return candidate;
207
+ return undefined;
208
+ }
209
+ function topLevelNames(ts, source) {
210
+ const names = new Set();
211
+ for (const statement of source.statements) {
212
+ if ((ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement)) && statement.name)
213
+ names.add(statement.name.text);
214
+ if (ts.isVariableStatement(statement))
215
+ for (const d of statement.declarationList.declarations)
216
+ collectBindings(ts, d.name, names);
217
+ if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause))
218
+ for (const specifier of statement.exportClause.elements)
219
+ names.add(specifier.name.text);
220
+ if (ts.isImportDeclaration(statement) && statement.importClause) {
221
+ const clause = statement.importClause;
222
+ if (clause.name)
223
+ names.add(clause.name.text);
224
+ const bindings = clause.namedBindings;
225
+ if (bindings && ts.isNamedImports(bindings))
226
+ for (const s of bindings.elements)
227
+ names.add(s.name.text);
228
+ if (bindings && ts.isNamespaceImport(bindings))
229
+ names.add(bindings.name.text);
230
+ }
231
+ if ((ts.isEnumDeclaration(statement) || ts.isModuleDeclaration(statement)) && ts.isIdentifier(statement.name))
232
+ names.add(statement.name.text);
233
+ }
234
+ return names;
235
+ }
236
+ function collectBindings(ts, name, out) {
237
+ if (ts.isIdentifier(name))
238
+ out.add(name.text);
239
+ else
240
+ for (const element of name.elements)
241
+ if (!ts.isOmittedExpression(element))
242
+ collectBindings(ts, element.name, out);
243
+ }
244
+ function applyEdits(text, list) {
245
+ for (const e of [...list].sort((a, b) => b.start - a.start || b.end - a.end))
246
+ text = text.slice(0, e.start) + e.text + text.slice(e.end);
247
+ return text;
248
+ }
249
+ /** Whether a function body needs a function expression (`this`, `arguments`, `super`, `new.target`), not an arrow. */
250
+ function needsFunction(ts, node) {
251
+ let found = false;
252
+ const visit = (n) => {
253
+ if (found)
254
+ return;
255
+ if (n.kind === ts.SyntaxKind.ThisKeyword || n.kind === ts.SyntaxKind.SuperKeyword || ts.isMetaProperty(n) || (ts.isIdentifier(n) && n.text === "arguments")) {
256
+ found = true;
257
+ return;
258
+ }
259
+ // Own `this` / `arguments` of nested functions and classes do not count.
260
+ if (ts.isFunctionDeclaration(n) ||
261
+ ts.isFunctionExpression(n) ||
262
+ ts.isClassDeclaration(n) ||
263
+ ts.isClassExpression(n) ||
264
+ ts.isMethodDeclaration(n) ||
265
+ ts.isGetAccessorDeclaration(n) ||
266
+ ts.isSetAccessorDeclaration(n))
267
+ return;
268
+ ts.forEachChild(n, visit);
269
+ };
270
+ ts.forEachChild(node, visit);
271
+ return found;
272
+ }
273
+ function wrapExports(ts, text, file, plan) {
274
+ const kind = /\.tsx$/.test(file) ? ts.ScriptKind.TSX : /\.jsx$/.test(file) ? ts.ScriptKind.JSX : /\.[cm]?js$/.test(file) ? ts.ScriptKind.JS : ts.ScriptKind.TS;
275
+ const typed = kind === ts.ScriptKind.TS || kind === ts.ScriptKind.TSX;
276
+ const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, kind);
277
+ const edits = [];
278
+ const slice = (node) => text.slice(node.getStart(source), node.getEnd());
279
+ const locals = topLevelNames(ts, source);
280
+ // Wrapper local names: an existing value import of `@rivetjs/router` (aliases kept), else added.
281
+ const imported = new Map();
282
+ let routerImport;
283
+ for (const statement of source.statements) {
284
+ if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "@rivetjs/router")
285
+ continue;
286
+ const clause = statement.importClause;
287
+ if (!clause || clause.isTypeOnly || !clause.namedBindings || !ts.isNamedImports(clause.namedBindings))
288
+ continue;
289
+ routerImport ??= statement;
290
+ for (const s of clause.namedBindings.elements)
291
+ if (!s.isTypeOnly)
292
+ imported.set((s.propertyName ?? s.name).text, s.name.text);
293
+ }
294
+ const added = [];
295
+ const local = (wrapper) => {
296
+ const known = imported.get(wrapper);
297
+ if (known)
298
+ return known;
299
+ const name = locals.has(wrapper) ? "rivet" + wrapper[0].toUpperCase() + wrapper.slice(1) : wrapper;
300
+ imported.set(wrapper, name);
301
+ added.push(name === wrapper ? wrapper : `${wrapper} as ${name}`);
302
+ locals.add(name);
303
+ return name;
304
+ };
305
+ const appended = [];
306
+ for (const { name, wrapper } of plan) {
307
+ for (const statement of source.statements) {
308
+ if (ts.isVariableStatement(statement)) {
309
+ const d = statement.declarationList.declarations[0];
310
+ if (!d || !ts.isIdentifier(d.name) || d.name.text !== name || !d.initializer)
311
+ continue;
312
+ const call = local(wrapper);
313
+ let init = slice(d.initializer);
314
+ if (d.type && typed) {
315
+ init = `(${init}) satisfies ${slice(d.type)}`;
316
+ edits.push({ start: d.name.getEnd(), end: d.type.getEnd(), text: "" });
317
+ }
318
+ edits.push({ start: d.initializer.getStart(source), end: d.initializer.getEnd(), text: `${call}(${init})` });
319
+ if (!(statement.declarationList.flags & ts.NodeFlags.Const)) {
320
+ const keyword = statement.declarationList.getStart(source);
321
+ const word = /^(let|var)/.exec(text.slice(keyword))[1];
322
+ edits.push({ start: keyword, end: keyword + word.length, text: "const" });
323
+ }
324
+ }
325
+ else if (ts.isFunctionDeclaration(statement) && statement.name?.text === name && statement.body) {
326
+ const call = local(wrapper);
327
+ const isAsync = statement.modifiers?.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword) ?? false;
328
+ const params = text.slice(statement.parameters.pos - 1, statement.parameters.end + 1);
329
+ const returns = statement.type ? `: ${slice(statement.type)}` : "";
330
+ const typeParams = statement.typeParameters ? `<${statement.typeParameters.map(slice).join(", ")}>` : "";
331
+ const fn = statement.asteriskToken || needsFunction(ts, statement.body) || typeParams
332
+ ? `${isAsync ? "async " : ""}function${statement.asteriskToken ? "*" : ""} ${name}${typeParams}${params}${returns} ${slice(statement.body)}`
333
+ : `${isAsync ? "async " : ""}${params}${returns} => ${slice(statement.body)}`;
334
+ const start = statement.getStart(source);
335
+ edits.push({ start, end: statement.getEnd(), text: `export const ${name} = ${call}(${fn});` });
336
+ }
337
+ else if (ts.isExportDeclaration(statement) && !statement.moduleSpecifier && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
338
+ const elements = statement.exportClause.elements;
339
+ const target = elements.find((s) => s.name.text === name && s.propertyName);
340
+ if (!target)
341
+ continue;
342
+ const call = local(wrapper);
343
+ const rest = elements.filter((s) => s !== target).map(slice);
344
+ const end = statement.getEnd() + (text[statement.getEnd()] === "\n" ? 1 : 0);
345
+ edits.push({ start: statement.getStart(source), end: rest.length ? statement.getEnd() : end, text: rest.length ? `export { ${rest.join(", ")} };` : "" });
346
+ appended.push(`export const ${name} = ${call}(${target.propertyName.text});`);
347
+ }
348
+ }
349
+ }
350
+ if (added.length) {
351
+ if (routerImport && ts.isNamedImports(routerImport.importClause.namedBindings)) {
352
+ const named = routerImport.importClause.namedBindings;
353
+ const last = named.elements[named.elements.length - 1];
354
+ if (last)
355
+ edits.push({ start: last.getEnd(), end: last.getEnd(), text: `, ${added.join(", ")}` });
356
+ else
357
+ edits.push({ start: named.getStart(source) + 1, end: named.getStart(source) + 1, text: ` ${added.join(", ")} ` });
358
+ }
359
+ else {
360
+ const imports = source.statements.filter(ts.isImportDeclaration);
361
+ const line = `import { ${added.join(", ")} } from "@rivetjs/router";`;
362
+ const after = imports[imports.length - 1];
363
+ if (after) {
364
+ // At the end of the last import's line (after a trailing comment, which stays with its import).
365
+ const eol = text.indexOf("\n", after.getEnd());
366
+ const at = eol === -1 ? text.length : eol;
367
+ edits.push({ start: at, end: at, text: "\n" + line });
368
+ }
369
+ else {
370
+ // After a leading comment block / directive prologue: before the first statement.
371
+ const first = source.statements[0];
372
+ const at = first ? first.getStart(source) : text.length;
373
+ edits.push({ start: at, end: at, text: line + "\n" });
374
+ }
375
+ }
376
+ }
377
+ let out = applyEdits(text, edits);
378
+ if (appended.length)
379
+ out = out.replace(/\s*$/, "\n") + appended.join("\n") + "\n";
380
+ return out;
381
+ }
382
+ export async function writeMigration(report) {
383
+ for (const [file, text] of report.changes)
384
+ await writeFile(file, text);
385
+ }
@@ -0,0 +1,60 @@
1
+ import { type ResolvedConfig } from "vite";
2
+ import { type ServerOnlyPattern } from "@rivetjs/compiler/vite";
3
+ import type { RouteTable } from "./routes.js";
4
+ /** Resolve a specifier for the browser graph: an absolute id (files: path, maybe `?query`), or undefined (virtual / unknown). */
5
+ export type ReachResolve = (specifier: string, importer: string) => Promise<string | undefined>;
6
+ /** `rivetRouter({ serverOnly })`. */
7
+ export type ServerOnlyOption = readonly ServerOnlyPattern[] | {
8
+ modules?: readonly ServerOnlyPattern[];
9
+ reach?: "error" | "warn";
10
+ };
11
+ export interface ReachOptions {
12
+ root: string;
13
+ /** Absolute routes directory. */
14
+ routesDir: string;
15
+ table: RouteTable;
16
+ resolve: ReachResolve;
17
+ /** Declared server-only modules (option + plugins); the marker and `*.server.*` always count. */
18
+ serverOnly: readonly ServerOnlyPattern[];
19
+ /** Components reaching a server-only module: build error (default) or warning (the page then renders as a document). */
20
+ reach?: "error" | "warn";
21
+ /** Absolute route module paths (compiled with `serverExports`: server exports and code only they use are no browser code). */
22
+ routeModules: ReadonlySet<string>;
23
+ /** Vite config (tsconfig options of the type stripping). */
24
+ config?: ResolvedConfig;
25
+ }
26
+ /** One RV3011 report. */
27
+ export interface ReachProblem {
28
+ readonly severity: "error" | "warning";
29
+ /** What reaches it: a component module of page routes, a layout, an error / not-found page. */
30
+ readonly kind: "component" | "layout" | "special";
31
+ /** Display chain from the first component (or the layout / page) to the server-only module. */
32
+ readonly chain: readonly string[];
33
+ /** Route patterns (component: the page routes rendering it; layout: the routes under it). */
34
+ readonly routes: readonly string[];
35
+ /** Routes-relative page / layout files involved. */
36
+ readonly files: readonly string[];
37
+ /** Terminal text: `[rivet:router] RV3011 …` + `fix:` / `docs:` lines. */
38
+ readonly message: string;
39
+ }
40
+ export interface Reach {
41
+ /** Document routes: routes-relative page file → display chain to the server-only module it reaches. */
42
+ readonly documents: ReadonlyMap<string, readonly string[]>;
43
+ readonly problems: readonly ReachProblem[];
44
+ /** Modules (absolute paths) only document pages reach: a server-only import from one gets the browser stand-in. */
45
+ readonly documentOnly: ReadonlySet<string>;
46
+ /** `importer \0 specifier` of each server-only import → its key (resolved id, else the specifier). */
47
+ readonly targets: ReadonlyMap<string, string>;
48
+ /** Server-only key → names imported from it (stand-in exports). */
49
+ readonly names: ReadonlyMap<string, ReadonlySet<string>>;
50
+ /** Server-only key → one display chain of importers (outermost first). */
51
+ readonly chains: ReadonlyMap<string, readonly string[]>;
52
+ }
53
+ export declare const RV_REACH = "RV3011";
54
+ /** Walk every route's browser graph and classify the routes (see the module comment). */
55
+ export declare function analyzeReach(options: ReachOptions): Promise<Reach>;
56
+ /** Normalize `rivetRouter({ serverOnly })`. */
57
+ export declare function serverOnlyOption(option: ServerOnlyOption | undefined): {
58
+ modules: readonly ServerOnlyPattern[];
59
+ reach: "error" | "warn";
60
+ };