@rangojs/router 0.0.0-experimental.bd6e11bc → 0.0.0-experimental.bdaf10aa
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 +296 -887
- package/dist/bin/rango.js +459 -91
- package/dist/testing/vitest.js +36 -2
- package/dist/vite/index.js +1708 -414
- package/package.json +35 -10
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +82 -5
- package/skills/bundle-analysis/SKILL.md +2 -2
- package/skills/cache-guide/SKILL.md +14 -9
- package/skills/caching/SKILL.md +221 -12
- 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 +83 -2
- 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 +11 -3
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +9 -9
- package/skills/hooks/SKILL.md +73 -900
- 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 +84 -7
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +51 -17
- package/skills/layout/SKILL.md +38 -16
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +48 -20
- package/skills/middleware/SKILL.md +11 -5
- package/skills/migrate-nextjs/SKILL.md +203 -20
- package/skills/migrate-react-router/SKILL.md +59 -675
- 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 +3 -3
- package/skills/observability/SKILL.md +70 -5
- package/skills/parallel/SKILL.md +32 -8
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +59 -28
- package/skills/rango/SKILL.md +124 -50
- package/skills/response-routes/SKILL.md +78 -46
- package/skills/route/SKILL.md +85 -6
- package/skills/router-setup/SKILL.md +41 -6
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +28 -3
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +28 -4
- package/skills/testing/SKILL.md +68 -654
- 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 -918
- 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 +47 -17
- package/skills/vercel/SKILL.md +128 -0
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/__augment-tests__/augmented.check.ts +2 -3
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +14 -27
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +178 -100
- 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 +81 -68
- package/src/browser/navigation-client.ts +115 -70
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +153 -88
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +157 -144
- package/src/browser/prefetch/cache.ts +148 -81
- package/src/browser/prefetch/fetch.ts +231 -51
- package/src/browser/prefetch/queue.ts +25 -7
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +40 -7
- package/src/browser/react/NavigationProvider.tsx +140 -99
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +17 -2
- package/src/browser/react/index.ts +0 -51
- package/src/browser/react/location-state-shared.ts +14 -15
- package/src/browser/react/location-state.ts +0 -1
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +0 -5
- 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 +10 -5
- package/src/browser/react/use-params.ts +0 -2
- package/src/browser/react/use-router.ts +6 -4
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/response-adapter.ts +74 -8
- package/src/browser/rsc-router.tsx +97 -22
- package/src/browser/scroll-restoration.ts +15 -8
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +216 -38
- package/src/browser/types.ts +94 -22
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/generate-route-types.ts +1 -1
- package/src/build/index.ts +11 -5
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +152 -22
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +12 -1
- package/src/build/route-types/include-resolution.ts +455 -61
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +15 -2
- package/src/build/route-types/router-processing.ts +77 -41
- package/src/build/route-types/source-scan.ts +105 -7
- package/src/build/runtime-discovery.ts +4 -1
- 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 +454 -101
- package/src/cache/cache-scope.ts +159 -54
- 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 +2170 -377
- 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 +194 -99
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +1132 -0
- package/src/client.rsc.tsx +39 -22
- package/src/client.tsx +28 -58
- 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 +13 -1
- package/src/decode-loader-results.ts +18 -2
- package/src/defer.ts +185 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +0 -3
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +57 -40
- 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 +7 -0
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +7 -12
- package/src/index.rsc.ts +88 -8
- package/src/index.ts +90 -16
- package/src/internal-debug.ts +11 -10
- package/src/loader.rsc.ts +19 -9
- package/src/loader.ts +12 -4
- 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 +75 -7
- 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 +25 -0
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +13 -49
- package/src/route-definition/dsl-helpers.ts +60 -53
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +46 -46
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +6 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -20
- package/src/route-types.ts +0 -5
- package/src/router/content-negotiation.ts +58 -23
- package/src/router/error-handling.ts +44 -17
- package/src/router/find-match.ts +129 -30
- package/src/router/handler-context.ts +6 -1
- package/src/router/instrument.ts +355 -0
- package/src/router/intercept-resolution.ts +35 -2
- package/src/router/lazy-includes.ts +79 -56
- package/src/router/loader-resolution.ts +151 -73
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +74 -40
- package/src/router/match-api.ts +76 -52
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +181 -178
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +115 -194
- package/src/router/match-middleware/cache-store.ts +61 -50
- 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 +36 -67
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +0 -116
- package/src/router/middleware.ts +231 -120
- 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 +99 -152
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +111 -66
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +47 -42
- package/src/router/revalidation.ts +75 -81
- package/src/router/route-snapshot.ts +14 -3
- package/src/router/router-context.ts +6 -29
- package/src/router/router-interfaces.ts +70 -8
- package/src/router/router-options.ts +126 -4
- package/src/router/segment-resolution/fresh.ts +104 -80
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +155 -39
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +215 -304
- 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 +35 -15
- 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 +54 -35
- package/src/router/telemetry-otel.ts +160 -200
- package/src/router/telemetry.ts +9 -23
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +215 -0
- package/src/router/trie-matching.ts +171 -64
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +13 -5
- package/src/router.ts +119 -48
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +267 -152
- package/src/rsc/helpers.ts +78 -4
- package/src/rsc/index.ts +1 -4
- 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 +11 -15
- package/src/rsc/progressive-enhancement.ts +120 -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 +58 -141
- package/src/rsc/rsc-rendering.ts +492 -49
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +268 -82
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +45 -3
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +31 -26
- package/src/segment-loader-promise.ts +49 -4
- package/src/segment-system.tsx +260 -95
- package/src/server/context.ts +99 -9
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +125 -2
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +379 -138
- package/src/ssr/index.tsx +491 -182
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +44 -48
- package/src/testing/collect-handle.ts +14 -31
- package/src/testing/dispatch.ts +533 -160
- package/src/testing/e2e/fixture.ts +45 -11
- package/src/testing/e2e/index.ts +1 -22
- package/src/testing/e2e/matchers.ts +0 -16
- package/src/testing/e2e/parity.ts +85 -4
- package/src/testing/e2e/server.ts +12 -0
- package/src/testing/flight-matchers.ts +7 -14
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +36 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +30 -0
- package/src/testing/flight.ts +145 -70
- package/src/testing/generated-routes.ts +26 -50
- package/src/testing/index.ts +18 -19
- package/src/testing/internal/context.ts +184 -68
- 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 +134 -115
- package/src/testing/run-loader.ts +140 -51
- package/src/testing/run-middleware.ts +59 -33
- package/src/testing/run-transition-when.ts +164 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
- package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
- package/src/testing/vitest.ts +138 -16
- 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 +15 -15
- package/src/types/handler-context.ts +45 -15
- package/src/types/index.ts +2 -10
- package/src/types/loader-types.ts +6 -3
- package/src/types/request-scope.ts +8 -22
- package/src/types/route-config.ts +20 -52
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +100 -13
- package/src/urls/include-helper.ts +10 -12
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +2 -8
- package/src/urls/path-helper-types.ts +52 -14
- package/src/urls/path-helper.ts +5 -54
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +76 -42
- package/src/urls/urls-function.ts +0 -14
- package/src/use-loader.tsx +0 -186
- package/src/vercel/index.ts +11 -0
- package/src/vercel/tracing.ts +88 -0
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +34 -43
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/discovery/prerender-collection.ts +33 -46
- package/src/vite/discovery/state.ts +12 -1
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/index.ts +9 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugin-types.ts +143 -10
- package/src/vite/plugins/cjs-to-esm.ts +8 -12
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +0 -10
- package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
- package/src/vite/plugins/expose-action-id.ts +2 -73
- package/src/vite/plugins/expose-id-utils.ts +85 -56
- package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
- package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
- package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
- package/src/vite/plugins/expose-internal-ids.ts +10 -1
- package/src/vite/plugins/performance-tracks.ts +0 -3
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +21 -46
- package/src/vite/plugins/vercel-output.ts +384 -0
- package/src/vite/plugins/version-injector.ts +22 -27
- package/src/vite/plugins/version-plugin.ts +6 -66
- package/src/vite/plugins/virtual-entries.ts +137 -26
- package/src/vite/rango.ts +146 -135
- package/src/vite/router-discovery.ts +189 -48
- package/src/vite/utils/ast-handler-extract.ts +11 -20
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +4 -75
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +71 -44
- package/src/vite/utils/shared-utils.ts +55 -37
- 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,6 +1,6 @@
|
|
|
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
6
|
import { decodeLoaderResults } from "./decode-loader-results.js";
|
|
@@ -10,17 +10,58 @@ import {
|
|
|
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
|
+
* Debug log for the segment tree build, gated on the baked flag. Runs on BOTH
|
|
22
|
+
* sides now, environment-tagged: `[Browser][segments]` lines up with the
|
|
23
|
+
* `[Browser][boot]` sequence around hydrateRoot; `[Server][segments]` exposes
|
|
24
|
+
* the SSR/RSC tree-build stalls (blocking loader awaits during fizz are what
|
|
25
|
+
* dominate MISS TTFB) that used to be invisible because the logs were
|
|
26
|
+
* window-gated. Server lines have no request correlation — segment-system is
|
|
27
|
+
* shared client code and cannot import request-context (node:async_hooks
|
|
28
|
+
* would enter the browser bundle) — so on a busy server, correlate by
|
|
29
|
+
* timestamp + segment ids.
|
|
30
|
+
*/
|
|
31
|
+
function segDebugLog(msg: string, details?: Record<string, unknown>): void {
|
|
32
|
+
if (!INTERNAL_RANGO_DEBUG) return;
|
|
33
|
+
const env = typeof window === "object" ? "[Browser]" : "[Server]";
|
|
34
|
+
const prefix = `${env}[segments] ${msg} @ ${Math.round(performance.now())}ms`;
|
|
35
|
+
if (details) {
|
|
36
|
+
console.log(prefix, details);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
console.log(prefix);
|
|
40
|
+
}
|
|
15
41
|
|
|
16
42
|
// ViewTransition is only available in React experimental.
|
|
17
43
|
// Access via namespace import to avoid compile-time errors on stable React.
|
|
18
44
|
const ReactViewTransition: any =
|
|
19
45
|
"ViewTransition" in React ? (React as any).ViewTransition : null;
|
|
20
46
|
|
|
21
|
-
|
|
47
|
+
// A loading skeleton is renderable only when it is a real ReactNode value.
|
|
48
|
+
// `false` is treated as "not renderable" here. This is the three-term gate;
|
|
49
|
+
// the distinct two-term gate at the LoaderBoundary site deliberately treats
|
|
50
|
+
// `false` as "create a boundary without a RouteContentWrapper"
|
|
51
|
+
// (tree-structure.md), so it must NOT use this helper.
|
|
52
|
+
function isRenderableLoading(loading: ReactNode): boolean {
|
|
53
|
+
return loading !== undefined && loading !== null && loading !== false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Exported for unit testing the no-parallel fast path (D6); internal otherwise.
|
|
57
|
+
export function restoreParallelLoaderMarkers(
|
|
22
58
|
segments: ResolvedSegment[],
|
|
23
59
|
): ResolvedSegment[] {
|
|
60
|
+
// Parallel-loading markers only exist when a parallel segment is present, so
|
|
61
|
+
// a list with no parallel slot has nothing to restore. Skip the Map alloc and
|
|
62
|
+
// full scan in that (common) case — this runs on every render.
|
|
63
|
+
if (!segments.some((s) => s.type === "parallel")) return segments;
|
|
64
|
+
|
|
24
65
|
const parallelLoadingByNamespace = new Map<string, ReactNode>();
|
|
25
66
|
let nextSegments: ResolvedSegment[] | null = null;
|
|
26
67
|
|
|
@@ -28,12 +69,7 @@ function restoreParallelLoaderMarkers(
|
|
|
28
69
|
const segment = segments[i];
|
|
29
70
|
|
|
30
71
|
if (segment.type === "parallel") {
|
|
31
|
-
if (
|
|
32
|
-
segment.namespace &&
|
|
33
|
-
segment.loading !== undefined &&
|
|
34
|
-
segment.loading !== null &&
|
|
35
|
-
segment.loading !== false
|
|
36
|
-
) {
|
|
72
|
+
if (segment.namespace && isRenderableLoading(segment.loading)) {
|
|
37
73
|
parallelLoadingByNamespace.set(segment.namespace, segment.loading);
|
|
38
74
|
}
|
|
39
75
|
continue;
|
|
@@ -142,8 +178,14 @@ function wrapDefaultOutletContent(
|
|
|
142
178
|
/**
|
|
143
179
|
* Render segments into a React tree with proper layout nesting
|
|
144
180
|
*
|
|
145
|
-
* Layouts nest using OutletProvider
|
|
146
|
-
* render as
|
|
181
|
+
* Layouts nest using OutletProvider; a layout receives the inner content via
|
|
182
|
+
* its `<Outlet />`. Parallel segments do NOT render as inline Fragment siblings
|
|
183
|
+
* — they flow through OutletContext.parallel and are resolved where a layout
|
|
184
|
+
* places `<ParallelOutlet name="@sidebar" />` (or `<Outlet name="@sidebar" />`).
|
|
185
|
+
*
|
|
186
|
+
* The result is always wrapped in RootErrorBoundary so unhandled errors never
|
|
187
|
+
* blank the screen. When `options.rootLayout` is provided it wraps the error
|
|
188
|
+
* boundary at the OUTERMOST level (so the app shell survives errors).
|
|
147
189
|
*
|
|
148
190
|
* Error segments are treated like route segments - they render their fallback
|
|
149
191
|
* component in place of the failed segment. When an error occurs in a handler,
|
|
@@ -155,27 +197,30 @@ function wrapDefaultOutletContent(
|
|
|
155
197
|
* notFoundBoundary's fallback component.
|
|
156
198
|
*
|
|
157
199
|
* @param segments - Array of resolved segments to render
|
|
158
|
-
* @returns ReactNode
|
|
200
|
+
* @returns Promise resolving to the ReactNode tree (the function is async)
|
|
159
201
|
*
|
|
160
202
|
* @example
|
|
161
203
|
* ```typescript
|
|
162
204
|
* const segments = [
|
|
163
|
-
* { id: 'L0.0', type: 'layout', component: <
|
|
164
|
-
* { id: '
|
|
165
|
-
* { id: '
|
|
166
|
-
* { id: 'P3.0', type: 'parallel', component: <Sidebar />, slot: '@sidebar' }
|
|
205
|
+
* { id: 'L0.0', type: 'layout', component: <BlogLayout /> },
|
|
206
|
+
* { id: 'L0R1', type: 'route', component: <BlogPost /> },
|
|
207
|
+
* { id: 'L0R1.@sidebar', type: 'parallel', component: <Sidebar />, slot: '@sidebar' }
|
|
167
208
|
* ];
|
|
168
209
|
*
|
|
169
|
-
*
|
|
170
|
-
* //
|
|
171
|
-
*
|
|
172
|
-
* //
|
|
173
|
-
* //
|
|
174
|
-
* //
|
|
175
|
-
* //
|
|
210
|
+
* // BlogLayout renders <Outlet /> for the route and
|
|
211
|
+
* // <ParallelOutlet name="@sidebar" /> for the parallel slot.
|
|
212
|
+
* const tree = await renderSegments(segments, { rootLayout: RootLayout });
|
|
213
|
+
* // Results in (outermost first):
|
|
214
|
+
* // <RootLayout>
|
|
215
|
+
* // <RootErrorBoundary>
|
|
216
|
+
* // <OutletProvider segment={BlogLayout} parallel={[Sidebar]}>
|
|
217
|
+
* // <BlogPost />
|
|
218
|
+
* // </OutletProvider>
|
|
219
|
+
* // </RootErrorBoundary>
|
|
220
|
+
* // </RootLayout>
|
|
176
221
|
*
|
|
177
222
|
* // For server actions, pass isAction to await components:
|
|
178
|
-
* const tree = renderSegments(segments, { isAction: true });
|
|
223
|
+
* const tree = await renderSegments(segments, { isAction: true });
|
|
179
224
|
* ```
|
|
180
225
|
*/
|
|
181
226
|
export async function renderSegments(
|
|
@@ -189,6 +234,17 @@ export async function renderSegments(
|
|
|
189
234
|
rootLayout: RootLayout,
|
|
190
235
|
} = options || {};
|
|
191
236
|
|
|
237
|
+
const segDebug = INTERNAL_RANGO_DEBUG;
|
|
238
|
+
const segDebugStart = segDebug ? performance.now() : 0;
|
|
239
|
+
if (segDebug) {
|
|
240
|
+
segDebugLog("renderSegments start", {
|
|
241
|
+
segments: segments.map((s) => `${s.id}:${s.type}`),
|
|
242
|
+
isAction: !!isAction,
|
|
243
|
+
forceAwait: !!forceAwait,
|
|
244
|
+
intercepts: interceptSegments?.length ?? 0,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
192
248
|
const temporalLazyRefs: Promise<any>[] = [];
|
|
193
249
|
const normalizedSegments = restoreParallelLoaderMarkers(segments);
|
|
194
250
|
const normalizedInterceptSegments = interceptSegments
|
|
@@ -249,6 +305,7 @@ export async function renderSegments(
|
|
|
249
305
|
`Expected layout, route, error, or notFound segment, got ${node.segment.type}`,
|
|
250
306
|
);
|
|
251
307
|
const { component, id, params, loading } = node.segment;
|
|
308
|
+
const segNodeStart = segDebug ? performance.now() : 0;
|
|
252
309
|
|
|
253
310
|
// Param-agnostic keys are opt-in via the transition() DSL (see
|
|
254
311
|
// inTransitionScope above). A route (and its route-owned layouts) inside a
|
|
@@ -283,31 +340,71 @@ export async function renderSegments(
|
|
|
283
340
|
.map(([k, v]) => `${k}=${v}`)
|
|
284
341
|
.join(",")
|
|
285
342
|
: "";
|
|
286
|
-
const key =
|
|
343
|
+
const key = paramStr ? `${id}-${paramStr}` : id;
|
|
287
344
|
|
|
288
|
-
// Get loader entries for this node
|
|
289
345
|
const loaderEntries = node.loaders.filter(
|
|
290
346
|
(loader) => loader.loaderId && loader.loaderData !== undefined,
|
|
291
347
|
);
|
|
292
348
|
|
|
293
|
-
// Determine the component content (with or without Suspense wrapper)
|
|
294
|
-
// Wrap when loading skeleton defined OR component is Promise (needs Suspense)
|
|
295
|
-
// During actions, await component Promise to prevent Suspense from triggering
|
|
296
|
-
// This keeps existing content visible instead of showing loading skeleton
|
|
297
349
|
let resolvedComponent = component;
|
|
298
350
|
if (isAction && component instanceof Promise) {
|
|
351
|
+
const componentAwaitStart = segDebug ? performance.now() : 0;
|
|
299
352
|
resolvedComponent = await component;
|
|
353
|
+
if (segDebug) {
|
|
354
|
+
segDebugLog(`segment ${id}: component awaited (action)`, {
|
|
355
|
+
ms: Math.round(performance.now() - componentAwaitStart),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
300
358
|
}
|
|
301
359
|
|
|
302
|
-
let nodeContent: ReactNode =
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
360
|
+
let nodeContent: ReactNode = null;
|
|
361
|
+
if (isRenderableLoading(loading)) {
|
|
362
|
+
// forceAwait (popstate, stale-revalidation, fully-prefetched nav) renders a
|
|
363
|
+
// loading() route with the route content ALREADY resolved, so its
|
|
364
|
+
// RouteContentWrapper Suspender does not suspend for a microtask and flash
|
|
365
|
+
// the loading() fallback on a NORMAL (non-transition) commit. The router
|
|
366
|
+
// data is known-ready on these paths, so awaiting the content here is free.
|
|
367
|
+
// The wrapper tree is unchanged (RouteContentWrapper is still created with
|
|
368
|
+
// the same key/fallback) — only the `content` prop is a resolved node
|
|
369
|
+
// instead of a pending promise, which Suspender renders synchronously. This
|
|
370
|
+
// mirrors the forceAwait loaderData unwrap above; a CLIENT component that
|
|
371
|
+
// suspends on mount inside the content still reveals a fallback (it is not
|
|
372
|
+
// pre-resolved).
|
|
373
|
+
const contentPromise = getMemoizedContentPromise(resolvedComponent);
|
|
374
|
+
let loadingContent: Promise<ReactNode> | ReactNode = contentPromise;
|
|
375
|
+
if (forceAwait) {
|
|
376
|
+
const contentAwaitStart = segDebug ? performance.now() : 0;
|
|
377
|
+
loadingContent = await contentPromise;
|
|
378
|
+
if (segDebug) {
|
|
379
|
+
segDebugLog(`segment ${id}: content awaited (forceAwait)`, {
|
|
380
|
+
ms: Math.round(performance.now() - contentAwaitStart),
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
nodeContent = createElement(RouteContentWrapper, {
|
|
385
|
+
key: `suspense-loading-${id}`,
|
|
386
|
+
content: loadingContent,
|
|
387
|
+
fallback: loading,
|
|
388
|
+
segmentId: id,
|
|
389
|
+
});
|
|
390
|
+
} else {
|
|
391
|
+
// [VT-DIAG] Gated behind INTERNAL_RANGO_DEBUG. A segment in the no-loading()
|
|
392
|
+
// branch whose component decodes as a Promise/lazy gets registered into
|
|
393
|
+
// temporalLazyRefs and awaited before commit (see below) — which on builds
|
|
394
|
+
// where the segment component arrives deferred defeats client-nav streaming.
|
|
395
|
+
if (INTERNAL_RANGO_DEBUG && typeof window === "object") {
|
|
396
|
+
const c = resolvedComponent as unknown;
|
|
397
|
+
console.log("[VT-DIAG] renderSegments no-loading-branch segment", {
|
|
398
|
+
id,
|
|
399
|
+
type: node.segment.type,
|
|
400
|
+
componentIsPromise: c instanceof Promise,
|
|
401
|
+
componentIsLazy:
|
|
402
|
+
c != null && typeof c === "object" && "_payload" in c,
|
|
403
|
+
componentTypeof: typeof c,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
nodeContent = registerLazyRef(resolvedComponent);
|
|
407
|
+
}
|
|
311
408
|
|
|
312
409
|
// Wrap with <ViewTransition> if transition config exists (React experimental only).
|
|
313
410
|
// An empty config ({}) creates a bare <ViewTransition> boundary that participates
|
|
@@ -351,18 +448,27 @@ export async function renderSegments(
|
|
|
351
448
|
// Prepare loader data if there are loaders
|
|
352
449
|
const loaderIds = loaderEntries.map((loader) => loader.loaderId!);
|
|
353
450
|
|
|
354
|
-
// Use LoaderBoundary when loading is defined to maintain consistent tree structure
|
|
355
|
-
// This ensures cached segments (which may not have loader segments) have the same
|
|
356
|
-
// tree structure as fresh segments, preventing React remounts
|
|
357
|
-
// If forceAwait or isAction is set, pre-resolve promises so LoaderBoundary won't suspend
|
|
358
451
|
if (loading !== undefined && loading !== null) {
|
|
359
|
-
// Aggregate built here only — the loaderless and no-loading branches don't
|
|
360
|
-
// read it (the latter builds its own per-parallel promises).
|
|
361
452
|
const loaderDataPromise = getMemoizedLoaderPromise(loaderEntries);
|
|
453
|
+
let boundaryLoaderData: Promise<any[]> | any[] = loaderDataPromise;
|
|
454
|
+
if (forceAwait || isAction) {
|
|
455
|
+
const awaitStart = segDebug ? performance.now() : 0;
|
|
456
|
+
boundaryLoaderData = await loaderDataPromise;
|
|
457
|
+
if (segDebug) {
|
|
458
|
+
segDebugLog(`segment ${id}: loaders awaited (forceAwait/action)`, {
|
|
459
|
+
loaderIds,
|
|
460
|
+
ms: Math.round(performance.now() - awaitStart),
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
} else if (segDebug) {
|
|
464
|
+
segDebugLog(
|
|
465
|
+
`segment ${id}: streaming loaders via LoaderBoundary (suspense)`,
|
|
466
|
+
{ loaderIds },
|
|
467
|
+
);
|
|
468
|
+
}
|
|
362
469
|
content = createElement(LoaderBoundary, {
|
|
363
470
|
key: `loader-boundary-${key}`,
|
|
364
|
-
loaderDataPromise:
|
|
365
|
-
forceAwait || isAction ? await loaderDataPromise : loaderDataPromise,
|
|
471
|
+
loaderDataPromise: boundaryLoaderData,
|
|
366
472
|
loaderIds,
|
|
367
473
|
fallback: loading,
|
|
368
474
|
outletKey: key,
|
|
@@ -372,7 +478,6 @@ export async function renderSegments(
|
|
|
372
478
|
children: nodeContent,
|
|
373
479
|
});
|
|
374
480
|
} else if (loaderEntries.length === 0) {
|
|
375
|
-
// No loaders, no loading - simple OutletProvider
|
|
376
481
|
content = createElement(OutletProvider, {
|
|
377
482
|
key,
|
|
378
483
|
content: outletContent,
|
|
@@ -381,34 +486,37 @@ export async function renderSegments(
|
|
|
381
486
|
children: nodeContent,
|
|
382
487
|
});
|
|
383
488
|
} else {
|
|
384
|
-
// Has loaders but no loading skeleton.
|
|
385
|
-
// Split: parallel-owned loaders stream (their parallel has loading()),
|
|
386
|
-
// layout-owned loaders are awaited (they gate the layout content).
|
|
387
489
|
const layoutLoaders = loaderEntries.filter((l) => !l.parallelLoading);
|
|
388
490
|
const parallelOwnedLoaders = loaderEntries.filter(
|
|
389
491
|
(l) => !!l.parallelLoading,
|
|
390
492
|
);
|
|
391
493
|
|
|
392
|
-
// Await only layout-owned loaders
|
|
393
494
|
const layoutLoaderIds = layoutLoaders.map((l) => l.loaderId!);
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
495
|
+
// No loading() on this segment, so its loader data cannot stream behind
|
|
496
|
+
// a Suspense fallback — the tree build BLOCKS here until the data
|
|
497
|
+
// arrives. On the initial document this await runs before hydrateRoot.
|
|
498
|
+
const layoutAwaitStart = segDebug ? performance.now() : 0;
|
|
499
|
+
const resolvedData = await buildLoaderPromise(layoutLoaders);
|
|
500
|
+
if (segDebug) {
|
|
501
|
+
segDebugLog(`segment ${id}: layout loaders awaited (blocking)`, {
|
|
502
|
+
loaderIds: layoutLoaderIds,
|
|
503
|
+
ms: Math.round(performance.now() - layoutAwaitStart),
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
const decodeStart = segDebug ? performance.now() : 0;
|
|
405
507
|
const { loaderData, errorFallback } = decodeLoaderResults(
|
|
406
508
|
resolvedData,
|
|
407
509
|
layoutLoaderIds,
|
|
408
510
|
);
|
|
511
|
+
if (segDebug) {
|
|
512
|
+
const decodeMs = Math.round(performance.now() - decodeStart);
|
|
513
|
+
if (decodeMs > 0) {
|
|
514
|
+
segDebugLog(`segment ${id}: loader results decoded`, {
|
|
515
|
+
ms: decodeMs,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
409
519
|
|
|
410
|
-
// Parallel-owned loaders: attach to their owning parallel segment
|
|
411
|
-
// as loaderDataPromise so ParallelOutlet wraps in LoaderBoundary
|
|
412
520
|
if (parallelOwnedLoaders.length > 0) {
|
|
413
521
|
const loadersByParallelNamespace = new Map<string, ResolvedSegment[]>();
|
|
414
522
|
|
|
@@ -436,10 +544,27 @@ export async function renderSegments(
|
|
|
436
544
|
|
|
437
545
|
p.loaderIds = ownedLoaders.map((l) => l.loaderId!);
|
|
438
546
|
const aggregated = getMemoizedLoaderPromise(ownedLoaders);
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
547
|
+
if ((forceAwait || isAction) && aggregated instanceof Promise) {
|
|
548
|
+
const parallelAwaitStart = segDebug ? performance.now() : 0;
|
|
549
|
+
p.loaderDataPromise = await aggregated;
|
|
550
|
+
if (segDebug) {
|
|
551
|
+
segDebugLog(
|
|
552
|
+
`segment ${id}: parallel ${p.id} loaders awaited (forceAwait/action)`,
|
|
553
|
+
{
|
|
554
|
+
loaderIds: p.loaderIds,
|
|
555
|
+
ms: Math.round(performance.now() - parallelAwaitStart),
|
|
556
|
+
},
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
} else {
|
|
560
|
+
p.loaderDataPromise = aggregated;
|
|
561
|
+
if (segDebug) {
|
|
562
|
+
segDebugLog(
|
|
563
|
+
`segment ${id}: parallel ${p.id} loaders streaming (suspense)`,
|
|
564
|
+
{ loaderIds: p.loaderIds },
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
443
568
|
}
|
|
444
569
|
}
|
|
445
570
|
|
|
@@ -463,28 +588,56 @@ export async function renderSegments(
|
|
|
463
588
|
children: content,
|
|
464
589
|
});
|
|
465
590
|
}
|
|
591
|
+
|
|
592
|
+
if (segDebug) {
|
|
593
|
+
segDebugLog(`segment ${id} built`, {
|
|
594
|
+
type: node.segment.type,
|
|
595
|
+
ms: Math.round(performance.now() - segNodeStart),
|
|
596
|
+
loaders: node.loaders.map((l) => l.loaderId).filter(Boolean),
|
|
597
|
+
hasLoading: loading !== undefined && loading !== null,
|
|
598
|
+
parallel: node.parallel.map((p) => p.id),
|
|
599
|
+
});
|
|
600
|
+
}
|
|
466
601
|
}
|
|
467
602
|
|
|
468
|
-
// Always wrap with root error boundary to prevent white screens
|
|
469
|
-
// This catches any unhandled errors that bubble up from the segment tree
|
|
470
603
|
const errorBoundaryWrapped = createElement(RootErrorBoundary, {
|
|
471
604
|
children: content,
|
|
472
605
|
});
|
|
473
606
|
if (typeof window === "object") {
|
|
607
|
+
// [VT-DIAG] Gated behind INTERNAL_RANGO_DEBUG. If this await dominates the
|
|
608
|
+
// navigation time, a deferred/lazy segment component is being fully resolved
|
|
609
|
+
// before commit, which defeats client-nav streaming. The await itself is
|
|
610
|
+
// functional (it preloads lazy chunk refs); only the timing log is gated.
|
|
611
|
+
const vtDebug = INTERNAL_RANGO_DEBUG && temporalLazyRefs.length > 0;
|
|
612
|
+
const vtDebugStart = vtDebug ? performance.now() : 0;
|
|
613
|
+
if (vtDebug) {
|
|
614
|
+
console.log("[VT-DIAG] renderSegments awaiting temporalLazyRefs", {
|
|
615
|
+
count: temporalLazyRefs.length,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
474
618
|
await Promise.allSettled(temporalLazyRefs);
|
|
619
|
+
if (vtDebug) {
|
|
620
|
+
console.log("[VT-DIAG] renderSegments temporalLazyRefs settled", {
|
|
621
|
+
count: temporalLazyRefs.length,
|
|
622
|
+
ms: Math.round(performance.now() - vtDebugStart),
|
|
623
|
+
});
|
|
624
|
+
}
|
|
475
625
|
}
|
|
476
626
|
|
|
477
|
-
// Build the final result, optionally wrapped with root layout
|
|
478
627
|
let result: ReactNode = errorBoundaryWrapped;
|
|
479
628
|
|
|
480
|
-
// If rootLayout is provided, wrap the error boundary with it
|
|
481
|
-
// This ensures the app shell stays mounted even during errors (prevents FOUC)
|
|
482
629
|
if (RootLayout) {
|
|
483
630
|
result = createElement(RootLayout, {
|
|
484
631
|
children: errorBoundaryWrapped,
|
|
485
632
|
});
|
|
486
633
|
}
|
|
487
634
|
|
|
635
|
+
if (segDebug) {
|
|
636
|
+
segDebugLog("renderSegments complete", {
|
|
637
|
+
ms: Math.round(performance.now() - segDebugStart),
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
488
641
|
return result;
|
|
489
642
|
}
|
|
490
643
|
|
|
@@ -516,6 +669,31 @@ export async function renderSegments(
|
|
|
516
669
|
* @param segments - Main segments from the route tree
|
|
517
670
|
* @param interceptSegments - Optional intercept segments to inject
|
|
518
671
|
*/
|
|
672
|
+
// Loader segment ids have the grammar `${parentId}D${index}.${loaderId}`.
|
|
673
|
+
// parentId is the parent shortCode (M/L/P/R/C + digits, never "D") for normal
|
|
674
|
+
// loaders, or `${shortCode}.${slotName}` for intercept-slot loaders, where the
|
|
675
|
+
// slot name is user-controlled (`@${string}`) and may contain an uppercase "D"
|
|
676
|
+
// (e.g. "@Detail"). Strip from the first `D<index>.` separator so the slot name
|
|
677
|
+
// is preserved; splitting on a bare "D" mis-cut "@Detail" to "@" and silently
|
|
678
|
+
// dropped the loader's data. The first-`D<index>.` strip is only correct because
|
|
679
|
+
// slot names cannot contain "." -- assertValidSlotName (route-definition/
|
|
680
|
+
// dsl-helpers.ts) rejects a "." at definition time, so a name like "@D3.foo"
|
|
681
|
+
// (which WOULD mis-cut here) can never reach this function.
|
|
682
|
+
function loaderParentId(loaderSegmentId: string): string {
|
|
683
|
+
return loaderSegmentId.replace(/D\d+\..*$/, "");
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// Append a value to the array stored under `key`, creating the array on first
|
|
687
|
+
// use. Single Map lookup (vs the has/get!().push double-lookup idiom).
|
|
688
|
+
function pushToGroup<K, V>(map: Map<K, V[]>, key: K, value: V): void {
|
|
689
|
+
const arr = map.get(key);
|
|
690
|
+
if (arr) {
|
|
691
|
+
arr.push(value);
|
|
692
|
+
} else {
|
|
693
|
+
map.set(key, [value]);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
519
697
|
function* segmentTreeWalk(
|
|
520
698
|
segments: ResolvedSegment[],
|
|
521
699
|
interceptSegments?: ResolvedSegment[],
|
|
@@ -536,19 +714,12 @@ function* segmentTreeWalk(
|
|
|
536
714
|
// Extract parent ID from parallel ID
|
|
537
715
|
// Example: "L0R1L0.@sidebar" → "L0R1L0"
|
|
538
716
|
const parentId = segment.id.split(".")[0];
|
|
539
|
-
|
|
540
|
-
parallelsByParent.set(parentId, []);
|
|
541
|
-
}
|
|
542
|
-
parallelsByParent.get(parentId)!.push(segment);
|
|
717
|
+
pushToGroup(parallelsByParent, parentId, segment);
|
|
543
718
|
} else if (segment.type === "loader") {
|
|
544
719
|
// Extract parent ID from loader ID
|
|
545
|
-
// Example: "L0D0.cart" → "L0"
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
if (!loadersByParent.has(parentId)) {
|
|
549
|
-
loadersByParent.set(parentId, []);
|
|
550
|
-
}
|
|
551
|
-
loadersByParent.get(parentId)!.push(segment);
|
|
720
|
+
// Example: "L0D0.cart" → "L0"; "L0.@DetailD0.x" → "L0.@Detail"
|
|
721
|
+
const parentId = loaderParentId(segment.id);
|
|
722
|
+
pushToGroup(loadersByParent, parentId, segment);
|
|
552
723
|
} else {
|
|
553
724
|
// Layout, route, error, and notFound segments are all rendered in the tree
|
|
554
725
|
// Error/notFound segments replace the failed segment with fallback UI
|
|
@@ -563,17 +734,11 @@ function* segmentTreeWalk(
|
|
|
563
734
|
if (intercept.type === "parallel" && intercept.slot) {
|
|
564
735
|
// Extract parent ID from intercept ID (e.g., "M4L0L0L2.@modal" → "M4L0L0L2")
|
|
565
736
|
const parentId = intercept.id.split(".")[0];
|
|
566
|
-
|
|
567
|
-
parallelsByParent.set(parentId, []);
|
|
568
|
-
}
|
|
569
|
-
parallelsByParent.get(parentId)!.push(intercept);
|
|
737
|
+
pushToGroup(parallelsByParent, parentId, intercept);
|
|
570
738
|
} else if (intercept.type === "loader") {
|
|
571
|
-
// Intercept loaders - extract parent from loader ID
|
|
572
|
-
const parentId = intercept.id
|
|
573
|
-
|
|
574
|
-
loadersByParent.set(parentId, []);
|
|
575
|
-
}
|
|
576
|
-
loadersByParent.get(parentId)!.push(intercept);
|
|
739
|
+
// Intercept loaders - extract parent from loader ID (slot name preserved)
|
|
740
|
+
const parentId = loaderParentId(intercept.id);
|
|
741
|
+
pushToGroup(loadersByParent, parentId, intercept);
|
|
577
742
|
}
|
|
578
743
|
}
|
|
579
744
|
}
|
package/src/server/context.ts
CHANGED
|
@@ -150,6 +150,19 @@ export type InterceptWhenFn<TEnv = any> = (
|
|
|
150
150
|
ctx: InterceptSelectorContext<TEnv>,
|
|
151
151
|
) => boolean;
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Config object passed to intercept() (its 4th argument). `when` gates whether
|
|
155
|
+
* the intercept activates on a soft navigation — a single match-time selector or
|
|
156
|
+
* an array of them (ALL must return true; omit to always activate). This is the
|
|
157
|
+
* intercept counterpart to transition({ when }); both express conditional
|
|
158
|
+
* behavior as a config field rather than a separate DSL helper.
|
|
159
|
+
*
|
|
160
|
+
* @internal This type is an implementation detail and may change without notice.
|
|
161
|
+
*/
|
|
162
|
+
export interface InterceptConfig<TEnv = any> {
|
|
163
|
+
when?: InterceptWhenFn<TEnv> | InterceptWhenFn<TEnv>[];
|
|
164
|
+
}
|
|
165
|
+
|
|
153
166
|
/**
|
|
154
167
|
* Intercept entry stored in EntryData
|
|
155
168
|
* Contains the slot name, route to intercept, and handler
|
|
@@ -220,6 +233,13 @@ export type EntryData =
|
|
|
220
233
|
staticHandlerId?: string;
|
|
221
234
|
/** Response type for non-RSC routes (json, text, image, any) */
|
|
222
235
|
responseType?: string;
|
|
236
|
+
/**
|
|
237
|
+
* PPR (partial pre-rendering) opt-in from the path() `ppr` option. A
|
|
238
|
+
* document-level property of the page route: `true` uses the default
|
|
239
|
+
* shell policy, an object carries ttl/swr/tags. Read by the integrated
|
|
240
|
+
* PPR serve path (rsc/shell-serve.ts resolvePprConfig).
|
|
241
|
+
*/
|
|
242
|
+
ppr?: boolean | import("../urls/pattern-types.js").PartialPrerenderProps;
|
|
223
243
|
} & EntryPropCommon &
|
|
224
244
|
EntryPropDatas &
|
|
225
245
|
EntryPropSegments &
|
|
@@ -750,14 +770,23 @@ const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
|
|
|
750
770
|
|
|
751
771
|
// Purity-only scope: marks that a loader FUNCTION BODY is executing, regardless
|
|
752
772
|
// of how the loader was invoked (DSL via runInsideLoaderScope, or handler-
|
|
753
|
-
// invoked via ctx.use). Consulted
|
|
754
|
-
// request-scoped reads
|
|
755
|
-
//
|
|
756
|
-
//
|
|
773
|
+
// invoked via ctx.use). Consulted by isInsideCacheScope() to exempt
|
|
774
|
+
// request-scoped reads, by getCurrentLoaderBodyId() for guard-warning
|
|
775
|
+
// attribution, and by isInsideHandlerInvokedLoaderBody() for the
|
|
776
|
+
// consumption-lane rule (the shell-capture guard exemption). It deliberately
|
|
777
|
+
// does NOT affect isInsideLoaderScope(), so rendered()/barrier/deadlock
|
|
778
|
+
// gating (which must distinguish DSL from handler-invoked loaders) is
|
|
779
|
+
// unchanged.
|
|
757
780
|
const LOADER_BODY_SCOPE_KEY = Symbol.for("rangojs-router:loader-body-scope");
|
|
758
|
-
const loaderBodyScopeALS: AsyncLocalStorage<{
|
|
759
|
-
|
|
760
|
-
|
|
781
|
+
const loaderBodyScopeALS: AsyncLocalStorage<{
|
|
782
|
+
active: true;
|
|
783
|
+
loaderId?: string;
|
|
784
|
+
handlerInvoked?: boolean;
|
|
785
|
+
}> = ((globalThis as any)[LOADER_BODY_SCOPE_KEY] ??= new AsyncLocalStorage<{
|
|
786
|
+
active: true;
|
|
787
|
+
loaderId?: string;
|
|
788
|
+
handlerInvoked?: boolean;
|
|
789
|
+
}>());
|
|
761
790
|
|
|
762
791
|
/**
|
|
763
792
|
* Check if the current execution is inside a cache() DSL boundary.
|
|
@@ -802,6 +831,67 @@ export function runInsideLoaderScope<T>(fn: () => T): T {
|
|
|
802
831
|
* and handler-invoked via ctx.use) so request-scoped reads inside a loader
|
|
803
832
|
* never trip the cache-scope guards — loaders always run fresh.
|
|
804
833
|
*/
|
|
805
|
-
export function runInsideLoaderBodyScope<T>(
|
|
806
|
-
|
|
834
|
+
export function runInsideLoaderBodyScope<T>(
|
|
835
|
+
fn: () => T,
|
|
836
|
+
loaderId?: string,
|
|
837
|
+
handlerInvoked?: boolean,
|
|
838
|
+
): T {
|
|
839
|
+
return loaderBodyScopeALS.run({ active: true, loaderId, handlerInvoked }, fn);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* The $$id of the loader whose body is currently executing, or undefined
|
|
844
|
+
* outside any loader body. Used by the shell-capture identity guard
|
|
845
|
+
* (cookie-store.ts) so its refusal warning can name the loader that read
|
|
846
|
+
* cookies()/headers() instead of blaming a lane it cannot see — the old
|
|
847
|
+
* hardcoded "bake-lane loader" text misled a live-lane debugging session
|
|
848
|
+
* (issue #672, secondary).
|
|
849
|
+
*/
|
|
850
|
+
export function getCurrentLoaderBodyId(): string | undefined {
|
|
851
|
+
return loaderBodyScopeALS.getStore()?.loaderId;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* True while a HANDLER-invoked loader body (`await ctx.use(Loader)` from a
|
|
856
|
+
* handler, not the DSL segment funnel) is executing. The consumption-lane
|
|
857
|
+
* rule keys off this: handler consumption yields a BAKED copy in every shared
|
|
858
|
+
* artifact — cache(), "use cache", and the PPR shell — so the shell-capture
|
|
859
|
+
* identity guard (cookie-store.ts) permits cookies()/headers() here, exactly
|
|
860
|
+
* like the cache-purity guards do. DSL segment loaders (live lane masked at
|
|
861
|
+
* capture, bake lane guarded) never set the flag.
|
|
862
|
+
*/
|
|
863
|
+
export function isInsideHandlerInvokedLoaderBody(): boolean {
|
|
864
|
+
return loaderBodyScopeALS.getStore()?.handlerInvoked === true;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// Scope for handle PUSH CALLBACKS (push(() => ...), including async ones).
|
|
868
|
+
// A push callback's value is stored as-is; if it is a promise it is NOT tracked
|
|
869
|
+
// by handleStore.settled and does not block segment resolution, so a
|
|
870
|
+
// ctx.use(loader) made from inside such a callback can never form a rendered()
|
|
871
|
+
// deadlock. This is an ALS (not a plain boolean) so the exemption survives the
|
|
872
|
+
// callback's own awaits — an async push callback that resumes after `await`
|
|
873
|
+
// still reads as "inside a push callback" and stays out of the deadlock guard.
|
|
874
|
+
const PUSH_CALLBACK_SCOPE_KEY = Symbol.for(
|
|
875
|
+
"rangojs-router:push-callback-scope",
|
|
876
|
+
);
|
|
877
|
+
const pushCallbackScopeALS: AsyncLocalStorage<{ active: true }> = ((
|
|
878
|
+
globalThis as any
|
|
879
|
+
)[PUSH_CALLBACK_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Check if the current execution is inside a handle push callback (sync or an
|
|
883
|
+
* async callback's continuation). Used by the handler-to-loader deadlock guard
|
|
884
|
+
* to exempt push-callback continuations.
|
|
885
|
+
*/
|
|
886
|
+
export function isInsidePushCallbackScope(): boolean {
|
|
887
|
+
return pushCallbackScopeALS.getStore()?.active === true;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Run `fn` inside a push-callback scope. Wraps the invocation of a handle push
|
|
892
|
+
* callback so that any ctx.use(loader) it makes — including after one of its own
|
|
893
|
+
* awaits — is exempt from the deadlock guard.
|
|
894
|
+
*/
|
|
895
|
+
export function runInsidePushCallbackScope<T>(fn: () => T): T {
|
|
896
|
+
return pushCallbackScopeALS.run({ active: true }, fn);
|
|
807
897
|
}
|