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