@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.
- package/CLAUDE.md +43 -0
- package/README.md +19 -0
- package/dist/bin/rango.js +227 -0
- package/dist/vite/index.js +3039 -0
- package/package.json +171 -0
- package/skills/caching/SKILL.md +191 -0
- package/skills/debug-manifest/SKILL.md +108 -0
- package/skills/document-cache/SKILL.md +180 -0
- package/skills/fonts/SKILL.md +165 -0
- package/skills/hooks/SKILL.md +442 -0
- package/skills/intercept/SKILL.md +190 -0
- package/skills/layout/SKILL.md +213 -0
- package/skills/links/SKILL.md +180 -0
- package/skills/loader/SKILL.md +246 -0
- package/skills/middleware/SKILL.md +202 -0
- package/skills/mime-routes/SKILL.md +124 -0
- package/skills/parallel/SKILL.md +228 -0
- package/skills/prerender/SKILL.md +283 -0
- package/skills/rango/SKILL.md +54 -0
- package/skills/response-routes/SKILL.md +358 -0
- package/skills/route/SKILL.md +173 -0
- package/skills/router-setup/SKILL.md +346 -0
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +78 -0
- package/skills/typesafety/SKILL.md +394 -0
- package/src/__internal.ts +175 -0
- package/src/bin/rango.ts +24 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +913 -0
- package/src/browser/navigation-client.ts +165 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +600 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +346 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/mount-context.ts +32 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +203 -0
- package/src/browser/react/use-href.tsx +40 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-mount.ts +31 -0
- package/src/browser/react/use-navigation.ts +140 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +164 -0
- package/src/browser/rsc-router.tsx +352 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/segment-structure-assert.ts +67 -0
- package/src/browser/server-action-bridge.ts +762 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +478 -0
- package/src/build/generate-manifest.ts +377 -0
- package/src/build/generate-route-types.ts +828 -0
- package/src/build/index.ts +36 -0
- package/src/build/route-trie.ts +239 -0
- package/src/cache/cache-scope.ts +563 -0
- package/src/cache/cf/cf-cache-store.ts +428 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/document-cache.ts +340 -0
- package/src/cache/index.ts +58 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +392 -0
- package/src/client.rsc.tsx +83 -0
- package/src/client.tsx +643 -0
- package/src/component-utils.ts +76 -0
- package/src/components/DefaultDocument.tsx +23 -0
- package/src/debug.ts +233 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +295 -0
- package/src/handle.ts +130 -0
- package/src/handles/MetaTags.tsx +193 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/host/cookie-handler.ts +159 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +56 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +330 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +138 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +202 -0
- package/src/href-context.ts +33 -0
- package/src/index.rsc.ts +121 -0
- package/src/index.ts +165 -0
- package/src/loader.rsc.ts +207 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/prerender/param-hash.ts +35 -0
- package/src/prerender/store.ts +40 -0
- package/src/prerender.ts +156 -0
- package/src/reverse.ts +267 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +193 -0
- package/src/route-definition.ts +1431 -0
- package/src/route-map-builder.ts +242 -0
- package/src/route-types.ts +220 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +158 -0
- package/src/router/intercept-resolution.ts +387 -0
- package/src/router/loader-resolution.ts +327 -0
- package/src/router/manifest.ts +216 -0
- package/src/router/match-api.ts +621 -0
- package/src/router/match-context.ts +264 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +382 -0
- package/src/router/match-middleware/cache-store.ts +276 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +281 -0
- package/src/router/match-middleware/segment-resolution.ts +184 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +213 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.ts +791 -0
- package/src/router/pattern-matching.ts +407 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +301 -0
- package/src/router/segment-resolution.ts +1315 -0
- package/src/router/trie-matching.ts +172 -0
- package/src/router/types.ts +163 -0
- package/src/router.gen.ts +6 -0
- package/src/router.ts +2423 -0
- package/src/rsc/handler.ts +1443 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +236 -0
- package/src/segment-system.tsx +442 -0
- package/src/server/context.ts +466 -0
- package/src/server/handle-store.ts +229 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +554 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +171 -0
- package/src/ssr/index.tsx +296 -0
- package/src/theme/ThemeProvider.tsx +291 -0
- package/src/theme/ThemeScript.tsx +61 -0
- package/src/theme/constants.ts +59 -0
- package/src/theme/index.ts +58 -0
- package/src/theme/theme-context.ts +70 -0
- package/src/theme/theme-script.ts +152 -0
- package/src/theme/types.ts +182 -0
- package/src/theme/use-theme.ts +44 -0
- package/src/types.ts +1757 -0
- package/src/urls.gen.ts +8 -0
- package/src/urls.ts +1282 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +426 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/expose-prerender-handler-id.ts +429 -0
- package/src/vite/index.ts +2068 -0
- package/src/vite/package-resolution.ts +125 -0
- package/src/vite/version.d.ts +12 -0
- package/src/vite/virtual-entries.ts +114 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { PartialCacheOptions, ErrorBoundaryHandler, Handler, LoaderDefinition, MiddlewareFn, NotFoundBoundaryHandler, ShouldRevalidateFn } from "../types";
|
|
4
|
+
import { invariant } from "../errors";
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Performance Metrics Types
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Performance metric entry for a single measured operation
|
|
12
|
+
*
|
|
13
|
+
* @internal This type is an implementation detail and may change without notice.
|
|
14
|
+
*/
|
|
15
|
+
export interface PerformanceMetric {
|
|
16
|
+
label: string; // e.g., "route-matching", "loader:UserLoader"
|
|
17
|
+
duration: number; // milliseconds
|
|
18
|
+
startTime: number; // relative to request start
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Request-scoped metrics store
|
|
23
|
+
*
|
|
24
|
+
* @internal This type is an implementation detail and may change without notice.
|
|
25
|
+
*/
|
|
26
|
+
export interface MetricsStore {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
requestStart: number;
|
|
29
|
+
metrics: PerformanceMetric[];
|
|
30
|
+
}
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// RSC Router Context
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Cache configuration for an entry
|
|
37
|
+
* When set, this entry and its children will use this cache config
|
|
38
|
+
* unless overridden by a nested cache() call.
|
|
39
|
+
*
|
|
40
|
+
* @internal This type is an implementation detail and may change without notice.
|
|
41
|
+
*/
|
|
42
|
+
export type EntryCacheConfig = {
|
|
43
|
+
/** Cache options (false means caching disabled for this entry) - ttl is optional, uses defaults */
|
|
44
|
+
options: PartialCacheOptions | false;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Entry data structure for manifest
|
|
49
|
+
*
|
|
50
|
+
* @internal This type is an implementation detail and may change without notice.
|
|
51
|
+
*/
|
|
52
|
+
export type EntryPropCommon = {
|
|
53
|
+
id: string;
|
|
54
|
+
shortCode: string; // Short identifier for network efficiency (e.g., "L0", "P1", "R2")
|
|
55
|
+
parent: EntryData | null;
|
|
56
|
+
/** Cache configuration for this entry (set by cache() DSL) */
|
|
57
|
+
cache?: EntryCacheConfig;
|
|
58
|
+
/** URL prefix from include() scope, used for MountContext on client */
|
|
59
|
+
mountPath?: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @internal This type is an implementation detail and may change without notice.
|
|
64
|
+
*/
|
|
65
|
+
export type EntryPropDatas = {
|
|
66
|
+
middleware: MiddlewareFn<any, any>[];
|
|
67
|
+
revalidate: ShouldRevalidateFn<any, any>[];
|
|
68
|
+
errorBoundary: (ReactNode | ErrorBoundaryHandler)[];
|
|
69
|
+
notFoundBoundary: (ReactNode | NotFoundBoundaryHandler)[];
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Loader entry stored in EntryData
|
|
74
|
+
* Contains the loader definition and its revalidation rules
|
|
75
|
+
*
|
|
76
|
+
* @internal This type is an implementation detail and may change without notice.
|
|
77
|
+
*/
|
|
78
|
+
export type LoaderEntry = {
|
|
79
|
+
loader: LoaderDefinition<any>;
|
|
80
|
+
revalidate: ShouldRevalidateFn<any, any>[];
|
|
81
|
+
/** Cache config for this specific loader (loaders are NOT cached by default) */
|
|
82
|
+
cache?: EntryCacheConfig;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Segments state for intercept context
|
|
87
|
+
* Matches the structure from useSegments() for consistency
|
|
88
|
+
*
|
|
89
|
+
* @internal This type is an implementation detail and may change without notice.
|
|
90
|
+
*/
|
|
91
|
+
export type InterceptSegmentsState = {
|
|
92
|
+
/** URL path segments (e.g., /shop/products/123 → ["shop", "products", "123"]) */
|
|
93
|
+
path: readonly string[];
|
|
94
|
+
/** Matched segment IDs in order (layouts and routes only, e.g., ["L0", "L0L1", "L0L1R0"]) */
|
|
95
|
+
ids: readonly string[];
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Context passed to intercept selector functions (when())
|
|
100
|
+
* Contains navigation context to determine if interception should occur.
|
|
101
|
+
*
|
|
102
|
+
* Note: when() is evaluated during route matching, BEFORE middleware runs.
|
|
103
|
+
* So ctx.get()/ctx.use() are not available, but env (platform bindings) is.
|
|
104
|
+
*
|
|
105
|
+
* @internal This type is an implementation detail and may change without notice.
|
|
106
|
+
*/
|
|
107
|
+
export type InterceptSelectorContext<TEnv = any> = {
|
|
108
|
+
from: URL; // Source URL (where user is coming from)
|
|
109
|
+
to: URL; // Destination URL (where user is navigating to)
|
|
110
|
+
params: Record<string, string>; // Matched route params
|
|
111
|
+
request: Request; // The HTTP request object
|
|
112
|
+
env: TEnv; // Platform bindings (Cloudflare env, etc.)
|
|
113
|
+
segments: InterceptSegmentsState; // Client's current segments (where navigating FROM)
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Selector function for conditional interception
|
|
118
|
+
* Returns true to intercept, false to skip and fall through to route handler
|
|
119
|
+
*
|
|
120
|
+
* @internal This type is an implementation detail and may change without notice.
|
|
121
|
+
*/
|
|
122
|
+
export type InterceptWhenFn<TEnv = any> = (ctx: InterceptSelectorContext<TEnv>) => boolean;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Intercept entry stored in EntryData
|
|
126
|
+
* Contains the slot name, route to intercept, and handler
|
|
127
|
+
*
|
|
128
|
+
* @internal This type is an implementation detail and may change without notice.
|
|
129
|
+
*/
|
|
130
|
+
export type InterceptEntry = {
|
|
131
|
+
slotName: `@${string}`; // e.g., "@modal"
|
|
132
|
+
routeName: string; // e.g., "card"
|
|
133
|
+
handler: ReactNode | Handler<any, any, any>;
|
|
134
|
+
middleware: MiddlewareFn<any, any>[];
|
|
135
|
+
revalidate: ShouldRevalidateFn<any, any>[];
|
|
136
|
+
errorBoundary: (ReactNode | ErrorBoundaryHandler)[];
|
|
137
|
+
notFoundBoundary: (ReactNode | NotFoundBoundaryHandler)[];
|
|
138
|
+
loader: LoaderEntry[];
|
|
139
|
+
loading?: ReactNode | false;
|
|
140
|
+
layout?: ReactNode | Handler<any, any, any>; // Wrapper layout with <Outlet /> for content
|
|
141
|
+
when: InterceptWhenFn[]; // Selector conditions - all must return true to intercept
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export type EntryPropSegments = {
|
|
145
|
+
loader: LoaderEntry[];
|
|
146
|
+
layout: EntryData[];
|
|
147
|
+
parallel: EntryData[]; // type: "parallel" entries with their own loaders/revalidate/loading
|
|
148
|
+
intercept: InterceptEntry[]; // intercept definitions for soft navigation
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export type EntryData =
|
|
152
|
+
| ({
|
|
153
|
+
type: "route";
|
|
154
|
+
handler: Handler<any, any, any>;
|
|
155
|
+
loading?: ReactNode | false;
|
|
156
|
+
/** URL pattern for this route (used by path() in urls()) */
|
|
157
|
+
pattern?: string;
|
|
158
|
+
/** Set when handler is a createPrerenderHandler definition */
|
|
159
|
+
isPrerender?: true;
|
|
160
|
+
/** Original PrerenderHandlerDefinition (for build-time getParams access) */
|
|
161
|
+
prerenderDef?: { getParams?: () => Promise<any[]> | any[]; options?: { passthrough?: boolean } };
|
|
162
|
+
/** Response type for non-RSC routes (json, text, image, any) */
|
|
163
|
+
responseType?: string;
|
|
164
|
+
} & EntryPropCommon &
|
|
165
|
+
EntryPropDatas &
|
|
166
|
+
EntryPropSegments)
|
|
167
|
+
| ({
|
|
168
|
+
type: "layout";
|
|
169
|
+
handler: ReactNode | Handler<any, any, any>;
|
|
170
|
+
loading?: ReactNode | false;
|
|
171
|
+
} & EntryPropCommon &
|
|
172
|
+
EntryPropDatas &
|
|
173
|
+
EntryPropSegments)
|
|
174
|
+
| ({
|
|
175
|
+
type: "parallel";
|
|
176
|
+
handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
|
|
177
|
+
loading?: ReactNode | false;
|
|
178
|
+
} & EntryPropCommon &
|
|
179
|
+
EntryPropDatas &
|
|
180
|
+
EntryPropSegments)
|
|
181
|
+
| ({
|
|
182
|
+
type: "cache";
|
|
183
|
+
/** Cache entries create cache boundaries and render like layouts (with Outlet) */
|
|
184
|
+
handler: ReactNode | Handler<any, any, any>;
|
|
185
|
+
loading?: ReactNode | false;
|
|
186
|
+
} & EntryPropCommon &
|
|
187
|
+
EntryPropDatas &
|
|
188
|
+
EntryPropSegments);
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Tracked include info for build-time manifest generation
|
|
192
|
+
*/
|
|
193
|
+
export interface TrackedInclude {
|
|
194
|
+
prefix: string;
|
|
195
|
+
fullPrefix: string;
|
|
196
|
+
namePrefix?: string;
|
|
197
|
+
patterns: unknown; // UrlPatterns
|
|
198
|
+
lazy: boolean;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Context stored in AsyncLocalStorage
|
|
203
|
+
*/
|
|
204
|
+
interface HelperContext {
|
|
205
|
+
manifest: Map<string, EntryData>;
|
|
206
|
+
namespace: string;
|
|
207
|
+
parent: EntryData | null;
|
|
208
|
+
counters: Record<string, number>;
|
|
209
|
+
forRoute?: string;
|
|
210
|
+
mountIndex?: number;
|
|
211
|
+
metrics?: MetricsStore;
|
|
212
|
+
/** True when rendering for SSR (document requests) */
|
|
213
|
+
isSSR?: boolean;
|
|
214
|
+
/** URL patterns map for path() routes (route name -> pattern) */
|
|
215
|
+
patterns?: Map<string, string>;
|
|
216
|
+
/** URL patterns grouped by include prefix for separate entry creation */
|
|
217
|
+
patternsByPrefix?: Map<string, Map<string, string>>;
|
|
218
|
+
/** Trailing slash config per route name */
|
|
219
|
+
trailingSlash?: Map<string, "never" | "always" | "ignore">;
|
|
220
|
+
/** URL prefix from include() - applied to all path() patterns */
|
|
221
|
+
urlPrefix?: string;
|
|
222
|
+
/** Name prefix from include() - applied to all named routes */
|
|
223
|
+
namePrefix?: string;
|
|
224
|
+
/** Run helper for cleaner middleware code */
|
|
225
|
+
run?: <T>(fn: () => T | Promise<T>) => T | Promise<T>;
|
|
226
|
+
/** Tracked includes for build-time manifest generation */
|
|
227
|
+
trackedIncludes?: TrackedInclude[];
|
|
228
|
+
}
|
|
229
|
+
export const RSCRouterContext: AsyncLocalStorage<HelperContext> =
|
|
230
|
+
new AsyncLocalStorage<HelperContext>();
|
|
231
|
+
|
|
232
|
+
export const getContext = (): {
|
|
233
|
+
context: AsyncLocalStorage<HelperContext>;
|
|
234
|
+
getStore: () => HelperContext;
|
|
235
|
+
getParent: () => EntryData | null;
|
|
236
|
+
getOrCreateStore: (forRoute?: string) => HelperContext;
|
|
237
|
+
getNextIndex: (
|
|
238
|
+
type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate"
|
|
239
|
+
) => string;
|
|
240
|
+
getShortCode: (
|
|
241
|
+
type: "layout" | "parallel" | "route" | "loader" | "cache"
|
|
242
|
+
) => string;
|
|
243
|
+
run: <T>(
|
|
244
|
+
namespace: string,
|
|
245
|
+
parent: EntryData | null,
|
|
246
|
+
callback: (...args: any[]) => T
|
|
247
|
+
) => T;
|
|
248
|
+
runWithStore: <T>(
|
|
249
|
+
store: HelperContext,
|
|
250
|
+
namespace: string,
|
|
251
|
+
parent: EntryData | null,
|
|
252
|
+
callback: (...args: any[]) => T
|
|
253
|
+
) => T;
|
|
254
|
+
} => {
|
|
255
|
+
const context = RSCRouterContext;
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
context,
|
|
259
|
+
getOrCreateStore: (forRoute?: string): HelperContext => {
|
|
260
|
+
let store = RSCRouterContext.getStore();
|
|
261
|
+
if (!store) {
|
|
262
|
+
store = {
|
|
263
|
+
manifest: new Map<string, EntryData>(),
|
|
264
|
+
namespace: "",
|
|
265
|
+
parent: null,
|
|
266
|
+
forRoute,
|
|
267
|
+
counters: {},
|
|
268
|
+
patterns: new Map<string, string>(),
|
|
269
|
+
patternsByPrefix: new Map<string, Map<string, string>>(),
|
|
270
|
+
trailingSlash: new Map<string, "never" | "always" | "ignore">(),
|
|
271
|
+
} satisfies HelperContext;
|
|
272
|
+
}
|
|
273
|
+
return store;
|
|
274
|
+
},
|
|
275
|
+
getStore: (): HelperContext => {
|
|
276
|
+
const store = context.getStore();
|
|
277
|
+
if (!store) {
|
|
278
|
+
throw new Error(
|
|
279
|
+
"RSC Router context store is not available. Make sure to run within RSC Router context."
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
return store;
|
|
283
|
+
},
|
|
284
|
+
getParent: (): EntryData | null => {
|
|
285
|
+
const store = context.getStore();
|
|
286
|
+
if (!store) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return store.parent;
|
|
291
|
+
},
|
|
292
|
+
getNextIndex: (
|
|
293
|
+
type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate"
|
|
294
|
+
) => {
|
|
295
|
+
const store = context.getStore();
|
|
296
|
+
invariant(store, "No context RSCRouterContext available");
|
|
297
|
+
store.counters[type] ??= 0;
|
|
298
|
+
const index = store.counters[type];
|
|
299
|
+
store.counters[type] = index + 1;
|
|
300
|
+
return `$${type}.${index}`;
|
|
301
|
+
},
|
|
302
|
+
getShortCode: (type: "layout" | "parallel" | "route" | "loader" | "cache") => {
|
|
303
|
+
const store = context.getStore();
|
|
304
|
+
invariant(store, "No context RSCRouterContext available");
|
|
305
|
+
|
|
306
|
+
const parent = store.parent;
|
|
307
|
+
const prefix = type === "layout" ? "L" : type === "parallel" ? "P" : type === "loader" ? "D" : type === "cache" ? "C" : "R";
|
|
308
|
+
const mountPrefix = store.mountIndex !== undefined ? `M${store.mountIndex}` : "";
|
|
309
|
+
|
|
310
|
+
if (!parent) {
|
|
311
|
+
// Root entry: prefix with mount index and use mount-scoped counter
|
|
312
|
+
const counterKey = mountPrefix ? `${mountPrefix}_root_${type}` : `root_${type}`;
|
|
313
|
+
store.counters[counterKey] ??= 0;
|
|
314
|
+
const index = store.counters[counterKey];
|
|
315
|
+
store.counters[counterKey] = index + 1;
|
|
316
|
+
return `${mountPrefix}${prefix}${index}`;
|
|
317
|
+
} else {
|
|
318
|
+
// Child entry: use parent-scoped counter (parent already has M prefix)
|
|
319
|
+
const counterKey = `${parent.shortCode}_${type}`;
|
|
320
|
+
store.counters[counterKey] ??= 0;
|
|
321
|
+
const index = store.counters[counterKey];
|
|
322
|
+
store.counters[counterKey] = index + 1;
|
|
323
|
+
return `${parent.shortCode}${prefix}${index}`;
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
runWithStore: <T>(
|
|
327
|
+
store: HelperContext,
|
|
328
|
+
namespace: string,
|
|
329
|
+
parent: EntryData | null,
|
|
330
|
+
callback: (...args: any[]) => T
|
|
331
|
+
): T => {
|
|
332
|
+
return context.run(
|
|
333
|
+
{
|
|
334
|
+
manifest: store.manifest,
|
|
335
|
+
namespace,
|
|
336
|
+
parent: parent || null,
|
|
337
|
+
counters: store.counters,
|
|
338
|
+
forRoute: store.forRoute,
|
|
339
|
+
mountIndex: store.mountIndex,
|
|
340
|
+
metrics: store.metrics,
|
|
341
|
+
isSSR: store.isSSR,
|
|
342
|
+
patterns: store.patterns,
|
|
343
|
+
trailingSlash: store.trailingSlash,
|
|
344
|
+
urlPrefix: store.urlPrefix,
|
|
345
|
+
namePrefix: store.namePrefix,
|
|
346
|
+
trackedIncludes: store.trackedIncludes,
|
|
347
|
+
},
|
|
348
|
+
callback
|
|
349
|
+
);
|
|
350
|
+
},
|
|
351
|
+
run: <T>(
|
|
352
|
+
namespace: string,
|
|
353
|
+
parent: EntryData | null,
|
|
354
|
+
callback: (...args: any[]) => T
|
|
355
|
+
) => {
|
|
356
|
+
const store = context.getStore();
|
|
357
|
+
// Preserve parent counters to ensure globally unique shortCodes
|
|
358
|
+
const counters = store?.counters || {};
|
|
359
|
+
const manifest = store ? store.manifest : new Map<string, EntryData>();
|
|
360
|
+
const patterns = store?.patterns || new Map<string, string>();
|
|
361
|
+
const trailingSlash = store?.trailingSlash || new Map<string, "never" | "always" | "ignore">();
|
|
362
|
+
return context.run(
|
|
363
|
+
{
|
|
364
|
+
manifest,
|
|
365
|
+
namespace,
|
|
366
|
+
parent: parent || null,
|
|
367
|
+
counters,
|
|
368
|
+
forRoute: store?.forRoute,
|
|
369
|
+
mountIndex: store?.mountIndex,
|
|
370
|
+
metrics: store?.metrics,
|
|
371
|
+
isSSR: store?.isSSR,
|
|
372
|
+
patterns,
|
|
373
|
+
trailingSlash,
|
|
374
|
+
urlPrefix: store?.urlPrefix,
|
|
375
|
+
namePrefix: store?.namePrefix,
|
|
376
|
+
trackedIncludes: store?.trackedIncludes,
|
|
377
|
+
},
|
|
378
|
+
callback
|
|
379
|
+
);
|
|
380
|
+
},
|
|
381
|
+
};
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Run a callback with specific URL and name prefixes
|
|
386
|
+
* Used by include() to apply prefixes to nested patterns
|
|
387
|
+
*/
|
|
388
|
+
export function runWithPrefixes<T>(
|
|
389
|
+
urlPrefix: string,
|
|
390
|
+
namePrefix: string | undefined,
|
|
391
|
+
callback: () => T
|
|
392
|
+
): T {
|
|
393
|
+
const store = RSCRouterContext.getStore();
|
|
394
|
+
if (!store) {
|
|
395
|
+
throw new Error("runWithPrefixes must be called within router context");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Combine prefixes if there are existing ones
|
|
399
|
+
const combinedUrlPrefix = store.urlPrefix
|
|
400
|
+
? `${store.urlPrefix}${urlPrefix}`
|
|
401
|
+
: urlPrefix;
|
|
402
|
+
const combinedNamePrefix = namePrefix
|
|
403
|
+
? store.namePrefix
|
|
404
|
+
? `${store.namePrefix}.${namePrefix}`
|
|
405
|
+
: namePrefix
|
|
406
|
+
: store.namePrefix;
|
|
407
|
+
|
|
408
|
+
return RSCRouterContext.run(
|
|
409
|
+
{
|
|
410
|
+
...store,
|
|
411
|
+
urlPrefix: combinedUrlPrefix,
|
|
412
|
+
namePrefix: combinedNamePrefix,
|
|
413
|
+
},
|
|
414
|
+
callback
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Get current URL prefix from context
|
|
420
|
+
*/
|
|
421
|
+
export function getUrlPrefix(): string {
|
|
422
|
+
const store = RSCRouterContext.getStore();
|
|
423
|
+
return store?.urlPrefix || "";
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Get current name prefix from context
|
|
428
|
+
*/
|
|
429
|
+
export function getNamePrefix(): string | undefined {
|
|
430
|
+
const store = RSCRouterContext.getStore();
|
|
431
|
+
return store?.namePrefix;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Export HelperContext type for use in other modules
|
|
435
|
+
export type { HelperContext };
|
|
436
|
+
|
|
437
|
+
// ============================================================================
|
|
438
|
+
// Performance Metrics Helpers
|
|
439
|
+
// ============================================================================
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Track performance of a code block (no-op if metrics not enabled)
|
|
443
|
+
* Returns a done() callback to mark completion and record duration
|
|
444
|
+
*
|
|
445
|
+
* @example
|
|
446
|
+
* ```typescript
|
|
447
|
+
* const done = track("route-matching");
|
|
448
|
+
* // ... do work ...
|
|
449
|
+
* done(); // Records duration
|
|
450
|
+
* ```
|
|
451
|
+
*/
|
|
452
|
+
export function track(label: string): () => void {
|
|
453
|
+
const store = RSCRouterContext.getStore();
|
|
454
|
+
|
|
455
|
+
// No-op if context unavailable or metrics not enabled
|
|
456
|
+
if (!store?.metrics?.enabled) {
|
|
457
|
+
return () => {};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const startTime = performance.now() - store.metrics.requestStart;
|
|
461
|
+
|
|
462
|
+
return () => {
|
|
463
|
+
const duration = performance.now() - store.metrics!.requestStart - startTime;
|
|
464
|
+
store.metrics!.metrics.push({ label, duration, startTime });
|
|
465
|
+
};
|
|
466
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle data structure: handleName -> segmentId -> entries[]
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* {
|
|
7
|
+
* "breadcrumbs": {
|
|
8
|
+
* "$root.layout": [{ label: "Home", href: "/" }],
|
|
9
|
+
* "shop.layout": [{ label: "Shop", href: "/shop" }],
|
|
10
|
+
* }
|
|
11
|
+
* }
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export type HandleData = Record<string, Record<string, unknown[]>>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Deep clone handle data to create a snapshot.
|
|
18
|
+
* @internal
|
|
19
|
+
*/
|
|
20
|
+
function cloneHandleData(data: HandleData): HandleData {
|
|
21
|
+
const clone: HandleData = {};
|
|
22
|
+
for (const handleName in data) {
|
|
23
|
+
clone[handleName] = {};
|
|
24
|
+
for (const segmentId in data[handleName]) {
|
|
25
|
+
clone[handleName][segmentId] = [...data[handleName][segmentId]];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return clone;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* HandleStore tracks pending handler promises and stores handle data.
|
|
33
|
+
*
|
|
34
|
+
* Combines two responsibilities:
|
|
35
|
+
* 1. Promise tracking - know when all handlers have resolved
|
|
36
|
+
* 2. Data storage - collect handle data pushed by handlers
|
|
37
|
+
* 3. Streaming - emit handle data via async iterator on each push
|
|
38
|
+
*/
|
|
39
|
+
export interface HandleStore {
|
|
40
|
+
/**
|
|
41
|
+
* Track a handler promise (non-blocking).
|
|
42
|
+
* Returns the promise unchanged - just registers it for tracking.
|
|
43
|
+
*/
|
|
44
|
+
track<T>(promise: Promise<T>): Promise<T>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Promise that resolves when all tracked handlers have settled.
|
|
48
|
+
* Does not reject - uses Promise.allSettled internally.
|
|
49
|
+
*/
|
|
50
|
+
readonly settled: Promise<void>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Push handle data for a specific handle and segment.
|
|
54
|
+
* Multiple pushes to the same handle/segment accumulate in an array.
|
|
55
|
+
* Each push triggers an emission on the stream.
|
|
56
|
+
*/
|
|
57
|
+
push(handleName: string, segmentId: string, data: unknown): void;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get all collected handle data after all handlers have settled.
|
|
61
|
+
* Returns a promise that waits for `settled`, then returns the data.
|
|
62
|
+
* The data may contain unresolved promises which RSC will stream.
|
|
63
|
+
* @deprecated Use stream() for progressive updates
|
|
64
|
+
*/
|
|
65
|
+
getData(): Promise<HandleData>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get an async iterator that yields handle data on each push.
|
|
69
|
+
* The iterator completes when all handlers have settled.
|
|
70
|
+
* Each yield contains the full accumulated state (not just the delta).
|
|
71
|
+
*/
|
|
72
|
+
stream(): AsyncGenerator<HandleData, void, unknown>;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get handle data for a specific segment (for caching).
|
|
76
|
+
* Returns data in format: { handleName: [values...] }
|
|
77
|
+
*/
|
|
78
|
+
getDataForSegment(segmentId: string): Record<string, unknown[]>;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Replay cached handle data back into the store (for cache hits).
|
|
82
|
+
* Used to restore handle data when serving cached segments.
|
|
83
|
+
*/
|
|
84
|
+
replaySegmentData(segmentId: string, segmentHandles: Record<string, unknown[]>): void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Create a new HandleStore instance.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* const handleStore = createHandleStore();
|
|
93
|
+
*
|
|
94
|
+
* // In router - track without awaiting
|
|
95
|
+
* const component = handleStore.track(entry.handler(context));
|
|
96
|
+
*
|
|
97
|
+
* // In handler - push handle data (value, promise, or async callback result)
|
|
98
|
+
* handleStore.push("breadcrumbs", segmentId, { label: "Home", href: "/" });
|
|
99
|
+
* handleStore.push("meta", segmentId, fetchMetaAsync()); // promise
|
|
100
|
+
*
|
|
101
|
+
* // Stream handle data progressively
|
|
102
|
+
* for await (const handles of handleStore.stream()) {
|
|
103
|
+
* console.log("Handle update:", handles);
|
|
104
|
+
* }
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export function createHandleStore(): HandleStore {
|
|
108
|
+
const pending: Promise<unknown>[] = [];
|
|
109
|
+
const data: HandleData = {};
|
|
110
|
+
|
|
111
|
+
// Queue for pending emissions and resolver for waiting consumer
|
|
112
|
+
let pendingEmissions: HandleData[] = [];
|
|
113
|
+
let emissionResolver: (() => void) | null = null;
|
|
114
|
+
let completed = false;
|
|
115
|
+
|
|
116
|
+
// Signal that a new emission is available
|
|
117
|
+
function signalEmission() {
|
|
118
|
+
if (emissionResolver) {
|
|
119
|
+
const resolver = emissionResolver;
|
|
120
|
+
emissionResolver = null;
|
|
121
|
+
resolver();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Wait for the next emission or completion
|
|
126
|
+
function waitForEmission(): Promise<void> {
|
|
127
|
+
if (pendingEmissions.length > 0 || completed) {
|
|
128
|
+
return Promise.resolve();
|
|
129
|
+
}
|
|
130
|
+
return new Promise((resolve) => {
|
|
131
|
+
emissionResolver = resolve;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
track<T>(promise: Promise<T>): Promise<T> {
|
|
137
|
+
pending.push(promise);
|
|
138
|
+
return promise;
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
get settled(): Promise<void> {
|
|
142
|
+
if (pending.length === 0) {
|
|
143
|
+
return Promise.resolve();
|
|
144
|
+
}
|
|
145
|
+
return Promise.allSettled(pending).then(() => {});
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
push(handleName: string, segmentId: string, value: unknown): void {
|
|
149
|
+
if (!data[handleName]) {
|
|
150
|
+
data[handleName] = {};
|
|
151
|
+
}
|
|
152
|
+
if (!data[handleName][segmentId]) {
|
|
153
|
+
data[handleName][segmentId] = [];
|
|
154
|
+
}
|
|
155
|
+
data[handleName][segmentId].push(value);
|
|
156
|
+
|
|
157
|
+
// Queue a snapshot for emission
|
|
158
|
+
pendingEmissions.push(cloneHandleData(data));
|
|
159
|
+
signalEmission();
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
getData(): Promise<HandleData> {
|
|
163
|
+
return this.settled.then(() => data);
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
async *stream(): AsyncGenerator<HandleData, void, unknown> {
|
|
167
|
+
// Set up completion handler
|
|
168
|
+
this.settled.then(() => {
|
|
169
|
+
completed = true;
|
|
170
|
+
signalEmission();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Initial small delay to batch rapid synchronous pushes
|
|
174
|
+
// This allows multiple handles pushing in quick succession to be batched
|
|
175
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
176
|
+
|
|
177
|
+
// If we already have data, yield the accumulated state
|
|
178
|
+
if (Object.keys(data).length > 0) {
|
|
179
|
+
// Clear pending emissions since we're yielding current state
|
|
180
|
+
pendingEmissions = [];
|
|
181
|
+
yield cloneHandleData(data);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Continue streaming on each push
|
|
185
|
+
while (!completed) {
|
|
186
|
+
await waitForEmission();
|
|
187
|
+
|
|
188
|
+
// Yield all pending emissions (yield latest only)
|
|
189
|
+
if (pendingEmissions.length > 0) {
|
|
190
|
+
// Skip intermediate states, yield the latest
|
|
191
|
+
const latest = pendingEmissions[pendingEmissions.length - 1];
|
|
192
|
+
pendingEmissions = [];
|
|
193
|
+
yield latest;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Final yield only if there are pending emissions that weren't yielded
|
|
198
|
+
// (handles that pushed after our last yield but before completion)
|
|
199
|
+
if (pendingEmissions.length > 0) {
|
|
200
|
+
yield cloneHandleData(data);
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
getDataForSegment(segmentId: string): Record<string, unknown[]> {
|
|
205
|
+
const result: Record<string, unknown[]> = {};
|
|
206
|
+
for (const handleName in data) {
|
|
207
|
+
if (data[handleName][segmentId]) {
|
|
208
|
+
result[handleName] = [...data[handleName][segmentId]];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return result;
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
replaySegmentData(segmentId: string, segmentHandles: Record<string, unknown[]>): void {
|
|
215
|
+
for (const handleName in segmentHandles) {
|
|
216
|
+
if (!data[handleName]) {
|
|
217
|
+
data[handleName] = {};
|
|
218
|
+
}
|
|
219
|
+
// Replace with replayed data (not append) to avoid handle bleeding between routes.
|
|
220
|
+
// When a cached segment is restored, its handles should replace any existing data
|
|
221
|
+
// for that segment, not accumulate on top of data from a different route.
|
|
222
|
+
data[handleName][segmentId] = [...segmentHandles[handleName]];
|
|
223
|
+
}
|
|
224
|
+
// Trigger emission for streaming
|
|
225
|
+
pendingEmissions.push(cloneHandleData(data));
|
|
226
|
+
signalEmission();
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|