@ivogt/rsc-router 0.0.0-experimental.1

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 (123) hide show
  1. package/README.md +19 -0
  2. package/package.json +131 -0
  3. package/src/__mocks__/version.ts +6 -0
  4. package/src/__tests__/route-definition.test.ts +63 -0
  5. package/src/browser/event-controller.ts +876 -0
  6. package/src/browser/index.ts +18 -0
  7. package/src/browser/link-interceptor.ts +121 -0
  8. package/src/browser/lru-cache.ts +69 -0
  9. package/src/browser/merge-segment-loaders.ts +126 -0
  10. package/src/browser/navigation-bridge.ts +891 -0
  11. package/src/browser/navigation-client.ts +155 -0
  12. package/src/browser/navigation-store.ts +823 -0
  13. package/src/browser/partial-update.ts +545 -0
  14. package/src/browser/react/Link.tsx +248 -0
  15. package/src/browser/react/NavigationProvider.tsx +228 -0
  16. package/src/browser/react/ScrollRestoration.tsx +94 -0
  17. package/src/browser/react/context.ts +53 -0
  18. package/src/browser/react/index.ts +52 -0
  19. package/src/browser/react/location-state-shared.ts +120 -0
  20. package/src/browser/react/location-state.ts +62 -0
  21. package/src/browser/react/use-action.ts +240 -0
  22. package/src/browser/react/use-client-cache.ts +56 -0
  23. package/src/browser/react/use-handle.ts +178 -0
  24. package/src/browser/react/use-link-status.ts +134 -0
  25. package/src/browser/react/use-navigation.ts +150 -0
  26. package/src/browser/react/use-segments.ts +188 -0
  27. package/src/browser/request-controller.ts +149 -0
  28. package/src/browser/rsc-router.tsx +310 -0
  29. package/src/browser/scroll-restoration.ts +324 -0
  30. package/src/browser/server-action-bridge.ts +747 -0
  31. package/src/browser/shallow.ts +35 -0
  32. package/src/browser/types.ts +443 -0
  33. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  34. package/src/cache/__tests__/memory-store.test.ts +484 -0
  35. package/src/cache/cache-scope.ts +565 -0
  36. package/src/cache/cf/__tests__/cf-cache-store.test.ts +361 -0
  37. package/src/cache/cf/cf-cache-store.ts +274 -0
  38. package/src/cache/cf/index.ts +19 -0
  39. package/src/cache/index.ts +52 -0
  40. package/src/cache/memory-segment-store.ts +150 -0
  41. package/src/cache/memory-store.ts +253 -0
  42. package/src/cache/types.ts +366 -0
  43. package/src/client.rsc.tsx +88 -0
  44. package/src/client.tsx +609 -0
  45. package/src/components/DefaultDocument.tsx +20 -0
  46. package/src/default-error-boundary.tsx +88 -0
  47. package/src/deps/browser.ts +8 -0
  48. package/src/deps/html-stream-client.ts +2 -0
  49. package/src/deps/html-stream-server.ts +2 -0
  50. package/src/deps/rsc.ts +10 -0
  51. package/src/deps/ssr.ts +2 -0
  52. package/src/errors.ts +259 -0
  53. package/src/handle.ts +120 -0
  54. package/src/handles/MetaTags.tsx +178 -0
  55. package/src/handles/index.ts +6 -0
  56. package/src/handles/meta.ts +247 -0
  57. package/src/href-client.ts +128 -0
  58. package/src/href.ts +139 -0
  59. package/src/index.rsc.ts +69 -0
  60. package/src/index.ts +84 -0
  61. package/src/loader.rsc.ts +204 -0
  62. package/src/loader.ts +47 -0
  63. package/src/network-error-thrower.tsx +21 -0
  64. package/src/outlet-context.ts +15 -0
  65. package/src/root-error-boundary.tsx +277 -0
  66. package/src/route-content-wrapper.tsx +198 -0
  67. package/src/route-definition.ts +1333 -0
  68. package/src/route-map-builder.ts +140 -0
  69. package/src/route-types.ts +148 -0
  70. package/src/route-utils.ts +89 -0
  71. package/src/router/__tests__/match-context.test.ts +104 -0
  72. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  73. package/src/router/__tests__/match-result.test.ts +566 -0
  74. package/src/router/__tests__/on-error.test.ts +935 -0
  75. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  76. package/src/router/error-handling.ts +287 -0
  77. package/src/router/handler-context.ts +60 -0
  78. package/src/router/loader-resolution.ts +326 -0
  79. package/src/router/manifest.ts +116 -0
  80. package/src/router/match-context.ts +261 -0
  81. package/src/router/match-middleware/background-revalidation.ts +236 -0
  82. package/src/router/match-middleware/cache-lookup.ts +261 -0
  83. package/src/router/match-middleware/cache-store.ts +250 -0
  84. package/src/router/match-middleware/index.ts +81 -0
  85. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  86. package/src/router/match-middleware/segment-resolution.ts +174 -0
  87. package/src/router/match-pipelines.ts +214 -0
  88. package/src/router/match-result.ts +212 -0
  89. package/src/router/metrics.ts +62 -0
  90. package/src/router/middleware.test.ts +1355 -0
  91. package/src/router/middleware.ts +748 -0
  92. package/src/router/pattern-matching.ts +271 -0
  93. package/src/router/revalidation.ts +190 -0
  94. package/src/router/router-context.ts +299 -0
  95. package/src/router/types.ts +96 -0
  96. package/src/router.ts +3484 -0
  97. package/src/rsc/__tests__/helpers.test.ts +175 -0
  98. package/src/rsc/handler.ts +942 -0
  99. package/src/rsc/helpers.ts +64 -0
  100. package/src/rsc/index.ts +56 -0
  101. package/src/rsc/nonce.ts +18 -0
  102. package/src/rsc/types.ts +225 -0
  103. package/src/segment-system.tsx +405 -0
  104. package/src/server/__tests__/request-context.test.ts +171 -0
  105. package/src/server/context.ts +340 -0
  106. package/src/server/handle-store.ts +230 -0
  107. package/src/server/loader-registry.ts +174 -0
  108. package/src/server/request-context.ts +470 -0
  109. package/src/server/root-layout.tsx +10 -0
  110. package/src/server/tsconfig.json +14 -0
  111. package/src/server.ts +126 -0
  112. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  113. package/src/ssr/index.tsx +215 -0
  114. package/src/types.ts +1473 -0
  115. package/src/use-loader.tsx +346 -0
  116. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  117. package/src/vite/expose-action-id.ts +344 -0
  118. package/src/vite/expose-handle-id.ts +209 -0
  119. package/src/vite/expose-loader-id.ts +357 -0
  120. package/src/vite/expose-location-state-id.ts +177 -0
  121. package/src/vite/index.ts +608 -0
  122. package/src/vite/version.d.ts +12 -0
  123. package/src/vite/virtual-entries.ts +109 -0
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Router Context using AsyncLocalStorage
3
+ *
4
+ * Provides clean dependency injection for router middleware without parameter drilling.
5
+ * All closure functions from createRSCRouter() are made available via getRouterContext().
6
+ */
7
+ import { AsyncLocalStorage } from "async_hooks";
8
+ import type { CacheScope } from "../cache/cache-scope.js";
9
+ import type {
10
+ EntryData,
11
+ InterceptEntry,
12
+ InterceptSelectorContext,
13
+ MetricsStore,
14
+ } from "../server/context.js";
15
+ import type {
16
+ HandlerContext,
17
+ ResolvedSegment,
18
+ ShouldRevalidateFn,
19
+ } from "../types.js";
20
+ import type { RouteMatchResult } from "./pattern-matching.js";
21
+
22
+ /**
23
+ * Revalidation context passed to segment resolution
24
+ */
25
+ export interface RevalidationContext {
26
+ clientSegmentIds: Set<string>;
27
+ prevParams: Record<string, string>;
28
+ request: Request;
29
+ prevUrl: URL;
30
+ nextUrl: URL;
31
+ routeKey: string;
32
+ actionContext?: {
33
+ actionId?: string;
34
+ actionUrl?: URL;
35
+ actionResult?: any;
36
+ formData?: FormData;
37
+ };
38
+ stale: boolean;
39
+ }
40
+
41
+ /**
42
+ * Result from intercept lookup
43
+ */
44
+ export interface InterceptResult {
45
+ intercept: InterceptEntry;
46
+ entry: EntryData;
47
+ }
48
+
49
+ /**
50
+ * Router context available via AsyncLocalStorage
51
+ *
52
+ * Contains closure functions from createRSCRouter() that middleware needs access to.
53
+ * Instead of passing 20+ parameters, middleware calls getRouterContext() to access them.
54
+ */
55
+ export interface RouterContext<TEnv = any> {
56
+ // Route matching
57
+ findMatch: (pathname: string) => RouteMatchResult | null;
58
+
59
+ // Manifest loading
60
+ loadManifest: (
61
+ entry: any,
62
+ routeKey: string,
63
+ pathname: string,
64
+ metricsStore?: MetricsStore,
65
+ isSSR?: boolean
66
+ ) => Promise<EntryData>;
67
+
68
+ // Entry traversal
69
+ traverseBack: (entry: EntryData) => Generator<EntryData>;
70
+
71
+ // Handler context creation
72
+ createHandlerContext: (
73
+ params: Record<string, string>,
74
+ request: Request,
75
+ searchParams: URLSearchParams,
76
+ pathname: string,
77
+ url: URL,
78
+ env: TEnv
79
+ ) => HandlerContext<any, TEnv>;
80
+
81
+ // Loader setup
82
+ setupLoaderAccess: (
83
+ ctx: HandlerContext<any, TEnv>,
84
+ loaderPromises: Map<string, Promise<any>>
85
+ ) => void;
86
+
87
+ setupLoaderAccessSilent: (
88
+ ctx: HandlerContext<any, TEnv>,
89
+ loaderPromises: Map<string, Promise<any>>
90
+ ) => void;
91
+
92
+ // Context access
93
+ getContext: () => {
94
+ getOrCreateStore: (key: string) => any;
95
+ runWithStore: <T>(
96
+ store: any,
97
+ namespace: string,
98
+ parent: any,
99
+ fn: () => T
100
+ ) => T;
101
+ };
102
+
103
+ // Metrics
104
+ getMetricsStore: () => MetricsStore | undefined;
105
+
106
+ // Cache
107
+ createCacheScope: (
108
+ cacheConfig: any,
109
+ parent: CacheScope | null
110
+ ) => CacheScope | null;
111
+
112
+ // Intercept detection
113
+ findInterceptForRoute: (
114
+ routeKey: string,
115
+ parentEntry: EntryData | null,
116
+ selectorContext: InterceptSelectorContext,
117
+ isAction: boolean
118
+ ) => InterceptResult | null;
119
+
120
+ // Segment resolution (with revalidation)
121
+ resolveAllSegmentsWithRevalidation: (
122
+ entries: EntryData[],
123
+ routeKey: string,
124
+ params: Record<string, string>,
125
+ handlerContext: HandlerContext<any, TEnv>,
126
+ clientSegmentSet: Set<string>,
127
+ prevParams: Record<string, string>,
128
+ request: Request,
129
+ prevUrl: URL,
130
+ nextUrl: URL,
131
+ loaderPromises: Map<string, Promise<any>>,
132
+ actionContext: any | undefined,
133
+ interceptResult: InterceptResult | null,
134
+ localRouteName: string,
135
+ pathname: string
136
+ ) => Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }>;
137
+
138
+ // Generator-based segment resolution (for pipeline)
139
+ resolveAllSegmentsWithRevalidationGenerator?: (
140
+ entries: EntryData[],
141
+ routeKey: string,
142
+ params: Record<string, string>,
143
+ handlerContext: HandlerContext<any, TEnv>,
144
+ clientSegmentSet: Set<string>,
145
+ prevParams: Record<string, string>,
146
+ request: Request,
147
+ prevUrl: URL,
148
+ nextUrl: URL,
149
+ loaderPromises: Map<string, Promise<any>>,
150
+ actionContext?: any
151
+ ) => AsyncGenerator<ResolvedSegment | { __type: "id"; id: string }>;
152
+
153
+ // Intercept resolution
154
+ resolveInterceptEntry: (
155
+ intercept: InterceptEntry,
156
+ entry: EntryData,
157
+ params: Record<string, string>,
158
+ handlerContext: HandlerContext<any, TEnv>,
159
+ belongsToRoute: boolean,
160
+ revalidationContext?: RevalidationContext
161
+ ) => Promise<ResolvedSegment[]>;
162
+
163
+ // Collect with markers
164
+ collectWithMarkers?: <T>(
165
+ gen: AsyncGenerator<T | { __type: "id"; id: string }>
166
+ ) => Promise<{ items: T[]; matchedIds: string[] }>;
167
+
168
+ // Revalidation evaluation
169
+ evaluateRevalidation: (params: {
170
+ segment: ResolvedSegment;
171
+ prevParams: Record<string, string>;
172
+ getPrevSegment: (() => Promise<ResolvedSegment | undefined>) | null;
173
+ request: Request;
174
+ prevUrl: URL;
175
+ nextUrl: URL;
176
+ revalidations: Array<{ name: string; fn: any }>;
177
+ routeKey: string;
178
+ context: HandlerContext<any, TEnv>;
179
+ actionContext?: any;
180
+ stale?: boolean;
181
+ }) => Promise<boolean>;
182
+
183
+ // Request context
184
+ getRequestContext: () =>
185
+ | {
186
+ waitUntil: (fn: () => Promise<void>) => void;
187
+ _handleStore?: any;
188
+ }
189
+ | undefined;
190
+
191
+ // Simple segment resolution (without revalidation - for full match)
192
+ resolveAllSegments: (
193
+ entries: EntryData[],
194
+ routeKey: string,
195
+ params: Record<string, string>,
196
+ handlerContext: HandlerContext<any, TEnv>,
197
+ loaderPromises: Map<string, Promise<any>>
198
+ ) => Promise<ResolvedSegment[]>;
199
+
200
+ // Generator-based simple resolution
201
+ resolveAllSegmentsGenerator?: (
202
+ entries: EntryData[],
203
+ routeKey: string,
204
+ params: Record<string, string>,
205
+ handlerContext: HandlerContext<any, TEnv>,
206
+ loaderPromises: Map<string, Promise<any>>
207
+ ) => AsyncGenerator<ResolvedSegment | { __type: "id"; id: string }>;
208
+
209
+ // Collect segments from generator
210
+ collectSegmentsFromGenerator?: <T>(
211
+ gen: AsyncGenerator<T | { __type: "id"; id: string }>
212
+ ) => Promise<T[]>;
213
+
214
+ // Handle store
215
+ createHandleStore: () => any;
216
+
217
+ // Loaders-only resolution (for full match cache hit - no revalidation)
218
+ resolveLoadersOnly?: (
219
+ entries: EntryData[],
220
+ handlerContext: HandlerContext<any, TEnv>
221
+ ) => Promise<ResolvedSegment[]>;
222
+
223
+ // Loaders-only resolution (for cache hit scenarios)
224
+ resolveLoadersOnlyWithRevalidation?: (
225
+ entries: EntryData[],
226
+ handlerContext: HandlerContext<any, TEnv>,
227
+ clientSegmentSet: Set<string>,
228
+ prevParams: Record<string, string>,
229
+ request: Request,
230
+ prevUrl: URL,
231
+ nextUrl: URL,
232
+ routeKey: string,
233
+ actionContext?: any
234
+ ) => Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }>;
235
+
236
+ // Entry revalidation map
237
+ buildEntryRevalidateMap?: (
238
+ entries: EntryData[]
239
+ ) => Map<string, { revalidate: ShouldRevalidateFn[] }>;
240
+
241
+ // Intercept loaders only (for cache hit + intercept scenarios)
242
+ resolveInterceptLoadersOnly?: (
243
+ intercept: InterceptEntry,
244
+ entry: EntryData,
245
+ params: Record<string, string>,
246
+ handlerContext: HandlerContext<any, TEnv>,
247
+ belongsToRoute: boolean,
248
+ revalidationContext: {
249
+ clientSegmentIds: Set<string>;
250
+ prevParams: Record<string, string>;
251
+ request: Request;
252
+ prevUrl: URL;
253
+ nextUrl: URL;
254
+ routeKey: string;
255
+ actionContext?: any;
256
+ stale?: boolean;
257
+ }
258
+ ) => Promise<{
259
+ loaderDataPromise: Promise<any[]> | any[];
260
+ loaderIds: string[];
261
+ } | null>;
262
+ }
263
+
264
+ // AsyncLocalStorage instance for router context
265
+ const routerContext = new AsyncLocalStorage<RouterContext<any>>();
266
+
267
+ /**
268
+ * Get router dependencies from AsyncLocalStorage context
269
+ *
270
+ * @throws Error if called outside of router context (runWithRouterContext)
271
+ */
272
+ export function getRouterContext<TEnv = any>(): RouterContext<TEnv> {
273
+ const deps = routerContext.getStore();
274
+ if (!deps) {
275
+ throw new Error(
276
+ "getRouterContext() called outside of router context. " +
277
+ "Ensure code is running inside runWithRouterContext()."
278
+ );
279
+ }
280
+ return deps as RouterContext<TEnv>;
281
+ }
282
+
283
+ /**
284
+ * Run a function with router dependencies available via getRouterContext()
285
+ *
286
+ * All async code within fn() can call getRouterContext() to access router closures.
287
+ * This works across async boundaries thanks to AsyncLocalStorage.
288
+ *
289
+ * @param deps Router dependencies to make available
290
+ * @param fn Function to run with dependencies available
291
+ * @returns Result of fn()
292
+ */
293
+ export function runWithRouterContext<T, TEnv = any>(
294
+ deps: RouterContext<TEnv>,
295
+ fn: () => T
296
+ ): T {
297
+ return routerContext.run(deps, fn);
298
+ }
299
+
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Router Internal Types
3
+ *
4
+ * Shared types for router module utilities.
5
+ */
6
+
7
+ import type { ReactNode } from "react";
8
+ import type { EntryData } from "../server/context";
9
+ import type {
10
+ ResolvedSegment,
11
+ HandlerContext,
12
+ ErrorBoundaryHandler,
13
+ NotFoundBoundaryHandler,
14
+ } from "../types";
15
+
16
+ /**
17
+ * Result of resolving loaders with revalidation
18
+ * Contains both segments to render and all matched segment IDs
19
+ */
20
+ export interface LoaderRevalidationResult {
21
+ segments: ResolvedSegment[];
22
+ matchedIds: string[];
23
+ }
24
+
25
+ /**
26
+ * Result of resolving segments with revalidation
27
+ * Contains both segments to render and all matched segment IDs
28
+ */
29
+ export interface SegmentRevalidationResult {
30
+ segments: ResolvedSegment[];
31
+ matchedIds: string[];
32
+ }
33
+
34
+ /**
35
+ * Action context type for revalidation
36
+ */
37
+ export type ActionContext = {
38
+ actionId?: string;
39
+ actionUrl?: URL;
40
+ actionResult?: any;
41
+ formData?: FormData;
42
+ };
43
+
44
+ /**
45
+ * Dependencies passed to segment resolution functions
46
+ * These are created within createRSCRouter and passed to extracted utilities
47
+ */
48
+ export interface RouterDependencies<TEnv> {
49
+ findNearestErrorBoundary: (
50
+ entry: EntryData | null
51
+ ) => ReactNode | ErrorBoundaryHandler | null;
52
+ findNearestNotFoundBoundary: (
53
+ entry: EntryData | null
54
+ ) => ReactNode | NotFoundBoundaryHandler | null;
55
+ }
56
+
57
+ /**
58
+ * Title descriptor types for template support
59
+ */
60
+ export type TitleDescriptor =
61
+ | string
62
+ | { template: string; default: string } // For layouts - template applied to child titles
63
+ | { absolute: string }; // Bypass parent template
64
+
65
+ /**
66
+ * Unset descriptor to remove inherited meta
67
+ * Key format matches getMetaKey output: "title", "name:description", "property:og:image"
68
+ */
69
+ export type UnsetDescriptor = { unset: string };
70
+
71
+ /**
72
+ * Base meta descriptor types (sync values)
73
+ */
74
+ export type MetaDescriptorBase =
75
+ | { charSet: "utf-8" }
76
+ | { title: TitleDescriptor }
77
+ | { name: string; content: string }
78
+ | { property: string; content: string }
79
+ | { httpEquiv: string; content: string }
80
+ | { "script:ld+json": LdJsonObject }
81
+ | { tagName: "meta" | "link"; [name: string]: string }
82
+ | UnsetDescriptor
83
+ | { [name: string]: unknown };
84
+
85
+ /**
86
+ * Meta descriptor that can be sync or async.
87
+ * Use Promise<MetaDescriptorBase> for streaming meta that resolves after initial render.
88
+ */
89
+ export type MetaDescriptor = MetaDescriptorBase | Promise<MetaDescriptorBase>;
90
+
91
+ type LdJsonObject = { [Key in string]: LdJsonValue } & {
92
+ [Key in string]?: LdJsonValue | undefined;
93
+ };
94
+ type LdJsonArray = LdJsonValue[] | readonly LdJsonValue[];
95
+ type LdJsonPrimitive = string | number | boolean | null;
96
+ type LdJsonValue = LdJsonPrimitive | LdJsonObject | LdJsonArray;