@rangojs/router 0.0.0-experimental.ae6e7825 → 0.0.0-experimental.b02a2fec
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/README.md +76 -18
- package/dist/bin/rango.js +130 -47
- package/dist/vite/index.js +688 -433
- package/dist/vite/index.js.bak +5448 -0
- package/package.json +1 -1
- package/skills/links/SKILL.md +3 -1
- package/skills/middleware/SKILL.md +2 -0
- package/skills/prerender/SKILL.md +110 -68
- package/skills/router-setup/SKILL.md +35 -0
- package/src/__internal.ts +1 -1
- package/src/browser/app-version.ts +14 -0
- package/src/browser/navigation-bridge.ts +19 -4
- package/src/browser/navigation-client.ts +64 -63
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/partial-update.ts +27 -5
- package/src/browser/prefetch/fetch.ts +8 -2
- package/src/browser/react/Link.tsx +44 -8
- package/src/browser/react/NavigationProvider.tsx +8 -1
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-router.ts +21 -8
- package/src/browser/rsc-router.tsx +28 -22
- package/src/browser/scroll-restoration.ts +10 -8
- package/src/browser/server-action-bridge.ts +8 -18
- package/src/browser/types.ts +21 -15
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +211 -72
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/client.tsx +2 -56
- package/src/deps/browser.ts +0 -1
- package/src/handle.ts +40 -0
- package/src/index.rsc.ts +3 -1
- package/src/index.ts +12 -0
- package/src/prerender/store.ts +5 -4
- package/src/prerender.ts +138 -77
- package/src/reverse.ts +22 -1
- package/src/route-definition/dsl-helpers.ts +42 -19
- package/src/route-definition/helpers-types.ts +4 -1
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +9 -1
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-types.ts +11 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/handler-context.ts +48 -15
- package/src/router/intercept-resolution.ts +9 -4
- package/src/router/loader-resolution.ts +150 -21
- package/src/router/match-api.ts +124 -189
- package/src/router/match-middleware/cache-lookup.ts +28 -8
- package/src/router/match-middleware/segment-resolution.ts +53 -0
- package/src/router/match-result.ts +82 -4
- package/src/router/middleware-types.ts +0 -6
- package/src/router/middleware.ts +0 -3
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/prerender-match.ts +110 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-interfaces.ts +36 -4
- package/src/router/router-options.ts +37 -11
- package/src/router/segment-resolution/fresh.ts +70 -5
- package/src/router/segment-resolution/revalidation.ts +87 -9
- package/src/router.ts +53 -5
- package/src/rsc/handler.ts +472 -398
- package/src/rsc/loader-fetch.ts +18 -3
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +12 -3
- package/src/rsc/rsc-rendering.ts +8 -3
- package/src/rsc/server-action.ts +8 -2
- package/src/rsc/ssr-setup.ts +2 -2
- package/src/rsc/types.ts +6 -7
- package/src/server/context.ts +39 -2
- package/src/server/handle-store.ts +19 -0
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +131 -16
- package/src/ssr/index.tsx +4 -13
- package/src/static-handler.ts +18 -6
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +17 -11
- package/src/types/loader-types.ts +32 -5
- package/src/types/route-entry.ts +1 -1
- package/src/types/segments.ts +1 -0
- package/src/urls/path-helper-types.ts +9 -2
- package/src/urls/path-helper.ts +47 -12
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +16 -6
- package/src/use-loader.tsx +77 -5
- package/src/vite/discovery/bundle-postprocess.ts +30 -33
- package/src/vite/discovery/discover-routers.ts +5 -1
- package/src/vite/discovery/prerender-collection.ts +128 -74
- package/src/vite/discovery/state.ts +13 -4
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +60 -5
- package/src/vite/plugins/expose-id-utils.ts +12 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +30 -0
- package/src/vite/plugins/expose-internal-ids.ts +257 -40
- package/src/vite/plugins/performance-tracks.ts +57 -280
- package/src/vite/plugins/refresh-cmd.ts +88 -26
- package/src/vite/rango.ts +17 -11
- package/src/vite/router-discovery.ts +178 -37
- package/src/vite/utils/prerender-utils.ts +18 -0
- package/src/vite/utils/shared-utils.ts +3 -2
- package/src/browser/debug-channel.ts +0 -112
package/src/rsc/loader-fetch.ts
CHANGED
|
@@ -168,11 +168,17 @@ export async function handleLoaderFetch<TEnv>(
|
|
|
168
168
|
loaderResult: unknown;
|
|
169
169
|
}
|
|
170
170
|
const loaderPayload: LoaderPayload = { loaderResult: result };
|
|
171
|
-
const debugChannel = reqCtx._debugChannel;
|
|
172
171
|
const rscStream = ctx.renderToReadableStream<LoaderPayload>(
|
|
173
172
|
loaderPayload,
|
|
174
173
|
{
|
|
175
|
-
|
|
174
|
+
onError: (error: unknown) => {
|
|
175
|
+
ctx.callOnError(error, "rendering", {
|
|
176
|
+
request,
|
|
177
|
+
url,
|
|
178
|
+
env,
|
|
179
|
+
loaderName: loaderId,
|
|
180
|
+
});
|
|
181
|
+
},
|
|
176
182
|
},
|
|
177
183
|
);
|
|
178
184
|
|
|
@@ -204,7 +210,16 @@ export async function handleLoaderFetch<TEnv>(
|
|
|
204
210
|
name: err.name,
|
|
205
211
|
},
|
|
206
212
|
};
|
|
207
|
-
const rscStream = ctx.renderToReadableStream(errorPayload
|
|
213
|
+
const rscStream = ctx.renderToReadableStream(errorPayload, {
|
|
214
|
+
onError: (error: unknown) => {
|
|
215
|
+
ctx.callOnError(error, "rendering", {
|
|
216
|
+
request,
|
|
217
|
+
url,
|
|
218
|
+
env,
|
|
219
|
+
loaderName: loaderId,
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
});
|
|
208
223
|
|
|
209
224
|
return createResponseWithMergedHeaders(rscStream, {
|
|
210
225
|
status: 500,
|
package/src/rsc/manifest-init.ts
CHANGED
|
@@ -31,7 +31,11 @@ export async function buildRouterTrieFromUrlpatterns(
|
|
|
31
31
|
): Promise<void> {
|
|
32
32
|
const { generateManifestFull } =
|
|
33
33
|
await import("../build/generate-manifest.js");
|
|
34
|
-
const generated = generateManifestFull(
|
|
34
|
+
const generated = generateManifestFull(
|
|
35
|
+
router.urlpatterns,
|
|
36
|
+
undefined,
|
|
37
|
+
router.basename ? { urlPrefix: router.basename } : undefined,
|
|
38
|
+
);
|
|
35
39
|
if (
|
|
36
40
|
generated._routeAncestry &&
|
|
37
41
|
Object.keys(generated._routeAncestry).length > 0
|
|
@@ -243,6 +243,8 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
243
243
|
const payload: RscPayload = {
|
|
244
244
|
metadata: {
|
|
245
245
|
pathname: url.pathname,
|
|
246
|
+
routerId: ctx.router.id,
|
|
247
|
+
basename: ctx.router.basename,
|
|
246
248
|
segments: match.segments,
|
|
247
249
|
matched: match.matched,
|
|
248
250
|
diff: match.diff,
|
|
@@ -257,9 +259,10 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
257
259
|
formState: actionResult,
|
|
258
260
|
};
|
|
259
261
|
|
|
260
|
-
const debugChannel = requireRequestContext()._debugChannel;
|
|
261
262
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
262
|
-
|
|
263
|
+
onError: (error: unknown) => {
|
|
264
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
265
|
+
},
|
|
263
266
|
});
|
|
264
267
|
// metricsStore=undefined is safe: the handler already stashed the early
|
|
265
268
|
// SSR setup promise on request variables, so getSSRSetup returns it
|
|
@@ -345,6 +348,8 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
345
348
|
const payload: RscPayload = {
|
|
346
349
|
metadata: {
|
|
347
350
|
pathname: url.pathname,
|
|
351
|
+
routerId: ctx.router.id,
|
|
352
|
+
basename: ctx.router.basename,
|
|
348
353
|
segments: errorResult.segments,
|
|
349
354
|
matched: errorResult.matched,
|
|
350
355
|
diff: errorResult.diff,
|
|
@@ -359,7 +364,11 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
359
364
|
},
|
|
360
365
|
};
|
|
361
366
|
|
|
362
|
-
const rscStream = ctx.renderToReadableStream<RscPayload>(payload
|
|
367
|
+
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
368
|
+
onError: (error: unknown) => {
|
|
369
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
370
|
+
},
|
|
371
|
+
});
|
|
363
372
|
// metricsStore=undefined is safe: the handler already stashed the early
|
|
364
373
|
// SSR setup promise on request variables, so getSSRSetup returns it
|
|
365
374
|
// without falling back to a fresh startSSRSetup.
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -54,6 +54,8 @@ export async function handleRscRendering<TEnv>(
|
|
|
54
54
|
payload = {
|
|
55
55
|
metadata: {
|
|
56
56
|
pathname: url.pathname,
|
|
57
|
+
routerId: ctx.router.id,
|
|
58
|
+
basename: ctx.router.basename,
|
|
57
59
|
segments: match.segments,
|
|
58
60
|
matched: match.matched,
|
|
59
61
|
diff: match.diff,
|
|
@@ -75,6 +77,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
75
77
|
payload = {
|
|
76
78
|
metadata: {
|
|
77
79
|
pathname: url.pathname,
|
|
80
|
+
routerId: ctx.router.id,
|
|
78
81
|
segments: result.segments,
|
|
79
82
|
matched: result.matched,
|
|
80
83
|
diff: result.diff,
|
|
@@ -136,6 +139,8 @@ export async function handleRscRendering<TEnv>(
|
|
|
136
139
|
|
|
137
140
|
metadata: {
|
|
138
141
|
pathname: url.pathname,
|
|
142
|
+
routerId: ctx.router.id,
|
|
143
|
+
basename: ctx.router.basename,
|
|
139
144
|
segments: match.segments,
|
|
140
145
|
matched: match.matched,
|
|
141
146
|
diff: match.diff,
|
|
@@ -168,9 +173,10 @@ export async function handleRscRendering<TEnv>(
|
|
|
168
173
|
|
|
169
174
|
// Serialize to RSC stream
|
|
170
175
|
const rscSerializeStart = performance.now();
|
|
171
|
-
const debugChannel = reqCtx._debugChannel;
|
|
172
176
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
173
|
-
|
|
177
|
+
onError: (error: unknown) => {
|
|
178
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
179
|
+
},
|
|
174
180
|
});
|
|
175
181
|
const rscSerializeDur = performance.now() - rscSerializeStart;
|
|
176
182
|
// This measures synchronous stream creation, not end-to-end stream consumption.
|
|
@@ -227,7 +233,6 @@ export async function handleRscRendering<TEnv>(
|
|
|
227
233
|
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
228
234
|
nonce,
|
|
229
235
|
streamMode,
|
|
230
|
-
debugId: reqCtx._debugId,
|
|
231
236
|
});
|
|
232
237
|
const ssrRenderDur = performance.now() - ssrRenderStart;
|
|
233
238
|
appendMetric(metricsStore, "ssr-render-html", ssrRenderStart, ssrRenderDur);
|
package/src/rsc/server-action.ts
CHANGED
|
@@ -208,6 +208,7 @@ export async function executeServerAction<TEnv>(
|
|
|
208
208
|
const payload: RscPayload = {
|
|
209
209
|
metadata: {
|
|
210
210
|
pathname: url.pathname,
|
|
211
|
+
routerId: ctx.router.id,
|
|
211
212
|
segments: errorResult.segments,
|
|
212
213
|
isPartial: true,
|
|
213
214
|
matched: errorResult.matched,
|
|
@@ -223,10 +224,11 @@ export async function executeServerAction<TEnv>(
|
|
|
223
224
|
// location state is a success-only semantic. Error boundary responses
|
|
224
225
|
// update the error UI but should not mutate browser history state.
|
|
225
226
|
|
|
226
|
-
const debugChannel = requireRequestContext()._debugChannel;
|
|
227
227
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
228
228
|
temporaryReferences,
|
|
229
|
-
|
|
229
|
+
onError: (error: unknown) => {
|
|
230
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
231
|
+
},
|
|
230
232
|
});
|
|
231
233
|
|
|
232
234
|
return createResponseWithMergedHeaders(rscStream, {
|
|
@@ -316,6 +318,7 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
316
318
|
const payload: RscPayload = {
|
|
317
319
|
metadata: {
|
|
318
320
|
pathname: url.pathname,
|
|
321
|
+
routerId: ctx.router.id,
|
|
319
322
|
segments: matchResult.segments,
|
|
320
323
|
isPartial: true,
|
|
321
324
|
matched: matchResult.matched,
|
|
@@ -332,6 +335,9 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
332
335
|
const renderStart = performance.now();
|
|
333
336
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
334
337
|
temporaryReferences,
|
|
338
|
+
onError: (error: unknown) => {
|
|
339
|
+
ctx.callOnError(error, "rendering", { request, url, env });
|
|
340
|
+
},
|
|
335
341
|
});
|
|
336
342
|
const rscSerializeDur = performance.now() - renderStart;
|
|
337
343
|
// This measures synchronous stream creation, not end-to-end stream consumption.
|
package/src/rsc/ssr-setup.ts
CHANGED
|
@@ -77,7 +77,7 @@ export function getSSRSetup<TEnv>(
|
|
|
77
77
|
url: URL,
|
|
78
78
|
metricsStore: MetricsStore | undefined,
|
|
79
79
|
): Promise<SSRSetup> {
|
|
80
|
-
const early = _getRequestContext()?.
|
|
80
|
+
const early = _getRequestContext()?._variables?.[SSR_SETUP_VAR] as
|
|
81
81
|
| Promise<SSRSetup>
|
|
82
82
|
| undefined;
|
|
83
83
|
if (early) return early;
|
|
@@ -98,7 +98,7 @@ export function getSSRSetup<TEnv>(
|
|
|
98
98
|
* the isRscRequest decision in rsc-rendering.ts.
|
|
99
99
|
*
|
|
100
100
|
* Note: response/mime routes are excluded by the caller — this function
|
|
101
|
-
* runs after
|
|
101
|
+
* runs after classifyRequest() determines the request mode.
|
|
102
102
|
*/
|
|
103
103
|
export function mayNeedSSR(request: Request, url: URL): boolean {
|
|
104
104
|
if (
|
package/src/rsc/types.ts
CHANGED
|
@@ -19,6 +19,9 @@ export interface RscPayload {
|
|
|
19
19
|
metadata?: {
|
|
20
20
|
pathname: string;
|
|
21
21
|
segments: ResolvedSegment[];
|
|
22
|
+
/** Router instance ID. When this changes between navigations, the client
|
|
23
|
+
* discards cached segments and does a full tree replacement (app switch). */
|
|
24
|
+
routerId?: string;
|
|
22
25
|
isPartial?: boolean;
|
|
23
26
|
isError?: boolean;
|
|
24
27
|
matched?: string[];
|
|
@@ -38,6 +41,8 @@ export interface RscPayload {
|
|
|
38
41
|
themeConfig?: ResolvedThemeConfig | null;
|
|
39
42
|
/** Initial theme from cookie (for SSR hydration) */
|
|
40
43
|
initialTheme?: Theme;
|
|
44
|
+
/** URL prefix for all routes (from createRouter({ basename })). */
|
|
45
|
+
basename?: string;
|
|
41
46
|
/** Whether connection warmup is enabled */
|
|
42
47
|
warmupEnabled?: boolean;
|
|
43
48
|
/** Server-side redirect with optional state (for partial requests) */
|
|
@@ -65,10 +70,7 @@ export interface RSCDependencies {
|
|
|
65
70
|
payload: T,
|
|
66
71
|
options?: {
|
|
67
72
|
temporaryReferences?: unknown;
|
|
68
|
-
|
|
69
|
-
readable?: ReadableStream;
|
|
70
|
-
writable?: WritableStream;
|
|
71
|
-
};
|
|
73
|
+
onError?: (error: unknown) => string | void;
|
|
72
74
|
},
|
|
73
75
|
) => ReadableStream<Uint8Array>;
|
|
74
76
|
|
|
@@ -130,9 +132,6 @@ export interface SSRRenderOptions {
|
|
|
130
132
|
* - `"allReady"` — await `stream.allReady` before returning.
|
|
131
133
|
*/
|
|
132
134
|
streamMode?: import("../router/router-options.js").SSRStreamMode;
|
|
133
|
-
|
|
134
|
-
/** @internal Dev-only: debug channel ID for React Performance Tracks */
|
|
135
|
-
debugId?: string;
|
|
136
135
|
}
|
|
137
136
|
|
|
138
137
|
/**
|
package/src/server/context.ts
CHANGED
|
@@ -191,8 +191,12 @@ export type EntryData =
|
|
|
191
191
|
/** Original PrerenderHandlerDefinition (for build-time getParams access) */
|
|
192
192
|
prerenderDef?: {
|
|
193
193
|
getParams?: (ctx: any) => Promise<any[]> | any[];
|
|
194
|
-
options?: {
|
|
194
|
+
options?: { concurrency?: number };
|
|
195
195
|
};
|
|
196
|
+
/** Set when route is wrapped with Passthrough() — has a separate live handler */
|
|
197
|
+
isPassthrough?: true;
|
|
198
|
+
/** Live handler for runtime fallback (only set on Passthrough routes) */
|
|
199
|
+
liveHandler?: Handler<any, any, any>;
|
|
196
200
|
/** Set when handler is a Static definition (build-time only) */
|
|
197
201
|
isStaticPrerender?: true;
|
|
198
202
|
/** Static handler $$id for build-time store lookup */
|
|
@@ -670,11 +674,44 @@ export function track(label: string, depth?: number): () => void {
|
|
|
670
674
|
};
|
|
671
675
|
}
|
|
672
676
|
|
|
677
|
+
/**
|
|
678
|
+
* Separate ALS for tracking loader execution scope.
|
|
679
|
+
* Uses a dedicated ALS (not RSCRouterContext) to avoid issues with
|
|
680
|
+
* nested RSCRouterContext.run() calls in Vite's module runner.
|
|
681
|
+
*/
|
|
682
|
+
const LOADER_SCOPE_KEY = Symbol.for("rangojs-router:loader-scope");
|
|
683
|
+
const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
|
|
684
|
+
globalThis as any
|
|
685
|
+
)[LOADER_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
|
|
686
|
+
|
|
673
687
|
/**
|
|
674
688
|
* Check if the current execution is inside a cache() DSL boundary.
|
|
675
689
|
* Returns false inside loader execution — loaders are always fresh
|
|
676
690
|
* (never cached), so non-cacheable reads are safe.
|
|
677
691
|
*/
|
|
678
692
|
export function isInsideCacheScope(): boolean {
|
|
679
|
-
|
|
693
|
+
if (RSCRouterContext.getStore()?.insideCacheScope !== true) return false;
|
|
694
|
+
// Loaders are always fresh — even inside a cache() boundary, the loader
|
|
695
|
+
// function re-executes on every request. Skip the guard when running
|
|
696
|
+
// inside a loader.
|
|
697
|
+
if (loaderScopeALS.getStore()?.active) return false;
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* Check if the current execution is inside a DSL loader scope
|
|
703
|
+
* (wrapped by runInsideLoaderScope). Used by rendered() barrier
|
|
704
|
+
* to distinguish DSL loaders from handler-invoked loaders.
|
|
705
|
+
*/
|
|
706
|
+
export function isInsideLoaderScope(): boolean {
|
|
707
|
+
return loaderScopeALS.getStore()?.active === true;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Run `fn` inside a loader scope. While active, cache-scope guards
|
|
712
|
+
* are bypassed because loaders are always fresh (never cached) and
|
|
713
|
+
* their side effects (setCookie, header, etc.) are safe.
|
|
714
|
+
*/
|
|
715
|
+
export function runInsideLoaderScope<T>(fn: () => T): T {
|
|
716
|
+
return loaderScopeALS.run({ active: true }, fn);
|
|
680
717
|
}
|
|
@@ -13,6 +13,25 @@
|
|
|
13
13
|
*/
|
|
14
14
|
export type HandleData = Record<string, Record<string, unknown[]>>;
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Build a HandleData snapshot from a HandleStore using segment ordering.
|
|
18
|
+
* Reads data directly from the store for each segment in order.
|
|
19
|
+
*/
|
|
20
|
+
export function buildHandleSnapshot(
|
|
21
|
+
handleStore: HandleStore,
|
|
22
|
+
segmentOrder: string[],
|
|
23
|
+
): HandleData {
|
|
24
|
+
const data: HandleData = {};
|
|
25
|
+
for (const segmentId of segmentOrder) {
|
|
26
|
+
const segData = handleStore.getDataForSegment(segmentId);
|
|
27
|
+
for (const handleName in segData) {
|
|
28
|
+
if (!data[handleName]) data[handleName] = {};
|
|
29
|
+
data[handleName][segmentId] = segData[handleName];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return data;
|
|
33
|
+
}
|
|
34
|
+
|
|
16
35
|
function createLateHandlePushError(
|
|
17
36
|
handleName: string,
|
|
18
37
|
segmentId: string,
|
|
@@ -44,20 +44,21 @@ export function setLoaderImports(
|
|
|
44
44
|
export async function getLoaderLazy(
|
|
45
45
|
id: string,
|
|
46
46
|
): Promise<LoaderRegistryEntry | undefined> {
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return existing;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Check the fetchable loader registry (populated by createLoader)
|
|
47
|
+
// Always check fetchableLoaderRegistry first — it's the source of truth.
|
|
48
|
+
// createLoader() updates it during module re-evaluation (HMR), so checking
|
|
49
|
+
// here ensures we pick up the fresh function after a loader file change.
|
|
54
50
|
const fetchable = getFetchableLoader(id);
|
|
55
51
|
if (fetchable) {
|
|
56
|
-
// Cache in main registry for future requests
|
|
57
52
|
loaderRegistry.set(id, fetchable);
|
|
58
53
|
return fetchable;
|
|
59
54
|
}
|
|
60
55
|
|
|
56
|
+
// Fall back to local cache (populated by previous lazy imports in production)
|
|
57
|
+
const existing = loaderRegistry.get(id);
|
|
58
|
+
if (existing) {
|
|
59
|
+
return existing;
|
|
60
|
+
}
|
|
61
|
+
|
|
61
62
|
// Try to lazy load from the import map (production mode)
|
|
62
63
|
if (lazyLoaderImports && lazyLoaderImports.size > 0) {
|
|
63
64
|
const lazyImport = lazyLoaderImports.get(id);
|
|
@@ -26,7 +26,12 @@ import {
|
|
|
26
26
|
contextSet,
|
|
27
27
|
isNonCacheable,
|
|
28
28
|
} from "../context-var.js";
|
|
29
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
createHandleStore,
|
|
31
|
+
buildHandleSnapshot,
|
|
32
|
+
type HandleStore,
|
|
33
|
+
type HandleData,
|
|
34
|
+
} from "./handle-store.js";
|
|
30
35
|
import { isHandle } from "../handle.js";
|
|
31
36
|
import { track, type MetricsStore } from "./context.js";
|
|
32
37
|
import { getFetchableLoader } from "./fetchable-loader-store.js";
|
|
@@ -69,8 +74,8 @@ export interface RequestContext<
|
|
|
69
74
|
pathname: string;
|
|
70
75
|
/** URL search params (with internal `_rsc*` params stripped, same as `url.searchParams`) */
|
|
71
76
|
searchParams: URLSearchParams;
|
|
72
|
-
/**
|
|
73
|
-
|
|
77
|
+
/** @internal Shared variable backing store for ctx.get()/ctx.set(). */
|
|
78
|
+
_variables: Record<string, any>;
|
|
74
79
|
/** Get a variable set by middleware */
|
|
75
80
|
get: {
|
|
76
81
|
<T>(contextVar: ContextVar<T>): T | undefined;
|
|
@@ -271,6 +276,54 @@ export interface RequestContext<
|
|
|
271
276
|
/** @internal Previous route key (from the navigation source), used for revalidation */
|
|
272
277
|
_prevRouteKey?: string;
|
|
273
278
|
|
|
279
|
+
/**
|
|
280
|
+
* @internal Render barrier for experimental `rendered()` API.
|
|
281
|
+
* Resolves when all non-loader segments have settled and handle data
|
|
282
|
+
* is available. Used by DSL loaders that call `ctx.rendered()`.
|
|
283
|
+
*/
|
|
284
|
+
_renderBarrier: Promise<void>;
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* @internal Resolve the render barrier. Accepts resolved segments, filters
|
|
288
|
+
* out loaders, and captures non-loader segment IDs as the handle ordering.
|
|
289
|
+
* Called after segment resolution (fresh) or handle replay (cache/prerender).
|
|
290
|
+
*/
|
|
291
|
+
_resolveRenderBarrier: (
|
|
292
|
+
segments: Array<{ type: string; id: string }>,
|
|
293
|
+
) => void;
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* @internal Segment order at barrier resolution time, used by loader
|
|
297
|
+
* ctx.use(handle) to collect handle data in correct order.
|
|
298
|
+
*/
|
|
299
|
+
_renderBarrierSegmentOrder?: string[];
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @internal Set to true when the matched entry tree contains any `loading()`
|
|
303
|
+
* entries (streaming). Used by rendered() to fail fast.
|
|
304
|
+
*/
|
|
305
|
+
_treeHasStreaming?: boolean;
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* @internal Loader IDs that have called rendered() and are waiting for the
|
|
309
|
+
* barrier. Used to detect deadlocks when a handler tries to await the same
|
|
310
|
+
* loader via ctx.use(Loader).
|
|
311
|
+
*/
|
|
312
|
+
_renderBarrierWaiters?: Set<string>;
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* @internal Loader IDs that handlers have started awaiting via ctx.use().
|
|
316
|
+
* Used for bidirectional deadlock detection: if a loader later calls
|
|
317
|
+
* rendered() and a handler already awaits it, we can detect the deadlock.
|
|
318
|
+
*/
|
|
319
|
+
_handlerLoaderDeps?: Set<string>;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* @internal Cached HandleData snapshot built at barrier resolution time.
|
|
323
|
+
* Avoids rebuilding the snapshot on every loader ctx.use(handle) call.
|
|
324
|
+
*/
|
|
325
|
+
_renderBarrierHandleSnapshot?: HandleData;
|
|
326
|
+
|
|
274
327
|
/** @internal Per-request error dedup set for onError reporting */
|
|
275
328
|
_reportedErrors: WeakSet<object>;
|
|
276
329
|
|
|
@@ -288,14 +341,14 @@ export interface RequestContext<
|
|
|
288
341
|
/** @internal Request-scoped performance metrics store */
|
|
289
342
|
_metricsStore?: MetricsStore;
|
|
290
343
|
|
|
291
|
-
/** @internal
|
|
292
|
-
|
|
293
|
-
readable: ReadableStream;
|
|
294
|
-
writable: WritableStream;
|
|
295
|
-
};
|
|
344
|
+
/** @internal Router basename for this request (used by redirect()) */
|
|
345
|
+
_basename?: string;
|
|
296
346
|
|
|
297
|
-
/**
|
|
298
|
-
|
|
347
|
+
/**
|
|
348
|
+
* @internal RouteSnapshot from classifyRequest, reused by match/matchPartial
|
|
349
|
+
* to avoid a second resolveRoute call. Cleared on HMR invalidation.
|
|
350
|
+
*/
|
|
351
|
+
_classifiedRoute?: import("../router/route-snapshot.js").RouteSnapshot;
|
|
299
352
|
}
|
|
300
353
|
|
|
301
354
|
/**
|
|
@@ -322,12 +375,20 @@ export type PublicRequestContext<
|
|
|
322
375
|
| "_routeName"
|
|
323
376
|
| "_prevRouteKey"
|
|
324
377
|
| "_reportedErrors"
|
|
378
|
+
| "_renderBarrier"
|
|
379
|
+
| "_resolveRenderBarrier"
|
|
380
|
+
| "_renderBarrierSegmentOrder"
|
|
381
|
+
| "_treeHasStreaming"
|
|
382
|
+
| "_renderBarrierWaiters"
|
|
383
|
+
| "_handlerLoaderDeps"
|
|
384
|
+
| "_renderBarrierHandleSnapshot"
|
|
325
385
|
| "_reportBackgroundError"
|
|
326
386
|
| "_debugPerformance"
|
|
327
387
|
| "_metricsStore"
|
|
328
|
-
| "
|
|
329
|
-
| "_debugId"
|
|
388
|
+
| "_basename"
|
|
330
389
|
| "_setStatus"
|
|
390
|
+
| "_variables"
|
|
391
|
+
| "_classifiedRoute"
|
|
331
392
|
| "res"
|
|
332
393
|
>;
|
|
333
394
|
|
|
@@ -602,7 +663,7 @@ export function createRequestContext<TEnv>(
|
|
|
602
663
|
originalUrl: new URL(request.url),
|
|
603
664
|
pathname: url.pathname,
|
|
604
665
|
searchParams: cleanUrl.searchParams,
|
|
605
|
-
|
|
666
|
+
_variables: variables,
|
|
606
667
|
get: ((keyOrVar: any) => {
|
|
607
668
|
if (isNonCacheable(variables, keyOrVar) && isInsideCacheScope()) {
|
|
608
669
|
throw new Error(
|
|
@@ -749,9 +810,58 @@ export function createRequestContext<TEnv>(
|
|
|
749
810
|
_reportedErrors: new WeakSet<object>(),
|
|
750
811
|
_metricsStore: undefined,
|
|
751
812
|
|
|
813
|
+
// Render barrier: deferred promise resolved after non-loader segments settle.
|
|
814
|
+
_renderBarrier: null as any, // set below
|
|
815
|
+
_resolveRenderBarrier: null as any, // set below
|
|
816
|
+
_renderBarrierSegmentOrder: undefined,
|
|
817
|
+
|
|
752
818
|
reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
|
|
753
819
|
};
|
|
754
820
|
|
|
821
|
+
// Lazy render barrier: only allocate the Promise when a loader actually
|
|
822
|
+
// calls rendered(). Requests that don't use rendered() pay zero cost.
|
|
823
|
+
let barrierResolved = false;
|
|
824
|
+
let resolveBarrier: (() => void) | undefined;
|
|
825
|
+
ctx._renderBarrier = null as any; // lazy — created on first access
|
|
826
|
+
ctx._resolveRenderBarrier = (
|
|
827
|
+
segments: Array<{ type: string; id: string }>,
|
|
828
|
+
) => {
|
|
829
|
+
if (barrierResolved) return;
|
|
830
|
+
barrierResolved = true;
|
|
831
|
+
const segOrder = segments
|
|
832
|
+
.filter((s) => s.type !== "loader")
|
|
833
|
+
.map((s) => s.id);
|
|
834
|
+
ctx._renderBarrierSegmentOrder = segOrder;
|
|
835
|
+
// Build and cache handle snapshot so loader ctx.use(handle) calls
|
|
836
|
+
// don't rebuild it on every invocation.
|
|
837
|
+
ctx._renderBarrierHandleSnapshot = buildHandleSnapshot(
|
|
838
|
+
handleStore,
|
|
839
|
+
segOrder,
|
|
840
|
+
);
|
|
841
|
+
ctx._renderBarrierWaiters = undefined;
|
|
842
|
+
ctx._handlerLoaderDeps = undefined;
|
|
843
|
+
if (resolveBarrier) resolveBarrier();
|
|
844
|
+
};
|
|
845
|
+
Object.defineProperty(ctx, "_renderBarrier", {
|
|
846
|
+
get() {
|
|
847
|
+
// Barrier already resolved (cache/prerender hit) or first lazy access.
|
|
848
|
+
// Either way, replace the getter with a concrete value to avoid
|
|
849
|
+
// repeated Promise.resolve() allocations on subsequent reads.
|
|
850
|
+
const p = barrierResolved
|
|
851
|
+
? Promise.resolve()
|
|
852
|
+
: new Promise<void>((resolve) => {
|
|
853
|
+
resolveBarrier = resolve;
|
|
854
|
+
});
|
|
855
|
+
Object.defineProperty(ctx, "_renderBarrier", {
|
|
856
|
+
value: p,
|
|
857
|
+
writable: false,
|
|
858
|
+
configurable: false,
|
|
859
|
+
});
|
|
860
|
+
return p;
|
|
861
|
+
},
|
|
862
|
+
configurable: true,
|
|
863
|
+
});
|
|
864
|
+
|
|
755
865
|
// Now create use() with access to ctx
|
|
756
866
|
ctx.use = createUseFunction({
|
|
757
867
|
handleStore,
|
|
@@ -934,14 +1044,13 @@ export function createUseFunction<TEnv>(
|
|
|
934
1044
|
pathname: ctx.pathname,
|
|
935
1045
|
url: ctx.url,
|
|
936
1046
|
env: ctx.env as any,
|
|
937
|
-
var: ctx.var as any,
|
|
938
1047
|
get: ctx.get as any,
|
|
939
|
-
use: <TDep, TDepParams = any>(
|
|
1048
|
+
use: (<TDep, TDepParams = any>(
|
|
940
1049
|
dep: LoaderDefinition<TDep, TDepParams>,
|
|
941
1050
|
): Promise<TDep> => {
|
|
942
1051
|
// Recursive call - will start dep loader if not already started
|
|
943
1052
|
return ctx.use(dep);
|
|
944
|
-
},
|
|
1053
|
+
}) as LoaderContext["use"],
|
|
945
1054
|
method: "GET",
|
|
946
1055
|
body: undefined,
|
|
947
1056
|
reverse: createReverseFunction(
|
|
@@ -950,6 +1059,12 @@ export function createUseFunction<TEnv>(
|
|
|
950
1059
|
ctx.params as Record<string, string>,
|
|
951
1060
|
ctx._routeName ? isRouteRootScoped(ctx._routeName) : undefined,
|
|
952
1061
|
),
|
|
1062
|
+
rendered: () => {
|
|
1063
|
+
throw new Error(
|
|
1064
|
+
`ctx.rendered() is only available in DSL loaders (registered via loader() in urls()). ` +
|
|
1065
|
+
`It cannot be used from request-context loaders or server actions.`,
|
|
1066
|
+
);
|
|
1067
|
+
},
|
|
953
1068
|
};
|
|
954
1069
|
|
|
955
1070
|
const doneLoader = track(`loader:${loader.$$id}`, 2);
|
package/src/ssr/index.tsx
CHANGED
|
@@ -64,9 +64,6 @@ export interface SSRRenderOptions {
|
|
|
64
64
|
* - `"allReady"` — await `stream.allReady` before returning.
|
|
65
65
|
*/
|
|
66
66
|
streamMode?: import("../router/router-options.js").SSRStreamMode;
|
|
67
|
-
|
|
68
|
-
/** @internal Dev-only: debug channel ID for React Performance Tracks */
|
|
69
|
-
debugId?: string;
|
|
70
67
|
}
|
|
71
68
|
|
|
72
69
|
/**
|
|
@@ -132,6 +129,7 @@ interface RscPayload {
|
|
|
132
129
|
matched?: string[];
|
|
133
130
|
pathname?: string;
|
|
134
131
|
params?: Record<string, string>;
|
|
132
|
+
basename?: string;
|
|
135
133
|
themeConfig?: ResolvedThemeConfig | null;
|
|
136
134
|
initialTheme?: Theme;
|
|
137
135
|
version?: string;
|
|
@@ -264,6 +262,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
264
262
|
function SsrRoot() {
|
|
265
263
|
payload ??= createFromReadableStream<RscPayload>(rscStream1);
|
|
266
264
|
const resolved = React.use(payload);
|
|
265
|
+
|
|
267
266
|
const themeConfig = resolved.metadata?.themeConfig ?? null;
|
|
268
267
|
const pathname = resolved.metadata?.pathname ?? "/";
|
|
269
268
|
|
|
@@ -289,6 +288,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
289
288
|
navigate: async () => {},
|
|
290
289
|
refresh: async () => {},
|
|
291
290
|
version: resolved.metadata?.version,
|
|
291
|
+
basename: resolved.metadata?.basename,
|
|
292
292
|
};
|
|
293
293
|
|
|
294
294
|
// Build content tree from segments.
|
|
@@ -332,16 +332,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
|
332
332
|
}
|
|
333
333
|
|
|
334
334
|
// Get bootstrap script content
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
// Dev-only: inject debugId for React Performance Tracks.
|
|
338
|
-
// The client reads this during hydration to create the matching WS channel.
|
|
339
|
-
const debugId = options?.debugId;
|
|
340
|
-
if (debugId) {
|
|
341
|
-
bootstrapScriptContent =
|
|
342
|
-
`globalThis.__RANGO_DEBUG_ID__=${JSON.stringify(debugId)};` +
|
|
343
|
-
bootstrapScriptContent;
|
|
344
|
-
}
|
|
335
|
+
const bootstrapScriptContent = await loadBootstrapScriptContent();
|
|
345
336
|
|
|
346
337
|
// Render React tree to HTML stream
|
|
347
338
|
// Pass formState for useActionState progressive enhancement if provided
|