@rangojs/router 0.0.0-experimental.1b930379 → 0.0.0-experimental.1c0bdfad
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 +12 -0
- package/README.md +245 -49
- package/dist/bin/rango.js +441 -134
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3453 -1240
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +76 -21
- 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 +318 -15
- package/skills/composability/SKILL.md +27 -2
- 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 +50 -6
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +279 -53
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +771 -0
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +203 -7
- package/skills/prerender/SKILL.md +153 -109
- 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 +116 -8
- 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 +129 -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 +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 +123 -0
- package/skills/typesafety/SKILL.md +332 -29
- package/skills/use-cache/SKILL.md +46 -14
- 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 +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 +197 -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 +111 -31
- package/src/browser/navigation-client.ts +201 -67
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +18 -66
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +187 -112
- package/src/browser/prefetch/cache.ts +230 -35
- package/src/browser/prefetch/fetch.ts +338 -39
- package/src/browser/prefetch/queue.ts +126 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +111 -16
- package/src/browser/react/NavigationProvider.tsx +135 -120
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- 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 +80 -5
- package/src/browser/rsc-router.tsx +218 -76
- package/src/browser/scroll-restoration.ts +54 -42
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +222 -61
- package/src/browser/types.ts +91 -11
- 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 +272 -96
- 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 +214 -48
- package/src/cache/cache-scope.ts +236 -89
- 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 +2224 -171
- 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 +11 -17
- package/src/cache/document-cache.ts +89 -27
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +11 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +31 -31
- package/src/cache/read-through-swr.ts +41 -11
- 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/debug.ts +2 -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 +56 -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 +93 -12
- package/src/index.ts +133 -15
- 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 +7 -72
- package/src/route-definition/dsl-helpers.ts +469 -276
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +113 -37
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +53 -12
- package/src/route-definition/resolve-handler-use.ts +161 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -17
- package/src/route-types.ts +37 -41
- 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 +45 -22
- package/src/router/handler-context.ts +83 -39
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +50 -24
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +274 -56
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +49 -45
- package/src/router/match-api.ts +120 -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 +33 -6
- package/src/router/match-middleware/cache-lookup.ts +214 -263
- package/src/router/match-middleware/cache-store.ts +73 -33
- package/src/router/match-middleware/intercept-resolution.ts +8 -28
- package/src/router/match-middleware/segment-resolution.ts +52 -18
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +104 -40
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +270 -172
- 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/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 +8 -28
- package/src/router/router-interfaces.ts +115 -35
- package/src/router/router-options.ts +172 -15
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +264 -77
- package/src/router/segment-resolution/helpers.ts +115 -30
- package/src/router/segment-resolution/loader-cache.ts +63 -37
- package/src/router/segment-resolution/revalidation.ts +474 -385
- 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 +5 -1
- package/src/router/segment-wrappers.ts +8 -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 +163 -55
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +658 -511
- 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 +77 -11
- 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 +105 -72
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +263 -112
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +32 -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 +281 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +309 -61
- 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 +26 -46
- package/src/server/request-context.ts +398 -172
- package/src/ssr/index.tsx +25 -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 +99 -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 +343 -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/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 +233 -81
- package/src/types/index.ts +1 -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 +19 -7
- package/src/types/segments.ts +37 -14
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +58 -11
- 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 -18
- 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 -6
- 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 +155 -65
- 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 +49 -98
- 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 +21 -25
- package/src/vite/plugins/version-plugin.ts +46 -37
- package/src/vite/plugins/virtual-entries.ts +13 -18
- package/src/vite/rango.ts +238 -295
- package/src/vite/router-discovery.ts +940 -149
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +4 -4
- 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,59 +1,75 @@
|
|
|
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 { getMemoizedContentPromise } from "./segment-content-promise.js";
|
|
14
|
+
import {
|
|
15
|
+
buildLoaderPromise,
|
|
16
|
+
getMemoizedLoaderPromise,
|
|
17
|
+
} from "./segment-loader-promise.js";
|
|
17
18
|
|
|
18
19
|
// ViewTransition is only available in React experimental.
|
|
19
20
|
// Access via namespace import to avoid compile-time errors on stable React.
|
|
20
21
|
const ReactViewTransition: any =
|
|
21
22
|
"ViewTransition" in React ? (React as any).ViewTransition : null;
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
+
|
|
33
|
+
// Exported for unit testing the no-parallel fast path (D6); internal otherwise.
|
|
34
|
+
export function restoreParallelLoaderMarkers(
|
|
35
|
+
segments: ResolvedSegment[],
|
|
36
|
+
): ResolvedSegment[] {
|
|
37
|
+
// Parallel-loading markers only exist when a parallel segment is present, so
|
|
38
|
+
// a list with no parallel slot has nothing to restore. Skip the Map alloc and
|
|
39
|
+
// full scan in that (common) case — this runs on every render.
|
|
40
|
+
if (!segments.some((s) => s.type === "parallel")) return segments;
|
|
41
|
+
|
|
42
|
+
const parallelLoadingByNamespace = new Map<string, ReactNode>();
|
|
43
|
+
let nextSegments: ResolvedSegment[] | null = null;
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < segments.length; i++) {
|
|
46
|
+
const segment = segments[i];
|
|
47
|
+
|
|
48
|
+
if (segment.type === "parallel") {
|
|
49
|
+
if (segment.namespace && isRenderableLoading(segment.loading)) {
|
|
50
|
+
parallelLoadingByNamespace.set(segment.namespace, segment.loading);
|
|
51
|
+
}
|
|
40
52
|
continue;
|
|
41
53
|
}
|
|
42
54
|
|
|
43
|
-
if (
|
|
44
|
-
loaderData[id] = result.data;
|
|
55
|
+
if (segment.type !== "loader" || segment.parallelLoading !== undefined) {
|
|
45
56
|
continue;
|
|
46
57
|
}
|
|
47
58
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
const parallelLoading = segment.namespace
|
|
60
|
+
? parallelLoadingByNamespace.get(segment.namespace)
|
|
61
|
+
: undefined;
|
|
62
|
+
if (parallelLoading === undefined) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!nextSegments) {
|
|
67
|
+
nextSegments = segments.slice();
|
|
53
68
|
}
|
|
69
|
+
nextSegments[i] = { ...segment, parallelLoading };
|
|
54
70
|
}
|
|
55
71
|
|
|
56
|
-
return
|
|
72
|
+
return nextSegments ?? segments;
|
|
57
73
|
}
|
|
58
74
|
|
|
59
75
|
/**
|
|
@@ -92,11 +108,61 @@ export interface RenderSegmentsOptions {
|
|
|
92
108
|
rootLayout?: ComponentType<RootLayoutProps>;
|
|
93
109
|
}
|
|
94
110
|
|
|
111
|
+
function createViewTransitionBoundary(
|
|
112
|
+
transition: NonNullable<ResolvedSegment["transition"]>,
|
|
113
|
+
children: ReactNode,
|
|
114
|
+
): ReactNode {
|
|
115
|
+
// `viewTransition` is a router-specific flag (boundary opt-out), not a React
|
|
116
|
+
// <ViewTransition> prop — strip it so it never reaches React.
|
|
117
|
+
const { viewTransition: _viewTransition, ...vtProps } = transition;
|
|
118
|
+
return createElement(ReactViewTransition, {
|
|
119
|
+
...vtProps,
|
|
120
|
+
children,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function wrapDefaultOutletContent(
|
|
125
|
+
content: ReactNode,
|
|
126
|
+
transition: NonNullable<ResolvedSegment["transition"]>,
|
|
127
|
+
): ReactNode {
|
|
128
|
+
if (!React.isValidElement(content)) {
|
|
129
|
+
return createViewTransitionBoundary(transition, content);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const props = content.props as any;
|
|
133
|
+
|
|
134
|
+
if (content.type === MountContextProvider) {
|
|
135
|
+
return React.cloneElement(content, {
|
|
136
|
+
children: wrapDefaultOutletContent(props.children, transition),
|
|
137
|
+
} as any);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (content.type === OutletProvider && props.segment?.type === "layout") {
|
|
141
|
+
return React.cloneElement(content, {
|
|
142
|
+
content: wrapDefaultOutletContent(props.content, transition),
|
|
143
|
+
} as any);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (content.type === LoaderBoundary && props.segment?.type === "layout") {
|
|
147
|
+
return React.cloneElement(content, {
|
|
148
|
+
outletContent: wrapDefaultOutletContent(props.outletContent, transition),
|
|
149
|
+
} as any);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return createViewTransitionBoundary(transition, content);
|
|
153
|
+
}
|
|
154
|
+
|
|
95
155
|
/**
|
|
96
156
|
* Render segments into a React tree with proper layout nesting
|
|
97
157
|
*
|
|
98
|
-
* Layouts nest using OutletProvider
|
|
99
|
-
* render as
|
|
158
|
+
* Layouts nest using OutletProvider; a layout receives the inner content via
|
|
159
|
+
* its `<Outlet />`. Parallel segments do NOT render as inline Fragment siblings
|
|
160
|
+
* — they flow through OutletContext.parallel and are resolved where a layout
|
|
161
|
+
* places `<ParallelOutlet name="@sidebar" />` (or `<Outlet name="@sidebar" />`).
|
|
162
|
+
*
|
|
163
|
+
* The result is always wrapped in RootErrorBoundary so unhandled errors never
|
|
164
|
+
* blank the screen. When `options.rootLayout` is provided it wraps the error
|
|
165
|
+
* boundary at the OUTERMOST level (so the app shell survives errors).
|
|
100
166
|
*
|
|
101
167
|
* Error segments are treated like route segments - they render their fallback
|
|
102
168
|
* component in place of the failed segment. When an error occurs in a handler,
|
|
@@ -108,27 +174,30 @@ export interface RenderSegmentsOptions {
|
|
|
108
174
|
* notFoundBoundary's fallback component.
|
|
109
175
|
*
|
|
110
176
|
* @param segments - Array of resolved segments to render
|
|
111
|
-
* @returns ReactNode
|
|
177
|
+
* @returns Promise resolving to the ReactNode tree (the function is async)
|
|
112
178
|
*
|
|
113
179
|
* @example
|
|
114
180
|
* ```typescript
|
|
115
181
|
* const segments = [
|
|
116
|
-
* { id: 'L0.0', type: 'layout', component: <
|
|
117
|
-
* { id: '
|
|
118
|
-
* { id: '
|
|
119
|
-
* { id: 'P3.0', type: 'parallel', component: <Sidebar />, slot: '@sidebar' }
|
|
182
|
+
* { id: 'L0.0', type: 'layout', component: <BlogLayout /> },
|
|
183
|
+
* { id: 'L0R1', type: 'route', component: <BlogPost /> },
|
|
184
|
+
* { id: 'L0R1.@sidebar', type: 'parallel', component: <Sidebar />, slot: '@sidebar' }
|
|
120
185
|
* ];
|
|
121
186
|
*
|
|
122
|
-
*
|
|
123
|
-
* //
|
|
124
|
-
*
|
|
125
|
-
* //
|
|
126
|
-
* //
|
|
127
|
-
* //
|
|
128
|
-
* //
|
|
187
|
+
* // BlogLayout renders <Outlet /> for the route and
|
|
188
|
+
* // <ParallelOutlet name="@sidebar" /> for the parallel slot.
|
|
189
|
+
* const tree = await renderSegments(segments, { rootLayout: RootLayout });
|
|
190
|
+
* // Results in (outermost first):
|
|
191
|
+
* // <RootLayout>
|
|
192
|
+
* // <RootErrorBoundary>
|
|
193
|
+
* // <OutletProvider segment={BlogLayout} parallel={[Sidebar]}>
|
|
194
|
+
* // <BlogPost />
|
|
195
|
+
* // </OutletProvider>
|
|
196
|
+
* // </RootErrorBoundary>
|
|
197
|
+
* // </RootLayout>
|
|
129
198
|
*
|
|
130
199
|
* // For server actions, pass isAction to await components:
|
|
131
|
-
* const tree = renderSegments(segments, { isAction: true });
|
|
200
|
+
* const tree = await renderSegments(segments, { isAction: true });
|
|
132
201
|
* ```
|
|
133
202
|
*/
|
|
134
203
|
export async function renderSegments(
|
|
@@ -143,6 +212,10 @@ export async function renderSegments(
|
|
|
143
212
|
} = options || {};
|
|
144
213
|
|
|
145
214
|
const temporalLazyRefs: Promise<any>[] = [];
|
|
215
|
+
const normalizedSegments = restoreParallelLoaderMarkers(segments);
|
|
216
|
+
const normalizedInterceptSegments = interceptSegments
|
|
217
|
+
? restoreParallelLoaderMarkers(interceptSegments)
|
|
218
|
+
: undefined;
|
|
146
219
|
|
|
147
220
|
/**
|
|
148
221
|
* Registers promises from lazy/async components for awaiting.
|
|
@@ -167,7 +240,26 @@ export async function renderSegments(
|
|
|
167
240
|
);
|
|
168
241
|
}
|
|
169
242
|
// Separate segments by type, passing intercept segments for explicit injection
|
|
170
|
-
const tree = segmentTreeWalk(
|
|
243
|
+
const tree = segmentTreeWalk(normalizedSegments, normalizedInterceptSegments);
|
|
244
|
+
|
|
245
|
+
// A route is "in a transition scope" when its own segment OR any layout in
|
|
246
|
+
// its matched chain declares transition(). Both transition() forms land here:
|
|
247
|
+
// the per-route item form sets transition on the route entry, and the block
|
|
248
|
+
// wrapper form sets it on a transparent ancestor layout (dsl-helpers.ts). When
|
|
249
|
+
// in scope, the route and its route-owned layouts use param-agnostic keys so a
|
|
250
|
+
// same-route navigation reconciles (holds content) instead of remounting. The
|
|
251
|
+
// value is a static property of the route's position in the tree, so it is the
|
|
252
|
+
// same on every render of that route (SSR, navigation, action) — the keys
|
|
253
|
+
// never drift. Cross-route navigation still remounts: different routes have
|
|
254
|
+
// different segment ids regardless of transition scope.
|
|
255
|
+
const inTransitionScope = normalizedSegments.some(
|
|
256
|
+
(s) =>
|
|
257
|
+
s.transition != null &&
|
|
258
|
+
(s.type === "layout" ||
|
|
259
|
+
s.type === "route" ||
|
|
260
|
+
s.type === "error" ||
|
|
261
|
+
s.type === "notFound"),
|
|
262
|
+
);
|
|
171
263
|
// Render content segments as siblings
|
|
172
264
|
let content: ReactNode = null;
|
|
173
265
|
for (const node of tree) {
|
|
@@ -180,17 +272,31 @@ export async function renderSegments(
|
|
|
180
272
|
);
|
|
181
273
|
const { component, id, params, loading } = node.segment;
|
|
182
274
|
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
275
|
+
// Param-agnostic keys are opt-in via the transition() DSL (see
|
|
276
|
+
// inTransitionScope above). A route (and its route-owned layouts) inside a
|
|
277
|
+
// transition scope drops the param from its key, so navigating between two
|
|
278
|
+
// param values of the SAME route (e.g. /product/1 -> /product/2) reconciles
|
|
279
|
+
// the route subtree instead of remounting it. Combined with the
|
|
280
|
+
// startTransition wrap that shouldStartViewTransition already applies to
|
|
281
|
+
// transition routes (browser/partial-update.ts), the previous content stays
|
|
282
|
+
// on screen while the new loaders resolve (stale-while-revalidate) instead
|
|
283
|
+
// of flashing the loading skeleton. This works on stable React; experimental
|
|
284
|
+
// React adds the animated <ViewTransition> cross-fade on top.
|
|
285
|
+
//
|
|
286
|
+
// Outside a transition scope the key stays param-bearing and the route
|
|
287
|
+
// remounts on param change (the default: a fresh skeleton and fresh
|
|
288
|
+
// component state).
|
|
289
|
+
//
|
|
290
|
+
// error/notFound always keep param-bearing keys: createErrorSegment reuses
|
|
291
|
+
// the boundary layout's shortCode as the error segment id (router/
|
|
292
|
+
// error-handling.ts), so a param-agnostic error key could collide with that
|
|
293
|
+
// layout's key within the same render.
|
|
189
294
|
const includeParams =
|
|
190
|
-
node.segment.type === "route" ||
|
|
191
295
|
node.segment.type === "error" ||
|
|
192
296
|
node.segment.type === "notFound" ||
|
|
193
|
-
(node.segment.type === "
|
|
297
|
+
((node.segment.type === "route" ||
|
|
298
|
+
(node.segment.type === "layout" && node.segment.belongsToRoute)) &&
|
|
299
|
+
!inTransitionScope);
|
|
194
300
|
|
|
195
301
|
const paramStr =
|
|
196
302
|
includeParams && params && Object.keys(params).length > 0
|
|
@@ -199,69 +305,70 @@ export async function renderSegments(
|
|
|
199
305
|
.map(([k, v]) => `${k}=${v}`)
|
|
200
306
|
.join(",")
|
|
201
307
|
: "";
|
|
202
|
-
const key =
|
|
308
|
+
const key = paramStr ? `${id}-${paramStr}` : id;
|
|
203
309
|
|
|
204
|
-
// Get loader entries for this node
|
|
205
310
|
const loaderEntries = node.loaders.filter(
|
|
206
311
|
(loader) => loader.loaderId && loader.loaderData !== undefined,
|
|
207
312
|
);
|
|
208
313
|
|
|
209
|
-
// Determine the component content (with or without Suspense wrapper)
|
|
210
|
-
// Wrap when loading skeleton defined OR component is Promise (needs Suspense)
|
|
211
|
-
// During actions, await component Promise to prevent Suspense from triggering
|
|
212
|
-
// This keeps existing content visible instead of showing loading skeleton
|
|
213
314
|
let resolvedComponent = component;
|
|
214
315
|
if (isAction && component instanceof Promise) {
|
|
215
316
|
resolvedComponent = await component;
|
|
216
317
|
}
|
|
217
318
|
|
|
218
|
-
let nodeContent: ReactNode =
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
fallback: loading,
|
|
227
|
-
segmentId: id,
|
|
228
|
-
})
|
|
229
|
-
: registerLazyRef(resolvedComponent);
|
|
319
|
+
let nodeContent: ReactNode = isRenderableLoading(loading)
|
|
320
|
+
? createElement(RouteContentWrapper, {
|
|
321
|
+
key: `suspense-loading-${id}`,
|
|
322
|
+
content: getMemoizedContentPromise(resolvedComponent),
|
|
323
|
+
fallback: loading,
|
|
324
|
+
segmentId: id,
|
|
325
|
+
})
|
|
326
|
+
: registerLazyRef(resolvedComponent);
|
|
230
327
|
|
|
231
328
|
// Wrap with <ViewTransition> if transition config exists (React experimental only).
|
|
232
329
|
// An empty config ({}) creates a bare <ViewTransition> boundary that participates
|
|
233
330
|
// in transitions without adding custom animation classes. Named element-level
|
|
234
331
|
// <ViewTransition> components inside (with name/share props) morph independently
|
|
235
332
|
// from the parent's default cross-fade.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
//
|
|
244
|
-
|
|
333
|
+
//
|
|
334
|
+
// For layouts, wrap the outlet content (what `<Outlet />` renders) rather
|
|
335
|
+
// than the layout component itself. Parallel slots like `<ParallelOutlet
|
|
336
|
+
// name="@modal" />` read from a separate context channel and end up as
|
|
337
|
+
// siblings of the VT in the rendered tree, so modal mounts don't trigger a
|
|
338
|
+
// subtree update on the layout-level VT — which would otherwise make
|
|
339
|
+
// React's commit walker fire `document.startViewTransition` and apply
|
|
340
|
+
// view-transition-names to the underlying main subtree (cover/title/etc.).
|
|
341
|
+
//
|
|
342
|
+
// `transition.viewTransition === false` opts out of the router-owned
|
|
343
|
+
// boundary only. Driving (the startTransition wrap in browser/partial-update.ts
|
|
344
|
+
// and the param-agnostic key/hold below) keys off transition *presence*, not
|
|
345
|
+
// this flag, so a boundary-less transition still holds content and lets
|
|
346
|
+
// consumer-placed <ViewTransition> elements animate. The global
|
|
347
|
+
// createRouter({ viewTransition }) default is resolved into this field
|
|
348
|
+
// during segment resolution (only `false` is stamped; unset/"auto" is left
|
|
349
|
+
// as-is and means "wrap"), so this gate needs no router-option threading.
|
|
350
|
+
let outletContent: ReactNode =
|
|
245
351
|
node.segment.type === "layout" ? content : null;
|
|
246
352
|
|
|
353
|
+
const transition = node.segment.transition;
|
|
354
|
+
|
|
355
|
+
if (
|
|
356
|
+
ReactViewTransition &&
|
|
357
|
+
transition &&
|
|
358
|
+
transition.viewTransition !== false
|
|
359
|
+
) {
|
|
360
|
+
if (node.segment.type === "layout") {
|
|
361
|
+
outletContent = wrapDefaultOutletContent(outletContent, transition);
|
|
362
|
+
} else {
|
|
363
|
+
nodeContent = createViewTransitionBoundary(transition, nodeContent);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
247
367
|
// Prepare loader data if there are loaders
|
|
248
368
|
const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
|
|
249
|
-
|
|
250
|
-
loaderEntries.length > 0
|
|
251
|
-
? Promise.all(
|
|
252
|
-
loaderEntries.map((loader) =>
|
|
253
|
-
loader.loaderData instanceof Promise
|
|
254
|
-
? loader.loaderData
|
|
255
|
-
: Promise.resolve(loader.loaderData),
|
|
256
|
-
),
|
|
257
|
-
)
|
|
258
|
-
: Promise.resolve([]);
|
|
259
|
-
|
|
260
|
-
// Use LoaderBoundary when loading is defined to maintain consistent tree structure
|
|
261
|
-
// This ensures cached segments (which may not have loader segments) have the same
|
|
262
|
-
// tree structure as fresh segments, preventing React remounts
|
|
263
|
-
// If forceAwait or isAction is set, pre-resolve promises so LoaderBoundary won't suspend
|
|
369
|
+
|
|
264
370
|
if (loading !== undefined && loading !== null) {
|
|
371
|
+
const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
|
|
265
372
|
content = createElement(LoaderBoundary, {
|
|
266
373
|
key: `loader-boundary-${key}`,
|
|
267
374
|
loaderDataPromise:
|
|
@@ -275,7 +382,6 @@ export async function renderSegments(
|
|
|
275
382
|
children: nodeContent,
|
|
276
383
|
});
|
|
277
384
|
} else if (loaderEntries.length === 0) {
|
|
278
|
-
// No loaders, no loading - simple OutletProvider
|
|
279
385
|
content = createElement(OutletProvider, {
|
|
280
386
|
key,
|
|
281
387
|
content: outletContent,
|
|
@@ -284,13 +390,52 @@ export async function renderSegments(
|
|
|
284
390
|
children: nodeContent,
|
|
285
391
|
});
|
|
286
392
|
} else {
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
393
|
+
const layoutLoaders = loaderEntries.filter((l) => !l.parallelLoading);
|
|
394
|
+
const parallelOwnedLoaders = loaderEntries.filter(
|
|
395
|
+
(l) => !!l.parallelLoading,
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
const layoutLoaderIds = layoutLoaders.map((l) => l.loaderId!);
|
|
399
|
+
const resolvedData = await buildLoaderPromise(layoutLoaders);
|
|
400
|
+
const { loaderData, errorFallback } = decodeLoaderResults(
|
|
290
401
|
resolvedData,
|
|
291
|
-
|
|
402
|
+
layoutLoaderIds,
|
|
292
403
|
);
|
|
293
404
|
|
|
405
|
+
if (parallelOwnedLoaders.length > 0) {
|
|
406
|
+
const loadersByParallelNamespace = new Map<string, ResolvedSegment[]>();
|
|
407
|
+
|
|
408
|
+
for (const loader of parallelOwnedLoaders) {
|
|
409
|
+
if (!loader.namespace) {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const existing = loadersByParallelNamespace.get(loader.namespace);
|
|
413
|
+
if (existing) {
|
|
414
|
+
existing.push(loader);
|
|
415
|
+
} else {
|
|
416
|
+
loadersByParallelNamespace.set(loader.namespace, [loader]);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
for (const p of node.parallel) {
|
|
421
|
+
if (!p.loading || !p.namespace) {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const ownedLoaders = loadersByParallelNamespace.get(p.namespace);
|
|
426
|
+
if (!ownedLoaders || ownedLoaders.length === 0) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
p.loaderIds = ownedLoaders.map((l) => l.loaderId!);
|
|
431
|
+
const aggregated = getMemoizedLoaderPromise(ownedLoaders);
|
|
432
|
+
p.loaderDataPromise =
|
|
433
|
+
(forceAwait || isAction) && aggregated instanceof Promise
|
|
434
|
+
? await aggregated
|
|
435
|
+
: aggregated;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
294
439
|
content = createElement(OutletProvider, {
|
|
295
440
|
key,
|
|
296
441
|
content: outletContent,
|
|
@@ -313,8 +458,6 @@ export async function renderSegments(
|
|
|
313
458
|
}
|
|
314
459
|
}
|
|
315
460
|
|
|
316
|
-
// Always wrap with root error boundary to prevent white screens
|
|
317
|
-
// This catches any unhandled errors that bubble up from the segment tree
|
|
318
461
|
const errorBoundaryWrapped = createElement(RootErrorBoundary, {
|
|
319
462
|
children: content,
|
|
320
463
|
});
|
|
@@ -322,11 +465,8 @@ export async function renderSegments(
|
|
|
322
465
|
await Promise.allSettled(temporalLazyRefs);
|
|
323
466
|
}
|
|
324
467
|
|
|
325
|
-
// Build the final result, optionally wrapped with root layout
|
|
326
468
|
let result: ReactNode = errorBoundaryWrapped;
|
|
327
469
|
|
|
328
|
-
// If rootLayout is provided, wrap the error boundary with it
|
|
329
|
-
// This ensures the app shell stays mounted even during errors (prevents FOUC)
|
|
330
470
|
if (RootLayout) {
|
|
331
471
|
result = createElement(RootLayout, {
|
|
332
472
|
children: errorBoundaryWrapped,
|
|
@@ -364,6 +504,31 @@ export async function renderSegments(
|
|
|
364
504
|
* @param segments - Main segments from the route tree
|
|
365
505
|
* @param interceptSegments - Optional intercept segments to inject
|
|
366
506
|
*/
|
|
507
|
+
// Loader segment ids have the grammar `${parentId}D${index}.${loaderId}`.
|
|
508
|
+
// parentId is the parent shortCode (M/L/P/R/C + digits, never "D") for normal
|
|
509
|
+
// loaders, or `${shortCode}.${slotName}` for intercept-slot loaders, where the
|
|
510
|
+
// slot name is user-controlled (`@${string}`) and may contain an uppercase "D"
|
|
511
|
+
// (e.g. "@Detail"). Strip from the first `D<index>.` separator so the slot name
|
|
512
|
+
// is preserved; splitting on a bare "D" mis-cut "@Detail" to "@" and silently
|
|
513
|
+
// dropped the loader's data. The first-`D<index>.` strip is only correct because
|
|
514
|
+
// slot names cannot contain "." -- assertValidSlotName (route-definition/
|
|
515
|
+
// dsl-helpers.ts) rejects a "." at definition time, so a name like "@D3.foo"
|
|
516
|
+
// (which WOULD mis-cut here) can never reach this function.
|
|
517
|
+
function loaderParentId(loaderSegmentId: string): string {
|
|
518
|
+
return loaderSegmentId.replace(/D\d+\..*$/, "");
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Append a value to the array stored under `key`, creating the array on first
|
|
522
|
+
// use. Single Map lookup (vs the has/get!().push double-lookup idiom).
|
|
523
|
+
function pushToGroup<K, V>(map: Map<K, V[]>, key: K, value: V): void {
|
|
524
|
+
const arr = map.get(key);
|
|
525
|
+
if (arr) {
|
|
526
|
+
arr.push(value);
|
|
527
|
+
} else {
|
|
528
|
+
map.set(key, [value]);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
367
532
|
function* segmentTreeWalk(
|
|
368
533
|
segments: ResolvedSegment[],
|
|
369
534
|
interceptSegments?: ResolvedSegment[],
|
|
@@ -384,19 +549,12 @@ function* segmentTreeWalk(
|
|
|
384
549
|
// Extract parent ID from parallel ID
|
|
385
550
|
// Example: "L0R1L0.@sidebar" → "L0R1L0"
|
|
386
551
|
const parentId = segment.id.split(".")[0];
|
|
387
|
-
|
|
388
|
-
parallelsByParent.set(parentId, []);
|
|
389
|
-
}
|
|
390
|
-
parallelsByParent.get(parentId)!.push(segment);
|
|
552
|
+
pushToGroup(parallelsByParent, parentId, segment);
|
|
391
553
|
} else if (segment.type === "loader") {
|
|
392
554
|
// Extract parent ID from loader ID
|
|
393
|
-
// Example: "L0D0.cart" → "L0"
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if (!loadersByParent.has(parentId)) {
|
|
397
|
-
loadersByParent.set(parentId, []);
|
|
398
|
-
}
|
|
399
|
-
loadersByParent.get(parentId)!.push(segment);
|
|
555
|
+
// Example: "L0D0.cart" → "L0"; "L0.@DetailD0.x" → "L0.@Detail"
|
|
556
|
+
const parentId = loaderParentId(segment.id);
|
|
557
|
+
pushToGroup(loadersByParent, parentId, segment);
|
|
400
558
|
} else {
|
|
401
559
|
// Layout, route, error, and notFound segments are all rendered in the tree
|
|
402
560
|
// Error/notFound segments replace the failed segment with fallback UI
|
|
@@ -411,17 +569,11 @@ function* segmentTreeWalk(
|
|
|
411
569
|
if (intercept.type === "parallel" && intercept.slot) {
|
|
412
570
|
// Extract parent ID from intercept ID (e.g., "M4L0L0L2.@modal" → "M4L0L0L2")
|
|
413
571
|
const parentId = intercept.id.split(".")[0];
|
|
414
|
-
|
|
415
|
-
parallelsByParent.set(parentId, []);
|
|
416
|
-
}
|
|
417
|
-
parallelsByParent.get(parentId)!.push(intercept);
|
|
572
|
+
pushToGroup(parallelsByParent, parentId, intercept);
|
|
418
573
|
} else if (intercept.type === "loader") {
|
|
419
|
-
// Intercept loaders - extract parent from loader ID
|
|
420
|
-
const parentId = intercept.id
|
|
421
|
-
|
|
422
|
-
loadersByParent.set(parentId, []);
|
|
423
|
-
}
|
|
424
|
-
loadersByParent.get(parentId)!.push(intercept);
|
|
574
|
+
// Intercept loaders - extract parent from loader ID (slot name preserved)
|
|
575
|
+
const parentId = loaderParentId(intercept.id);
|
|
576
|
+
pushToGroup(loadersByParent, parentId, intercept);
|
|
425
577
|
}
|
|
426
578
|
}
|
|
427
579
|
}
|