@rangojs/router 0.0.0-experimental.97 → 0.0.0-experimental.98914650
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 +24 -9
- package/dist/bin/rango.js +157 -63
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +1584 -639
- package/package.json +71 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +60 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +222 -30
- package/skills/caching/SKILL.md +263 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +235 -28
- package/skills/host-router/SKILL.md +122 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +29 -5
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +173 -17
- package/skills/loader/SKILL.md +170 -23
- package/skills/middleware/SKILL.md +16 -10
- package/skills/migrate-nextjs/SKILL.md +38 -16
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +11 -7
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +250 -25
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +114 -47
- package/skills/route/SKILL.md +42 -5
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +78 -42
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +121 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +316 -26
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/vercel/SKILL.md +107 -0
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +14 -27
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +37 -143
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +30 -59
- package/src/browser/navigation-client.ts +96 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +32 -82
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/partial-update.ts +60 -127
- package/src/browser/prefetch/cache.ts +82 -72
- package/src/browser/prefetch/fetch.ts +108 -33
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +41 -48
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +17 -14
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- package/src/browser/react/use-params.ts +11 -11
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +20 -5
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/response-adapter.ts +52 -1
- package/src/browser/rsc-router.tsx +70 -34
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +168 -44
- package/src/browser/types.ts +36 -21
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +3 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +89 -10
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +122 -22
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +134 -32
- package/src/cache/cache-scope.ts +100 -74
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2255 -238
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +61 -20
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +22 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +25 -61
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +17 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +31 -23
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +63 -9
- package/src/index.ts +64 -9
- package/src/internal-debug.ts +2 -4
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +61 -6
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +65 -40
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +244 -281
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +40 -17
- package/src/route-definition/redirect.ts +43 -9
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -15
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +4 -41
- package/src/router/intercept-resolution.ts +14 -19
- package/src/router/lazy-includes.ts +9 -46
- package/src/router/loader-resolution.ts +91 -46
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +57 -58
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +150 -271
- package/src/router/match-middleware/cache-store.ts +3 -33
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +31 -80
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +5 -112
- package/src/router/middleware.ts +118 -133
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +62 -67
- package/src/router/prerender-match.ts +99 -63
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +50 -56
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +68 -35
- package/src/router/router-options.ts +55 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +44 -63
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +40 -37
- package/src/router/segment-resolution/revalidation.ts +203 -285
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +0 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +87 -48
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +80 -41
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +83 -78
- package/src/rsc/helpers.ts +93 -5
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +12 -1
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +76 -62
- package/src/rsc/rsc-rendering.ts +41 -60
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +62 -67
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +10 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +199 -142
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +150 -51
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +165 -87
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +618 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +97 -22
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +6 -3
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +18 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +19 -5
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +95 -82
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +26 -34
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/state.ts +39 -1
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +185 -10
- package/src/vite/plugins/cjs-to-esm.ts +3 -18
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +12 -11
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +4 -75
- package/src/vite/plugins/expose-id-utils.ts +3 -54
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +57 -67
- package/src/vite/plugins/performance-tracks.ts +9 -16
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +26 -49
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +2 -32
- package/src/vite/plugins/version-plugin.ts +32 -23
- package/src/vite/plugins/virtual-entries.ts +35 -17
- package/src/vite/rango.ts +148 -115
- package/src/vite/router-discovery.ts +220 -68
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -34
- package/src/vite/utils/shared-utils.ts +95 -43
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
package/src/segment-system.tsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { createElement, type ReactNode, type ComponentType } from "react";
|
|
3
|
-
import { OutletProvider } from "./
|
|
3
|
+
import { OutletProvider } from "./outlet-provider.js";
|
|
4
4
|
import { MountContextProvider } from "./browser/react/mount-context.js";
|
|
5
5
|
import type { ResolvedSegment, RootLayoutProps } from "./types.js";
|
|
6
|
-
import {
|
|
6
|
+
import { decodeLoaderResults } from "./decode-loader-results.js";
|
|
7
7
|
import { invariant } from "./errors.js";
|
|
8
8
|
import {
|
|
9
9
|
RouteContentWrapper,
|
|
@@ -11,13 +11,25 @@ import {
|
|
|
11
11
|
} from "./route-content-wrapper.js";
|
|
12
12
|
import { RootErrorBoundary } from "./root-error-boundary.js";
|
|
13
13
|
import { getMemoizedContentPromise } from "./segment-content-promise.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
buildLoaderPromise,
|
|
16
|
+
getMemoizedLoaderPromise,
|
|
17
|
+
} from "./segment-loader-promise.js";
|
|
15
18
|
|
|
16
19
|
// ViewTransition is only available in React experimental.
|
|
17
20
|
// Access via namespace import to avoid compile-time errors on stable React.
|
|
18
21
|
const ReactViewTransition: any =
|
|
19
22
|
"ViewTransition" in React ? (React as any).ViewTransition : null;
|
|
20
23
|
|
|
24
|
+
// A loading skeleton is renderable only when it is a real ReactNode value.
|
|
25
|
+
// `false` is treated as "not renderable" here. This is the three-term gate;
|
|
26
|
+
// the distinct two-term gate at the LoaderBoundary site deliberately treats
|
|
27
|
+
// `false` as "create a boundary without a RouteContentWrapper"
|
|
28
|
+
// (tree-structure.md), so it must NOT use this helper.
|
|
29
|
+
function isRenderableLoading(loading: ReactNode): boolean {
|
|
30
|
+
return loading !== undefined && loading !== null && loading !== false;
|
|
31
|
+
}
|
|
32
|
+
|
|
21
33
|
function restoreParallelLoaderMarkers(
|
|
22
34
|
segments: ResolvedSegment[],
|
|
23
35
|
): ResolvedSegment[] {
|
|
@@ -28,12 +40,7 @@ function restoreParallelLoaderMarkers(
|
|
|
28
40
|
const segment = segments[i];
|
|
29
41
|
|
|
30
42
|
if (segment.type === "parallel") {
|
|
31
|
-
if (
|
|
32
|
-
segment.namespace &&
|
|
33
|
-
segment.loading !== undefined &&
|
|
34
|
-
segment.loading !== null &&
|
|
35
|
-
segment.loading !== false
|
|
36
|
-
) {
|
|
43
|
+
if (segment.namespace && isRenderableLoading(segment.loading)) {
|
|
37
44
|
parallelLoadingByNamespace.set(segment.namespace, segment.loading);
|
|
38
45
|
}
|
|
39
46
|
continue;
|
|
@@ -59,42 +66,6 @@ function restoreParallelLoaderMarkers(
|
|
|
59
66
|
return nextSegments ?? segments;
|
|
60
67
|
}
|
|
61
68
|
|
|
62
|
-
/**
|
|
63
|
-
* Resolve loader data from raw results, unwrapping LoaderDataResult wrappers
|
|
64
|
-
*/
|
|
65
|
-
function resolveLoaderData(
|
|
66
|
-
resolvedData: any[],
|
|
67
|
-
loaderIds: string[],
|
|
68
|
-
): { loaderData: Record<string, any>; errorFallback: ReactNode } {
|
|
69
|
-
const loaderData: Record<string, any> = {};
|
|
70
|
-
let errorFallback: ReactNode = null;
|
|
71
|
-
|
|
72
|
-
for (let i = 0; i < loaderIds.length; i++) {
|
|
73
|
-
const id = loaderIds[i];
|
|
74
|
-
const result = resolvedData[i];
|
|
75
|
-
|
|
76
|
-
if (!isLoaderDataResult(result)) {
|
|
77
|
-
// Legacy format - direct data
|
|
78
|
-
loaderData[id] = result;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (result.ok) {
|
|
83
|
-
loaderData[id] = result.data;
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Error case
|
|
88
|
-
if (result.fallback) {
|
|
89
|
-
errorFallback = result.fallback;
|
|
90
|
-
} else {
|
|
91
|
-
throw new Error(result.error.message);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return { loaderData, errorFallback };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
69
|
/**
|
|
99
70
|
* Options for renderSegments
|
|
100
71
|
*/
|
|
@@ -131,11 +102,61 @@ export interface RenderSegmentsOptions {
|
|
|
131
102
|
rootLayout?: ComponentType<RootLayoutProps>;
|
|
132
103
|
}
|
|
133
104
|
|
|
105
|
+
function createViewTransitionBoundary(
|
|
106
|
+
transition: NonNullable<ResolvedSegment["transition"]>,
|
|
107
|
+
children: ReactNode,
|
|
108
|
+
): ReactNode {
|
|
109
|
+
// `viewTransition` is a router-specific flag (boundary opt-out), not a React
|
|
110
|
+
// <ViewTransition> prop — strip it so it never reaches React.
|
|
111
|
+
const { viewTransition: _viewTransition, ...vtProps } = transition;
|
|
112
|
+
return createElement(ReactViewTransition, {
|
|
113
|
+
...vtProps,
|
|
114
|
+
children,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function wrapDefaultOutletContent(
|
|
119
|
+
content: ReactNode,
|
|
120
|
+
transition: NonNullable<ResolvedSegment["transition"]>,
|
|
121
|
+
): ReactNode {
|
|
122
|
+
if (!React.isValidElement(content)) {
|
|
123
|
+
return createViewTransitionBoundary(transition, content);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const props = content.props as any;
|
|
127
|
+
|
|
128
|
+
if (content.type === MountContextProvider) {
|
|
129
|
+
return React.cloneElement(content, {
|
|
130
|
+
children: wrapDefaultOutletContent(props.children, transition),
|
|
131
|
+
} as any);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (content.type === OutletProvider && props.segment?.type === "layout") {
|
|
135
|
+
return React.cloneElement(content, {
|
|
136
|
+
content: wrapDefaultOutletContent(props.content, transition),
|
|
137
|
+
} as any);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (content.type === LoaderBoundary && props.segment?.type === "layout") {
|
|
141
|
+
return React.cloneElement(content, {
|
|
142
|
+
outletContent: wrapDefaultOutletContent(props.outletContent, transition),
|
|
143
|
+
} as any);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return createViewTransitionBoundary(transition, content);
|
|
147
|
+
}
|
|
148
|
+
|
|
134
149
|
/**
|
|
135
150
|
* Render segments into a React tree with proper layout nesting
|
|
136
151
|
*
|
|
137
|
-
* Layouts nest using OutletProvider
|
|
138
|
-
* render as
|
|
152
|
+
* Layouts nest using OutletProvider; a layout receives the inner content via
|
|
153
|
+
* its `<Outlet />`. Parallel segments do NOT render as inline Fragment siblings
|
|
154
|
+
* — they flow through OutletContext.parallel and are resolved where a layout
|
|
155
|
+
* places `<ParallelOutlet name="@sidebar" />` (or `<Outlet name="@sidebar" />`).
|
|
156
|
+
*
|
|
157
|
+
* The result is always wrapped in RootErrorBoundary so unhandled errors never
|
|
158
|
+
* blank the screen. When `options.rootLayout` is provided it wraps the error
|
|
159
|
+
* boundary at the OUTERMOST level (so the app shell survives errors).
|
|
139
160
|
*
|
|
140
161
|
* Error segments are treated like route segments - they render their fallback
|
|
141
162
|
* component in place of the failed segment. When an error occurs in a handler,
|
|
@@ -147,27 +168,30 @@ export interface RenderSegmentsOptions {
|
|
|
147
168
|
* notFoundBoundary's fallback component.
|
|
148
169
|
*
|
|
149
170
|
* @param segments - Array of resolved segments to render
|
|
150
|
-
* @returns ReactNode
|
|
171
|
+
* @returns Promise resolving to the ReactNode tree (the function is async)
|
|
151
172
|
*
|
|
152
173
|
* @example
|
|
153
174
|
* ```typescript
|
|
154
175
|
* const segments = [
|
|
155
|
-
* { id: 'L0.0', type: 'layout', component: <
|
|
156
|
-
* { id: '
|
|
157
|
-
* { id: '
|
|
158
|
-
* { id: 'P3.0', type: 'parallel', component: <Sidebar />, slot: '@sidebar' }
|
|
176
|
+
* { id: 'L0.0', type: 'layout', component: <BlogLayout /> },
|
|
177
|
+
* { id: 'L0R1', type: 'route', component: <BlogPost /> },
|
|
178
|
+
* { id: 'L0R1.@sidebar', type: 'parallel', component: <Sidebar />, slot: '@sidebar' }
|
|
159
179
|
* ];
|
|
160
180
|
*
|
|
161
|
-
*
|
|
162
|
-
* //
|
|
163
|
-
*
|
|
164
|
-
* //
|
|
165
|
-
* //
|
|
166
|
-
* //
|
|
167
|
-
* //
|
|
181
|
+
* // BlogLayout renders <Outlet /> for the route and
|
|
182
|
+
* // <ParallelOutlet name="@sidebar" /> for the parallel slot.
|
|
183
|
+
* const tree = await renderSegments(segments, { rootLayout: RootLayout });
|
|
184
|
+
* // Results in (outermost first):
|
|
185
|
+
* // <RootLayout>
|
|
186
|
+
* // <RootErrorBoundary>
|
|
187
|
+
* // <OutletProvider segment={BlogLayout} parallel={[Sidebar]}>
|
|
188
|
+
* // <BlogPost />
|
|
189
|
+
* // </OutletProvider>
|
|
190
|
+
* // </RootErrorBoundary>
|
|
191
|
+
* // </RootLayout>
|
|
168
192
|
*
|
|
169
193
|
* // For server actions, pass isAction to await components:
|
|
170
|
-
* const tree = renderSegments(segments, { isAction: true });
|
|
194
|
+
* const tree = await renderSegments(segments, { isAction: true });
|
|
171
195
|
* ```
|
|
172
196
|
*/
|
|
173
197
|
export async function renderSegments(
|
|
@@ -211,6 +235,25 @@ export async function renderSegments(
|
|
|
211
235
|
}
|
|
212
236
|
// Separate segments by type, passing intercept segments for explicit injection
|
|
213
237
|
const tree = segmentTreeWalk(normalizedSegments, normalizedInterceptSegments);
|
|
238
|
+
|
|
239
|
+
// A route is "in a transition scope" when its own segment OR any layout in
|
|
240
|
+
// its matched chain declares transition(). Both transition() forms land here:
|
|
241
|
+
// the per-route item form sets transition on the route entry, and the block
|
|
242
|
+
// wrapper form sets it on a transparent ancestor layout (dsl-helpers.ts). When
|
|
243
|
+
// in scope, the route and its route-owned layouts use param-agnostic keys so a
|
|
244
|
+
// same-route navigation reconciles (holds content) instead of remounting. The
|
|
245
|
+
// value is a static property of the route's position in the tree, so it is the
|
|
246
|
+
// same on every render of that route (SSR, navigation, action) — the keys
|
|
247
|
+
// never drift. Cross-route navigation still remounts: different routes have
|
|
248
|
+
// different segment ids regardless of transition scope.
|
|
249
|
+
const inTransitionScope = normalizedSegments.some(
|
|
250
|
+
(s) =>
|
|
251
|
+
s.transition != null &&
|
|
252
|
+
(s.type === "layout" ||
|
|
253
|
+
s.type === "route" ||
|
|
254
|
+
s.type === "error" ||
|
|
255
|
+
s.type === "notFound"),
|
|
256
|
+
);
|
|
214
257
|
// Render content segments as siblings
|
|
215
258
|
let content: ReactNode = null;
|
|
216
259
|
for (const node of tree) {
|
|
@@ -223,17 +266,31 @@ export async function renderSegments(
|
|
|
223
266
|
);
|
|
224
267
|
const { component, id, params, loading } = node.segment;
|
|
225
268
|
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
269
|
+
// Param-agnostic keys are opt-in via the transition() DSL (see
|
|
270
|
+
// inTransitionScope above). A route (and its route-owned layouts) inside a
|
|
271
|
+
// transition scope drops the param from its key, so navigating between two
|
|
272
|
+
// param values of the SAME route (e.g. /product/1 -> /product/2) reconciles
|
|
273
|
+
// the route subtree instead of remounting it. Combined with the
|
|
274
|
+
// startTransition wrap that shouldStartViewTransition already applies to
|
|
275
|
+
// transition routes (browser/partial-update.ts), the previous content stays
|
|
276
|
+
// on screen while the new loaders resolve (stale-while-revalidate) instead
|
|
277
|
+
// of flashing the loading skeleton. This works on stable React; experimental
|
|
278
|
+
// React adds the animated <ViewTransition> cross-fade on top.
|
|
279
|
+
//
|
|
280
|
+
// Outside a transition scope the key stays param-bearing and the route
|
|
281
|
+
// remounts on param change (the default: a fresh skeleton and fresh
|
|
282
|
+
// component state).
|
|
283
|
+
//
|
|
284
|
+
// error/notFound always keep param-bearing keys: createErrorSegment reuses
|
|
285
|
+
// the boundary layout's shortCode as the error segment id (router/
|
|
286
|
+
// error-handling.ts), so a param-agnostic error key could collide with that
|
|
287
|
+
// layout's key within the same render.
|
|
232
288
|
const includeParams =
|
|
233
|
-
node.segment.type === "route" ||
|
|
234
289
|
node.segment.type === "error" ||
|
|
235
290
|
node.segment.type === "notFound" ||
|
|
236
|
-
(node.segment.type === "
|
|
291
|
+
((node.segment.type === "route" ||
|
|
292
|
+
(node.segment.type === "layout" && node.segment.belongsToRoute)) &&
|
|
293
|
+
!inTransitionScope);
|
|
237
294
|
|
|
238
295
|
const paramStr =
|
|
239
296
|
includeParams && params && Object.keys(params).length > 0
|
|
@@ -242,57 +299,70 @@ export async function renderSegments(
|
|
|
242
299
|
.map(([k, v]) => `${k}=${v}`)
|
|
243
300
|
.join(",")
|
|
244
301
|
: "";
|
|
245
|
-
const key =
|
|
302
|
+
const key = paramStr ? `${id}-${paramStr}` : id;
|
|
246
303
|
|
|
247
|
-
// Get loader entries for this node
|
|
248
304
|
const loaderEntries = node.loaders.filter(
|
|
249
305
|
(loader) => loader.loaderId && loader.loaderData !== undefined,
|
|
250
306
|
);
|
|
251
307
|
|
|
252
|
-
// Determine the component content (with or without Suspense wrapper)
|
|
253
|
-
// Wrap when loading skeleton defined OR component is Promise (needs Suspense)
|
|
254
|
-
// During actions, await component Promise to prevent Suspense from triggering
|
|
255
|
-
// This keeps existing content visible instead of showing loading skeleton
|
|
256
308
|
let resolvedComponent = component;
|
|
257
309
|
if (isAction && component instanceof Promise) {
|
|
258
310
|
resolvedComponent = await component;
|
|
259
311
|
}
|
|
260
312
|
|
|
261
|
-
let nodeContent: ReactNode =
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
: registerLazyRef(resolvedComponent);
|
|
313
|
+
let nodeContent: ReactNode = isRenderableLoading(loading)
|
|
314
|
+
? createElement(RouteContentWrapper, {
|
|
315
|
+
key: `suspense-loading-${id}`,
|
|
316
|
+
content: getMemoizedContentPromise(resolvedComponent),
|
|
317
|
+
fallback: loading,
|
|
318
|
+
segmentId: id,
|
|
319
|
+
})
|
|
320
|
+
: registerLazyRef(resolvedComponent);
|
|
270
321
|
|
|
271
322
|
// Wrap with <ViewTransition> if transition config exists (React experimental only).
|
|
272
323
|
// An empty config ({}) creates a bare <ViewTransition> boundary that participates
|
|
273
324
|
// in transitions without adding custom animation classes. Named element-level
|
|
274
325
|
// <ViewTransition> components inside (with name/share props) morph independently
|
|
275
326
|
// from the parent's default cross-fade.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
//
|
|
284
|
-
|
|
327
|
+
//
|
|
328
|
+
// For layouts, wrap the outlet content (what `<Outlet />` renders) rather
|
|
329
|
+
// than the layout component itself. Parallel slots like `<ParallelOutlet
|
|
330
|
+
// name="@modal" />` read from a separate context channel and end up as
|
|
331
|
+
// siblings of the VT in the rendered tree, so modal mounts don't trigger a
|
|
332
|
+
// subtree update on the layout-level VT — which would otherwise make
|
|
333
|
+
// React's commit walker fire `document.startViewTransition` and apply
|
|
334
|
+
// view-transition-names to the underlying main subtree (cover/title/etc.).
|
|
335
|
+
//
|
|
336
|
+
// `transition.viewTransition === false` opts out of the router-owned
|
|
337
|
+
// boundary only. Driving (the startTransition wrap in browser/partial-update.ts
|
|
338
|
+
// and the param-agnostic key/hold below) keys off transition *presence*, not
|
|
339
|
+
// this flag, so a boundary-less transition still holds content and lets
|
|
340
|
+
// consumer-placed <ViewTransition> elements animate. The global
|
|
341
|
+
// createRouter({ viewTransition }) default is resolved into this field
|
|
342
|
+
// during segment resolution (only `false` is stamped; unset/"auto" is left
|
|
343
|
+
// as-is and means "wrap"), so this gate needs no router-option threading.
|
|
344
|
+
let outletContent: ReactNode =
|
|
285
345
|
node.segment.type === "layout" ? content : null;
|
|
286
346
|
|
|
347
|
+
const transition = node.segment.transition;
|
|
348
|
+
|
|
349
|
+
if (
|
|
350
|
+
ReactViewTransition &&
|
|
351
|
+
transition &&
|
|
352
|
+
transition.viewTransition !== false
|
|
353
|
+
) {
|
|
354
|
+
if (node.segment.type === "layout") {
|
|
355
|
+
outletContent = wrapDefaultOutletContent(outletContent, transition);
|
|
356
|
+
} else {
|
|
357
|
+
nodeContent = createViewTransitionBoundary(transition, nodeContent);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
287
361
|
// Prepare loader data if there are loaders
|
|
288
362
|
const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
|
|
289
|
-
const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
|
|
290
363
|
|
|
291
|
-
// Use LoaderBoundary when loading is defined to maintain consistent tree structure
|
|
292
|
-
// This ensures cached segments (which may not have loader segments) have the same
|
|
293
|
-
// tree structure as fresh segments, preventing React remounts
|
|
294
|
-
// If forceAwait or isAction is set, pre-resolve promises so LoaderBoundary won't suspend
|
|
295
364
|
if (loading !== undefined && loading !== null) {
|
|
365
|
+
const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
|
|
296
366
|
content = createElement(LoaderBoundary, {
|
|
297
367
|
key: `loader-boundary-${key}`,
|
|
298
368
|
loaderDataPromise:
|
|
@@ -306,7 +376,6 @@ export async function renderSegments(
|
|
|
306
376
|
children: nodeContent,
|
|
307
377
|
});
|
|
308
378
|
} else if (loaderEntries.length === 0) {
|
|
309
|
-
// No loaders, no loading - simple OutletProvider
|
|
310
379
|
content = createElement(OutletProvider, {
|
|
311
380
|
key,
|
|
312
381
|
content: outletContent,
|
|
@@ -315,34 +384,18 @@ export async function renderSegments(
|
|
|
315
384
|
children: nodeContent,
|
|
316
385
|
});
|
|
317
386
|
} else {
|
|
318
|
-
// Has loaders but no loading skeleton.
|
|
319
|
-
// Split: parallel-owned loaders stream (their parallel has loading()),
|
|
320
|
-
// layout-owned loaders are awaited (they gate the layout content).
|
|
321
387
|
const layoutLoaders = loaderEntries.filter((l) => !l.parallelLoading);
|
|
322
388
|
const parallelOwnedLoaders = loaderEntries.filter(
|
|
323
389
|
(l) => !!l.parallelLoading,
|
|
324
390
|
);
|
|
325
391
|
|
|
326
|
-
// Await only layout-owned loaders
|
|
327
392
|
const layoutLoaderIds = layoutLoaders.map((l) => l.loaderId!);
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
? Promise.all(
|
|
331
|
-
layoutLoaders.map((l) =>
|
|
332
|
-
l.loaderData instanceof Promise
|
|
333
|
-
? l.loaderData
|
|
334
|
-
: Promise.resolve(l.loaderData),
|
|
335
|
-
),
|
|
336
|
-
)
|
|
337
|
-
: Promise.resolve([]);
|
|
338
|
-
const resolvedData = await layoutLoaderDataPromise;
|
|
339
|
-
const { loaderData, errorFallback } = resolveLoaderData(
|
|
393
|
+
const resolvedData = await buildLoaderPromise(layoutLoaders);
|
|
394
|
+
const { loaderData, errorFallback } = decodeLoaderResults(
|
|
340
395
|
resolvedData,
|
|
341
396
|
layoutLoaderIds,
|
|
342
397
|
);
|
|
343
398
|
|
|
344
|
-
// Parallel-owned loaders: attach to their owning parallel segment
|
|
345
|
-
// as loaderDataPromise so ParallelOutlet wraps in LoaderBoundary
|
|
346
399
|
if (parallelOwnedLoaders.length > 0) {
|
|
347
400
|
const loadersByParallelNamespace = new Map<string, ResolvedSegment[]>();
|
|
348
401
|
|
|
@@ -399,8 +452,6 @@ export async function renderSegments(
|
|
|
399
452
|
}
|
|
400
453
|
}
|
|
401
454
|
|
|
402
|
-
// Always wrap with root error boundary to prevent white screens
|
|
403
|
-
// This catches any unhandled errors that bubble up from the segment tree
|
|
404
455
|
const errorBoundaryWrapped = createElement(RootErrorBoundary, {
|
|
405
456
|
children: content,
|
|
406
457
|
});
|
|
@@ -408,11 +459,8 @@ export async function renderSegments(
|
|
|
408
459
|
await Promise.allSettled(temporalLazyRefs);
|
|
409
460
|
}
|
|
410
461
|
|
|
411
|
-
// Build the final result, optionally wrapped with root layout
|
|
412
462
|
let result: ReactNode = errorBoundaryWrapped;
|
|
413
463
|
|
|
414
|
-
// If rootLayout is provided, wrap the error boundary with it
|
|
415
|
-
// This ensures the app shell stays mounted even during errors (prevents FOUC)
|
|
416
464
|
if (RootLayout) {
|
|
417
465
|
result = createElement(RootLayout, {
|
|
418
466
|
children: errorBoundaryWrapped,
|
|
@@ -450,6 +498,28 @@ export async function renderSegments(
|
|
|
450
498
|
* @param segments - Main segments from the route tree
|
|
451
499
|
* @param interceptSegments - Optional intercept segments to inject
|
|
452
500
|
*/
|
|
501
|
+
// Loader segment ids have the grammar `${parentId}D${index}.${loaderId}`.
|
|
502
|
+
// parentId is the parent shortCode (M/L/P/R/C + digits, never "D") for normal
|
|
503
|
+
// loaders, or `${shortCode}.${slotName}` for intercept-slot loaders, where the
|
|
504
|
+
// slot name is user-controlled (`@${string}`) and may contain an uppercase "D"
|
|
505
|
+
// (e.g. "@Detail"). Strip from the first `D<index>.` separator so the slot name
|
|
506
|
+
// is preserved; splitting on a bare "D" mis-cut "@Detail" to "@" and silently
|
|
507
|
+
// dropped the loader's data.
|
|
508
|
+
function loaderParentId(loaderSegmentId: string): string {
|
|
509
|
+
return loaderSegmentId.replace(/D\d+\..*$/, "");
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Append a value to the array stored under `key`, creating the array on first
|
|
513
|
+
// use. Single Map lookup (vs the has/get!().push double-lookup idiom).
|
|
514
|
+
function pushToGroup<K, V>(map: Map<K, V[]>, key: K, value: V): void {
|
|
515
|
+
const arr = map.get(key);
|
|
516
|
+
if (arr) {
|
|
517
|
+
arr.push(value);
|
|
518
|
+
} else {
|
|
519
|
+
map.set(key, [value]);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
453
523
|
function* segmentTreeWalk(
|
|
454
524
|
segments: ResolvedSegment[],
|
|
455
525
|
interceptSegments?: ResolvedSegment[],
|
|
@@ -470,19 +540,12 @@ function* segmentTreeWalk(
|
|
|
470
540
|
// Extract parent ID from parallel ID
|
|
471
541
|
// Example: "L0R1L0.@sidebar" → "L0R1L0"
|
|
472
542
|
const parentId = segment.id.split(".")[0];
|
|
473
|
-
|
|
474
|
-
parallelsByParent.set(parentId, []);
|
|
475
|
-
}
|
|
476
|
-
parallelsByParent.get(parentId)!.push(segment);
|
|
543
|
+
pushToGroup(parallelsByParent, parentId, segment);
|
|
477
544
|
} else if (segment.type === "loader") {
|
|
478
545
|
// Extract parent ID from loader ID
|
|
479
|
-
// Example: "L0D0.cart" → "L0"
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
if (!loadersByParent.has(parentId)) {
|
|
483
|
-
loadersByParent.set(parentId, []);
|
|
484
|
-
}
|
|
485
|
-
loadersByParent.get(parentId)!.push(segment);
|
|
546
|
+
// Example: "L0D0.cart" → "L0"; "L0.@DetailD0.x" → "L0.@Detail"
|
|
547
|
+
const parentId = loaderParentId(segment.id);
|
|
548
|
+
pushToGroup(loadersByParent, parentId, segment);
|
|
486
549
|
} else {
|
|
487
550
|
// Layout, route, error, and notFound segments are all rendered in the tree
|
|
488
551
|
// Error/notFound segments replace the failed segment with fallback UI
|
|
@@ -497,17 +560,11 @@ function* segmentTreeWalk(
|
|
|
497
560
|
if (intercept.type === "parallel" && intercept.slot) {
|
|
498
561
|
// Extract parent ID from intercept ID (e.g., "M4L0L0L2.@modal" → "M4L0L0L2")
|
|
499
562
|
const parentId = intercept.id.split(".")[0];
|
|
500
|
-
|
|
501
|
-
parallelsByParent.set(parentId, []);
|
|
502
|
-
}
|
|
503
|
-
parallelsByParent.get(parentId)!.push(intercept);
|
|
563
|
+
pushToGroup(parallelsByParent, parentId, intercept);
|
|
504
564
|
} else if (intercept.type === "loader") {
|
|
505
|
-
// Intercept loaders - extract parent from loader ID
|
|
506
|
-
const parentId = intercept.id
|
|
507
|
-
|
|
508
|
-
loadersByParent.set(parentId, []);
|
|
509
|
-
}
|
|
510
|
-
loadersByParent.get(parentId)!.push(intercept);
|
|
565
|
+
// Intercept loaders - extract parent from loader ID (slot name preserved)
|
|
566
|
+
const parentId = loaderParentId(intercept.id);
|
|
567
|
+
pushToGroup(loadersByParent, parentId, intercept);
|
|
511
568
|
}
|
|
512
569
|
}
|
|
513
570
|
}
|