@rangojs/router 0.34.0 → 0.35.0
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 +1 -1
- package/dist/bin/{chunk-4HHFQSDC.js → chunk-3CQXDX3A.js} +5 -5
- package/dist/bin/{client-urls-projection-4Q5EQDAL.js → client-urls-projection-2GBDM5YK.js} +1 -1
- package/dist/bin/rango.js +1 -1
- package/dist/bin/{runtime-discovery-2ZTYRZ3V.js → runtime-discovery-PA3V3IFQ.js} +2 -2
- package/dist/bin/{temp-rsc-server-EUNKRFJD.js → temp-rsc-server-WDXEHDY7.js} +2 -2
- package/dist/types/cache/shell-snapshot.d.ts +1 -1
- package/dist/types/cache/types.d.ts +1 -1
- package/dist/types/client-urls/server-projection.d.ts +1 -1
- package/dist/types/client-urls/types.d.ts +6 -6
- package/dist/types/decode-loader-results.d.ts +1 -1
- package/dist/types/errors.d.ts +3 -3
- package/dist/types/handles/meta.d.ts +2 -2
- package/dist/types/loader-store.d.ts +20 -8
- package/dist/types/outlet-context.d.ts +1 -1
- package/dist/types/route-definition/helpers-types.d.ts +5 -5
- package/dist/types/router/loader-resolution.d.ts +1 -1
- package/dist/types/router/match-context.d.ts +12 -0
- package/dist/types/router/navigation-snapshot.d.ts +17 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +7 -7
- package/dist/types/router/types.d.ts +1 -1
- package/dist/types/rsc/shell-capture-constants.d.ts +2 -2
- package/dist/types/server/context.d.ts +3 -3
- package/dist/types/server/request-context.d.ts +1 -1
- package/dist/types/ssr/index.d.ts +1 -1
- package/dist/types/ssr/ssr-root.d.ts +1 -1
- package/dist/types/ssr-suspension-warning.d.ts +1 -1
- package/dist/types/types/loader-types.d.ts +19 -19
- package/dist/types/types/segments.d.ts +2 -2
- package/dist/types/urls/path-helper-types.d.ts +4 -4
- package/dist/types/urls/pattern-types.d.ts +1 -1
- package/dist/types/vite/plugin-types.d.ts +1 -1
- package/dist/vite/index.js +7 -7
- package/package.json +1 -1
- package/skills/app-structure/e-commerce.md +1 -1
- package/skills/breadcrumbs/SKILL.md +5 -5
- package/skills/cache-guide/SKILL.md +4 -4
- package/skills/caching/SKILL.md +1 -1
- package/skills/catalog.json +2 -2
- package/skills/client-urls/SKILL.md +20 -20
- package/skills/comparison/references/framework-comparison.md +6 -6
- package/skills/defer-hydration/SKILL.md +2 -2
- package/skills/handler-use/SKILL.md +5 -5
- package/skills/hooks/data.md +5 -2
- package/skills/hooks/handle-and-actions.md +9 -9
- package/skills/loader/SKILL.md +52 -47
- package/skills/middleware/SKILL.md +2 -2
- package/skills/migrate-nextjs/SKILL.md +10 -10
- package/skills/migrate-react-router/SKILL.md +1 -1
- package/skills/migrate-react-router/component-migration.md +2 -2
- package/skills/migrate-react-router/data-and-actions.md +2 -2
- package/skills/migrate-react-router/route-mapping.md +2 -2
- package/skills/parallel/SKILL.md +2 -2
- package/skills/ppr/SKILL.md +91 -92
- package/skills/prerender/SKILL.md +5 -5
- package/skills/rango/SKILL.md +7 -7
- package/skills/route/SKILL.md +1 -1
- package/skills/router-setup/SKILL.md +2 -2
- package/skills/scripts/SKILL.md +3 -3
- package/skills/shell-manifest/SKILL.md +3 -3
- package/skills/testing/serve-shell-request.md +1 -1
- package/skills/theme/SKILL.md +2 -2
- package/skills/typesafety/env-and-bindings.md +2 -2
- package/skills/use-cache/SKILL.md +1 -1
- package/skills/ux-architecture/SKILL.md +7 -7
- package/src/cache/shell-snapshot.ts +1 -1
- package/src/cache/types.ts +1 -1
- package/src/client-urls/client-urls.ts +16 -9
- package/src/client-urls/server-projection.ts +4 -4
- package/src/client-urls/types.ts +6 -6
- package/src/decode-loader-results.ts +1 -1
- package/src/errors.ts +9 -9
- package/src/handles/meta.ts +2 -2
- package/src/loader-store.ts +29 -13
- package/src/outlet-context.ts +1 -1
- package/src/route-definition/dsl-helpers.ts +4 -8
- package/src/route-definition/helpers-types.ts +5 -5
- package/src/router/loader-resolution.ts +4 -4
- package/src/router/match-api.ts +2 -0
- package/src/router/match-context.ts +22 -1
- package/src/router/match-middleware/cache-lookup.ts +17 -8
- package/src/router/match-middleware/intercept-resolution.ts +3 -3
- package/src/router/navigation-snapshot.ts +39 -8
- package/src/router/segment-resolution/loader-cache.ts +8 -8
- package/src/router/segment-resolution/prefetch-deferral.ts +1 -1
- package/src/router/segment-resolution/revalidation.ts +3 -3
- package/src/router/types.ts +1 -1
- package/src/rsc/shell-capture-constants.ts +3 -3
- package/src/rsc/shell-capture.ts +12 -12
- package/src/segment-system.tsx +3 -3
- package/src/server/context.ts +5 -5
- package/src/server/cookie-store.ts +1 -1
- package/src/server/request-context.ts +1 -1
- package/src/ssr/index.tsx +6 -6
- package/src/ssr/ssr-root.tsx +1 -1
- package/src/ssr-suspension-warning.ts +4 -4
- package/src/types/loader-types.ts +19 -19
- package/src/types/segments.ts +2 -2
- package/src/urls/path-helper-types.ts +4 -4
- package/src/urls/pattern-types.ts +1 -1
- package/src/use-loader.tsx +5 -3
- package/src/vite/discovery/shell-prerender-phase.ts +1 -1
- package/src/vite/plugin-types.ts +1 -1
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
type LoaderEntry,
|
|
27
27
|
} from "../server/context";
|
|
28
28
|
import {
|
|
29
|
-
|
|
29
|
+
assertLoaderSsrOption,
|
|
30
30
|
assertPrefetchOption,
|
|
31
31
|
invariant,
|
|
32
32
|
} from "../errors";
|
|
@@ -920,13 +920,9 @@ const loader: RouteHelpers<any, any>["loader"] = (
|
|
|
920
920
|
);
|
|
921
921
|
invariant(
|
|
922
922
|
(optionsGiven as { stream?: unknown } | undefined)?.stream === undefined,
|
|
923
|
-
|
|
924
|
-
);
|
|
925
|
-
assertBooleanOption(
|
|
926
|
-
optionsGiven?.ssr,
|
|
927
|
-
"loader() ssr",
|
|
928
|
-
"stream on every render",
|
|
923
|
+
'loader() stream was replaced: use loader(Def, { ssr: "await" }) to await the loader before first flush on document requests.',
|
|
929
924
|
);
|
|
925
|
+
assertLoaderSsrOption(optionsGiven?.ssr, "loader()");
|
|
930
926
|
assertPrefetchOption(
|
|
931
927
|
optionsGiven?.prefetch,
|
|
932
928
|
"loader() prefetch",
|
|
@@ -942,7 +938,7 @@ const loader: RouteHelpers<any, any>["loader"] = (
|
|
|
942
938
|
const loaderEntry: LoaderEntry = {
|
|
943
939
|
loader: loaderDef,
|
|
944
940
|
revalidate: [] as ShouldRevalidateFn<any, any>[],
|
|
945
|
-
...(optionsGiven?.ssr ===
|
|
941
|
+
...(optionsGiven?.ssr === "await" && { bake: true as const }),
|
|
946
942
|
...(isPrefetchFlag(optionsGiven?.prefetch) && {
|
|
947
943
|
prefetch: optionsGiven.prefetch,
|
|
948
944
|
}),
|
|
@@ -323,14 +323,14 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
323
323
|
* return <div>{data.name}</div>;
|
|
324
324
|
* }
|
|
325
325
|
* ```
|
|
326
|
-
* Pass `{ ssr:
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
326
|
+
* Pass `{ ssr: "await" }` to await this loader before first flush on
|
|
327
|
+
* DOCUMENT requests (see {@link LoaderOptions}): the opt-in for loaders
|
|
328
|
+
* whose data, handle pushes, or thrown notFound()/redirect() must be in the
|
|
329
|
+
* SSR'd HTML.
|
|
330
330
|
* Per-loader: a streaming sibling in the same segment keeps streaming.
|
|
331
331
|
*
|
|
332
332
|
* ```typescript
|
|
333
|
-
* loader(ProductLoader, { ssr:
|
|
333
|
+
* loader(ProductLoader, { ssr: "await" }, () => [cache()]),
|
|
334
334
|
* loader(RecommendationsLoader), // still streams behind loading()
|
|
335
335
|
* ```
|
|
336
336
|
*
|
|
@@ -160,7 +160,7 @@ function pushHandleValue(
|
|
|
160
160
|
* This has a simplified signature for internal use - the caller (typically
|
|
161
161
|
* wrapLoaderPromise in router.ts) is responsible for bridging to the full
|
|
162
162
|
* OnErrorCallback with complete request context (request, url, env, etc.).
|
|
163
|
-
* @param useDefaultErrorBoundary - An `ssr:
|
|
163
|
+
* @param useDefaultErrorBoundary - An `ssr: "await"` loader on a document or
|
|
164
164
|
* capture render: an error with no declared boundary gets
|
|
165
165
|
* `DefaultErrorFallback` (it would otherwise reject the tree), and every
|
|
166
166
|
* error answers 500.
|
|
@@ -758,7 +758,7 @@ function createLoaderExecutor<TEnv>(
|
|
|
758
758
|
}
|
|
759
759
|
|
|
760
760
|
// awaitBeforeFlush cycle: segment resolution awaits this loader
|
|
761
|
-
// (loader(Def, { ssr:
|
|
761
|
+
// (loader(Def, { ssr: "await" })), the barrier awaits segment
|
|
762
762
|
// resolution, and rendered() awaits the barrier — waiting here can
|
|
763
763
|
// never complete. Fail fast with the cause instead of hanging the
|
|
764
764
|
// document render. Only document renders populate the set (the flag is
|
|
@@ -767,10 +767,10 @@ function createLoaderExecutor<TEnv>(
|
|
|
767
767
|
if (reqCtx._awaitBeforeFlushLoaderIds?.has(currentLoaderId)) {
|
|
768
768
|
throw new Error(
|
|
769
769
|
`Deadlock: loader "${currentLoaderId}" is registered with ` +
|
|
770
|
-
`ssr:
|
|
770
|
+
`ssr: "await", so the document render awaits it before ` +
|
|
771
771
|
`the render barrier resolves — ctx.rendered() (and the ` +
|
|
772
772
|
`ctx.get(handle) read it gates) can never settle here. Drop ` +
|
|
773
|
-
`ssr:
|
|
773
|
+
`ssr: "await" on this loader or move the handle read to ` +
|
|
774
774
|
`a component.`,
|
|
775
775
|
);
|
|
776
776
|
}
|
package/src/router/match-api.ts
CHANGED
|
@@ -203,6 +203,7 @@ export async function createMatchContextForFull<TEnv>(
|
|
|
203
203
|
prevUrl: cleanUrl,
|
|
204
204
|
prevParams: {},
|
|
205
205
|
prevMatch: null,
|
|
206
|
+
interceptPrev: { url: cleanUrl, params: {} },
|
|
206
207
|
matched,
|
|
207
208
|
manifestEntry: snapshot.manifestEntry,
|
|
208
209
|
entries: snapshot.entries,
|
|
@@ -520,6 +521,7 @@ export async function createMatchContextForPartial<TEnv>(
|
|
|
520
521
|
prevUrl: nav.prevUrl,
|
|
521
522
|
prevParams: nav.prevParams,
|
|
522
523
|
prevMatch: nav.prevMatch,
|
|
524
|
+
interceptPrev: nav.interceptPrev,
|
|
523
525
|
matched,
|
|
524
526
|
manifestEntry: snapshot.manifestEntry,
|
|
525
527
|
entries: snapshot.entries,
|
|
@@ -116,6 +116,7 @@ import type {
|
|
|
116
116
|
} from "../server/context.js";
|
|
117
117
|
import type { HandlerContext, ResolvedSegment } from "../types.js";
|
|
118
118
|
import type { RouteMatchResult } from "./pattern-matching.js";
|
|
119
|
+
import type { PrevPage } from "./navigation-snapshot.js";
|
|
119
120
|
import type { InterceptResult } from "./router-context.js";
|
|
120
121
|
import type { ResolutionPassInput } from "./segment-resolution/resolution-pass.js";
|
|
121
122
|
import type { ActionContext } from "./types.js";
|
|
@@ -139,10 +140,13 @@ export interface MatchContext<TEnv = any> {
|
|
|
139
140
|
/** NavigationSnapshot.heldBoundary: the walk stops above it. */
|
|
140
141
|
heldBoundary?: string;
|
|
141
142
|
|
|
142
|
-
// Previous navigation state
|
|
143
|
+
// Previous navigation state: see NavigationSnapshot.
|
|
143
144
|
prevUrl: URL;
|
|
144
145
|
prevParams: Record<string, string>;
|
|
146
|
+
/** The match of the page left: the intercept's URL while one is open. */
|
|
145
147
|
prevMatch: RouteMatchResult | null;
|
|
148
|
+
/** The page an open intercept's slot was rendered for. */
|
|
149
|
+
interceptPrev: PrevPage;
|
|
146
150
|
|
|
147
151
|
// Current route match
|
|
148
152
|
matched: RouteMatchResult;
|
|
@@ -225,6 +229,23 @@ export function resolutionPassInput<TEnv>(
|
|
|
225
229
|
};
|
|
226
230
|
}
|
|
227
231
|
|
|
232
|
+
/**
|
|
233
|
+
* This match as the input of the pass that resolves its intercept's slot: its
|
|
234
|
+
* held segments compare with the intercept's own URL (`interceptPrev`), not
|
|
235
|
+
* with the source page under it. From /item/2 over /list/1, a link to /item/1
|
|
236
|
+
* compared with the source's params would read as param-equal and keep item
|
|
237
|
+
* 2's slot.
|
|
238
|
+
*/
|
|
239
|
+
export function interceptPassInput<TEnv>(
|
|
240
|
+
ctx: MatchContext<TEnv>,
|
|
241
|
+
): ResolutionPassInput<TEnv> {
|
|
242
|
+
return {
|
|
243
|
+
...resolutionPassInput(ctx),
|
|
244
|
+
prevUrl: ctx.interceptPrev.url,
|
|
245
|
+
prevParams: ctx.interceptPrev.params,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
228
249
|
/**
|
|
229
250
|
* Mutable state that flows through the pipeline
|
|
230
251
|
*/
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
type MatchPipelineState,
|
|
50
50
|
} from "../match-context.js";
|
|
51
51
|
import { getRouterContext, type RouterContext } from "../router-context.js";
|
|
52
|
+
import type { PrevPage } from "../navigation-snapshot.js";
|
|
52
53
|
import { freshPassInput } from "../segment-resolution/resolution-pass.js";
|
|
53
54
|
import { observeEvent } from "../instrument.js";
|
|
54
55
|
import { pushRevalidationTraceEntry, isTraceActive } from "../logging.js";
|
|
@@ -336,7 +337,12 @@ function storedEntrySends<TEnv>(
|
|
|
336
337
|
router: RouterContext<TEnv>,
|
|
337
338
|
): (segment: ResolvedSegment) => boolean | Promise<boolean> {
|
|
338
339
|
if (ctx.isFullMatch) return () => true;
|
|
339
|
-
|
|
340
|
+
// An intercept variant's slot compares with the intercept's own page, the
|
|
341
|
+
// page under it with the source (MatchContext.interceptPrev).
|
|
342
|
+
const pagePrev: PrevPage = { url: ctx.prevUrl, params: ctx.prevParams };
|
|
343
|
+
const slotPrev = ctx.interceptPrev;
|
|
344
|
+
const pageParamsChanged = !paramsEqual(ctx.matched.params, pagePrev.params);
|
|
345
|
+
const slotParamsChanged = !paramsEqual(ctx.matched.params, slotPrev.params);
|
|
340
346
|
const resendRouteSegments = replaceable && (ctx.isAction || ctx.stale);
|
|
341
347
|
const entryRevalidateMap =
|
|
342
348
|
ctx.clientSegmentSet.size > 0
|
|
@@ -345,11 +351,11 @@ function storedEntrySends<TEnv>(
|
|
|
345
351
|
// A keep that hangs on the source's params is right for the pages with
|
|
346
352
|
// the same params only: scope a prefetch of the answer to them, as a
|
|
347
353
|
// predicate that compared every param would (#1331).
|
|
348
|
-
const heldForSource = (): false => {
|
|
354
|
+
const heldForSource = (prevParams: Record<string, string>): false => {
|
|
349
355
|
markSourceHeld(
|
|
350
356
|
ctx.handlerContext as InternalHandlerContext<any, TEnv>,
|
|
351
|
-
readsOfAllParams(
|
|
352
|
-
{ params:
|
|
357
|
+
readsOfAllParams(prevParams),
|
|
358
|
+
{ params: prevParams, routeName: undefined },
|
|
353
359
|
);
|
|
354
360
|
return false;
|
|
355
361
|
};
|
|
@@ -362,23 +368,26 @@ function storedEntrySends<TEnv>(
|
|
|
362
368
|
ctx.clientSegmentSet,
|
|
363
369
|
);
|
|
364
370
|
if (fill !== undefined) return fill;
|
|
371
|
+
const ofSlot = isInterceptSegment(segment);
|
|
372
|
+
const prev = ofSlot ? slotPrev : pagePrev;
|
|
373
|
+
const paramsChanged = ofSlot ? slotParamsChanged : pageParamsChanged;
|
|
365
374
|
if (segment.type === "route" || segment.belongsToRoute === true) {
|
|
366
375
|
if (paramsChanged || resendRouteSegments) return true;
|
|
367
376
|
// Kept on equal params, whatever the search: a source with other
|
|
368
377
|
// params is sent it.
|
|
369
|
-
return heldForSource();
|
|
378
|
+
return heldForSource(prev.params);
|
|
370
379
|
}
|
|
371
380
|
const revalidate = entryRevalidateMap?.get(segment.id)?.revalidate;
|
|
372
381
|
if (!revalidate?.length) return false;
|
|
373
382
|
// Kept on equal params: from another source the rules run and may send
|
|
374
383
|
// it.
|
|
375
|
-
if (!paramsChanged) return heldForSource();
|
|
384
|
+
if (!paramsChanged) return heldForSource(prev.params);
|
|
376
385
|
return router.evaluateRevalidation({
|
|
377
386
|
segment,
|
|
378
|
-
prevParams:
|
|
387
|
+
prevParams: prev.params,
|
|
379
388
|
getPrevSegment: null,
|
|
380
389
|
request: ctx.request,
|
|
381
|
-
prevUrl:
|
|
390
|
+
prevUrl: prev.url,
|
|
382
391
|
nextUrl: ctx.url,
|
|
383
392
|
revalidations: revalidate.map((fn, i) => ({
|
|
384
393
|
name: `revalidate${i}`,
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
*/
|
|
104
104
|
import type { ResolvedSegment } from "../../types.js";
|
|
105
105
|
import type { MatchContext, MatchPipelineState } from "../match-context.js";
|
|
106
|
-
import {
|
|
106
|
+
import { interceptPassInput } from "../match-context.js";
|
|
107
107
|
import { getRouterContext } from "../router-context.js";
|
|
108
108
|
import type { GeneratorMiddleware } from "./cache-lookup.js";
|
|
109
109
|
import { debugLog } from "../logging.js";
|
|
@@ -176,7 +176,7 @@ export function withInterceptResolution<TEnv>(
|
|
|
176
176
|
const Store = ctx.Store;
|
|
177
177
|
const interceptSegments = await Store.run(() =>
|
|
178
178
|
resolveInterceptEntry(
|
|
179
|
-
|
|
179
|
+
interceptPassInput(ctx),
|
|
180
180
|
ctx.interceptResult!.intercept,
|
|
181
181
|
ctx.interceptResult!.entry,
|
|
182
182
|
true, // belongsToRoute
|
|
@@ -232,7 +232,7 @@ async function handleCacheHitIntercept<TEnv>(
|
|
|
232
232
|
const loaderSegments = resolveInterceptLoadersOnly
|
|
233
233
|
? await ctx.Store.run(() =>
|
|
234
234
|
resolveInterceptLoadersOnly(
|
|
235
|
-
|
|
235
|
+
interceptPassInput(ctx),
|
|
236
236
|
ctx.interceptResult!.intercept,
|
|
237
237
|
ctx.interceptResult!.entry,
|
|
238
238
|
true, // belongsToRoute
|
|
@@ -3,12 +3,30 @@ import { requestHeaders } from "../server/request-headers.js";
|
|
|
3
3
|
import type { RouteMatchResult } from "./pattern-matching.js";
|
|
4
4
|
import { WIRE } from "../wire-params.js";
|
|
5
5
|
|
|
6
|
+
/** A page held segments were rendered for: what their revalidation compares with. */
|
|
7
|
+
export interface PrevPage {
|
|
8
|
+
url: URL;
|
|
9
|
+
params: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
export interface NavigationSnapshot {
|
|
13
|
+
/**
|
|
14
|
+
* `prevUrl` and `prevParams`: the page the held page segments were rendered
|
|
15
|
+
* for. That is the page left, or the source page while an intercept is open
|
|
16
|
+
* (`hasInterceptSource`).
|
|
17
|
+
*/
|
|
7
18
|
prevUrl: URL;
|
|
8
19
|
/** The visitor's URL of the page left, when `prevUrl` is its rewrite target. */
|
|
9
20
|
prevVisitorUrl: URL | null;
|
|
10
21
|
prevParams: Record<string, string>;
|
|
22
|
+
/** The match of the page left: the intercept's URL while one is open. */
|
|
11
23
|
prevMatch: RouteMatchResult | null;
|
|
24
|
+
/**
|
|
25
|
+
* The page an open intercept's slot was rendered for: the intercept's own
|
|
26
|
+
* URL (match-context.ts interceptPassInput). Equals `prevUrl`/`prevParams`
|
|
27
|
+
* when the request names no intercept source.
|
|
28
|
+
*/
|
|
29
|
+
interceptPrev: PrevPage;
|
|
12
30
|
|
|
13
31
|
interceptContextUrl: URL;
|
|
14
32
|
interceptContextMatch: RouteMatchResult | null;
|
|
@@ -176,29 +194,40 @@ export async function resolveNavigation(
|
|
|
176
194
|
}
|
|
177
195
|
// An intercept's request diffs from its source, which _rsc_rt does not name.
|
|
178
196
|
const shown = interceptSourceUrl ? null : getShownRouteUrl(url);
|
|
179
|
-
const
|
|
197
|
+
const left = shown ?? visitor;
|
|
180
198
|
|
|
181
199
|
let interceptContextUrl: URL;
|
|
182
200
|
try {
|
|
183
201
|
interceptContextUrl = interceptSourceUrl
|
|
184
202
|
? withoutHash(new URL(interceptSourceUrl, url.origin))
|
|
185
|
-
:
|
|
203
|
+
: left;
|
|
186
204
|
} catch {
|
|
187
|
-
interceptContextUrl =
|
|
205
|
+
interceptContextUrl = left;
|
|
188
206
|
}
|
|
189
207
|
|
|
190
|
-
const prevMatch = await deps.findMatch(
|
|
191
|
-
const prevParams = prevMatch?.params || {};
|
|
208
|
+
const prevMatch = await deps.findMatch(left.pathname);
|
|
192
209
|
const interceptContextMatch = interceptSourceUrl
|
|
193
210
|
? await deps.findMatch(interceptContextUrl.pathname)
|
|
194
211
|
: prevMatch;
|
|
212
|
+
const interceptPrev: PrevPage = {
|
|
213
|
+
url: left,
|
|
214
|
+
params: prevMatch?.params || {},
|
|
215
|
+
};
|
|
216
|
+
// The page under an open intercept was rendered for the source, with its
|
|
217
|
+
// search, not for the intercept's URL.
|
|
218
|
+
const prev: PrevPage = interceptSourceUrl
|
|
219
|
+
? {
|
|
220
|
+
url: formSearch(new URL(interceptContextUrl)),
|
|
221
|
+
params: interceptContextMatch?.params || {},
|
|
222
|
+
}
|
|
223
|
+
: interceptPrev;
|
|
195
224
|
|
|
196
225
|
const isSameRouteNavigation = !!(
|
|
197
226
|
interceptContextMatch && interceptContextMatch.routeKey === currentRouteKey
|
|
198
227
|
);
|
|
199
228
|
|
|
200
229
|
const hasInterceptSource = !!interceptSourceUrl;
|
|
201
|
-
const effectiveFromUrl = hasInterceptSource ? interceptContextUrl :
|
|
230
|
+
const effectiveFromUrl = hasInterceptSource ? interceptContextUrl : left;
|
|
202
231
|
const effectiveFromMatch = hasInterceptSource
|
|
203
232
|
? interceptContextMatch
|
|
204
233
|
: prevMatch;
|
|
@@ -212,10 +241,11 @@ export async function resolveNavigation(
|
|
|
212
241
|
const clientSegmentSet = new Set(clientSegmentIds);
|
|
213
242
|
|
|
214
243
|
return {
|
|
215
|
-
prevUrl,
|
|
244
|
+
prevUrl: prev.url,
|
|
216
245
|
prevVisitorUrl: shown && visitor,
|
|
217
|
-
prevParams,
|
|
246
|
+
prevParams: prev.params,
|
|
218
247
|
prevMatch,
|
|
248
|
+
interceptPrev,
|
|
219
249
|
interceptContextUrl,
|
|
220
250
|
interceptContextMatch,
|
|
221
251
|
clientSegmentIds,
|
|
@@ -240,6 +270,7 @@ export function createNavigationSnapshot(
|
|
|
240
270
|
prevVisitorUrl: null,
|
|
241
271
|
prevParams: {},
|
|
242
272
|
prevMatch: null,
|
|
273
|
+
interceptPrev: { url: defaultUrl, params: {} },
|
|
243
274
|
interceptContextUrl: defaultUrl,
|
|
244
275
|
interceptContextMatch: null,
|
|
245
276
|
clientSegmentIds: [],
|
|
@@ -261,7 +261,7 @@ function chainLoaderEntries(entry: EntryData): readonly LoaderEntry[] {
|
|
|
261
261
|
|
|
262
262
|
/**
|
|
263
263
|
* The lane of each loader the matched chain `entries` registers: "live" (no
|
|
264
|
-
* `ssr:
|
|
264
|
+
* `ssr: "await"`) or "bake". A loader registered on both is "live". A PPR
|
|
265
265
|
* capture credits a push to the first registered loader around it when that
|
|
266
266
|
* one is live (shell-capture.ts deriveShellCaptureContext).
|
|
267
267
|
*/
|
|
@@ -302,7 +302,7 @@ function servedPins(
|
|
|
302
302
|
|
|
303
303
|
/**
|
|
304
304
|
* This request serves `loaderEntry` from a pin (servedPins), so its body does
|
|
305
|
-
* not decide the value: resolveLoaderData serves a pin to an `ssr:
|
|
305
|
+
* not decide the value: resolveLoaderData serves a pin to an `ssr: "await"`
|
|
306
306
|
* registration only. A prefetch does not defer such a loader
|
|
307
307
|
* (prefetch-deferral.ts defersLoader): nothing would run, and the pin is the
|
|
308
308
|
* value the document shows.
|
|
@@ -325,14 +325,14 @@ export function servesLoaderPin(
|
|
|
325
325
|
* only and the store asks while loader bodies run.
|
|
326
326
|
*
|
|
327
327
|
* Each side applies its own lane to the pin. The value is per registration
|
|
328
|
-
* (resolveLoaderData: an `ssr:
|
|
329
|
-
* pushes are per loader: one the route also registers without `ssr:
|
|
328
|
+
* (resolveLoaderData: an `ssr: "await"` registration is served its pin). The
|
|
329
|
+
* pushes are per loader: one the route also registers without `ssr: "await"`
|
|
330
330
|
* is a hole whatever its other registration's pin says, because the capture
|
|
331
331
|
* credited its pushes to the live registration's run (routeLoaderLanes).
|
|
332
332
|
*
|
|
333
333
|
* A dependency the route does not register has no pin of its own, and the
|
|
334
334
|
* record does not name the loader that ran it at capture: its copies stand
|
|
335
|
-
* only while every `ssr:
|
|
335
|
+
* only while every `ssr: "await"` loader is pinned (exact when the pins are
|
|
336
336
|
* all there or all gone).
|
|
337
337
|
*/
|
|
338
338
|
export function loaderPins(
|
|
@@ -482,7 +482,7 @@ function recordOwnerKey(cachedLoaderId: string): () => string {
|
|
|
482
482
|
* never run under a shell capture), so the lane is decided here, per LOADER,
|
|
483
483
|
* never by the entry's loading():
|
|
484
484
|
*
|
|
485
|
-
* - BAKE lane: `loader(Def, { ssr:
|
|
485
|
+
* - BAKE lane: `loader(Def, { ssr: "await" })` (LoaderEntry `bake`: the
|
|
486
486
|
* caller passes a `bakeSegmentKey` for it on document and navigation
|
|
487
487
|
* evaluations alike; a capture, a document evaluation, also has
|
|
488
488
|
* awaitBeforeFlush). The loader EXECUTES at capture (the
|
|
@@ -504,7 +504,7 @@ function recordOwnerKey(cachedLoaderId: string): () => string {
|
|
|
504
504
|
* (loader-mask.ts) and postpones at the reader's boundary — loading() or an
|
|
505
505
|
* inline Suspense — then streams fresh per request. A masked read with NO
|
|
506
506
|
* boundary above it root-postpones and the <body> sanity gate refuses the
|
|
507
|
-
* shell (eternal-MISS warning). A route whose loaders are ALL `ssr:
|
|
507
|
+
* shell (eternal-MISS warning). A route whose loaders are ALL `ssr: "await"`
|
|
508
508
|
* needs no loading(): nothing masks.
|
|
509
509
|
*/
|
|
510
510
|
export function resolveLoaderData<TEnv>(
|
|
@@ -540,7 +540,7 @@ export function resolveLoaderData<TEnv>(
|
|
|
540
540
|
return createMaskedLoaderPromise();
|
|
541
541
|
}
|
|
542
542
|
|
|
543
|
-
// `bake`: the pin is the `ssr:
|
|
543
|
+
// `bake`: the pin is the `ssr: "await"` registration's. The caller's key is
|
|
544
544
|
// passed for an unflagged loader of an entry without loading() too.
|
|
545
545
|
if (bakeSegmentKey && loaderEntry.bake) {
|
|
546
546
|
const recorded = servedPins(reqCtx)?.get(loaderEntry.loader.$$id);
|
|
@@ -283,7 +283,7 @@ function collectDeferrable(
|
|
|
283
283
|
for (const loaderEntry of entry.loader ?? []) {
|
|
284
284
|
const own = counts(loaderEntry.prefetch);
|
|
285
285
|
if (!own && behind.length === 0) continue;
|
|
286
|
-
// An ssr:
|
|
286
|
+
// An ssr: "await" loader on a ppr route is in scope too: whether this
|
|
287
287
|
// request serves it from a shell pin is per request (defersLoader).
|
|
288
288
|
loaders.set(loaderEntry, own ? true : behind);
|
|
289
289
|
}
|
|
@@ -472,7 +472,7 @@ export async function resolveOwnerLoaders<TEnv>(
|
|
|
472
472
|
loaderEntry,
|
|
473
473
|
ctx,
|
|
474
474
|
ctx.pathname,
|
|
475
|
-
// The bake key rides for flagged loaders too (`bake`): ssr:
|
|
475
|
+
// The bake key rides for flagged loaders too (`bake`): ssr: "await"
|
|
476
476
|
// bakes at capture regardless of the entry's loading() lane
|
|
477
477
|
// (loader-cache.ts capture branch), its HIT-tail seed overlay needs
|
|
478
478
|
// the same key, and on a navigation it pins exactly as on the
|
|
@@ -510,7 +510,7 @@ export async function resolveOwnerLoaders<TEnv>(
|
|
|
510
510
|
? buildLoaderErrorContext(ctx)
|
|
511
511
|
: { ...buildLoaderErrorContext(ctx), isPartial: true };
|
|
512
512
|
|
|
513
|
-
// FRESH: awaitBeforeFlush (loader(Def, { ssr:
|
|
513
|
+
// FRESH: awaitBeforeFlush (loader(Def, { ssr: "await" })). A document render
|
|
514
514
|
// awaits these loaders before returning, so their data is settled, their
|
|
515
515
|
// handle pushes beat the barrier snapshot, and a thrown notFound()'s status
|
|
516
516
|
// write deterministically precedes Response construction. The ids must
|
|
@@ -554,7 +554,7 @@ export async function resolveOwnerLoaders<TEnv>(
|
|
|
554
554
|
registerDeferredLoader(ctx, loader.$$id, shortCode, () =>
|
|
555
555
|
kickoff(loaderEntry, segmentId),
|
|
556
556
|
);
|
|
557
|
-
// A ppr shell record this request restored keeps an ssr:
|
|
557
|
+
// A ppr shell record this request restored keeps an ssr: "await"
|
|
558
558
|
// loader's pushes as copies that wait for its run (loaderPins "hole").
|
|
559
559
|
// No run is made here: drop them, the fill sends the run's pushes.
|
|
560
560
|
_getRequestContext()?._handleStore?.settleLoaderRun(loader.$$id);
|
package/src/router/types.ts
CHANGED
|
@@ -58,7 +58,7 @@ export interface SegmentResolutionDeps<TEnv = any> {
|
|
|
58
58
|
env?: TEnv;
|
|
59
59
|
isPartial?: boolean;
|
|
60
60
|
},
|
|
61
|
-
/** An `ssr:
|
|
61
|
+
/** An `ssr: "await"` loader on a document or capture render. */
|
|
62
62
|
useDefaultErrorBoundary?: boolean,
|
|
63
63
|
) => Promise<LoaderDataResult<T>>;
|
|
64
64
|
trackHandler: <T>(
|
|
@@ -210,12 +210,12 @@ export const SHELL_CAPTURE_TASK_HARD_CAP_MS = 25_000;
|
|
|
210
210
|
* (ShellCaptureDebugEvent.refusal, `caches.refusal` on a `router.prerender()`
|
|
211
211
|
* result): `identity` (a request-scoped read: cookies(), headers(), the
|
|
212
212
|
* theme, a `{ cache: false }` variable), `dynamic` (`ctx.dynamic()`),
|
|
213
|
-
* `loader` (a `{ ssr:
|
|
213
|
+
* `loader` (a `{ ssr: "await" }` loader rejected, settled with
|
|
214
214
|
* redirect()/notFound(), or a promise it pushed rejected),
|
|
215
215
|
* `no-record` (no doc record: the route's cache() refused the write, or the
|
|
216
216
|
* handler output failed), `handles` (a pushed
|
|
217
217
|
* handle value did not finish encoding), `size` (over the store's value
|
|
218
|
-
* limit, or `ssr:
|
|
218
|
+
* limit, or `ssr: "await"` loader pins over `ppr.maxSnapshotBytes`: a
|
|
219
219
|
* navigation-only entry was stored in place of the document shell),
|
|
220
220
|
* `record-expired` (the route cache() record
|
|
221
221
|
* the capture wrote ran out first), `invalidated` (a tag of the shell was
|
|
@@ -241,7 +241,7 @@ export type ShellCaptureRefusal =
|
|
|
241
241
|
* rule: resolveLoaderData (router/segment-resolution/loader-cache.ts).
|
|
242
242
|
*/
|
|
243
243
|
export const PPR_LANE_HINT: string =
|
|
244
|
-
|
|
244
|
+
'Lane rule: only loader(Def, { ssr: "await" }) executes at capture and bakes its ' +
|
|
245
245
|
"non-promise data; every other loader is live (masked at capture, fresh per " +
|
|
246
246
|
"request) and needs loading() or an inline <Suspense> above its reader. See the " +
|
|
247
247
|
'/ppr skill, "The loader lane rule" (node_modules/@rangojs/router/skills/ppr/SKILL.md).';
|
package/src/rsc/shell-capture.ts
CHANGED
|
@@ -391,15 +391,15 @@ function warnNullCaptureOnce(
|
|
|
391
391
|
(cause
|
|
392
392
|
? `Cause: ${cause}. Every capture waits for the handler layer to finish and ` +
|
|
393
393
|
'bakes it; make the slow part cheaper (cache() or "use cache"), raise ' +
|
|
394
|
-
|
|
394
|
+
'ppr.captureTimeout, or move it into a live loader (no ssr: "await") read under ' +
|
|
395
395
|
"loading() or an inline <Suspense>.\n"
|
|
396
396
|
: "Causes, told apart by whether the route ever flips to HIT:\n" +
|
|
397
397
|
" 1. Cold-start warmup (dev module transform, or a cold worker): the capture raced " +
|
|
398
398
|
"an unfinished shell render. This SELF-HEALS — the route flips to HIT once a later " +
|
|
399
399
|
"request warms the modules.\n" +
|
|
400
400
|
" 2. Something suspends above <body> with no Suspense boundary and never settles " +
|
|
401
|
-
|
|
402
|
-
|
|
401
|
+
'within the capture window: a live loader (no ssr: "await") read without loading() or ' +
|
|
402
|
+
'an inline <Suspense>, or a loader(Def, { ssr: "await" }) or top-level push (e.g. Meta) ' +
|
|
403
403
|
"slower than ppr.captureTimeout. The boundary belongs to the entry or component " +
|
|
404
404
|
"that owns the data.\n") +
|
|
405
405
|
(pendingStacks && pendingStacks.length > 0
|
|
@@ -565,8 +565,8 @@ function warnSnapshotOverCapOnce(
|
|
|
565
565
|
key,
|
|
566
566
|
() =>
|
|
567
567
|
`[rango] Shell capture for "${key}" recorded ${snapshotBytes} bytes of ` +
|
|
568
|
-
`ssr:
|
|
569
|
-
|
|
568
|
+
`ssr: "await" loader pins, over the ${capBytes}-byte cap: the document ` +
|
|
569
|
+
'shell was not stored; raise maxSnapshotBytes or shrink the ssr: "await" ' +
|
|
570
570
|
"data. Documents of this route are a MISS until then; client " +
|
|
571
571
|
"navigations still replay its handler layer.",
|
|
572
572
|
);
|
|
@@ -599,9 +599,9 @@ function warnBakeCostOnce(key: string, source: string, ms: number): void {
|
|
|
599
599
|
"of the route and occupies the per-isolate capture queue; the served " +
|
|
600
600
|
"response never shows it. To keep it baked but cheap, wrap the work in " +
|
|
601
601
|
'cache() or "use cache"; to make it per-request, load it in a live ' +
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
602
|
+
'loader (no ssr: "await") read under loading() or an inline <Suspense>; for ' +
|
|
603
|
+
'a loader(Def, { ssr: "await" }), return the slow part as a nested promise ' +
|
|
604
|
+
'or drop ssr: "await". ' +
|
|
605
605
|
PPR_LANE_HINT,
|
|
606
606
|
);
|
|
607
607
|
}
|
|
@@ -1747,7 +1747,7 @@ export async function settleCaptureRecord(
|
|
|
1747
1747
|
capture.key,
|
|
1748
1748
|
`${describeLoaderPromisePush(rejected)} rejected during capture; a ` +
|
|
1749
1749
|
"failed value must not bake into the shared shell. Catch it and push " +
|
|
1750
|
-
|
|
1750
|
+
'a fallback, or push it from a loader without ssr: "await".',
|
|
1751
1751
|
PPR_LANE_HINT,
|
|
1752
1752
|
);
|
|
1753
1753
|
return { kind: "refused", refusal: "loader" };
|
|
@@ -2209,7 +2209,7 @@ export function deriveShellCaptureContext(
|
|
|
2209
2209
|
// cache() record's restore (handle-snapshot.ts restoreHandles) lands
|
|
2210
2210
|
// here too: a value it restores as a placeholder keeps that loader as
|
|
2211
2211
|
// owner, so a HIT restores it as that loader's (pushRestored).
|
|
2212
|
-
// - DSL-LOADER pushes. Only bake-lane { ssr:
|
|
2212
|
+
// - DSL-LOADER pushes. Only bake-lane { ssr: "await" } loaders and the
|
|
2213
2213
|
// loaders they await via ctx.use execute at capture (live loaders are
|
|
2214
2214
|
// masked), their pushes are in the captured HTML (<head> title/meta,
|
|
2215
2215
|
// useHandle echoes), and a HIT does not run a promise-free bake-lane
|
|
@@ -2547,7 +2547,7 @@ async function captureAndStoreShell(
|
|
|
2547
2547
|
warnCaptureRefusedOnce(
|
|
2548
2548
|
capture.key,
|
|
2549
2549
|
`the loader for segment "${segmentKey}" rejected during capture; its error UI must not bake into the shared shell. ` +
|
|
2550
|
-
|
|
2550
|
+
'Fix the loader, or drop its ssr: "await" to move it to the live lane.',
|
|
2551
2551
|
PPR_LANE_HINT,
|
|
2552
2552
|
);
|
|
2553
2553
|
return refuse(stats, "loader");
|
|
@@ -2564,7 +2564,7 @@ async function captureAndStoreShell(
|
|
|
2564
2564
|
warnCaptureRefusedOnce(
|
|
2565
2565
|
capture.key,
|
|
2566
2566
|
`the loader for segment "${segmentKey}" settled with redirect()/notFound() during capture; a request-specific signal must not bake into the shared shell. ` +
|
|
2567
|
-
|
|
2567
|
+
'Drop its ssr: "await" to move it to the live lane, or move the decision into middleware.',
|
|
2568
2568
|
PPR_LANE_HINT,
|
|
2569
2569
|
);
|
|
2570
2570
|
return refuse(stats, "loader");
|
package/src/segment-system.tsx
CHANGED
|
@@ -294,7 +294,7 @@ function wrapDefaultOutletContent(
|
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
/**
|
|
297
|
-
* Per-loader stream map for read-site suspension. { ssr:
|
|
297
|
+
* Per-loader stream map for read-site suspension. { ssr: "await" } loaders were
|
|
298
298
|
* awaited before flush (resolveOwnerLoaders), so deliver the SETTLED result, not the
|
|
299
299
|
* settled promise: the read site decodes synchronously instead of use()ing a
|
|
300
300
|
* Flight chunk whose fulfilled-at-read-time status is a scheduling race the
|
|
@@ -844,7 +844,7 @@ export async function renderSegments(
|
|
|
844
844
|
// shell-cache slot-use: masked loaderData is what pins
|
|
845
845
|
// "srv badge pending...") and, on reuse, leaves a stale stream
|
|
846
846
|
// map that ignores the post-action aggregate (mini @cart).
|
|
847
|
-
// Flagged (ssr:
|
|
847
|
+
// Flagged (ssr: "await") delivery still holds: when every owned
|
|
848
848
|
// loader paid the pre-flush await, settle the aggregate so
|
|
849
849
|
// LoaderResolver decodes the array without a Flight-chunk use().
|
|
850
850
|
const settleParallelAggregate = settlesLoaders(
|
|
@@ -860,7 +860,7 @@ export async function renderSegments(
|
|
|
860
860
|
p.awaitedLoaderIds = undefined;
|
|
861
861
|
if (segDebug) {
|
|
862
862
|
segDebugLog(
|
|
863
|
-
`segment ${id}: parallel ${p.id} loaders awaited (forceAwait/action/ssr:
|
|
863
|
+
`segment ${id}: parallel ${p.id} loaders awaited (forceAwait/action/ssr: "await")`,
|
|
864
864
|
{
|
|
865
865
|
loaderIds: p.loaderIds,
|
|
866
866
|
ms: Math.round(performance.now() - parallelAwaitStart),
|
package/src/server/context.ts
CHANGED
|
@@ -154,7 +154,7 @@ export type LoaderEntry = {
|
|
|
154
154
|
cache?: EntryCacheConfig;
|
|
155
155
|
/**
|
|
156
156
|
* Document renders await this loader before segment resolution returns
|
|
157
|
-
* (loader(Def, { ssr:
|
|
157
|
+
* (loader(Def, { ssr: "await" })), so its data, handle pushes, and
|
|
158
158
|
* thrown notFound()/redirect() deterministically precede first flush.
|
|
159
159
|
* Resolved at DSL-evaluation time from ctx.isSSR — entries are cached
|
|
160
160
|
* per-isSSR (router/manifest.ts cache key), so the flag is already
|
|
@@ -164,7 +164,7 @@ export type LoaderEntry = {
|
|
|
164
164
|
*/
|
|
165
165
|
awaitBeforeFlush?: true;
|
|
166
166
|
/**
|
|
167
|
-
* loader(Def, { ssr:
|
|
167
|
+
* loader(Def, { ssr: "await" }) on every DSL evaluation, document and
|
|
168
168
|
* navigation: the loader's PPR bake lane (loader-cache.ts
|
|
169
169
|
* resolveLoaderData). The bake/seed key rides on it, so a client
|
|
170
170
|
* navigation's shell replay pins exactly the loaders a document HIT pins,
|
|
@@ -913,7 +913,7 @@ export function getIsolatedLazyParent(
|
|
|
913
913
|
* called from both.
|
|
914
914
|
*/
|
|
915
915
|
|
|
916
|
-
/** loader(Def, { ssr:
|
|
916
|
+
/** loader(Def, { ssr: "await" }): a document awaits it before the first flush. */
|
|
917
917
|
export function documentModeLoader(entry: LoaderEntry): LoaderEntry {
|
|
918
918
|
return entry.bake ? { ...entry, awaitBeforeFlush: true } : entry;
|
|
919
919
|
}
|
|
@@ -1213,11 +1213,11 @@ export function requestReadCaptureFix(
|
|
|
1213
1213
|
capture:
|
|
1214
1214
|
`The captured shell is served to every user of this URL, so ` +
|
|
1215
1215
|
`request-scoped data read here would leak one user's ${what} to ` +
|
|
1216
|
-
`others. Read it inside a loader without ssr:
|
|
1216
|
+
`others. Read it inside a loader without ssr: "await" and consume it ` +
|
|
1217
1217
|
`with useLoader, e.g. createLoader(async () => ` +
|
|
1218
1218
|
`getUser(cookies().get("session")?.value)). ${PPR_LANE_HINT}`,
|
|
1219
1219
|
warning:
|
|
1220
|
-
|
|
1220
|
+
'Read it in a loader without ssr: "await" and consume it with useLoader ' +
|
|
1221
1221
|
"under loading() or an inline <Suspense> (a live hole). A promise the " +
|
|
1222
1222
|
"handler passes or pushes does not help: the capture waits for it.",
|
|
1223
1223
|
};
|
|
@@ -195,7 +195,7 @@ export function guardRequestHeaders(request: Request): Request {
|
|
|
195
195
|
/** The fix for a theme read that a cache() boundary or a ppr capture refuses. */
|
|
196
196
|
const THEME_READ_FIX =
|
|
197
197
|
"On ppr and cache() routes, read the theme with useTheme() in a client " +
|
|
198
|
-
|
|
198
|
+
'component, or in a live loader (no ssr: "await") with cookies().get(<storageKey>). ' +
|
|
199
199
|
"The <html> theme class needs neither: the theme script sets it before paint. " +
|
|
200
200
|
"See the /theme skill (node_modules/@rangojs/router/skills/theme/SKILL.md).";
|
|
201
201
|
|
|
@@ -863,7 +863,7 @@ export interface RequestContext<
|
|
|
863
863
|
|
|
864
864
|
/**
|
|
865
865
|
* @internal Loader IDs ($$id) whose entries carry `awaitBeforeFlush`
|
|
866
|
-
* (loader(Def, { ssr:
|
|
866
|
+
* (loader(Def, { ssr: "await" })): segment resolution awaits these
|
|
867
867
|
* before returning, so the render barrier cannot resolve until they settle.
|
|
868
868
|
* rendered() checks this set to fail fast — a flagged loader awaiting the
|
|
869
869
|
* barrier is a guaranteed cycle, not a race. Registered by resolveOwnerLoaders
|