@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.80

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 (312) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +942 -4
  3. package/dist/bin/rango.js +1689 -0
  4. package/dist/vite/index.js +4960 -935
  5. package/package.json +70 -60
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +294 -0
  8. package/skills/caching/SKILL.md +93 -23
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/handler-use/SKILL.md +362 -0
  14. package/skills/hooks/SKILL.md +334 -72
  15. package/skills/host-router/SKILL.md +218 -0
  16. package/skills/intercept/SKILL.md +151 -8
  17. package/skills/layout/SKILL.md +122 -3
  18. package/skills/links/SKILL.md +92 -31
  19. package/skills/loader/SKILL.md +404 -44
  20. package/skills/middleware/SKILL.md +205 -37
  21. package/skills/migrate-nextjs/SKILL.md +560 -0
  22. package/skills/migrate-react-router/SKILL.md +764 -0
  23. package/skills/mime-routes/SKILL.md +128 -0
  24. package/skills/parallel/SKILL.md +263 -1
  25. package/skills/prerender/SKILL.md +685 -0
  26. package/skills/rango/SKILL.md +87 -16
  27. package/skills/response-routes/SKILL.md +411 -0
  28. package/skills/route/SKILL.md +281 -14
  29. package/skills/router-setup/SKILL.md +210 -32
  30. package/skills/tailwind/SKILL.md +129 -0
  31. package/skills/theme/SKILL.md +9 -8
  32. package/skills/typesafety/SKILL.md +328 -89
  33. package/skills/use-cache/SKILL.md +324 -0
  34. package/src/__internal.ts +102 -4
  35. package/src/bin/rango.ts +321 -0
  36. package/src/browser/action-coordinator.ts +97 -0
  37. package/src/browser/action-response-classifier.ts +99 -0
  38. package/src/browser/app-version.ts +14 -0
  39. package/src/browser/event-controller.ts +92 -64
  40. package/src/browser/history-state.ts +80 -0
  41. package/src/browser/intercept-utils.ts +52 -0
  42. package/src/browser/link-interceptor.ts +24 -4
  43. package/src/browser/logging.ts +55 -0
  44. package/src/browser/merge-segment-loaders.ts +20 -12
  45. package/src/browser/navigation-bridge.ts +317 -560
  46. package/src/browser/navigation-client.ts +206 -68
  47. package/src/browser/navigation-store.ts +73 -55
  48. package/src/browser/navigation-transaction.ts +297 -0
  49. package/src/browser/network-error-handler.ts +61 -0
  50. package/src/browser/partial-update.ts +343 -316
  51. package/src/browser/prefetch/cache.ts +216 -0
  52. package/src/browser/prefetch/fetch.ts +206 -0
  53. package/src/browser/prefetch/observer.ts +65 -0
  54. package/src/browser/prefetch/policy.ts +48 -0
  55. package/src/browser/prefetch/queue.ts +160 -0
  56. package/src/browser/prefetch/resource-ready.ts +77 -0
  57. package/src/browser/rango-state.ts +112 -0
  58. package/src/browser/react/Link.tsx +253 -74
  59. package/src/browser/react/NavigationProvider.tsx +87 -11
  60. package/src/browser/react/context.ts +11 -0
  61. package/src/browser/react/filter-segment-order.ts +11 -0
  62. package/src/browser/react/index.ts +12 -12
  63. package/src/browser/react/location-state-shared.ts +95 -53
  64. package/src/browser/react/location-state.ts +60 -15
  65. package/src/browser/react/mount-context.ts +6 -1
  66. package/src/browser/react/nonce-context.ts +23 -0
  67. package/src/browser/react/shallow-equal.ts +27 -0
  68. package/src/browser/react/use-action.ts +29 -51
  69. package/src/browser/react/use-client-cache.ts +5 -3
  70. package/src/browser/react/use-handle.ts +30 -126
  71. package/src/browser/react/use-href.tsx +2 -2
  72. package/src/browser/react/use-link-status.ts +6 -5
  73. package/src/browser/react/use-navigation.ts +44 -65
  74. package/src/browser/react/use-params.ts +65 -0
  75. package/src/browser/react/use-pathname.ts +47 -0
  76. package/src/browser/react/use-router.ts +76 -0
  77. package/src/browser/react/use-search-params.ts +56 -0
  78. package/src/browser/react/use-segments.ts +80 -97
  79. package/src/browser/response-adapter.ts +73 -0
  80. package/src/browser/rsc-router.tsx +214 -58
  81. package/src/browser/scroll-restoration.ts +127 -52
  82. package/src/browser/segment-reconciler.ts +243 -0
  83. package/src/browser/segment-structure-assert.ts +16 -0
  84. package/src/browser/server-action-bridge.ts +510 -603
  85. package/src/browser/shallow.ts +6 -1
  86. package/src/browser/types.ts +141 -48
  87. package/src/browser/validate-redirect-origin.ts +29 -0
  88. package/src/build/generate-manifest.ts +235 -24
  89. package/src/build/generate-route-types.ts +39 -0
  90. package/src/build/index.ts +13 -0
  91. package/src/build/route-trie.ts +291 -0
  92. package/src/build/route-types/ast-helpers.ts +25 -0
  93. package/src/build/route-types/ast-route-extraction.ts +98 -0
  94. package/src/build/route-types/codegen.ts +102 -0
  95. package/src/build/route-types/include-resolution.ts +418 -0
  96. package/src/build/route-types/param-extraction.ts +48 -0
  97. package/src/build/route-types/per-module-writer.ts +128 -0
  98. package/src/build/route-types/router-processing.ts +618 -0
  99. package/src/build/route-types/scan-filter.ts +85 -0
  100. package/src/build/runtime-discovery.ts +231 -0
  101. package/src/cache/background-task.ts +34 -0
  102. package/src/cache/cache-key-utils.ts +44 -0
  103. package/src/cache/cache-policy.ts +125 -0
  104. package/src/cache/cache-runtime.ts +342 -0
  105. package/src/cache/cache-scope.ts +167 -309
  106. package/src/cache/cf/cf-cache-store.ts +571 -17
  107. package/src/cache/cf/index.ts +13 -3
  108. package/src/cache/document-cache.ts +116 -77
  109. package/src/cache/handle-capture.ts +81 -0
  110. package/src/cache/handle-snapshot.ts +41 -0
  111. package/src/cache/index.ts +1 -15
  112. package/src/cache/memory-segment-store.ts +191 -13
  113. package/src/cache/profile-registry.ts +73 -0
  114. package/src/cache/read-through-swr.ts +134 -0
  115. package/src/cache/segment-codec.ts +256 -0
  116. package/src/cache/taint.ts +153 -0
  117. package/src/cache/types.ts +72 -122
  118. package/src/client.rsc.tsx +3 -1
  119. package/src/client.tsx +135 -301
  120. package/src/component-utils.ts +4 -4
  121. package/src/components/DefaultDocument.tsx +5 -1
  122. package/src/context-var.ts +156 -0
  123. package/src/debug.ts +19 -9
  124. package/src/errors.ts +108 -2
  125. package/src/handle.ts +55 -29
  126. package/src/handles/MetaTags.tsx +73 -20
  127. package/src/handles/breadcrumbs.ts +66 -0
  128. package/src/handles/index.ts +1 -0
  129. package/src/handles/meta.ts +30 -13
  130. package/src/host/cookie-handler.ts +21 -15
  131. package/src/host/errors.ts +8 -8
  132. package/src/host/index.ts +4 -7
  133. package/src/host/pattern-matcher.ts +27 -27
  134. package/src/host/router.ts +61 -39
  135. package/src/host/testing.ts +8 -8
  136. package/src/host/types.ts +15 -7
  137. package/src/host/utils.ts +1 -1
  138. package/src/href-client.ts +119 -29
  139. package/src/index.rsc.ts +155 -19
  140. package/src/index.ts +251 -30
  141. package/src/internal-debug.ts +11 -0
  142. package/src/loader.rsc.ts +26 -157
  143. package/src/loader.ts +27 -10
  144. package/src/network-error-thrower.tsx +3 -1
  145. package/src/outlet-provider.tsx +45 -0
  146. package/src/prerender/param-hash.ts +37 -0
  147. package/src/prerender/store.ts +186 -0
  148. package/src/prerender.ts +524 -0
  149. package/src/reverse.ts +354 -0
  150. package/src/root-error-boundary.tsx +41 -29
  151. package/src/route-content-wrapper.tsx +7 -4
  152. package/src/route-definition/dsl-helpers.ts +1121 -0
  153. package/src/route-definition/helper-factories.ts +200 -0
  154. package/src/route-definition/helpers-types.ts +478 -0
  155. package/src/route-definition/index.ts +55 -0
  156. package/src/route-definition/redirect.ts +101 -0
  157. package/src/route-definition/resolve-handler-use.ts +149 -0
  158. package/src/route-definition.ts +1 -1428
  159. package/src/route-map-builder.ts +217 -123
  160. package/src/route-name.ts +53 -0
  161. package/src/route-types.ts +77 -8
  162. package/src/router/content-negotiation.ts +215 -0
  163. package/src/router/debug-manifest.ts +72 -0
  164. package/src/router/error-handling.ts +9 -9
  165. package/src/router/find-match.ts +160 -0
  166. package/src/router/handler-context.ts +438 -86
  167. package/src/router/intercept-resolution.ts +402 -0
  168. package/src/router/lazy-includes.ts +237 -0
  169. package/src/router/loader-resolution.ts +356 -128
  170. package/src/router/logging.ts +251 -0
  171. package/src/router/manifest.ts +163 -35
  172. package/src/router/match-api.ts +555 -0
  173. package/src/router/match-context.ts +5 -3
  174. package/src/router/match-handlers.ts +440 -0
  175. package/src/router/match-middleware/background-revalidation.ts +108 -93
  176. package/src/router/match-middleware/cache-lookup.ts +460 -10
  177. package/src/router/match-middleware/cache-store.ts +98 -26
  178. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  179. package/src/router/match-middleware/segment-resolution.ts +80 -6
  180. package/src/router/match-pipelines.ts +10 -45
  181. package/src/router/match-result.ts +135 -35
  182. package/src/router/metrics.ts +240 -15
  183. package/src/router/middleware-cookies.ts +55 -0
  184. package/src/router/middleware-types.ts +220 -0
  185. package/src/router/middleware.ts +324 -369
  186. package/src/router/navigation-snapshot.ts +182 -0
  187. package/src/router/pattern-matching.ts +211 -43
  188. package/src/router/prerender-match.ts +502 -0
  189. package/src/router/preview-match.ts +98 -0
  190. package/src/router/request-classification.ts +310 -0
  191. package/src/router/revalidation.ts +137 -38
  192. package/src/router/route-snapshot.ts +245 -0
  193. package/src/router/router-context.ts +41 -21
  194. package/src/router/router-interfaces.ts +484 -0
  195. package/src/router/router-options.ts +618 -0
  196. package/src/router/router-registry.ts +24 -0
  197. package/src/router/segment-resolution/fresh.ts +748 -0
  198. package/src/router/segment-resolution/helpers.ts +268 -0
  199. package/src/router/segment-resolution/loader-cache.ts +199 -0
  200. package/src/router/segment-resolution/revalidation.ts +1379 -0
  201. package/src/router/segment-resolution/static-store.ts +67 -0
  202. package/src/router/segment-resolution.ts +21 -0
  203. package/src/router/segment-wrappers.ts +291 -0
  204. package/src/router/telemetry-otel.ts +299 -0
  205. package/src/router/telemetry.ts +300 -0
  206. package/src/router/timeout.ts +148 -0
  207. package/src/router/trie-matching.ts +239 -0
  208. package/src/router/types.ts +78 -3
  209. package/src/router.ts +740 -4252
  210. package/src/rsc/handler-context.ts +45 -0
  211. package/src/rsc/handler.ts +907 -797
  212. package/src/rsc/helpers.ts +140 -6
  213. package/src/rsc/index.ts +0 -20
  214. package/src/rsc/loader-fetch.ts +229 -0
  215. package/src/rsc/manifest-init.ts +90 -0
  216. package/src/rsc/nonce.ts +14 -0
  217. package/src/rsc/origin-guard.ts +141 -0
  218. package/src/rsc/progressive-enhancement.ts +391 -0
  219. package/src/rsc/response-error.ts +37 -0
  220. package/src/rsc/response-route-handler.ts +347 -0
  221. package/src/rsc/rsc-rendering.ts +246 -0
  222. package/src/rsc/runtime-warnings.ts +42 -0
  223. package/src/rsc/server-action.ts +356 -0
  224. package/src/rsc/ssr-setup.ts +128 -0
  225. package/src/rsc/types.ts +46 -11
  226. package/src/search-params.ts +230 -0
  227. package/src/segment-content-promise.ts +67 -0
  228. package/src/segment-loader-promise.ts +122 -0
  229. package/src/segment-system.tsx +134 -36
  230. package/src/server/context.ts +341 -61
  231. package/src/server/cookie-store.ts +190 -0
  232. package/src/server/fetchable-loader-store.ts +37 -0
  233. package/src/server/handle-store.ts +113 -15
  234. package/src/server/loader-registry.ts +24 -64
  235. package/src/server/request-context.ts +607 -81
  236. package/src/server.ts +35 -130
  237. package/src/ssr/index.tsx +103 -30
  238. package/src/static-handler.ts +126 -0
  239. package/src/theme/ThemeProvider.tsx +21 -15
  240. package/src/theme/ThemeScript.tsx +5 -5
  241. package/src/theme/constants.ts +5 -2
  242. package/src/theme/index.ts +4 -14
  243. package/src/theme/theme-context.ts +4 -30
  244. package/src/theme/theme-script.ts +21 -18
  245. package/src/types/boundaries.ts +158 -0
  246. package/src/types/cache-types.ts +198 -0
  247. package/src/types/error-types.ts +192 -0
  248. package/src/types/global-namespace.ts +100 -0
  249. package/src/types/handler-context.ts +791 -0
  250. package/src/types/index.ts +88 -0
  251. package/src/types/loader-types.ts +210 -0
  252. package/src/types/route-config.ts +170 -0
  253. package/src/types/route-entry.ts +120 -0
  254. package/src/types/segments.ts +150 -0
  255. package/src/types.ts +1 -1623
  256. package/src/urls/include-helper.ts +207 -0
  257. package/src/urls/index.ts +53 -0
  258. package/src/urls/path-helper-types.ts +372 -0
  259. package/src/urls/path-helper.ts +364 -0
  260. package/src/urls/pattern-types.ts +107 -0
  261. package/src/urls/response-types.ts +116 -0
  262. package/src/urls/type-extraction.ts +372 -0
  263. package/src/urls/urls-function.ts +98 -0
  264. package/src/urls.ts +1 -802
  265. package/src/use-loader.tsx +161 -81
  266. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  267. package/src/vite/discovery/discover-routers.ts +348 -0
  268. package/src/vite/discovery/prerender-collection.ts +439 -0
  269. package/src/vite/discovery/route-types-writer.ts +258 -0
  270. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  271. package/src/vite/discovery/state.ts +117 -0
  272. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  273. package/src/vite/index.ts +15 -1133
  274. package/src/vite/plugin-types.ts +103 -0
  275. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  276. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  277. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  278. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  279. package/src/vite/plugins/expose-id-utils.ts +299 -0
  280. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  281. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  282. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  283. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  284. package/src/vite/plugins/expose-ids/types.ts +45 -0
  285. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  286. package/src/vite/plugins/performance-tracks.ts +88 -0
  287. package/src/vite/plugins/refresh-cmd.ts +127 -0
  288. package/src/vite/plugins/use-cache-transform.ts +323 -0
  289. package/src/vite/plugins/version-injector.ts +83 -0
  290. package/src/vite/plugins/version-plugin.ts +266 -0
  291. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  292. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  293. package/src/vite/rango.ts +462 -0
  294. package/src/vite/router-discovery.ts +918 -0
  295. package/src/vite/utils/ast-handler-extract.ts +517 -0
  296. package/src/vite/utils/banner.ts +36 -0
  297. package/src/vite/utils/bundle-analysis.ts +137 -0
  298. package/src/vite/utils/manifest-utils.ts +70 -0
  299. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  300. package/src/vite/utils/prerender-utils.ts +221 -0
  301. package/src/vite/utils/shared-utils.ts +170 -0
  302. package/CLAUDE.md +0 -43
  303. package/src/browser/lru-cache.ts +0 -69
  304. package/src/browser/request-controller.ts +0 -164
  305. package/src/cache/memory-store.ts +0 -253
  306. package/src/href-context.ts +0 -33
  307. package/src/href.ts +0 -255
  308. package/src/server/route-manifest-cache.ts +0 -173
  309. package/src/vite/expose-handle-id.ts +0 -209
  310. package/src/vite/expose-loader-id.ts +0 -426
  311. package/src/vite/expose-location-state-id.ts +0 -177
  312. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Typed context variables for ctx.set() / ctx.get().
3
+ *
4
+ * createVar<T>() produces a typed token that handlers set and layouts/middleware
5
+ * read. The token carries a unique Symbol used as the property key on the
6
+ * per-request variables object — no build-time processing, no IDs.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { createVar } from "@rangojs/router";
11
+ *
12
+ * interface PaginationData { current: number; total: number }
13
+ * export const Pagination = createVar<PaginationData>();
14
+ *
15
+ * // Non-cacheable var — throws if set/get inside cache() or "use cache"
16
+ * export const User = createVar<UserData>({ cache: false });
17
+ *
18
+ * // handler
19
+ * ctx.set(Pagination, { current: 1, total: 4 });
20
+ *
21
+ * // layout
22
+ * const pg = ctx.get(Pagination); // PaginationData | undefined
23
+ * ```
24
+ */
25
+
26
+ export interface ContextVar<T> {
27
+ readonly __brand: "context-var";
28
+ readonly key: symbol;
29
+ /** When false, the var is non-cacheable — throws inside cache() / "use cache" */
30
+ readonly cache: boolean;
31
+ /** Phantom field to carry the type parameter. Never set at runtime. */
32
+ readonly __type?: T;
33
+ }
34
+
35
+ export interface ContextVarOptions {
36
+ /**
37
+ * When false, marks this variable as non-cacheable.
38
+ * Setting or getting this var inside a cache() boundary or "use cache"
39
+ * function will throw. Use for inherently request-specific data (user
40
+ * sessions, auth tokens, etc.) that must never be baked into cached segments.
41
+ *
42
+ * @default true
43
+ */
44
+ cache?: boolean;
45
+ }
46
+
47
+ /**
48
+ * Create a typed context variable token.
49
+ *
50
+ * The returned object is used with ctx.set(token, value) and ctx.get(token)
51
+ * for compile-time-checked data flow between handlers, layouts, and middleware.
52
+ */
53
+ export function createVar<T>(options?: ContextVarOptions): ContextVar<T> {
54
+ return {
55
+ __brand: "context-var" as const,
56
+ key: Symbol(),
57
+ cache: options?.cache !== false,
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Type guard: is the value a ContextVar token?
63
+ */
64
+ export function isContextVar(value: unknown): value is ContextVar<unknown> {
65
+ return (
66
+ typeof value === "object" &&
67
+ value !== null &&
68
+ "__brand" in value &&
69
+ (value as { __brand: unknown }).__brand === "context-var"
70
+ );
71
+ }
72
+
73
+ /**
74
+ * Symbol used as a Set stored on the variables object to track
75
+ * which keys hold non-cacheable values (from write-level { cache: false }).
76
+ */
77
+ const NON_CACHEABLE_KEYS: unique symbol = Symbol.for(
78
+ "rango:non-cacheable-keys",
79
+ ) as any;
80
+
81
+ function getNonCacheableKeys(variables: any): Set<string | symbol> {
82
+ if (!variables[NON_CACHEABLE_KEYS]) {
83
+ variables[NON_CACHEABLE_KEYS] = new Set();
84
+ }
85
+ return variables[NON_CACHEABLE_KEYS];
86
+ }
87
+
88
+ /**
89
+ * Check if a variable value is non-cacheable (either var-level or write-level).
90
+ */
91
+ export function isNonCacheable(
92
+ variables: any,
93
+ keyOrVar: string | ContextVar<any>,
94
+ ): boolean {
95
+ if (typeof keyOrVar !== "string" && !keyOrVar.cache) {
96
+ return true; // var-level policy
97
+ }
98
+ const key = typeof keyOrVar === "string" ? keyOrVar : keyOrVar.key;
99
+ const set = variables[NON_CACHEABLE_KEYS] as Set<string | symbol> | undefined;
100
+ return set?.has(key) ?? false; // write-level policy
101
+ }
102
+
103
+ /**
104
+ * Read a variable from the variables store.
105
+ * Accepts either a string key (legacy) or a ContextVar token (typed).
106
+ */
107
+ export function contextGet(
108
+ variables: any,
109
+ keyOrVar: string | ContextVar<any>,
110
+ ): any {
111
+ if (typeof keyOrVar === "string") return variables[keyOrVar];
112
+ return variables[keyOrVar.key];
113
+ }
114
+
115
+ /** Keys that must never be used as string variable names */
116
+ const FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
117
+
118
+ export interface ContextSetOptions {
119
+ /**
120
+ * When false, marks this specific write as non-cacheable.
121
+ * "Least cacheable wins" — if either the var definition or this option
122
+ * says cache: false, the value is non-cacheable.
123
+ *
124
+ * @default true (inherits from createVar)
125
+ */
126
+ cache?: boolean;
127
+ }
128
+
129
+ /**
130
+ * Write a variable to the variables store.
131
+ * Accepts either a string key (legacy) or a ContextVar token (typed).
132
+ */
133
+ export function contextSet(
134
+ variables: any,
135
+ keyOrVar: string | ContextVar<any>,
136
+ value: any,
137
+ options?: ContextSetOptions,
138
+ ): void {
139
+ if (typeof keyOrVar === "string") {
140
+ if (FORBIDDEN_KEYS.has(keyOrVar)) {
141
+ throw new Error(
142
+ `ctx.set(): "${keyOrVar}" is a reserved key and cannot be used as a variable name.`,
143
+ );
144
+ }
145
+ variables[keyOrVar] = value;
146
+ if (options?.cache === false) {
147
+ getNonCacheableKeys(variables).add(keyOrVar);
148
+ }
149
+ } else {
150
+ variables[keyOrVar.key] = value;
151
+ // Track write-level non-cacheable (var-level is checked via keyOrVar.cache)
152
+ if (options?.cache === false) {
153
+ getNonCacheableKeys(variables).add(keyOrVar.key);
154
+ }
155
+ }
156
+ }
package/src/debug.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Debug utilities for manifest inspection and comparison
3
3
  */
4
4
 
5
- import type { EntryData } from "./server/context";
5
+ import { getParallelSlotCount, type EntryData } from "./server/context";
6
6
 
7
7
  /**
8
8
  * Serialized entry for debug output
@@ -34,7 +34,7 @@ export interface SerializedManifest {
34
34
  * Serialize a manifest Map into a JSON-friendly structure
35
35
  */
36
36
  export function serializeManifest(
37
- manifest: Map<string, EntryData>
37
+ manifest: Map<string, EntryData>,
38
38
  ): SerializedManifest {
39
39
  const routes: Record<string, SerializedEntry> = {};
40
40
  const layouts: Record<string, SerializedEntry> = {};
@@ -64,7 +64,7 @@ export function serializeManifest(
64
64
  hasLoader: entry.loader?.length > 0,
65
65
  hasMiddleware: entry.middleware?.length > 0,
66
66
  hasErrorBoundary: entry.errorBoundary?.length > 0,
67
- parallelCount: entry.parallel?.length ?? 0,
67
+ parallelCount: getParallelSlotCount(entry.parallel),
68
68
  interceptCount: entry.intercept?.length ?? 0,
69
69
  };
70
70
 
@@ -92,7 +92,7 @@ export function serializeManifest(
92
92
  */
93
93
  export function compareManifests(
94
94
  oldManifest: SerializedManifest,
95
- newManifest: SerializedManifest
95
+ newManifest: SerializedManifest,
96
96
  ): {
97
97
  addedRoutes: string[];
98
98
  removedRoutes: string[];
@@ -113,10 +113,20 @@ export function compareManifests(
113
113
  } {
114
114
  const addedRoutes: string[] = [];
115
115
  const removedRoutes: string[] = [];
116
- const changedRoutes: Array<{ key: string; field: string; old: any; new: any }> = [];
116
+ const changedRoutes: Array<{
117
+ key: string;
118
+ field: string;
119
+ old: any;
120
+ new: any;
121
+ }> = [];
117
122
  const addedLayouts: string[] = [];
118
123
  const removedLayouts: string[] = [];
119
- const changedLayouts: Array<{ key: string; field: string; old: any; new: any }> = [];
124
+ const changedLayouts: Array<{
125
+ key: string;
126
+ field: string;
127
+ old: any;
128
+ new: any;
129
+ }> = [];
120
130
 
121
131
  // Compare routes
122
132
  const oldRouteKeys = new Set(Object.keys(oldManifest.routes));
@@ -191,7 +201,7 @@ export function compareManifests(
191
201
  * Format manifest diff as a human-readable string
192
202
  */
193
203
  export function formatManifestDiff(
194
- diff: ReturnType<typeof compareManifests>
204
+ diff: ReturnType<typeof compareManifests>,
195
205
  ): string {
196
206
  const lines: string[] = [];
197
207
 
@@ -208,7 +218,7 @@ export function formatManifestDiff(
208
218
  if (diff.changedRoutes.length > 0) {
209
219
  lines.push("Changed routes:");
210
220
  diff.changedRoutes.forEach((c) =>
211
- lines.push(` ~ ${c.key}.${c.field}: ${c.old} -> ${c.new}`)
221
+ lines.push(` ~ ${c.key}.${c.field}: ${c.old} -> ${c.new}`),
212
222
  );
213
223
  }
214
224
 
@@ -225,7 +235,7 @@ export function formatManifestDiff(
225
235
  if (diff.changedLayouts.length > 0) {
226
236
  lines.push("Changed layouts:");
227
237
  diff.changedLayouts.forEach((c) =>
228
- lines.push(` ~ ${c.key}.${c.field}: ${c.old} -> ${c.new}`)
238
+ lines.push(` ~ ${c.key}.${c.field}: ${c.old} -> ${c.new}`),
229
239
  );
230
240
  }
231
241
 
package/src/errors.ts CHANGED
@@ -22,6 +22,7 @@ export class RouteNotFoundError extends Error {
22
22
 
23
23
  constructor(message: string, options?: ErrorOptions) {
24
24
  super(message);
25
+ Object.setPrototypeOf(this, RouteNotFoundError.prototype);
25
26
  this.cause = options?.cause;
26
27
  }
27
28
  }
@@ -45,6 +46,7 @@ export class DataNotFoundError extends Error {
45
46
 
46
47
  constructor(message: string = "Not found", options?: ErrorOptions) {
47
48
  super(message);
49
+ Object.setPrototypeOf(this, DataNotFoundError.prototype);
48
50
  this.cause = options?.cause;
49
51
  }
50
52
  }
@@ -74,6 +76,7 @@ export class MiddlewareError extends Error {
74
76
 
75
77
  constructor(message: string, options?: ErrorOptions) {
76
78
  super(message);
79
+ Object.setPrototypeOf(this, MiddlewareError.prototype);
77
80
  this.cause = options?.cause;
78
81
  }
79
82
  }
@@ -87,6 +90,7 @@ export class HandlerError extends Error {
87
90
 
88
91
  constructor(message: string, options?: ErrorOptions) {
89
92
  super(message);
93
+ Object.setPrototypeOf(this, HandlerError.prototype);
90
94
  this.cause = options?.cause;
91
95
  }
92
96
  }
@@ -100,6 +104,7 @@ export class BuildError extends Error {
100
104
 
101
105
  constructor(message: string, options?: ErrorOptions) {
102
106
  super(message);
107
+ Object.setPrototypeOf(this, BuildError.prototype);
103
108
  this.cause = options?.cause;
104
109
  }
105
110
  }
@@ -133,9 +138,10 @@ export class NetworkError extends Error {
133
138
  options?: ErrorOptions & {
134
139
  url?: string;
135
140
  operation?: "action" | "navigation" | "revalidation";
136
- }
141
+ },
137
142
  ) {
138
143
  super(message);
144
+ Object.setPrototypeOf(this, NetworkError.prototype);
139
145
  this.cause = options?.cause;
140
146
  this.url = options?.url;
141
147
  this.operation = options?.operation;
@@ -171,6 +177,100 @@ export function isNetworkError(error: unknown): boolean {
171
177
  return false;
172
178
  }
173
179
 
180
+ /**
181
+ * Structured error for JSON response routes.
182
+ * Thrown by handlers to return a typed error envelope with a specific HTTP status.
183
+ *
184
+ * Unlike standard Error, RouterError messages are always exposed to the client
185
+ * (the developer intentionally crafted them for consumers).
186
+ *
187
+ * @example
188
+ * ```typescript
189
+ * path("/products/:id", (ctx) => {
190
+ * const product = products.find(p => p.id === ctx.params.id);
191
+ * if (!product) throw new RouterError("NOT_FOUND", "Product not found", { status: 404 });
192
+ * return product;
193
+ * }, { name: "productDetail" })
194
+ * ```
195
+ */
196
+ export class RouterError extends Error {
197
+ name = "RouterError" as const;
198
+ code: string;
199
+ type?: string;
200
+ status: number;
201
+ cause?: unknown;
202
+
203
+ constructor(
204
+ code: string,
205
+ message: string,
206
+ options?: {
207
+ status?: number;
208
+ type?: string;
209
+ cause?: unknown;
210
+ },
211
+ ) {
212
+ super(message);
213
+ Object.setPrototypeOf(this, RouterError.prototype);
214
+ this.code = code;
215
+ this.status = options?.status ?? 500;
216
+ this.type = options?.type;
217
+ this.cause = options?.cause;
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Thrown inside a Prerender or Static handler at build time to signal
223
+ * "skip this entry, log it, and continue with the rest."
224
+ *
225
+ * When thrown, the entry is excluded from the pre-render manifest
226
+ * but the build continues normally. Regular throws (non-Skip)
227
+ * stop all further pre-rendering and fail the build.
228
+ *
229
+ * @example
230
+ * ```typescript
231
+ * export const BlogPost = Prerender(
232
+ * async () => allPosts.map(p => ({ slug: p.slug })),
233
+ * async (ctx) => {
234
+ * const post = await getPost(ctx.params.slug);
235
+ * if (post.draft) throw new Skip(`"${ctx.params.slug}" is a draft`);
236
+ * return <PostPage post={post} />;
237
+ * },
238
+ * );
239
+ * ```
240
+ */
241
+ export class Skip extends Error {
242
+ name = "Skip" as const;
243
+ cause?: unknown;
244
+
245
+ constructor(message: string = "Entry skipped", options?: ErrorOptions) {
246
+ super(message);
247
+ Object.setPrototypeOf(this, Skip.prototype);
248
+ this.cause = options?.cause;
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Type guard to check if a thrown value is a Skip signal.
254
+ */
255
+ export function isSkip(value: unknown): value is Skip {
256
+ return value instanceof Skip;
257
+ }
258
+
259
+ /**
260
+ * Thrown by the partial updater when the server responds with a redirect payload
261
+ * that carries location state. Caught by navigate() to re-navigate to the
262
+ * redirect target with the server-set state merged into history.pushState.
263
+ *
264
+ * Not an Error subclass -- this is a control flow signal, not a failure.
265
+ */
266
+ export class ServerRedirect {
267
+ readonly name = "ServerRedirect";
268
+ constructor(
269
+ public readonly url: string,
270
+ public readonly state: Record<string, unknown> | undefined,
271
+ ) {}
272
+ }
273
+
174
274
  /**
175
275
  * Thrown when route handler returns invalid type
176
276
  */
@@ -180,6 +280,7 @@ export class InvalidHandlerError extends Error {
180
280
 
181
281
  constructor(message: string, options?: ErrorOptions) {
182
282
  super(message);
283
+ Object.setPrototypeOf(this, InvalidHandlerError.prototype);
183
284
  this.cause = options?.cause;
184
285
  }
185
286
  }
@@ -226,7 +327,12 @@ export function sanitizeError(error: unknown): Response {
226
327
  return error;
227
328
  }
228
329
 
229
- const isDev = (import.meta as any).env?.DEV ?? true;
330
+ // Vite replaces import.meta.env.DEV at compile time. The fallback covers
331
+ // non-Vite environments (plain Node, test runners without Vite transforms).
332
+ // SECURITY: fail closed — default to production when the environment is ambiguous.
333
+ const isDev =
334
+ (import.meta as any).env?.DEV ??
335
+ globalThis.process?.env?.NODE_ENV === "development";
230
336
 
231
337
  if (isDev) {
232
338
  // Development: Send full error details for debugging
package/src/handle.ts CHANGED
@@ -29,24 +29,6 @@ export interface Handle<TData, TAccumulated = TData[]> {
29
29
  * Format: "filePath#ExportName" in dev, "hash#ExportName" in production
30
30
  */
31
31
  readonly $$id: string;
32
-
33
- /**
34
- * Collect function to transform segment data into final value.
35
- * Receives array of arrays - each inner array contains values pushed
36
- * by one segment, ordered parent-to-child.
37
- * Optional because RSC serialization (toJSON) strips it. On the client,
38
- * useHandle() recovers collect from the module-level registry.
39
- *
40
- * @param segments - Array of segment data arrays, e.g. [[a, b], [c], [d, e]]
41
- * @returns The accumulated value
42
- */
43
- readonly collect?: (segments: TData[][]) => TAccumulated;
44
-
45
- /**
46
- * RSC serialization - strips collect function, keeps only brand + id.
47
- * When passed as a prop to a client component, RSC Flight calls toJSON.
48
- */
49
- toJSON?: () => { __brand: "handle"; $$id: string };
50
32
  }
51
33
 
52
34
  /**
@@ -65,14 +47,16 @@ const collectRegistry = new Map<string, (segments: unknown[][]) => unknown>();
65
47
  * Look up a collect function from the registry by handle $$id.
66
48
  * Returns undefined if not registered (falls back to defaultCollect in useHandle).
67
49
  */
68
- export function getCollectFn(id: string): ((segments: unknown[][]) => unknown) | undefined {
50
+ export function getCollectFn(
51
+ id: string,
52
+ ): ((segments: unknown[][]) => unknown) | undefined {
69
53
  return collectRegistry.get(id);
70
54
  }
71
55
 
72
56
  /**
73
57
  * Create a handle definition for accumulating data across route segments.
74
58
  *
75
- * The $$id is auto-generated by the Vite exposeHandleId plugin based on
59
+ * The $$id is auto-generated by the Vite exposeInternalIds plugin based on
76
60
  * file path and export name. No manual naming required.
77
61
  *
78
62
  * @param collect - Optional collect function (default: flatten into array)
@@ -107,32 +91,34 @@ export function getCollectFn(id: string): ((segments: unknown[][]) => unknown) |
107
91
  */
108
92
  export function createHandle<TData, TAccumulated = TData[]>(
109
93
  collect?: (segments: TData[][]) => TAccumulated,
110
- __injectedId?: string
94
+ __injectedId?: string,
111
95
  ): Handle<TData, TAccumulated> {
112
96
  const handleId = __injectedId ?? "";
113
97
 
114
- if (!handleId && process.env.NODE_ENV !== "production") {
115
- console.warn(
98
+ if (!handleId && process.env.NODE_ENV === "development") {
99
+ throw new Error(
116
100
  "[rsc-router] Handle is missing $$id. " +
117
- "Make sure the exposeHandleId Vite plugin is enabled and " +
118
- "the handle is exported with: export const MyHandle = createHandle(...)"
101
+ "Make sure the exposeInternalIds Vite plugin is enabled and " +
102
+ "the handle is exported with: export const MyHandle = createHandle(...)",
119
103
  );
120
104
  }
121
105
 
122
- const collectFn = collect ??
106
+ const collectFn =
107
+ collect ??
123
108
  (defaultCollect as unknown as (segments: TData[][]) => TAccumulated);
124
109
 
125
110
  // Register collect in module-level registry so useHandle() can recover it
126
111
  // when the handle is deserialized from RSC props (toJSON strips collect).
127
112
  if (handleId) {
128
- collectRegistry.set(handleId, collectFn as (segments: unknown[][]) => unknown);
113
+ collectRegistry.set(
114
+ handleId,
115
+ collectFn as (segments: unknown[][]) => unknown,
116
+ );
129
117
  }
130
118
 
131
119
  return {
132
120
  __brand: "handle" as const,
133
121
  $$id: handleId,
134
- collect: collectFn,
135
- toJSON: () => ({ __brand: "handle" as const, $$id: handleId }),
136
122
  };
137
123
  }
138
124
 
@@ -147,3 +133,43 @@ export function isHandle(value: unknown): value is Handle<unknown, unknown> {
147
133
  (value as { __brand: unknown }).__brand === "handle"
148
134
  );
149
135
  }
136
+
137
+ /**
138
+ * Collect handle data from a HandleData map, applying the handle's collect
139
+ * function over segments in order. Shared between server-side rendered()
140
+ * reads and client-side useHandle().
141
+ *
142
+ * @param handle - The handle to collect data for
143
+ * @param data - Full handle data map (handleName -> segmentId -> entries[])
144
+ * @param segmentOrder - Segment IDs in parent -> child resolution order
145
+ */
146
+ export function collectHandleData<TData, TAccumulated>(
147
+ handle: Handle<TData, TAccumulated>,
148
+ data: Record<string, Record<string, unknown[]>>,
149
+ segmentOrder: string[],
150
+ ): TAccumulated {
151
+ const collectFn = getCollectFn(handle.$$id);
152
+ if (!collectFn && process.env.NODE_ENV !== "production") {
153
+ console.warn(
154
+ `[rsc-router] Handle "${handle.$$id}" has no registered collect function. ` +
155
+ `Falling back to flat array. Ensure the handle module is imported so ` +
156
+ `createHandle() runs and registers the collect function.`,
157
+ );
158
+ }
159
+ const collect = (collectFn ??
160
+ (defaultCollect as unknown as (segments: unknown[][]) => unknown)) as (
161
+ segments: TData[][],
162
+ ) => TAccumulated;
163
+
164
+ const segmentData = data[handle.$$id];
165
+ if (!segmentData) return collect([]);
166
+
167
+ const segmentArrays: TData[][] = [];
168
+ for (const segmentId of segmentOrder) {
169
+ const entries = segmentData[segmentId];
170
+ if (entries && entries.length > 0) {
171
+ segmentArrays.push(entries as TData[]);
172
+ }
173
+ }
174
+ return collect(segmentArrays);
175
+ }