@rangojs/router 0.0.0-experimental.29 → 0.0.0-experimental.2a0dea97

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 (156) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +78 -19
  3. package/dist/bin/rango.js +138 -50
  4. package/dist/vite/index.js +853 -435
  5. package/package.json +17 -16
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +32 -0
  8. package/skills/caching/SKILL.md +45 -4
  9. package/skills/handler-use/SKILL.md +362 -0
  10. package/skills/hooks/SKILL.md +22 -4
  11. package/skills/intercept/SKILL.md +20 -0
  12. package/skills/layout/SKILL.md +22 -0
  13. package/skills/links/SKILL.md +3 -1
  14. package/skills/loader/SKILL.md +71 -21
  15. package/skills/middleware/SKILL.md +34 -3
  16. package/skills/migrate-nextjs/SKILL.md +560 -0
  17. package/skills/migrate-react-router/SKILL.md +764 -0
  18. package/skills/parallel/SKILL.md +185 -0
  19. package/skills/prerender/SKILL.md +110 -68
  20. package/skills/rango/SKILL.md +24 -22
  21. package/skills/route/SKILL.md +56 -2
  22. package/skills/router-setup/SKILL.md +87 -2
  23. package/skills/typesafety/SKILL.md +33 -21
  24. package/src/__internal.ts +92 -0
  25. package/src/browser/app-version.ts +14 -0
  26. package/src/browser/event-controller.ts +5 -0
  27. package/src/browser/link-interceptor.ts +4 -0
  28. package/src/browser/navigation-bridge.ts +125 -16
  29. package/src/browser/navigation-client.ts +142 -57
  30. package/src/browser/navigation-store.ts +43 -8
  31. package/src/browser/navigation-transaction.ts +11 -9
  32. package/src/browser/partial-update.ts +94 -17
  33. package/src/browser/prefetch/cache.ts +82 -12
  34. package/src/browser/prefetch/fetch.ts +98 -27
  35. package/src/browser/prefetch/policy.ts +6 -0
  36. package/src/browser/prefetch/queue.ts +92 -20
  37. package/src/browser/prefetch/resource-ready.ts +77 -0
  38. package/src/browser/react/Link.tsx +88 -9
  39. package/src/browser/react/NavigationProvider.tsx +40 -4
  40. package/src/browser/react/context.ts +7 -2
  41. package/src/browser/react/use-handle.ts +9 -58
  42. package/src/browser/react/use-router.ts +21 -8
  43. package/src/browser/rsc-router.tsx +134 -59
  44. package/src/browser/scroll-restoration.ts +41 -42
  45. package/src/browser/segment-reconciler.ts +72 -10
  46. package/src/browser/server-action-bridge.ts +8 -6
  47. package/src/browser/types.ts +55 -5
  48. package/src/build/generate-manifest.ts +6 -6
  49. package/src/build/generate-route-types.ts +3 -0
  50. package/src/build/route-trie.ts +50 -24
  51. package/src/build/route-types/include-resolution.ts +8 -1
  52. package/src/build/route-types/router-processing.ts +223 -74
  53. package/src/build/route-types/scan-filter.ts +8 -1
  54. package/src/cache/cache-runtime.ts +15 -11
  55. package/src/cache/cache-scope.ts +48 -7
  56. package/src/cache/cf/cf-cache-store.ts +453 -11
  57. package/src/cache/cf/index.ts +5 -1
  58. package/src/cache/document-cache.ts +17 -7
  59. package/src/cache/index.ts +1 -0
  60. package/src/cache/taint.ts +55 -0
  61. package/src/client.rsc.tsx +2 -0
  62. package/src/client.tsx +6 -66
  63. package/src/context-var.ts +72 -2
  64. package/src/debug.ts +2 -2
  65. package/src/handle.ts +40 -0
  66. package/src/handles/breadcrumbs.ts +66 -0
  67. package/src/handles/index.ts +1 -0
  68. package/src/index.rsc.ts +6 -36
  69. package/src/index.ts +50 -43
  70. package/src/prerender/store.ts +5 -4
  71. package/src/prerender.ts +138 -77
  72. package/src/reverse.ts +25 -1
  73. package/src/route-definition/dsl-helpers.ts +224 -37
  74. package/src/route-definition/helpers-types.ts +67 -19
  75. package/src/route-definition/index.ts +3 -0
  76. package/src/route-definition/redirect.ts +11 -3
  77. package/src/route-definition/resolve-handler-use.ts +149 -0
  78. package/src/route-map-builder.ts +7 -1
  79. package/src/route-types.ts +11 -0
  80. package/src/router/content-negotiation.ts +100 -1
  81. package/src/router/find-match.ts +4 -2
  82. package/src/router/handler-context.ts +111 -25
  83. package/src/router/intercept-resolution.ts +11 -4
  84. package/src/router/lazy-includes.ts +4 -1
  85. package/src/router/loader-resolution.ts +156 -21
  86. package/src/router/logging.ts +5 -2
  87. package/src/router/manifest.ts +9 -3
  88. package/src/router/match-api.ts +125 -190
  89. package/src/router/match-middleware/background-revalidation.ts +30 -2
  90. package/src/router/match-middleware/cache-lookup.ts +94 -17
  91. package/src/router/match-middleware/cache-store.ts +53 -10
  92. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  93. package/src/router/match-middleware/segment-resolution.ts +61 -5
  94. package/src/router/match-result.ts +104 -10
  95. package/src/router/metrics.ts +6 -1
  96. package/src/router/middleware-types.ts +16 -22
  97. package/src/router/middleware.ts +24 -30
  98. package/src/router/navigation-snapshot.ts +182 -0
  99. package/src/router/prerender-match.ts +114 -10
  100. package/src/router/preview-match.ts +30 -102
  101. package/src/router/request-classification.ts +310 -0
  102. package/src/router/route-snapshot.ts +245 -0
  103. package/src/router/router-context.ts +6 -1
  104. package/src/router/router-interfaces.ts +36 -4
  105. package/src/router/router-options.ts +37 -11
  106. package/src/router/segment-resolution/fresh.ts +198 -20
  107. package/src/router/segment-resolution/helpers.ts +30 -25
  108. package/src/router/segment-resolution/loader-cache.ts +1 -0
  109. package/src/router/segment-resolution/revalidation.ts +438 -300
  110. package/src/router/segment-wrappers.ts +2 -0
  111. package/src/router/types.ts +1 -0
  112. package/src/router.ts +59 -6
  113. package/src/rsc/handler.ts +472 -372
  114. package/src/rsc/loader-fetch.ts +23 -3
  115. package/src/rsc/manifest-init.ts +5 -1
  116. package/src/rsc/progressive-enhancement.ts +14 -2
  117. package/src/rsc/rsc-rendering.ts +12 -1
  118. package/src/rsc/server-action.ts +8 -0
  119. package/src/rsc/ssr-setup.ts +2 -2
  120. package/src/rsc/types.ts +9 -1
  121. package/src/segment-content-promise.ts +33 -0
  122. package/src/segment-system.tsx +164 -23
  123. package/src/server/context.ts +140 -14
  124. package/src/server/handle-store.ts +19 -0
  125. package/src/server/loader-registry.ts +9 -8
  126. package/src/server/request-context.ts +204 -28
  127. package/src/ssr/index.tsx +4 -0
  128. package/src/static-handler.ts +18 -6
  129. package/src/types/cache-types.ts +4 -4
  130. package/src/types/handler-context.ts +149 -49
  131. package/src/types/loader-types.ts +36 -9
  132. package/src/types/route-entry.ts +8 -1
  133. package/src/types/segments.ts +6 -0
  134. package/src/urls/path-helper-types.ts +39 -6
  135. package/src/urls/path-helper.ts +48 -13
  136. package/src/urls/pattern-types.ts +12 -0
  137. package/src/urls/response-types.ts +16 -6
  138. package/src/use-loader.tsx +77 -5
  139. package/src/vite/discovery/bundle-postprocess.ts +30 -33
  140. package/src/vite/discovery/discover-routers.ts +5 -1
  141. package/src/vite/discovery/prerender-collection.ts +128 -74
  142. package/src/vite/discovery/state.ts +13 -6
  143. package/src/vite/index.ts +4 -0
  144. package/src/vite/plugin-types.ts +51 -79
  145. package/src/vite/plugins/expose-action-id.ts +1 -3
  146. package/src/vite/plugins/expose-id-utils.ts +12 -0
  147. package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
  148. package/src/vite/plugins/expose-internal-ids.ts +257 -40
  149. package/src/vite/plugins/performance-tracks.ts +88 -0
  150. package/src/vite/plugins/refresh-cmd.ts +88 -26
  151. package/src/vite/plugins/version-plugin.ts +13 -1
  152. package/src/vite/rango.ts +163 -211
  153. package/src/vite/router-discovery.ts +178 -45
  154. package/src/vite/utils/banner.ts +3 -3
  155. package/src/vite/utils/prerender-utils.ts +37 -5
  156. package/src/vite/utils/shared-utils.ts +3 -2
@@ -13,11 +13,13 @@ export const VIRTUAL_ROUTES_MANIFEST_ID = "virtual:rsc-router/routes-manifest";
13
13
  export interface PluginOptions {
14
14
  enableBuildPrerender?: boolean;
15
15
  staticRouteTypesGeneration?: boolean;
16
- include?: string[];
17
- exclude?: string[];
18
16
  // Mutable ref for deferred auto-discovery (node preset).
19
17
  // The auto-discover config() hook populates this before configResolved.
20
18
  routerPathRef?: { path?: string };
19
+ /** Build-time env option from rango() config. */
20
+ buildEnv?: import("../plugin-types.js").BuildEnvOption;
21
+ /** Deployment preset (needed for buildEnv "auto" resolution). */
22
+ preset?: "node" | "cloudflare";
21
23
  }
22
24
 
23
25
  export interface PrecomputedEntry {
@@ -58,8 +60,8 @@ export interface DiscoveryState {
58
60
 
59
61
  prerenderManifestEntries: Record<string, string> | null;
60
62
  staticManifestEntries: Record<string, string> | null;
61
- handlerChunkInfo: ChunkInfo | null;
62
- staticHandlerChunkInfo: ChunkInfo | null;
63
+ handlerChunkInfoMap: Map<string, ChunkInfo>;
64
+ staticHandlerChunkInfoMap: Map<string, ChunkInfo>;
63
65
  rscEntryFileName: string | null;
64
66
  resolvedPrerenderModules: Map<string, string[]> | undefined;
65
67
  resolvedStaticModules: Map<string, string[]> | undefined;
@@ -69,6 +71,11 @@ export interface DiscoveryState {
69
71
  devServer: any;
70
72
  selfWrittenGenFiles: Map<string, { at: number; hash: string }>;
71
73
  SELF_WRITE_WINDOW_MS: number;
74
+
75
+ /** Resolved build-time env bindings (set during buildStart/configureServer). */
76
+ resolvedBuildEnv?: Record<string, unknown>;
77
+ /** Cleanup function for build-time env resources (e.g., miniflare). */
78
+ buildEnvDispose?: (() => Promise<void> | void) | null;
72
79
  }
73
80
 
74
81
  export function createDiscoveryState(
@@ -95,8 +102,8 @@ export function createDiscoveryState(
95
102
 
96
103
  prerenderManifestEntries: null,
97
104
  staticManifestEntries: null,
98
- handlerChunkInfo: null,
99
- staticHandlerChunkInfo: null,
105
+ handlerChunkInfoMap: new Map(),
106
+ staticHandlerChunkInfoMap: new Map(),
100
107
  rscEntryFileName: null,
101
108
  resolvedPrerenderModules: undefined,
102
109
  resolvedStaticModules: undefined,
package/src/vite/index.ts CHANGED
@@ -13,4 +13,8 @@ export type {
13
13
  RangoNodeOptions,
14
14
  RangoCloudflareOptions,
15
15
  RangoOptions,
16
+ BuildEnvOption,
17
+ BuildEnvFactory,
18
+ BuildEnvFactoryContext,
19
+ BuildEnvResult,
16
20
  } from "./plugin-types.js";
@@ -1,38 +1,53 @@
1
+ // -- Build-time environment types -------------------------------------------
2
+
1
3
  /**
2
- * RSC plugin entry points configuration.
3
- * All entries use virtual modules by default. Specify a path to use a custom entry file.
4
+ * Context passed to a buildEnv factory function.
5
+ * Provides Vite config details for conditional env setup.
4
6
  */
5
- export interface RscEntries {
6
- /**
7
- * Path to a custom browser/client entry file.
8
- * If not specified, a default virtual entry is used.
9
- */
10
- client?: string;
7
+ export interface BuildEnvFactoryContext {
8
+ /** Vite project root directory. */
9
+ root: string;
10
+ /** Vite mode (e.g. "development", "production"). */
11
+ mode: string;
12
+ /** Vite command ("serve" for dev, "build" for production). */
13
+ command: "serve" | "build";
14
+ /** Router deployment preset. */
15
+ preset: "node" | "cloudflare";
16
+ }
11
17
 
12
- /**
13
- * Path to a custom SSR entry file.
14
- * If not specified, a default virtual entry is used.
15
- */
16
- ssr?: string;
18
+ /**
19
+ * Factory function that creates build-time environment bindings.
20
+ * Called once at plugin startup. Return `dispose` to clean up resources.
21
+ */
22
+ export type BuildEnvFactory = (
23
+ ctx: BuildEnvFactoryContext,
24
+ ) => Promise<BuildEnvResult> | BuildEnvResult;
17
25
 
18
- /**
19
- * Path to a custom RSC entry file.
20
- * If not specified, a default virtual entry is used that imports the router from the `entry` option.
21
- */
22
- rsc?: string;
26
+ /**
27
+ * Result of resolving build-time environment bindings.
28
+ */
29
+ export interface BuildEnvResult {
30
+ /** Environment bindings available to Prerender/Static handlers via ctx.env. */
31
+ env: Record<string, unknown>;
32
+ /** Called after build completes to clean up resources (e.g., miniflare). */
33
+ dispose?: () => Promise<void> | void;
23
34
  }
24
35
 
25
36
  /**
26
- * Options for @vitejs/plugin-rsc integration
37
+ * Build-time environment configuration for Prerender and Static handlers.
38
+ *
39
+ * - `false` (default): no build-time env, `ctx.env` throws.
40
+ * - `"auto"`: calls `wrangler.getPlatformProxy()` (cloudflare preset only).
41
+ * - Object: used directly as `ctx.env` during build.
42
+ * - Factory: called once at startup, must return `{ env, dispose? }`.
27
43
  */
28
- export interface RscPluginOptions {
29
- /**
30
- * Entry points for client, ssr, and rsc environments.
31
- * All entries use virtual modules by default.
32
- * Specify paths only when you need custom entry files.
33
- */
34
- entries?: RscEntries;
35
- }
44
+ export type BuildEnvOption =
45
+ | false
46
+ | "auto"
47
+ | Record<string, unknown>
48
+ | BuildEnvFactory;
49
+
50
+ // -- Plugin options ---------------------------------------------------------
36
51
 
37
52
  /**
38
53
  * Base options shared by all presets
@@ -45,27 +60,16 @@ interface RangoBaseOptions {
45
60
  banner?: boolean;
46
61
 
47
62
  /**
48
- * Generate named-routes.gen.ts by parsing url modules at startup.
49
- * Provides type-safe Handler<"name"> and href() without executing router code.
50
- * Set to `false` to disable (run `npx rango extract-names` manually instead).
51
- * @default true
52
- */
53
- staticRouteTypesGeneration?: boolean;
54
-
55
- /**
56
- * Glob patterns for files to include in route type scanning.
57
- * Only files matching at least one pattern will be scanned.
58
- * Patterns are relative to the project root.
59
- * When unset, all .ts/.tsx files are scanned.
60
- */
61
- include?: string[];
62
-
63
- /**
64
- * Glob patterns for files to exclude from route type scanning.
65
- * Takes precedence over `include`. Patterns are relative to the project root.
66
- * Defaults to common test/build directories.
63
+ * Environment bindings available to Prerender and Static handlers at build
64
+ * time via `ctx.env`. Applies to both production build and dev on-demand
65
+ * prerender (`/__rsc_prerender`).
66
+ *
67
+ * This is the build-time env supplied by the Vite plugin, not the live
68
+ * request env. It is shared across all prerender invocations for the build.
69
+ *
70
+ * @default false
67
71
  */
68
- exclude?: string[];
72
+ buildEnv?: BuildEnvOption;
69
73
  }
70
74
 
71
75
  /**
@@ -76,38 +80,6 @@ export interface RangoNodeOptions extends RangoBaseOptions {
76
80
  * Deployment preset. Defaults to 'node' when not specified.
77
81
  */
78
82
  preset?: "node";
79
-
80
- /**
81
- * Path to your router configuration file that exports the route tree.
82
- * This file must export a `router` object created with `createRouter()`.
83
- *
84
- * When omitted, auto-discovers the router by scanning for files containing
85
- * `createRouter`. If exactly one is found, it is used automatically.
86
- * If multiple are found, an error is thrown with the list of candidates.
87
- *
88
- * @example
89
- * ```ts
90
- * rango({ router: './src/router.tsx' })
91
- * // or simply:
92
- * rango()
93
- * ```
94
- */
95
- router?: string;
96
-
97
- /**
98
- * RSC plugin configuration. By default, rsc-router includes @vitejs/plugin-rsc
99
- * with sensible defaults.
100
- *
101
- * Entry files (browser, ssr, rsc) are optional - if they don't exist,
102
- * virtual defaults are used.
103
- *
104
- * - Omit or pass `true`/`{}` to use defaults (recommended)
105
- * - Pass `{ entries: {...} }` to customize entry paths
106
- * - Pass `false` to disable (for manual @vitejs/plugin-rsc configuration)
107
- *
108
- * @default true
109
- */
110
- rsc?: boolean | RscPluginOptions;
111
83
  }
112
84
 
113
85
  /**
@@ -278,9 +278,7 @@ export function exposeActionId(): Plugin {
278
278
  if (!rscPluginApi) {
279
279
  throw new Error(
280
280
  "[rsc-router] Could not find @vitejs/plugin-rsc. " +
281
- "@rangojs/router requires the Vite RSC plugin.\n" +
282
- "The RSC plugin should be included automatically. If you disabled it with\n" +
283
- "rango({ rsc: false }), add rsc() before rango() in your config.",
281
+ "@rangojs/router requires the Vite RSC plugin, which is included automatically by rango().",
284
282
  );
285
283
  }
286
284
 
@@ -19,6 +19,18 @@ export function hashId(filePath: string, exportName: string): string {
19
19
  return `${hash.slice(0, 8)}#${exportName}`;
20
20
  }
21
21
 
22
+ /**
23
+ * Build a stable ID for an export binding. Uses hashed IDs in production
24
+ * builds (short + opaque) and readable path#name IDs in dev.
25
+ */
26
+ export function makeStubId(
27
+ filePath: string,
28
+ exportName: string,
29
+ isBuild: boolean,
30
+ ): string {
31
+ return isBuild ? hashId(filePath, exportName) : `${filePath}#${exportName}`;
32
+ }
33
+
22
34
  /**
23
35
  * Generate an 8-char hex hash for an inline static handler call site.
24
36
  * Uses file path and line number (plus optional index for same-line collisions).
@@ -138,6 +138,36 @@ export function generateExprStubs(
138
138
  };
139
139
  }
140
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.
146
+ */
147
+ export function stubHandlerExprs(
148
+ cfg: HandlerTransformConfig,
149
+ bindings: CreateExportBinding[],
150
+ s: MagicString,
151
+ filePath: string,
152
+ isBuild: boolean,
153
+ ): boolean {
154
+ let hasChanges = false;
155
+ for (const binding of bindings) {
156
+ const exportName = binding.exportNames[0];
157
+ const handlerId = isBuild
158
+ ? hashId(filePath, exportName)
159
+ : `${filePath}#${exportName}`;
160
+
161
+ s.overwrite(
162
+ binding.callExprStart,
163
+ binding.callCloseParenPos + 1,
164
+ `{ __brand: "${cfg.brand}", $$id: "${handlerId}" }`,
165
+ );
166
+ hasChanges = true;
167
+ }
168
+ return hasChanges;
169
+ }
170
+
141
171
  /**
142
172
  * Inject $$id into export const handler calls in RSC environments.
143
173
  */
@@ -2,7 +2,12 @@ import type { Plugin, ResolvedConfig } from "vite";
2
2
  import { parseAst } from "vite";
3
3
  import MagicString from "magic-string";
4
4
  import path from "node:path";
5
- import { normalizePath, hashId, detectImports } from "./expose-id-utils.js";
5
+ import {
6
+ normalizePath,
7
+ hashId,
8
+ makeStubId,
9
+ detectImports,
10
+ } from "./expose-id-utils.js";
6
11
  import {
7
12
  transformInlineHandlers,
8
13
  type VirtualHandlerEntry,
@@ -23,6 +28,7 @@ import {
23
28
  getImportedFnNames,
24
29
  collectCreateExportBindings,
25
30
  buildUnsupportedShapeWarning,
31
+ isExportOnlyFile,
26
32
  } from "./expose-ids/export-analysis.js";
27
33
  import {
28
34
  hasCreateLoaderImport,
@@ -34,6 +40,7 @@ import {
34
40
  transformLocationState,
35
41
  generateWholeFileStubs,
36
42
  generateExprStubs,
43
+ stubHandlerExprs,
37
44
  transformHandlerIds,
38
45
  } from "./expose-ids/handler-transform.js";
39
46
 
@@ -385,7 +392,9 @@ ${lazyImports.join(",\n")}
385
392
  if (stubResult) return stubResult;
386
393
  }
387
394
 
388
- // --- PrerenderHandler: non-RSC stub replacement ---
395
+ // --- PrerenderHandler: non-RSC whole-file stub replacement ---
396
+ // When ALL exports are Prerender() calls, replace the entire file.
397
+ // Mixed-export files are handled in the unified pipeline below.
389
398
  if (hasPrerenderHandlerCode && !isRscEnv) {
390
399
  const fnNames = getFnNames(PRERENDER_CONFIG.fnName);
391
400
  const bindings = getBindings(code, fnNames);
@@ -397,16 +406,6 @@ ${lazyImports.join(",\n")}
397
406
  isBuild,
398
407
  );
399
408
  if (wholeFile) return wholeFile;
400
-
401
- const exprStubs = generateExprStubs(
402
- PRERENDER_CONFIG,
403
- bindings,
404
- code,
405
- filePath,
406
- id,
407
- isBuild,
408
- );
409
- if (exprStubs) return exprStubs;
410
409
  }
411
410
 
412
411
  // --- PrerenderHandler: RSC build module tracking ---
@@ -467,7 +466,8 @@ ${lazyImports.join(",\n")}
467
466
  }
468
467
  }
469
468
 
470
- // --- StaticHandler: non-RSC stub replacement ---
469
+ // --- StaticHandler: non-RSC whole-file stub replacement ---
470
+ // When ALL exports are Static() calls, replace the entire file.
471
471
  if (hasStaticHandlerCode && !isRscEnv) {
472
472
  const fnNames = getFnNames(STATIC_CONFIG.fnName);
473
473
  const bindings = getBindings(code, fnNames);
@@ -479,16 +479,212 @@ ${lazyImports.join(",\n")}
479
479
  isBuild,
480
480
  );
481
481
  if (wholeFile) return wholeFile;
482
+ }
482
483
 
483
- const exprStubs = generateExprStubs(
484
- STATIC_CONFIG,
485
- bindings,
486
- code,
487
- filePath,
488
- id,
489
- isBuild,
490
- );
491
- if (exprStubs) return exprStubs;
484
+ // --- Mixed-type whole-file stub replacement (non-RSC) ---
485
+ // When the individual whole-file checks above fail (each only checks
486
+ // one type), the file has mixed exports (e.g. createLoader + Prerender).
487
+ // Gather ALL stub-safe bindings and check if they cover every export.
488
+ // If yes, replace the entire file with stubs — this strips server-only
489
+ // imports (node:fs, DB clients, etc.) that would crash in the browser.
490
+ //
491
+ // Only applies when the file contains Prerender/Static (the handler
492
+ // types that bring server-only code). Files with only loaders, handles,
493
+ // or locationState are handled correctly by the unified pipeline below.
494
+ //
495
+ // Loader, Prerender, and Static exports become plain { __brand, $$id }
496
+ // stubs. createHandle and createLocationState need their create*()
497
+ // functions to execute (collect registration / __rsc_ls_key), so their
498
+ // call expressions are preserved with only a @rangojs/router import.
499
+ // This strips all server-only imports while keeping the correct
500
+ // client contract for every export type.
501
+ if (!isRscEnv && (hasPrerenderHandlerCode || hasStaticHandlerCode)) {
502
+ const prerenderFnNames = hasPrerenderHandlerCode
503
+ ? getFnNames(PRERENDER_CONFIG.fnName)
504
+ : [];
505
+ const staticFnNames = hasStaticHandlerCode
506
+ ? getFnNames(STATIC_CONFIG.fnName)
507
+ : [];
508
+ const loaderFnNames = hasLoaderCode ? getFnNames("createLoader") : [];
509
+ const handleFnNames = hasHandleCode ? getFnNames("createHandle") : [];
510
+ const lsFnNames = hasLocationStateCode
511
+ ? getFnNames("createLocationState")
512
+ : [];
513
+
514
+ // Collect ALL recognized bindings to check export coverage
515
+ const allBindings: CreateExportBinding[] = [];
516
+ for (const fnNames of [
517
+ prerenderFnNames,
518
+ staticFnNames,
519
+ loaderFnNames,
520
+ handleFnNames,
521
+ lsFnNames,
522
+ ]) {
523
+ if (fnNames.length > 0) {
524
+ allBindings.push(...getBindings(code, fnNames));
525
+ }
526
+ }
527
+
528
+ // Check if preserved createHandle/createLocationState calls
529
+ // reference non-exported locals (e.g. helper functions, constants).
530
+ // If so, the whole-file stub would strip those locals, breaking
531
+ // the call. Fall through to the unified pipeline instead.
532
+ let canStubWholeFile =
533
+ allBindings.length > 0 && isExportOnlyFile(code, allBindings);
534
+
535
+ if (
536
+ canStubWholeFile &&
537
+ (handleFnNames.length > 0 || lsFnNames.length > 0)
538
+ ) {
539
+ const exportedLocals = new Set(allBindings.map((b) => b.localName));
540
+ // Collect bindings that would be stripped by whole-file replacement:
541
+ // local declarations and imported bindings from non-@rangojs/router
542
+ // modules. This is a regex-based heuristic — it intentionally skips
543
+ // edge cases (class decls, destructured bindings, combined
544
+ // default+named imports) since those rarely appear in route files.
545
+ const strippedBindings: string[] = [];
546
+
547
+ // Skip React Fast Refresh temporaries (_c, _c2, ...) which are
548
+ // injected by @vitejs/plugin-react in the client environment and
549
+ // would falsely trigger the bailout.
550
+ const localDeclPattern =
551
+ /(?:^|;|\n)\s*(?:const|let|var|function)\s+(\w+)/g;
552
+ let declMatch: RegExpExecArray | null;
553
+ while ((declMatch = localDeclPattern.exec(code)) !== null) {
554
+ const name = declMatch[1];
555
+ if (!exportedLocals.has(name) && !/^_c\d*$/.test(name)) {
556
+ strippedBindings.push(name);
557
+ }
558
+ }
559
+
560
+ const importPattern =
561
+ /import\s*\{([^}]*)\}\s*from\s*["'](?!@rangojs\/router)[^"']*["']/g;
562
+ let importMatch: RegExpExecArray | null;
563
+ while ((importMatch = importPattern.exec(code)) !== null) {
564
+ for (const spec of importMatch[1].split(",")) {
565
+ const m = spec
566
+ .trim()
567
+ .match(/^[A-Za-z_$][\w$]*(?:\s+as\s+([A-Za-z_$][\w$]*))?$/);
568
+ if (m) strippedBindings.push(m[1] || m[0].trim().split(/\s/)[0]);
569
+ }
570
+ }
571
+ const defaultImportPattern =
572
+ /import\s+([A-Za-z_$][\w$]*)\s+from\s*["'](?!@rangojs\/router)[^"']*["']/g;
573
+ while ((importMatch = defaultImportPattern.exec(code)) !== null) {
574
+ strippedBindings.push(importMatch[1]);
575
+ }
576
+ const nsImportPattern =
577
+ /import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s*["'](?!@rangojs\/router)[^"']*["']/g;
578
+ while ((importMatch = nsImportPattern.exec(code)) !== null) {
579
+ strippedBindings.push(importMatch[1]);
580
+ }
581
+
582
+ if (strippedBindings.length > 0) {
583
+ const preservedBindings = allBindings.filter((b) => {
584
+ const fc = code.slice(b.callExprStart, b.callOpenParenPos + 1);
585
+ return (
586
+ handleFnNames.some((n) => fc.includes(n)) ||
587
+ lsFnNames.some((n) => fc.includes(n))
588
+ );
589
+ });
590
+ const strippedRe = new RegExp(
591
+ `\\b(?:${strippedBindings.join("|")})\\b`,
592
+ );
593
+ canStubWholeFile = !preservedBindings.some((b) => {
594
+ const expr = code.slice(b.callExprStart, b.callCloseParenPos + 1);
595
+ return strippedRe.test(expr);
596
+ });
597
+ }
598
+ }
599
+
600
+ if (canStubWholeFile) {
601
+ const lines: string[] = [];
602
+ const neededImports: string[] = [];
603
+ if (handleFnNames.length > 0) neededImports.push("createHandle");
604
+ if (lsFnNames.length > 0) neededImports.push("createLocationState");
605
+ if (neededImports.length > 0) {
606
+ lines.push(
607
+ `import { ${neededImports.join(", ")} } from "@rangojs/router";`,
608
+ );
609
+ }
610
+
611
+ for (const binding of allBindings) {
612
+ const fnCall = code.slice(
613
+ binding.callExprStart,
614
+ binding.callOpenParenPos + 1,
615
+ );
616
+ const isHandle = handleFnNames.some((n) => fnCall.includes(n));
617
+ const isLocationState = lsFnNames.some((n) => fnCall.includes(n));
618
+
619
+ // Aliases share the primary name's ID (matches server transforms).
620
+ const primaryName = binding.exportNames[0];
621
+ const stubId = makeStubId(filePath, primaryName, isBuild);
622
+
623
+ if (isHandle || isLocationState) {
624
+ // Rewrite alias to canonical name since the stub file only
625
+ // imports canonical names from @rangojs/router.
626
+ // Strip React Fast Refresh `_c = ` wrappers from args
627
+ // (e.g. `_c = (segments) => ...` → `(segments) => ...`)
628
+ const rawArgs = code
629
+ .slice(binding.callOpenParenPos + 1, binding.callCloseParenPos)
630
+ .replace(/\b_c\d*\s*=\s*/g, "");
631
+ const canonicalName = isHandle
632
+ ? "createHandle"
633
+ : "createLocationState";
634
+ const activeFnNames = isHandle ? handleFnNames : lsFnNames;
635
+
636
+ // Reconstruct the function name (handling aliases + generics)
637
+ let rawCallee = code.slice(
638
+ binding.callExprStart,
639
+ binding.callOpenParenPos,
640
+ );
641
+ for (const alias of activeFnNames) {
642
+ if (alias !== canonicalName && rawCallee.startsWith(alias)) {
643
+ rawCallee = canonicalName + rawCallee.slice(alias.length);
644
+ break;
645
+ }
646
+ }
647
+
648
+ if (isHandle) {
649
+ // createHandle checks __injectedId DURING the call, so $$id
650
+ // must be a parameter, not a post-call property assignment.
651
+ const idParam =
652
+ binding.argCount === 0
653
+ ? `undefined, "${stubId}"`
654
+ : `, "${stubId}"`;
655
+ lines.push(
656
+ `export const ${primaryName} = ${rawCallee}(${rawArgs}${idParam});`,
657
+ );
658
+ lines.push(`${primaryName}.$$id = "${stubId}";`);
659
+ } else {
660
+ lines.push(
661
+ `export const ${primaryName} = ${rawCallee}(${rawArgs});`,
662
+ );
663
+ lines.push(
664
+ `${primaryName}.__rsc_ls_key = "__rsc_ls_${stubId}";`,
665
+ );
666
+ }
667
+ for (const name of binding.exportNames.slice(1)) {
668
+ lines.push(`export const ${name} = ${primaryName};`);
669
+ }
670
+ } else {
671
+ let brand = "loader";
672
+ if (prerenderFnNames.some((n) => fnCall.includes(n))) {
673
+ brand = PRERENDER_CONFIG.brand;
674
+ } else if (staticFnNames.some((n) => fnCall.includes(n))) {
675
+ brand = STATIC_CONFIG.brand;
676
+ }
677
+ lines.push(
678
+ `export const ${primaryName} = { __brand: "${brand}", $$id: "${stubId}" };`,
679
+ );
680
+ for (const name of binding.exportNames.slice(1)) {
681
+ lines.push(`export const ${name} = ${primaryName};`);
682
+ }
683
+ }
684
+ }
685
+
686
+ return { code: lines.join("\n") + "\n", map: null };
687
+ }
492
688
  }
493
689
 
494
690
  // --- StaticHandler: RSC build module tracking ---
@@ -535,27 +731,48 @@ ${lazyImports.join(",\n")}
535
731
  isBuild,
536
732
  ) || changed;
537
733
  }
538
- if (hasPrerenderHandlerCode && isRscEnv) {
734
+ if (hasPrerenderHandlerCode) {
539
735
  const fnNames = getFnNames(PRERENDER_CONFIG.fnName);
540
- changed =
541
- transformHandlerIds(
542
- PRERENDER_CONFIG,
543
- getBindings(code, fnNames),
544
- s,
545
- filePath,
546
- isBuild,
547
- ) || changed;
736
+ const bindings = getBindings(code, fnNames);
737
+ if (isRscEnv) {
738
+ changed =
739
+ transformHandlerIds(
740
+ PRERENDER_CONFIG,
741
+ bindings,
742
+ s,
743
+ filePath,
744
+ isBuild,
745
+ ) || changed;
746
+ } else {
747
+ // Non-RSC mixed-export file: replace Prerender() calls with stubs
748
+ // on the shared MagicString so sourcemaps stay accurate.
749
+ changed =
750
+ stubHandlerExprs(
751
+ PRERENDER_CONFIG,
752
+ bindings,
753
+ s,
754
+ filePath,
755
+ isBuild,
756
+ ) || changed;
757
+ }
548
758
  }
549
- if (hasStaticHandlerCode && isRscEnv) {
759
+ if (hasStaticHandlerCode) {
550
760
  const fnNames = getFnNames(STATIC_CONFIG.fnName);
551
- changed =
552
- transformHandlerIds(
553
- STATIC_CONFIG,
554
- getBindings(code, fnNames),
555
- s,
556
- filePath,
557
- isBuild,
558
- ) || changed;
761
+ const bindings = getBindings(code, fnNames);
762
+ if (isRscEnv) {
763
+ changed =
764
+ transformHandlerIds(
765
+ STATIC_CONFIG,
766
+ bindings,
767
+ s,
768
+ filePath,
769
+ isBuild,
770
+ ) || changed;
771
+ } else {
772
+ changed =
773
+ stubHandlerExprs(STATIC_CONFIG, bindings, s, filePath, isBuild) ||
774
+ changed;
775
+ }
559
776
  }
560
777
 
561
778
  if (!changed) return;