@rangojs/router 0.0.0-experimental.20 → 0.0.0-experimental.204030a9

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 (293) hide show
  1. package/AGENTS.md +4 -0
  2. package/README.md +242 -55
  3. package/dist/bin/rango.js +277 -99
  4. package/dist/vite/index.js +2929 -1132
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +68 -21
  8. package/skills/breadcrumbs/SKILL.md +252 -0
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +243 -21
  11. package/skills/caching/SKILL.md +159 -10
  12. package/skills/composability/SKILL.md +27 -2
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +262 -51
  16. package/skills/host-router/SKILL.md +243 -0
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +46 -4
  19. package/skills/layout/SKILL.md +28 -7
  20. package/skills/links/SKILL.md +249 -17
  21. package/skills/loader/SKILL.md +291 -31
  22. package/skills/middleware/SKILL.md +49 -12
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +27 -0
  26. package/skills/observability/SKILL.md +137 -0
  27. package/skills/parallel/SKILL.md +197 -6
  28. package/skills/prerender/SKILL.md +125 -102
  29. package/skills/rango/SKILL.md +242 -23
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +66 -9
  32. package/skills/route/SKILL.md +91 -8
  33. package/skills/router-setup/SKILL.md +98 -8
  34. package/skills/server-actions/SKILL.md +751 -0
  35. package/skills/streams-and-websockets/SKILL.md +283 -0
  36. package/skills/testing/SKILL.md +511 -188
  37. package/skills/typesafety/SKILL.md +354 -50
  38. package/skills/use-cache/SKILL.md +34 -5
  39. package/skills/view-transitions/SKILL.md +294 -0
  40. package/src/__augment-tests__/augment.ts +81 -0
  41. package/src/__augment-tests__/augmented.check.ts +117 -0
  42. package/src/__internal.ts +92 -0
  43. package/src/browser/action-coordinator.ts +53 -36
  44. package/src/browser/app-shell.ts +52 -0
  45. package/src/browser/app-version.ts +14 -0
  46. package/src/browser/event-controller.ts +91 -70
  47. package/src/browser/history-state.ts +21 -0
  48. package/src/browser/index.ts +3 -3
  49. package/src/browser/link-interceptor.ts +4 -0
  50. package/src/browser/navigation-bridge.ts +183 -18
  51. package/src/browser/navigation-client.ts +187 -57
  52. package/src/browser/navigation-store.ts +75 -17
  53. package/src/browser/navigation-transaction.ts +21 -37
  54. package/src/browser/partial-update.ts +143 -40
  55. package/src/browser/prefetch/cache.ts +275 -28
  56. package/src/browser/prefetch/fetch.ts +191 -46
  57. package/src/browser/prefetch/policy.ts +6 -0
  58. package/src/browser/prefetch/queue.ts +123 -20
  59. package/src/browser/prefetch/resource-ready.ts +77 -0
  60. package/src/browser/rango-state.ts +53 -13
  61. package/src/browser/react/Link.tsx +98 -14
  62. package/src/browser/react/NavigationProvider.tsx +110 -33
  63. package/src/browser/react/context.ts +7 -2
  64. package/src/browser/react/filter-segment-order.ts +51 -7
  65. package/src/browser/react/index.ts +3 -0
  66. package/src/browser/react/location-state-shared.ts +175 -4
  67. package/src/browser/react/location-state.ts +39 -13
  68. package/src/browser/react/use-handle.ts +23 -64
  69. package/src/browser/react/use-navigation.ts +22 -2
  70. package/src/browser/react/use-params.ts +20 -8
  71. package/src/browser/react/use-reverse.ts +106 -0
  72. package/src/browser/react/use-router.ts +43 -10
  73. package/src/browser/react/use-segments.ts +11 -8
  74. package/src/browser/response-adapter.ts +25 -0
  75. package/src/browser/rsc-router.tsx +200 -75
  76. package/src/browser/scroll-restoration.ts +46 -39
  77. package/src/browser/segment-reconciler.ts +36 -9
  78. package/src/browser/segment-structure-assert.ts +2 -2
  79. package/src/browser/server-action-bridge.ts +31 -36
  80. package/src/browser/types.ts +81 -5
  81. package/src/build/collect-fallback-refs.ts +107 -0
  82. package/src/build/generate-manifest.ts +65 -40
  83. package/src/build/generate-route-types.ts +5 -0
  84. package/src/build/index.ts +2 -0
  85. package/src/build/route-trie.ts +69 -26
  86. package/src/build/route-types/codegen.ts +4 -4
  87. package/src/build/route-types/include-resolution.ts +9 -2
  88. package/src/build/route-types/per-module-writer.ts +7 -4
  89. package/src/build/route-types/router-processing.ts +278 -88
  90. package/src/build/route-types/scan-filter.ts +9 -2
  91. package/src/build/route-types/source-scan.ts +118 -0
  92. package/src/build/runtime-discovery.ts +9 -20
  93. package/src/cache/cache-runtime.ts +15 -11
  94. package/src/cache/cache-scope.ts +76 -49
  95. package/src/cache/cf/cf-cache-store.ts +501 -18
  96. package/src/cache/cf/index.ts +5 -1
  97. package/src/cache/document-cache.ts +17 -7
  98. package/src/cache/index.ts +1 -0
  99. package/src/cache/taint.ts +55 -0
  100. package/src/client.rsc.tsx +5 -1
  101. package/src/client.tsx +95 -284
  102. package/src/context-var.ts +72 -2
  103. package/src/debug.ts +2 -2
  104. package/src/decode-loader-results.ts +36 -0
  105. package/src/errors.ts +30 -1
  106. package/src/handle.ts +65 -12
  107. package/src/handles/breadcrumbs.ts +66 -0
  108. package/src/handles/index.ts +1 -0
  109. package/src/host/index.ts +2 -5
  110. package/src/host/router.ts +129 -57
  111. package/src/host/types.ts +31 -2
  112. package/src/host/utils.ts +1 -1
  113. package/src/href-client.ts +140 -20
  114. package/src/index.rsc.ts +15 -40
  115. package/src/index.ts +92 -76
  116. package/src/loader-store.ts +500 -0
  117. package/src/loader.rsc.ts +2 -5
  118. package/src/loader.ts +3 -10
  119. package/src/missing-id-error.ts +68 -0
  120. package/src/outlet-context.ts +1 -1
  121. package/src/prerender/store.ts +57 -15
  122. package/src/prerender.ts +141 -80
  123. package/src/response-utils.ts +37 -0
  124. package/src/reverse.ts +65 -15
  125. package/src/route-content-wrapper.tsx +6 -28
  126. package/src/route-definition/dsl-helpers.ts +435 -260
  127. package/src/route-definition/helper-factories.ts +29 -139
  128. package/src/route-definition/helpers-types.ts +110 -34
  129. package/src/route-definition/index.ts +3 -3
  130. package/src/route-definition/redirect.ts +11 -3
  131. package/src/route-definition/resolve-handler-use.ts +155 -0
  132. package/src/route-definition/use-item-types.ts +32 -0
  133. package/src/route-map-builder.ts +7 -1
  134. package/src/route-types.ts +37 -41
  135. package/src/router/basename.ts +14 -0
  136. package/src/router/content-negotiation.ts +113 -1
  137. package/src/router/error-handling.ts +1 -1
  138. package/src/router/find-match.ts +4 -2
  139. package/src/router/handler-context.ts +105 -39
  140. package/src/router/intercept-resolution.ts +15 -22
  141. package/src/router/lazy-includes.ts +12 -9
  142. package/src/router/loader-resolution.ts +175 -23
  143. package/src/router/logging.ts +5 -2
  144. package/src/router/manifest.ts +31 -16
  145. package/src/router/match-api.ts +129 -193
  146. package/src/router/match-handlers.ts +63 -20
  147. package/src/router/match-middleware/background-revalidation.ts +30 -2
  148. package/src/router/match-middleware/cache-lookup.ts +136 -106
  149. package/src/router/match-middleware/cache-store.ts +54 -10
  150. package/src/router/match-middleware/intercept-resolution.ts +9 -7
  151. package/src/router/match-middleware/segment-resolution.ts +61 -5
  152. package/src/router/match-result.ts +124 -18
  153. package/src/router/metrics.ts +239 -14
  154. package/src/router/middleware-types.ts +61 -31
  155. package/src/router/middleware.ts +226 -124
  156. package/src/router/navigation-snapshot.ts +182 -0
  157. package/src/router/pattern-matching.ts +118 -19
  158. package/src/router/prerender-match.ts +114 -10
  159. package/src/router/preview-match.ts +32 -102
  160. package/src/router/request-classification.ts +286 -0
  161. package/src/router/revalidation.ts +85 -9
  162. package/src/router/route-snapshot.ts +245 -0
  163. package/src/router/router-context.ts +6 -1
  164. package/src/router/router-interfaces.ts +91 -29
  165. package/src/router/router-options.ts +89 -19
  166. package/src/router/router-registry.ts +2 -5
  167. package/src/router/segment-resolution/fresh.ts +240 -23
  168. package/src/router/segment-resolution/helpers.ts +30 -25
  169. package/src/router/segment-resolution/loader-cache.ts +1 -0
  170. package/src/router/segment-resolution/revalidation.ts +483 -289
  171. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  172. package/src/router/segment-wrappers.ts +2 -0
  173. package/src/router/substitute-pattern-params.ts +56 -0
  174. package/src/router/telemetry.ts +99 -0
  175. package/src/router/trie-matching.ts +38 -15
  176. package/src/router/types.ts +9 -0
  177. package/src/router/url-params.ts +49 -0
  178. package/src/router.ts +120 -32
  179. package/src/rsc/handler-context.ts +2 -2
  180. package/src/rsc/handler.ts +524 -370
  181. package/src/rsc/helpers.ts +91 -43
  182. package/src/rsc/index.ts +1 -21
  183. package/src/rsc/loader-fetch.ts +23 -3
  184. package/src/rsc/manifest-init.ts +5 -1
  185. package/src/rsc/origin-guard.ts +28 -10
  186. package/src/rsc/progressive-enhancement.ts +39 -10
  187. package/src/rsc/response-route-handler.ts +46 -53
  188. package/src/rsc/rsc-rendering.ts +69 -89
  189. package/src/rsc/runtime-warnings.ts +9 -10
  190. package/src/rsc/server-action.ts +39 -47
  191. package/src/rsc/ssr-setup.ts +144 -0
  192. package/src/rsc/types.ts +19 -3
  193. package/src/search-params.ts +20 -17
  194. package/src/segment-content-promise.ts +67 -0
  195. package/src/segment-loader-promise.ts +122 -0
  196. package/src/segment-system.tsx +219 -67
  197. package/src/serialize.ts +243 -0
  198. package/src/server/context.ts +285 -63
  199. package/src/server/cookie-store.ts +28 -4
  200. package/src/server/handle-store.ts +19 -0
  201. package/src/server/loader-registry.ts +9 -8
  202. package/src/server/request-context.ts +228 -65
  203. package/src/server.ts +6 -0
  204. package/src/ssr/index.tsx +9 -1
  205. package/src/static-handler.ts +19 -7
  206. package/src/testing/cache-status.ts +166 -0
  207. package/src/testing/collect-handle.ts +63 -0
  208. package/src/testing/dispatch.ts +440 -0
  209. package/src/testing/dom.entry.ts +22 -0
  210. package/src/testing/e2e/fixture.ts +154 -0
  211. package/src/testing/e2e/index.ts +149 -0
  212. package/src/testing/e2e/matchers.ts +51 -0
  213. package/src/testing/e2e/page-helpers.ts +272 -0
  214. package/src/testing/e2e/parity.ts +306 -0
  215. package/src/testing/e2e/server.ts +183 -0
  216. package/src/testing/flight-matchers.ts +104 -0
  217. package/src/testing/flight-runtime.d.ts +21 -0
  218. package/src/testing/flight.entry.ts +22 -0
  219. package/src/testing/flight.ts +182 -0
  220. package/src/testing/generated-routes.ts +223 -0
  221. package/src/testing/index.ts +98 -0
  222. package/src/testing/internal/context.ts +151 -0
  223. package/src/testing/render-route.tsx +536 -0
  224. package/src/testing/run-loader.ts +296 -0
  225. package/src/testing/run-middleware.ts +170 -0
  226. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  227. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  228. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  229. package/src/testing/vitest-stubs/version.ts +5 -0
  230. package/src/testing/vitest.ts +112 -0
  231. package/src/theme/index.ts +4 -13
  232. package/src/types/cache-types.ts +4 -4
  233. package/src/types/global-namespace.ts +39 -26
  234. package/src/types/handler-context.ts +197 -79
  235. package/src/types/index.ts +1 -0
  236. package/src/types/loader-types.ts +41 -15
  237. package/src/types/request-scope.ts +126 -0
  238. package/src/types/route-config.ts +17 -8
  239. package/src/types/route-entry.ts +19 -1
  240. package/src/types/segments.ts +37 -6
  241. package/src/urls/include-helper.ts +34 -67
  242. package/src/urls/index.ts +0 -3
  243. package/src/urls/path-helper-types.ts +50 -9
  244. package/src/urls/path-helper.ts +63 -63
  245. package/src/urls/pattern-types.ts +48 -19
  246. package/src/urls/response-types.ts +25 -22
  247. package/src/urls/type-extraction.ts +26 -116
  248. package/src/urls/urls-function.ts +1 -5
  249. package/src/use-loader.tsx +487 -44
  250. package/src/vite/debug.ts +185 -0
  251. package/src/vite/discovery/bundle-postprocess.ts +63 -91
  252. package/src/vite/discovery/discover-routers.ts +106 -53
  253. package/src/vite/discovery/discovery-errors.ts +194 -0
  254. package/src/vite/discovery/gate-state.ts +171 -0
  255. package/src/vite/discovery/prerender-collection.ts +222 -107
  256. package/src/vite/discovery/route-types-writer.ts +40 -84
  257. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  258. package/src/vite/discovery/state.ts +50 -13
  259. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  260. package/src/vite/index.ts +10 -3
  261. package/src/vite/plugin-types.ts +111 -72
  262. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  263. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  264. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  265. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  266. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  267. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  268. package/src/vite/plugins/expose-action-id.ts +55 -33
  269. package/src/vite/plugins/expose-id-utils.ts +24 -8
  270. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  271. package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
  272. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  273. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  274. package/src/vite/plugins/expose-internal-ids.ts +544 -317
  275. package/src/vite/plugins/performance-tracks.ts +92 -0
  276. package/src/vite/plugins/refresh-cmd.ts +127 -0
  277. package/src/vite/plugins/use-cache-transform.ts +65 -50
  278. package/src/vite/plugins/version-injector.ts +39 -23
  279. package/src/vite/plugins/version-plugin.ts +72 -3
  280. package/src/vite/plugins/virtual-entries.ts +2 -2
  281. package/src/vite/rango.ts +265 -226
  282. package/src/vite/router-discovery.ts +924 -137
  283. package/src/vite/utils/ast-handler-extract.ts +15 -15
  284. package/src/vite/utils/banner.ts +4 -4
  285. package/src/vite/utils/bundle-analysis.ts +4 -2
  286. package/src/vite/utils/client-chunks.ts +190 -0
  287. package/src/vite/utils/forward-user-plugins.ts +193 -0
  288. package/src/vite/utils/manifest-utils.ts +21 -5
  289. package/src/vite/utils/package-resolution.ts +41 -1
  290. package/src/vite/utils/prerender-utils.ts +98 -5
  291. package/src/vite/utils/shared-utils.ts +109 -27
  292. package/src/browser/action-response-classifier.ts +0 -99
  293. package/src/route-definition/route-function.ts +0 -119
@@ -6,6 +6,7 @@ import {
6
6
  buildExportMap,
7
7
  escapeRegExp,
8
8
  } from "../expose-id-utils.js";
9
+ import { codeMatchIndices } from "../../../build/route-types/source-scan.js";
9
10
  import type { CreateExportBinding } from "./types.js";
10
11
 
11
12
  /**
@@ -59,19 +60,57 @@ export function isExportOnlyFile(
59
60
  return true;
60
61
  }
61
62
 
62
- // NOTE: This regex may over-count when the fn name appears inside strings or
63
- // comments, but it's only used for the warning heuristic (totalCalls >
64
- // supportedBindings) and the inline-extraction pre-check, so over-counting
65
- // triggers a harmless extra AST parse rather than affecting correctness.
63
+ function createCallPattern(fnNames: string[]): RegExp {
64
+ return new RegExp(
65
+ `\\b(?:${fnNames.map(escapeRegExp).join("|")})\\s*(?:<[^>]*>\\s*)?\\(`,
66
+ "g",
67
+ );
68
+ }
69
+
70
+ // Counts real create*() call sites, ignoring occurrences inside comments and
71
+ // string literals. Used by the unsupported-shape warning heuristic and the
72
+ // inline-extraction pre-check.
66
73
  export function countCreateCallsForNames(
67
74
  code: string,
68
75
  fnNames: string[],
69
76
  ): number {
70
- const pattern = new RegExp(
71
- `\\b(?:${fnNames.map(escapeRegExp).join("|")})\\s*(?:<[^>]*>\\s*)?\\(`,
72
- "g",
73
- );
74
- return (code.match(pattern) || []).length;
77
+ return codeMatchIndices(code, createCallPattern(fnNames)).length;
78
+ }
79
+
80
+ /** Convert a 0-based byte offset to a 1-based { line, column }. */
81
+ export function offsetToLineColumn(
82
+ code: string,
83
+ index: number,
84
+ ): { line: number; column: number } {
85
+ let line = 1;
86
+ let lineStart = 0;
87
+ const end = Math.min(index, code.length);
88
+ for (let i = 0; i < end; i++) {
89
+ if (code[i] === "\n") {
90
+ line++;
91
+ lineStart = i + 1;
92
+ }
93
+ }
94
+ return { line, column: index - lineStart + 1 };
95
+ }
96
+
97
+ /**
98
+ * Locate every real create*() call site (comment/string-free) that is NOT one
99
+ * of the supported, id-injectable export bindings, returning each as a 1-based
100
+ * { line, column }. The empty result means every call is in a supported shape.
101
+ * Both binding-collection paths anchor `callExprStart` at the start of the
102
+ * create* identifier — exactly where this pattern matches — so the set
103
+ * difference is exact.
104
+ */
105
+ export function findUnsupportedCreateCallSites(
106
+ code: string,
107
+ fnNames: string[],
108
+ supportedBindings: CreateExportBinding[],
109
+ ): Array<{ line: number; column: number }> {
110
+ const supported = new Set(supportedBindings.map((b) => b.callExprStart));
111
+ return codeMatchIndices(code, createCallPattern(fnNames))
112
+ .filter((index) => !supported.has(index))
113
+ .map((index) => offsetToLineColumn(code, index));
75
114
  }
76
115
 
77
116
  export function getImportedFnNames(
@@ -119,6 +158,28 @@ export function getCalledIdentifierFromCall(callExpr: any): string | null {
119
158
  return null;
120
159
  }
121
160
 
161
+ /**
162
+ * plugin-react's dev Fast Refresh wraps exports whose function body uses
163
+ * hook-like calls in a signature-registration call. A loader/handle that calls
164
+ * `ctx.use(...)` trips this heuristic, so `export const X = createLoader(...)`
165
+ * becomes `export const X = _s(createLoader(...), "<sig>", true)` — the create*
166
+ * call is the first argument of an unrelated wrapper call. Unwrap a single such
167
+ * layer so ID injection still targets the inner create* call. The `$$id`
168
+ * assignment is appended after the whole statement (against the export local),
169
+ * which is unaffected by the wrapper since `_s(x)` returns `x`.
170
+ */
171
+ function unwrapSignatureWrappedCall(init: any, fnNameSet: Set<string>): any {
172
+ if (init?.type !== "CallExpression") return init;
173
+ const directId = getCalledIdentifierFromCall(init);
174
+ if (directId && fnNameSet.has(directId)) return init;
175
+ const firstArg = init.arguments?.[0];
176
+ if (firstArg?.type === "CallExpression") {
177
+ const innerId = getCalledIdentifierFromCall(firstArg);
178
+ if (innerId && fnNameSet.has(innerId)) return firstArg;
179
+ }
180
+ return init;
181
+ }
182
+
122
183
  export function collectCreateExportBindingsFallback(
123
184
  code: string,
124
185
  fnNames: string[],
@@ -196,7 +257,7 @@ export function collectCreateExportBindings(
196
257
  ): CreateExportBinding[] {
197
258
  if (!program) {
198
259
  try {
199
- program = parseAst(code, { jsx: true });
260
+ program = parseAst(code, { lang: "tsx" });
200
261
  } catch {
201
262
  return collectCreateExportBindingsFallback(code, fnNames);
202
263
  }
@@ -212,10 +273,13 @@ export function collectCreateExportBindings(
212
273
  }
213
274
 
214
275
  for (const decl of varDecl.declarations ?? []) {
215
- const calledIdentifier = getCalledIdentifierFromCall(decl?.init);
276
+ // Unwrap a Fast Refresh signature wrapper (`_s(createLoader(...), ...)`)
277
+ // so injection targets the inner create* call. Falls back to decl.init.
278
+ const callExpr = unwrapSignatureWrappedCall(decl?.init, fnNameSet);
279
+ const calledIdentifier = getCalledIdentifierFromCall(callExpr);
216
280
  if (
217
281
  decl?.id?.type !== "Identifier" ||
218
- decl?.init?.type !== "CallExpression" ||
282
+ callExpr?.type !== "CallExpression" ||
219
283
  !calledIdentifier ||
220
284
  !fnNameSet.has(calledIdentifier)
221
285
  ) {
@@ -226,9 +290,8 @@ export function collectCreateExportBindings(
226
290
  const exportNames = exportMap.get(localName) ?? [];
227
291
  if (exportNames.length === 0) continue;
228
292
 
229
- const callStart = decl.init.start as number;
230
- const callEnd = decl.init.end as number;
231
- const calleeEnd = decl.init.callee.end as number;
293
+ const callEnd = callExpr.end as number;
294
+ const calleeEnd = callExpr.callee.end as number;
232
295
 
233
296
  let openParenPos = -1;
234
297
  for (let i = calleeEnd; i < callEnd; i++) {
@@ -245,10 +308,10 @@ export function collectCreateExportBindings(
245
308
  bindings.push({
246
309
  localName,
247
310
  exportNames,
248
- callExprStart: decl.init.start as number,
311
+ callExprStart: callExpr.start as number,
249
312
  callOpenParenPos: openParenPos,
250
313
  callCloseParenPos: closeParenPos,
251
- argCount: decl.init.arguments?.length ?? 0,
314
+ argCount: callExpr.arguments?.length ?? 0,
252
315
  statementEnd,
253
316
  });
254
317
  }
@@ -282,9 +345,25 @@ export function collectCreateExportBindings(
282
345
  export function buildUnsupportedShapeWarning(
283
346
  filePath: string,
284
347
  fnName: string,
348
+ sites: Array<{ line: number; column: number }> = [],
285
349
  ): string {
286
- return [
287
- `[rsc-router] Unsupported ${fnName} shape in "${filePath}".`,
350
+ const lines = [`[rango] Unsupported ${fnName} shape in "${filePath}".`];
351
+
352
+ // Point at the exact call(s) so the location is clickable in the terminal/IDE
353
+ // (file:line:column) instead of leaving the user to scan the whole file.
354
+ if (sites.length === 1) {
355
+ const s = sites[0];
356
+ lines.push(
357
+ `The ${fnName}(...) call at ${filePath}:${s.line}:${s.column} has no stable $$id injected — it is not in a supported shape.`,
358
+ );
359
+ } else if (sites.length > 1) {
360
+ lines.push(
361
+ `These ${fnName}(...) calls have no stable $$id injected — they are not in a supported shape:`,
362
+ );
363
+ for (const s of sites) lines.push(` - ${filePath}:${s.line}:${s.column}`);
364
+ }
365
+
366
+ lines.push(
288
367
  `Supported shapes are:`,
289
368
  ` - export const X = ${fnName}(...)`,
290
369
  ` - const X = ${fnName}(...); export { X }`,
@@ -292,5 +371,6 @@ export function buildUnsupportedShapeWarning(
292
371
  `Potentially unsupported forms include:`,
293
372
  ` - export let/var X = ${fnName}(...)`,
294
373
  ` - inline ${fnName}(...) calls`,
295
- ].join("\n");
374
+ );
375
+ return lines.join("\n");
296
376
  }
@@ -1,5 +1,5 @@
1
1
  import MagicString from "magic-string";
2
- import { hashId } from "../expose-id-utils.js";
2
+ import { makeStubId } from "../expose-id-utils.js";
3
3
  import type { HandlerTransformConfig, CreateExportBinding } from "./types.js";
4
4
  import { isExportOnlyFile } from "./export-analysis.js";
5
5
 
@@ -28,9 +28,7 @@ export function transformHandles(
28
28
  binding.callCloseParenPos,
29
29
  );
30
30
 
31
- const handleId = isBuild
32
- ? hashId(filePath, exportName)
33
- : `${filePath}#${exportName}`;
31
+ const handleId = makeStubId(filePath, exportName, isBuild);
34
32
 
35
33
  let paramInjection: string;
36
34
  if (!args.hasArgs) {
@@ -58,9 +56,7 @@ export function transformLocationState(
58
56
  for (const binding of bindings) {
59
57
  const exportName = binding.exportNames[0];
60
58
 
61
- const stateKey = isBuild
62
- ? hashId(filePath, exportName)
63
- : `${filePath}#${exportName}`;
59
+ const stateKey = makeStubId(filePath, exportName, isBuild);
64
60
 
65
61
  // Key is injected as a property assignment (not as a function argument).
66
62
  // This allows createLocationState to accept options like { flash: true }
@@ -88,7 +84,7 @@ export function generateWholeFileStubs(
88
84
 
89
85
  const exportNames = bindings.flatMap((b) => b.exportNames);
90
86
  const stubs = exportNames.map((name) => {
91
- const handlerId = isBuild ? hashId(filePath, name) : `${filePath}#${name}`;
87
+ const handlerId = makeStubId(filePath, name, isBuild);
92
88
  return `export const ${name} = { __brand: "${cfg.brand}", $$id: "${handlerId}" };`;
93
89
  });
94
90
 
@@ -96,27 +92,20 @@ 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.
95
+ * Replace handler call expressions with lightweight stub objects on the shared
96
+ * unified-pipeline MagicString so all transforms share one sourcemap.
101
97
  */
102
- export function generateExprStubs(
98
+ export function stubHandlerExprs(
103
99
  cfg: HandlerTransformConfig,
104
100
  bindings: CreateExportBinding[],
105
- code: string,
101
+ s: MagicString,
106
102
  filePath: string,
107
- sourceId: string,
108
103
  isBuild: boolean,
109
- ): { code: string; map: ReturnType<MagicString["generateMap"]> } | null {
110
- if (bindings.length === 0) return null;
111
-
112
- const s = new MagicString(code);
104
+ ): boolean {
113
105
  let hasChanges = false;
114
-
115
106
  for (const binding of bindings) {
116
107
  const exportName = binding.exportNames[0];
117
- const handlerId = isBuild
118
- ? hashId(filePath, exportName)
119
- : `${filePath}#${exportName}`;
108
+ const handlerId = makeStubId(filePath, exportName, isBuild);
120
109
 
121
110
  s.overwrite(
122
111
  binding.callExprStart,
@@ -125,17 +114,7 @@ export function generateExprStubs(
125
114
  );
126
115
  hasChanges = true;
127
116
  }
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
- };
117
+ return hasChanges;
139
118
  }
140
119
 
141
120
  /**
@@ -152,9 +131,7 @@ export function transformHandlerIds(
152
131
  for (const binding of bindings) {
153
132
  const exportName = binding.exportNames[0];
154
133
 
155
- const handlerId = isBuild
156
- ? hashId(filePath, exportName)
157
- : `${filePath}#${exportName}`;
134
+ const handlerId = makeStubId(filePath, exportName, isBuild);
158
135
 
159
136
  // Injection strategy matches the runtime overload signatures:
160
137
  // 0 args -> inject undefined, "id"
@@ -1,5 +1,5 @@
1
1
  import type MagicString from "magic-string";
2
- import { hashId } from "../expose-id-utils.js";
2
+ import { makeStubId } from "../expose-id-utils.js";
3
3
  import type { CreateExportBinding } from "./types.js";
4
4
  import { isExportOnlyFile } from "./export-analysis.js";
5
5
 
@@ -33,7 +33,7 @@ export function generateClientLoaderStubs(
33
33
 
34
34
  for (const binding of bindings) {
35
35
  for (const name of binding.exportNames) {
36
- const loaderId = isBuild ? hashId(filePath, name) : `${filePath}#${name}`;
36
+ const loaderId = makeStubId(filePath, name, isBuild);
37
37
  lines.push(
38
38
  `export const ${name} = { __brand: "loader", $$id: "${loaderId}" };`,
39
39
  );
@@ -54,9 +54,7 @@ export function transformLoaders(
54
54
  for (const binding of bindings) {
55
55
  const exportName = binding.exportNames[0];
56
56
 
57
- const loaderId = isBuild
58
- ? hashId(filePath, exportName)
59
- : `${filePath}#${exportName}`;
57
+ const loaderId = makeStubId(filePath, exportName, isBuild);
60
58
 
61
59
  // Inject $$id as hidden third parameter.
62
60
  // createLoader(fn) -> createLoader(fn, undefined, "id")
@@ -4,6 +4,9 @@ import path from "node:path";
4
4
  import { createHash } from "node:crypto";
5
5
  import { normalizePath, findMatchingParen } from "../expose-id-utils.js";
6
6
  import { getImportedFnNames } from "./export-analysis.js";
7
+ import { createRangoDebugger, createCounter, NS } from "../../debug.js";
8
+
9
+ const debug = createRangoDebugger(NS.transform);
7
10
 
8
11
  export function transformRouter(
9
12
  code: string,
@@ -82,11 +85,15 @@ export function transformRouter(
82
85
  */
83
86
  export function exposeRouterId(): Plugin {
84
87
  let projectRoot = "";
88
+ const counter = createCounter(debug, "expose-router-id");
85
89
  return {
86
90
  name: "@rangojs/router:expose-router-id",
87
91
  configResolved(config) {
88
92
  projectRoot = config.root;
89
93
  },
94
+ buildEnd() {
95
+ counter?.flush();
96
+ },
90
97
  transform(code, id) {
91
98
  if (!code.includes("createRouter")) return null;
92
99
  // Accepts both @rangojs/router and @rangojs/router/server subpath.
@@ -102,9 +109,19 @@ export function exposeRouterId(): Plugin {
102
109
  }
103
110
  if (id.includes("node_modules")) return null;
104
111
 
105
- const filePath = normalizePath(path.relative(projectRoot, id));
106
- const routerFnNames = getImportedFnNames(code, "createRouter");
107
- return transformRouter(code, filePath, routerFnNames, normalizePath(id));
112
+ const start = counter ? performance.now() : 0;
113
+ try {
114
+ const filePath = normalizePath(path.relative(projectRoot, id));
115
+ const routerFnNames = getImportedFnNames(code, "createRouter");
116
+ return transformRouter(
117
+ code,
118
+ filePath,
119
+ routerFnNames,
120
+ normalizePath(id),
121
+ );
122
+ } finally {
123
+ counter?.record(id, performance.now() - start);
124
+ }
108
125
  },
109
126
  };
110
127
  }