@rangojs/router 0.0.0-experimental.135c6902 → 0.0.0-experimental.136
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/AGENTS.md +8 -0
- package/README.md +245 -49
- package/dist/bin/rango.js +440 -133
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3386 -1111
- package/dist/vite/index.js.bak +5448 -0
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +78 -25
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +64 -2
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +247 -23
- package/skills/caching/SKILL.md +281 -11
- package/skills/composability/SKILL.md +27 -3
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +250 -30
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +87 -18
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +289 -53
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +914 -0
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +144 -7
- package/skills/prerender/SKILL.md +172 -125
- package/skills/rango/SKILL.md +251 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +123 -48
- package/skills/route/SKILL.md +101 -5
- package/skills/router-setup/SKILL.md +117 -10
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +130 -0
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -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 +122 -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 +123 -0
- package/skills/typesafety/SKILL.md +332 -29
- package/skills/use-cache/SKILL.md +57 -14
- package/skills/view-transitions/SKILL.md +337 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +1 -66
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +192 -150
- 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 +131 -30
- package/src/browser/navigation-client.ts +199 -86
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +157 -74
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +174 -109
- package/src/browser/prefetch/cache.ts +222 -69
- package/src/browser/prefetch/fetch.ts +347 -39
- package/src/browser/prefetch/queue.ts +113 -32
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +102 -15
- package/src/browser/react/NavigationProvider.tsx +300 -122
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/deferred-handle-resolution.ts +75 -0
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -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 +23 -69
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +32 -7
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +99 -8
- package/src/browser/rsc-router.tsx +125 -28
- package/src/browser/scroll-restoration.ts +37 -22
- package/src/browser/segment-reconciler.ts +36 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +222 -61
- package/src/browser/types.ts +115 -12
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +165 -36
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +125 -24
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +260 -94
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +239 -52
- package/src/cache/cache-scope.ts +234 -87
- package/src/cache/cache-tag.ts +103 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +1989 -378
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +89 -21
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +10 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +44 -21
- package/src/client.tsx +119 -290
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +84 -2
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +62 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +37 -8
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +51 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +23 -52
- 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 +99 -13
- package/src/index.ts +139 -19
- package/src/internal-debug.ts +11 -10
- 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/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +62 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +461 -304
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +149 -74
- package/src/route-definition/index.ts +4 -2
- package/src/route-definition/redirect.ts +51 -10
- package/src/route-definition/resolve-handler-use.ts +160 -0
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +37 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +164 -17
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +83 -39
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +48 -24
- package/src/router/lazy-includes.ts +15 -52
- package/src/router/loader-resolution.ts +274 -56
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +40 -42
- package/src/router/match-api.ts +124 -204
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +54 -25
- package/src/router/match-middleware/cache-lookup.ts +205 -271
- package/src/router/match-middleware/cache-store.ts +81 -50
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +45 -14
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +93 -39
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +268 -171
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +132 -90
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +195 -56
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +3 -27
- package/src/router/router-interfaces.ts +129 -35
- package/src/router/router-options.ts +202 -15
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +197 -92
- package/src/router/segment-resolution/helpers.ts +115 -30
- package/src/router/segment-resolution/loader-cache.ts +76 -39
- package/src/router/segment-resolution/revalidation.ts +392 -338
- 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 +56 -0
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +6 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +161 -199
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +163 -59
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +174 -54
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +660 -518
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +127 -31
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +133 -13
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +99 -189
- package/src/rsc/rsc-rendering.ts +115 -73
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +287 -113
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +36 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +149 -0
- package/src/segment-system.tsx +269 -202
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +235 -51
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +40 -38
- package/src/server/loader-registry.ts +38 -46
- package/src/server/request-context.ts +442 -173
- package/src/ssr/index.tsx +24 -16
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +701 -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 +257 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +371 -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 +357 -0
- package/src/testing/render-route.tsx +581 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/run-transition-when.ts +164 -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 +20 -58
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +234 -82
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +12 -7
- package/src/types/segments.ts +136 -15
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +68 -18
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/discover-routers.ts +130 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +214 -132
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +57 -4
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +6 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +178 -5
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/plugins/expose-action-id.ts +48 -95
- package/src/vite/plugins/expose-id-utils.ts +96 -51
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +553 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/version-injector.ts +40 -29
- package/src/vite/plugins/version-plugin.ts +37 -40
- package/src/vite/plugins/virtual-entries.ts +39 -25
- package/src/vite/rango.ts +119 -111
- package/src/vite/router-discovery.ts +941 -142
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -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 +20 -52
- package/src/vite/utils/prerender-utils.ts +81 -34
- package/src/vite/utils/shared-utils.ts +92 -42
- 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/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
package/src/segment-system.tsx
CHANGED
|
@@ -1,28 +1,45 @@
|
|
|
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
|
-
import type {
|
|
6
|
-
|
|
7
|
-
LoaderDataResult,
|
|
8
|
-
RootLayoutProps,
|
|
9
|
-
} from "./types.js";
|
|
10
|
-
import { isLoaderDataResult } from "./types.js";
|
|
5
|
+
import type { ResolvedSegment, RootLayoutProps } from "./types.js";
|
|
6
|
+
import { decodeLoaderResults } from "./decode-loader-results.js";
|
|
11
7
|
import { invariant } from "./errors.js";
|
|
12
8
|
import {
|
|
13
9
|
RouteContentWrapper,
|
|
14
10
|
LoaderBoundary,
|
|
15
11
|
} from "./route-content-wrapper.js";
|
|
16
12
|
import { RootErrorBoundary } from "./root-error-boundary.js";
|
|
13
|
+
import { INTERNAL_RANGO_DEBUG } from "./internal-debug.js";
|
|
14
|
+
import { getMemoizedContentPromise } from "./segment-content-promise.js";
|
|
15
|
+
import {
|
|
16
|
+
buildLoaderPromise,
|
|
17
|
+
getMemoizedLoaderPromise,
|
|
18
|
+
} from "./segment-loader-promise.js";
|
|
17
19
|
|
|
18
20
|
// ViewTransition is only available in React experimental.
|
|
19
21
|
// Access via namespace import to avoid compile-time errors on stable React.
|
|
20
22
|
const ReactViewTransition: any =
|
|
21
23
|
"ViewTransition" in React ? (React as any).ViewTransition : null;
|
|
22
24
|
|
|
23
|
-
|
|
25
|
+
// A loading skeleton is renderable only when it is a real ReactNode value.
|
|
26
|
+
// `false` is treated as "not renderable" here. This is the three-term gate;
|
|
27
|
+
// the distinct two-term gate at the LoaderBoundary site deliberately treats
|
|
28
|
+
// `false` as "create a boundary without a RouteContentWrapper"
|
|
29
|
+
// (tree-structure.md), so it must NOT use this helper.
|
|
30
|
+
function isRenderableLoading(loading: ReactNode): boolean {
|
|
31
|
+
return loading !== undefined && loading !== null && loading !== false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Exported for unit testing the no-parallel fast path (D6); internal otherwise.
|
|
35
|
+
export function restoreParallelLoaderMarkers(
|
|
24
36
|
segments: ResolvedSegment[],
|
|
25
37
|
): ResolvedSegment[] {
|
|
38
|
+
// Parallel-loading markers only exist when a parallel segment is present, so
|
|
39
|
+
// a list with no parallel slot has nothing to restore. Skip the Map alloc and
|
|
40
|
+
// full scan in that (common) case — this runs on every render.
|
|
41
|
+
if (!segments.some((s) => s.type === "parallel")) return segments;
|
|
42
|
+
|
|
26
43
|
const parallelLoadingByNamespace = new Map<string, ReactNode>();
|
|
27
44
|
let nextSegments: ResolvedSegment[] | null = null;
|
|
28
45
|
|
|
@@ -30,12 +47,7 @@ function restoreParallelLoaderMarkers(
|
|
|
30
47
|
const segment = segments[i];
|
|
31
48
|
|
|
32
49
|
if (segment.type === "parallel") {
|
|
33
|
-
if (
|
|
34
|
-
segment.namespace &&
|
|
35
|
-
segment.loading !== undefined &&
|
|
36
|
-
segment.loading !== null &&
|
|
37
|
-
segment.loading !== false
|
|
38
|
-
) {
|
|
50
|
+
if (segment.namespace && isRenderableLoading(segment.loading)) {
|
|
39
51
|
parallelLoadingByNamespace.set(segment.namespace, segment.loading);
|
|
40
52
|
}
|
|
41
53
|
continue;
|
|
@@ -61,56 +73,6 @@ function restoreParallelLoaderMarkers(
|
|
|
61
73
|
return nextSegments ?? segments;
|
|
62
74
|
}
|
|
63
75
|
|
|
64
|
-
function hasSameReferences(a: unknown[] | undefined, b: unknown[]): boolean {
|
|
65
|
-
if (!a || a.length !== b.length) {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
for (let i = 0; i < a.length; i++) {
|
|
70
|
-
if (a[i] !== b[i]) {
|
|
71
|
-
return false;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Resolve loader data from raw results, unwrapping LoaderDataResult wrappers
|
|
80
|
-
*/
|
|
81
|
-
function resolveLoaderData(
|
|
82
|
-
resolvedData: any[],
|
|
83
|
-
loaderIds: string[],
|
|
84
|
-
): { loaderData: Record<string, any>; errorFallback: ReactNode } {
|
|
85
|
-
const loaderData: Record<string, any> = {};
|
|
86
|
-
let errorFallback: ReactNode = null;
|
|
87
|
-
|
|
88
|
-
for (let i = 0; i < loaderIds.length; i++) {
|
|
89
|
-
const id = loaderIds[i];
|
|
90
|
-
const result = resolvedData[i];
|
|
91
|
-
|
|
92
|
-
if (!isLoaderDataResult(result)) {
|
|
93
|
-
// Legacy format - direct data
|
|
94
|
-
loaderData[id] = result;
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (result.ok) {
|
|
99
|
-
loaderData[id] = result.data;
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Error case
|
|
104
|
-
if (result.fallback) {
|
|
105
|
-
errorFallback = result.fallback;
|
|
106
|
-
} else {
|
|
107
|
-
throw new Error(result.error.message);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return { loaderData, errorFallback };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
76
|
/**
|
|
115
77
|
* Options for renderSegments
|
|
116
78
|
*/
|
|
@@ -147,11 +109,61 @@ export interface RenderSegmentsOptions {
|
|
|
147
109
|
rootLayout?: ComponentType<RootLayoutProps>;
|
|
148
110
|
}
|
|
149
111
|
|
|
112
|
+
function createViewTransitionBoundary(
|
|
113
|
+
transition: NonNullable<ResolvedSegment["transition"]>,
|
|
114
|
+
children: ReactNode,
|
|
115
|
+
): ReactNode {
|
|
116
|
+
// `viewTransition` is a router-specific flag (boundary opt-out), not a React
|
|
117
|
+
// <ViewTransition> prop — strip it so it never reaches React.
|
|
118
|
+
const { viewTransition: _viewTransition, ...vtProps } = transition;
|
|
119
|
+
return createElement(ReactViewTransition, {
|
|
120
|
+
...vtProps,
|
|
121
|
+
children,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function wrapDefaultOutletContent(
|
|
126
|
+
content: ReactNode,
|
|
127
|
+
transition: NonNullable<ResolvedSegment["transition"]>,
|
|
128
|
+
): ReactNode {
|
|
129
|
+
if (!React.isValidElement(content)) {
|
|
130
|
+
return createViewTransitionBoundary(transition, content);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const props = content.props as any;
|
|
134
|
+
|
|
135
|
+
if (content.type === MountContextProvider) {
|
|
136
|
+
return React.cloneElement(content, {
|
|
137
|
+
children: wrapDefaultOutletContent(props.children, transition),
|
|
138
|
+
} as any);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (content.type === OutletProvider && props.segment?.type === "layout") {
|
|
142
|
+
return React.cloneElement(content, {
|
|
143
|
+
content: wrapDefaultOutletContent(props.content, transition),
|
|
144
|
+
} as any);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (content.type === LoaderBoundary && props.segment?.type === "layout") {
|
|
148
|
+
return React.cloneElement(content, {
|
|
149
|
+
outletContent: wrapDefaultOutletContent(props.outletContent, transition),
|
|
150
|
+
} as any);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return createViewTransitionBoundary(transition, content);
|
|
154
|
+
}
|
|
155
|
+
|
|
150
156
|
/**
|
|
151
157
|
* Render segments into a React tree with proper layout nesting
|
|
152
158
|
*
|
|
153
|
-
* Layouts nest using OutletProvider
|
|
154
|
-
* render as
|
|
159
|
+
* Layouts nest using OutletProvider; a layout receives the inner content via
|
|
160
|
+
* its `<Outlet />`. Parallel segments do NOT render as inline Fragment siblings
|
|
161
|
+
* — they flow through OutletContext.parallel and are resolved where a layout
|
|
162
|
+
* places `<ParallelOutlet name="@sidebar" />` (or `<Outlet name="@sidebar" />`).
|
|
163
|
+
*
|
|
164
|
+
* The result is always wrapped in RootErrorBoundary so unhandled errors never
|
|
165
|
+
* blank the screen. When `options.rootLayout` is provided it wraps the error
|
|
166
|
+
* boundary at the OUTERMOST level (so the app shell survives errors).
|
|
155
167
|
*
|
|
156
168
|
* Error segments are treated like route segments - they render their fallback
|
|
157
169
|
* component in place of the failed segment. When an error occurs in a handler,
|
|
@@ -163,27 +175,30 @@ export interface RenderSegmentsOptions {
|
|
|
163
175
|
* notFoundBoundary's fallback component.
|
|
164
176
|
*
|
|
165
177
|
* @param segments - Array of resolved segments to render
|
|
166
|
-
* @returns ReactNode
|
|
178
|
+
* @returns Promise resolving to the ReactNode tree (the function is async)
|
|
167
179
|
*
|
|
168
180
|
* @example
|
|
169
181
|
* ```typescript
|
|
170
182
|
* const segments = [
|
|
171
|
-
* { id: 'L0.0', type: 'layout', component: <
|
|
172
|
-
* { id: '
|
|
173
|
-
* { id: '
|
|
174
|
-
* { id: 'P3.0', type: 'parallel', component: <Sidebar />, slot: '@sidebar' }
|
|
183
|
+
* { id: 'L0.0', type: 'layout', component: <BlogLayout /> },
|
|
184
|
+
* { id: 'L0R1', type: 'route', component: <BlogPost /> },
|
|
185
|
+
* { id: 'L0R1.@sidebar', type: 'parallel', component: <Sidebar />, slot: '@sidebar' }
|
|
175
186
|
* ];
|
|
176
187
|
*
|
|
177
|
-
*
|
|
178
|
-
* //
|
|
179
|
-
*
|
|
180
|
-
* //
|
|
181
|
-
* //
|
|
182
|
-
* //
|
|
183
|
-
* //
|
|
188
|
+
* // BlogLayout renders <Outlet /> for the route and
|
|
189
|
+
* // <ParallelOutlet name="@sidebar" /> for the parallel slot.
|
|
190
|
+
* const tree = await renderSegments(segments, { rootLayout: RootLayout });
|
|
191
|
+
* // Results in (outermost first):
|
|
192
|
+
* // <RootLayout>
|
|
193
|
+
* // <RootErrorBoundary>
|
|
194
|
+
* // <OutletProvider segment={BlogLayout} parallel={[Sidebar]}>
|
|
195
|
+
* // <BlogPost />
|
|
196
|
+
* // </OutletProvider>
|
|
197
|
+
* // </RootErrorBoundary>
|
|
198
|
+
* // </RootLayout>
|
|
184
199
|
*
|
|
185
200
|
* // For server actions, pass isAction to await components:
|
|
186
|
-
* const tree = renderSegments(segments, { isAction: true });
|
|
201
|
+
* const tree = await renderSegments(segments, { isAction: true });
|
|
187
202
|
* ```
|
|
188
203
|
*/
|
|
189
204
|
export async function renderSegments(
|
|
@@ -227,6 +242,25 @@ export async function renderSegments(
|
|
|
227
242
|
}
|
|
228
243
|
// Separate segments by type, passing intercept segments for explicit injection
|
|
229
244
|
const tree = segmentTreeWalk(normalizedSegments, normalizedInterceptSegments);
|
|
245
|
+
|
|
246
|
+
// A route is "in a transition scope" when its own segment OR any layout in
|
|
247
|
+
// its matched chain declares transition(). Both transition() forms land here:
|
|
248
|
+
// the per-route item form sets transition on the route entry, and the block
|
|
249
|
+
// wrapper form sets it on a transparent ancestor layout (dsl-helpers.ts). When
|
|
250
|
+
// in scope, the route and its route-owned layouts use param-agnostic keys so a
|
|
251
|
+
// same-route navigation reconciles (holds content) instead of remounting. The
|
|
252
|
+
// value is a static property of the route's position in the tree, so it is the
|
|
253
|
+
// same on every render of that route (SSR, navigation, action) — the keys
|
|
254
|
+
// never drift. Cross-route navigation still remounts: different routes have
|
|
255
|
+
// different segment ids regardless of transition scope.
|
|
256
|
+
const inTransitionScope = normalizedSegments.some(
|
|
257
|
+
(s) =>
|
|
258
|
+
s.transition != null &&
|
|
259
|
+
(s.type === "layout" ||
|
|
260
|
+
s.type === "route" ||
|
|
261
|
+
s.type === "error" ||
|
|
262
|
+
s.type === "notFound"),
|
|
263
|
+
);
|
|
230
264
|
// Render content segments as siblings
|
|
231
265
|
let content: ReactNode = null;
|
|
232
266
|
for (const node of tree) {
|
|
@@ -239,17 +273,31 @@ export async function renderSegments(
|
|
|
239
273
|
);
|
|
240
274
|
const { component, id, params, loading } = node.segment;
|
|
241
275
|
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
276
|
+
// Param-agnostic keys are opt-in via the transition() DSL (see
|
|
277
|
+
// inTransitionScope above). A route (and its route-owned layouts) inside a
|
|
278
|
+
// transition scope drops the param from its key, so navigating between two
|
|
279
|
+
// param values of the SAME route (e.g. /product/1 -> /product/2) reconciles
|
|
280
|
+
// the route subtree instead of remounting it. Combined with the
|
|
281
|
+
// startTransition wrap that shouldStartViewTransition already applies to
|
|
282
|
+
// transition routes (browser/partial-update.ts), the previous content stays
|
|
283
|
+
// on screen while the new loaders resolve (stale-while-revalidate) instead
|
|
284
|
+
// of flashing the loading skeleton. This works on stable React; experimental
|
|
285
|
+
// React adds the animated <ViewTransition> cross-fade on top.
|
|
286
|
+
//
|
|
287
|
+
// Outside a transition scope the key stays param-bearing and the route
|
|
288
|
+
// remounts on param change (the default: a fresh skeleton and fresh
|
|
289
|
+
// component state).
|
|
290
|
+
//
|
|
291
|
+
// error/notFound always keep param-bearing keys: createErrorSegment reuses
|
|
292
|
+
// the boundary layout's shortCode as the error segment id (router/
|
|
293
|
+
// error-handling.ts), so a param-agnostic error key could collide with that
|
|
294
|
+
// layout's key within the same render.
|
|
248
295
|
const includeParams =
|
|
249
|
-
node.segment.type === "route" ||
|
|
250
296
|
node.segment.type === "error" ||
|
|
251
297
|
node.segment.type === "notFound" ||
|
|
252
|
-
(node.segment.type === "
|
|
298
|
+
((node.segment.type === "route" ||
|
|
299
|
+
(node.segment.type === "layout" && node.segment.belongsToRoute)) &&
|
|
300
|
+
!inTransitionScope);
|
|
253
301
|
|
|
254
302
|
const paramStr =
|
|
255
303
|
includeParams && params && Object.keys(params).length > 0
|
|
@@ -258,69 +306,103 @@ export async function renderSegments(
|
|
|
258
306
|
.map(([k, v]) => `${k}=${v}`)
|
|
259
307
|
.join(",")
|
|
260
308
|
: "";
|
|
261
|
-
const key =
|
|
309
|
+
const key = paramStr ? `${id}-${paramStr}` : id;
|
|
262
310
|
|
|
263
|
-
// Get loader entries for this node
|
|
264
311
|
const loaderEntries = node.loaders.filter(
|
|
265
312
|
(loader) => loader.loaderId && loader.loaderData !== undefined,
|
|
266
313
|
);
|
|
267
314
|
|
|
268
|
-
// Determine the component content (with or without Suspense wrapper)
|
|
269
|
-
// Wrap when loading skeleton defined OR component is Promise (needs Suspense)
|
|
270
|
-
// During actions, await component Promise to prevent Suspense from triggering
|
|
271
|
-
// This keeps existing content visible instead of showing loading skeleton
|
|
272
315
|
let resolvedComponent = component;
|
|
273
316
|
if (isAction && component instanceof Promise) {
|
|
274
317
|
resolvedComponent = await component;
|
|
275
318
|
}
|
|
276
319
|
|
|
277
|
-
let nodeContent: ReactNode =
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
320
|
+
let nodeContent: ReactNode = null;
|
|
321
|
+
if (isRenderableLoading(loading)) {
|
|
322
|
+
// forceAwait (popstate, stale-revalidation, fully-prefetched nav) renders a
|
|
323
|
+
// loading() route with the route content ALREADY resolved, so its
|
|
324
|
+
// RouteContentWrapper Suspender does not suspend for a microtask and flash
|
|
325
|
+
// the loading() fallback on a NORMAL (non-transition) commit. The router
|
|
326
|
+
// data is known-ready on these paths, so awaiting the content here is free.
|
|
327
|
+
// The wrapper tree is unchanged (RouteContentWrapper is still created with
|
|
328
|
+
// the same key/fallback) — only the `content` prop is a resolved node
|
|
329
|
+
// instead of a pending promise, which Suspender renders synchronously. This
|
|
330
|
+
// mirrors the forceAwait loaderData unwrap above; a CLIENT component that
|
|
331
|
+
// suspends on mount inside the content still reveals a fallback (it is not
|
|
332
|
+
// pre-resolved).
|
|
333
|
+
const contentPromise = getMemoizedContentPromise(resolvedComponent);
|
|
334
|
+
const loadingContent: Promise<ReactNode> | ReactNode = forceAwait
|
|
335
|
+
? await contentPromise
|
|
336
|
+
: contentPromise;
|
|
337
|
+
nodeContent = createElement(RouteContentWrapper, {
|
|
338
|
+
key: `suspense-loading-${id}`,
|
|
339
|
+
content: loadingContent,
|
|
340
|
+
fallback: loading,
|
|
341
|
+
segmentId: id,
|
|
342
|
+
});
|
|
343
|
+
} else {
|
|
344
|
+
// [VT-DIAG] Gated behind INTERNAL_RANGO_DEBUG. A segment in the no-loading()
|
|
345
|
+
// branch whose component decodes as a Promise/lazy gets registered into
|
|
346
|
+
// temporalLazyRefs and awaited before commit (see below) — which on builds
|
|
347
|
+
// where the segment component arrives deferred defeats client-nav streaming.
|
|
348
|
+
if (INTERNAL_RANGO_DEBUG && typeof window === "object") {
|
|
349
|
+
const c = resolvedComponent as unknown;
|
|
350
|
+
console.log("[VT-DIAG] renderSegments no-loading-branch segment", {
|
|
351
|
+
id,
|
|
352
|
+
type: node.segment.type,
|
|
353
|
+
componentIsPromise: c instanceof Promise,
|
|
354
|
+
componentIsLazy:
|
|
355
|
+
c != null && typeof c === "object" && "_payload" in c,
|
|
356
|
+
componentTypeof: typeof c,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
nodeContent = registerLazyRef(resolvedComponent);
|
|
360
|
+
}
|
|
289
361
|
|
|
290
362
|
// Wrap with <ViewTransition> if transition config exists (React experimental only).
|
|
291
363
|
// An empty config ({}) creates a bare <ViewTransition> boundary that participates
|
|
292
364
|
// in transitions without adding custom animation classes. Named element-level
|
|
293
365
|
// <ViewTransition> components inside (with name/share props) morph independently
|
|
294
366
|
// from the parent's default cross-fade.
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
//
|
|
303
|
-
|
|
367
|
+
//
|
|
368
|
+
// For layouts, wrap the outlet content (what `<Outlet />` renders) rather
|
|
369
|
+
// than the layout component itself. Parallel slots like `<ParallelOutlet
|
|
370
|
+
// name="@modal" />` read from a separate context channel and end up as
|
|
371
|
+
// siblings of the VT in the rendered tree, so modal mounts don't trigger a
|
|
372
|
+
// subtree update on the layout-level VT — which would otherwise make
|
|
373
|
+
// React's commit walker fire `document.startViewTransition` and apply
|
|
374
|
+
// view-transition-names to the underlying main subtree (cover/title/etc.).
|
|
375
|
+
//
|
|
376
|
+
// `transition.viewTransition === false` opts out of the router-owned
|
|
377
|
+
// boundary only. Driving (the startTransition wrap in browser/partial-update.ts
|
|
378
|
+
// and the param-agnostic key/hold below) keys off transition *presence*, not
|
|
379
|
+
// this flag, so a boundary-less transition still holds content and lets
|
|
380
|
+
// consumer-placed <ViewTransition> elements animate. The global
|
|
381
|
+
// createRouter({ viewTransition }) default is resolved into this field
|
|
382
|
+
// during segment resolution (only `false` is stamped; unset/"auto" is left
|
|
383
|
+
// as-is and means "wrap"), so this gate needs no router-option threading.
|
|
384
|
+
let outletContent: ReactNode =
|
|
304
385
|
node.segment.type === "layout" ? content : null;
|
|
305
386
|
|
|
387
|
+
const transition = node.segment.transition;
|
|
388
|
+
|
|
389
|
+
if (
|
|
390
|
+
ReactViewTransition &&
|
|
391
|
+
transition &&
|
|
392
|
+
transition.viewTransition !== false
|
|
393
|
+
) {
|
|
394
|
+
if (node.segment.type === "layout") {
|
|
395
|
+
outletContent = wrapDefaultOutletContent(outletContent, transition);
|
|
396
|
+
} else {
|
|
397
|
+
nodeContent = createViewTransitionBoundary(transition, nodeContent);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
306
401
|
// Prepare loader data if there are loaders
|
|
307
402
|
const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
|
|
308
|
-
|
|
309
|
-
loaderEntries.length > 0
|
|
310
|
-
? Promise.all(
|
|
311
|
-
loaderEntries.map((loader) =>
|
|
312
|
-
loader.loaderData instanceof Promise
|
|
313
|
-
? loader.loaderData
|
|
314
|
-
: Promise.resolve(loader.loaderData),
|
|
315
|
-
),
|
|
316
|
-
)
|
|
317
|
-
: Promise.resolve([]);
|
|
318
|
-
|
|
319
|
-
// Use LoaderBoundary when loading is defined to maintain consistent tree structure
|
|
320
|
-
// This ensures cached segments (which may not have loader segments) have the same
|
|
321
|
-
// tree structure as fresh segments, preventing React remounts
|
|
322
|
-
// If forceAwait or isAction is set, pre-resolve promises so LoaderBoundary won't suspend
|
|
403
|
+
|
|
323
404
|
if (loading !== undefined && loading !== null) {
|
|
405
|
+
const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
|
|
324
406
|
content = createElement(LoaderBoundary, {
|
|
325
407
|
key: `loader-boundary-${key}`,
|
|
326
408
|
loaderDataPromise:
|
|
@@ -334,7 +416,6 @@ export async function renderSegments(
|
|
|
334
416
|
children: nodeContent,
|
|
335
417
|
});
|
|
336
418
|
} else if (loaderEntries.length === 0) {
|
|
337
|
-
// No loaders, no loading - simple OutletProvider
|
|
338
419
|
content = createElement(OutletProvider, {
|
|
339
420
|
key,
|
|
340
421
|
content: outletContent,
|
|
@@ -343,34 +424,18 @@ export async function renderSegments(
|
|
|
343
424
|
children: nodeContent,
|
|
344
425
|
});
|
|
345
426
|
} else {
|
|
346
|
-
// Has loaders but no loading skeleton.
|
|
347
|
-
// Split: parallel-owned loaders stream (their parallel has loading()),
|
|
348
|
-
// layout-owned loaders are awaited (they gate the layout content).
|
|
349
427
|
const layoutLoaders = loaderEntries.filter((l) => !l.parallelLoading);
|
|
350
428
|
const parallelOwnedLoaders = loaderEntries.filter(
|
|
351
429
|
(l) => !!l.parallelLoading,
|
|
352
430
|
);
|
|
353
431
|
|
|
354
|
-
// Await only layout-owned loaders
|
|
355
432
|
const layoutLoaderIds = layoutLoaders.map((l) => l.loaderId!);
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
? Promise.all(
|
|
359
|
-
layoutLoaders.map((l) =>
|
|
360
|
-
l.loaderData instanceof Promise
|
|
361
|
-
? l.loaderData
|
|
362
|
-
: Promise.resolve(l.loaderData),
|
|
363
|
-
),
|
|
364
|
-
)
|
|
365
|
-
: Promise.resolve([]);
|
|
366
|
-
const resolvedData = await layoutLoaderDataPromise;
|
|
367
|
-
const { loaderData, errorFallback } = resolveLoaderData(
|
|
433
|
+
const resolvedData = await buildLoaderPromise(layoutLoaders);
|
|
434
|
+
const { loaderData, errorFallback } = decodeLoaderResults(
|
|
368
435
|
resolvedData,
|
|
369
436
|
layoutLoaderIds,
|
|
370
437
|
);
|
|
371
438
|
|
|
372
|
-
// Parallel-owned loaders: attach to their owning parallel segment
|
|
373
|
-
// as loaderDataPromise so ParallelOutlet wraps in LoaderBoundary
|
|
374
439
|
if (parallelOwnedLoaders.length > 0) {
|
|
375
440
|
const loadersByParallelNamespace = new Map<string, ResolvedSegment[]>();
|
|
376
441
|
|
|
@@ -396,34 +461,12 @@ export async function renderSegments(
|
|
|
396
461
|
continue;
|
|
397
462
|
}
|
|
398
463
|
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
p.
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
hasSameReferences(p.parallelLoaderSources, parallelLoaderSources);
|
|
406
|
-
|
|
407
|
-
const parallelLoaderDataPromise = shouldReuseParallelPromise
|
|
408
|
-
? p.loaderDataPromise
|
|
409
|
-
: forceAwait || isAction
|
|
410
|
-
? await Promise.all(
|
|
411
|
-
ownedLoaders.map((l) =>
|
|
412
|
-
l.loaderData instanceof Promise
|
|
413
|
-
? l.loaderData
|
|
414
|
-
: Promise.resolve(l.loaderData),
|
|
415
|
-
),
|
|
416
|
-
)
|
|
417
|
-
: Promise.all(
|
|
418
|
-
ownedLoaders.map((l) =>
|
|
419
|
-
l.loaderData instanceof Promise
|
|
420
|
-
? l.loaderData
|
|
421
|
-
: Promise.resolve(l.loaderData),
|
|
422
|
-
),
|
|
423
|
-
);
|
|
424
|
-
|
|
425
|
-
p.loaderDataPromise = parallelLoaderDataPromise;
|
|
426
|
-
p.parallelLoaderSources = parallelLoaderSources;
|
|
464
|
+
p.loaderIds = ownedLoaders.map((l) => l.loaderId!);
|
|
465
|
+
const aggregated = getMemoizedLoaderPromise(ownedLoaders);
|
|
466
|
+
p.loaderDataPromise =
|
|
467
|
+
(forceAwait || isAction) && aggregated instanceof Promise
|
|
468
|
+
? await aggregated
|
|
469
|
+
: aggregated;
|
|
427
470
|
}
|
|
428
471
|
}
|
|
429
472
|
|
|
@@ -449,20 +492,32 @@ export async function renderSegments(
|
|
|
449
492
|
}
|
|
450
493
|
}
|
|
451
494
|
|
|
452
|
-
// Always wrap with root error boundary to prevent white screens
|
|
453
|
-
// This catches any unhandled errors that bubble up from the segment tree
|
|
454
495
|
const errorBoundaryWrapped = createElement(RootErrorBoundary, {
|
|
455
496
|
children: content,
|
|
456
497
|
});
|
|
457
498
|
if (typeof window === "object") {
|
|
499
|
+
// [VT-DIAG] Gated behind INTERNAL_RANGO_DEBUG. If this await dominates the
|
|
500
|
+
// navigation time, a deferred/lazy segment component is being fully resolved
|
|
501
|
+
// before commit, which defeats client-nav streaming. The await itself is
|
|
502
|
+
// functional (it preloads lazy chunk refs); only the timing log is gated.
|
|
503
|
+
const vtDebug = INTERNAL_RANGO_DEBUG && temporalLazyRefs.length > 0;
|
|
504
|
+
const vtDebugStart = vtDebug ? performance.now() : 0;
|
|
505
|
+
if (vtDebug) {
|
|
506
|
+
console.log("[VT-DIAG] renderSegments awaiting temporalLazyRefs", {
|
|
507
|
+
count: temporalLazyRefs.length,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
458
510
|
await Promise.allSettled(temporalLazyRefs);
|
|
511
|
+
if (vtDebug) {
|
|
512
|
+
console.log("[VT-DIAG] renderSegments temporalLazyRefs settled", {
|
|
513
|
+
count: temporalLazyRefs.length,
|
|
514
|
+
ms: Math.round(performance.now() - vtDebugStart),
|
|
515
|
+
});
|
|
516
|
+
}
|
|
459
517
|
}
|
|
460
518
|
|
|
461
|
-
// Build the final result, optionally wrapped with root layout
|
|
462
519
|
let result: ReactNode = errorBoundaryWrapped;
|
|
463
520
|
|
|
464
|
-
// If rootLayout is provided, wrap the error boundary with it
|
|
465
|
-
// This ensures the app shell stays mounted even during errors (prevents FOUC)
|
|
466
521
|
if (RootLayout) {
|
|
467
522
|
result = createElement(RootLayout, {
|
|
468
523
|
children: errorBoundaryWrapped,
|
|
@@ -500,6 +555,31 @@ export async function renderSegments(
|
|
|
500
555
|
* @param segments - Main segments from the route tree
|
|
501
556
|
* @param interceptSegments - Optional intercept segments to inject
|
|
502
557
|
*/
|
|
558
|
+
// Loader segment ids have the grammar `${parentId}D${index}.${loaderId}`.
|
|
559
|
+
// parentId is the parent shortCode (M/L/P/R/C + digits, never "D") for normal
|
|
560
|
+
// loaders, or `${shortCode}.${slotName}` for intercept-slot loaders, where the
|
|
561
|
+
// slot name is user-controlled (`@${string}`) and may contain an uppercase "D"
|
|
562
|
+
// (e.g. "@Detail"). Strip from the first `D<index>.` separator so the slot name
|
|
563
|
+
// is preserved; splitting on a bare "D" mis-cut "@Detail" to "@" and silently
|
|
564
|
+
// dropped the loader's data. The first-`D<index>.` strip is only correct because
|
|
565
|
+
// slot names cannot contain "." -- assertValidSlotName (route-definition/
|
|
566
|
+
// dsl-helpers.ts) rejects a "." at definition time, so a name like "@D3.foo"
|
|
567
|
+
// (which WOULD mis-cut here) can never reach this function.
|
|
568
|
+
function loaderParentId(loaderSegmentId: string): string {
|
|
569
|
+
return loaderSegmentId.replace(/D\d+\..*$/, "");
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Append a value to the array stored under `key`, creating the array on first
|
|
573
|
+
// use. Single Map lookup (vs the has/get!().push double-lookup idiom).
|
|
574
|
+
function pushToGroup<K, V>(map: Map<K, V[]>, key: K, value: V): void {
|
|
575
|
+
const arr = map.get(key);
|
|
576
|
+
if (arr) {
|
|
577
|
+
arr.push(value);
|
|
578
|
+
} else {
|
|
579
|
+
map.set(key, [value]);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
503
583
|
function* segmentTreeWalk(
|
|
504
584
|
segments: ResolvedSegment[],
|
|
505
585
|
interceptSegments?: ResolvedSegment[],
|
|
@@ -520,19 +600,12 @@ function* segmentTreeWalk(
|
|
|
520
600
|
// Extract parent ID from parallel ID
|
|
521
601
|
// Example: "L0R1L0.@sidebar" → "L0R1L0"
|
|
522
602
|
const parentId = segment.id.split(".")[0];
|
|
523
|
-
|
|
524
|
-
parallelsByParent.set(parentId, []);
|
|
525
|
-
}
|
|
526
|
-
parallelsByParent.get(parentId)!.push(segment);
|
|
603
|
+
pushToGroup(parallelsByParent, parentId, segment);
|
|
527
604
|
} else if (segment.type === "loader") {
|
|
528
605
|
// Extract parent ID from loader ID
|
|
529
|
-
// Example: "L0D0.cart" → "L0"
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
if (!loadersByParent.has(parentId)) {
|
|
533
|
-
loadersByParent.set(parentId, []);
|
|
534
|
-
}
|
|
535
|
-
loadersByParent.get(parentId)!.push(segment);
|
|
606
|
+
// Example: "L0D0.cart" → "L0"; "L0.@DetailD0.x" → "L0.@Detail"
|
|
607
|
+
const parentId = loaderParentId(segment.id);
|
|
608
|
+
pushToGroup(loadersByParent, parentId, segment);
|
|
536
609
|
} else {
|
|
537
610
|
// Layout, route, error, and notFound segments are all rendered in the tree
|
|
538
611
|
// Error/notFound segments replace the failed segment with fallback UI
|
|
@@ -547,17 +620,11 @@ function* segmentTreeWalk(
|
|
|
547
620
|
if (intercept.type === "parallel" && intercept.slot) {
|
|
548
621
|
// Extract parent ID from intercept ID (e.g., "M4L0L0L2.@modal" → "M4L0L0L2")
|
|
549
622
|
const parentId = intercept.id.split(".")[0];
|
|
550
|
-
|
|
551
|
-
parallelsByParent.set(parentId, []);
|
|
552
|
-
}
|
|
553
|
-
parallelsByParent.get(parentId)!.push(intercept);
|
|
623
|
+
pushToGroup(parallelsByParent, parentId, intercept);
|
|
554
624
|
} else if (intercept.type === "loader") {
|
|
555
|
-
// Intercept loaders - extract parent from loader ID
|
|
556
|
-
const parentId = intercept.id
|
|
557
|
-
|
|
558
|
-
loadersByParent.set(parentId, []);
|
|
559
|
-
}
|
|
560
|
-
loadersByParent.get(parentId)!.push(intercept);
|
|
625
|
+
// Intercept loaders - extract parent from loader ID (slot name preserved)
|
|
626
|
+
const parentId = loaderParentId(intercept.id);
|
|
627
|
+
pushToGroup(loadersByParent, parentId, intercept);
|
|
561
628
|
}
|
|
562
629
|
}
|
|
563
630
|
}
|