@rangojs/router 0.0.0-experimental.110 → 0.0.0-experimental.112

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 (65) hide show
  1. package/dist/bin/rango.js +41 -37
  2. package/dist/vite/index.js +144 -191
  3. package/package.json +17 -14
  4. package/skills/handler-use/SKILL.md +1 -1
  5. package/skills/rango/SKILL.md +20 -0
  6. package/src/browser/action-coordinator.ts +53 -36
  7. package/src/browser/event-controller.ts +42 -66
  8. package/src/browser/navigation-bridge.ts +4 -0
  9. package/src/browser/navigation-client.ts +12 -15
  10. package/src/browser/navigation-store.ts +7 -8
  11. package/src/browser/navigation-transaction.ts +7 -21
  12. package/src/browser/partial-update.ts +8 -16
  13. package/src/browser/react/NavigationProvider.tsx +29 -40
  14. package/src/browser/react/use-params.ts +3 -4
  15. package/src/browser/response-adapter.ts +25 -0
  16. package/src/browser/rsc-router.tsx +16 -2
  17. package/src/browser/server-action-bridge.ts +23 -30
  18. package/src/browser/types.ts +2 -0
  19. package/src/build/generate-manifest.ts +29 -31
  20. package/src/build/generate-route-types.ts +2 -0
  21. package/src/build/route-types/router-processing.ts +37 -9
  22. package/src/build/runtime-discovery.ts +9 -20
  23. package/src/decode-loader-results.ts +36 -0
  24. package/src/errors.ts +29 -0
  25. package/src/index.rsc.ts +1 -0
  26. package/src/index.ts +1 -0
  27. package/src/response-utils.ts +9 -0
  28. package/src/route-content-wrapper.tsx +6 -28
  29. package/src/route-definition/dsl-helpers.ts +231 -259
  30. package/src/route-definition/helper-factories.ts +29 -139
  31. package/src/route-definition/use-item-types.ts +32 -0
  32. package/src/route-types.ts +19 -41
  33. package/src/router/content-negotiation.ts +15 -2
  34. package/src/router/intercept-resolution.ts +4 -18
  35. package/src/router/match-result.ts +32 -30
  36. package/src/router/middleware.ts +46 -78
  37. package/src/router/preview-match.ts +3 -1
  38. package/src/router/request-classification.ts +4 -28
  39. package/src/rsc/handler.ts +20 -65
  40. package/src/rsc/helpers.ts +3 -2
  41. package/src/rsc/origin-guard.ts +28 -10
  42. package/src/rsc/response-route-handler.ts +32 -52
  43. package/src/rsc/rsc-rendering.ts +27 -53
  44. package/src/rsc/runtime-warnings.ts +9 -10
  45. package/src/rsc/server-action.ts +13 -37
  46. package/src/rsc/ssr-setup.ts +16 -0
  47. package/src/segment-system.tsx +5 -39
  48. package/src/server/context.ts +76 -35
  49. package/src/urls/include-helper.ts +10 -53
  50. package/src/urls/index.ts +0 -3
  51. package/src/urls/path-helper.ts +17 -52
  52. package/src/urls/pattern-types.ts +2 -19
  53. package/src/urls/response-types.ts +20 -19
  54. package/src/urls/type-extraction.ts +20 -115
  55. package/src/urls/urls-function.ts +1 -5
  56. package/src/vite/discovery/discover-routers.ts +10 -22
  57. package/src/vite/discovery/route-types-writer.ts +38 -82
  58. package/src/vite/plugins/cjs-to-esm.ts +3 -7
  59. package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
  60. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  61. package/src/vite/plugins/expose-internal-ids.ts +34 -62
  62. package/src/vite/plugins/version-injector.ts +2 -12
  63. package/src/vite/router-discovery.ts +71 -26
  64. package/src/vite/utils/shared-utils.ts +13 -1
  65. package/src/browser/action-response-classifier.ts +0 -99
@@ -32,21 +32,32 @@ type ResponseReverseFunction = [DefaultReverseRouteMap] extends [
32
32
  * Symbol marking a route as a response route (non-RSC).
33
33
  * Stored on PathOptions and UrlPatterns to signal the trie to short-circuit.
34
34
  */
35
- export const RESPONSE_TYPE: unique symbol = Symbol.for(
36
- "rangojs.responseType",
37
- ) as any;
35
+ export const RESPONSE_TYPE: unique symbol = Symbol.for("rangojs.responseType");
38
36
 
39
37
  /**
40
- * Handler that must return Response (not ReactNode).
41
- * Used by path.image(), path.stream(), path.any() (binary/streaming data).
38
+ * Shared shape of a response-route handler: a function returning TReturn (or a
39
+ * promise of it), plus an optional composable `use` thunk merged at mount time.
42
40
  */
43
- export type ResponseHandler<TParams = Record<string, string>, TEnv = any> = ((
41
+ type ResponseHandlerOf<
42
+ TReturn,
43
+ TParams = Record<string, string>,
44
+ TEnv = any,
45
+ > = ((
44
46
  ctx: ResponseHandlerContext<TParams, TEnv>,
45
- ) => Response | Promise<Response>) & {
47
+ ) => TReturn | Promise<TReturn>) & {
46
48
  /** Composable default DSL items merged when the handler is mounted. */
47
49
  use?: () => UseItems<ResponseRouteUseItem>;
48
50
  };
49
51
 
52
+ /**
53
+ * Handler that must return Response (not ReactNode).
54
+ * Used by path.image(), path.stream(), path.any() (binary/streaming data).
55
+ */
56
+ export type ResponseHandler<
57
+ TParams = Record<string, string>,
58
+ TEnv = any,
59
+ > = ResponseHandlerOf<Response, TParams, TEnv>;
60
+
50
61
  /**
51
62
  * JSON-serializable value type for auto-wrap support.
52
63
  */
@@ -65,12 +76,7 @@ export type JsonValue =
65
76
  export type JsonResponseHandler<
66
77
  TParams = Record<string, string>,
67
78
  TEnv = any,
68
- > = ((
69
- ctx: ResponseHandlerContext<TParams, TEnv>,
70
- ) => JsonValue | Response | Promise<JsonValue | Response>) & {
71
- /** Composable default DSL items merged when the handler is mounted. */
72
- use?: () => UseItems<ResponseRouteUseItem>;
73
- };
79
+ > = ResponseHandlerOf<JsonValue | Response, TParams, TEnv>;
74
80
 
75
81
  /**
76
82
  * Handler for text-based response routes (text, html, xml).
@@ -79,12 +85,7 @@ export type JsonResponseHandler<
79
85
  export type TextResponseHandler<
80
86
  TParams = Record<string, string>,
81
87
  TEnv = any,
82
- > = ((
83
- ctx: ResponseHandlerContext<TParams, TEnv>,
84
- ) => string | Response | Promise<string | Response>) & {
85
- /** Composable default DSL items merged when the handler is mounted. */
86
- use?: () => UseItems<ResponseRouteUseItem>;
87
- };
88
+ > = ResponseHandlerOf<string | Response, TParams, TEnv>;
88
89
 
89
90
  /**
90
91
  * Lighter handler context for response routes.
@@ -1,4 +1,3 @@
1
- import type { ExtractParams } from "../types.js";
2
1
  import type { JsonSerialize } from "../serialize.js";
3
2
  import type {
4
3
  TypedRouteItem,
@@ -7,11 +6,7 @@ import type {
7
6
  TypedCacheItem,
8
7
  TypedTransitionItem,
9
8
  } from "../route-types.js";
10
- import type {
11
- LocalOnlyInclude,
12
- UnnamedRoute,
13
- UrlPatterns,
14
- } from "./pattern-types.js";
9
+ import type { LocalOnlyInclude, UnnamedRoute } from "./pattern-types.js";
15
10
 
16
11
  // ============================================================================
17
12
  // Route Type Extraction Utilities
@@ -68,62 +63,6 @@ type PrefixPatterns<
68
63
  : TRoutes[K];
69
64
  };
70
65
 
71
- /**
72
- * Depth counter for limiting recursion (max 40 levels)
73
- * Supports up to 40 sibling items at any level of a urls() call
74
- * Note: Higher values hit TypeScript's internal recursion limits
75
- */
76
- type Depth = [
77
- never,
78
- 0,
79
- 1,
80
- 2,
81
- 3,
82
- 4,
83
- 5,
84
- 6,
85
- 7,
86
- 8,
87
- 9,
88
- 10,
89
- 11,
90
- 12,
91
- 13,
92
- 14,
93
- 15,
94
- 16,
95
- 17,
96
- 18,
97
- 19,
98
- 20,
99
- 21,
100
- 22,
101
- 23,
102
- 24,
103
- 25,
104
- 26,
105
- 27,
106
- 28,
107
- 29,
108
- 30,
109
- 31,
110
- 32,
111
- 33,
112
- 34,
113
- 35,
114
- 36,
115
- 37,
116
- 38,
117
- 39,
118
- ];
119
-
120
- /**
121
- * Force TypeScript to eagerly evaluate a type.
122
- * This helps with interface extension by creating a "concrete" object type.
123
- */
124
- type Simplify<T> =
125
- T extends Record<string, string> ? { [K in keyof T]: T[K] } : T;
126
-
127
66
  /**
128
67
  * Convert a union type to an intersection type.
129
68
  * Used to combine route maps from multiple siblings without recursive tuple processing.
@@ -136,13 +75,11 @@ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
136
75
 
137
76
  /**
138
77
  * Extract routes from a single item (path, include, layout, cache with children)
139
- * D is the current depth level for nested layouts/caches
140
78
  */
141
- type ExtractRoutesFromItem<T, D extends number = 40> = [D] extends [never]
142
- ? {} // Max depth reached, stop recursion
143
- : // TypedRouteItem: extract name -> pattern (exclude unnamed routes)
144
- // When search schema is non-empty, value becomes { path, search } object
145
- T extends TypedRouteItem<infer TName, infer TPattern, any, infer TSearch>
79
+ type ExtractRoutesFromItem<T> =
80
+ // TypedRouteItem: extract name -> pattern (exclude unnamed routes)
81
+ // When search schema is non-empty, value becomes { path, search } object
82
+ T extends TypedRouteItem<infer TName, infer TPattern, any, infer TSearch>
146
83
  ? TName extends string
147
84
  ? TName extends UnnamedRoute
148
85
  ? {} // Exclude unnamed routes from type map
@@ -186,14 +123,10 @@ type ExtractRoutesFromItem<T, D extends number = 40> = [D] extends [never]
186
123
  * Extract routes from an array of items using mapped types.
187
124
  * Uses UnionToIntersection to combine routes without recursive tuple processing,
188
125
  * removing the sibling limit that was caused by TypeScript recursion limits.
189
- * D is passed to ExtractRoutesFromItem for nested depth tracking.
190
126
  */
191
- type ExtractRoutesFromItems<
192
- T extends readonly any[],
193
- D extends number = 40,
194
- > = T extends readonly any[]
127
+ type ExtractRoutesFromItems<T extends readonly any[]> = T extends readonly any[]
195
128
  ? UnionToIntersection<
196
- { [K in keyof T]: ExtractRoutesFromItem<T[K], D> }[number]
129
+ { [K in keyof T]: ExtractRoutesFromItem<T[K]> }[number]
197
130
  > extends infer R
198
131
  ? R extends Record<string, any>
199
132
  ? R
@@ -204,12 +137,8 @@ type ExtractRoutesFromItems<
204
137
  /**
205
138
  * Main utility: extract route map from urls() callback return type
206
139
  * Uses mapped types for sibling processing (no sibling limit).
207
- * Uses Simplify to force eager evaluation for interface extension compatibility.
208
140
  */
209
- export type ExtractRoutes<T extends readonly any[]> = ExtractRoutesFromItems<
210
- T,
211
- 40
212
- >;
141
+ export type ExtractRoutes<T extends readonly any[]> = ExtractRoutesFromItems<T>;
213
142
 
214
143
  // ============================================================================
215
144
  // Response Type Extraction Utilities
@@ -237,9 +166,8 @@ type PrefixKeys<
237
166
  * Extract response data types from a single item.
238
167
  * Parallel to ExtractRoutesFromItem but extracts name -> TData mapping.
239
168
  */
240
- type ExtractResponsesFromItem<T, D extends number = 40> = [D] extends [never]
241
- ? {}
242
- : T extends TypedRouteItem<infer TName, any, infer TData>
169
+ type ExtractResponsesFromItem<T> =
170
+ T extends TypedRouteItem<infer TName, any, infer TData>
243
171
  ? TName extends string
244
172
  ? TName extends UnnamedRoute
245
173
  ? {}
@@ -273,46 +201,23 @@ type ExtractResponsesFromItem<T, D extends number = 40> = [D] extends [never]
273
201
  * Extract responses from an array of items using mapped types.
274
202
  * Parallel to ExtractRoutesFromItems.
275
203
  */
276
- type ExtractResponsesFromItems<
277
- T extends readonly any[],
278
- D extends number = 40,
279
- > = T extends readonly any[]
280
- ? UnionToIntersection<
281
- { [K in keyof T]: ExtractResponsesFromItem<T[K], D> }[number]
282
- > extends infer R
283
- ? R extends Record<string, unknown>
284
- ? R
204
+ type ExtractResponsesFromItems<T extends readonly any[]> =
205
+ T extends readonly any[]
206
+ ? UnionToIntersection<
207
+ { [K in keyof T]: ExtractResponsesFromItem<T[K]> }[number]
208
+ > extends infer R
209
+ ? R extends Record<string, unknown>
210
+ ? R
211
+ : {}
285
212
  : {}
286
- : {}
287
- : {};
213
+ : {};
288
214
 
289
215
  /**
290
216
  * Main utility: extract response data type map from urls() callback return type.
291
217
  * Parallel to ExtractRoutes.
292
218
  */
293
219
  export type ExtractResponses<T extends readonly any[]> =
294
- ExtractResponsesFromItems<T, 40>;
295
-
296
- // ============================================================================
297
- // Type Utilities for path()
298
- // ============================================================================
299
-
300
- /**
301
- * Extract route names from a UrlPatterns result
302
- * Used for type-safe href() generation
303
- */
304
- export type ExtractRouteNames<T extends UrlPatterns<any>> =
305
- T extends UrlPatterns<infer _TEnv>
306
- ? string // For now, will be refined with full implementation
307
- : never;
308
-
309
- /**
310
- * Extract params for a specific route name
311
- */
312
- export type ExtractPathParams<
313
- T extends UrlPatterns<any>,
314
- K extends string,
315
- > = ExtractParams<string>; // Will be refined with pattern tracking
220
+ ExtractResponsesFromItems<T>;
316
221
 
317
222
  // ============================================================================
318
223
  // Response Envelope Types
@@ -3,7 +3,7 @@ import type { AllUseItems } from "../route-types.js";
3
3
  import { getContext } from "../server/context";
4
4
  import { invariant } from "../errors";
5
5
  import { createRouteHelpers } from "../route-definition.js";
6
- import type { PathDefinition, UrlPatterns } from "./pattern-types.js";
6
+ import type { UrlPatterns } from "./pattern-types.js";
7
7
  import type { PathHelpers } from "./path-helper-types.js";
8
8
  import type { ExtractRoutes, ExtractResponses } from "./type-extraction.js";
9
9
  import { createPathHelper, attachPathResponseTags } from "./path-helper.js";
@@ -34,9 +34,6 @@ export function urls<
34
34
  >(
35
35
  builder: (helpers: PathHelpers<TEnv>) => TItems,
36
36
  ): UrlPatterns<TEnv, ExtractRoutes<TItems>, ExtractResponses<TItems>> {
37
- // Collect path definitions during build
38
- const definitions: PathDefinition[] = [];
39
-
40
37
  // Create the handler function that will be called by the router
41
38
  const handler = () => {
42
39
  invariant(
@@ -82,7 +79,6 @@ export function urls<
82
79
  // trailingSlash config is populated when handler() runs
83
80
  // We expose it via a getter that reads from the context after handler execution
84
81
  return {
85
- definitions,
86
82
  handler,
87
83
  get trailingSlash() {
88
84
  // Get the trailingSlash map from the current context
@@ -292,18 +292,16 @@ export async function discoverRouters(
292
292
  }
293
293
  }
294
294
 
295
+ // buildRouteTrie reads these via ?.has / ?.[] — empty is observationally
296
+ // identical to undefined, so no empty->undefined coercion is needed.
295
297
  newMergedRouteTrie = buildRouteTrie(
296
298
  newMergedRouteManifest,
297
299
  mergedRouteAncestry,
298
300
  routeToStaticPrefix,
299
- Object.keys(mergedRouteTrailingSlash).length > 0
300
- ? mergedRouteTrailingSlash
301
- : undefined,
302
- prerenderRouteNames.size > 0 ? prerenderRouteNames : undefined,
303
- passthroughRouteNames.size > 0 ? passthroughRouteNames : undefined,
304
- Object.keys(mergedResponseTypeRoutes).length > 0
305
- ? mergedResponseTypeRoutes
306
- : undefined,
301
+ mergedRouteTrailingSlash,
302
+ prerenderRouteNames,
303
+ passthroughRouteNames,
304
+ mergedResponseTypeRoutes,
307
305
  );
308
306
 
309
307
  // Build per-router tries for multi-router isolation.
@@ -330,20 +328,10 @@ export async function discoverRouters(
330
328
  manifest.routeManifest,
331
329
  manifest._routeAncestry,
332
330
  perRouterStaticPrefix,
333
- manifest.routeTrailingSlash &&
334
- Object.keys(manifest.routeTrailingSlash).length > 0
335
- ? manifest.routeTrailingSlash
336
- : undefined,
337
- perRouterPrerenderNames && perRouterPrerenderNames.size > 0
338
- ? perRouterPrerenderNames
339
- : undefined,
340
- perRouterPassthroughNames && perRouterPassthroughNames.size > 0
341
- ? perRouterPassthroughNames
342
- : undefined,
343
- manifest.responseTypeRoutes &&
344
- Object.keys(manifest.responseTypeRoutes).length > 0
345
- ? manifest.responseTypeRoutes
346
- : undefined,
331
+ manifest.routeTrailingSlash,
332
+ perRouterPrerenderNames,
333
+ perRouterPassthroughNames,
334
+ manifest.responseTypeRoutes,
347
335
  );
348
336
  newPerRouterTrieMap.set(id, perRouterTrie);
349
337
  }
@@ -5,13 +5,15 @@
5
5
  * from discovered router manifests and static source parsing.
6
6
  */
7
7
 
8
- import { dirname, basename, join, resolve } from "node:path";
8
+ import { dirname, join, resolve } from "node:path";
9
9
  import { readFileSync, writeFileSync, existsSync, unlinkSync } from "node:fs";
10
10
  import {
11
11
  generateRouteTypesSource,
12
12
  writeCombinedRouteTypes,
13
13
  findRouterFiles,
14
14
  buildCombinedRouteMapForRouterFile,
15
+ genFileTsPath,
16
+ resolveSearchSchemas,
15
17
  } from "../../build/generate-route-types.js";
16
18
  import type { DiscoveryState } from "./state.js";
17
19
  import { markSelfGenWrite } from "./self-gen-tracking.js";
@@ -35,6 +37,22 @@ function filterUserNamedRoutes(
35
37
  return filtered;
36
38
  }
37
39
 
40
+ // Write a gen file only when content changed, marking the write as
41
+ // self-generated BEFORE writeFileSync so the watcher distinguishes it from a
42
+ // manual edit (the HMR self-gen-loop guard).
43
+ function writeGenFileIfChanged(
44
+ state: DiscoveryState,
45
+ outPath: string,
46
+ source: string,
47
+ opts?: { log?: boolean },
48
+ ): void {
49
+ const existing = existsSync(outPath) ? readFileSync(outPath, "utf-8") : null;
50
+ if (existing === source) return;
51
+ markSelfGenWrite(state, outPath, source);
52
+ writeFileSync(outPath, source);
53
+ if (opts?.log) console.log(`[rango] Generated route types -> ${outPath}`);
54
+ }
55
+
38
56
  /**
39
57
  * Write combined route types for all router files.
40
58
  * Only writes when content has changed to avoid triggering HMR loops.
@@ -48,45 +66,16 @@ export function writeCombinedRouteTypesWithTracking(
48
66
  findRouterFiles(state.projectRoot, state.scanFilter);
49
67
  state.cachedRouterFiles = routerFiles;
50
68
 
51
- // Snapshot pre-write content to detect which files actually change.
52
- const preContent = new Map<string, string>();
53
- for (const routerFilePath of routerFiles) {
54
- const routerDir = dirname(routerFilePath);
55
- const routerBasename = basename(routerFilePath).replace(
56
- /\.(tsx?|jsx?)$/,
57
- "",
58
- );
59
- const outPath = join(routerDir, `${routerBasename}.named-routes.gen.ts`);
60
- try {
61
- preContent.set(outPath, readFileSync(outPath, "utf-8"));
62
- } catch {
63
- // File doesn't exist yet — any write is a real change.
64
- }
65
- }
66
-
67
- writeCombinedRouteTypes(state.projectRoot, routerFiles, opts);
68
-
69
- // Mark only files that were actually written so the watcher can
70
- // distinguish self-triggered change events from manual edits.
71
- // Marking unchanged files creates stale entries that interfere with
72
- // multi-server setups (e.g. shared webServer + isolated HMR server).
73
- for (const routerFilePath of routerFiles) {
74
- const routerDir = dirname(routerFilePath);
75
- const routerBasename = basename(routerFilePath).replace(
76
- /\.(tsx?|jsx?)$/,
77
- "",
78
- );
79
- const outPath = join(routerDir, `${routerBasename}.named-routes.gen.ts`);
80
- if (!existsSync(outPath)) continue;
81
- try {
82
- const content = readFileSync(outPath, "utf-8");
83
- if (content !== preContent.get(outPath)) {
84
- markSelfGenWrite(state, outPath, content);
85
- }
86
- } catch {
87
- // Ignore transient fs errors while files are being rewritten.
88
- }
89
- }
69
+ // Mark each gen file as self-generated BEFORE it is written, via the onWrite
70
+ // callback fired at every writeFileSync site, so the watcher distinguishes
71
+ // self-triggered change events from manual edits. The callback fires only
72
+ // for files actually written, so unchanged files are never marked (stale
73
+ // entries interfere with multi-server setups such as a shared webServer plus
74
+ // an isolated HMR server).
75
+ writeCombinedRouteTypes(state.projectRoot, routerFiles, {
76
+ ...opts,
77
+ onWrite: (outPath, content) => markSelfGenWrite(state, outPath, content),
78
+ });
90
79
  }
91
80
 
92
81
  /**
@@ -128,34 +117,16 @@ export function writeRouteTypesFiles(state: DiscoveryState): void {
128
117
  );
129
118
  }
130
119
 
131
- const routerDir = dirname(sourceFile);
132
- const routerBasename = basename(sourceFile).replace(/\.(tsx?|jsx?)$/, "");
133
- const outPath = join(routerDir, `${routerBasename}.named-routes.gen.ts`);
120
+ const outPath = genFileTsPath(sourceFile);
134
121
 
135
122
  // Filter out auto-generated route names (e.g. "$path____debug_reverse-test")
136
123
  // to match the static parser's output and prevent HMR oscillation.
137
124
  const userRoutes = filterUserNamedRoutes(routeManifest);
138
- let effectiveSearchSchemas = routeSearchSchemas;
139
-
140
- // Runtime manifest may omit search schema metadata in some module-runner
141
- // flows. Fall back to static source parsing from the router file.
142
- if (
143
- (!effectiveSearchSchemas ||
144
- Object.keys(effectiveSearchSchemas).length === 0) &&
145
- sourceFile
146
- ) {
147
- const staticParsed = buildCombinedRouteMapForRouterFile(sourceFile);
148
- if (Object.keys(staticParsed.searchSchemas).length > 0) {
149
- const filtered: Record<string, Record<string, string>> = {};
150
- for (const name of Object.keys(userRoutes)) {
151
- const schema = staticParsed.searchSchemas[name];
152
- if (schema) filtered[name] = schema;
153
- }
154
- if (Object.keys(filtered).length > 0) {
155
- effectiveSearchSchemas = filtered;
156
- }
157
- }
158
- }
125
+ const effectiveSearchSchemas = resolveSearchSchemas(
126
+ Object.keys(userRoutes),
127
+ routeSearchSchemas,
128
+ sourceFile,
129
+ );
159
130
 
160
131
  const source = generateRouteTypesSource(
161
132
  userRoutes,
@@ -163,14 +134,7 @@ export function writeRouteTypesFiles(state: DiscoveryState): void {
163
134
  ? effectiveSearchSchemas
164
135
  : undefined,
165
136
  );
166
- const existing = existsSync(outPath)
167
- ? readFileSync(outPath, "utf-8")
168
- : null;
169
- if (existing !== source) {
170
- markSelfGenWrite(state, outPath, source);
171
- writeFileSync(outPath, source);
172
- console.log(`[rango] Generated route types -> ${outPath}`);
173
- }
137
+ writeGenFileIfChanged(state, outPath, source, { log: true });
174
138
  }
175
139
  }
176
140
 
@@ -236,22 +200,14 @@ export function supplementGenFilesWithRuntimeRoutes(
236
200
  }
237
201
  }
238
202
 
239
- const routerDir = dirname(sourceFile);
240
- const routerBasename = basename(sourceFile).replace(/\.(tsx?|jsx?)$/, "");
241
- const outPath = join(routerDir, `${routerBasename}.named-routes.gen.ts`);
203
+ const outPath = genFileTsPath(sourceFile);
242
204
  const source = generateRouteTypesSource(
243
205
  mergedRoutes,
244
206
  Object.keys(mergedSearchSchemas).length > 0
245
207
  ? mergedSearchSchemas
246
208
  : undefined,
247
209
  );
248
- const existing = existsSync(outPath)
249
- ? readFileSync(outPath, "utf-8")
250
- : null;
251
- if (existing !== source) {
252
- markSelfGenWrite(state, outPath, source);
253
- writeFileSync(outPath, source);
254
- }
210
+ writeGenFileIfChanged(state, outPath, source);
255
211
  }
256
212
  // No manual manifest update needed: the virtual module imports the gen
257
213
  // file, so Vite's HMR automatically re-evaluates it with fresh data.
@@ -12,13 +12,10 @@ export function createCjsToEsmPlugin(): Plugin {
12
12
  name: "@rangojs/router:cjs-to-esm",
13
13
  enforce: "pre",
14
14
  transform(code, id) {
15
- const cleanId = id.split("?")[0];
15
+ const cleanId = id.split("?")[0].replaceAll("\\", "/");
16
16
 
17
17
  // Transform the client.browser.js entry point to re-export from CJS
18
- if (
19
- cleanId.includes("vendor/react-server-dom/client.browser.js") ||
20
- cleanId.includes("vendor\\react-server-dom\\client.browser.js")
21
- ) {
18
+ if (cleanId.includes("vendor/react-server-dom/client.browser.js")) {
22
19
  const isProd = process.env.NODE_ENV === "production";
23
20
  const cjsFile = isProd
24
21
  ? "./cjs/react-server-dom-webpack-client.browser.production.js"
@@ -33,8 +30,7 @@ export function createCjsToEsmPlugin(): Plugin {
33
30
 
34
31
  // Transform the actual CJS files to ESM
35
32
  if (
36
- (cleanId.includes("vendor/react-server-dom/cjs/") ||
37
- cleanId.includes("vendor\\react-server-dom\\cjs\\")) &&
33
+ cleanId.includes("vendor/react-server-dom/cjs/") &&
38
34
  cleanId.includes("client.browser")
39
35
  ) {
40
36
  let transformed = code;
@@ -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"