@rangojs/router 0.0.0-experimental.18 → 0.0.0-experimental.19

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 (177) hide show
  1. package/README.md +46 -8
  2. package/dist/bin/rango.js +105 -18
  3. package/dist/vite/index.js +227 -93
  4. package/package.json +15 -14
  5. package/skills/hooks/SKILL.md +1 -1
  6. package/skills/intercept/SKILL.md +79 -0
  7. package/skills/layout/SKILL.md +62 -2
  8. package/skills/loader/SKILL.md +94 -1
  9. package/skills/middleware/SKILL.md +81 -0
  10. package/skills/parallel/SKILL.md +57 -2
  11. package/skills/prerender/SKILL.md +187 -17
  12. package/skills/route/SKILL.md +42 -1
  13. package/skills/router-setup/SKILL.md +77 -0
  14. package/src/__internal.ts +1 -1
  15. package/src/bin/rango.ts +38 -19
  16. package/src/browser/action-coordinator.ts +97 -0
  17. package/src/browser/event-controller.ts +25 -27
  18. package/src/browser/history-state.ts +80 -0
  19. package/src/browser/intercept-utils.ts +1 -1
  20. package/src/browser/link-interceptor.ts +0 -3
  21. package/src/browser/merge-segment-loaders.ts +9 -2
  22. package/src/browser/navigation-bridge.ts +46 -13
  23. package/src/browser/navigation-client.ts +32 -61
  24. package/src/browser/navigation-store.ts +1 -31
  25. package/src/browser/navigation-transaction.ts +46 -207
  26. package/src/browser/partial-update.ts +102 -150
  27. package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
  28. package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
  29. package/src/browser/prefetch/policy.ts +42 -0
  30. package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
  31. package/src/browser/react/Link.tsx +28 -23
  32. package/src/browser/react/NavigationProvider.tsx +9 -1
  33. package/src/browser/react/index.ts +2 -6
  34. package/src/browser/react/location-state-shared.ts +1 -1
  35. package/src/browser/react/location-state.ts +2 -0
  36. package/src/browser/react/nonce-context.ts +23 -0
  37. package/src/browser/react/use-action.ts +9 -1
  38. package/src/browser/react/use-handle.ts +3 -25
  39. package/src/browser/react/use-params.ts +2 -4
  40. package/src/browser/react/use-pathname.ts +2 -3
  41. package/src/browser/react/use-router.ts +1 -1
  42. package/src/browser/react/use-search-params.ts +2 -1
  43. package/src/browser/react/use-segments.ts +7 -60
  44. package/src/browser/response-adapter.ts +73 -0
  45. package/src/browser/rsc-router.tsx +29 -23
  46. package/src/browser/scroll-restoration.ts +10 -7
  47. package/src/browser/server-action-bridge.ts +115 -96
  48. package/src/browser/types.ts +1 -31
  49. package/src/browser/validate-redirect-origin.ts +29 -0
  50. package/src/build/generate-manifest.ts +5 -0
  51. package/src/build/generate-route-types.ts +2 -0
  52. package/src/build/route-types/codegen.ts +13 -4
  53. package/src/build/route-types/include-resolution.ts +13 -0
  54. package/src/build/route-types/per-module-writer.ts +15 -3
  55. package/src/build/route-types/router-processing.ts +45 -3
  56. package/src/build/runtime-discovery.ts +13 -1
  57. package/src/cache/background-task.ts +34 -0
  58. package/src/cache/cache-key-utils.ts +44 -0
  59. package/src/cache/cache-policy.ts +125 -0
  60. package/src/cache/cache-runtime.ts +132 -96
  61. package/src/cache/cache-scope.ts +71 -73
  62. package/src/cache/cf/cf-cache-store.ts +9 -4
  63. package/src/cache/document-cache.ts +72 -47
  64. package/src/cache/handle-capture.ts +81 -0
  65. package/src/cache/memory-segment-store.ts +18 -7
  66. package/src/cache/profile-registry.ts +43 -8
  67. package/src/cache/read-through-swr.ts +134 -0
  68. package/src/cache/segment-codec.ts +101 -112
  69. package/src/cache/taint.ts +26 -0
  70. package/src/client.tsx +53 -30
  71. package/src/errors.ts +6 -1
  72. package/src/handle.ts +1 -1
  73. package/src/handles/MetaTags.tsx +5 -2
  74. package/src/host/cookie-handler.ts +8 -3
  75. package/src/host/router.ts +14 -1
  76. package/src/href-client.ts +3 -1
  77. package/src/index.rsc.ts +33 -1
  78. package/src/index.ts +27 -0
  79. package/src/loader.rsc.ts +12 -4
  80. package/src/loader.ts +8 -0
  81. package/src/prerender/store.ts +4 -3
  82. package/src/prerender.ts +76 -18
  83. package/src/reverse.ts +11 -7
  84. package/src/root-error-boundary.tsx +30 -26
  85. package/src/route-definition/dsl-helpers.ts +9 -6
  86. package/src/route-definition/redirect.ts +15 -3
  87. package/src/route-map-builder.ts +38 -2
  88. package/src/route-name.ts +53 -0
  89. package/src/route-types.ts +7 -0
  90. package/src/router/content-negotiation.ts +1 -1
  91. package/src/router/debug-manifest.ts +16 -3
  92. package/src/router/handler-context.ts +94 -15
  93. package/src/router/intercept-resolution.ts +6 -4
  94. package/src/router/lazy-includes.ts +4 -0
  95. package/src/router/loader-resolution.ts +1 -0
  96. package/src/router/logging.ts +100 -3
  97. package/src/router/manifest.ts +32 -3
  98. package/src/router/match-api.ts +61 -7
  99. package/src/router/match-context.ts +3 -0
  100. package/src/router/match-handlers.ts +185 -11
  101. package/src/router/match-middleware/background-revalidation.ts +65 -85
  102. package/src/router/match-middleware/cache-lookup.ts +69 -4
  103. package/src/router/match-middleware/cache-store.ts +2 -0
  104. package/src/router/match-pipelines.ts +8 -43
  105. package/src/router/middleware-types.ts +7 -0
  106. package/src/router/middleware.ts +93 -8
  107. package/src/router/pattern-matching.ts +41 -5
  108. package/src/router/prerender-match.ts +34 -6
  109. package/src/router/preview-match.ts +7 -1
  110. package/src/router/revalidation.ts +61 -2
  111. package/src/router/router-context.ts +15 -0
  112. package/src/router/router-interfaces.ts +34 -0
  113. package/src/router/router-options.ts +200 -0
  114. package/src/router/segment-resolution/fresh.ts +123 -30
  115. package/src/router/segment-resolution/helpers.ts +19 -0
  116. package/src/router/segment-resolution/loader-cache.ts +37 -146
  117. package/src/router/segment-resolution/revalidation.ts +358 -94
  118. package/src/router/segment-wrappers.ts +3 -0
  119. package/src/router/telemetry-otel.ts +299 -0
  120. package/src/router/telemetry.ts +300 -0
  121. package/src/router/timeout.ts +148 -0
  122. package/src/router/types.ts +7 -1
  123. package/src/router.ts +155 -11
  124. package/src/rsc/handler-context.ts +11 -0
  125. package/src/rsc/handler.ts +380 -88
  126. package/src/rsc/helpers.ts +25 -16
  127. package/src/rsc/loader-fetch.ts +84 -42
  128. package/src/rsc/origin-guard.ts +141 -0
  129. package/src/rsc/progressive-enhancement.ts +232 -19
  130. package/src/rsc/response-route-handler.ts +37 -26
  131. package/src/rsc/rsc-rendering.ts +12 -5
  132. package/src/rsc/runtime-warnings.ts +42 -0
  133. package/src/rsc/server-action.ts +134 -58
  134. package/src/rsc/types.ts +8 -0
  135. package/src/search-params.ts +22 -10
  136. package/src/server/context.ts +53 -5
  137. package/src/server/fetchable-loader-store.ts +11 -6
  138. package/src/server/handle-store.ts +66 -9
  139. package/src/server/loader-registry.ts +11 -46
  140. package/src/server/request-context.ts +90 -9
  141. package/src/ssr/index.tsx +63 -27
  142. package/src/static-handler.ts +7 -0
  143. package/src/theme/ThemeProvider.tsx +6 -1
  144. package/src/theme/index.ts +1 -6
  145. package/src/theme/theme-context.ts +1 -28
  146. package/src/theme/theme-script.ts +2 -1
  147. package/src/types/cache-types.ts +5 -0
  148. package/src/types/error-types.ts +3 -0
  149. package/src/types/global-namespace.ts +9 -0
  150. package/src/types/handler-context.ts +35 -13
  151. package/src/types/loader-types.ts +7 -0
  152. package/src/types/route-entry.ts +28 -0
  153. package/src/urls/include-helper.ts +49 -8
  154. package/src/urls/index.ts +1 -0
  155. package/src/urls/path-helper-types.ts +30 -12
  156. package/src/urls/path-helper.ts +17 -2
  157. package/src/urls/pattern-types.ts +21 -1
  158. package/src/urls/response-types.ts +27 -2
  159. package/src/urls/type-extraction.ts +23 -15
  160. package/src/use-loader.tsx +12 -4
  161. package/src/vite/discovery/bundle-postprocess.ts +12 -7
  162. package/src/vite/discovery/discover-routers.ts +30 -18
  163. package/src/vite/discovery/prerender-collection.ts +24 -27
  164. package/src/vite/discovery/route-types-writer.ts +7 -7
  165. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  166. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  167. package/src/vite/plugins/use-cache-transform.ts +91 -3
  168. package/src/vite/rango.ts +3 -3
  169. package/src/vite/router-discovery.ts +99 -36
  170. package/src/vite/utils/prerender-utils.ts +21 -0
  171. package/src/vite/utils/shared-utils.ts +3 -1
  172. package/src/browser/request-controller.ts +0 -164
  173. package/src/href-context.ts +0 -33
  174. package/src/router.gen.ts +0 -6
  175. package/src/static-handler.gen.ts +0 -5
  176. package/src/urls.gen.ts +0 -8
  177. /package/src/browser/{prefetch-observer.ts → prefetch/observer.ts} +0 -0
@@ -88,6 +88,7 @@ function useLoaderInternal<T>(
88
88
  const [fetchedData, setFetchedData] = useState<T | undefined>(undefined);
89
89
  const [isLoading, setIsLoading] = useState(false);
90
90
  const [error, setError] = useState<Error | null>(null);
91
+ const requestIdRef = useRef(0);
91
92
 
92
93
  // Track context data changes to reset fetched data on navigation
93
94
  const prevContextDataRef = useRef(contextData);
@@ -118,6 +119,7 @@ function useLoaderInternal<T>(
118
119
  // Supports GET (data fetching) and POST/PUT/PATCH/DELETE (mutations).
119
120
  const load = useCallback(
120
121
  async (loadOptions?: LoadOptions): Promise<T> => {
122
+ const requestId = ++requestIdRef.current;
121
123
  const loaderId = loaderIdRef.current;
122
124
  // Verify the loader has $$id
123
125
  if (!loaderId) {
@@ -130,7 +132,7 @@ function useLoaderInternal<T>(
130
132
  setError(null);
131
133
 
132
134
  try {
133
- const url = new URL(window.location.pathname, window.location.origin);
135
+ const url = new URL(window.location.href);
134
136
  url.searchParams.set("_rsc_loader", loaderId);
135
137
 
136
138
  const method = loadOptions?.method ?? "GET";
@@ -212,11 +214,15 @@ function useLoaderInternal<T>(
212
214
  }
213
215
 
214
216
  const result = payload.loaderResult;
215
- setFetchedData(result);
217
+ if (requestId === requestIdRef.current) {
218
+ setFetchedData(result);
219
+ }
216
220
  return result;
217
221
  } catch (e) {
218
222
  const err = e instanceof Error ? e : new Error(String(e));
219
- setError(err);
223
+ if (requestId === requestIdRef.current) {
224
+ setError(err);
225
+ }
220
226
  if (throwOnError) {
221
227
  throw err;
222
228
  }
@@ -224,7 +230,9 @@ function useLoaderInternal<T>(
224
230
  // successful value or undefined). Caller should check error state.
225
231
  return dataRef.current as T;
226
232
  } finally {
227
- setIsLoading(false);
233
+ if (requestId === requestIdRef.current) {
234
+ setIsLoading(false);
235
+ }
228
236
  }
229
237
  },
230
238
  [throwOnError],
@@ -91,7 +91,10 @@ export function postprocessBundle(state: DiscoveryState): void {
91
91
  // and inject a manifest import into the RSC entry.
92
92
  if (hasPrerenderData && existsSync(rscEntryPath)) {
93
93
  const rscCode = readFileSync(rscEntryPath, "utf-8");
94
- if (!rscCode.includes("__PRERENDER_MANIFEST")) {
94
+ // Check for the specific injection marker, not just the variable name.
95
+ // The runtime code (prerender store) also references __PRERENDER_MANIFEST,
96
+ // so a broad string check would false-positive and skip injection.
97
+ if (!rscCode.includes("__prerender-manifest.js")) {
95
98
  try {
96
99
  const assetsDir = resolve(state.projectRoot, "dist/rsc/assets");
97
100
  mkdirSync(assetsDir, { recursive: true });
@@ -155,17 +158,19 @@ export function postprocessBundle(state: DiscoveryState): void {
155
158
  for (const [handlerId, { encoded, handles }] of Object.entries(
156
159
  state.staticCollectedData!,
157
160
  )) {
158
- const contentHash = createHash("sha256")
159
- .update(encoded)
160
- .digest("hex")
161
- .slice(0, 8);
162
- const assetFileName = `__st-${contentHash}.js`;
163
- const assetPath = resolve(assetsDir, assetFileName);
164
161
  // Store both the Flight payload and handle data
165
162
  const hasHandles = Object.keys(handles).length > 0;
166
163
  const exportValue = hasHandles
167
164
  ? JSON.stringify({ encoded, handles })
168
165
  : JSON.stringify(encoded);
166
+ // Hash the full payload that is written so distinct handle
167
+ // snapshots produce distinct asset filenames.
168
+ const contentHash = createHash("sha256")
169
+ .update(exportValue)
170
+ .digest("hex")
171
+ .slice(0, 8);
172
+ const assetFileName = `__st-${contentHash}.js`;
173
+ const assetPath = resolve(assetsDir, assetFileName);
169
174
  const assetCode = `export default ${exportValue};\n`;
170
175
  writeFileSync(assetPath, assetCode);
171
176
  totalBytes += Buffer.byteLength(assetCode);
@@ -100,12 +100,15 @@ export async function discoverRouters(
100
100
  const buildMod = await rscEnv.runner.import("@rangojs/router/build");
101
101
  const generateManifestFull = buildMod.generateManifestFull;
102
102
 
103
- state.mergedRouteManifest = {};
104
- state.mergedPrecomputedEntries = [];
105
- state.perRouterManifests = [];
106
- state.perRouterManifestDataMap = new Map();
107
- state.perRouterPrecomputedMap = new Map();
108
- state.perRouterTrieMap = new Map();
103
+ // Build into local variables first. Only commit to state after the
104
+ // full pass succeeds, so a failed re-discovery preserves the last
105
+ // known-good state instead of leaving it partially wiped.
106
+ const newMergedRouteManifest: Record<string, string> = {};
107
+ const newMergedPrecomputedEntries: PrecomputedEntry[] = [];
108
+ const newPerRouterManifests: typeof state.perRouterManifests = [];
109
+ const newPerRouterManifestDataMap = new Map<string, any>();
110
+ const newPerRouterPrecomputedMap = new Map<string, PrecomputedEntry[]>();
111
+ const newPerRouterTrieMap = new Map<string, any>();
109
112
  let mergedRouteAncestry: Record<string, string[]> = {};
110
113
  let mergedRouteTrailingSlash: Record<string, string> = {};
111
114
 
@@ -128,7 +131,7 @@ export async function discoverRouters(
128
131
  const dynamicRoutes = routeCount - staticRoutes;
129
132
 
130
133
  // Merge into the combined manifest
131
- Object.assign(state.mergedRouteManifest, manifest.routeManifest);
134
+ Object.assign(newMergedRouteManifest, manifest.routeManifest);
132
135
 
133
136
  // Compute factory-only prefixes: dot-prefixed groups in the runtime
134
137
  // manifest that the static parser cannot see. These are routes created
@@ -152,7 +155,7 @@ export async function discoverRouters(
152
155
  if (factoryOnlyPrefixes.size === 0) factoryOnlyPrefixes = undefined;
153
156
  }
154
157
 
155
- state.perRouterManifests.push({
158
+ newPerRouterManifests.push({
156
159
  id,
157
160
  routeManifest: manifest.routeManifest,
158
161
  routeSearchSchemas: manifest.routeSearchSchemas,
@@ -175,18 +178,18 @@ export async function discoverRouters(
175
178
  flattenLeafEntries(
176
179
  manifest.prefixTree,
177
180
  manifest.routeManifest,
178
- state.mergedPrecomputedEntries,
181
+ newMergedPrecomputedEntries,
179
182
  );
180
183
 
181
184
  // Store per-router manifest and precomputed entries for isolated virtual modules.
182
- state.perRouterManifestDataMap.set(id, manifest.routeManifest);
185
+ newPerRouterManifestDataMap.set(id, manifest.routeManifest);
183
186
  const routerPrecomputed: PrecomputedEntry[] = [];
184
187
  flattenLeafEntries(
185
188
  manifest.prefixTree,
186
189
  manifest.routeManifest,
187
190
  routerPrecomputed,
188
191
  );
189
- state.perRouterPrecomputedMap.set(id, routerPrecomputed);
192
+ newPerRouterPrecomputedMap.set(id, routerPrecomputed);
190
193
 
191
194
  console.log(
192
195
  `[rsc-router] Router "${id}" -> ${routeCount} routes ` +
@@ -214,10 +217,8 @@ export async function discoverRouters(
214
217
  }
215
218
 
216
219
  // Build route trie from merged manifest + ancestry
217
- if (
218
- state.mergedRouteManifest &&
219
- Object.keys(state.mergedRouteManifest).length > 0
220
- ) {
220
+ let newMergedRouteTrie: any = null;
221
+ if (Object.keys(newMergedRouteManifest).length > 0) {
221
222
  const buildRouteTrie = buildMod.buildRouteTrie;
222
223
  if (buildRouteTrie && mergedRouteAncestry) {
223
224
  // Build routeToStaticPrefix from saved manifests
@@ -252,8 +253,8 @@ export async function discoverRouters(
252
253
  }
253
254
  }
254
255
 
255
- state.mergedRouteTrie = buildRouteTrie(
256
- state.mergedRouteManifest,
256
+ newMergedRouteTrie = buildRouteTrie(
257
+ newMergedRouteManifest,
257
258
  mergedRouteAncestry,
258
259
  routeToStaticPrefix,
259
260
  Object.keys(mergedRouteTrailingSlash).length > 0
@@ -305,11 +306,22 @@ export async function discoverRouters(
305
306
  ? manifest.responseTypeRoutes
306
307
  : undefined,
307
308
  );
308
- state.perRouterTrieMap.set(id, perRouterTrie);
309
+ newPerRouterTrieMap.set(id, perRouterTrie);
309
310
  }
310
311
  }
311
312
  }
312
313
 
314
+ // Commit all local state to the shared discovery state atomically.
315
+ // This ensures a failed re-discovery (e.g. from a transient module
316
+ // evaluation error) preserves the last known-good state.
317
+ state.mergedRouteManifest = newMergedRouteManifest;
318
+ state.mergedPrecomputedEntries = newMergedPrecomputedEntries;
319
+ state.perRouterManifests = newPerRouterManifests;
320
+ state.perRouterManifestDataMap = newPerRouterManifestDataMap;
321
+ state.perRouterPrecomputedMap = newPerRouterPrecomputedMap;
322
+ state.perRouterTrieMap = newPerRouterTrieMap;
323
+ state.mergedRouteTrie = newMergedRouteTrie;
324
+
313
325
  // Expand prerender routes and render static handlers (build mode only)
314
326
  await expandPrerenderRoutes(state, rscEnv, registry, allManifests);
315
327
  await renderStaticHandlers(state, rscEnv, registry);
@@ -9,7 +9,7 @@
9
9
  import { contextSet } from "../../context-var.js";
10
10
  import {
11
11
  encodePathParam,
12
- escapeRegExp,
12
+ substituteRouteParams,
13
13
  runWithConcurrency,
14
14
  groupByConcurrency,
15
15
  notifyOnError,
@@ -33,6 +33,7 @@ export async function expandPrerenderRoutes(
33
33
  routeName: string;
34
34
  concurrency: number;
35
35
  buildVars?: Record<string, any>;
36
+ isPassthroughRoute?: boolean;
36
37
  };
37
38
  const entries: PrerenderEntry[] = [];
38
39
 
@@ -44,19 +45,8 @@ export async function expandPrerenderRoutes(
44
45
  const getParamsReverse = (name: string, params?: Record<string, string>) => {
45
46
  const pattern = allRoutes[name];
46
47
  if (!pattern) throw new Error(`Unknown route: "${name}"`);
47
- let result = pattern;
48
- if (params) {
49
- for (const [key, value] of Object.entries(params)) {
50
- // Strip constraint syntax: :param(a|b) -> value
51
- const escaped = escapeRegExp(key);
52
- result = result.replace(
53
- new RegExp(`:${escaped}(\\([^)]*\\))?`),
54
- encodeURIComponent(value),
55
- );
56
- result = result.replace(`*${key}`, encodeURIComponent(value));
57
- }
58
- }
59
- return result;
48
+ if (!params) return pattern;
49
+ return substituteRouteParams(pattern, params);
60
50
  };
61
51
 
62
52
  for (const { manifest } of allManifests) {
@@ -65,6 +55,8 @@ export async function expandPrerenderRoutes(
65
55
  for (const routeName of manifest.prerenderRoutes) {
66
56
  const pattern = manifest.routeManifest[routeName];
67
57
  if (!pattern) continue;
58
+ const def = defs[routeName];
59
+ const isPassthroughRoute = !!def?.options?.passthrough;
68
60
  const hasDynamic = pattern.includes(":") || pattern.includes("*");
69
61
  if (!hasDynamic) {
70
62
  // Static route: use pattern directly (strip trailing slash for URL)
@@ -72,10 +64,10 @@ export async function expandPrerenderRoutes(
72
64
  urlPath: pattern.replace(/\/$/, "") || "/",
73
65
  routeName,
74
66
  concurrency: 1,
67
+ isPassthroughRoute,
75
68
  });
76
69
  } else {
77
70
  // Dynamic route: call getParams() to enumerate param combinations
78
- const def = defs[routeName];
79
71
  if (def?.getParams) {
80
72
  try {
81
73
  const buildVars: Record<string, any> = {};
@@ -92,19 +84,11 @@ export async function expandPrerenderRoutes(
92
84
  Object.keys(buildVars).length > 0 ||
93
85
  Object.getOwnPropertySymbols(buildVars).length > 0;
94
86
  for (const params of paramsList) {
95
- let url = pattern;
96
- for (const [key, value] of Object.entries(
87
+ let url = substituteRouteParams(
88
+ pattern,
97
89
  params as Record<string, string>,
98
- )) {
99
- const encoded = encodePathParam(value);
100
- // Strip constraint syntax: :param(a|b) -> value
101
- const escaped = escapeRegExp(key);
102
- url = url.replace(
103
- new RegExp(`:${escaped}(\\([^)]*\\))?`),
104
- encoded,
105
- );
106
- url = url.replace(`*${key}`, encoded);
107
- }
90
+ encodePathParam,
91
+ );
108
92
  // Anonymous wildcard fallback: use conventional keys if provided
109
93
  if (url.includes("*")) {
110
94
  const wildcardValue =
@@ -119,6 +103,7 @@ export async function expandPrerenderRoutes(
119
103
  routeName,
120
104
  concurrency,
121
105
  ...(hasBuildVars ? { buildVars } : {}),
106
+ isPassthroughRoute,
122
107
  });
123
108
  }
124
109
  } catch (err: any) {
@@ -187,8 +172,20 @@ export async function expandPrerenderRoutes(
187
172
  entry.urlPath,
188
173
  {},
189
174
  entry.buildVars,
175
+ entry.isPassthroughRoute,
190
176
  );
191
177
  if (!result) continue;
178
+
179
+ // Handler returned ctx.passthrough() — skip manifest entry
180
+ if (result.passthrough) {
181
+ const elapsed = (performance.now() - startUrl).toFixed(0);
182
+ console.log(
183
+ `[rsc-router] PASS ${entry.urlPath.padEnd(40)} (${elapsed}ms) - live fallback`,
184
+ );
185
+ doneCount++;
186
+ break;
187
+ }
188
+
192
189
  const paramHash = hashParams(result.params || {});
193
190
  collectedData[`${result.routeName}/${paramHash}`] = {
194
191
  segments: result.segments,
@@ -15,20 +15,20 @@ import {
15
15
  } from "../../build/generate-route-types.js";
16
16
  import type { DiscoveryState } from "./state.js";
17
17
  import { markSelfGenWrite } from "./self-gen-tracking.js";
18
+ import { isAutoGeneratedRouteName } from "../../route-name.js";
18
19
 
19
20
  /**
20
21
  * Filter out auto-generated route names from a manifest.
21
- * Routes with names starting with "$" are internal (auto-generated from
22
- * patterns for unnamed routes) and should not appear in the typed gen file.
23
- * This keeps the runtime writer's output consistent with the static parser,
24
- * which never produces these names.
22
+ * Unnamed routes get "$path_"-prefixed names at runtime (see path-helper.ts).
23
+ * These should not appear in the typed gen file. User-defined names
24
+ * containing "$" (e.g. "$admin") are valid and preserved.
25
25
  */
26
26
  function filterUserNamedRoutes(
27
27
  manifest: Record<string, string>,
28
28
  ): Record<string, string> {
29
29
  const filtered: Record<string, string> = {};
30
30
  for (const [name, pattern] of Object.entries(manifest)) {
31
- if (!name.startsWith("$")) {
31
+ if (!isAutoGeneratedRouteName(name)) {
32
32
  filtered[name] = pattern;
33
33
  }
34
34
  }
@@ -222,8 +222,8 @@ export function supplementGenFilesWithRuntimeRoutes(
222
222
  };
223
223
 
224
224
  for (const [name, pattern] of Object.entries(routeManifest)) {
225
- // Skip auto-generated names
226
- if (name.startsWith("$")) continue;
225
+ // Skip internal runtime-only names from unnamed routes/includes.
226
+ if (isAutoGeneratedRouteName(name)) continue;
227
227
  const dotIdx = name.indexOf(".");
228
228
  if (dotIdx <= 0) continue;
229
229
  const prefix = name.substring(0, dotIdx + 1);
@@ -42,7 +42,7 @@ export function generateRoutesManifestModule(state: DiscoveryState): string {
42
42
  const genPath = join(
43
43
  routerDir,
44
44
  `${routerBasename}.named-routes.gen.js`,
45
- );
45
+ ).replaceAll("\\", "/");
46
46
  const varName = `_r${varIdx++}`;
47
47
  genFileImports.push(
48
48
  `import { NamedRoutes as ${varName} } from ${JSON.stringify(genPath)};`,
@@ -176,7 +176,10 @@ export function generatePerRouterModule(
176
176
  /\.(tsx?|jsx?)$/,
177
177
  "",
178
178
  );
179
- const genPath = join(routerDir, `${routerBasename}.named-routes.gen.js`);
179
+ const genPath = join(
180
+ routerDir,
181
+ `${routerBasename}.named-routes.gen.js`,
182
+ ).replaceAll("\\", "/");
180
183
  lines.push(`import { NamedRoutes as _r } from ${JSON.stringify(genPath)};`);
181
184
  lines.push(
182
185
  `function __flat(r) { const o = {}; for (const [k, v] of Object.entries(r)) o[k] = typeof v === "string" ? v : v.path; return o; }`,
@@ -1,5 +1,5 @@
1
1
  import type { Plugin } from "vite";
2
- import { relative, posix } from "node:path";
2
+ import { relative } from "node:path";
3
3
  import { createHash } from "node:crypto";
4
4
 
5
5
  // Dev-mode client-reference key prefixes emitted by @vitejs/plugin-rsc
@@ -33,11 +33,11 @@ export function computeProductionHash(
33
33
  const absPath = decodeURIComponent(
34
34
  refKey.slice(CLIENT_IN_SERVER_PKG_PROXY_PREFIX.length),
35
35
  );
36
- toHash = posix.normalize(relative(projectRoot, absPath));
36
+ toHash = relative(projectRoot, absPath).replaceAll("\\", "/");
37
37
  } else if (refKey.startsWith(FS_PREFIX)) {
38
38
  // /@fs/abs/path.tsx -> hash(relative(root, "/abs/path.tsx"))
39
39
  const absPath = refKey.slice(FS_PREFIX.length - 1); // keep leading /
40
- toHash = posix.normalize(relative(projectRoot, absPath));
40
+ toHash = relative(projectRoot, absPath).replaceAll("\\", "/");
41
41
  } else if (refKey.startsWith("/")) {
42
42
  // /src/Button.tsx -> hash("src/Button.tsx")
43
43
  toHash = refKey.slice(1);
@@ -97,7 +97,7 @@ export function useCacheTransform(): Plugin {
97
97
 
98
98
  // Check for function-level "use cache" / "use cache: profileName"
99
99
  // (only if there's no file-level directive but code still contains the string)
100
- return transformFunctionLevelUseCache(
100
+ const functionResult = transformFunctionLevelUseCache(
101
101
  code,
102
102
  ast,
103
103
  filePath,
@@ -105,6 +105,13 @@ export function useCacheTransform(): Plugin {
105
105
  isBuild,
106
106
  transformHoistInlineDirective,
107
107
  );
108
+
109
+ // Always check for near-miss directives, even when valid directives
110
+ // exist. A file may contain both valid and invalid "use cache" directives
111
+ // in different functions — the invalid ones should still warn.
112
+ warnOnNearMissDirectives(ast, id, this.warn.bind(this));
113
+
114
+ if (functionResult) return functionResult;
108
115
  },
109
116
  };
110
117
  }
@@ -118,19 +125,38 @@ function transformFileLevelUseCache(
118
125
  isLayoutOrTemplate: boolean,
119
126
  transformWrapExport: (typeof import("@vitejs/plugin-rsc/transforms"))["transformWrapExport"],
120
127
  ) {
128
+ // Collect non-function exports to report after wrapping
129
+ const nonFunctionExports: string[] = [];
130
+
121
131
  const { exportNames, output } = transformWrapExport(code, ast, {
122
132
  runtime: (value: string, name: string) => {
123
133
  const funcId = isBuild ? hashId(filePath, name) : `${filePath}#${name}`;
124
134
  return `__rango_registerCachedFunction(${value}, ${JSON.stringify(funcId)}, "default")`;
125
135
  },
126
136
  rejectNonAsyncFunction: false,
127
- filter: (name: string) => {
137
+ filter: (name: string, meta: { isFunction?: boolean }) => {
128
138
  // Skip default export of layout/template files (they receive children)
129
139
  if (name === "default" && isLayoutOrTemplate) return false;
140
+ // Non-function exports cannot be wrapped with registerCachedFunction
141
+ if (meta.isFunction === false) {
142
+ nonFunctionExports.push(name);
143
+ return false;
144
+ }
130
145
  return true;
131
146
  },
132
147
  });
133
148
 
149
+ if (nonFunctionExports.length > 0) {
150
+ throw new Error(
151
+ `[rango:use-cache] File-level "use cache" in ${sourceId} cannot wrap ` +
152
+ `non-function export${nonFunctionExports.length > 1 ? "s" : ""}: ` +
153
+ `${nonFunctionExports.map((n) => `"${n}"`).join(", ")}. ` +
154
+ `Only function exports can be cached. Either remove "use cache" from ` +
155
+ `the file level and add it inside individual functions, or move the ` +
156
+ `non-function exports to a separate module.`,
157
+ );
158
+ }
159
+
134
160
  if (exportNames.length === 0) {
135
161
  // Even if no exports were wrapped, strip the directive
136
162
  const s = new MagicString(code);
@@ -180,7 +206,7 @@ function transformFunctionLevelUseCache(
180
206
  ) {
181
207
  try {
182
208
  const { output, names } = transformHoistInlineDirective(code, ast, {
183
- directive: /^use cache(:\s*\w+)?$/,
209
+ directive: /^use cache(:\s*[\w-]+)?$/,
184
210
  runtime: (
185
211
  value: string,
186
212
  name: string,
@@ -233,3 +259,65 @@ function findFileLevelDirective(
233
259
  }
234
260
  return null;
235
261
  }
262
+
263
+ /**
264
+ * The valid directive regex (must stay in sync with transformFunctionLevelUseCache).
265
+ */
266
+ const VALID_DIRECTIVE_RE = /^use cache(:\s*[\w-]+)?$/;
267
+
268
+ /**
269
+ * Regex for near-miss: starts with "use cache:" but has invalid tokens.
270
+ */
271
+ const NEAR_MISS_RE = /^use cache:\s*.+$/;
272
+
273
+ /**
274
+ * Walk the AST looking for string literals that look like malformed
275
+ * "use cache" directives and emit a Vite warning for each.
276
+ *
277
+ * This catches cases like `"use cache: bad.name"` or `"use cache: "`
278
+ * that the transform regex silently ignores.
279
+ */
280
+ function warnOnNearMissDirectives(
281
+ ast: any,
282
+ fileId: string,
283
+ warn: (message: string) => void,
284
+ ): void {
285
+ const visit = (node: any) => {
286
+ if (!node || typeof node !== "object") return;
287
+
288
+ if (
289
+ node.type === "ExpressionStatement" &&
290
+ node.expression?.type === "Literal" &&
291
+ typeof node.expression.value === "string"
292
+ ) {
293
+ const value = node.expression.value;
294
+ if (
295
+ value.startsWith("use cache") &&
296
+ NEAR_MISS_RE.test(value) &&
297
+ !VALID_DIRECTIVE_RE.test(value)
298
+ ) {
299
+ const profilePart = value.slice("use cache:".length).trim();
300
+ warn(
301
+ `[rango:use-cache] "${value}" in ${fileId} has an invalid profile name "${profilePart}". ` +
302
+ `Profile names must match [a-zA-Z0-9_-]+. This directive will be ignored.`,
303
+ );
304
+ }
305
+ }
306
+
307
+ // Walk into function bodies where directives appear
308
+ for (const key of Object.keys(node)) {
309
+ const child = node[key];
310
+ if (Array.isArray(child)) {
311
+ for (const item of child) {
312
+ visit(item);
313
+ }
314
+ } else if (child && typeof child === "object" && child.type) {
315
+ visit(child);
316
+ }
317
+ }
318
+ };
319
+
320
+ for (const node of ast.body ?? []) {
321
+ visit(node);
322
+ }
323
+ }
package/src/vite/rango.ts CHANGED
@@ -222,9 +222,9 @@ export async function rango(options?: RangoOptions): Promise<PluginOption[]> {
222
222
  const candidates = findRouterFiles(root, filter);
223
223
  if (candidates.length === 1) {
224
224
  const abs = candidates[0];
225
- routerRef.path = abs.startsWith(root)
226
- ? "./" + abs.slice(root.length + 1)
227
- : abs;
225
+ routerRef.path = (
226
+ abs.startsWith(root) ? "./" + abs.slice(root.length + 1) : abs
227
+ ).replaceAll("\\", "/");
228
228
  } else if (candidates.length > 1) {
229
229
  const list = candidates
230
230
  .map(