@rangojs/router 0.0.0-experimental.10

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 (172) hide show
  1. package/CLAUDE.md +43 -0
  2. package/README.md +19 -0
  3. package/dist/bin/rango.js +227 -0
  4. package/dist/vite/index.js +3039 -0
  5. package/package.json +171 -0
  6. package/skills/caching/SKILL.md +191 -0
  7. package/skills/debug-manifest/SKILL.md +108 -0
  8. package/skills/document-cache/SKILL.md +180 -0
  9. package/skills/fonts/SKILL.md +165 -0
  10. package/skills/hooks/SKILL.md +442 -0
  11. package/skills/intercept/SKILL.md +190 -0
  12. package/skills/layout/SKILL.md +213 -0
  13. package/skills/links/SKILL.md +180 -0
  14. package/skills/loader/SKILL.md +246 -0
  15. package/skills/middleware/SKILL.md +202 -0
  16. package/skills/mime-routes/SKILL.md +124 -0
  17. package/skills/parallel/SKILL.md +228 -0
  18. package/skills/prerender/SKILL.md +283 -0
  19. package/skills/rango/SKILL.md +54 -0
  20. package/skills/response-routes/SKILL.md +358 -0
  21. package/skills/route/SKILL.md +173 -0
  22. package/skills/router-setup/SKILL.md +346 -0
  23. package/skills/tailwind/SKILL.md +129 -0
  24. package/skills/theme/SKILL.md +78 -0
  25. package/skills/typesafety/SKILL.md +394 -0
  26. package/src/__internal.ts +175 -0
  27. package/src/bin/rango.ts +24 -0
  28. package/src/browser/event-controller.ts +876 -0
  29. package/src/browser/index.ts +18 -0
  30. package/src/browser/link-interceptor.ts +121 -0
  31. package/src/browser/lru-cache.ts +69 -0
  32. package/src/browser/merge-segment-loaders.ts +126 -0
  33. package/src/browser/navigation-bridge.ts +913 -0
  34. package/src/browser/navigation-client.ts +165 -0
  35. package/src/browser/navigation-store.ts +823 -0
  36. package/src/browser/partial-update.ts +600 -0
  37. package/src/browser/react/Link.tsx +248 -0
  38. package/src/browser/react/NavigationProvider.tsx +346 -0
  39. package/src/browser/react/ScrollRestoration.tsx +94 -0
  40. package/src/browser/react/context.ts +53 -0
  41. package/src/browser/react/index.ts +52 -0
  42. package/src/browser/react/location-state-shared.ts +120 -0
  43. package/src/browser/react/location-state.ts +62 -0
  44. package/src/browser/react/mount-context.ts +32 -0
  45. package/src/browser/react/use-action.ts +240 -0
  46. package/src/browser/react/use-client-cache.ts +56 -0
  47. package/src/browser/react/use-handle.ts +203 -0
  48. package/src/browser/react/use-href.tsx +40 -0
  49. package/src/browser/react/use-link-status.ts +134 -0
  50. package/src/browser/react/use-mount.ts +31 -0
  51. package/src/browser/react/use-navigation.ts +140 -0
  52. package/src/browser/react/use-segments.ts +188 -0
  53. package/src/browser/request-controller.ts +164 -0
  54. package/src/browser/rsc-router.tsx +352 -0
  55. package/src/browser/scroll-restoration.ts +324 -0
  56. package/src/browser/segment-structure-assert.ts +67 -0
  57. package/src/browser/server-action-bridge.ts +762 -0
  58. package/src/browser/shallow.ts +35 -0
  59. package/src/browser/types.ts +478 -0
  60. package/src/build/generate-manifest.ts +377 -0
  61. package/src/build/generate-route-types.ts +828 -0
  62. package/src/build/index.ts +36 -0
  63. package/src/build/route-trie.ts +239 -0
  64. package/src/cache/cache-scope.ts +563 -0
  65. package/src/cache/cf/cf-cache-store.ts +428 -0
  66. package/src/cache/cf/index.ts +19 -0
  67. package/src/cache/document-cache.ts +340 -0
  68. package/src/cache/index.ts +58 -0
  69. package/src/cache/memory-segment-store.ts +150 -0
  70. package/src/cache/memory-store.ts +253 -0
  71. package/src/cache/types.ts +392 -0
  72. package/src/client.rsc.tsx +83 -0
  73. package/src/client.tsx +643 -0
  74. package/src/component-utils.ts +76 -0
  75. package/src/components/DefaultDocument.tsx +23 -0
  76. package/src/debug.ts +233 -0
  77. package/src/default-error-boundary.tsx +88 -0
  78. package/src/deps/browser.ts +8 -0
  79. package/src/deps/html-stream-client.ts +2 -0
  80. package/src/deps/html-stream-server.ts +2 -0
  81. package/src/deps/rsc.ts +10 -0
  82. package/src/deps/ssr.ts +2 -0
  83. package/src/errors.ts +295 -0
  84. package/src/handle.ts +130 -0
  85. package/src/handles/MetaTags.tsx +193 -0
  86. package/src/handles/index.ts +6 -0
  87. package/src/handles/meta.ts +247 -0
  88. package/src/host/cookie-handler.ts +159 -0
  89. package/src/host/errors.ts +97 -0
  90. package/src/host/index.ts +56 -0
  91. package/src/host/pattern-matcher.ts +214 -0
  92. package/src/host/router.ts +330 -0
  93. package/src/host/testing.ts +79 -0
  94. package/src/host/types.ts +138 -0
  95. package/src/host/utils.ts +25 -0
  96. package/src/href-client.ts +202 -0
  97. package/src/href-context.ts +33 -0
  98. package/src/index.rsc.ts +121 -0
  99. package/src/index.ts +165 -0
  100. package/src/loader.rsc.ts +207 -0
  101. package/src/loader.ts +47 -0
  102. package/src/network-error-thrower.tsx +21 -0
  103. package/src/outlet-context.ts +15 -0
  104. package/src/prerender/param-hash.ts +35 -0
  105. package/src/prerender/store.ts +40 -0
  106. package/src/prerender.ts +156 -0
  107. package/src/reverse.ts +267 -0
  108. package/src/root-error-boundary.tsx +277 -0
  109. package/src/route-content-wrapper.tsx +193 -0
  110. package/src/route-definition.ts +1431 -0
  111. package/src/route-map-builder.ts +242 -0
  112. package/src/route-types.ts +220 -0
  113. package/src/router/error-handling.ts +287 -0
  114. package/src/router/handler-context.ts +158 -0
  115. package/src/router/intercept-resolution.ts +387 -0
  116. package/src/router/loader-resolution.ts +327 -0
  117. package/src/router/manifest.ts +216 -0
  118. package/src/router/match-api.ts +621 -0
  119. package/src/router/match-context.ts +264 -0
  120. package/src/router/match-middleware/background-revalidation.ts +236 -0
  121. package/src/router/match-middleware/cache-lookup.ts +382 -0
  122. package/src/router/match-middleware/cache-store.ts +276 -0
  123. package/src/router/match-middleware/index.ts +81 -0
  124. package/src/router/match-middleware/intercept-resolution.ts +281 -0
  125. package/src/router/match-middleware/segment-resolution.ts +184 -0
  126. package/src/router/match-pipelines.ts +214 -0
  127. package/src/router/match-result.ts +213 -0
  128. package/src/router/metrics.ts +62 -0
  129. package/src/router/middleware.ts +791 -0
  130. package/src/router/pattern-matching.ts +407 -0
  131. package/src/router/revalidation.ts +190 -0
  132. package/src/router/router-context.ts +301 -0
  133. package/src/router/segment-resolution.ts +1315 -0
  134. package/src/router/trie-matching.ts +172 -0
  135. package/src/router/types.ts +163 -0
  136. package/src/router.gen.ts +6 -0
  137. package/src/router.ts +2423 -0
  138. package/src/rsc/handler.ts +1443 -0
  139. package/src/rsc/helpers.ts +64 -0
  140. package/src/rsc/index.ts +56 -0
  141. package/src/rsc/nonce.ts +18 -0
  142. package/src/rsc/types.ts +236 -0
  143. package/src/segment-system.tsx +442 -0
  144. package/src/server/context.ts +466 -0
  145. package/src/server/handle-store.ts +229 -0
  146. package/src/server/loader-registry.ts +174 -0
  147. package/src/server/request-context.ts +554 -0
  148. package/src/server/root-layout.tsx +10 -0
  149. package/src/server/tsconfig.json +14 -0
  150. package/src/server.ts +171 -0
  151. package/src/ssr/index.tsx +296 -0
  152. package/src/theme/ThemeProvider.tsx +291 -0
  153. package/src/theme/ThemeScript.tsx +61 -0
  154. package/src/theme/constants.ts +59 -0
  155. package/src/theme/index.ts +58 -0
  156. package/src/theme/theme-context.ts +70 -0
  157. package/src/theme/theme-script.ts +152 -0
  158. package/src/theme/types.ts +182 -0
  159. package/src/theme/use-theme.ts +44 -0
  160. package/src/types.ts +1757 -0
  161. package/src/urls.gen.ts +8 -0
  162. package/src/urls.ts +1282 -0
  163. package/src/use-loader.tsx +346 -0
  164. package/src/vite/expose-action-id.ts +344 -0
  165. package/src/vite/expose-handle-id.ts +209 -0
  166. package/src/vite/expose-loader-id.ts +426 -0
  167. package/src/vite/expose-location-state-id.ts +177 -0
  168. package/src/vite/expose-prerender-handler-id.ts +429 -0
  169. package/src/vite/index.ts +2068 -0
  170. package/src/vite/package-resolution.ts +125 -0
  171. package/src/vite/version.d.ts +12 -0
  172. package/src/vite/virtual-entries.ts +114 -0
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ import type { ReactNode, ReactElement } from "react";
4
+ import { MetaTags } from "../handles/MetaTags.js";
5
+
6
+ /**
7
+ * Default document component that provides a basic HTML structure.
8
+ * Used when no custom document is provided to createRouter.
9
+ * Includes MetaTags for automatic charset, viewport, and route meta support.
10
+ *
11
+ * Uses suppressHydrationWarning on <html> because the theme script
12
+ * may modify class/style attributes before React hydrates.
13
+ */
14
+ export function DefaultDocument({ children }: { children: ReactNode }): ReactElement {
15
+ return (
16
+ <html lang="en" suppressHydrationWarning>
17
+ <head>
18
+ <MetaTags />
19
+ </head>
20
+ <body>{children}</body>
21
+ </html>
22
+ );
23
+ }
package/src/debug.ts ADDED
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Debug utilities for manifest inspection and comparison
3
+ */
4
+
5
+ import type { EntryData } from "./server/context";
6
+
7
+ /**
8
+ * Serialized entry for debug output
9
+ */
10
+ export interface SerializedEntry {
11
+ id: string;
12
+ shortCode: string;
13
+ type: string;
14
+ parentShortCode: string | null;
15
+ pattern?: string;
16
+ hasLoader: boolean;
17
+ hasMiddleware: boolean;
18
+ hasErrorBoundary: boolean;
19
+ parallelCount: number;
20
+ interceptCount: number;
21
+ }
22
+
23
+ /**
24
+ * Serialized manifest structure
25
+ */
26
+ export interface SerializedManifest {
27
+ routes: Record<string, SerializedEntry>;
28
+ layouts: Record<string, SerializedEntry>;
29
+ totalRoutes: number;
30
+ totalLayouts: number;
31
+ }
32
+
33
+ /**
34
+ * Serialize a manifest Map into a JSON-friendly structure
35
+ */
36
+ export function serializeManifest(
37
+ manifest: Map<string, EntryData>
38
+ ): SerializedManifest {
39
+ const routes: Record<string, SerializedEntry> = {};
40
+ const layouts: Record<string, SerializedEntry> = {};
41
+
42
+ // Collect all entries including parents
43
+ const allEntries = new Map<string, EntryData>();
44
+
45
+ for (const [key, entry] of manifest.entries()) {
46
+ allEntries.set(key, entry);
47
+
48
+ // Walk up parent chain to collect all layouts
49
+ let parent = entry.parent;
50
+ while (parent) {
51
+ if (!allEntries.has(parent.id)) {
52
+ allEntries.set(parent.id, parent);
53
+ }
54
+ parent = parent.parent;
55
+ }
56
+ }
57
+
58
+ for (const [key, entry] of allEntries.entries()) {
59
+ const serialized: SerializedEntry = {
60
+ id: entry.id,
61
+ shortCode: entry.shortCode,
62
+ type: entry.type,
63
+ parentShortCode: entry.parent?.shortCode ?? null,
64
+ hasLoader: entry.loader?.length > 0,
65
+ hasMiddleware: entry.middleware?.length > 0,
66
+ hasErrorBoundary: entry.errorBoundary?.length > 0,
67
+ parallelCount: entry.parallel?.length ?? 0,
68
+ interceptCount: entry.intercept?.length ?? 0,
69
+ };
70
+
71
+ if (entry.type === "route" && "pattern" in entry) {
72
+ serialized.pattern = entry.pattern;
73
+ }
74
+
75
+ if (entry.type === "route") {
76
+ routes[key] = serialized;
77
+ } else if (entry.type === "layout" || entry.type === "cache") {
78
+ layouts[key] = serialized;
79
+ }
80
+ }
81
+
82
+ return {
83
+ routes,
84
+ layouts,
85
+ totalRoutes: Object.keys(routes).length,
86
+ totalLayouts: Object.keys(layouts).length,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Compare two manifests and return differences
92
+ */
93
+ export function compareManifests(
94
+ oldManifest: SerializedManifest,
95
+ newManifest: SerializedManifest
96
+ ): {
97
+ addedRoutes: string[];
98
+ removedRoutes: string[];
99
+ changedRoutes: Array<{
100
+ key: string;
101
+ field: string;
102
+ old: any;
103
+ new: any;
104
+ }>;
105
+ addedLayouts: string[];
106
+ removedLayouts: string[];
107
+ changedLayouts: Array<{
108
+ key: string;
109
+ field: string;
110
+ old: any;
111
+ new: any;
112
+ }>;
113
+ } {
114
+ const addedRoutes: string[] = [];
115
+ const removedRoutes: string[] = [];
116
+ const changedRoutes: Array<{ key: string; field: string; old: any; new: any }> = [];
117
+ const addedLayouts: string[] = [];
118
+ const removedLayouts: string[] = [];
119
+ const changedLayouts: Array<{ key: string; field: string; old: any; new: any }> = [];
120
+
121
+ // Compare routes
122
+ const oldRouteKeys = new Set(Object.keys(oldManifest.routes));
123
+ const newRouteKeys = new Set(Object.keys(newManifest.routes));
124
+
125
+ for (const key of newRouteKeys) {
126
+ if (!oldRouteKeys.has(key)) {
127
+ addedRoutes.push(key);
128
+ }
129
+ }
130
+
131
+ for (const key of oldRouteKeys) {
132
+ if (!newRouteKeys.has(key)) {
133
+ removedRoutes.push(key);
134
+ } else {
135
+ // Compare fields
136
+ const oldEntry = oldManifest.routes[key];
137
+ const newEntry = newManifest.routes[key];
138
+ for (const field of Object.keys(oldEntry) as (keyof SerializedEntry)[]) {
139
+ if (oldEntry[field] !== newEntry[field]) {
140
+ changedRoutes.push({
141
+ key,
142
+ field,
143
+ old: oldEntry[field],
144
+ new: newEntry[field],
145
+ });
146
+ }
147
+ }
148
+ }
149
+ }
150
+
151
+ // Compare layouts
152
+ const oldLayoutKeys = new Set(Object.keys(oldManifest.layouts));
153
+ const newLayoutKeys = new Set(Object.keys(newManifest.layouts));
154
+
155
+ for (const key of newLayoutKeys) {
156
+ if (!oldLayoutKeys.has(key)) {
157
+ addedLayouts.push(key);
158
+ }
159
+ }
160
+
161
+ for (const key of oldLayoutKeys) {
162
+ if (!newLayoutKeys.has(key)) {
163
+ removedLayouts.push(key);
164
+ } else {
165
+ const oldEntry = oldManifest.layouts[key];
166
+ const newEntry = newManifest.layouts[key];
167
+ for (const field of Object.keys(oldEntry) as (keyof SerializedEntry)[]) {
168
+ if (oldEntry[field] !== newEntry[field]) {
169
+ changedLayouts.push({
170
+ key,
171
+ field,
172
+ old: oldEntry[field],
173
+ new: newEntry[field],
174
+ });
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ return {
181
+ addedRoutes,
182
+ removedRoutes,
183
+ changedRoutes,
184
+ addedLayouts,
185
+ removedLayouts,
186
+ changedLayouts,
187
+ };
188
+ }
189
+
190
+ /**
191
+ * Format manifest diff as a human-readable string
192
+ */
193
+ export function formatManifestDiff(
194
+ diff: ReturnType<typeof compareManifests>
195
+ ): string {
196
+ const lines: string[] = [];
197
+
198
+ if (diff.addedRoutes.length > 0) {
199
+ lines.push("Added routes:");
200
+ diff.addedRoutes.forEach((r) => lines.push(` + ${r}`));
201
+ }
202
+
203
+ if (diff.removedRoutes.length > 0) {
204
+ lines.push("Removed routes:");
205
+ diff.removedRoutes.forEach((r) => lines.push(` - ${r}`));
206
+ }
207
+
208
+ if (diff.changedRoutes.length > 0) {
209
+ lines.push("Changed routes:");
210
+ diff.changedRoutes.forEach((c) =>
211
+ lines.push(` ~ ${c.key}.${c.field}: ${c.old} -> ${c.new}`)
212
+ );
213
+ }
214
+
215
+ if (diff.addedLayouts.length > 0) {
216
+ lines.push("Added layouts:");
217
+ diff.addedLayouts.forEach((l) => lines.push(` + ${l}`));
218
+ }
219
+
220
+ if (diff.removedLayouts.length > 0) {
221
+ lines.push("Removed layouts:");
222
+ diff.removedLayouts.forEach((l) => lines.push(` - ${l}`));
223
+ }
224
+
225
+ if (diff.changedLayouts.length > 0) {
226
+ lines.push("Changed layouts:");
227
+ diff.changedLayouts.forEach((c) =>
228
+ lines.push(` ~ ${c.key}.${c.field}: ${c.old} -> ${c.new}`)
229
+ );
230
+ }
231
+
232
+ return lines.length > 0 ? lines.join("\n") : "No differences found";
233
+ }
@@ -0,0 +1,88 @@
1
+ import type { ReactNode } from "react";
2
+ import type { ErrorBoundaryFallbackProps } from "./types.js";
3
+
4
+ /**
5
+ * Default error boundary fallback component
6
+ *
7
+ * This is rendered when an error occurs and no custom error boundary
8
+ * is defined in the route tree. Shows a simple "Internal Server Error"
9
+ * message with the error details in development.
10
+ */
11
+ export function DefaultErrorFallback({
12
+ error,
13
+ }: ErrorBoundaryFallbackProps): ReactNode {
14
+ const isDev = process.env.NODE_ENV !== "production";
15
+
16
+ return (
17
+ <div
18
+ style={{
19
+ fontFamily: "system-ui, -apple-system, sans-serif",
20
+ padding: "2rem",
21
+ maxWidth: "600px",
22
+ margin: "2rem auto",
23
+ }}
24
+ >
25
+ <h1
26
+ style={{
27
+ color: "#dc2626",
28
+ fontSize: "1.5rem",
29
+ marginBottom: "1rem",
30
+ }}
31
+ >
32
+ Internal Server Error
33
+ </h1>
34
+ <p
35
+ style={{
36
+ color: "#374151",
37
+ marginBottom: "1rem",
38
+ }}
39
+ >
40
+ An unexpected error occurred while processing your request.
41
+ </p>
42
+ {isDev && (
43
+ <div
44
+ style={{
45
+ background: "#fef2f2",
46
+ border: "1px solid #fecaca",
47
+ borderRadius: "0.5rem",
48
+ padding: "1rem",
49
+ marginBottom: "1rem",
50
+ }}
51
+ >
52
+ <p
53
+ style={{
54
+ fontWeight: 600,
55
+ color: "#991b1b",
56
+ marginBottom: "0.5rem",
57
+ }}
58
+ >
59
+ {error.name}: {error.message}
60
+ </p>
61
+ {error.stack && (
62
+ <pre
63
+ style={{
64
+ fontSize: "0.75rem",
65
+ color: "#6b7280",
66
+ overflow: "auto",
67
+ whiteSpace: "pre-wrap",
68
+ wordBreak: "break-word",
69
+ }}
70
+ >
71
+ {error.stack}
72
+ </pre>
73
+ )}
74
+ </div>
75
+ )}
76
+ <a
77
+ href="/"
78
+ style={{
79
+ display: "inline-block",
80
+ color: "#2563eb",
81
+ textDecoration: "underline",
82
+ }}
83
+ >
84
+ Go to homepage
85
+ </a>
86
+ </div>
87
+ );
88
+ }
@@ -0,0 +1,8 @@
1
+ // Re-export @vitejs/plugin-rsc/browser for internal use by virtual entries
2
+ export {
3
+ createFromReadableStream,
4
+ createFromFetch,
5
+ setServerCallback,
6
+ encodeReply,
7
+ createTemporaryReferenceSet,
8
+ } from "@vitejs/plugin-rsc/browser";
@@ -0,0 +1,2 @@
1
+ // Re-export rsc-html-stream/client for internal use by virtual entries
2
+ export { rscStream } from "rsc-html-stream/client";
@@ -0,0 +1,2 @@
1
+ // Re-export rsc-html-stream/server for internal use by virtual entries
2
+ export { injectRSCPayload } from "rsc-html-stream/server";
@@ -0,0 +1,10 @@
1
+ /// <reference types="@vitejs/plugin-rsc/types" />
2
+ // Re-export @vitejs/plugin-rsc/rsc for internal use by virtual entries
3
+ export {
4
+ renderToReadableStream,
5
+ decodeReply,
6
+ createTemporaryReferenceSet,
7
+ loadServerAction,
8
+ decodeAction,
9
+ decodeFormState,
10
+ } from "@vitejs/plugin-rsc/rsc";
@@ -0,0 +1,2 @@
1
+ // Re-export @vitejs/plugin-rsc/ssr for internal use by virtual entries
2
+ export { createFromReadableStream } from "@vitejs/plugin-rsc/ssr";
package/src/errors.ts ADDED
@@ -0,0 +1,295 @@
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
+ this.cause = options?.cause;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Thrown when data is not found (e.g., product with ID doesn't exist)
31
+ * Use this in handlers/loaders to trigger the nearest notFoundBoundary
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * route("products.detail", async (ctx) => {
36
+ * const product = await db.products.get(ctx.params.slug);
37
+ * if (!product) throw new DataNotFoundError("Product not found");
38
+ * return <ProductDetail product={product} />;
39
+ * });
40
+ * ```
41
+ */
42
+ export class DataNotFoundError extends Error {
43
+ name = "DataNotFoundError" as const;
44
+ cause?: unknown;
45
+
46
+ constructor(message: string = "Not found", options?: ErrorOptions) {
47
+ super(message);
48
+ this.cause = options?.cause;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Convenience function to throw a DataNotFoundError
54
+ * Shorter syntax for common not-found scenarios
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * const product = await db.products.get(slug);
59
+ * if (!product) throw notFound("Product not found");
60
+ * // or simply:
61
+ * if (!product) throw notFound();
62
+ * ```
63
+ */
64
+ export function notFound(message?: string): never {
65
+ throw new DataNotFoundError(message);
66
+ }
67
+
68
+ /**
69
+ * Thrown when middleware execution fails
70
+ */
71
+ export class MiddlewareError extends Error {
72
+ name = "MiddlewareError" as const;
73
+ cause?: unknown;
74
+
75
+ constructor(message: string, options?: ErrorOptions) {
76
+ super(message);
77
+ this.cause = options?.cause;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Thrown when a route handler fails
83
+ */
84
+ export class HandlerError extends Error {
85
+ name = "HandlerError" as const;
86
+ cause?: unknown;
87
+
88
+ constructor(message: string, options?: ErrorOptions) {
89
+ super(message);
90
+ this.cause = options?.cause;
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Thrown when segment building fails
96
+ */
97
+ export class BuildError extends Error {
98
+ name = "BuildError" as const;
99
+ cause?: unknown;
100
+
101
+ constructor(message: string, options?: ErrorOptions) {
102
+ super(message);
103
+ this.cause = options?.cause;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Thrown when a network request fails (server unreachable, no internet, etc.)
109
+ * This error triggers the root error boundary with retry capability.
110
+ *
111
+ * @example
112
+ * ```typescript
113
+ * try {
114
+ * await fetch(url);
115
+ * } catch (error) {
116
+ * if (error instanceof TypeError) {
117
+ * throw new NetworkError("Unable to connect to server", { cause: error });
118
+ * }
119
+ * throw error;
120
+ * }
121
+ * ```
122
+ */
123
+ export class NetworkError extends Error {
124
+ name = "NetworkError" as const;
125
+ cause?: unknown;
126
+ /** The URL that failed to fetch */
127
+ url?: string;
128
+ /** Whether this was during an action, navigation, or revalidation */
129
+ operation?: "action" | "navigation" | "revalidation";
130
+
131
+ constructor(
132
+ message: string = "Network request failed",
133
+ options?: ErrorOptions & {
134
+ url?: string;
135
+ operation?: "action" | "navigation" | "revalidation";
136
+ }
137
+ ) {
138
+ super(message);
139
+ this.cause = options?.cause;
140
+ this.url = options?.url;
141
+ this.operation = options?.operation;
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Check if an error is a network-level failure (server unreachable, no internet)
147
+ * These are typically TypeError from fetch when the network request itself fails.
148
+ */
149
+ export function isNetworkError(error: unknown): boolean {
150
+ // NetworkError we throw ourselves
151
+ if (error instanceof NetworkError) {
152
+ return true;
153
+ }
154
+
155
+ // TypeError from fetch() when network fails (e.g., "Failed to fetch")
156
+ if (error instanceof TypeError) {
157
+ const message = error.message.toLowerCase();
158
+ return (
159
+ message.includes("failed to fetch") ||
160
+ message.includes("network request failed") ||
161
+ message.includes("networkerror") ||
162
+ message.includes("load failed")
163
+ );
164
+ }
165
+
166
+ // DOMException with network-related names
167
+ if (error instanceof DOMException) {
168
+ return error.name === "NetworkError";
169
+ }
170
+
171
+ return false;
172
+ }
173
+
174
+ /**
175
+ * Structured error for JSON response routes.
176
+ * Thrown by handlers to return a typed error envelope with a specific HTTP status.
177
+ *
178
+ * Unlike standard Error, RouterError messages are always exposed to the client
179
+ * (the developer intentionally crafted them for consumers).
180
+ *
181
+ * @example
182
+ * ```typescript
183
+ * path("/products/:id", (ctx) => {
184
+ * const product = products.find(p => p.id === ctx.params.id);
185
+ * if (!product) throw new RouterError("NOT_FOUND", "Product not found", { status: 404 });
186
+ * return product;
187
+ * }, { name: "productDetail" })
188
+ * ```
189
+ */
190
+ export class RouterError extends Error {
191
+ name = "RouterError" as const;
192
+ code: string;
193
+ type?: string;
194
+ status: number;
195
+ cause?: unknown;
196
+
197
+ constructor(code: string, message: string, options?: {
198
+ status?: number;
199
+ type?: string;
200
+ cause?: unknown;
201
+ }) {
202
+ super(message);
203
+ this.code = code;
204
+ this.status = options?.status ?? 500;
205
+ this.type = options?.type;
206
+ this.cause = options?.cause;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Thrown when route handler returns invalid type
212
+ */
213
+ export class InvalidHandlerError extends Error {
214
+ name = "InvalidHandlerError" as const;
215
+ cause?: unknown;
216
+
217
+ constructor(message: string, options?: ErrorOptions) {
218
+ super(message);
219
+ this.cause = options?.cause;
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Internal invariant assertion for development-time checks
225
+ *
226
+ * @internal
227
+ * @param condition - Condition that must be true
228
+ * @param message - Error message to throw if condition is false
229
+ * @throws {Error} If condition is false
230
+ *
231
+ * @example
232
+ * ```typescript
233
+ * invariant(user !== null, 'User must be defined');
234
+ * invariant(node.type === 'layout', `Expected layout, got ${node.type}`);
235
+ * ```
236
+ */
237
+ export function invariant(condition: any, message: string): asserts condition {
238
+ if (!condition) {
239
+ throw new Error(`Invariant: ${message}`);
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Sanitize errors for production - prevents leaking sensitive information
245
+ *
246
+ * SECURITY CRITICAL:
247
+ * - In production: NEVER send stack traces, file paths, or internal state
248
+ * - In development: Show full error details for debugging
249
+ * - ALWAYS consume error.stack to prevent memory leaks
250
+ *
251
+ * @param error - Error to sanitize
252
+ * @returns Response suitable for sending to client
253
+ */
254
+ export function sanitizeError(error: unknown): Response {
255
+ // CRITICAL: Consume stack trace to prevent memory leaks
256
+ if (error && typeof error === "object" && "stack" in error) {
257
+ const _ = error.stack; // Force V8 to generate/consume stack trace
258
+ }
259
+
260
+ // Response objects are safe to return as-is
261
+ if (error instanceof Response) {
262
+ return error;
263
+ }
264
+
265
+ const isDev = (import.meta as any).env?.DEV ?? true;
266
+
267
+ if (isDev) {
268
+ // Development: Send full error details for debugging
269
+ const errorDetails = {
270
+ name: error instanceof Error ? error.name : "Error",
271
+ message: error instanceof Error ? error.message : String(error),
272
+ stack: error instanceof Error ? error.stack : undefined,
273
+ cause:
274
+ error && typeof error === "object" && "cause" in error
275
+ ? (error as any).cause
276
+ : undefined,
277
+ };
278
+
279
+ return new Response(JSON.stringify(errorDetails, null, 2), {
280
+ status: 500,
281
+ headers: {
282
+ "Content-Type": "application/json",
283
+ },
284
+ });
285
+ }
286
+
287
+ // Production: Generic error only - NO internal details
288
+ // SECURITY: Never leak stack traces, file paths, or application state
289
+ return new Response("Internal Server Error", {
290
+ status: 500,
291
+ headers: {
292
+ "Content-Type": "text/plain",
293
+ },
294
+ });
295
+ }