@rangojs/router 0.0.0-experimental.fb4fdc18 → 0.0.0-experimental.fce7fbd1

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 (214) hide show
  1. package/README.md +9 -9
  2. package/dist/bin/rango.js +147 -57
  3. package/dist/testing/vitest.js +48 -0
  4. package/dist/vite/index.js +914 -485
  5. package/package.json +55 -11
  6. package/skills/bundle-analysis/SKILL.md +159 -0
  7. package/skills/cache-guide/SKILL.md +220 -30
  8. package/skills/caching/SKILL.md +116 -8
  9. package/skills/composability/SKILL.md +27 -2
  10. package/skills/document-cache/SKILL.md +78 -55
  11. package/skills/handler-use/SKILL.md +3 -1
  12. package/skills/hooks/SKILL.md +214 -18
  13. package/skills/host-router/SKILL.md +45 -20
  14. package/skills/intercept/SKILL.md +26 -4
  15. package/skills/layout/SKILL.md +6 -7
  16. package/skills/links/SKILL.md +173 -17
  17. package/skills/loader/SKILL.md +149 -6
  18. package/skills/middleware/SKILL.md +13 -9
  19. package/skills/migrate-nextjs/SKILL.md +1 -1
  20. package/skills/mime-routes/SKILL.md +27 -0
  21. package/skills/observability/SKILL.md +137 -0
  22. package/skills/parallel/SKILL.md +5 -6
  23. package/skills/prerender/SKILL.md +14 -33
  24. package/skills/rango/SKILL.md +242 -26
  25. package/skills/react-compiler/SKILL.md +168 -0
  26. package/skills/response-routes/SKILL.md +58 -9
  27. package/skills/route/SKILL.md +13 -4
  28. package/skills/router-setup/SKILL.md +3 -3
  29. package/skills/server-actions/SKILL.md +53 -41
  30. package/skills/testing/SKILL.md +599 -0
  31. package/skills/typesafety/SKILL.md +310 -26
  32. package/skills/use-cache/SKILL.md +34 -5
  33. package/skills/view-transitions/SKILL.md +294 -0
  34. package/src/__augment-tests__/augment.ts +81 -0
  35. package/src/__augment-tests__/augmented.check.ts +117 -0
  36. package/src/browser/action-coordinator.ts +53 -36
  37. package/src/browser/event-controller.ts +42 -66
  38. package/src/browser/history-state.ts +21 -0
  39. package/src/browser/index.ts +3 -3
  40. package/src/browser/navigation-bridge.ts +6 -6
  41. package/src/browser/navigation-client.ts +12 -15
  42. package/src/browser/navigation-store.ts +7 -8
  43. package/src/browser/navigation-transaction.ts +10 -28
  44. package/src/browser/partial-update.ts +9 -19
  45. package/src/browser/react/NavigationProvider.tsx +29 -40
  46. package/src/browser/react/index.ts +3 -0
  47. package/src/browser/react/location-state-shared.ts +175 -4
  48. package/src/browser/react/location-state.ts +39 -13
  49. package/src/browser/react/use-handle.ts +17 -9
  50. package/src/browser/react/use-params.ts +3 -4
  51. package/src/browser/react/use-reverse.ts +106 -0
  52. package/src/browser/react/use-router.ts +14 -1
  53. package/src/browser/response-adapter.ts +25 -0
  54. package/src/browser/rsc-router.tsx +30 -16
  55. package/src/browser/scroll-restoration.ts +22 -14
  56. package/src/browser/segment-structure-assert.ts +2 -2
  57. package/src/browser/server-action-bridge.ts +23 -30
  58. package/src/browser/types.ts +2 -0
  59. package/src/build/collect-fallback-refs.ts +107 -0
  60. package/src/build/generate-manifest.ts +60 -35
  61. package/src/build/generate-route-types.ts +2 -0
  62. package/src/build/index.ts +2 -0
  63. package/src/build/route-types/codegen.ts +4 -4
  64. package/src/build/route-types/include-resolution.ts +1 -1
  65. package/src/build/route-types/per-module-writer.ts +7 -4
  66. package/src/build/route-types/router-processing.ts +55 -14
  67. package/src/build/route-types/scan-filter.ts +1 -1
  68. package/src/build/route-types/source-scan.ts +118 -0
  69. package/src/build/runtime-discovery.ts +9 -20
  70. package/src/cache/cache-scope.ts +28 -42
  71. package/src/cache/cf/cf-cache-store.ts +49 -6
  72. package/src/client.rsc.tsx +3 -0
  73. package/src/client.tsx +10 -8
  74. package/src/context-var.ts +5 -5
  75. package/src/decode-loader-results.ts +36 -0
  76. package/src/errors.ts +30 -1
  77. package/src/handle.ts +26 -13
  78. package/src/host/index.ts +2 -2
  79. package/src/host/router.ts +129 -57
  80. package/src/host/types.ts +31 -2
  81. package/src/host/utils.ts +1 -1
  82. package/src/href-client.ts +140 -20
  83. package/src/index.rsc.ts +6 -4
  84. package/src/index.ts +13 -6
  85. package/src/loader-store.ts +500 -0
  86. package/src/loader.rsc.ts +2 -5
  87. package/src/loader.ts +3 -10
  88. package/src/missing-id-error.ts +68 -0
  89. package/src/prerender.ts +4 -4
  90. package/src/response-utils.ts +9 -0
  91. package/src/reverse.ts +65 -41
  92. package/src/route-content-wrapper.tsx +6 -28
  93. package/src/route-definition/dsl-helpers.ts +238 -263
  94. package/src/route-definition/helper-factories.ts +29 -139
  95. package/src/route-definition/helpers-types.ts +37 -14
  96. package/src/route-definition/use-item-types.ts +32 -0
  97. package/src/route-types.ts +19 -41
  98. package/src/router/basename.ts +14 -0
  99. package/src/router/content-negotiation.ts +15 -2
  100. package/src/router/error-handling.ts +1 -1
  101. package/src/router/handler-context.ts +4 -42
  102. package/src/router/intercept-resolution.ts +4 -18
  103. package/src/router/lazy-includes.ts +2 -2
  104. package/src/router/loader-resolution.ts +16 -2
  105. package/src/router/match-handlers.ts +62 -20
  106. package/src/router/match-middleware/cache-lookup.ts +44 -91
  107. package/src/router/match-middleware/cache-store.ts +3 -2
  108. package/src/router/match-result.ts +32 -30
  109. package/src/router/metrics.ts +1 -1
  110. package/src/router/middleware-types.ts +1 -1
  111. package/src/router/middleware.ts +46 -78
  112. package/src/router/prerender-match.ts +1 -1
  113. package/src/router/preview-match.ts +3 -1
  114. package/src/router/request-classification.ts +4 -28
  115. package/src/router/revalidation.ts +43 -1
  116. package/src/router/router-interfaces.ts +45 -28
  117. package/src/router/router-options.ts +40 -1
  118. package/src/router/router-registry.ts +2 -5
  119. package/src/router/segment-resolution/fresh.ts +19 -6
  120. package/src/router/segment-resolution/revalidation.ts +19 -6
  121. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  122. package/src/router/substitute-pattern-params.ts +56 -0
  123. package/src/router/telemetry.ts +99 -0
  124. package/src/router/types.ts +8 -0
  125. package/src/router.ts +37 -21
  126. package/src/rsc/handler-context.ts +2 -2
  127. package/src/rsc/handler.ts +20 -65
  128. package/src/rsc/helpers.ts +22 -2
  129. package/src/rsc/index.ts +1 -1
  130. package/src/rsc/origin-guard.ts +28 -10
  131. package/src/rsc/response-route-handler.ts +32 -52
  132. package/src/rsc/rsc-rendering.ts +27 -53
  133. package/src/rsc/runtime-warnings.ts +9 -10
  134. package/src/rsc/server-action.ts +13 -37
  135. package/src/rsc/ssr-setup.ts +16 -0
  136. package/src/rsc/types.ts +2 -2
  137. package/src/search-params.ts +4 -4
  138. package/src/segment-system.tsx +121 -65
  139. package/src/serialize.ts +243 -0
  140. package/src/server/context.ts +118 -51
  141. package/src/server/cookie-store.ts +28 -4
  142. package/src/server/request-context.ts +10 -0
  143. package/src/static-handler.ts +1 -1
  144. package/src/testing/cache-status.ts +166 -0
  145. package/src/testing/collect-handle.ts +63 -0
  146. package/src/testing/dispatch.ts +440 -0
  147. package/src/testing/dom.entry.ts +22 -0
  148. package/src/testing/e2e/fixture.ts +154 -0
  149. package/src/testing/e2e/index.ts +149 -0
  150. package/src/testing/e2e/matchers.ts +51 -0
  151. package/src/testing/e2e/page-helpers.ts +272 -0
  152. package/src/testing/e2e/parity.ts +306 -0
  153. package/src/testing/e2e/server.ts +183 -0
  154. package/src/testing/flight-matchers.ts +104 -0
  155. package/src/testing/flight-runtime.d.ts +21 -0
  156. package/src/testing/flight.entry.ts +22 -0
  157. package/src/testing/flight.ts +182 -0
  158. package/src/testing/generated-routes.ts +223 -0
  159. package/src/testing/index.ts +105 -0
  160. package/src/testing/internal/context.ts +193 -0
  161. package/src/testing/render-route.tsx +536 -0
  162. package/src/testing/run-loader.ts +296 -0
  163. package/src/testing/run-middleware.ts +170 -0
  164. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  165. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  166. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  167. package/src/testing/vitest-stubs/version.ts +5 -0
  168. package/src/testing/vitest.ts +183 -0
  169. package/src/types/global-namespace.ts +39 -26
  170. package/src/types/handler-context.ts +56 -11
  171. package/src/types/index.ts +1 -0
  172. package/src/types/segments.ts +18 -1
  173. package/src/urls/include-helper.ts +10 -53
  174. package/src/urls/index.ts +0 -3
  175. package/src/urls/path-helper-types.ts +11 -3
  176. package/src/urls/path-helper.ts +17 -52
  177. package/src/urls/pattern-types.ts +36 -19
  178. package/src/urls/response-types.ts +20 -19
  179. package/src/urls/type-extraction.ts +26 -116
  180. package/src/urls/urls-function.ts +1 -5
  181. package/src/use-loader.tsx +413 -42
  182. package/src/vite/debug.ts +1 -0
  183. package/src/vite/discovery/bundle-postprocess.ts +6 -6
  184. package/src/vite/discovery/discover-routers.ts +70 -48
  185. package/src/vite/discovery/discovery-errors.ts +194 -0
  186. package/src/vite/discovery/prerender-collection.ts +19 -25
  187. package/src/vite/discovery/route-types-writer.ts +40 -84
  188. package/src/vite/discovery/state.ts +33 -0
  189. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  190. package/src/vite/index.ts +2 -0
  191. package/src/vite/plugin-types.ts +67 -0
  192. package/src/vite/plugins/cjs-to-esm.ts +3 -7
  193. package/src/vite/plugins/client-ref-hashing.ts +12 -1
  194. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
  195. package/src/vite/plugins/expose-action-id.ts +2 -2
  196. package/src/vite/plugins/expose-id-utils.ts +12 -8
  197. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  198. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  199. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  200. package/src/vite/plugins/expose-internal-ids.ts +47 -67
  201. package/src/vite/plugins/performance-tracks.ts +12 -16
  202. package/src/vite/plugins/use-cache-transform.ts +13 -11
  203. package/src/vite/plugins/version-injector.ts +2 -12
  204. package/src/vite/plugins/version-plugin.ts +59 -2
  205. package/src/vite/plugins/virtual-entries.ts +2 -2
  206. package/src/vite/rango.ts +67 -15
  207. package/src/vite/router-discovery.ts +208 -63
  208. package/src/vite/utils/ast-handler-extract.ts +15 -15
  209. package/src/vite/utils/bundle-analysis.ts +4 -2
  210. package/src/vite/utils/client-chunks.ts +190 -0
  211. package/src/vite/utils/forward-user-plugins.ts +193 -0
  212. package/src/vite/utils/manifest-utils.ts +21 -5
  213. package/src/vite/utils/shared-utils.ts +107 -26
  214. package/src/browser/action-response-classifier.ts +0 -99
@@ -6,6 +6,7 @@ import {
6
6
  buildExportMap,
7
7
  escapeRegExp,
8
8
  } from "../expose-id-utils.js";
9
+ import { codeMatchIndices } from "../../../build/route-types/source-scan.js";
9
10
  import type { CreateExportBinding } from "./types.js";
10
11
 
11
12
  /**
@@ -59,19 +60,57 @@ export function isExportOnlyFile(
59
60
  return true;
60
61
  }
61
62
 
62
- // NOTE: This regex may over-count when the fn name appears inside strings or
63
- // comments, but it's only used for the warning heuristic (totalCalls >
64
- // supportedBindings) and the inline-extraction pre-check, so over-counting
65
- // triggers a harmless extra AST parse rather than affecting correctness.
63
+ function createCallPattern(fnNames: string[]): RegExp {
64
+ return new RegExp(
65
+ `\\b(?:${fnNames.map(escapeRegExp).join("|")})\\s*(?:<[^>]*>\\s*)?\\(`,
66
+ "g",
67
+ );
68
+ }
69
+
70
+ // Counts real create*() call sites, ignoring occurrences inside comments and
71
+ // string literals. Used by the unsupported-shape warning heuristic and the
72
+ // inline-extraction pre-check.
66
73
  export function countCreateCallsForNames(
67
74
  code: string,
68
75
  fnNames: string[],
69
76
  ): number {
70
- const pattern = new RegExp(
71
- `\\b(?:${fnNames.map(escapeRegExp).join("|")})\\s*(?:<[^>]*>\\s*)?\\(`,
72
- "g",
73
- );
74
- return (code.match(pattern) || []).length;
77
+ return codeMatchIndices(code, createCallPattern(fnNames)).length;
78
+ }
79
+
80
+ /** Convert a 0-based byte offset to a 1-based { line, column }. */
81
+ export function offsetToLineColumn(
82
+ code: string,
83
+ index: number,
84
+ ): { line: number; column: number } {
85
+ let line = 1;
86
+ let lineStart = 0;
87
+ const end = Math.min(index, code.length);
88
+ for (let i = 0; i < end; i++) {
89
+ if (code[i] === "\n") {
90
+ line++;
91
+ lineStart = i + 1;
92
+ }
93
+ }
94
+ return { line, column: index - lineStart + 1 };
95
+ }
96
+
97
+ /**
98
+ * Locate every real create*() call site (comment/string-free) that is NOT one
99
+ * of the supported, id-injectable export bindings, returning each as a 1-based
100
+ * { line, column }. The empty result means every call is in a supported shape.
101
+ * Both binding-collection paths anchor `callExprStart` at the start of the
102
+ * create* identifier — exactly where this pattern matches — so the set
103
+ * difference is exact.
104
+ */
105
+ export function findUnsupportedCreateCallSites(
106
+ code: string,
107
+ fnNames: string[],
108
+ supportedBindings: CreateExportBinding[],
109
+ ): Array<{ line: number; column: number }> {
110
+ const supported = new Set(supportedBindings.map((b) => b.callExprStart));
111
+ return codeMatchIndices(code, createCallPattern(fnNames))
112
+ .filter((index) => !supported.has(index))
113
+ .map((index) => offsetToLineColumn(code, index));
75
114
  }
76
115
 
77
116
  export function getImportedFnNames(
@@ -119,6 +158,28 @@ export function getCalledIdentifierFromCall(callExpr: any): string | null {
119
158
  return null;
120
159
  }
121
160
 
161
+ /**
162
+ * plugin-react's dev Fast Refresh wraps exports whose function body uses
163
+ * hook-like calls in a signature-registration call. A loader/handle that calls
164
+ * `ctx.use(...)` trips this heuristic, so `export const X = createLoader(...)`
165
+ * becomes `export const X = _s(createLoader(...), "<sig>", true)` — the create*
166
+ * call is the first argument of an unrelated wrapper call. Unwrap a single such
167
+ * layer so ID injection still targets the inner create* call. The `$$id`
168
+ * assignment is appended after the whole statement (against the export local),
169
+ * which is unaffected by the wrapper since `_s(x)` returns `x`.
170
+ */
171
+ function unwrapSignatureWrappedCall(init: any, fnNameSet: Set<string>): any {
172
+ if (init?.type !== "CallExpression") return init;
173
+ const directId = getCalledIdentifierFromCall(init);
174
+ if (directId && fnNameSet.has(directId)) return init;
175
+ const firstArg = init.arguments?.[0];
176
+ if (firstArg?.type === "CallExpression") {
177
+ const innerId = getCalledIdentifierFromCall(firstArg);
178
+ if (innerId && fnNameSet.has(innerId)) return firstArg;
179
+ }
180
+ return init;
181
+ }
182
+
122
183
  export function collectCreateExportBindingsFallback(
123
184
  code: string,
124
185
  fnNames: string[],
@@ -196,7 +257,7 @@ export function collectCreateExportBindings(
196
257
  ): CreateExportBinding[] {
197
258
  if (!program) {
198
259
  try {
199
- program = parseAst(code, { jsx: true });
260
+ program = parseAst(code, { lang: "tsx" });
200
261
  } catch {
201
262
  return collectCreateExportBindingsFallback(code, fnNames);
202
263
  }
@@ -212,10 +273,13 @@ export function collectCreateExportBindings(
212
273
  }
213
274
 
214
275
  for (const decl of varDecl.declarations ?? []) {
215
- const calledIdentifier = getCalledIdentifierFromCall(decl?.init);
276
+ // Unwrap a Fast Refresh signature wrapper (`_s(createLoader(...), ...)`)
277
+ // so injection targets the inner create* call. Falls back to decl.init.
278
+ const callExpr = unwrapSignatureWrappedCall(decl?.init, fnNameSet);
279
+ const calledIdentifier = getCalledIdentifierFromCall(callExpr);
216
280
  if (
217
281
  decl?.id?.type !== "Identifier" ||
218
- decl?.init?.type !== "CallExpression" ||
282
+ callExpr?.type !== "CallExpression" ||
219
283
  !calledIdentifier ||
220
284
  !fnNameSet.has(calledIdentifier)
221
285
  ) {
@@ -226,9 +290,8 @@ export function collectCreateExportBindings(
226
290
  const exportNames = exportMap.get(localName) ?? [];
227
291
  if (exportNames.length === 0) continue;
228
292
 
229
- const callStart = decl.init.start as number;
230
- const callEnd = decl.init.end as number;
231
- const calleeEnd = decl.init.callee.end as number;
293
+ const callEnd = callExpr.end as number;
294
+ const calleeEnd = callExpr.callee.end as number;
232
295
 
233
296
  let openParenPos = -1;
234
297
  for (let i = calleeEnd; i < callEnd; i++) {
@@ -245,10 +308,10 @@ export function collectCreateExportBindings(
245
308
  bindings.push({
246
309
  localName,
247
310
  exportNames,
248
- callExprStart: decl.init.start as number,
311
+ callExprStart: callExpr.start as number,
249
312
  callOpenParenPos: openParenPos,
250
313
  callCloseParenPos: closeParenPos,
251
- argCount: decl.init.arguments?.length ?? 0,
314
+ argCount: callExpr.arguments?.length ?? 0,
252
315
  statementEnd,
253
316
  });
254
317
  }
@@ -282,9 +345,25 @@ export function collectCreateExportBindings(
282
345
  export function buildUnsupportedShapeWarning(
283
346
  filePath: string,
284
347
  fnName: string,
348
+ sites: Array<{ line: number; column: number }> = [],
285
349
  ): string {
286
- return [
287
- `[rsc-router] Unsupported ${fnName} shape in "${filePath}".`,
350
+ const lines = [`[rango] Unsupported ${fnName} shape in "${filePath}".`];
351
+
352
+ // Point at the exact call(s) so the location is clickable in the terminal/IDE
353
+ // (file:line:column) instead of leaving the user to scan the whole file.
354
+ if (sites.length === 1) {
355
+ const s = sites[0];
356
+ lines.push(
357
+ `The ${fnName}(...) call at ${filePath}:${s.line}:${s.column} has no stable $$id injected — it is not in a supported shape.`,
358
+ );
359
+ } else if (sites.length > 1) {
360
+ lines.push(
361
+ `These ${fnName}(...) calls have no stable $$id injected — they are not in a supported shape:`,
362
+ );
363
+ for (const s of sites) lines.push(` - ${filePath}:${s.line}:${s.column}`);
364
+ }
365
+
366
+ lines.push(
288
367
  `Supported shapes are:`,
289
368
  ` - export const X = ${fnName}(...)`,
290
369
  ` - const X = ${fnName}(...); export { X }`,
@@ -292,5 +371,6 @@ export function buildUnsupportedShapeWarning(
292
371
  `Potentially unsupported forms include:`,
293
372
  ` - export let/var X = ${fnName}(...)`,
294
373
  ` - inline ${fnName}(...) calls`,
295
- ].join("\n");
374
+ );
375
+ return lines.join("\n");
296
376
  }
@@ -1,5 +1,5 @@
1
1
  import MagicString from "magic-string";
2
- import { hashId } from "../expose-id-utils.js";
2
+ import { makeStubId } from "../expose-id-utils.js";
3
3
  import type { HandlerTransformConfig, CreateExportBinding } from "./types.js";
4
4
  import { isExportOnlyFile } from "./export-analysis.js";
5
5
 
@@ -28,9 +28,7 @@ export function transformHandles(
28
28
  binding.callCloseParenPos,
29
29
  );
30
30
 
31
- const handleId = isBuild
32
- ? hashId(filePath, exportName)
33
- : `${filePath}#${exportName}`;
31
+ const handleId = makeStubId(filePath, exportName, isBuild);
34
32
 
35
33
  let paramInjection: string;
36
34
  if (!args.hasArgs) {
@@ -58,9 +56,7 @@ export function transformLocationState(
58
56
  for (const binding of bindings) {
59
57
  const exportName = binding.exportNames[0];
60
58
 
61
- const stateKey = isBuild
62
- ? hashId(filePath, exportName)
63
- : `${filePath}#${exportName}`;
59
+ const stateKey = makeStubId(filePath, exportName, isBuild);
64
60
 
65
61
  // Key is injected as a property assignment (not as a function argument).
66
62
  // This allows createLocationState to accept options like { flash: true }
@@ -88,7 +84,7 @@ export function generateWholeFileStubs(
88
84
 
89
85
  const exportNames = bindings.flatMap((b) => b.exportNames);
90
86
  const stubs = exportNames.map((name) => {
91
- const handlerId = isBuild ? hashId(filePath, name) : `${filePath}#${name}`;
87
+ const handlerId = makeStubId(filePath, name, isBuild);
92
88
  return `export const ${name} = { __brand: "${cfg.brand}", $$id: "${handlerId}" };`;
93
89
  });
94
90
 
@@ -96,53 +92,8 @@ export function generateWholeFileStubs(
96
92
  }
97
93
 
98
94
  /**
99
- * Replace handler call expressions with lightweight stub objects in non-RSC
100
- * environments. Other exports, imports, and module-level code remain untouched.
101
- */
102
- export function generateExprStubs(
103
- cfg: HandlerTransformConfig,
104
- bindings: CreateExportBinding[],
105
- code: string,
106
- filePath: string,
107
- sourceId: string,
108
- isBuild: boolean,
109
- ): { code: string; map: ReturnType<MagicString["generateMap"]> } | null {
110
- if (bindings.length === 0) return null;
111
-
112
- const s = new MagicString(code);
113
- let hasChanges = false;
114
-
115
- for (const binding of bindings) {
116
- const exportName = binding.exportNames[0];
117
- const handlerId = isBuild
118
- ? hashId(filePath, exportName)
119
- : `${filePath}#${exportName}`;
120
-
121
- s.overwrite(
122
- binding.callExprStart,
123
- binding.callCloseParenPos + 1,
124
- `{ __brand: "${cfg.brand}", $$id: "${handlerId}" }`,
125
- );
126
- hasChanges = true;
127
- }
128
-
129
- if (!hasChanges) return null;
130
-
131
- return {
132
- code: s.toString(),
133
- map: s.generateMap({
134
- source: sourceId,
135
- includeContent: true,
136
- hires: "boundary",
137
- }),
138
- };
139
- }
140
-
141
- /**
142
- * Replace handler call expressions with lightweight stub objects on an
143
- * existing MagicString. Unlike generateExprStubs (which creates its own
144
- * MagicString and returns the full result), this integrates into the
145
- * unified transform pipeline so all transforms share one sourcemap.
95
+ * Replace handler call expressions with lightweight stub objects on the shared
96
+ * unified-pipeline MagicString so all transforms share one sourcemap.
146
97
  */
147
98
  export function stubHandlerExprs(
148
99
  cfg: HandlerTransformConfig,
@@ -154,9 +105,7 @@ export function stubHandlerExprs(
154
105
  let hasChanges = false;
155
106
  for (const binding of bindings) {
156
107
  const exportName = binding.exportNames[0];
157
- const handlerId = isBuild
158
- ? hashId(filePath, exportName)
159
- : `${filePath}#${exportName}`;
108
+ const handlerId = makeStubId(filePath, exportName, isBuild);
160
109
 
161
110
  s.overwrite(
162
111
  binding.callExprStart,
@@ -182,9 +131,7 @@ export function transformHandlerIds(
182
131
  for (const binding of bindings) {
183
132
  const exportName = binding.exportNames[0];
184
133
 
185
- const handlerId = isBuild
186
- ? hashId(filePath, exportName)
187
- : `${filePath}#${exportName}`;
134
+ const handlerId = makeStubId(filePath, exportName, isBuild);
188
135
 
189
136
  // Injection strategy matches the runtime overload signatures:
190
137
  // 0 args -> inject undefined, "id"
@@ -1,5 +1,5 @@
1
1
  import type MagicString from "magic-string";
2
- import { hashId } from "../expose-id-utils.js";
2
+ import { makeStubId } from "../expose-id-utils.js";
3
3
  import type { CreateExportBinding } from "./types.js";
4
4
  import { isExportOnlyFile } from "./export-analysis.js";
5
5
 
@@ -33,7 +33,7 @@ export function generateClientLoaderStubs(
33
33
 
34
34
  for (const binding of bindings) {
35
35
  for (const name of binding.exportNames) {
36
- const loaderId = isBuild ? hashId(filePath, name) : `${filePath}#${name}`;
36
+ const loaderId = makeStubId(filePath, name, isBuild);
37
37
  lines.push(
38
38
  `export const ${name} = { __brand: "loader", $$id: "${loaderId}" };`,
39
39
  );
@@ -54,9 +54,7 @@ export function transformLoaders(
54
54
  for (const binding of bindings) {
55
55
  const exportName = binding.exportNames[0];
56
56
 
57
- const loaderId = isBuild
58
- ? hashId(filePath, exportName)
59
- : `${filePath}#${exportName}`;
57
+ const loaderId = makeStubId(filePath, exportName, isBuild);
60
58
 
61
59
  // Inject $$id as hidden third parameter.
62
60
  // createLoader(fn) -> createLoader(fn, undefined, "id")
@@ -28,6 +28,7 @@ import {
28
28
  getImportedFnNames,
29
29
  collectCreateExportBindings,
30
30
  buildUnsupportedShapeWarning,
31
+ findUnsupportedCreateCallSites,
31
32
  isExportOnlyFile,
32
33
  } from "./expose-ids/export-analysis.js";
33
34
  import {
@@ -39,7 +40,6 @@ import {
39
40
  transformHandles,
40
41
  transformLocationState,
41
42
  generateWholeFileStubs,
42
- generateExprStubs,
43
43
  stubHandlerExprs,
44
44
  transformHandlerIds,
45
45
  } from "./expose-ids/handler-transform.js";
@@ -335,7 +335,7 @@ ${lazyImports.join(",\n")}
335
335
  }
336
336
  if (_cachedAst !== undefined || _astParseFailed) return _cachedAst;
337
337
  try {
338
- _cachedAst = parseAst(code, { jsx: true });
338
+ _cachedAst = parseAst(code, { lang: "tsx" });
339
339
  } catch {
340
340
  _astParseFailed = true;
341
341
  }
@@ -371,14 +371,21 @@ ${lazyImports.join(",\n")}
371
371
  if (!hasCode) continue;
372
372
 
373
373
  const fnNames = getFnNames(cfg.fnName);
374
- const totalCalls = countCreateCallsForNames(code, fnNames);
375
- const supportedBindings = getBindings(code, fnNames).length;
376
- if (totalCalls <= supportedBindings) continue;
374
+ // Locate the real (comment/string-free) create* calls not covered by
375
+ // a supported, id-injectable export shape. Empty means every call is
376
+ // fine in particular, a create*() token in a comment or string no
377
+ // longer trips a spurious warning.
378
+ const sites = findUnsupportedCreateCallSites(
379
+ code,
380
+ fnNames,
381
+ getBindings(code, fnNames),
382
+ );
383
+ if (sites.length === 0) continue;
377
384
 
378
385
  const warnKey = `${id}::${cfg.fnName}`;
379
386
  if (unsupportedShapeWarnings.has(warnKey)) continue;
380
387
  unsupportedShapeWarnings.add(warnKey);
381
- this.warn(buildUnsupportedShapeWarning(filePath, cfg.fnName));
388
+ this.warn(buildUnsupportedShapeWarning(filePath, cfg.fnName, sites));
382
389
  }
383
390
 
384
391
  // --- Loader: track for manifest (RSC env only) ---
@@ -424,17 +431,6 @@ ${lazyImports.join(",\n")}
424
431
  if (wholeFile) return wholeFile;
425
432
  }
426
433
 
427
- // --- PrerenderHandler: RSC build module tracking ---
428
- if (hasPrerenderHandlerCode && isRscEnv && isBuild) {
429
- const fnNames = getFnNames(PRERENDER_CONFIG.fnName);
430
- const exportNames = getBindings(code, fnNames).map(
431
- (b) => b.exportNames[0],
432
- );
433
- if (exportNames.length > 0) {
434
- prerenderHandlerModules.set(id, exportNames);
435
- }
436
- }
437
-
438
434
  // --- Inline handler extraction to virtual modules ---
439
435
  // Runs before stubs/tracking so inline calls become imports, then
440
436
  // the existing regex fast path handles both the original file's
@@ -710,14 +706,27 @@ ${lazyImports.join(",\n")}
710
706
  }
711
707
  }
712
708
 
713
- // --- StaticHandler: RSC build module tracking ---
714
- if (hasStaticHandlerCode && isRscEnv && isBuild) {
715
- const fnNames = getFnNames(STATIC_CONFIG.fnName);
716
- const exportNames = getBindings(code, fnNames).map(
717
- (b) => b.exportNames[0],
718
- );
719
- if (exportNames.length > 0) {
720
- staticHandlerModules.set(id, exportNames);
709
+ // RSC build module tracking (prerender + static), consumed via the
710
+ // plugin API for prerender freezing. Export-binding sets are invariant
711
+ // across the inline-extraction loop, so tracking both here is equivalent
712
+ // to the pre-extraction prerender tracking this replaces.
713
+ if (isRscEnv && isBuild) {
714
+ const trackTypes: Array<
715
+ [boolean, HandlerTransformConfig, Map<string, string[]>]
716
+ > = [
717
+ [
718
+ hasPrerenderHandlerCode,
719
+ PRERENDER_CONFIG,
720
+ prerenderHandlerModules,
721
+ ],
722
+ [hasStaticHandlerCode, STATIC_CONFIG, staticHandlerModules],
723
+ ];
724
+ for (const [has, cfg, trackMap] of trackTypes) {
725
+ if (!has) continue;
726
+ const exportNames = getBindings(code, getFnNames(cfg.fnName)).map(
727
+ (b) => b.exportNames[0],
728
+ );
729
+ if (exportNames.length > 0) trackMap.set(id, exportNames);
721
730
  }
722
731
  }
723
732
 
@@ -758,48 +767,19 @@ ${lazyImports.join(",\n")}
758
767
  isBuild,
759
768
  ) || changed;
760
769
  }
761
- if (hasPrerenderHandlerCode) {
762
- const fnNames = getFnNames(PRERENDER_CONFIG.fnName);
763
- const bindings = getBindings(code, fnNames);
764
- if (isRscEnv) {
765
- changed =
766
- transformHandlerIds(
767
- PRERENDER_CONFIG,
768
- bindings,
769
- s,
770
- filePath,
771
- isBuild,
772
- ) || changed;
773
- } else {
774
- // Non-RSC mixed-export file: replace Prerender() calls with stubs
775
- // on the shared MagicString so sourcemaps stay accurate.
776
- changed =
777
- stubHandlerExprs(
778
- PRERENDER_CONFIG,
779
- bindings,
780
- s,
781
- filePath,
782
- isBuild,
783
- ) || changed;
784
- }
785
- }
786
- if (hasStaticHandlerCode) {
787
- const fnNames = getFnNames(STATIC_CONFIG.fnName);
788
- const bindings = getBindings(code, fnNames);
789
- if (isRscEnv) {
790
- changed =
791
- transformHandlerIds(
792
- STATIC_CONFIG,
793
- bindings,
794
- s,
795
- filePath,
796
- isBuild,
797
- ) || changed;
798
- } else {
799
- changed =
800
- stubHandlerExprs(STATIC_CONFIG, bindings, s, filePath, isBuild) ||
801
- changed;
802
- }
770
+ // Prerender + Static share the RSC inject-id vs non-RSC stub dispatch.
771
+ // Call sites are disjoint (distinct fnNames), so loop order is irrelevant.
772
+ const finalHandlerConfigs = [
773
+ hasPrerenderHandlerCode && PRERENDER_CONFIG,
774
+ hasStaticHandlerCode && STATIC_CONFIG,
775
+ ].filter((c): c is HandlerTransformConfig => !!c);
776
+ for (const cfg of finalHandlerConfigs) {
777
+ const bindings = getBindings(code, getFnNames(cfg.fnName));
778
+ changed =
779
+ (isRscEnv
780
+ ? transformHandlerIds(cfg, bindings, s, filePath, isBuild)
781
+ : stubHandlerExprs(cfg, bindings, s, filePath, isBuild)) ||
782
+ changed;
803
783
  }
804
784
 
805
785
  if (!changed) return;
@@ -48,25 +48,21 @@ export function patchRsdwClientDebugInfoRecovery(code: string): {
48
48
 
49
49
  export function performanceTracksOptimizeDepsPlugin(): {
50
50
  name: string;
51
- setup(build: any): void;
51
+ load(id: string): Promise<{ code: string } | null>;
52
52
  } {
53
+ const RSDW_CLIENT_RE =
54
+ /react-server-dom-webpack-client\.browser\.(development|production)\.js$/;
53
55
  return {
54
56
  name: "@rangojs/router:performance-tracks-optimize-deps",
55
- setup(build: any): void {
56
- build.onLoad(
57
- {
58
- filter:
59
- /react-server-dom-webpack-client\.browser\.(development|production)\.js$/,
60
- },
61
- async (args: { path: string }) => {
62
- const code = await readFile(args.path, "utf8");
63
- const patched = patchRsdwClientDebugInfoRecovery(code);
64
- return {
65
- contents: patched.code,
66
- loader: "js",
67
- };
68
- },
69
- );
57
+ // Vite 8 optimizes deps with Rolldown (Rollup-style plugin pipeline), so the
58
+ // pre-bundled RSDW client is patched via load() rather than esbuild's onLoad.
59
+ // Returning code overrides Rolldown's default filesystem read for the module.
60
+ async load(id: string): Promise<{ code: string } | null> {
61
+ const cleanId = id.split("?")[0] ?? id;
62
+ if (!RSDW_CLIENT_RE.test(cleanId)) return null;
63
+ const code = await readFile(cleanId, "utf8");
64
+ const patched = patchRsdwClientDebugInfoRecovery(code);
65
+ return { code: patched.code };
70
66
  },
71
67
  };
72
68
  }
@@ -30,6 +30,13 @@ const CACHE_RUNTIME_IMPORT = "@rangojs/router/cache-runtime";
30
30
  // and should not be wrapped (children can't be cache-keyed).
31
31
  const LAYOUT_TEMPLATE_PATTERN = /\/(layout|template)\.(tsx?|jsx?)$/;
32
32
 
33
+ /**
34
+ * Grammar for a valid function-level directive: `use cache` optionally followed
35
+ * by `: <profile-name>`. The single source of truth for both the transform and
36
+ * the near-miss validator below.
37
+ */
38
+ export const USE_CACHE_DIRECTIVE_RE: RegExp = /^use cache(:\s*[\w-]+)?$/;
39
+
33
40
  export function useCacheTransform(): Plugin {
34
41
  let projectRoot = "";
35
42
  let isBuild = false;
@@ -84,7 +91,7 @@ export function useCacheTransform(): Plugin {
84
91
  let ast: any;
85
92
  try {
86
93
  const { parseAst } = await import("vite");
87
- ast = parseAst(code);
94
+ ast = parseAst(code, { lang: "tsx" });
88
95
  } catch {
89
96
  return;
90
97
  }
@@ -116,9 +123,9 @@ export function useCacheTransform(): Plugin {
116
123
  transformHoistInlineDirective,
117
124
  );
118
125
 
119
- // Always check for near-miss directives, even when valid directives
120
- // exist. A file may contain both valid and invalid "use cache" directives
121
- // in different functions the invalid ones should still warn.
126
+ // Check for near-miss directives on the function-level path. The
127
+ // file-level branch above returns earlier (it wraps every export
128
+ // regardless), so this runs only when there is no file-level directive.
122
129
  warnOnNearMissDirectives(ast, id, this.warn.bind(this));
123
130
 
124
131
  if (functionResult) return functionResult;
@@ -219,7 +226,7 @@ function transformFunctionLevelUseCache(
219
226
  ) {
220
227
  try {
221
228
  const { output, names } = transformHoistInlineDirective(code, ast, {
222
- directive: /^use cache(:\s*[\w-]+)?$/,
229
+ directive: USE_CACHE_DIRECTIVE_RE,
223
230
  runtime: (
224
231
  value: string,
225
232
  name: string,
@@ -273,11 +280,6 @@ function findFileLevelDirective(
273
280
  return null;
274
281
  }
275
282
 
276
- /**
277
- * The valid directive regex (must stay in sync with transformFunctionLevelUseCache).
278
- */
279
- const VALID_DIRECTIVE_RE = /^use cache(:\s*[\w-]+)?$/;
280
-
281
283
  /**
282
284
  * Regex for near-miss: starts with "use cache:" but has invalid tokens.
283
285
  */
@@ -307,7 +309,7 @@ function warnOnNearMissDirectives(
307
309
  if (
308
310
  value.startsWith("use cache") &&
309
311
  NEAR_MISS_RE.test(value) &&
310
- !VALID_DIRECTIVE_RE.test(value)
312
+ !USE_CACHE_DIRECTIVE_RE.test(value)
311
313
  ) {
312
314
  const profilePart = value.slice("use cache:".length).trim();
313
315
  warn(
@@ -1,6 +1,7 @@
1
1
  import type { Plugin } from "vite";
2
2
  import { resolve } from "node:path";
3
3
  import * as Vite from "vite";
4
+ import { resolveRscEntryFromConfig } from "../utils/shared-utils.js";
4
5
 
5
6
  /**
6
7
  * Plugin that auto-injects VERSION and routes-manifest into custom entry.rsc files.
@@ -20,18 +21,7 @@ export function createVersionInjectorPlugin(
20
21
 
21
22
  configResolved(config) {
22
23
  let entryPath = rscEntryPath;
23
- // Cloudflare preset: read entry from resolved environment config.
24
- // The @cloudflare/vite-plugin reads wrangler config (toml/json/jsonc)
25
- // and sets optimizeDeps.entries on the RSC environment.
26
- if (!entryPath) {
27
- const rscEnvConfig = (config.environments as any)?.["rsc"];
28
- const entries = rscEnvConfig?.optimizeDeps?.entries;
29
- if (typeof entries === "string") {
30
- entryPath = entries;
31
- } else if (Array.isArray(entries) && entries.length > 0) {
32
- entryPath = entries[0];
33
- }
34
- }
24
+ if (!entryPath) entryPath = resolveRscEntryFromConfig(config);
35
25
  if (entryPath) {
36
26
  resolvedEntryPath = resolve(config.root, entryPath);
37
27
  }