@rangojs/router 0.5.2 → 0.6.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 +343 -125
- package/dist/types/browser/react/use-router.d.ts +10 -3
- package/dist/types/browser/react/use-search-params.d.ts +57 -10
- package/dist/types/browser/types.d.ts +22 -0
- package/dist/types/build/merge-full-manifests.d.ts +3 -0
- package/dist/types/build/route-trie.d.ts +4 -73
- package/dist/types/build/route-types/per-module-writer.d.ts +6 -4
- package/dist/types/build/route-types/router-processing.d.ts +2 -3
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/taint.d.ts +12 -6
- package/dist/types/client-urls/client-root.d.ts +38 -0
- package/dist/types/client-urls/client-urls.d.ts +5 -0
- package/dist/types/client-urls/navigation.d.ts +38 -0
- package/dist/types/client-urls/revalidation-protocol.d.ts +25 -0
- package/dist/types/client-urls/server-projection.d.ts +62 -0
- package/dist/types/client-urls/types.d.ts +144 -0
- package/dist/types/client.d.ts +12 -4
- package/dist/types/client.rsc.d.ts +4 -1
- package/dist/types/decode-loader-results.d.ts +37 -0
- package/dist/types/errors.d.ts +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +27 -0
- package/dist/types/outlet-context.d.ts +12 -0
- package/dist/types/outlet-provider.d.ts +3 -1
- package/dist/types/redirect-origin.d.ts +4 -0
- package/dist/types/route-content-wrapper.d.ts +42 -1
- package/dist/types/route-definition/helpers-types.d.ts +13 -2
- package/dist/types/router/error-handling.d.ts +35 -1
- package/dist/types/router/intercept-resolution.d.ts +12 -0
- package/dist/types/router/loader-resolution.d.ts +24 -2
- package/dist/types/router/revalidation.d.ts +7 -0
- package/dist/types/router/route-trie-builder.d.ts +77 -0
- package/dist/types/router/router-interfaces.d.ts +20 -0
- package/dist/types/router/segment-resolution/helpers.d.ts +1 -1
- package/dist/types/router/trie-matching.d.ts +1 -1
- package/dist/types/rsc/manifest-init.d.ts +5 -5
- package/dist/types/rsc/shell-capture.d.ts +9 -0
- package/dist/types/rsc/shell-serve.d.ts +11 -0
- package/dist/types/rsc/types.d.ts +30 -0
- package/dist/types/segment-system.d.ts +2 -0
- package/dist/types/server/context.d.ts +10 -0
- package/dist/types/server/handle-store.d.ts +34 -3
- package/dist/types/server/request-context.d.ts +11 -1
- package/dist/types/server.d.ts +1 -0
- package/dist/types/ssr/index.d.ts +22 -0
- package/dist/types/ssr/ssr-root.d.ts +10 -0
- package/dist/types/testing/dom.entry.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +16 -6
- package/dist/types/testing/run-loader.d.ts +9 -0
- package/dist/types/types/boundaries.d.ts +22 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/loader-types.d.ts +57 -5
- package/dist/types/types/segments.d.ts +7 -0
- package/dist/types/urls/path-helper-types.d.ts +10 -4
- package/dist/types/vite/discovery/client-urls-projection.d.ts +53 -0
- package/dist/types/vite/discovery/discover-routers.d.ts +1 -1
- package/dist/types/vite/discovery/state.d.ts +8 -1
- package/dist/vite/index.js +5313 -2365
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +39 -9
- package/skills/catalog.json +7 -1
- package/skills/client-urls/SKILL.md +338 -0
- package/skills/comparison/references/framework-comparison.md +23 -9
- package/skills/hooks/SKILL.md +2 -2
- package/skills/hooks/data.md +11 -2
- package/skills/hooks/handle-and-actions.md +7 -0
- package/skills/hooks/outlets.md +26 -5
- package/skills/hooks/urls.md +40 -3
- package/skills/loader/SKILL.md +132 -20
- package/skills/migrate-nextjs/SKILL.md +70 -10
- package/skills/migrate-react-router/SKILL.md +49 -13
- package/skills/migrate-react-router/component-migration.md +18 -13
- package/skills/migrate-react-router/data-and-actions.md +14 -3
- package/skills/migrate-react-router/route-mapping.md +15 -2
- package/skills/parallel/SKILL.md +32 -1
- package/skills/ppr/SKILL.md +16 -6
- package/skills/prerender/SKILL.md +8 -4
- package/skills/rango/SKILL.md +21 -17
- package/skills/react-compiler/SKILL.md +3 -3
- package/skills/route/SKILL.md +5 -2
- package/skills/router-setup/SKILL.md +16 -2
- package/skills/scripts/SKILL.md +16 -6
- package/skills/shell-manifest/SKILL.md +16 -7
- package/skills/testing/SKILL.md +2 -2
- package/skills/testing/client-components.md +6 -0
- package/skills/testing/handles.md +30 -8
- package/skills/testing/loader.md +51 -49
- package/skills/testing/middleware.md +1 -1
- package/skills/theme/SKILL.md +8 -5
- package/src/bin/rango.ts +7 -3
- package/src/browser/navigation-bridge.ts +6 -0
- package/src/browser/navigation-client.ts +5 -0
- package/src/browser/partial-update.ts +65 -13
- package/src/browser/react/use-router.ts +40 -11
- package/src/browser/react/use-search-params.ts +140 -17
- package/src/browser/rsc-router.tsx +59 -0
- package/src/browser/server-action-bridge.ts +26 -0
- package/src/browser/types.ts +22 -0
- package/src/build/merge-full-manifests.ts +161 -0
- package/src/build/route-trie.ts +9 -332
- package/src/build/route-types/include-resolution.ts +66 -11
- package/src/build/route-types/per-module-writer.ts +11 -6
- package/src/build/route-types/router-processing.ts +184 -153
- package/src/build/runtime-discovery.ts +23 -12
- package/src/cache/cache-exec-scope.ts +47 -0
- package/src/cache/cache-runtime.ts +24 -25
- package/src/cache/taint.ts +28 -9
- package/src/client-urls/client-root.tsx +168 -0
- package/src/client-urls/client-urls.ts +698 -0
- package/src/client-urls/navigation.ts +237 -0
- package/src/client-urls/revalidation-protocol.ts +56 -0
- package/src/client-urls/server-projection.ts +579 -0
- package/src/client-urls/types.ts +195 -0
- package/src/client.rsc.tsx +12 -0
- package/src/client.tsx +49 -6
- package/src/decode-loader-results.ts +113 -0
- package/src/errors.ts +14 -0
- package/src/handles/deferred-resolution.ts +14 -7
- package/src/index.ts +1 -0
- package/src/loader-redirect.tsx +64 -0
- package/src/outlet-context.ts +12 -0
- package/src/outlet-provider.tsx +15 -1
- package/src/redirect-origin.ts +29 -0
- package/src/route-content-wrapper.tsx +96 -3
- package/src/route-definition/dsl-helpers.ts +28 -3
- package/src/route-definition/helpers-types.ts +13 -0
- package/src/route-definition/redirect.ts +17 -18
- package/src/router/error-handling.ts +65 -11
- package/src/router/intercept-resolution.ts +29 -0
- package/src/router/loader-resolution.ts +261 -28
- package/src/router/match-result.ts +7 -0
- package/src/router/revalidation.ts +24 -11
- package/src/router/route-trie-builder.ts +334 -0
- package/src/router/router-interfaces.ts +38 -0
- package/src/router/segment-resolution/fresh.ts +47 -0
- package/src/router/segment-resolution/helpers.ts +9 -11
- package/src/router/segment-resolution/loader-cache.ts +14 -24
- package/src/router/segment-resolution/revalidation.ts +20 -1
- package/src/router/trie-matching.ts +3 -3
- package/src/router.ts +46 -1
- package/src/rsc/full-payload.ts +6 -0
- package/src/rsc/handler.ts +10 -7
- package/src/rsc/loader-fetch.ts +2 -2
- package/src/rsc/manifest-init.ts +28 -9
- package/src/rsc/rsc-rendering.ts +15 -1
- package/src/rsc/shell-capture.ts +12 -0
- package/src/rsc/shell-serve.ts +15 -2
- package/src/rsc/ssr-setup.ts +10 -1
- package/src/rsc/types.ts +31 -2
- package/src/segment-system.tsx +83 -26
- package/src/server/context.ts +10 -0
- package/src/server/cookie-store.ts +19 -19
- package/src/server/handle-store.ts +185 -48
- package/src/server/request-context.ts +30 -6
- package/src/server.ts +7 -0
- package/src/ssr/index.tsx +37 -2
- package/src/ssr/ssr-root.tsx +29 -2
- package/src/testing/dom.entry.ts +1 -1
- package/src/testing/render-route.tsx +22 -8
- package/src/testing/run-loader.ts +51 -13
- package/src/types/boundaries.ts +19 -0
- package/src/types/index.ts +1 -0
- package/src/types/loader-types.ts +60 -5
- package/src/types/segments.ts +7 -0
- package/src/urls/include-helper.ts +22 -4
- package/src/urls/path-helper-types.ts +14 -1
- package/src/use-loader.tsx +67 -6
- package/src/vite/discovery/client-urls-projection.ts +322 -0
- package/src/vite/discovery/discover-routers.ts +43 -17
- package/src/vite/discovery/state.ts +11 -1
- package/src/vite/discovery/virtual-module-codegen.ts +20 -0
- package/src/vite/plugins/virtual-entries.ts +12 -3
- package/src/vite/router-discovery.ts +163 -12
package/src/router.ts
CHANGED
|
@@ -3,6 +3,11 @@ import { createCacheScope } from "./cache/cache-scope.js";
|
|
|
3
3
|
import { resolveCacheProfiles } from "./cache/profile-registry.js";
|
|
4
4
|
import { isCachedFunction } from "./cache/taint.js";
|
|
5
5
|
import { assertClientComponent } from "./component-utils.js";
|
|
6
|
+
import {
|
|
7
|
+
isClientUrlPatterns,
|
|
8
|
+
isClientUrlReference,
|
|
9
|
+
} from "./client-urls/server-projection.js";
|
|
10
|
+
import type { ClientUrlPatterns } from "./client-urls/types.js";
|
|
6
11
|
import { DefaultDocument } from "./components/DefaultDocument.js";
|
|
7
12
|
import type { SerializedManifest } from "./debug.js";
|
|
8
13
|
import {
|
|
@@ -100,6 +105,7 @@ import type {
|
|
|
100
105
|
Rango,
|
|
101
106
|
RangoInternal,
|
|
102
107
|
RouterRequestInput,
|
|
108
|
+
UrlPatternMount,
|
|
103
109
|
} from "./router/router-interfaces.js";
|
|
104
110
|
|
|
105
111
|
// Extracted closure functions
|
|
@@ -330,6 +336,7 @@ export function createRouter<TEnv = any>(
|
|
|
330
336
|
|
|
331
337
|
// Store reference to urlpatterns for runtime manifest generation
|
|
332
338
|
let storedUrlPatterns: UrlPatterns<TEnv, any> | null = null;
|
|
339
|
+
const urlpatternMounts: UrlPatternMount<TEnv>[] = [];
|
|
333
340
|
|
|
334
341
|
// Global middleware storage
|
|
335
342
|
const globalMiddleware: MiddlewareEntry<TEnv>[] = [];
|
|
@@ -572,6 +579,13 @@ export function createRouter<TEnv = any>(
|
|
|
572
579
|
}
|
|
573
580
|
}
|
|
574
581
|
: undefined,
|
|
582
|
+
// notFound() resolution deps: the streamed envelope carries the
|
|
583
|
+
// server-rendered not-found UI, mirroring the consumption lane's
|
|
584
|
+
// boundary resolution (segment-resolution/helpers.ts).
|
|
585
|
+
{
|
|
586
|
+
findNearestNotFoundBoundary,
|
|
587
|
+
notFoundComponent: notFound,
|
|
588
|
+
},
|
|
575
589
|
);
|
|
576
590
|
|
|
577
591
|
// Emit loader.end after the promise settles (fire-and-forget)
|
|
@@ -756,7 +770,30 @@ export function createRouter<TEnv = any>(
|
|
|
756
770
|
id: routerId,
|
|
757
771
|
basename,
|
|
758
772
|
|
|
759
|
-
routes(
|
|
773
|
+
routes(
|
|
774
|
+
patternsOrBuilder:
|
|
775
|
+
| UrlPatterns<TEnv>
|
|
776
|
+
| UrlBuilder<TEnv>
|
|
777
|
+
| ClientUrlPatterns,
|
|
778
|
+
): any {
|
|
779
|
+
// Pure-client mounting shorthand: a clientUrls() definition passed
|
|
780
|
+
// directly NORMALIZES to a root include in the canonical urls() tree —
|
|
781
|
+
// exactly `include("/", definition, { name: "" })`, keeping local route
|
|
782
|
+
// names bare and local patterns app-absolute. This is sugar over the
|
|
783
|
+
// ONE composition model, not a second registration path: it rides the
|
|
784
|
+
// same lazy include materialization, so no ordering, one-definition, or
|
|
785
|
+
// deferral rules exist. Prefixing, wrapping RSC layouts, and middleware
|
|
786
|
+
// scope still come from mounting through include() in urls() yourself.
|
|
787
|
+
if (
|
|
788
|
+
isClientUrlPatterns(patternsOrBuilder) ||
|
|
789
|
+
isClientUrlReference(patternsOrBuilder)
|
|
790
|
+
) {
|
|
791
|
+
const clientSource = patternsOrBuilder as ClientUrlPatterns;
|
|
792
|
+
patternsOrBuilder = urls(({ include }) => [
|
|
793
|
+
include("/", clientSource, { name: "" }),
|
|
794
|
+
]) as UrlPatterns<TEnv>;
|
|
795
|
+
}
|
|
796
|
+
|
|
760
797
|
// Wrap builder functions in urls() automatically
|
|
761
798
|
const urlPatterns: UrlPatterns<TEnv> =
|
|
762
799
|
typeof patternsOrBuilder === "function"
|
|
@@ -766,6 +803,10 @@ export function createRouter<TEnv = any>(
|
|
|
766
803
|
// Store reference for runtime manifest generation
|
|
767
804
|
storedUrlPatterns = urlPatterns;
|
|
768
805
|
const currentMountIndex = mountIndex++;
|
|
806
|
+
urlpatternMounts.push({
|
|
807
|
+
patterns: urlPatterns,
|
|
808
|
+
mountIndex: currentMountIndex,
|
|
809
|
+
});
|
|
769
810
|
|
|
770
811
|
// Create manifest and patterns maps for route registration
|
|
771
812
|
const manifest = new Map<string, EntryData>();
|
|
@@ -1095,6 +1136,10 @@ export function createRouter<TEnv = any>(
|
|
|
1095
1136
|
return storedUrlPatterns ?? undefined;
|
|
1096
1137
|
},
|
|
1097
1138
|
|
|
1139
|
+
get __urlpatternMounts() {
|
|
1140
|
+
return urlpatternMounts;
|
|
1141
|
+
},
|
|
1142
|
+
|
|
1098
1143
|
// Expose source file for per-router type generation
|
|
1099
1144
|
__sourceFile,
|
|
1100
1145
|
|
package/src/rsc/full-payload.ts
CHANGED
|
@@ -44,11 +44,17 @@ export function buildFullPayload(
|
|
|
44
44
|
resolvedIds: m.resolvedIds,
|
|
45
45
|
params: m.params,
|
|
46
46
|
isPartial: false,
|
|
47
|
+
interceptTargets: m.interceptTargets,
|
|
47
48
|
rootLayout: ctx.router.rootLayout,
|
|
48
49
|
// Full render: resolve deferred handle values server-side so SSR markup and
|
|
49
50
|
// the first sync useHandle read see resolved values. Partial payloads
|
|
50
51
|
// (rsc-rendering.ts) keep streaming (handleStore.stream()).
|
|
51
52
|
handles: resolvedHandleStream(handleStore),
|
|
53
|
+
// Post-handler-barrier pushes (streaming loader ctx.handle() writes)
|
|
54
|
+
// stream here; the client applies them after hydration. Instantly
|
|
55
|
+
// complete when the loader lane is idle — including PPR shell capture,
|
|
56
|
+
// where masked loaders never run.
|
|
57
|
+
handlesLate: handleStore.streamLate(),
|
|
52
58
|
version: ctx.version,
|
|
53
59
|
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
54
60
|
prefetchCacheSize: ctx.router.prefetchCacheSize,
|
package/src/rsc/handler.ts
CHANGED
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* and progressive enhancement (no-JS form submissions).
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { createElement } from "react";
|
|
11
10
|
import { isRouteNotFoundError } from "../errors.js";
|
|
12
11
|
import { matchMiddleware, executeMiddleware } from "../router/middleware.js";
|
|
13
12
|
import {
|
|
@@ -50,7 +49,10 @@ import { generateNonce, nonce as nonceToken } from "./nonce.js";
|
|
|
50
49
|
import { VERSION } from "@rangojs/router:version";
|
|
51
50
|
import type { ErrorPhase } from "../types.js";
|
|
52
51
|
import type { RouterRequestInput } from "../router/router-interfaces.js";
|
|
53
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
invokeOnError,
|
|
54
|
+
resolveDefaultNotFound,
|
|
55
|
+
} from "../router/error-handling.js";
|
|
54
56
|
import {
|
|
55
57
|
createReverseFunction,
|
|
56
58
|
stripInternalParams,
|
|
@@ -1309,11 +1311,12 @@ export function createRSCHandler<
|
|
|
1309
1311
|
handledByBoundary: true,
|
|
1310
1312
|
});
|
|
1311
1313
|
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1314
|
+
// No boundary to consult: an unmatched route has no entry chain, so
|
|
1315
|
+
// this always lands on the router option or the shared default.
|
|
1316
|
+
const notFoundComponent = resolveDefaultNotFound(
|
|
1317
|
+
router.notFound,
|
|
1318
|
+
url.pathname,
|
|
1319
|
+
);
|
|
1317
1320
|
|
|
1318
1321
|
const notFoundSegment = {
|
|
1319
1322
|
id: "notFound",
|
package/src/rsc/loader-fetch.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { getLoaderLazy } from "../server/loader-registry.js";
|
|
15
|
-
import {
|
|
15
|
+
import { isDataNotFoundError } from "../errors.js";
|
|
16
16
|
import { executeLoaderMiddleware } from "../router/middleware.js";
|
|
17
17
|
import { getRequestContext } from "../server/request-context.js";
|
|
18
18
|
import { observePhase, PHASES } from "../router/instrument.js";
|
|
@@ -289,7 +289,7 @@ export async function handleLoaderFetch<TEnv>(
|
|
|
289
289
|
// so it does not match the branch above. Map it to a 404 before the generic
|
|
290
290
|
// 500 coercion so a no-middleware fetchable loader's notFound() is honored
|
|
291
291
|
// (the with-middleware path resolves it through the notFoundBoundary).
|
|
292
|
-
if (error
|
|
292
|
+
if (isDataNotFoundError(error)) {
|
|
293
293
|
return finalizeResponse(
|
|
294
294
|
createResponseWithMergedHeaders(null, { status: 404 }),
|
|
295
295
|
);
|
package/src/rsc/manifest-init.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Router Manifest Initialization
|
|
3
3
|
*
|
|
4
|
-
* Builds a fresh route trie from
|
|
5
|
-
* where the manifest exists but the trie needs rebuilding.
|
|
4
|
+
* Builds a fresh route trie from every registered URL-pattern mount for
|
|
5
|
+
* dev/HMR scenarios where the manifest exists but the trie needs rebuilding.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {
|
|
@@ -13,11 +13,12 @@ import {
|
|
|
13
13
|
setRouterTrie,
|
|
14
14
|
setRouterPrecomputedEntries,
|
|
15
15
|
} from "../route-map-builder.js";
|
|
16
|
+
import { mergeFullManifests } from "../build/merge-full-manifests.js";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
|
-
* Build a fresh route trie from router
|
|
19
|
-
* per-router cache. Also sets the per-router manifest and merges into
|
|
20
|
-
*
|
|
19
|
+
* Build a fresh route trie from the router's URL-pattern mounts and store it in
|
|
20
|
+
* the per-router cache. Also sets the per-router manifest and merges into the
|
|
21
|
+
* global manifest for reverse()/href().
|
|
21
22
|
*
|
|
22
23
|
* Called when manifest data may exist but the per-router trie is missing,
|
|
23
24
|
* which happens in dev mode after HMR: the virtual module sets the manifest
|
|
@@ -30,10 +31,28 @@ export async function buildRouterTrieFromUrlpatterns(
|
|
|
30
31
|
): Promise<void> {
|
|
31
32
|
const { generateManifestFull } =
|
|
32
33
|
await import("../build/generate-manifest.js");
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
const mounts =
|
|
35
|
+
router.__urlpatternMounts ??
|
|
36
|
+
(router.urlpatterns
|
|
37
|
+
? [{ patterns: router.urlpatterns, mountIndex: undefined }]
|
|
38
|
+
: []);
|
|
39
|
+
// mount.mountIndex mirrors the live router's per-mount counter partitioning
|
|
40
|
+
// (context.ts getShortCode `M{i}` prefixes), so entries generated here carry
|
|
41
|
+
// the same shortCodes as runtime registration. Build discovery
|
|
42
|
+
// (discover-routers.ts / runtime-discovery.ts) numbers mounts GLOBALLY across
|
|
43
|
+
// routers instead — that divergence is invisible: shortCodes never leave
|
|
44
|
+
// their realm, and route names (the trie keys) carry no mount index (unnamed
|
|
45
|
+
// routes are `$path_*`, see path-helper.ts).
|
|
46
|
+
const mountManifests = [];
|
|
47
|
+
for (const mount of mounts) {
|
|
48
|
+
mountManifests.push(
|
|
49
|
+
await generateManifestFull(mount.patterns, mount.mountIndex, {
|
|
50
|
+
routerId: router.id,
|
|
51
|
+
...(router.basename ? { urlPrefix: router.basename } : {}),
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
const generated = mergeFullManifests(mountManifests);
|
|
37
56
|
// Build the trie through the SAME shared helper the production discovery uses
|
|
38
57
|
// (discover-routers.ts), so the dev runtime-rebuilt trie and the prod
|
|
39
58
|
// serialized trie cannot drift. buildPerRouterTrie returns null when there
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
SHELL_STATUS_HEADER,
|
|
58
58
|
resolvePprConfig,
|
|
59
59
|
buildShellKey,
|
|
60
|
+
shellSearchSeed,
|
|
60
61
|
isValidShellHit,
|
|
61
62
|
hasIntactShellPayload,
|
|
62
63
|
base64ToBytes,
|
|
@@ -153,12 +154,14 @@ function buildNavigationShellKey(
|
|
|
153
154
|
function createShellCaptureDescriptor(
|
|
154
155
|
ctx: HandlerContext<any>,
|
|
155
156
|
key: string,
|
|
157
|
+
searchSeed: string,
|
|
156
158
|
pprConfig: ResolvedPprConfig,
|
|
157
159
|
store: SegmentCacheStore<any>,
|
|
158
160
|
navigationOnly?: true,
|
|
159
161
|
): ShellCaptureDescriptor {
|
|
160
162
|
return {
|
|
161
163
|
key,
|
|
164
|
+
searchSeed,
|
|
162
165
|
buildVersion: ctx.version,
|
|
163
166
|
ttl: pprConfig.ttl,
|
|
164
167
|
swr: pprConfig.swr,
|
|
@@ -512,7 +515,13 @@ function* shellServePlan<TEnv>(
|
|
|
512
515
|
) {
|
|
513
516
|
return { kind: "pass" };
|
|
514
517
|
}
|
|
515
|
-
const descriptor = createShellCaptureDescriptor(
|
|
518
|
+
const descriptor = createShellCaptureDescriptor(
|
|
519
|
+
ctx,
|
|
520
|
+
key,
|
|
521
|
+
shellSearchSeed(url, reqCtx._searchParamsFilter),
|
|
522
|
+
pprConfig,
|
|
523
|
+
store,
|
|
524
|
+
);
|
|
516
525
|
|
|
517
526
|
if (
|
|
518
527
|
cached &&
|
|
@@ -699,6 +708,7 @@ function* preparePayloadPlan<TEnv>(
|
|
|
699
708
|
params: result.params,
|
|
700
709
|
isPartial: true,
|
|
701
710
|
slots: result.slots,
|
|
711
|
+
interceptTargets: result.interceptTargets,
|
|
702
712
|
handles: handleStore.stream(),
|
|
703
713
|
version: ctx.version,
|
|
704
714
|
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
@@ -993,6 +1003,7 @@ function scheduleNavigationShellCapture<TEnv>(
|
|
|
993
1003
|
ctx,
|
|
994
1004
|
partialCaptureKey ??
|
|
995
1005
|
buildNavigationShellKey(url, reqCtx._searchParamsFilter),
|
|
1006
|
+
shellSearchSeed(url, reqCtx._searchParamsFilter),
|
|
996
1007
|
pprConfig,
|
|
997
1008
|
store,
|
|
998
1009
|
true,
|
|
@@ -1510,6 +1521,9 @@ function serveShellHit(
|
|
|
1510
1521
|
ssrModule.resumeShellHTML!(rscStream, {
|
|
1511
1522
|
postponed: entry.postponed,
|
|
1512
1523
|
nonce: undefined,
|
|
1524
|
+
// The shell key's own search — identical to the capture seed for
|
|
1525
|
+
// this key, so the resume tree matches the captured tree.
|
|
1526
|
+
search: shellSearchSeed(url, reqCtx._searchParamsFilter),
|
|
1513
1527
|
}),
|
|
1514
1528
|
);
|
|
1515
1529
|
};
|
package/src/rsc/shell-capture.ts
CHANGED
|
@@ -907,6 +907,15 @@ export function gateFlightForCapture(
|
|
|
907
907
|
*/
|
|
908
908
|
export interface ShellCaptureDescriptor {
|
|
909
909
|
key: string;
|
|
910
|
+
/**
|
|
911
|
+
* The key's search portion (shellSearchSeed: `?`-prefixed sorted search,
|
|
912
|
+
* cache.searchParams filter applied, or ""). Seeds the capture render's SSR
|
|
913
|
+
* store so static-part `useSearchParams` reads bake markup consistent with
|
|
914
|
+
* the shell's own key; the resume pass derives the identical string from
|
|
915
|
+
* the HIT request. Computed next to the key so the two cannot drift.
|
|
916
|
+
* Omitted (build-time bare-pathname captures) = search-less, same as "".
|
|
917
|
+
*/
|
|
918
|
+
searchSeed?: string;
|
|
910
919
|
/**
|
|
911
920
|
* The RSC handler's build version (HandlerContext.version), stamped into the
|
|
912
921
|
* stored entry as ShellCacheEntry.buildVersion — the serve-side
|
|
@@ -1753,6 +1762,9 @@ async function captureAndStoreShell(
|
|
|
1753
1762
|
captureShellHTML(gate.stream, {
|
|
1754
1763
|
quiesce,
|
|
1755
1764
|
maxWaitMs: capture.captureTimeout ?? SHELL_CAPTURE_MAX_WAIT_MS,
|
|
1765
|
+
// The shell key's own search seeds the capture render's store —
|
|
1766
|
+
// static-part search reads bake what the key names.
|
|
1767
|
+
search: capture.searchSeed,
|
|
1756
1768
|
}),
|
|
1757
1769
|
);
|
|
1758
1770
|
} catch (error) {
|
package/src/rsc/shell-serve.ts
CHANGED
|
@@ -129,9 +129,22 @@ export function resolvePprConfig(
|
|
|
129
129
|
* and lookup makes every shell request a permanent miss.
|
|
130
130
|
*/
|
|
131
131
|
export function buildShellKey(url: URL, filter?: SearchParamsFilter): string {
|
|
132
|
+
return `${url.host}${url.pathname}${shellSearchSeed(url, filter)}:shell`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The shell key's search portion (`?`-prefixed sorted search with the
|
|
137
|
+
* cache.searchParams filter applied, or "") — ALSO the string the capture and
|
|
138
|
+
* resume SSR renders seed their store location with (SSRRenderOptions.search
|
|
139
|
+
* / ShellCaptureOptions.search / ShellResumeOptions.search). Search is part
|
|
140
|
+
* of shell identity, so static-part `useSearchParams` reads render exactly
|
|
141
|
+
* what the key names; deriving seed and key from this one helper is what
|
|
142
|
+
* keeps capture, resume, and lookup byte-agreed (drift = replay mismatch or
|
|
143
|
+
* permanent MISS).
|
|
144
|
+
*/
|
|
145
|
+
export function shellSearchSeed(url: URL, filter?: SearchParamsFilter): string {
|
|
132
146
|
const sorted = sortedSearchString(url.searchParams, filter);
|
|
133
|
-
|
|
134
|
-
return `${url.host}${url.pathname}${searchSuffix}:shell`;
|
|
147
|
+
return sorted ? `?${sorted}` : "";
|
|
135
148
|
}
|
|
136
149
|
|
|
137
150
|
/**
|
package/src/rsc/ssr-setup.ts
CHANGED
|
@@ -111,7 +111,16 @@ export function createSsrHtmlStage<TEnv>(
|
|
|
111
111
|
);
|
|
112
112
|
return {
|
|
113
113
|
render: () =>
|
|
114
|
-
|
|
114
|
+
// search: the LIVE request's query string, threaded out-of-band into
|
|
115
|
+
// the SSR pass so useSearchParams sees real values during document
|
|
116
|
+
// renders. Deliberately not payload metadata: cached/prerendered
|
|
117
|
+
// payloads replay captured metadata, and search is not route
|
|
118
|
+
// identity — it must always come from the request being served.
|
|
119
|
+
ssrModule.renderHTML(flight.stream, {
|
|
120
|
+
...options.render,
|
|
121
|
+
streamMode,
|
|
122
|
+
search: options.url.search,
|
|
123
|
+
}),
|
|
115
124
|
...(options.init && { init: options.init }),
|
|
116
125
|
};
|
|
117
126
|
};
|
package/src/rsc/types.ts
CHANGED
|
@@ -35,10 +35,27 @@ export interface RscPayload {
|
|
|
35
35
|
/** Merged route params from the matched route */
|
|
36
36
|
params?: Record<string, string>;
|
|
37
37
|
slots?: Record<string, SlotState>;
|
|
38
|
+
/**
|
|
39
|
+
* Intercept TARGET route names reachable from this location as a
|
|
40
|
+
* navigation origin (see MatchResult.interceptTargets). The browser-local
|
|
41
|
+
* clientUrls matcher declines optimistic presentation for these.
|
|
42
|
+
*/
|
|
43
|
+
interceptTargets?: string[];
|
|
38
44
|
/** Root layout component for browser-side re-renders (client component reference) */
|
|
39
45
|
rootLayout?: React.ComponentType<{ children: React.ReactNode }>;
|
|
40
46
|
/** Handle data accumulated across route segments (async generator that yields on each push) */
|
|
41
47
|
handles?: AsyncGenerator<HandleData, void, unknown>;
|
|
48
|
+
/**
|
|
49
|
+
* Document-lane late handle channel: full-state updates for pushes landing
|
|
50
|
+
* AFTER the handler barrier (streaming loader bodies writing handles
|
|
51
|
+
* mid-body). `handles` above is drained to completion in blocking
|
|
52
|
+
* positions (SSR seed, pre-hydration), so late pushes need this separate
|
|
53
|
+
* generator, consumed non-blocking post-hydration (rsc-router.tsx).
|
|
54
|
+
* Empty/instantly-complete when no auxiliary-lane work is pending at the
|
|
55
|
+
* handler barrier. Full payloads only — partial payloads' `handles`
|
|
56
|
+
* generator streams to full settle already.
|
|
57
|
+
*/
|
|
58
|
+
handlesLate?: AsyncGenerator<HandleData, void, unknown>;
|
|
42
59
|
/** RSC version string for cache invalidation */
|
|
43
60
|
version?: string;
|
|
44
61
|
/** Cloudflare dev worker generation used for stale-document convergence. */
|
|
@@ -158,6 +175,18 @@ export interface SSRRenderOptions {
|
|
|
158
175
|
* - `"allReady"` — await `stream.allReady` before returning.
|
|
159
176
|
*/
|
|
160
177
|
streamMode?: import("../router/router-options.js").SSRStreamMode;
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The live request's query string (`url.search`, `?`-prefixed or empty).
|
|
181
|
+
* Seeds the SSR navigation store so `useSearchParams` (and
|
|
182
|
+
* `useNavigation().location`) carry real values during document renders.
|
|
183
|
+
* Out-of-band by design — never payload metadata, which cached/prerendered
|
|
184
|
+
* payloads replay; search is not route identity. The build-time prerender
|
|
185
|
+
* pass passes none — build shells capture bare pathnames and serve
|
|
186
|
+
* search-less requests only (runtime ppr captures seed the shell key's
|
|
187
|
+
* search; see ShellCaptureOptions.search in the SSR entry).
|
|
188
|
+
*/
|
|
189
|
+
search?: string;
|
|
161
190
|
}
|
|
162
191
|
|
|
163
192
|
/**
|
|
@@ -179,7 +208,7 @@ export interface SSRModule {
|
|
|
179
208
|
*/
|
|
180
209
|
captureShellHTML?: (
|
|
181
210
|
rscStream: ReadableStream<Uint8Array>,
|
|
182
|
-
options: { quiesce: Promise<void>; maxWaitMs?: number },
|
|
211
|
+
options: { quiesce: Promise<void>; maxWaitMs?: number; search?: string },
|
|
183
212
|
) => Promise<{ prelude: Uint8Array; postponed: string | null } | null>;
|
|
184
213
|
|
|
185
214
|
/**
|
|
@@ -192,7 +221,7 @@ export interface SSRModule {
|
|
|
192
221
|
*/
|
|
193
222
|
resumeShellHTML?: (
|
|
194
223
|
rscStream: ReadableStream<Uint8Array>,
|
|
195
|
-
options: { postponed: string | null; nonce?: string },
|
|
224
|
+
options: { postponed: string | null; nonce?: string; search?: string },
|
|
196
225
|
) => Promise<ReadableStream<Uint8Array>>;
|
|
197
226
|
}
|
|
198
227
|
|
package/src/segment-system.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import { invariant } from "./errors.js";
|
|
|
8
8
|
import {
|
|
9
9
|
RouteContentWrapper,
|
|
10
10
|
LoaderBoundary,
|
|
11
|
+
StreamedLoaderErrorBoundary,
|
|
11
12
|
} from "./route-content-wrapper.js";
|
|
12
13
|
import { RootErrorBoundary } from "./root-error-boundary.js";
|
|
13
14
|
import { INTERNAL_RANGO_DEBUG } from "./internal-debug.js";
|
|
@@ -113,6 +114,9 @@ export interface RenderSegmentsOptions {
|
|
|
113
114
|
*/
|
|
114
115
|
forceAwait?: boolean;
|
|
115
116
|
|
|
117
|
+
/** Seeded descendant client-route pending state for testing. */
|
|
118
|
+
outletPending?: boolean;
|
|
119
|
+
|
|
116
120
|
/**
|
|
117
121
|
* Intercept segments to inject into the tree.
|
|
118
122
|
* These are parallel segments from intercept routes that need to be
|
|
@@ -231,6 +235,7 @@ export async function renderSegments(
|
|
|
231
235
|
isAction,
|
|
232
236
|
interceptSegments,
|
|
233
237
|
forceAwait,
|
|
238
|
+
outletPending = false,
|
|
234
239
|
rootLayout: RootLayout,
|
|
235
240
|
} = options || {};
|
|
236
241
|
|
|
@@ -448,9 +453,30 @@ export async function renderSegments(
|
|
|
448
453
|
// Prepare loader data if there are loaders
|
|
449
454
|
const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
|
|
450
455
|
|
|
456
|
+
// Loader-bearing segments get the router-owned error boundary around
|
|
457
|
+
// their children so a read-site loader error renders its errorBoundary()
|
|
458
|
+
// fallback (LOADER_ERROR_FALLBACK marker on the thrown error) instead of
|
|
459
|
+
// escaping to app boundaries. Wrapped UNCONDITIONALLY on loader presence
|
|
460
|
+
// — streams and forceAwait lanes must produce the same tree shape or
|
|
461
|
+
// lane changes remount the subtree (docs/tree-structure.md).
|
|
462
|
+
if (loaderEntries.length > 0) {
|
|
463
|
+
nodeContent = createElement(StreamedLoaderErrorBoundary, {
|
|
464
|
+
children: nodeContent,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
451
468
|
if (loading !== undefined && loading !== null) {
|
|
452
469
|
const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
|
|
453
470
|
let boundaryLoaderData: Promise<any[]> | any[] = loaderDataPromise;
|
|
471
|
+
// SPIKE (streaming useLoader): per-loader streams for the boundary's
|
|
472
|
+
// readers. MUST come from the individual loader refs — splitting the
|
|
473
|
+
// aggregate client-side is wrong (Promise.all resolves at the slowest
|
|
474
|
+
// loader, erasing per-loader timing; measured 400ms data held to a
|
|
475
|
+
// 2000ms sibling). Zero-loader boundaries pass NO streams so the
|
|
476
|
+
// resolver keeps the fresh-promise resolve-above suspension — that
|
|
477
|
+
// microtask suspension is what makes SSR emit the loading() fallback
|
|
478
|
+
// for content-suspending routes (segment-loader-promise.ts).
|
|
479
|
+
let boundaryLoaderStreams: Record<string, unknown> | undefined;
|
|
454
480
|
if (forceAwait || isAction) {
|
|
455
481
|
const awaitStart = segDebug ? performance.now() : 0;
|
|
456
482
|
boundaryLoaderData = await loaderDataPromise;
|
|
@@ -460,16 +486,23 @@ export async function renderSegments(
|
|
|
460
486
|
ms: Math.round(performance.now() - awaitStart),
|
|
461
487
|
});
|
|
462
488
|
}
|
|
463
|
-
} else if (
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
489
|
+
} else if (loaderEntries.length > 0) {
|
|
490
|
+
boundaryLoaderStreams = {};
|
|
491
|
+
for (const l of loaderEntries) {
|
|
492
|
+
boundaryLoaderStreams[l.loaderId!] = l.loaderData;
|
|
493
|
+
}
|
|
494
|
+
if (segDebug) {
|
|
495
|
+
segDebugLog(
|
|
496
|
+
`segment ${id}: per-loader streams via LoaderBoundary (read-site suspense)`,
|
|
497
|
+
{ loaderIds },
|
|
498
|
+
);
|
|
499
|
+
}
|
|
468
500
|
}
|
|
469
501
|
content = createElement(LoaderBoundary, {
|
|
470
502
|
key: `loader-boundary-${key}`,
|
|
471
503
|
loaderDataPromise: boundaryLoaderData,
|
|
472
504
|
loaderIds,
|
|
505
|
+
loaderStreams: boundaryLoaderStreams,
|
|
473
506
|
fallback: loading,
|
|
474
507
|
outletKey: key,
|
|
475
508
|
outletContent,
|
|
@@ -483,6 +516,7 @@ export async function renderSegments(
|
|
|
483
516
|
content: outletContent,
|
|
484
517
|
segment: node.segment,
|
|
485
518
|
parallel: node.parallel,
|
|
519
|
+
pending: outletPending,
|
|
486
520
|
children: nodeContent,
|
|
487
521
|
});
|
|
488
522
|
} else {
|
|
@@ -492,29 +526,50 @@ export async function renderSegments(
|
|
|
492
526
|
);
|
|
493
527
|
|
|
494
528
|
const layoutLoaderIds = layoutLoaders.map((l) => l.loaderId!);
|
|
495
|
-
//
|
|
496
|
-
//
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
resolvedData
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
if (decodeMs > 0) {
|
|
514
|
-
segDebugLog(`segment ${id}: loader results decoded`, {
|
|
515
|
-
ms: decodeMs,
|
|
529
|
+
// SPIKE (streaming useLoader): a segment without loading() used to BLOCK
|
|
530
|
+
// the tree build here (await buildLoaderPromise) so its data was always
|
|
531
|
+
// resolved before render. Streaming lanes now skip the await and pass
|
|
532
|
+
// per-loader UNDECODED results (possibly pending promises) through
|
|
533
|
+
// OutletProvider.loaderStreams — useLoader suspends at the read site to
|
|
534
|
+
// the nearest CONSUMER Suspense boundary. forceAwait (popstate, stale
|
|
535
|
+
// revalidation, fully-prefetched) and action lanes keep the blocking
|
|
536
|
+
// await so those commits stay whole with no fallback flash.
|
|
537
|
+
let loaderData: Record<string, any> = {};
|
|
538
|
+
let errorFallback: ReactNode = null;
|
|
539
|
+
let loaderStreams: Record<string, unknown> | undefined;
|
|
540
|
+
if (forceAwait || isAction) {
|
|
541
|
+
const layoutAwaitStart = segDebug ? performance.now() : 0;
|
|
542
|
+
const resolvedData = await buildLoaderPromise(layoutLoaders);
|
|
543
|
+
if (segDebug) {
|
|
544
|
+
segDebugLog(`segment ${id}: layout loaders awaited (blocking)`, {
|
|
545
|
+
loaderIds: layoutLoaderIds,
|
|
546
|
+
ms: Math.round(performance.now() - layoutAwaitStart),
|
|
516
547
|
});
|
|
517
548
|
}
|
|
549
|
+
const decodeStart = segDebug ? performance.now() : 0;
|
|
550
|
+
({ loaderData, errorFallback } = decodeLoaderResults(
|
|
551
|
+
resolvedData,
|
|
552
|
+
layoutLoaderIds,
|
|
553
|
+
));
|
|
554
|
+
if (segDebug) {
|
|
555
|
+
const decodeMs = Math.round(performance.now() - decodeStart);
|
|
556
|
+
if (decodeMs > 0) {
|
|
557
|
+
segDebugLog(`segment ${id}: loader results decoded`, {
|
|
558
|
+
ms: decodeMs,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
} else if (layoutLoaders.length > 0) {
|
|
563
|
+
loaderStreams = {};
|
|
564
|
+
for (const l of layoutLoaders) {
|
|
565
|
+
loaderStreams[l.loaderId!] = l.loaderData;
|
|
566
|
+
}
|
|
567
|
+
if (segDebug) {
|
|
568
|
+
segDebugLog(
|
|
569
|
+
`segment ${id}: layout loaders streaming to read sites (no loading())`,
|
|
570
|
+
{ loaderIds: layoutLoaderIds },
|
|
571
|
+
);
|
|
572
|
+
}
|
|
518
573
|
}
|
|
519
574
|
|
|
520
575
|
if (parallelOwnedLoaders.length > 0) {
|
|
@@ -574,6 +629,8 @@ export async function renderSegments(
|
|
|
574
629
|
segment: node.segment,
|
|
575
630
|
parallel: node.parallel,
|
|
576
631
|
loaderData: Object.keys(loaderData).length > 0 ? loaderData : undefined,
|
|
632
|
+
loaderStreams,
|
|
633
|
+
pending: outletPending,
|
|
577
634
|
children: errorFallback ?? nodeContent,
|
|
578
635
|
});
|
|
579
636
|
}
|
package/src/server/context.ts
CHANGED
|
@@ -106,6 +106,16 @@ export type LoaderEntry = {
|
|
|
106
106
|
revalidate: ShouldRevalidateFn<any, any>[];
|
|
107
107
|
/** Cache config for this specific loader (loaders are NOT cached by default) */
|
|
108
108
|
cache?: EntryCacheConfig;
|
|
109
|
+
/**
|
|
110
|
+
* Document renders await this loader before segment resolution returns
|
|
111
|
+
* (loader(Def, { stream: "navigation" })), so its data, handle pushes, and
|
|
112
|
+
* thrown notFound()/redirect() deterministically precede first flush.
|
|
113
|
+
* Resolved at DSL-evaluation time from ctx.isSSR — entries are cached
|
|
114
|
+
* per-isSSR (router/manifest.ts cache key), so the flag is already
|
|
115
|
+
* request-mode-correct when resolveLoaders (fresh.ts) reads it and never
|
|
116
|
+
* appears on navigation-lane entries.
|
|
117
|
+
*/
|
|
118
|
+
awaitBeforeFlush?: true;
|
|
109
119
|
};
|
|
110
120
|
|
|
111
121
|
/**
|