@rangojs/router 0.0.0-experimental.2

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 (155) hide show
  1. package/CLAUDE.md +7 -0
  2. package/README.md +19 -0
  3. package/dist/vite/index.js +1298 -0
  4. package/package.json +140 -0
  5. package/skills/caching/SKILL.md +319 -0
  6. package/skills/document-cache/SKILL.md +152 -0
  7. package/skills/hooks/SKILL.md +359 -0
  8. package/skills/intercept/SKILL.md +292 -0
  9. package/skills/layout/SKILL.md +216 -0
  10. package/skills/loader/SKILL.md +365 -0
  11. package/skills/middleware/SKILL.md +442 -0
  12. package/skills/parallel/SKILL.md +255 -0
  13. package/skills/route/SKILL.md +141 -0
  14. package/skills/router-setup/SKILL.md +403 -0
  15. package/skills/theme/SKILL.md +54 -0
  16. package/skills/typesafety/SKILL.md +352 -0
  17. package/src/__mocks__/version.ts +6 -0
  18. package/src/__tests__/component-utils.test.ts +76 -0
  19. package/src/__tests__/route-definition.test.ts +63 -0
  20. package/src/__tests__/urls.test.tsx +436 -0
  21. package/src/browser/event-controller.ts +876 -0
  22. package/src/browser/index.ts +18 -0
  23. package/src/browser/link-interceptor.ts +121 -0
  24. package/src/browser/lru-cache.ts +69 -0
  25. package/src/browser/merge-segment-loaders.ts +126 -0
  26. package/src/browser/navigation-bridge.ts +893 -0
  27. package/src/browser/navigation-client.ts +162 -0
  28. package/src/browser/navigation-store.ts +823 -0
  29. package/src/browser/partial-update.ts +559 -0
  30. package/src/browser/react/Link.tsx +248 -0
  31. package/src/browser/react/NavigationProvider.tsx +275 -0
  32. package/src/browser/react/ScrollRestoration.tsx +94 -0
  33. package/src/browser/react/context.ts +53 -0
  34. package/src/browser/react/index.ts +52 -0
  35. package/src/browser/react/location-state-shared.ts +120 -0
  36. package/src/browser/react/location-state.ts +62 -0
  37. package/src/browser/react/use-action.ts +240 -0
  38. package/src/browser/react/use-client-cache.ts +56 -0
  39. package/src/browser/react/use-handle.ts +178 -0
  40. package/src/browser/react/use-href.tsx +208 -0
  41. package/src/browser/react/use-link-status.ts +134 -0
  42. package/src/browser/react/use-navigation.ts +150 -0
  43. package/src/browser/react/use-segments.ts +188 -0
  44. package/src/browser/request-controller.ts +164 -0
  45. package/src/browser/rsc-router.tsx +353 -0
  46. package/src/browser/scroll-restoration.ts +324 -0
  47. package/src/browser/server-action-bridge.ts +747 -0
  48. package/src/browser/shallow.ts +35 -0
  49. package/src/browser/types.ts +464 -0
  50. package/src/cache/__tests__/document-cache.test.ts +522 -0
  51. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  52. package/src/cache/__tests__/memory-store.test.ts +484 -0
  53. package/src/cache/cache-scope.ts +565 -0
  54. package/src/cache/cf/__tests__/cf-cache-store.test.ts +428 -0
  55. package/src/cache/cf/cf-cache-store.ts +428 -0
  56. package/src/cache/cf/index.ts +19 -0
  57. package/src/cache/document-cache.ts +340 -0
  58. package/src/cache/index.ts +58 -0
  59. package/src/cache/memory-segment-store.ts +150 -0
  60. package/src/cache/memory-store.ts +253 -0
  61. package/src/cache/types.ts +387 -0
  62. package/src/client.rsc.tsx +88 -0
  63. package/src/client.tsx +621 -0
  64. package/src/component-utils.ts +76 -0
  65. package/src/components/DefaultDocument.tsx +23 -0
  66. package/src/default-error-boundary.tsx +88 -0
  67. package/src/deps/browser.ts +8 -0
  68. package/src/deps/html-stream-client.ts +2 -0
  69. package/src/deps/html-stream-server.ts +2 -0
  70. package/src/deps/rsc.ts +10 -0
  71. package/src/deps/ssr.ts +2 -0
  72. package/src/errors.ts +259 -0
  73. package/src/handle.ts +120 -0
  74. package/src/handles/MetaTags.tsx +193 -0
  75. package/src/handles/index.ts +6 -0
  76. package/src/handles/meta.ts +247 -0
  77. package/src/href-client.ts +128 -0
  78. package/src/href-context.ts +33 -0
  79. package/src/href.ts +177 -0
  80. package/src/index.rsc.ts +79 -0
  81. package/src/index.ts +87 -0
  82. package/src/loader.rsc.ts +204 -0
  83. package/src/loader.ts +47 -0
  84. package/src/network-error-thrower.tsx +21 -0
  85. package/src/outlet-context.ts +15 -0
  86. package/src/root-error-boundary.tsx +277 -0
  87. package/src/route-content-wrapper.tsx +198 -0
  88. package/src/route-definition.ts +1371 -0
  89. package/src/route-map-builder.ts +146 -0
  90. package/src/route-types.ts +198 -0
  91. package/src/route-utils.ts +89 -0
  92. package/src/router/__tests__/match-context.test.ts +104 -0
  93. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  94. package/src/router/__tests__/match-result.test.ts +566 -0
  95. package/src/router/__tests__/on-error.test.ts +935 -0
  96. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  97. package/src/router/error-handling.ts +287 -0
  98. package/src/router/handler-context.ts +158 -0
  99. package/src/router/loader-resolution.ts +326 -0
  100. package/src/router/manifest.ts +138 -0
  101. package/src/router/match-context.ts +264 -0
  102. package/src/router/match-middleware/background-revalidation.ts +236 -0
  103. package/src/router/match-middleware/cache-lookup.ts +261 -0
  104. package/src/router/match-middleware/cache-store.ts +266 -0
  105. package/src/router/match-middleware/index.ts +81 -0
  106. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  107. package/src/router/match-middleware/segment-resolution.ts +174 -0
  108. package/src/router/match-pipelines.ts +214 -0
  109. package/src/router/match-result.ts +214 -0
  110. package/src/router/metrics.ts +62 -0
  111. package/src/router/middleware.test.ts +1355 -0
  112. package/src/router/middleware.ts +748 -0
  113. package/src/router/pattern-matching.ts +272 -0
  114. package/src/router/revalidation.ts +190 -0
  115. package/src/router/router-context.ts +299 -0
  116. package/src/router/types.ts +96 -0
  117. package/src/router.ts +3876 -0
  118. package/src/rsc/__tests__/helpers.test.ts +175 -0
  119. package/src/rsc/handler.ts +1060 -0
  120. package/src/rsc/helpers.ts +64 -0
  121. package/src/rsc/index.ts +56 -0
  122. package/src/rsc/nonce.ts +18 -0
  123. package/src/rsc/types.ts +237 -0
  124. package/src/segment-system.tsx +456 -0
  125. package/src/server/__tests__/request-context.test.ts +171 -0
  126. package/src/server/context.ts +417 -0
  127. package/src/server/handle-store.ts +230 -0
  128. package/src/server/loader-registry.ts +174 -0
  129. package/src/server/request-context.ts +554 -0
  130. package/src/server/root-layout.tsx +10 -0
  131. package/src/server/tsconfig.json +14 -0
  132. package/src/server.ts +146 -0
  133. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  134. package/src/ssr/index.tsx +234 -0
  135. package/src/theme/ThemeProvider.tsx +291 -0
  136. package/src/theme/ThemeScript.tsx +61 -0
  137. package/src/theme/__tests__/theme.test.ts +120 -0
  138. package/src/theme/constants.ts +55 -0
  139. package/src/theme/index.ts +58 -0
  140. package/src/theme/theme-context.ts +70 -0
  141. package/src/theme/theme-script.ts +152 -0
  142. package/src/theme/types.ts +182 -0
  143. package/src/theme/use-theme.ts +44 -0
  144. package/src/types.ts +1561 -0
  145. package/src/urls.ts +726 -0
  146. package/src/use-loader.tsx +346 -0
  147. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  148. package/src/vite/expose-action-id.ts +344 -0
  149. package/src/vite/expose-handle-id.ts +209 -0
  150. package/src/vite/expose-loader-id.ts +357 -0
  151. package/src/vite/expose-location-state-id.ts +177 -0
  152. package/src/vite/index.ts +787 -0
  153. package/src/vite/package-resolution.ts +125 -0
  154. package/src/vite/version.d.ts +12 -0
  155. package/src/vite/virtual-entries.ts +109 -0
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Component utilities for RSC
3
+ *
4
+ * Helpers for working with React Server Components and client components.
5
+ */
6
+
7
+ import type { ComponentType } from "react";
8
+
9
+ /**
10
+ * Symbol used by React to mark client component references.
11
+ * When a file has "use client" directive, the bundler transforms the exports
12
+ * to include this symbol on $$typeof.
13
+ */
14
+ const CLIENT_REFERENCE = Symbol.for("react.client.reference");
15
+
16
+ /**
17
+ * Check if a component is a client component (has "use client" directive).
18
+ *
19
+ * Client components are marked by the bundler with:
20
+ * - $$typeof: Symbol.for("react.client.reference")
21
+ * - $$id: string (module identifier)
22
+ *
23
+ * @param component - The component to check
24
+ * @returns true if the component has client reference marker
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * import { MyComponent } from "./my-component"; // has "use client"
29
+ *
30
+ * if (!isClientComponent(MyComponent)) {
31
+ * throw new Error("MyComponent must be a client component");
32
+ * }
33
+ * ```
34
+ */
35
+ export function isClientComponent(
36
+ component: ComponentType<unknown> | unknown
37
+ ): boolean {
38
+ if (typeof component !== "function") {
39
+ return false;
40
+ }
41
+ const withMeta = component as { $$typeof?: symbol };
42
+ return withMeta.$$typeof === CLIENT_REFERENCE;
43
+ }
44
+
45
+ /**
46
+ * Assert that a component is a client component.
47
+ * Throws a descriptive error if not.
48
+ *
49
+ * @param component - The component to check
50
+ * @param name - Name to use in error message (e.g., "document")
51
+ * @throws Error if the component is not a client component
52
+ */
53
+ export function assertClientComponent(
54
+ component: ComponentType<unknown> | unknown,
55
+ name: string
56
+ ): asserts component is ComponentType<unknown> {
57
+ if (typeof component !== "function") {
58
+ throw new Error(
59
+ `${name} must be a client component function with "use client" directive. ` +
60
+ `Make sure to pass the component itself, not a JSX element: ` +
61
+ `${name}: My${capitalize(name)} (correct) vs ${name}: <My${capitalize(name)} /> (incorrect)`
62
+ );
63
+ }
64
+
65
+ if (!isClientComponent(component)) {
66
+ throw new Error(
67
+ `${name} must be a client component with "use client" directive at the top of the file. ` +
68
+ `Server components cannot be used as the ${name} because their function reference ` +
69
+ `cannot be serialized in the RSC payload. Add "use client" to your ${name} file.`
70
+ );
71
+ }
72
+ }
73
+
74
+ function capitalize(str: string): string {
75
+ return str.charAt(0).toUpperCase() + str.slice(1);
76
+ }
@@ -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 createRSCRouter.
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
+ }
@@ -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,259 @@
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
+ * Thrown when route handler returns invalid type
176
+ */
177
+ export class InvalidHandlerError extends Error {
178
+ name = "InvalidHandlerError" as const;
179
+ cause?: unknown;
180
+
181
+ constructor(message: string, options?: ErrorOptions) {
182
+ super(message);
183
+ this.cause = options?.cause;
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Internal invariant assertion for development-time checks
189
+ *
190
+ * @internal
191
+ * @param condition - Condition that must be true
192
+ * @param message - Error message to throw if condition is false
193
+ * @throws {Error} If condition is false
194
+ *
195
+ * @example
196
+ * ```typescript
197
+ * invariant(user !== null, 'User must be defined');
198
+ * invariant(node.type === 'layout', `Expected layout, got ${node.type}`);
199
+ * ```
200
+ */
201
+ export function invariant(condition: any, message: string): asserts condition {
202
+ if (!condition) {
203
+ throw new Error(`Invariant: ${message}`);
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Sanitize errors for production - prevents leaking sensitive information
209
+ *
210
+ * SECURITY CRITICAL:
211
+ * - In production: NEVER send stack traces, file paths, or internal state
212
+ * - In development: Show full error details for debugging
213
+ * - ALWAYS consume error.stack to prevent memory leaks
214
+ *
215
+ * @param error - Error to sanitize
216
+ * @returns Response suitable for sending to client
217
+ */
218
+ export function sanitizeError(error: unknown): Response {
219
+ // CRITICAL: Consume stack trace to prevent memory leaks
220
+ if (error && typeof error === "object" && "stack" in error) {
221
+ const _ = error.stack; // Force V8 to generate/consume stack trace
222
+ }
223
+
224
+ // Response objects are safe to return as-is
225
+ if (error instanceof Response) {
226
+ return error;
227
+ }
228
+
229
+ const isDev = (import.meta as any).env?.DEV ?? true;
230
+
231
+ if (isDev) {
232
+ // Development: Send full error details for debugging
233
+ const errorDetails = {
234
+ name: error instanceof Error ? error.name : "Error",
235
+ message: error instanceof Error ? error.message : String(error),
236
+ stack: error instanceof Error ? error.stack : undefined,
237
+ cause:
238
+ error && typeof error === "object" && "cause" in error
239
+ ? (error as any).cause
240
+ : undefined,
241
+ };
242
+
243
+ return new Response(JSON.stringify(errorDetails, null, 2), {
244
+ status: 500,
245
+ headers: {
246
+ "Content-Type": "application/json",
247
+ },
248
+ });
249
+ }
250
+
251
+ // Production: Generic error only - NO internal details
252
+ // SECURITY: Never leak stack traces, file paths, or application state
253
+ return new Response("Internal Server Error", {
254
+ status: 500,
255
+ headers: {
256
+ "Content-Type": "text/plain",
257
+ },
258
+ });
259
+ }
package/src/handle.ts ADDED
@@ -0,0 +1,120 @@
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
+ * 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
+ *
38
+ * @param segments - Array of segment data arrays, e.g. [[a, b], [c], [d, e]]
39
+ * @returns The accumulated value
40
+ */
41
+ readonly collect: (segments: TData[][]) => TAccumulated;
42
+ }
43
+
44
+ /**
45
+ * Default collect function that flattens segment arrays into a single array.
46
+ */
47
+ function defaultCollect<T>(segments: T[][]): T[] {
48
+ return segments.flat();
49
+ }
50
+
51
+ /**
52
+ * Create a handle definition for accumulating data across route segments.
53
+ *
54
+ * The $$id is auto-generated by the Vite exposeHandleId plugin based on
55
+ * file path and export name. No manual naming required.
56
+ *
57
+ * @param collect - Optional collect function (default: flatten into array)
58
+ * @param __injectedId - Auto-injected by Vite plugin, do not provide manually
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * // Default: flatten into array
63
+ * export const Breadcrumbs = createHandle<BreadcrumbItem>();
64
+ * // Result type: BreadcrumbItem[]
65
+ *
66
+ * // Custom: last value wins
67
+ * export const PageTitle = createHandle<string, string>(
68
+ * (segments) => segments.flat().at(-1) ?? "Default Title"
69
+ * );
70
+ * // Result type: string
71
+ *
72
+ * // Custom: object merge
73
+ * export const Meta = createHandle<Partial<MetaTags>, MetaTags>(
74
+ * (segments) => Object.assign({ robots: "index,follow" }, ...segments.flat())
75
+ * );
76
+ * // Result type: MetaTags
77
+ *
78
+ * // Custom: dedupe by href
79
+ * export const Breadcrumbs = createHandle<BreadcrumbItem>(
80
+ * (segments) => {
81
+ * const all = segments.flat();
82
+ * return all.filter((item, i) => all.findIndex(x => x.href === item.href) === i);
83
+ * }
84
+ * );
85
+ * ```
86
+ */
87
+ export function createHandle<TData, TAccumulated = TData[]>(
88
+ collect?: (segments: TData[][]) => TAccumulated,
89
+ __injectedId?: string
90
+ ): Handle<TData, TAccumulated> {
91
+ const handleId = __injectedId ?? "";
92
+
93
+ if (!handleId && process.env.NODE_ENV !== "production") {
94
+ console.warn(
95
+ "[rsc-router] Handle is missing $$id. " +
96
+ "Make sure the exposeHandleId Vite plugin is enabled and " +
97
+ "the handle is exported with: export const MyHandle = createHandle(...)"
98
+ );
99
+ }
100
+
101
+ return {
102
+ __brand: "handle" as const,
103
+ $$id: handleId,
104
+ collect:
105
+ collect ??
106
+ (defaultCollect as unknown as (segments: TData[][]) => TAccumulated),
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Type guard to check if a value is a Handle.
112
+ */
113
+ export function isHandle(value: unknown): value is Handle<unknown, unknown> {
114
+ return (
115
+ typeof value === "object" &&
116
+ value !== null &&
117
+ "__brand" in value &&
118
+ (value as { __brand: unknown }).__brand === "handle"
119
+ );
120
+ }