@rangojs/router 0.22.1 → 0.23.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/dist/bin/rango.js +8 -3
- package/dist/testing/vitest.js +31 -1
- package/dist/types/browser/defer.d.ts +48 -0
- package/dist/types/browser/document-revealed.d.ts +17 -0
- package/dist/types/browser/event-controller.d.ts +7 -1
- package/dist/types/browser/navigation-transaction.d.ts +21 -7
- package/dist/types/browser/partial-update.d.ts +34 -5
- package/dist/types/browser/react/NavigationProvider.d.ts +13 -3
- package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
- package/dist/types/browser/response-adapter.d.ts +7 -0
- package/dist/types/browser/types.d.ts +19 -9
- package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
- package/dist/types/build/generate-route-types.d.ts +1 -1
- package/dist/types/build/route-types/router-processing.d.ts +1 -1
- package/dist/types/build/route-types/scan-filter.d.ts +16 -2
- package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
- package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
- package/dist/types/client-urls/client-urls.d.ts +1 -1
- package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
- package/dist/types/client-urls/navigation.d.ts +0 -37
- package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
- package/dist/types/client-urls/server-projection.d.ts +29 -1
- package/dist/types/client-urls/types.d.ts +34 -69
- package/dist/types/client.d.ts +0 -1
- package/dist/types/errors.d.ts +5 -0
- package/dist/types/handles/deferred-resolution.d.ts +2 -1
- package/dist/types/handles/pushed-by.d.ts +23 -0
- package/dist/types/loader-store.d.ts +8 -0
- package/dist/types/prefetch-markers.d.ts +11 -0
- package/dist/types/route-definition/helpers-types.d.ts +27 -0
- package/dist/types/router/is-action.d.ts +2 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +241 -0
- package/dist/types/server/context.d.ts +21 -1
- package/dist/types/server/handle-store.d.ts +9 -1
- package/dist/types/server/request-context.d.ts +14 -1
- package/dist/types/server.d.ts +1 -0
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +3 -3
- package/dist/types/testing/internal/action-id.d.ts +10 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
- package/dist/types/testing/internal/test-action.d.ts +8 -0
- package/dist/types/testing/run-loader.d.ts +2 -1
- package/dist/types/testing/run-revalidate.d.ts +74 -0
- package/dist/types/testing/run-transition-when.d.ts +2 -3
- package/dist/types/testing/serve-shell-request.d.ts +38 -0
- package/dist/types/testing/vitest.d.ts +11 -0
- package/dist/types/types/boundaries.d.ts +9 -0
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +30 -0
- package/dist/types/types/segments.d.ts +21 -4
- package/dist/types/urls/path-helper-types.d.ts +25 -4
- package/dist/types/urls/type-extraction.d.ts +6 -2
- package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
- package/dist/types/vite/discovery/state.d.ts +11 -4
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
- package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
- package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
- package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
- package/dist/types/vite/router-discovery.d.ts +10 -0
- package/dist/vite/index.js +842 -308
- package/package.json +1 -1
- package/skills/caching/SKILL.md +42 -5
- package/skills/catalog.json +2 -2
- package/skills/client-urls/SKILL.md +113 -24
- package/skills/cloudflare/SKILL.md +14 -9
- package/skills/hooks/SKILL.md +11 -3
- package/skills/links/SKILL.md +24 -0
- package/skills/loader/SKILL.md +192 -1
- package/skills/migrate-nextjs/SKILL.md +2 -2
- package/skills/migrate-react-router/SKILL.md +10 -7
- package/skills/migrate-react-router/data-and-actions.md +1 -1
- package/skills/ppr/SKILL.md +13 -0
- package/skills/prerender/SKILL.md +22 -8
- package/skills/rango/SKILL.md +12 -6
- package/skills/route/SKILL.md +139 -0
- package/skills/testing/SKILL.md +3 -2
- package/skills/testing/client-components.md +1 -1
- package/skills/testing/predicates.md +67 -32
- package/skills/testing/serve-shell-request.md +88 -18
- package/skills/testing/setup.md +2 -2
- package/skills/typesafety/route-types.md +2 -2
- package/skills/view-transitions/SKILL.md +26 -0
- package/src/browser/defer.ts +82 -0
- package/src/browser/document-revealed.ts +52 -0
- package/src/browser/event-controller.ts +43 -32
- package/src/browser/navigation-bridge.ts +64 -14
- package/src/browser/navigation-client.ts +48 -36
- package/src/browser/navigation-store.ts +13 -0
- package/src/browser/navigation-transaction.ts +25 -8
- package/src/browser/partial-update.ts +528 -135
- package/src/browser/prefetch/fetch.ts +2 -1
- package/src/browser/react/NavigationProvider.tsx +47 -28
- package/src/browser/react/filter-segment-order.ts +13 -2
- package/src/browser/response-adapter.ts +20 -0
- package/src/browser/rsc-router.tsx +17 -45
- package/src/browser/server-action-bridge.ts +19 -63
- package/src/browser/types.ts +19 -9
- package/src/browser/validate-redirect-origin.ts +16 -0
- package/src/build/generate-route-types.ts +1 -0
- package/src/build/route-types/router-processing.ts +3 -6
- package/src/build/route-types/scan-filter.ts +37 -10
- package/src/cache/cf/cf-cache-store.ts +59 -21
- package/src/cache/cf/cf-cache-types.ts +24 -2
- package/src/cache/document-cache.ts +22 -1
- package/src/client-urls/client-urls.ts +22 -6
- package/src/client-urls/hoisted-revalidate.ts +25 -0
- package/src/client-urls/navigation.ts +0 -96
- package/src/client-urls/revalidate-registry.ts +30 -0
- package/src/client-urls/server-projection.ts +151 -65
- package/src/client-urls/types.ts +39 -71
- package/src/client.tsx +9 -5
- package/src/errors.ts +15 -0
- package/src/handles/deferred-resolution.ts +28 -5
- package/src/handles/pushed-by.ts +122 -0
- package/src/loader-store.ts +18 -7
- package/src/prefetch-markers.ts +13 -0
- package/src/route-content-wrapper.tsx +18 -4
- package/src/route-definition/dsl-helpers.ts +27 -4
- package/src/route-definition/helpers-types.ts +27 -1
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/is-action.ts +2 -1
- package/src/router/loader-resolution.ts +83 -4
- package/src/router/match-api.ts +36 -1
- package/src/router/match-middleware/background-revalidation.ts +11 -1
- package/src/router/match-middleware/cache-lookup.ts +29 -6
- package/src/router/match-middleware/cache-store.ts +17 -1
- package/src/router/match-result.ts +14 -2
- package/src/router/navigation-snapshot.ts +2 -1
- package/src/router/segment-resolution/fresh.ts +22 -18
- package/src/router/segment-resolution/loader-cache.ts +2 -1
- package/src/router/segment-resolution/loader-mask.ts +5 -3
- package/src/router/segment-resolution/prefetch-deferral.ts +543 -0
- package/src/router/segment-resolution/revalidation.ts +169 -60
- package/src/rsc/rsc-rendering.ts +50 -7
- package/src/rsc/shell-capture.ts +2 -1
- package/src/segment-loader-promise.ts +23 -20
- package/src/segment-system.tsx +60 -26
- package/src/server/context.ts +39 -5
- package/src/server/handle-store.ts +104 -8
- package/src/server/request-context.ts +21 -1
- package/src/server.ts +4 -0
- package/src/testing/action-form.ts +2 -11
- package/src/testing/e2e/page-helpers.ts +3 -1
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +6 -3
- package/src/testing/internal/action-id.ts +16 -1
- package/src/testing/internal/client-urls-projection.ts +70 -0
- package/src/testing/internal/test-action.ts +25 -0
- package/src/testing/run-loader.ts +2 -1
- package/src/testing/run-revalidate.ts +142 -0
- package/src/testing/run-transition-when.ts +7 -8
- package/src/testing/serve-shell-request.ts +116 -6
- package/src/testing/vitest.ts +67 -1
- package/src/types/boundaries.ts +12 -0
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +30 -0
- package/src/types/segments.ts +26 -4
- package/src/urls/path-helper-types.ts +27 -5
- package/src/urls/type-extraction.ts +30 -10
- package/src/use-loader.tsx +18 -13
- package/src/vite/discovery/client-urls-projection.ts +20 -21
- package/src/vite/discovery/discover-routers.ts +94 -26
- package/src/vite/discovery/state.ts +20 -4
- package/src/vite/discovery/virtual-module-codegen.ts +22 -17
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
- package/src/vite/plugins/hoist/literal.ts +417 -0
- package/src/vite/plugins/hoist/scope.ts +370 -0
- package/src/vite/plugins/hoist/source.ts +84 -0
- package/src/vite/plugins/transition-when-hoist.ts +113 -779
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +93 -32
- package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
- package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
- package/dist/types/testing/run-client-revalidate.d.ts +0 -43
- package/src/client-urls/revalidate-chain.ts +0 -83
- package/src/client-urls/revalidation-protocol.ts +0 -56
- package/src/testing/run-client-revalidate.ts +0 -92
package/src/segment-system.tsx
CHANGED
|
@@ -9,6 +9,8 @@ import { AuditedOutletProvider, OutletProvider } from "./outlet-provider.js";
|
|
|
9
9
|
import { withOptimisticCommitNone } from "./browser/optimistic-commit.js";
|
|
10
10
|
import { MountContextProvider } from "./browser/react/mount-context.js";
|
|
11
11
|
import type { ResolvedSegment, RootLayoutProps } from "./types.js";
|
|
12
|
+
import { isRenderableLoading } from "./types/boundaries.js";
|
|
13
|
+
import { isPlaceholder } from "./types/segments.js";
|
|
12
14
|
import { decodeLoaderResults } from "./decode-loader-results.js";
|
|
13
15
|
import { LoaderRedirect } from "./loader-redirect.js";
|
|
14
16
|
import { invariant } from "./errors.js";
|
|
@@ -23,6 +25,7 @@ import { RootErrorBoundary } from "./root-error-boundary.js";
|
|
|
23
25
|
import { INTERNAL_RANGO_DEBUG } from "./internal-debug.js";
|
|
24
26
|
import { INTERNAL_RANGO_SUSPENSE_AUDIT } from "./internal-suspense-audit.js";
|
|
25
27
|
import { getBoundaryContent } from "./segment-boundary-content.js";
|
|
28
|
+
import { asStream } from "./browser/defer.js";
|
|
26
29
|
import {
|
|
27
30
|
buildLoaderPromise,
|
|
28
31
|
getMemoizedLoaderPromise,
|
|
@@ -61,15 +64,6 @@ function segDebugLog(msg: string, details?: Record<string, unknown>): void {
|
|
|
61
64
|
const ReactViewTransition: any =
|
|
62
65
|
"ViewTransition" in React ? (React as any).ViewTransition : null;
|
|
63
66
|
|
|
64
|
-
// A loading skeleton is renderable only when it is a real ReactNode value.
|
|
65
|
-
// `false` is treated as "not renderable" here. This is the three-term gate;
|
|
66
|
-
// the distinct two-term gate at the LoaderBoundary site deliberately treats
|
|
67
|
-
// `false` as "create a boundary without a RouteContentWrapper"
|
|
68
|
-
// (tree-structure.md), so it must NOT use this helper.
|
|
69
|
-
function isRenderableLoading(loading: ReactNode): boolean {
|
|
70
|
-
return loading !== undefined && loading !== null && loading !== false;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
67
|
// Exported for unit testing the no-parallel fast path (D6); internal otherwise.
|
|
74
68
|
export function restoreParallelLoaderMarkers(
|
|
75
69
|
segments: ResolvedSegment[],
|
|
@@ -264,32 +258,61 @@ function wrapDefaultOutletContent(
|
|
|
264
258
|
* reader sits above every boundary, so the throw is a Fizz shell error (500).
|
|
265
259
|
* `settledFallback` is that node; the caller plants it and, for a redirect,
|
|
266
260
|
* replaces the whole page (an ancestor reading the loader would still throw).
|
|
261
|
+
*
|
|
262
|
+
* `settle` is the forceAwait/action lane of a loader set that holds a
|
|
263
|
+
* deferred loader (`prefetch: false`, see hasDeferred): every other loader is
|
|
264
|
+
* settled like a flagged one, so the commit stays whole for them, and the
|
|
265
|
+
* deferred one keeps its hole for the read site to suspend on. A settled
|
|
266
|
+
* error with no boundary is left to the read site there, where the aggregate
|
|
267
|
+
* lane it replaces would have thrown it.
|
|
267
268
|
*/
|
|
268
|
-
async function buildLoaderStreams(
|
|
269
|
+
async function buildLoaderStreams(
|
|
270
|
+
loaders: ResolvedSegment[],
|
|
271
|
+
settle?: boolean,
|
|
272
|
+
): Promise<{
|
|
269
273
|
streams: Record<string, unknown>;
|
|
270
274
|
awaitedIds: string[] | undefined;
|
|
271
275
|
settledFallback: ReactNode | undefined;
|
|
272
276
|
}> {
|
|
273
277
|
const streams: Record<string, unknown> = {};
|
|
274
278
|
let awaitedIds: string[] | undefined;
|
|
275
|
-
let
|
|
279
|
+
let settledIds: string[] | undefined;
|
|
280
|
+
let settledValues: unknown[] | undefined;
|
|
276
281
|
for (const l of loaders) {
|
|
277
|
-
if (l.awaitBeforeFlush) {
|
|
282
|
+
if (l.awaitBeforeFlush || (settle && !isPlaceholder(l))) {
|
|
278
283
|
const value = await l.loaderData;
|
|
279
284
|
streams[l.loaderId!] = value;
|
|
280
|
-
(awaitedIds ??= []).push(l.loaderId!);
|
|
281
|
-
(
|
|
285
|
+
if (l.awaitBeforeFlush) (awaitedIds ??= []).push(l.loaderId!);
|
|
286
|
+
(settledIds ??= []).push(l.loaderId!);
|
|
287
|
+
(settledValues ??= []).push(value);
|
|
282
288
|
} else {
|
|
283
289
|
streams[l.loaderId!] = l.loaderData;
|
|
284
290
|
}
|
|
285
291
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
292
|
+
let settledFallback: ReactNode | undefined;
|
|
293
|
+
if (settledIds) {
|
|
294
|
+
try {
|
|
295
|
+
settledFallback =
|
|
296
|
+
decodeLoaderResults(settledValues!, settledIds).errorFallback ??
|
|
297
|
+
undefined;
|
|
298
|
+
} catch (error) {
|
|
299
|
+
if (!settle) throw error;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
290
302
|
return { streams, awaitedIds, settledFallback };
|
|
291
303
|
}
|
|
292
304
|
|
|
305
|
+
/**
|
|
306
|
+
* A loader set that holds a deferred loader (`prefetch: false`): its
|
|
307
|
+
* `loaderData` is a hole (browser/defer.ts) the browser resolves when the
|
|
308
|
+
* fill request lands (browser/partial-update.ts fillHoles), so no lane may
|
|
309
|
+
* await the set as a whole. The
|
|
310
|
+
* tree build would wait for a request that is only sent once it returns.
|
|
311
|
+
*/
|
|
312
|
+
function hasDeferred(loaders: ResolvedSegment[]): boolean {
|
|
313
|
+
return loaders.some(isPlaceholder);
|
|
314
|
+
}
|
|
315
|
+
|
|
293
316
|
function isLoaderRedirectNode(node: ReactNode): boolean {
|
|
294
317
|
return isValidElement(node) && node.type === LoaderRedirect;
|
|
295
318
|
}
|
|
@@ -354,6 +377,11 @@ export async function renderSegments(
|
|
|
354
377
|
rootLayout: RootLayout,
|
|
355
378
|
transitionGatedOff,
|
|
356
379
|
} = options || {};
|
|
380
|
+
// forceAwait and action lanes settle loader data before the commit, except
|
|
381
|
+
// a set that holds a deferred loader (hasDeferred).
|
|
382
|
+
const settleLoaders = !!(forceAwait || isAction);
|
|
383
|
+
const settleAll = (loaders: ResolvedSegment[]): boolean =>
|
|
384
|
+
settleLoaders && !hasDeferred(loaders);
|
|
357
385
|
|
|
358
386
|
const segDebug = INTERNAL_RANGO_DEBUG;
|
|
359
387
|
const segDebugStart = segDebug ? performance.now() : 0;
|
|
@@ -484,6 +512,8 @@ export async function renderSegments(
|
|
|
484
512
|
(loader) => loader.loaderId && loader.loaderData !== undefined,
|
|
485
513
|
);
|
|
486
514
|
|
|
515
|
+
// A deferred unit's component is a hole (browser/defer.ts), never a
|
|
516
|
+
// Promise: the awaits below pass it through.
|
|
487
517
|
let resolvedComponent = component;
|
|
488
518
|
if (isAction && component instanceof Promise) {
|
|
489
519
|
const componentAwaitStart = segDebug ? performance.now() : 0;
|
|
@@ -540,7 +570,11 @@ export async function renderSegments(
|
|
|
540
570
|
componentTypeof: typeof c,
|
|
541
571
|
});
|
|
542
572
|
}
|
|
543
|
-
|
|
573
|
+
// A hole here is read by React itself (browser/defer.ts asStream).
|
|
574
|
+
const streamed = asStream(resolvedComponent) as ReactNode;
|
|
575
|
+
nodeContent = isPlaceholder(node.segment)
|
|
576
|
+
? streamed
|
|
577
|
+
: registerLazyRef(streamed);
|
|
544
578
|
}
|
|
545
579
|
|
|
546
580
|
// Wrap with <ViewTransition> if transition config exists (React 19.3+ / experimental).
|
|
@@ -614,7 +648,7 @@ export async function renderSegments(
|
|
|
614
648
|
// for content-suspending routes (segment-loader-promise.ts).
|
|
615
649
|
let boundaryLoaderStreams: Record<string, unknown> | undefined;
|
|
616
650
|
let boundaryAwaitedLoaderIds: string[] | undefined;
|
|
617
|
-
if (
|
|
651
|
+
if (settleAll(loaderEntries)) {
|
|
618
652
|
const awaitStart = segDebug ? performance.now() : 0;
|
|
619
653
|
boundaryLoaderData =
|
|
620
654
|
loaderDataPromise instanceof Promise
|
|
@@ -632,7 +666,7 @@ export async function renderSegments(
|
|
|
632
666
|
streams: boundaryLoaderStreams,
|
|
633
667
|
awaitedIds: boundaryAwaitedLoaderIds,
|
|
634
668
|
settledFallback,
|
|
635
|
-
} = await buildLoaderStreams(loaderEntries));
|
|
669
|
+
} = await buildLoaderStreams(loaderEntries, settleLoaders));
|
|
636
670
|
if (settledFallback !== undefined) {
|
|
637
671
|
if (isLoaderRedirectNode(settledFallback)) {
|
|
638
672
|
content = settledFallback;
|
|
@@ -698,7 +732,7 @@ export async function renderSegments(
|
|
|
698
732
|
let errorFallback: ReactNode = null;
|
|
699
733
|
let loaderStreams: Record<string, unknown> | undefined;
|
|
700
734
|
let awaitedLoaderIds: string[] | undefined;
|
|
701
|
-
if (
|
|
735
|
+
if (settleAll(layoutLoaders)) {
|
|
702
736
|
const layoutAwaitStart = segDebug ? performance.now() : 0;
|
|
703
737
|
const resolvedData = await buildLoaderPromise(layoutLoaders);
|
|
704
738
|
if (segDebug) {
|
|
@@ -726,7 +760,7 @@ export async function renderSegments(
|
|
|
726
760
|
streams: loaderStreams,
|
|
727
761
|
awaitedIds: awaitedLoaderIds,
|
|
728
762
|
settledFallback,
|
|
729
|
-
} = await buildLoaderStreams(layoutLoaders));
|
|
763
|
+
} = await buildLoaderStreams(layoutLoaders, settleLoaders));
|
|
730
764
|
if (settledFallback !== undefined) {
|
|
731
765
|
if (isLoaderRedirectNode(settledFallback)) {
|
|
732
766
|
content = settledFallback;
|
|
@@ -780,9 +814,9 @@ export async function renderSegments(
|
|
|
780
814
|
// loader paid the pre-flush await, settle the aggregate so
|
|
781
815
|
// LoaderResolver decodes the array without a Flight-chunk use().
|
|
782
816
|
const settleParallelAggregate =
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
ownedLoaders
|
|
817
|
+
(settleLoaders ||
|
|
818
|
+
ownedLoaders.every((l) => l.awaitBeforeFlush === true)) &&
|
|
819
|
+
!hasDeferred(ownedLoaders);
|
|
786
820
|
if (settleParallelAggregate) {
|
|
787
821
|
const parallelAwaitStart = segDebug ? performance.now() : 0;
|
|
788
822
|
p.loaderDataPromise =
|
package/src/server/context.ts
CHANGED
|
@@ -117,6 +117,13 @@ export type EntryPropDatas = {
|
|
|
117
117
|
*/
|
|
118
118
|
export type EntryPropRender = {
|
|
119
119
|
loading?: ReactNode | false;
|
|
120
|
+
/**
|
|
121
|
+
* loading(fallback, { prefetch: false }) with a renderable fallback: a
|
|
122
|
+
* prefetch defers what the fallback covers while the entry's segment is
|
|
123
|
+
* new to the client (router/segment-resolution/prefetch-deferral.ts). Set
|
|
124
|
+
* on both DSL evaluations, whatever `ssr` did to `loading`.
|
|
125
|
+
*/
|
|
126
|
+
loadingPrefetch?: false;
|
|
120
127
|
transition?: TransitionConfig;
|
|
121
128
|
};
|
|
122
129
|
|
|
@@ -150,6 +157,12 @@ export type LoaderEntry = {
|
|
|
150
157
|
* navigation evaluation never has it.
|
|
151
158
|
*/
|
|
152
159
|
bake?: true;
|
|
160
|
+
/**
|
|
161
|
+
* loader(Def, { prefetch: false }): a prefetch defers this registration
|
|
162
|
+
* while its segment is new to the client
|
|
163
|
+
* (router/segment-resolution/prefetch-deferral.ts).
|
|
164
|
+
*/
|
|
165
|
+
prefetch?: false;
|
|
153
166
|
};
|
|
154
167
|
|
|
155
168
|
/**
|
|
@@ -867,9 +880,14 @@ export function track(label: string, depth?: number): () => void {
|
|
|
867
880
|
* nested RangoContext.run() calls in Vite's module runner.
|
|
868
881
|
*/
|
|
869
882
|
const LOADER_SCOPE_KEY = Symbol.for("rangojs-router:loader-scope");
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
883
|
+
interface LoaderScope {
|
|
884
|
+
active: true;
|
|
885
|
+
/** $$id of the DSL loader this scope resolves (runInsideLoaderScope). */
|
|
886
|
+
loaderId?: string;
|
|
887
|
+
}
|
|
888
|
+
const loaderScopeALS: AsyncLocalStorage<LoaderScope> = ((globalThis as any)[
|
|
889
|
+
LOADER_SCOPE_KEY
|
|
890
|
+
] ??= new AsyncLocalStorage<LoaderScope>());
|
|
873
891
|
|
|
874
892
|
// Purity-only scope: marks that a loader FUNCTION BODY is executing, regardless
|
|
875
893
|
// of how the loader was invoked (DSL via runInsideLoaderScope, or handler-
|
|
@@ -1429,8 +1447,24 @@ export function assertCachedHeaderWriteAllowed(
|
|
|
1429
1447
|
* are bypassed because loaders are always fresh (never cached) and
|
|
1430
1448
|
* their side effects (setCookie, header, etc.) are safe.
|
|
1431
1449
|
*/
|
|
1432
|
-
export function runInsideLoaderScope<T>(fn: () => T): T {
|
|
1433
|
-
return loaderScopeALS.run({ active: true }, fn);
|
|
1450
|
+
export function runInsideLoaderScope<T>(fn: () => T, loaderId?: string): T {
|
|
1451
|
+
return loaderScopeALS.run({ active: true, loaderId }, fn);
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* The loader whose run a handle push here belongs to, for the browser's
|
|
1456
|
+
* merge of a partial response (handles/pushed-by.ts): the DSL loader
|
|
1457
|
+
* this scope resolves, else the outermost loader body around the push (a
|
|
1458
|
+
* loader a handler reads with ctx.use). Undefined for a handler's own push.
|
|
1459
|
+
*/
|
|
1460
|
+
export function getHandlePushOwner(): string | undefined {
|
|
1461
|
+
const dsl = loaderScopeALS.getStore()?.loaderId;
|
|
1462
|
+
if (dsl !== undefined) return dsl;
|
|
1463
|
+
let outermost: string | undefined;
|
|
1464
|
+
for (let s = loaderBodyScopeALS.getStore(); s; s = s.parent) {
|
|
1465
|
+
if (s.loaderId !== undefined) outermost = s.loaderId;
|
|
1466
|
+
}
|
|
1467
|
+
return outermost;
|
|
1434
1468
|
}
|
|
1435
1469
|
|
|
1436
1470
|
/**
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
findEnclosingLoaderBody,
|
|
3
|
+
getHandlePushOwner,
|
|
4
|
+
isInsideLoaderScope,
|
|
5
|
+
} from "./context.js";
|
|
2
6
|
import { holdsThenable } from "../router/segment-resolution/mask-nested.js";
|
|
7
|
+
import { pushedByKey } from "../handles/pushed-by.js";
|
|
3
8
|
|
|
4
9
|
/**
|
|
5
10
|
* Handle data structure: handleName -> segmentId -> entries[]
|
|
@@ -75,6 +80,12 @@ interface SlotTag {
|
|
|
75
80
|
restoredOf?: string;
|
|
76
81
|
/** A restored slot a dropped settled push of its loader stood in for. */
|
|
77
82
|
matched?: true;
|
|
83
|
+
/**
|
|
84
|
+
* The loader whose run made the push, for the browser (pushed-by.ts): the
|
|
85
|
+
* DSL loader or loader body it is made in (getHandlePushOwner), else the
|
|
86
|
+
* loader a copy stands for. Unset for a handler's push.
|
|
87
|
+
*/
|
|
88
|
+
by?: string;
|
|
78
89
|
}
|
|
79
90
|
|
|
80
91
|
// Shared by every plain DSL-loader push; tags are never mutated in place.
|
|
@@ -111,6 +122,24 @@ function cloneHandleData(data: HandleData): HandleData {
|
|
|
111
122
|
return clone;
|
|
112
123
|
}
|
|
113
124
|
|
|
125
|
+
// SlotTag.by of each value, index-aligned; undefined when no value has one.
|
|
126
|
+
function pushedByOf(
|
|
127
|
+
values: unknown[],
|
|
128
|
+
tags: (SlotTag | undefined)[] | undefined,
|
|
129
|
+
keep?: (i: number) => boolean,
|
|
130
|
+
): (string | null)[] | undefined {
|
|
131
|
+
if (!tags) return undefined;
|
|
132
|
+
const by: (string | null)[] = [];
|
|
133
|
+
let any = false;
|
|
134
|
+
for (let i = 0; i < values.length; i++) {
|
|
135
|
+
if (keep && !keep(i)) continue;
|
|
136
|
+
const run = tags[i]?.by ?? null;
|
|
137
|
+
if (run !== null) any = true;
|
|
138
|
+
by.push(run);
|
|
139
|
+
}
|
|
140
|
+
return any ? by : undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
114
143
|
/**
|
|
115
144
|
* HandleStore tracks pending handler promises and stores handle data.
|
|
116
145
|
*
|
|
@@ -297,7 +326,8 @@ export interface HandleStore {
|
|
|
297
326
|
* nav/action payloads, whose consumer applies each yield progressively) or
|
|
298
327
|
* "settled" (the document lane, whose consumers drain to completion in
|
|
299
328
|
* blocking positions and must not wait on loader bodies).
|
|
300
|
-
* Each yield contains the full accumulated state (not just the delta)
|
|
329
|
+
* Each yield contains the full accumulated state (not just the delta),
|
|
330
|
+
* with the pushedBy records of its buckets (handles/pushed-by.ts).
|
|
301
331
|
* Safe for concurrent consumers (per-consumer version cursor).
|
|
302
332
|
* The "settled" lane of a frozen store (freezeDocumentSnapshot) yields the
|
|
303
333
|
* frozen state once, at once, and ends. That yield is the store's own
|
|
@@ -331,6 +361,17 @@ export interface HandleStore {
|
|
|
331
361
|
excludeLoaderPushes?: boolean,
|
|
332
362
|
): Record<string, unknown[]>;
|
|
333
363
|
|
|
364
|
+
/**
|
|
365
|
+
* The loader whose run pushed each value in getDataForSegment(segmentId,
|
|
366
|
+
* excludeLoaderPushes), index-aligned per handle (null: the handler's).
|
|
367
|
+
* Handles without such a value are omitted; undefined when none has one.
|
|
368
|
+
* The browser's pushedBy record (handles/pushed-by.ts).
|
|
369
|
+
*/
|
|
370
|
+
getPushedBy(
|
|
371
|
+
segmentId: string,
|
|
372
|
+
excludeLoaderPushes?: boolean,
|
|
373
|
+
): Record<string, (string | null)[]> | undefined;
|
|
374
|
+
|
|
334
375
|
/**
|
|
335
376
|
* The push `owner` of each value in getDataForSegment(segmentId, true),
|
|
336
377
|
* index-aligned per handle (null: no owner). Handles without an owned
|
|
@@ -379,6 +420,14 @@ export function createHandleStore(): HandleStore {
|
|
|
379
420
|
// (not value identity) so primitive values are covered too.
|
|
380
421
|
// replaySegmentData installs fresh arrays, so restored values are untagged.
|
|
381
422
|
const slotTags = new WeakMap<unknown[], (SlotTag | undefined)[]>();
|
|
423
|
+
// Frozen tags of plain pushes made by a run (SlotTag.by), one per loader.
|
|
424
|
+
const pushedBySlots = new Map<string, SlotTag>();
|
|
425
|
+
const pushedBySlot = (loader: true | undefined, by: string): SlotTag => {
|
|
426
|
+
const key = loader ? `L${by}` : `H${by}`;
|
|
427
|
+
let tag = pushedBySlots.get(key);
|
|
428
|
+
if (!tag) pushedBySlots.set(key, (tag = Object.freeze({ loader, by })));
|
|
429
|
+
return tag;
|
|
430
|
+
};
|
|
382
431
|
// Copies standing in for a loader that has not pushed live yet: loaderId ->
|
|
383
432
|
// the arrays holding them. Kept apart by kind: only a placeholder gives way
|
|
384
433
|
// to a run that ends without a push, or to a cached unit's delivery.
|
|
@@ -602,6 +651,25 @@ export function createHandleStore(): HandleStore {
|
|
|
602
651
|
}
|
|
603
652
|
}
|
|
604
653
|
|
|
654
|
+
// What a stream yields: a copy of the data, with each bucket's pushedBy
|
|
655
|
+
// record (pushed-by.ts) for the browser's merge.
|
|
656
|
+
function snapshot(): HandleData {
|
|
657
|
+
const out: HandleData = {};
|
|
658
|
+
for (const handleName in data) {
|
|
659
|
+
const buckets: Record<string, unknown[]> = {};
|
|
660
|
+
let pushedBy: Record<string, (string | null)[]> | undefined;
|
|
661
|
+
for (const segmentId in data[handleName]) {
|
|
662
|
+
const values = data[handleName][segmentId];
|
|
663
|
+
buckets[segmentId] = [...values];
|
|
664
|
+
const by = pushedByOf(values, slotTags.get(values));
|
|
665
|
+
if (by) (pushedBy ??= {})[segmentId] = by;
|
|
666
|
+
}
|
|
667
|
+
out[handleName] = buckets;
|
|
668
|
+
if (pushedBy) out[pushedByKey(handleName)] = pushedBy;
|
|
669
|
+
}
|
|
670
|
+
return out;
|
|
671
|
+
}
|
|
672
|
+
|
|
605
673
|
// Settlement barriers: `settled` (handler lane) resolves when sealed AND
|
|
606
674
|
// handler inflight === 0. `fullySettled` additionally waits for the
|
|
607
675
|
// auxiliary (loader) lane. seal() signals "no more track() calls"; each
|
|
@@ -812,6 +880,12 @@ export function createHandleStore(): HandleStore {
|
|
|
812
880
|
at = restoredInsertIndex(values, restoredBy);
|
|
813
881
|
if (replayOf === undefined) liveOf = restoredBy;
|
|
814
882
|
}
|
|
883
|
+
const by =
|
|
884
|
+
getHandlePushOwner() ??
|
|
885
|
+
replayOf ??
|
|
886
|
+
restoredOf ??
|
|
887
|
+
placeholderOf ??
|
|
888
|
+
owner;
|
|
815
889
|
insertSlot(
|
|
816
890
|
values,
|
|
817
891
|
at,
|
|
@@ -829,10 +903,13 @@ export function createHandleStore(): HandleStore {
|
|
|
829
903
|
liveOf,
|
|
830
904
|
under,
|
|
831
905
|
owner,
|
|
906
|
+
by,
|
|
832
907
|
}
|
|
833
|
-
:
|
|
834
|
-
?
|
|
835
|
-
:
|
|
908
|
+
: by !== undefined
|
|
909
|
+
? pushedBySlot(loader, by)
|
|
910
|
+
: loader
|
|
911
|
+
? LOADER_SLOT
|
|
912
|
+
: undefined,
|
|
836
913
|
);
|
|
837
914
|
if (replayOf !== undefined) {
|
|
838
915
|
addPending(pendingReplays, replayOf, values);
|
|
@@ -958,7 +1035,7 @@ export function createHandleStore(): HandleStore {
|
|
|
958
1035
|
if (this.onError) this.onError(error);
|
|
959
1036
|
else console.error(error);
|
|
960
1037
|
}
|
|
961
|
-
documentSnapshot = { version, data:
|
|
1038
|
+
documentSnapshot = { version, data: snapshot() };
|
|
962
1039
|
},
|
|
963
1040
|
|
|
964
1041
|
async *stream(
|
|
@@ -1005,7 +1082,7 @@ export function createHandleStore(): HandleStore {
|
|
|
1005
1082
|
while (true) {
|
|
1006
1083
|
if (version > seen && Object.keys(data).length > 0) {
|
|
1007
1084
|
seen = version;
|
|
1008
|
-
yield
|
|
1085
|
+
yield snapshot();
|
|
1009
1086
|
}
|
|
1010
1087
|
if (done) break;
|
|
1011
1088
|
await waitForEmission(seen, () => done);
|
|
@@ -1043,7 +1120,7 @@ export function createHandleStore(): HandleStore {
|
|
|
1043
1120
|
while (true) {
|
|
1044
1121
|
if (version > seen) {
|
|
1045
1122
|
seen = version;
|
|
1046
|
-
yield
|
|
1123
|
+
yield snapshot();
|
|
1047
1124
|
}
|
|
1048
1125
|
if (done) break;
|
|
1049
1126
|
await waitForEmission(seen, () => done);
|
|
@@ -1069,6 +1146,25 @@ export function createHandleStore(): HandleStore {
|
|
|
1069
1146
|
return result;
|
|
1070
1147
|
},
|
|
1071
1148
|
|
|
1149
|
+
getPushedBy(
|
|
1150
|
+
segmentId: string,
|
|
1151
|
+
excludeLoaderPushes?: boolean,
|
|
1152
|
+
): Record<string, (string | null)[]> | undefined {
|
|
1153
|
+
let result: Record<string, (string | null)[]> | undefined;
|
|
1154
|
+
for (const handleName in data) {
|
|
1155
|
+
const values = data[handleName][segmentId];
|
|
1156
|
+
if (!values) continue;
|
|
1157
|
+
const tags = slotTags.get(values);
|
|
1158
|
+
const by = pushedByOf(
|
|
1159
|
+
values,
|
|
1160
|
+
tags,
|
|
1161
|
+
excludeLoaderPushes ? (i) => !tags?.[i]?.loader : undefined,
|
|
1162
|
+
);
|
|
1163
|
+
if (by) (result ??= {})[handleName] = by;
|
|
1164
|
+
}
|
|
1165
|
+
return result;
|
|
1166
|
+
},
|
|
1167
|
+
|
|
1072
1168
|
getRecordOwners(
|
|
1073
1169
|
segmentId: string,
|
|
1074
1170
|
): Record<string, (string | null)[]> | undefined {
|
|
@@ -78,6 +78,7 @@ import {
|
|
|
78
78
|
readGuardedTheme,
|
|
79
79
|
} from "./cookie-store.js";
|
|
80
80
|
import { requestHeaders } from "./request-headers.js";
|
|
81
|
+
import { requestKind } from "../router/segment-resolution/prefetch-deferral.js";
|
|
81
82
|
import {
|
|
82
83
|
createReverseFunction,
|
|
83
84
|
stripInternalParams,
|
|
@@ -713,6 +714,21 @@ export interface RequestContext<
|
|
|
713
714
|
*/
|
|
714
715
|
_payloadVisitorTheme?: boolean;
|
|
715
716
|
|
|
717
|
+
/**
|
|
718
|
+
* @internal Set by the partial render when its payload carries deferred
|
|
719
|
+
* units (`prefetch: false`): what one page's prefetch left for the fill.
|
|
720
|
+
* The document cache does not store that response, whatever Cache-Control
|
|
721
|
+
* it ends up with (docs/design/prefetch-false.md, R7).
|
|
722
|
+
*/
|
|
723
|
+
_payloadDeferred?: boolean;
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* @internal What the request says it is: a `<Link>` prefetch or a fill
|
|
727
|
+
* (`prefetch: false`), read once at creation
|
|
728
|
+
* (segment-resolution/prefetch-deferral.ts requestKind).
|
|
729
|
+
*/
|
|
730
|
+
_requestKind?: "prefetch" | "fill";
|
|
731
|
+
|
|
716
732
|
/**
|
|
717
733
|
* Attach location state entries to the current response.
|
|
718
734
|
*
|
|
@@ -1012,6 +1028,8 @@ export type PublicRequestContext<
|
|
|
1012
1028
|
| "_readTheme"
|
|
1013
1029
|
| "_documentCacheRender"
|
|
1014
1030
|
| "_payloadVisitorTheme"
|
|
1031
|
+
| "_payloadDeferred"
|
|
1032
|
+
| "_requestKind"
|
|
1015
1033
|
| "_locationState"
|
|
1016
1034
|
| "_routeName"
|
|
1017
1035
|
| "_prevRouteKey"
|
|
@@ -1445,12 +1463,14 @@ export function createRequestContext<TEnv>(
|
|
|
1445
1463
|
};
|
|
1446
1464
|
|
|
1447
1465
|
const cleanUrl = stripInternalParams(url);
|
|
1466
|
+
const originalUrl = new URL(request.url);
|
|
1448
1467
|
|
|
1449
1468
|
const ctx: RequestContext<TEnv> = {
|
|
1450
1469
|
env,
|
|
1451
1470
|
request,
|
|
1452
1471
|
url: cleanUrl,
|
|
1453
|
-
originalUrl
|
|
1472
|
+
originalUrl,
|
|
1473
|
+
_requestKind: requestKind(request, originalUrl),
|
|
1454
1474
|
pathname: url.pathname,
|
|
1455
1475
|
searchParams: cleanUrl.searchParams,
|
|
1456
1476
|
_variables: variables,
|
package/src/server.ts
CHANGED
|
@@ -58,3 +58,7 @@ export {
|
|
|
58
58
|
setClientUrlProjection,
|
|
59
59
|
type ClientUrlProjection,
|
|
60
60
|
} from "./client-urls/server-projection.js";
|
|
61
|
+
|
|
62
|
+
// Hoisted clientUrls() revalidate() predicates: the generated
|
|
63
|
+
// `<file>?rango-revalidate` modules register them in the RSC realm.
|
|
64
|
+
export { registerClientUrlRevalidators } from "./client-urls/revalidate-registry.js";
|
|
@@ -12,8 +12,7 @@
|
|
|
12
12
|
* Post the result with `serveShellRequest(router, url, { form })`.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import
|
|
16
|
-
import { registerServerModule } from "./internal/server-modules.js";
|
|
15
|
+
import { registerTestAction } from "./internal/test-action.js";
|
|
17
16
|
|
|
18
17
|
/** Options for {@link createActionForm}. */
|
|
19
18
|
export interface CreateActionFormOptions {
|
|
@@ -41,15 +40,7 @@ export function createActionForm(
|
|
|
41
40
|
options: CreateActionFormOptions,
|
|
42
41
|
): FormData {
|
|
43
42
|
const { id, useActionState = true, fields = {} } = options;
|
|
44
|
-
|
|
45
|
-
if (!moduleId || !name) {
|
|
46
|
-
throw new Error(
|
|
47
|
-
`createActionForm: id must be "<module id>#<export name>", received ${JSON.stringify(id)}`,
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
registerServerModule(moduleId, {
|
|
51
|
-
[name]: RSDServer.registerServerReference(action, moduleId, name),
|
|
52
|
-
});
|
|
43
|
+
registerTestAction(action, id, "createActionForm");
|
|
53
44
|
|
|
54
45
|
const form = new FormData();
|
|
55
46
|
if (useActionState) {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// returned from `createPageHelpers(expect)`; the rest are standalone.
|
|
4
4
|
|
|
5
5
|
import type { ConsoleMessage, Expect, Locator, Page } from "@playwright/test";
|
|
6
|
+
import { PREFETCH_HEADER } from "../../prefetch-markers.js";
|
|
6
7
|
|
|
7
8
|
// ============================================================================
|
|
8
9
|
// Standalone helpers (need only a page/locator)
|
|
@@ -170,7 +171,8 @@ export async function measureTime<T>(
|
|
|
170
171
|
export function isPrefetchRequest(req: {
|
|
171
172
|
headers: () => Record<string, string>;
|
|
172
173
|
}): boolean {
|
|
173
|
-
|
|
174
|
+
// Playwright lowercases header names.
|
|
175
|
+
return !!req.headers()[PREFETCH_HEADER.toLowerCase()];
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
// Module-level matcher + handler so blockPrefetch/unblockPrefetch share
|
package/src/testing/index.ts
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* condition and would throw if pulled into this barrel.
|
|
27
27
|
*
|
|
28
28
|
* Layers:
|
|
29
|
-
* - Unit: runMiddleware, runLoader,
|
|
29
|
+
* - Unit: runMiddleware, runLoader, runRevalidate, runTransitionWhen
|
|
30
30
|
* - Integration: dispatch (request -> Response)
|
|
31
31
|
* - Cross-cut: assertCacheStatus, assertShellStatus, assertGeneratedRoutesMatch
|
|
32
32
|
* - Component: see @rangojs/router/testing/dom (renderRoute)
|
|
@@ -55,8 +55,11 @@ export type {
|
|
|
55
55
|
RunTransitionWhenResult,
|
|
56
56
|
} from "./run-transition-when.js";
|
|
57
57
|
|
|
58
|
-
export {
|
|
59
|
-
export type {
|
|
58
|
+
export { runRevalidate } from "./run-revalidate.js";
|
|
59
|
+
export type {
|
|
60
|
+
RunRevalidateAction,
|
|
61
|
+
RunRevalidateOptions,
|
|
62
|
+
} from "./run-revalidate.js";
|
|
60
63
|
|
|
61
64
|
export { dispatch } from "./dispatch.js";
|
|
62
65
|
export type { DispatchOptions } from "./dispatch.js";
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { resolveActionRefId } from "../../router/is-action.js";
|
|
2
2
|
|
|
3
|
+
/** An imported server action, as a testing primitive's `action` option takes it. */
|
|
4
|
+
export type ImportedAction = (...args: never[]) => unknown;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Normalizes a testing primitive's `action` option: a bare imported action or
|
|
8
|
+
* actionId string becomes `{ ref }`; the object form passes through.
|
|
9
|
+
*/
|
|
10
|
+
export function normalizeTestAction<
|
|
11
|
+
T extends { ref?: ImportedAction | string },
|
|
12
|
+
>(action: ImportedAction | string | T | undefined): T | undefined {
|
|
13
|
+
return typeof action === "function" || typeof action === "string"
|
|
14
|
+
? ({ ref: action } as T)
|
|
15
|
+
: action;
|
|
16
|
+
}
|
|
17
|
+
|
|
3
18
|
/**
|
|
4
19
|
* The id a testing primitive's `action` option names: a single imported
|
|
5
20
|
* server action (its `$id ?? $$id`, as the action boundary derives it) or a
|
|
@@ -7,7 +22,7 @@ import { resolveActionRefId } from "../../router/is-action.js";
|
|
|
7
22
|
* primitive.
|
|
8
23
|
*/
|
|
9
24
|
export function resolveTestActionId(
|
|
10
|
-
action:
|
|
25
|
+
action: ImportedAction | string | undefined,
|
|
11
26
|
primitive: string,
|
|
12
27
|
): string | undefined {
|
|
13
28
|
if (action === undefined) return undefined;
|