@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,277 @@
1
+ "use client";
2
+
3
+ import { Component, useState, type ReactNode } from "react";
4
+ import type { ClientErrorBoundaryFallbackProps } from "./types.js";
5
+
6
+ /**
7
+ * Check if an error is a network-related error
8
+ */
9
+ function isNetworkError(error: Error): boolean {
10
+ return error.name === "NetworkError";
11
+ }
12
+
13
+ /**
14
+ * Network error fallback UI with retry functionality
15
+ * Shows a connection-specific message and allows retrying via page refresh
16
+ */
17
+ function NetworkErrorFallback({
18
+ error,
19
+ reset,
20
+ }: ClientErrorBoundaryFallbackProps): ReactNode {
21
+ const [isRetrying, setIsRetrying] = useState(false);
22
+
23
+ const handleRetry = (): void => {
24
+ setIsRetrying(true);
25
+ // Refresh the page to retry the request
26
+ window.location.reload();
27
+ };
28
+
29
+ return (
30
+ <div
31
+ style={{
32
+ fontFamily: "system-ui, -apple-system, sans-serif",
33
+ padding: "2rem",
34
+ maxWidth: "600px",
35
+ margin: "2rem auto",
36
+ textAlign: "center",
37
+ }}
38
+ >
39
+ <div
40
+ style={{
41
+ fontSize: "3rem",
42
+ marginBottom: "1rem",
43
+ }}
44
+ >
45
+ {/* Simple cloud with x icon using CSS */}
46
+ <span style={{ color: "#9ca3af" }}>&#9729;</span>
47
+ </div>
48
+ <h1
49
+ style={{
50
+ color: "#374151",
51
+ fontSize: "1.5rem",
52
+ marginBottom: "0.5rem",
53
+ }}
54
+ >
55
+ Connection Error
56
+ </h1>
57
+ <p
58
+ style={{
59
+ color: "#6b7280",
60
+ marginBottom: "1.5rem",
61
+ }}
62
+ >
63
+ {error.message || "Unable to connect to the server. Please check your internet connection."}
64
+ </p>
65
+ <div style={{ display: "flex", gap: "1rem", justifyContent: "center" }}>
66
+ <button
67
+ type="button"
68
+ onClick={handleRetry}
69
+ disabled={isRetrying}
70
+ style={{
71
+ padding: "0.75rem 1.5rem",
72
+ backgroundColor: isRetrying ? "#9ca3af" : "#2563eb",
73
+ color: "white",
74
+ border: "none",
75
+ borderRadius: "0.375rem",
76
+ cursor: isRetrying ? "not-allowed" : "pointer",
77
+ fontSize: "1rem",
78
+ fontWeight: 500,
79
+ }}
80
+ >
81
+ {isRetrying ? "Retrying..." : "Retry"}
82
+ </button>
83
+ <button
84
+ type="button"
85
+ onClick={() => window.history.back()}
86
+ style={{
87
+ padding: "0.75rem 1.5rem",
88
+ backgroundColor: "transparent",
89
+ color: "#6b7280",
90
+ border: "1px solid #d1d5db",
91
+ borderRadius: "0.375rem",
92
+ cursor: "pointer",
93
+ fontSize: "1rem",
94
+ }}
95
+ >
96
+ Go Back
97
+ </button>
98
+ </div>
99
+ </div>
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Default fallback UI for root error boundary
105
+ * This is shown when an unhandled error bubbles up to the root
106
+ */
107
+ function RootErrorFallback({ error, reset }: ClientErrorBoundaryFallbackProps): ReactNode {
108
+ return (
109
+ <div
110
+ style={{
111
+ fontFamily: "system-ui, -apple-system, sans-serif",
112
+ padding: "2rem",
113
+ maxWidth: "600px",
114
+ margin: "2rem auto",
115
+ }}
116
+ >
117
+ <h1
118
+ style={{
119
+ color: "#dc2626",
120
+ fontSize: "1.5rem",
121
+ marginBottom: "1rem",
122
+ }}
123
+ >
124
+ Internal Server Error
125
+ </h1>
126
+ <p
127
+ style={{
128
+ color: "#374151",
129
+ marginBottom: "1rem",
130
+ }}
131
+ >
132
+ An unexpected error occurred while processing your request.
133
+ </p>
134
+ <div
135
+ style={{
136
+ background: "#fef2f2",
137
+ border: "1px solid #fecaca",
138
+ borderRadius: "0.5rem",
139
+ padding: "1rem",
140
+ marginBottom: "1rem",
141
+ }}
142
+ >
143
+ <p
144
+ style={{
145
+ fontWeight: 600,
146
+ color: "#991b1b",
147
+ marginBottom: "0.5rem",
148
+ }}
149
+ >
150
+ {error.name}: {error.message}
151
+ </p>
152
+ {error.stack && (
153
+ <pre
154
+ style={{
155
+ fontSize: "0.75rem",
156
+ color: "#6b7280",
157
+ overflow: "auto",
158
+ whiteSpace: "pre-wrap",
159
+ wordBreak: "break-word",
160
+ }}
161
+ >
162
+ {error.stack}
163
+ </pre>
164
+ )}
165
+ </div>
166
+ <div style={{ display: "flex", gap: "1rem" }}>
167
+ <button
168
+ type="button"
169
+ onClick={reset}
170
+ style={{
171
+ padding: "0.5rem 1rem",
172
+ backgroundColor: "#2563eb",
173
+ color: "white",
174
+ border: "none",
175
+ borderRadius: "0.25rem",
176
+ cursor: "pointer",
177
+ }}
178
+ >
179
+ Try Again
180
+ </button>
181
+ <a
182
+ href="/"
183
+ style={{
184
+ display: "inline-block",
185
+ padding: "0.5rem 1rem",
186
+ color: "#2563eb",
187
+ textDecoration: "underline",
188
+ }}
189
+ >
190
+ Go to homepage
191
+ </a>
192
+ </div>
193
+ </div>
194
+ );
195
+ }
196
+
197
+ interface RootErrorBoundaryState {
198
+ hasError: boolean;
199
+ error: Error | null;
200
+ }
201
+
202
+ /**
203
+ * Root error boundary component
204
+ *
205
+ * Wraps the entire segment tree to catch any unhandled errors that bubble up.
206
+ * This prevents the entire app from crashing with a white screen.
207
+ *
208
+ * This is a client component with an inline fallback to avoid the
209
+ * "Functions cannot be passed to Client Components" RSC error.
210
+ */
211
+ export class RootErrorBoundary extends Component<
212
+ { children: ReactNode },
213
+ RootErrorBoundaryState
214
+ > {
215
+ constructor(props: { children: ReactNode }) {
216
+ super(props);
217
+ this.state = { hasError: false, error: null };
218
+ }
219
+
220
+ static getDerivedStateFromError(error: Error): RootErrorBoundaryState {
221
+ return { hasError: true, error };
222
+ }
223
+
224
+ componentDidMount(): void {
225
+ // Listen for popstate (back/forward navigation) to reset error state
226
+ window.addEventListener("popstate", this.handlePopState);
227
+ }
228
+
229
+ componentWillUnmount(): void {
230
+ window.removeEventListener("popstate", this.handlePopState);
231
+ }
232
+
233
+ componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
234
+ console.error("[RootErrorBoundary] Unhandled error caught:", error, errorInfo);
235
+ }
236
+
237
+ componentDidUpdate(prevProps: { children: ReactNode }): void {
238
+ // Reset error state when children change (e.g., navigation)
239
+ // This allows the app to recover after navigation away from an errored route
240
+ if (this.state.hasError && prevProps.children !== this.props.children) {
241
+ this.setState({ hasError: false, error: null });
242
+ }
243
+ }
244
+
245
+ handlePopState = (): void => {
246
+ // Reset error state on back/forward navigation
247
+ if (this.state.hasError) {
248
+ this.setState({ hasError: false, error: null });
249
+ }
250
+ };
251
+
252
+ reset = (): void => {
253
+ this.setState({ hasError: false, error: null });
254
+ };
255
+
256
+ render(): ReactNode {
257
+ if (this.state.hasError && this.state.error) {
258
+ const errorInfo = {
259
+ message: this.state.error.message,
260
+ name: this.state.error.name,
261
+ stack: this.state.error.stack,
262
+ cause: this.state.error.cause,
263
+ segmentId: "root",
264
+ segmentType: "route" as const,
265
+ };
266
+
267
+ // Use specialized fallback for network errors
268
+ if (isNetworkError(this.state.error)) {
269
+ return <NetworkErrorFallback error={errorInfo} reset={this.reset} />;
270
+ }
271
+
272
+ return <RootErrorFallback error={errorInfo} reset={this.reset} />;
273
+ }
274
+
275
+ return this.props.children;
276
+ }
277
+ }
@@ -0,0 +1,198 @@
1
+ "use client";
2
+ import type { ReactNode } from "react";
3
+ import { Suspense, use, useId } from "react";
4
+ import { invariant } from "./errors";
5
+ import { OutletProvider } from "./client.js";
6
+ import type { ResolvedSegment } from "./types.js";
7
+ import { isLoaderDataResult } from "./types.js";
8
+
9
+ /**
10
+ * Stable async wrapper component for route content
11
+ * Using a module-level component ensures React sees the same component reference
12
+ * across renders, preventing unnecessary remounts during actions.
13
+ *
14
+ * When content is a pending promise, React suspends and shows the nearest
15
+ * Suspense fallback. When content is already resolved, it renders immediately
16
+ * without suspension.
17
+ *
18
+ * @param segmentId - Stable ID from segment, used for consistent keys across renders
19
+ */
20
+ export function RouteContentWrapper({
21
+ content,
22
+ fallback,
23
+ segmentId,
24
+ }: {
25
+ content: Promise<ReactNode>;
26
+ fallback?: ReactNode;
27
+ segmentId?: string;
28
+ }): ReactNode {
29
+ // Use server-provided segmentId for stable keys, fall back to useId for backwards compat
30
+ const generatedId = useId();
31
+ const id = segmentId || generatedId;
32
+ if (!content) {
33
+ // Already resolved
34
+ return content as ReactNode;
35
+ }
36
+ return (
37
+ <Suspense fallback={fallback ?? null} key={"route-content-suspense-" + id}>
38
+ <Suspender content={content} key={id} />
39
+ </Suspense>
40
+ );
41
+ }
42
+
43
+ export function RouteContentWrapperCallback<T>({
44
+ resolve,
45
+ fallback,
46
+ children,
47
+ }: {
48
+ resolve: Promise<T> | T;
49
+ fallback?: ReactNode;
50
+ children: (data: T) => ReactNode;
51
+ }): ReactNode {
52
+ const id = useId();
53
+ invariant(children, "RouteContentWrapperCallback requires children");
54
+ invariant(
55
+ typeof children === "function",
56
+ "RouteContentWrapperCallback requires children to be a function"
57
+ );
58
+ invariant(
59
+ resolve !== undefined,
60
+ "RouteContentWrapperCallback requires resolve"
61
+ );
62
+ return (
63
+ <Suspense
64
+ fallback={fallback ?? null}
65
+ key={"route-content-suspense-callback-" + id}
66
+ >
67
+ <SuspenderCallback resolve={resolve} key={id}>
68
+ {children}
69
+ </SuspenderCallback>
70
+ </Suspense>
71
+ );
72
+ }
73
+
74
+ const Suspender = ({
75
+ content,
76
+ }: {
77
+ content: Promise<ReactNode> | ReactNode;
78
+ }): ReactNode => {
79
+ invariant(content instanceof Promise, "Suspender expects a Promise content");
80
+
81
+ return use(content);
82
+ };
83
+
84
+ const SuspenderCallback = <T,>({
85
+ resolve,
86
+ children,
87
+ }: {
88
+ resolve: Promise<T> | T;
89
+ children: (data: T) => ReactNode;
90
+ }): ReactNode => {
91
+ return resolve instanceof Promise
92
+ ? children(use(resolve))
93
+ : children(resolve);
94
+ };
95
+
96
+ /**
97
+ * LoaderBoundary - Client component that resolves loader promises and renders OutletProvider
98
+ *
99
+ * This component enables streaming with loaders by:
100
+ * 1. Receiving loader promises (serializable across RSC boundary)
101
+ * 2. Using React's use() to resolve them (triggers Suspense)
102
+ * 3. Rendering OutletProvider with resolved data
103
+ *
104
+ * The callback logic lives inside this client component, avoiding the
105
+ * "Functions are not valid as a child of Client Components" error.
106
+ */
107
+ export interface LoaderBoundaryProps {
108
+ loaderDataPromise: Promise<any[]> | any[];
109
+ loaderIds: string[];
110
+ fallback?: ReactNode;
111
+ outletKey: string;
112
+ outletContent: ReactNode;
113
+ segment: ResolvedSegment;
114
+ parallel?: ResolvedSegment[];
115
+ children: ReactNode;
116
+ }
117
+
118
+ export function LoaderBoundary({
119
+ loaderDataPromise,
120
+ loaderIds,
121
+ fallback,
122
+ outletKey,
123
+ outletContent,
124
+ segment,
125
+ parallel,
126
+ children,
127
+ }: LoaderBoundaryProps): ReactNode {
128
+ const id = useId();
129
+
130
+ return (
131
+ <Suspense fallback={fallback ?? null} key={`loader-boundary-${id}`}>
132
+ <LoaderResolver
133
+ loaderDataPromise={loaderDataPromise}
134
+ loaderIds={loaderIds}
135
+ outletKey={outletKey}
136
+ outletContent={outletContent}
137
+ segment={segment}
138
+ parallel={parallel}
139
+ >
140
+ {children}
141
+ </LoaderResolver>
142
+ </Suspense>
143
+ );
144
+ }
145
+
146
+ /**
147
+ * Internal component that resolves loader promises and renders OutletProvider
148
+ */
149
+ function LoaderResolver({
150
+ loaderDataPromise,
151
+ loaderIds,
152
+ outletKey,
153
+ outletContent,
154
+ segment,
155
+ parallel,
156
+ children,
157
+ }: Omit<LoaderBoundaryProps, "fallback">): ReactNode {
158
+ // Resolve loader promises using React's use()
159
+ const resolvedData =
160
+ loaderDataPromise instanceof Promise
161
+ ? use(loaderDataPromise)
162
+ : loaderDataPromise;
163
+
164
+ // Build loaderData record from resolved values
165
+ const loaderData: Record<string, any> = {};
166
+ let loaderErrorFallback: ReactNode = null;
167
+
168
+ loaderIds.forEach((id, i) => {
169
+ const result = resolvedData[i];
170
+
171
+ if (isLoaderDataResult(result)) {
172
+ if (result.ok) {
173
+ loaderData[id] = result.data;
174
+ } else {
175
+ if (result.fallback) {
176
+ loaderErrorFallback = result.fallback;
177
+ } else {
178
+ throw new Error(result.error.message);
179
+ }
180
+ }
181
+ } else {
182
+ // Legacy format - direct data
183
+ loaderData[id] = result;
184
+ }
185
+ });
186
+
187
+ return (
188
+ <OutletProvider
189
+ key={outletKey}
190
+ content={outletContent}
191
+ segment={segment}
192
+ parallel={parallel}
193
+ loaderData={Object.keys(loaderData).length > 0 ? loaderData : undefined}
194
+ >
195
+ {loaderErrorFallback ?? children}
196
+ </OutletProvider>
197
+ );
198
+ }