@rangojs/router 0.0.0-experimental.002d056c

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 (305) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +899 -0
  3. package/dist/bin/rango.js +1606 -0
  4. package/dist/vite/index.js +5153 -0
  5. package/package.json +177 -0
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +253 -0
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +112 -0
  11. package/skills/document-cache/SKILL.md +182 -0
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +704 -0
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +313 -0
  16. package/skills/layout/SKILL.md +310 -0
  17. package/skills/links/SKILL.md +239 -0
  18. package/skills/loader/SKILL.md +596 -0
  19. package/skills/middleware/SKILL.md +339 -0
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +305 -0
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +118 -0
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +385 -0
  26. package/skills/router-setup/SKILL.md +439 -0
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +79 -0
  29. package/skills/typesafety/SKILL.md +623 -0
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +273 -0
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +899 -0
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/index.ts +18 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +141 -0
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +134 -0
  42. package/src/browser/navigation-bridge.ts +638 -0
  43. package/src/browser/navigation-client.ts +261 -0
  44. package/src/browser/navigation-store.ts +806 -0
  45. package/src/browser/navigation-transaction.ts +297 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +582 -0
  48. package/src/browser/prefetch/cache.ts +206 -0
  49. package/src/browser/prefetch/fetch.ts +145 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +48 -0
  52. package/src/browser/prefetch/queue.ts +128 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +368 -0
  55. package/src/browser/react/NavigationProvider.tsx +413 -0
  56. package/src/browser/react/ScrollRestoration.tsx +94 -0
  57. package/src/browser/react/context.ts +59 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +52 -0
  60. package/src/browser/react/location-state-shared.ts +162 -0
  61. package/src/browser/react/location-state.ts +107 -0
  62. package/src/browser/react/mount-context.ts +37 -0
  63. package/src/browser/react/nonce-context.ts +23 -0
  64. package/src/browser/react/shallow-equal.ts +27 -0
  65. package/src/browser/react/use-action.ts +218 -0
  66. package/src/browser/react/use-client-cache.ts +58 -0
  67. package/src/browser/react/use-handle.ts +162 -0
  68. package/src/browser/react/use-href.tsx +40 -0
  69. package/src/browser/react/use-link-status.ts +135 -0
  70. package/src/browser/react/use-mount.ts +31 -0
  71. package/src/browser/react/use-navigation.ts +99 -0
  72. package/src/browser/react/use-params.ts +65 -0
  73. package/src/browser/react/use-pathname.ts +47 -0
  74. package/src/browser/react/use-router.ts +63 -0
  75. package/src/browser/react/use-search-params.ts +56 -0
  76. package/src/browser/react/use-segments.ts +171 -0
  77. package/src/browser/response-adapter.ts +73 -0
  78. package/src/browser/rsc-router.tsx +464 -0
  79. package/src/browser/scroll-restoration.ts +397 -0
  80. package/src/browser/segment-reconciler.ts +216 -0
  81. package/src/browser/segment-structure-assert.ts +83 -0
  82. package/src/browser/server-action-bridge.ts +667 -0
  83. package/src/browser/shallow.ts +40 -0
  84. package/src/browser/types.ts +547 -0
  85. package/src/browser/validate-redirect-origin.ts +29 -0
  86. package/src/build/generate-manifest.ts +438 -0
  87. package/src/build/generate-route-types.ts +36 -0
  88. package/src/build/index.ts +35 -0
  89. package/src/build/route-trie.ts +265 -0
  90. package/src/build/route-types/ast-helpers.ts +25 -0
  91. package/src/build/route-types/ast-route-extraction.ts +98 -0
  92. package/src/build/route-types/codegen.ts +102 -0
  93. package/src/build/route-types/include-resolution.ts +411 -0
  94. package/src/build/route-types/param-extraction.ts +48 -0
  95. package/src/build/route-types/per-module-writer.ts +128 -0
  96. package/src/build/route-types/router-processing.ts +479 -0
  97. package/src/build/route-types/scan-filter.ts +78 -0
  98. package/src/build/runtime-discovery.ts +231 -0
  99. package/src/cache/background-task.ts +34 -0
  100. package/src/cache/cache-key-utils.ts +44 -0
  101. package/src/cache/cache-policy.ts +125 -0
  102. package/src/cache/cache-runtime.ts +338 -0
  103. package/src/cache/cache-scope.ts +382 -0
  104. package/src/cache/cf/cf-cache-store.ts +982 -0
  105. package/src/cache/cf/index.ts +29 -0
  106. package/src/cache/document-cache.ts +369 -0
  107. package/src/cache/handle-capture.ts +81 -0
  108. package/src/cache/handle-snapshot.ts +41 -0
  109. package/src/cache/index.ts +44 -0
  110. package/src/cache/memory-segment-store.ts +328 -0
  111. package/src/cache/profile-registry.ts +73 -0
  112. package/src/cache/read-through-swr.ts +134 -0
  113. package/src/cache/segment-codec.ts +256 -0
  114. package/src/cache/taint.ts +98 -0
  115. package/src/cache/types.ts +342 -0
  116. package/src/client.rsc.tsx +85 -0
  117. package/src/client.tsx +601 -0
  118. package/src/component-utils.ts +76 -0
  119. package/src/components/DefaultDocument.tsx +27 -0
  120. package/src/context-var.ts +86 -0
  121. package/src/debug.ts +243 -0
  122. package/src/default-error-boundary.tsx +88 -0
  123. package/src/deps/browser.ts +8 -0
  124. package/src/deps/html-stream-client.ts +2 -0
  125. package/src/deps/html-stream-server.ts +2 -0
  126. package/src/deps/rsc.ts +10 -0
  127. package/src/deps/ssr.ts +2 -0
  128. package/src/errors.ts +365 -0
  129. package/src/handle.ts +135 -0
  130. package/src/handles/MetaTags.tsx +246 -0
  131. package/src/handles/breadcrumbs.ts +66 -0
  132. package/src/handles/index.ts +7 -0
  133. package/src/handles/meta.ts +264 -0
  134. package/src/host/cookie-handler.ts +165 -0
  135. package/src/host/errors.ts +97 -0
  136. package/src/host/index.ts +53 -0
  137. package/src/host/pattern-matcher.ts +214 -0
  138. package/src/host/router.ts +352 -0
  139. package/src/host/testing.ts +79 -0
  140. package/src/host/types.ts +146 -0
  141. package/src/host/utils.ts +25 -0
  142. package/src/href-client.ts +222 -0
  143. package/src/index.rsc.ts +233 -0
  144. package/src/index.ts +277 -0
  145. package/src/internal-debug.ts +11 -0
  146. package/src/loader.rsc.ts +89 -0
  147. package/src/loader.ts +64 -0
  148. package/src/network-error-thrower.tsx +23 -0
  149. package/src/outlet-context.ts +15 -0
  150. package/src/outlet-provider.tsx +45 -0
  151. package/src/prerender/param-hash.ts +37 -0
  152. package/src/prerender/store.ts +185 -0
  153. package/src/prerender.ts +463 -0
  154. package/src/reverse.ts +330 -0
  155. package/src/root-error-boundary.tsx +289 -0
  156. package/src/route-content-wrapper.tsx +196 -0
  157. package/src/route-definition/dsl-helpers.ts +934 -0
  158. package/src/route-definition/helper-factories.ts +200 -0
  159. package/src/route-definition/helpers-types.ts +430 -0
  160. package/src/route-definition/index.ts +52 -0
  161. package/src/route-definition/redirect.ts +93 -0
  162. package/src/route-definition.ts +1 -0
  163. package/src/route-map-builder.ts +281 -0
  164. package/src/route-name.ts +53 -0
  165. package/src/route-types.ts +259 -0
  166. package/src/router/content-negotiation.ts +116 -0
  167. package/src/router/debug-manifest.ts +72 -0
  168. package/src/router/error-handling.ts +287 -0
  169. package/src/router/find-match.ts +160 -0
  170. package/src/router/handler-context.ts +451 -0
  171. package/src/router/intercept-resolution.ts +397 -0
  172. package/src/router/lazy-includes.ts +236 -0
  173. package/src/router/loader-resolution.ts +420 -0
  174. package/src/router/logging.ts +251 -0
  175. package/src/router/manifest.ts +269 -0
  176. package/src/router/match-api.ts +620 -0
  177. package/src/router/match-context.ts +266 -0
  178. package/src/router/match-handlers.ts +440 -0
  179. package/src/router/match-middleware/background-revalidation.ts +223 -0
  180. package/src/router/match-middleware/cache-lookup.ts +634 -0
  181. package/src/router/match-middleware/cache-store.ts +295 -0
  182. package/src/router/match-middleware/index.ts +81 -0
  183. package/src/router/match-middleware/intercept-resolution.ts +306 -0
  184. package/src/router/match-middleware/segment-resolution.ts +193 -0
  185. package/src/router/match-pipelines.ts +179 -0
  186. package/src/router/match-result.ts +219 -0
  187. package/src/router/metrics.ts +282 -0
  188. package/src/router/middleware-cookies.ts +55 -0
  189. package/src/router/middleware-types.ts +222 -0
  190. package/src/router/middleware.ts +749 -0
  191. package/src/router/pattern-matching.ts +563 -0
  192. package/src/router/prerender-match.ts +402 -0
  193. package/src/router/preview-match.ts +170 -0
  194. package/src/router/revalidation.ts +289 -0
  195. package/src/router/router-context.ts +320 -0
  196. package/src/router/router-interfaces.ts +452 -0
  197. package/src/router/router-options.ts +592 -0
  198. package/src/router/router-registry.ts +24 -0
  199. package/src/router/segment-resolution/fresh.ts +570 -0
  200. package/src/router/segment-resolution/helpers.ts +263 -0
  201. package/src/router/segment-resolution/loader-cache.ts +198 -0
  202. package/src/router/segment-resolution/revalidation.ts +1242 -0
  203. package/src/router/segment-resolution/static-store.ts +67 -0
  204. package/src/router/segment-resolution.ts +21 -0
  205. package/src/router/segment-wrappers.ts +291 -0
  206. package/src/router/telemetry-otel.ts +299 -0
  207. package/src/router/telemetry.ts +300 -0
  208. package/src/router/timeout.ts +148 -0
  209. package/src/router/trie-matching.ts +239 -0
  210. package/src/router/types.ts +170 -0
  211. package/src/router.ts +1006 -0
  212. package/src/rsc/handler-context.ts +45 -0
  213. package/src/rsc/handler.ts +1089 -0
  214. package/src/rsc/helpers.ts +198 -0
  215. package/src/rsc/index.ts +36 -0
  216. package/src/rsc/loader-fetch.ts +209 -0
  217. package/src/rsc/manifest-init.ts +86 -0
  218. package/src/rsc/nonce.ts +32 -0
  219. package/src/rsc/origin-guard.ts +141 -0
  220. package/src/rsc/progressive-enhancement.ts +379 -0
  221. package/src/rsc/response-error.ts +37 -0
  222. package/src/rsc/response-route-handler.ts +347 -0
  223. package/src/rsc/rsc-rendering.ts +237 -0
  224. package/src/rsc/runtime-warnings.ts +42 -0
  225. package/src/rsc/server-action.ts +348 -0
  226. package/src/rsc/ssr-setup.ts +128 -0
  227. package/src/rsc/types.ts +263 -0
  228. package/src/search-params.ts +230 -0
  229. package/src/segment-system.tsx +454 -0
  230. package/src/server/context.ts +591 -0
  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 +308 -0
  234. package/src/server/loader-registry.ts +133 -0
  235. package/src/server/request-context.ts +920 -0
  236. package/src/server/root-layout.tsx +10 -0
  237. package/src/server/tsconfig.json +14 -0
  238. package/src/server.ts +51 -0
  239. package/src/ssr/index.tsx +365 -0
  240. package/src/static-handler.ts +114 -0
  241. package/src/theme/ThemeProvider.tsx +297 -0
  242. package/src/theme/ThemeScript.tsx +61 -0
  243. package/src/theme/constants.ts +62 -0
  244. package/src/theme/index.ts +48 -0
  245. package/src/theme/theme-context.ts +44 -0
  246. package/src/theme/theme-script.ts +155 -0
  247. package/src/theme/types.ts +182 -0
  248. package/src/theme/use-theme.ts +44 -0
  249. package/src/types/boundaries.ts +158 -0
  250. package/src/types/cache-types.ts +198 -0
  251. package/src/types/error-types.ts +192 -0
  252. package/src/types/global-namespace.ts +100 -0
  253. package/src/types/handler-context.ts +687 -0
  254. package/src/types/index.ts +88 -0
  255. package/src/types/loader-types.ts +183 -0
  256. package/src/types/route-config.ts +170 -0
  257. package/src/types/route-entry.ts +109 -0
  258. package/src/types/segments.ts +148 -0
  259. package/src/types.ts +1 -0
  260. package/src/urls/include-helper.ts +197 -0
  261. package/src/urls/index.ts +53 -0
  262. package/src/urls/path-helper-types.ts +339 -0
  263. package/src/urls/path-helper.ts +329 -0
  264. package/src/urls/pattern-types.ts +95 -0
  265. package/src/urls/response-types.ts +106 -0
  266. package/src/urls/type-extraction.ts +372 -0
  267. package/src/urls/urls-function.ts +98 -0
  268. package/src/urls.ts +1 -0
  269. package/src/use-loader.tsx +354 -0
  270. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  271. package/src/vite/discovery/discover-routers.ts +344 -0
  272. package/src/vite/discovery/prerender-collection.ts +385 -0
  273. package/src/vite/discovery/route-types-writer.ts +258 -0
  274. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  275. package/src/vite/discovery/state.ts +108 -0
  276. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  277. package/src/vite/index.ts +16 -0
  278. package/src/vite/plugin-types.ts +48 -0
  279. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  280. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  281. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  282. package/src/vite/plugins/expose-action-id.ts +363 -0
  283. package/src/vite/plugins/expose-id-utils.ts +287 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  286. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  287. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  288. package/src/vite/plugins/expose-ids/types.ts +45 -0
  289. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  290. package/src/vite/plugins/refresh-cmd.ts +65 -0
  291. package/src/vite/plugins/use-cache-transform.ts +323 -0
  292. package/src/vite/plugins/version-injector.ts +83 -0
  293. package/src/vite/plugins/version-plugin.ts +266 -0
  294. package/src/vite/plugins/version.d.ts +12 -0
  295. package/src/vite/plugins/virtual-entries.ts +123 -0
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +445 -0
  298. package/src/vite/router-discovery.ts +777 -0
  299. package/src/vite/utils/ast-handler-extract.ts +517 -0
  300. package/src/vite/utils/banner.ts +36 -0
  301. package/src/vite/utils/bundle-analysis.ts +137 -0
  302. package/src/vite/utils/manifest-utils.ts +70 -0
  303. package/src/vite/utils/package-resolution.ts +121 -0
  304. package/src/vite/utils/prerender-utils.ts +189 -0
  305. package/src/vite/utils/shared-utils.ts +169 -0
package/src/errors.ts ADDED
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Custom error classes for RSC Router
3
+ *
4
+ * All errors include:
5
+ * - Descriptive names for easy identification
6
+ * - Cause property for structured debugging data
7
+ */
8
+
9
+ /**
10
+ * Options for error construction
11
+ */
12
+ interface ErrorOptions {
13
+ cause?: unknown;
14
+ }
15
+
16
+ /**
17
+ * Thrown when no route matches the requested pathname
18
+ */
19
+ export class RouteNotFoundError extends Error {
20
+ name = "RouteNotFoundError" as const;
21
+ cause?: unknown;
22
+
23
+ constructor(message: string, options?: ErrorOptions) {
24
+ super(message);
25
+ Object.setPrototypeOf(this, RouteNotFoundError.prototype);
26
+ this.cause = options?.cause;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Thrown when data is not found (e.g., product with ID doesn't exist)
32
+ * Use this in handlers/loaders to trigger the nearest notFoundBoundary
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * route("products.detail", async (ctx) => {
37
+ * const product = await db.products.get(ctx.params.slug);
38
+ * if (!product) throw new DataNotFoundError("Product not found");
39
+ * return <ProductDetail product={product} />;
40
+ * });
41
+ * ```
42
+ */
43
+ export class DataNotFoundError extends Error {
44
+ name = "DataNotFoundError" as const;
45
+ cause?: unknown;
46
+
47
+ constructor(message: string = "Not found", options?: ErrorOptions) {
48
+ super(message);
49
+ Object.setPrototypeOf(this, DataNotFoundError.prototype);
50
+ this.cause = options?.cause;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Convenience function to throw a DataNotFoundError
56
+ * Shorter syntax for common not-found scenarios
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * const product = await db.products.get(slug);
61
+ * if (!product) throw notFound("Product not found");
62
+ * // or simply:
63
+ * if (!product) throw notFound();
64
+ * ```
65
+ */
66
+ export function notFound(message?: string): never {
67
+ throw new DataNotFoundError(message);
68
+ }
69
+
70
+ /**
71
+ * Thrown when middleware execution fails
72
+ */
73
+ export class MiddlewareError extends Error {
74
+ name = "MiddlewareError" as const;
75
+ cause?: unknown;
76
+
77
+ constructor(message: string, options?: ErrorOptions) {
78
+ super(message);
79
+ Object.setPrototypeOf(this, MiddlewareError.prototype);
80
+ this.cause = options?.cause;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Thrown when a route handler fails
86
+ */
87
+ export class HandlerError extends Error {
88
+ name = "HandlerError" as const;
89
+ cause?: unknown;
90
+
91
+ constructor(message: string, options?: ErrorOptions) {
92
+ super(message);
93
+ Object.setPrototypeOf(this, HandlerError.prototype);
94
+ this.cause = options?.cause;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Thrown when segment building fails
100
+ */
101
+ export class BuildError extends Error {
102
+ name = "BuildError" as const;
103
+ cause?: unknown;
104
+
105
+ constructor(message: string, options?: ErrorOptions) {
106
+ super(message);
107
+ Object.setPrototypeOf(this, BuildError.prototype);
108
+ this.cause = options?.cause;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Thrown when a network request fails (server unreachable, no internet, etc.)
114
+ * This error triggers the root error boundary with retry capability.
115
+ *
116
+ * @example
117
+ * ```typescript
118
+ * try {
119
+ * await fetch(url);
120
+ * } catch (error) {
121
+ * if (error instanceof TypeError) {
122
+ * throw new NetworkError("Unable to connect to server", { cause: error });
123
+ * }
124
+ * throw error;
125
+ * }
126
+ * ```
127
+ */
128
+ export class NetworkError extends Error {
129
+ name = "NetworkError" as const;
130
+ cause?: unknown;
131
+ /** The URL that failed to fetch */
132
+ url?: string;
133
+ /** Whether this was during an action, navigation, or revalidation */
134
+ operation?: "action" | "navigation" | "revalidation";
135
+
136
+ constructor(
137
+ message: string = "Network request failed",
138
+ options?: ErrorOptions & {
139
+ url?: string;
140
+ operation?: "action" | "navigation" | "revalidation";
141
+ },
142
+ ) {
143
+ super(message);
144
+ Object.setPrototypeOf(this, NetworkError.prototype);
145
+ this.cause = options?.cause;
146
+ this.url = options?.url;
147
+ this.operation = options?.operation;
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Check if an error is a network-level failure (server unreachable, no internet)
153
+ * These are typically TypeError from fetch when the network request itself fails.
154
+ */
155
+ export function isNetworkError(error: unknown): boolean {
156
+ // NetworkError we throw ourselves
157
+ if (error instanceof NetworkError) {
158
+ return true;
159
+ }
160
+
161
+ // TypeError from fetch() when network fails (e.g., "Failed to fetch")
162
+ if (error instanceof TypeError) {
163
+ const message = error.message.toLowerCase();
164
+ return (
165
+ message.includes("failed to fetch") ||
166
+ message.includes("network request failed") ||
167
+ message.includes("networkerror") ||
168
+ message.includes("load failed")
169
+ );
170
+ }
171
+
172
+ // DOMException with network-related names
173
+ if (error instanceof DOMException) {
174
+ return error.name === "NetworkError";
175
+ }
176
+
177
+ return false;
178
+ }
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
+
274
+ /**
275
+ * Thrown when route handler returns invalid type
276
+ */
277
+ export class InvalidHandlerError extends Error {
278
+ name = "InvalidHandlerError" as const;
279
+ cause?: unknown;
280
+
281
+ constructor(message: string, options?: ErrorOptions) {
282
+ super(message);
283
+ Object.setPrototypeOf(this, InvalidHandlerError.prototype);
284
+ this.cause = options?.cause;
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Internal invariant assertion for development-time checks
290
+ *
291
+ * @internal
292
+ * @param condition - Condition that must be true
293
+ * @param message - Error message to throw if condition is false
294
+ * @throws {Error} If condition is false
295
+ *
296
+ * @example
297
+ * ```typescript
298
+ * invariant(user !== null, 'User must be defined');
299
+ * invariant(node.type === 'layout', `Expected layout, got ${node.type}`);
300
+ * ```
301
+ */
302
+ export function invariant(condition: any, message: string): asserts condition {
303
+ if (!condition) {
304
+ throw new Error(`Invariant: ${message}`);
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Sanitize errors for production - prevents leaking sensitive information
310
+ *
311
+ * SECURITY CRITICAL:
312
+ * - In production: NEVER send stack traces, file paths, or internal state
313
+ * - In development: Show full error details for debugging
314
+ * - ALWAYS consume error.stack to prevent memory leaks
315
+ *
316
+ * @param error - Error to sanitize
317
+ * @returns Response suitable for sending to client
318
+ */
319
+ export function sanitizeError(error: unknown): Response {
320
+ // CRITICAL: Consume stack trace to prevent memory leaks
321
+ if (error && typeof error === "object" && "stack" in error) {
322
+ const _ = error.stack; // Force V8 to generate/consume stack trace
323
+ }
324
+
325
+ // Response objects are safe to return as-is
326
+ if (error instanceof Response) {
327
+ return error;
328
+ }
329
+
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";
336
+
337
+ if (isDev) {
338
+ // Development: Send full error details for debugging
339
+ const errorDetails = {
340
+ name: error instanceof Error ? error.name : "Error",
341
+ message: error instanceof Error ? error.message : String(error),
342
+ stack: error instanceof Error ? error.stack : undefined,
343
+ cause:
344
+ error && typeof error === "object" && "cause" in error
345
+ ? (error as any).cause
346
+ : undefined,
347
+ };
348
+
349
+ return new Response(JSON.stringify(errorDetails, null, 2), {
350
+ status: 500,
351
+ headers: {
352
+ "Content-Type": "application/json",
353
+ },
354
+ });
355
+ }
356
+
357
+ // Production: Generic error only - NO internal details
358
+ // SECURITY: Never leak stack traces, file paths, or application state
359
+ return new Response("Internal Server Error", {
360
+ status: 500,
361
+ headers: {
362
+ "Content-Type": "text/plain",
363
+ },
364
+ });
365
+ }
package/src/handle.ts ADDED
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Handle definition for accumulating data across route segments.
3
+ *
4
+ * Handles allow server-side route handlers to pass accumulated data to client
5
+ * components. Unlike loaders (which fetch data for specific routes), handles
6
+ * accumulate data across all matched route segments.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * // Define a handle (name auto-generated from file + export)
11
+ * export const Breadcrumbs = createHandle<BreadcrumbItem>();
12
+ *
13
+ * // Use in handler
14
+ * const push = ctx.use(Breadcrumbs);
15
+ * push({ label: "Home", href: "/" });
16
+ *
17
+ * // Consume on client
18
+ * const crumbs = useHandle(Breadcrumbs);
19
+ * ```
20
+ */
21
+ export interface Handle<TData, TAccumulated = TData[]> {
22
+ /**
23
+ * Brand to distinguish handles from loaders in ctx.use()
24
+ */
25
+ readonly __brand: "handle";
26
+
27
+ /**
28
+ * Auto-generated unique ID for this handle (used as key in storage)
29
+ * Format: "filePath#ExportName" in dev, "hash#ExportName" in production
30
+ */
31
+ readonly $$id: string;
32
+ }
33
+
34
+ /**
35
+ * Default collect function that flattens segment arrays into a single array.
36
+ */
37
+ function defaultCollect<T>(segments: T[][]): T[] {
38
+ return segments.flat();
39
+ }
40
+
41
+ // Module-level registry mapping $$id to collect functions.
42
+ // Populated when createHandle() runs (both server and client).
43
+ // Used by useHandle() to recover collect when handle is deserialized from RSC prop.
44
+ const collectRegistry = new Map<string, (segments: unknown[][]) => unknown>();
45
+
46
+ /**
47
+ * Look up a collect function from the registry by handle $$id.
48
+ * Returns undefined if not registered (falls back to defaultCollect in useHandle).
49
+ */
50
+ export function getCollectFn(
51
+ id: string,
52
+ ): ((segments: unknown[][]) => unknown) | undefined {
53
+ return collectRegistry.get(id);
54
+ }
55
+
56
+ /**
57
+ * Create a handle definition for accumulating data across route segments.
58
+ *
59
+ * The $$id is auto-generated by the Vite exposeInternalIds plugin based on
60
+ * file path and export name. No manual naming required.
61
+ *
62
+ * @param collect - Optional collect function (default: flatten into array)
63
+ * @param __injectedId - Auto-injected by Vite plugin, do not provide manually
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * // Default: flatten into array
68
+ * export const Breadcrumbs = createHandle<BreadcrumbItem>();
69
+ * // Result type: BreadcrumbItem[]
70
+ *
71
+ * // Custom: last value wins
72
+ * export const PageTitle = createHandle<string, string>(
73
+ * (segments) => segments.flat().at(-1) ?? "Default Title"
74
+ * );
75
+ * // Result type: string
76
+ *
77
+ * // Custom: object merge
78
+ * export const Meta = createHandle<Partial<MetaTags>, MetaTags>(
79
+ * (segments) => Object.assign({ robots: "index,follow" }, ...segments.flat())
80
+ * );
81
+ * // Result type: MetaTags
82
+ *
83
+ * // Custom: dedupe by href
84
+ * export const Breadcrumbs = createHandle<BreadcrumbItem>(
85
+ * (segments) => {
86
+ * const all = segments.flat();
87
+ * return all.filter((item, i) => all.findIndex(x => x.href === item.href) === i);
88
+ * }
89
+ * );
90
+ * ```
91
+ */
92
+ export function createHandle<TData, TAccumulated = TData[]>(
93
+ collect?: (segments: TData[][]) => TAccumulated,
94
+ __injectedId?: string,
95
+ ): Handle<TData, TAccumulated> {
96
+ const handleId = __injectedId ?? "";
97
+
98
+ if (!handleId && process.env.NODE_ENV === "development") {
99
+ throw new Error(
100
+ "[rsc-router] Handle is missing $$id. " +
101
+ "Make sure the exposeInternalIds Vite plugin is enabled and " +
102
+ "the handle is exported with: export const MyHandle = createHandle(...)",
103
+ );
104
+ }
105
+
106
+ const collectFn =
107
+ collect ??
108
+ (defaultCollect as unknown as (segments: TData[][]) => TAccumulated);
109
+
110
+ // Register collect in module-level registry so useHandle() can recover it
111
+ // when the handle is deserialized from RSC props (toJSON strips collect).
112
+ if (handleId) {
113
+ collectRegistry.set(
114
+ handleId,
115
+ collectFn as (segments: unknown[][]) => unknown,
116
+ );
117
+ }
118
+
119
+ return {
120
+ __brand: "handle" as const,
121
+ $$id: handleId,
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Type guard to check if a value is a Handle.
127
+ */
128
+ export function isHandle(value: unknown): value is Handle<unknown, unknown> {
129
+ return (
130
+ typeof value === "object" &&
131
+ value !== null &&
132
+ "__brand" in value &&
133
+ (value as { __brand: unknown }).__brand === "handle"
134
+ );
135
+ }