@rangojs/router 0.0.0-experimental.1b930379 → 0.0.0-experimental.1c0bdfad
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +12 -0
- package/README.md +245 -49
- package/dist/bin/rango.js +441 -134
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3453 -1240
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +76 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +64 -2
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +247 -23
- package/skills/caching/SKILL.md +318 -15
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +250 -30
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +50 -6
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +279 -53
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +771 -0
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +203 -7
- package/skills/prerender/SKILL.md +153 -109
- package/skills/rango/SKILL.md +251 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +123 -48
- package/skills/route/SKILL.md +101 -5
- package/skills/router-setup/SKILL.md +116 -8
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +121 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/typesafety/SKILL.md +332 -29
- package/skills/use-cache/SKILL.md +46 -14
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +1 -66
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +197 -150
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +111 -31
- package/src/browser/navigation-client.ts +201 -67
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +18 -66
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +187 -112
- package/src/browser/prefetch/cache.ts +230 -35
- package/src/browser/prefetch/fetch.ts +338 -39
- package/src/browser/prefetch/queue.ts +126 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +111 -16
- package/src/browser/react/NavigationProvider.tsx +135 -120
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +23 -69
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +32 -7
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +80 -5
- package/src/browser/rsc-router.tsx +218 -76
- package/src/browser/scroll-restoration.ts +54 -42
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +222 -61
- package/src/browser/types.ts +91 -11
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +165 -36
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +125 -24
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +272 -96
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +214 -48
- package/src/cache/cache-scope.ts +236 -89
- package/src/cache/cache-tag.ts +103 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +2224 -171
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +11 -17
- package/src/cache/document-cache.ts +89 -27
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +11 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +31 -31
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +44 -21
- package/src/client.tsx +119 -290
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +84 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +56 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +37 -8
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +51 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +93 -12
- package/src/index.ts +133 -15
- package/src/internal-debug.ts +11 -10
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +62 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +469 -276
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +113 -37
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +53 -12
- package/src/route-definition/resolve-handler-use.ts +161 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -17
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +164 -17
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +45 -22
- package/src/router/handler-context.ts +83 -39
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +50 -24
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +274 -56
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +49 -45
- package/src/router/match-api.ts +120 -204
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +33 -6
- package/src/router/match-middleware/cache-lookup.ts +214 -263
- package/src/router/match-middleware/cache-store.ts +73 -33
- package/src/router/match-middleware/intercept-resolution.ts +8 -28
- package/src/router/match-middleware/segment-resolution.ts +52 -18
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +104 -40
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +270 -172
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +132 -90
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +195 -56
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +8 -28
- package/src/router/router-interfaces.ts +115 -35
- package/src/router/router-options.ts +172 -15
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +264 -77
- package/src/router/segment-resolution/helpers.ts +115 -30
- package/src/router/segment-resolution/loader-cache.ts +63 -37
- package/src/router/segment-resolution/revalidation.ts +474 -385
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +8 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +161 -199
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +163 -59
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +163 -55
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +658 -511
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +127 -31
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +77 -11
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +99 -189
- package/src/rsc/rsc-rendering.ts +105 -72
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +263 -112
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +32 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +149 -0
- package/src/segment-system.tsx +281 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +309 -61
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +40 -38
- package/src/server/loader-registry.ts +26 -46
- package/src/server/request-context.ts +398 -172
- package/src/ssr/index.tsx +25 -16
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +701 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +257 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +371 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +343 -0
- package/src/testing/render-route.tsx +581 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +20 -58
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +233 -81
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +37 -14
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +58 -11
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/discover-routers.ts +130 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +214 -132
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +57 -6
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +6 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +155 -65
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/plugins/expose-action-id.ts +49 -98
- package/src/vite/plugins/expose-id-utils.ts +96 -51
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +553 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +46 -37
- package/src/vite/plugins/virtual-entries.ts +13 -18
- package/src/vite/rango.ts +238 -295
- package/src/vite/router-discovery.ts +940 -149
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +81 -34
- package/src/vite/utils/shared-utils.ts +92 -42
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { ReactNode } from "react";
|
|
8
|
-
import { track } from "../server/context";
|
|
9
8
|
import type { EntryData } from "../server/context";
|
|
9
|
+
import { observePhase, PHASES } from "./instrument.js";
|
|
10
|
+
import { contextGet } from "../context-var.js";
|
|
10
11
|
import type {
|
|
11
12
|
ResolvedSegment,
|
|
12
13
|
HandlerContext,
|
|
@@ -18,11 +19,17 @@ import type {
|
|
|
18
19
|
ErrorBoundaryFallbackProps,
|
|
19
20
|
ErrorInfo,
|
|
20
21
|
} from "../types";
|
|
21
|
-
import
|
|
22
|
-
import {
|
|
23
|
-
import
|
|
22
|
+
import { isHandle, collectHandleData, type Handle } from "../handle.js";
|
|
23
|
+
import { withDefer } from "../defer.js";
|
|
24
|
+
import { buildHandleSnapshot } from "../server/handle-store.js";
|
|
24
25
|
import { getFetchableLoader } from "../server/fetchable-loader-store.js";
|
|
25
26
|
import { _getRequestContext } from "../server/request-context.js";
|
|
27
|
+
import {
|
|
28
|
+
isInsideLoaderScope,
|
|
29
|
+
runInsideLoaderBodyScope,
|
|
30
|
+
isInsidePushCallbackScope,
|
|
31
|
+
runInsidePushCallbackScope,
|
|
32
|
+
} from "../server/context.js";
|
|
26
33
|
import { debugLog } from "./logging.js";
|
|
27
34
|
|
|
28
35
|
/**
|
|
@@ -64,7 +71,9 @@ export function wrapLoaderWithErrorHandling<T>(
|
|
|
64
71
|
) => ErrorInfo,
|
|
65
72
|
onError?: LoaderErrorCallback,
|
|
66
73
|
): Promise<LoaderDataResult<T>> {
|
|
67
|
-
// Extract
|
|
74
|
+
// Extract the trailing token from segmentId (format: "<shortCode>D<i>.<loaderId>").
|
|
75
|
+
// The token is the loader's $$id (hash#export in prod, pathfrag#export in dev),
|
|
76
|
+
// not a clean display name.
|
|
68
77
|
const loaderName = segmentId.split(".").pop() || "unknown";
|
|
69
78
|
|
|
70
79
|
return Promise.resolve(promise)
|
|
@@ -100,16 +109,40 @@ export function wrapLoaderWithErrorHandling<T>(
|
|
|
100
109
|
};
|
|
101
110
|
}
|
|
102
111
|
|
|
103
|
-
// Render fallback on server
|
|
112
|
+
// Render fallback on server. The user ErrorBoundaryHandler may throw
|
|
113
|
+
// synchronously; if it does we must NOT let that rejection escape — the
|
|
114
|
+
// wrapped LoaderDataResult promise is contracted to never reject (see
|
|
115
|
+
// segment-resolution/fresh.ts `await Promise.all(...wrapped)`), and a
|
|
116
|
+
// rejection here would collapse the whole entry and discard healthy
|
|
117
|
+
// sibling loader data. On a fallback-render throw, fall back to the
|
|
118
|
+
// no-boundary result (fallback: null) so the client throws the ORIGINAL
|
|
119
|
+
// error, and the wrapped promise still resolves to a LoaderDataResult.
|
|
104
120
|
let renderedFallback: ReactNode;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
121
|
+
try {
|
|
122
|
+
if (typeof fallback === "function") {
|
|
123
|
+
// ErrorBoundaryHandler - call with error info
|
|
124
|
+
const props: ErrorBoundaryFallbackProps = {
|
|
125
|
+
error: errorInfo,
|
|
126
|
+
};
|
|
127
|
+
renderedFallback = fallback(props);
|
|
128
|
+
} else {
|
|
129
|
+
renderedFallback = fallback;
|
|
130
|
+
}
|
|
131
|
+
} catch (fallbackError) {
|
|
132
|
+
debugLog("loader", "error boundary fallback render threw", {
|
|
133
|
+
segmentId,
|
|
134
|
+
message: errorInfo.message,
|
|
135
|
+
fallbackError:
|
|
136
|
+
fallbackError instanceof Error
|
|
137
|
+
? fallbackError.message
|
|
138
|
+
: String(fallbackError),
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
__loaderResult: true,
|
|
142
|
+
ok: false,
|
|
108
143
|
error: errorInfo,
|
|
144
|
+
fallback: null,
|
|
109
145
|
};
|
|
110
|
-
renderedFallback = fallback(props);
|
|
111
|
-
} else {
|
|
112
|
-
renderedFallback = fallback;
|
|
113
146
|
}
|
|
114
147
|
|
|
115
148
|
debugLog("loader", "loader error wrapped with boundary fallback", {
|
|
@@ -241,6 +274,21 @@ function createLoaderExecutor<TEnv>(
|
|
|
241
274
|
pendingLoaders.add(loader.$$id);
|
|
242
275
|
|
|
243
276
|
const currentLoaderId = loader.$$id;
|
|
277
|
+
const variables = (ctx as InternalHandlerContext<any, TEnv>)._variables;
|
|
278
|
+
|
|
279
|
+
// Capture whether this loader is being started from a DSL loader scope
|
|
280
|
+
// (runInsideLoaderScope in fresh.ts). Handler-invoked loaders are NOT
|
|
281
|
+
// inside loader scope. This determines whether rendered() is allowed.
|
|
282
|
+
const isDslLoader = isInsideLoaderScope();
|
|
283
|
+
|
|
284
|
+
let renderedResolved = false;
|
|
285
|
+
let renderedPromise: Promise<void> | null = null;
|
|
286
|
+
|
|
287
|
+
// Loader functions are always fresh (never cached), so they get an
|
|
288
|
+
// unguarded get that bypasses non-cacheable read guards. This applies
|
|
289
|
+
// to ALL loaders — DSL and handler-called — because the loader
|
|
290
|
+
// function itself always re-executes. Also handles nested deps
|
|
291
|
+
// (loaderA → use(loaderB)) since all share this unguarded get.
|
|
244
292
|
const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
|
|
245
293
|
params: ctx.params,
|
|
246
294
|
routeParams: (ctx.params ?? {}) as Record<string, string>,
|
|
@@ -249,26 +297,138 @@ function createLoaderExecutor<TEnv>(
|
|
|
249
297
|
search: (ctx as any).search,
|
|
250
298
|
pathname: ctx.pathname,
|
|
251
299
|
url: ctx.url,
|
|
300
|
+
originalUrl: ctx.originalUrl,
|
|
252
301
|
env: ctx.env,
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
302
|
+
waitUntil: ctx.waitUntil.bind(ctx),
|
|
303
|
+
executionContext: ctx.executionContext,
|
|
304
|
+
get: ((keyOrVar: any) =>
|
|
305
|
+
contextGet(variables, keyOrVar)) as typeof ctx.get,
|
|
306
|
+
use: ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
307
|
+
if (isHandle(item)) {
|
|
308
|
+
if (!renderedResolved) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
`ctx.use(handle) in a loader requires "await ctx.rendered()" first. ` +
|
|
311
|
+
`Handle "${item.$$id}" cannot be read until the render tree has settled.`,
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
const reqCtx = reqCtxRef ?? _getRequestContext();
|
|
315
|
+
if (!reqCtx) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
`ctx.use(handle) failed: request context not available.`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
const segmentOrder = reqCtx._renderBarrierSegmentOrder ?? [];
|
|
321
|
+
// The complete snapshot is cached at barrier resolution for
|
|
322
|
+
// non-streaming trees, and by rendered() after handleStore.settled for
|
|
323
|
+
// streaming trees (where the eager snapshot would have been incomplete
|
|
324
|
+
// because loading() handlers were still in flight). Either way it is
|
|
325
|
+
// present by the time a loader reads a handle; the fresh build is only
|
|
326
|
+
// a defensive fallback.
|
|
327
|
+
const snapshot =
|
|
328
|
+
reqCtx._renderBarrierHandleSnapshot ??
|
|
329
|
+
buildHandleSnapshot(reqCtx._handleStore, segmentOrder);
|
|
330
|
+
return collectHandleData(item, snapshot, segmentOrder);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Loader case
|
|
334
|
+
return useLoader(item as LoaderDefinition<any, any>, currentLoaderId);
|
|
335
|
+
}) as LoaderContext["use"],
|
|
260
336
|
method: "GET",
|
|
261
337
|
body: undefined,
|
|
262
338
|
reverse: ctx.reverse as LoaderContext["reverse"],
|
|
339
|
+
rendered: (): Promise<void> => {
|
|
340
|
+
// Guard: only DSL loaders may use rendered()
|
|
341
|
+
if (!isDslLoader) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
`ctx.rendered() is only available in DSL loaders (registered via loader() in urls()). ` +
|
|
344
|
+
`Handler-invoked loaders (ctx.use(Loader) inside a handler) cannot use rendered().`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const reqCtx = reqCtxRef ?? _getRequestContext();
|
|
349
|
+
|
|
350
|
+
if (renderedPromise) return renderedPromise;
|
|
351
|
+
|
|
352
|
+
if (!reqCtx) {
|
|
353
|
+
throw new Error(
|
|
354
|
+
`ctx.rendered() failed: request context not available.`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Bidirectional deadlock check: if a handler already started
|
|
359
|
+
// awaiting this loader, calling rendered() would deadlock. This is the
|
|
360
|
+
// real cycle guard (it holds for both streaming and non-streaming): the
|
|
361
|
+
// handler blocks segment resolution, which blocks the barrier, which
|
|
362
|
+
// blocks this loader.
|
|
363
|
+
if (reqCtx._handlerLoaderDeps?.has(currentLoaderId)) {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`Deadlock: loader "${currentLoaderId}" called ctx.rendered() but a handler ` +
|
|
366
|
+
`is already awaiting this loader via ctx.use(). The handler blocks ` +
|
|
367
|
+
`segment resolution, which blocks the barrier, which blocks this loader. ` +
|
|
368
|
+
`Move the data dependency to a loader-to-loader pattern instead.`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Register this loader as waiting for the barrier so that
|
|
373
|
+
// setupLoaderAccess can detect deadlocks when a handler
|
|
374
|
+
// tries to await the same loader via ctx.use().
|
|
375
|
+
if (!reqCtx._renderBarrierWaiters) {
|
|
376
|
+
reqCtx._renderBarrierWaiters = new Set();
|
|
377
|
+
}
|
|
378
|
+
reqCtx._renderBarrierWaiters.add(currentLoaderId);
|
|
379
|
+
|
|
380
|
+
// Streaming trees (loading()): the barrier resolves once the segment
|
|
381
|
+
// tree is resolved, but loading() handlers stream behind Suspense and
|
|
382
|
+
// their handle pushes are still in flight then. Their async execution
|
|
383
|
+
// IS tracked in the handle store (trackHandler -> store.track), so after
|
|
384
|
+
// the barrier we seal (no further handlers register once the tree is
|
|
385
|
+
// resolved) and wait for settled — every tracked handler, streaming
|
|
386
|
+
// included, has finished pushing. The loader's own segment streams in
|
|
387
|
+
// after, so this does not block the shell; the deadlock guard above
|
|
388
|
+
// keeps a handler from depending on this loader.
|
|
389
|
+
const streaming = reqCtx._treeHasStreaming === true;
|
|
390
|
+
renderedPromise = reqCtx._renderBarrier.then(async () => {
|
|
391
|
+
if (streaming) {
|
|
392
|
+
reqCtx._handleStore.seal();
|
|
393
|
+
await reqCtx._handleStore.settled;
|
|
394
|
+
// The eager snapshot was intentionally left unbuilt for streaming
|
|
395
|
+
// (it would have been incomplete). Build the complete one once, now
|
|
396
|
+
// that the store has settled, so every ctx.use(handle) reads the
|
|
397
|
+
// cached snapshot instead of rebuilding it per call.
|
|
398
|
+
reqCtx._renderBarrierHandleSnapshot ??= buildHandleSnapshot(
|
|
399
|
+
reqCtx._handleStore,
|
|
400
|
+
reqCtx._renderBarrierSegmentOrder ?? [],
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
renderedResolved = true;
|
|
404
|
+
});
|
|
405
|
+
return renderedPromise;
|
|
406
|
+
},
|
|
263
407
|
};
|
|
264
408
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
409
|
+
// Meter this loader once via observePhase (loader:<id> perf metric +
|
|
410
|
+
// rango.loader span); loaderFn runs inside the span callback so its KV/D1/
|
|
411
|
+
// fetch spans nest under it. This is one of the observePhase loader funnels —
|
|
412
|
+
// see instrument.ts for the single-metering contract.
|
|
413
|
+
//
|
|
414
|
+
// Run the loader body inside loader scope so request-scoped reads
|
|
415
|
+
// (cookies()/headers() and non-cacheable ctx.get) are exempt from the
|
|
416
|
+
// cache-purity guards: loaders always run fresh, so their reads never leak
|
|
417
|
+
// into a cached segment. DSL loaders are already wrapped by fresh.ts; this
|
|
418
|
+
// also covers handler-invoked loaders (ctx.use(Loader) from a handler),
|
|
419
|
+
// which otherwise execute in the caller's cache scope and would wrongly
|
|
420
|
+
// throw. rendered() gating uses the captured isDslLoader (above), so this
|
|
421
|
+
// does not grant rendered() to handler-invoked loaders. Uses a body-only
|
|
422
|
+
// scope, so isInsideLoaderScope() / barrier / deadlock gating is unchanged.
|
|
423
|
+
const promise = observePhase(PHASES.loader(loader.$$id), () =>
|
|
424
|
+
Promise.resolve(
|
|
425
|
+
runInsideLoaderBodyScope(() =>
|
|
426
|
+
loaderFn(loaderCtx as LoaderContext<any, TEnv>),
|
|
427
|
+
),
|
|
428
|
+
).finally(() => {
|
|
429
|
+
pendingLoaders.delete(loader.$$id);
|
|
430
|
+
}),
|
|
431
|
+
);
|
|
272
432
|
|
|
273
433
|
loaderPromises.set(loader.$$id, promise);
|
|
274
434
|
return promise;
|
|
@@ -290,12 +450,13 @@ export function setupLoaderAccess<TEnv>(
|
|
|
290
450
|
ctx: HandlerContext<any, TEnv>,
|
|
291
451
|
loaderPromises: Map<string, Promise<any>>,
|
|
292
452
|
): void {
|
|
293
|
-
// Eagerly capture the HandleStore at setup time
|
|
294
|
-
// In workerd/Cloudflare, dynamic imports and
|
|
295
|
-
// can disrupt AsyncLocalStorage, causing
|
|
296
|
-
// undefined when handlers later call
|
|
297
|
-
//
|
|
298
|
-
const
|
|
453
|
+
// Eagerly capture the request context and HandleStore at setup time
|
|
454
|
+
// (before pipeline async ops). In workerd/Cloudflare, dynamic imports and
|
|
455
|
+
// fetch() in the match pipeline can disrupt AsyncLocalStorage, causing
|
|
456
|
+
// getRequestContext() to return undefined when handlers later call
|
|
457
|
+
// ctx.use(handle). Capturing early ensures references survive ALS disruption.
|
|
458
|
+
const reqCtxRef = _getRequestContext();
|
|
459
|
+
const handleStoreRef = reqCtxRef?._handleStore;
|
|
299
460
|
|
|
300
461
|
const useLoader = createLoaderExecutor(ctx, loaderPromises);
|
|
301
462
|
|
|
@@ -313,21 +474,72 @@ export function setupLoaderAccess<TEnv>(
|
|
|
313
474
|
);
|
|
314
475
|
}
|
|
315
476
|
|
|
316
|
-
return (
|
|
317
|
-
dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
477
|
+
return withDefer(
|
|
478
|
+
(dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
|
|
479
|
+
if (!store) return;
|
|
480
|
+
|
|
481
|
+
if (typeof dataOrFn === "function") {
|
|
482
|
+
// Run the callback inside the push-callback scope so ctx.use(loader)
|
|
483
|
+
// calls it makes — including after its own awaits, for an async
|
|
484
|
+
// callback — are not registered as handler-to-loader deps and do not
|
|
485
|
+
// trip the deadlock guard. A pushed promise value is not tracked by
|
|
486
|
+
// handleStore.settled and does not block segment resolution, so it
|
|
487
|
+
// cannot form a rendered() deadlock. The ALS scope (not a plain
|
|
488
|
+
// boolean) is what survives the callback's awaits.
|
|
489
|
+
const result = runInsidePushCallbackScope(() =>
|
|
490
|
+
(dataOrFn as () => Promise<unknown>)(),
|
|
491
|
+
);
|
|
492
|
+
store.push(handle.$$id, segmentId, result);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
store.push(handle.$$id, segmentId, dataOrFn);
|
|
497
|
+
},
|
|
498
|
+
);
|
|
499
|
+
}
|
|
325
500
|
|
|
326
|
-
|
|
327
|
-
|
|
501
|
+
// Deadlock guard and handler-to-loader dependency tracking.
|
|
502
|
+
// Skip when inside a DSL loader scope (resolveLoaderData also calls
|
|
503
|
+
// ctx.use() but that's DSL-to-DSL, not handler-to-loader) or when
|
|
504
|
+
// inside a handle push callback (push callbacks don't block segment
|
|
505
|
+
// resolution so they can't cause rendered() deadlocks). The push-callback
|
|
506
|
+
// check is an ALS scope so it also exempts an ASYNC callback's continuation
|
|
507
|
+
// after its first await — relevant on streaming trees, where the guard
|
|
508
|
+
// state now stays live until handleStore.settled.
|
|
509
|
+
const loader = item as LoaderDefinition<any, any>;
|
|
510
|
+
if (!isInsideLoaderScope() && !isInsidePushCallbackScope()) {
|
|
511
|
+
const reqCtx = reqCtxRef ?? _getRequestContext();
|
|
512
|
+
if (reqCtx) {
|
|
513
|
+
// Direction 1: handler awaits loader that already called rendered()
|
|
514
|
+
if (
|
|
515
|
+
loaderPromises.has(loader.$$id) &&
|
|
516
|
+
reqCtx._renderBarrierWaiters?.has(loader.$$id)
|
|
517
|
+
) {
|
|
518
|
+
throw new Error(
|
|
519
|
+
`Deadlock: handler is awaiting loader "${loader.$$id}" which called ctx.rendered(). ` +
|
|
520
|
+
`The loader is waiting for segment resolution, but the handler blocks resolution. ` +
|
|
521
|
+
`Move the data dependency to a loader-to-loader pattern instead.`,
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
// Direction 2: track dep so rendered() can detect the deadlock if the
|
|
525
|
+
// loader calls it later. Skip once the guard window is CLOSED — for a
|
|
526
|
+
// non-streaming tree that is when the barrier resolves (rendered()
|
|
527
|
+
// resolves immediately), and for a streaming tree it is when
|
|
528
|
+
// handleStore.settled completes (rendered() keeps waiting until then, so
|
|
529
|
+
// a loading() handler resuming after the barrier can still form a
|
|
530
|
+
// cycle). Using the explicit guard-closed flag rather than
|
|
531
|
+
// _renderBarrierSegmentOrder keeps tracking live across the streaming
|
|
532
|
+
// settle wait. (Handle push callbacks are already excluded above via
|
|
533
|
+
// isInsidePushCallbackScope(), so they cannot produce false positives
|
|
534
|
+
// here.)
|
|
535
|
+
if (!reqCtx._renderBarrierGuardClosed) {
|
|
536
|
+
if (!reqCtx._handlerLoaderDeps) reqCtx._handlerLoaderDeps = new Set();
|
|
537
|
+
reqCtx._handlerLoaderDeps.add(loader.$$id);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
328
540
|
}
|
|
329
541
|
|
|
330
|
-
return useLoader(
|
|
542
|
+
return useLoader(loader, null);
|
|
331
543
|
}) as typeof ctx.use;
|
|
332
544
|
}
|
|
333
545
|
|
|
@@ -354,18 +566,21 @@ export function setupBuildUse<TEnv>(ctx: HandlerContext<any, TEnv>): void {
|
|
|
354
566
|
);
|
|
355
567
|
}
|
|
356
568
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
)
|
|
360
|
-
|
|
569
|
+
// Wrap with withDefer so ctx.use(Handle).defer(...) works on the build /
|
|
570
|
+
// prerender path, matching production setupLoaderAccess. Without it a
|
|
571
|
+
// prerender handler calling .defer() throws "defer is not a function".
|
|
572
|
+
return withDefer(
|
|
573
|
+
(dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
|
|
574
|
+
if (!store) return;
|
|
361
575
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
576
|
+
const valueOrPromise =
|
|
577
|
+
typeof dataOrFn === "function"
|
|
578
|
+
? (dataOrFn as () => Promise<unknown>)()
|
|
579
|
+
: dataOrFn;
|
|
366
580
|
|
|
367
|
-
|
|
368
|
-
|
|
581
|
+
store.push(handle.$$id, segmentId, valueOrPromise);
|
|
582
|
+
},
|
|
583
|
+
);
|
|
369
584
|
}
|
|
370
585
|
|
|
371
586
|
// Loader case: not available during pre-rendering
|
|
@@ -393,8 +608,11 @@ export function setupLoaderAccessSilent<TEnv>(
|
|
|
393
608
|
|
|
394
609
|
ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
|
|
395
610
|
if (isHandle(item)) {
|
|
396
|
-
// Silent mode - return a no-op so handle data is not pushed during caching
|
|
397
|
-
|
|
611
|
+
// Silent mode - return a no-op so handle data is not pushed during caching.
|
|
612
|
+
// Wrap with withDefer so ctx.use(Handle).defer(...) still resolves to a
|
|
613
|
+
// callable resolver (also a no-op here), matching production's push shape
|
|
614
|
+
// instead of throwing "defer is not a function".
|
|
615
|
+
return withDefer((_dataOrFn: unknown) => {});
|
|
398
616
|
}
|
|
399
617
|
|
|
400
618
|
return useLoader(item as LoaderDefinition<any, any>, null);
|
package/src/router/logging.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
3
3
|
|
|
4
|
-
// -- Revalidation trace types --
|
|
5
|
-
|
|
6
4
|
export interface RevalidationTraceEntry {
|
|
7
5
|
segmentId: string;
|
|
8
6
|
segmentType: string;
|
|
@@ -12,7 +10,10 @@ export interface RevalidationTraceEntry {
|
|
|
12
10
|
| "cache-hit"
|
|
13
11
|
| "loader"
|
|
14
12
|
| "parallel"
|
|
15
|
-
| "orphan-layout"
|
|
13
|
+
| "orphan-layout"
|
|
14
|
+
| "route-handler"
|
|
15
|
+
| "layout-handler"
|
|
16
|
+
| "intercept-loader";
|
|
16
17
|
defaultShouldRevalidate: boolean;
|
|
17
18
|
finalShouldRevalidate: boolean;
|
|
18
19
|
reason: string;
|
|
@@ -33,8 +34,6 @@ export interface RevalidationTrace {
|
|
|
33
34
|
entries: RevalidationTraceEntry[];
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
// -- Log context --
|
|
37
|
-
|
|
38
37
|
interface RouterLogContext {
|
|
39
38
|
requestId: string;
|
|
40
39
|
transactionId: string;
|
|
@@ -71,7 +70,7 @@ function getHeaderRequestId(request: Request): string | null {
|
|
|
71
70
|
return trimmed.length > 0 ? trimmed : null;
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
function getOrCreateRequestId(request: Request): string {
|
|
73
|
+
export function getOrCreateRequestId(request: Request): string {
|
|
75
74
|
const existing = requestIds.get(request);
|
|
76
75
|
if (existing) return existing;
|
|
77
76
|
|
|
@@ -192,8 +191,6 @@ export function debugWarn(
|
|
|
192
191
|
console.warn(`${prefix} ${message}`);
|
|
193
192
|
}
|
|
194
193
|
|
|
195
|
-
// -- Revalidation trace helpers --
|
|
196
|
-
|
|
197
194
|
export function isTraceActive(): boolean {
|
|
198
195
|
if (!INTERNAL_RANGO_DEBUG) return false;
|
|
199
196
|
const ctx = routerLogContext.getStore();
|
package/src/router/manifest.ts
CHANGED
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Router Manifest Loading
|
|
3
|
-
*
|
|
4
|
-
* Handles lazy loading and validation of route manifests.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
import { invariant, RouteNotFoundError } from "../errors";
|
|
8
2
|
import { createRouteHelpers } from "../route-definition";
|
|
9
3
|
import {
|
|
10
4
|
getContext,
|
|
11
5
|
runWithPrefixes,
|
|
6
|
+
getIsolatedLazyParent,
|
|
12
7
|
type EntryData,
|
|
13
8
|
type MetricsStore,
|
|
14
9
|
} from "../server/context";
|
|
15
10
|
import MapRootLayout from "../server/root-layout";
|
|
11
|
+
import { joinPrefix } from "./pattern-matching.js";
|
|
16
12
|
import type { RouteEntry } from "../types";
|
|
17
13
|
import type { UrlPatterns } from "../urls";
|
|
18
14
|
import { VERSION } from "@rangojs/router:version";
|
|
@@ -22,20 +18,19 @@ import { VERSION } from "@rangojs/router:version";
|
|
|
22
18
|
// stable references), so the resulting EntryData tree can be safely cached and reused
|
|
23
19
|
// across requests within the same isolate.
|
|
24
20
|
//
|
|
25
|
-
// Cache is keyed by (VERSION, mountIndex, routeKey, isSSR).
|
|
21
|
+
// Cache is keyed by (VERSION, routerId, mountIndex, routeKey, isSSR). routeKey is
|
|
22
|
+
// REQUIRED in the key: loadManifest() runs the handler with forRoute=routeKey, and
|
|
23
|
+
// path-helper.ts prunes (skips registering) every route except forRoute, so the
|
|
24
|
+
// resulting Store.manifest is pruned to the requested route — NOT the full include.
|
|
25
|
+
// Dropping routeKey would make a sibling route miss and overwrite this entry with its
|
|
26
|
+
// own pruned manifest, so alternating sibling requests would thrash (re-run the
|
|
27
|
+
// handler every time). Running the include handler once per isolate instead of once
|
|
28
|
+
// per route is possible but needs an unpruned manifest cache with prune-on-read — see
|
|
29
|
+
// LP1 in docs/internal/matching-and-lazy-discovery.md. VERSION comes from the
|
|
26
30
|
// @rangojs/router:version virtual module which Vite invalidates on RSC module HMR.
|
|
27
31
|
// When VERSION changes, this module re-evaluates and the cache is recreated empty.
|
|
28
|
-
// Including VERSION in the key is additional defense against stale entries.
|
|
29
32
|
const manifestModuleCache = new Map<string, Map<string, EntryData>>();
|
|
30
33
|
|
|
31
|
-
/**
|
|
32
|
-
* Load manifest from route entry with AsyncLocalStorage context
|
|
33
|
-
* Handles lazy imports, unwrapping, and validation
|
|
34
|
-
*
|
|
35
|
-
* Results are cached at module level after first execution. Subsequent calls
|
|
36
|
-
* for the same (routeKey, isSSR) within the same isolate return cached data
|
|
37
|
-
* without re-executing the DSL handler.
|
|
38
|
-
*/
|
|
39
34
|
/**
|
|
40
35
|
* Clear the module-level manifest cache.
|
|
41
36
|
* Called on HMR to ensure stale handler references are discarded.
|
|
@@ -64,8 +59,12 @@ export async function loadManifest(
|
|
|
64
59
|
|
|
65
60
|
const mountIndex = entry.mountIndex;
|
|
66
61
|
|
|
67
|
-
// Check module-level cache (persists across requests within same isolate)
|
|
68
|
-
|
|
62
|
+
// Check module-level cache (persists across requests within same isolate).
|
|
63
|
+
// Include routerId so multi-router setups (host routing) don't share cached
|
|
64
|
+
// EntryData across routers with overlapping mountIndex + routeKey combinations.
|
|
65
|
+
// routeKey is in the key because loadManifest() builds a manifest pruned to
|
|
66
|
+
// forRoute=routeKey (see path-helper.ts) — see the cache comment above.
|
|
67
|
+
const cacheKey = `${VERSION}:${entry.routerId ?? ""}:${mountIndex ?? ""}:${routeKey}:${isSSR ? 1 : 0}`;
|
|
69
68
|
const cached = manifestModuleCache.get(cacheKey);
|
|
70
69
|
if (cached) {
|
|
71
70
|
const cacheStart = performance.now();
|
|
@@ -85,20 +84,15 @@ export async function loadManifest(
|
|
|
85
84
|
const storeSetupStart = performance.now();
|
|
86
85
|
const Store = getContext().getOrCreateStore(routeKey);
|
|
87
86
|
|
|
88
|
-
// Set mount index in store for unique shortCode prefixes
|
|
89
87
|
Store.mountIndex = mountIndex;
|
|
90
|
-
|
|
91
|
-
// Set isSSR flag so loading() can check if we're in SSR
|
|
92
88
|
Store.isSSR = isSSR;
|
|
93
89
|
|
|
94
|
-
// Attach metrics store to context if provided
|
|
95
90
|
if (metricsStore) {
|
|
96
91
|
Store.metrics = metricsStore;
|
|
97
92
|
}
|
|
98
93
|
|
|
99
94
|
pushMetric?.("manifest:store-setup", storeSetupStart);
|
|
100
95
|
|
|
101
|
-
// Clear manifest before rebuilding to prevent stale entry mutations
|
|
102
96
|
const clearStart = performance.now();
|
|
103
97
|
Store.manifest.clear();
|
|
104
98
|
pushMetric?.("manifest:clear", clearStart);
|
|
@@ -112,36 +106,48 @@ export async function loadManifest(
|
|
|
112
106
|
// This ensures routes are registered under the correct layout hierarchy
|
|
113
107
|
const lazyContext =
|
|
114
108
|
entry.lazy && entry.lazyPatterns ? entry.lazyContext : null;
|
|
115
|
-
const parentForContext =
|
|
116
|
-
(
|
|
109
|
+
const parentForContext = lazyContext
|
|
110
|
+
? getIsolatedLazyParent(
|
|
111
|
+
(lazyContext.parent as EntryData | null) ?? Store.parent,
|
|
112
|
+
)
|
|
113
|
+
: Store.parent;
|
|
117
114
|
|
|
118
115
|
// For lazy entries, merge captured counters from include() so the
|
|
119
116
|
// handler's entries get shortCode indices after sibling entries that
|
|
120
117
|
// were created during pattern extraction. This prevents shortCode
|
|
121
118
|
// collisions between lazy and non-lazy entries under the same parent
|
|
122
119
|
// (e.g., ArticlesLayout and BlogLayout both under NavLayout).
|
|
123
|
-
if (lazyContext
|
|
124
|
-
const
|
|
125
|
-
for (const [key, value] of Object.entries(captured)) {
|
|
120
|
+
if (lazyContext?.counters) {
|
|
121
|
+
for (const [key, value] of Object.entries(lazyContext.counters)) {
|
|
126
122
|
Store.counters[key] = Math.max(Store.counters[key] ?? 0, value);
|
|
127
123
|
}
|
|
128
124
|
}
|
|
129
125
|
|
|
130
126
|
// Propagate cache profiles for DSL-time cache("profileName") resolution.
|
|
131
127
|
// Non-lazy entries carry profiles directly; lazy entries carry them
|
|
132
|
-
// in the captured lazyContext from include() time.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
128
|
+
// in the captured lazyContext from include() time. Always write
|
|
129
|
+
// (including clearing to undefined) so a prior lazy build's profile
|
|
130
|
+
// map cannot leak into a later non-lazy build on the same ALS-backed
|
|
131
|
+
// Store — which would otherwise let cache("name") resolve a profile
|
|
132
|
+
// from an unrelated entry.
|
|
133
|
+
Store.cacheProfiles = entry.cacheProfiles ?? lazyContext?.cacheProfiles;
|
|
138
134
|
|
|
139
135
|
// Propagate rootScoped from lazyContext so that routes inside
|
|
140
136
|
// nested { name: "sub" } under { name: "" } keep inherited root scope
|
|
141
|
-
// when the manifest is rebuilt on each request.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
137
|
+
// when the manifest is rebuilt on each request. Always write
|
|
138
|
+
// (including clearing to undefined, which makes getRootScoped()
|
|
139
|
+
// return its true default) so a prior lazy build's scope cannot leak
|
|
140
|
+
// into a later non-lazy build on the same ALS-backed Store — which
|
|
141
|
+
// would otherwise mis-register plain routes as non-root-scoped and
|
|
142
|
+
// break dot-local reverse resolution.
|
|
143
|
+
Store.rootScoped = lazyContext?.rootScoped;
|
|
144
|
+
|
|
145
|
+
// Propagate includeScope from lazyContext so that direct-descendant
|
|
146
|
+
// shortCodes of this include use the correct scoped counter namespace
|
|
147
|
+
// on every manifest rebuild. Always write (including clearing to
|
|
148
|
+
// undefined) so a prior lazy build's scope cannot leak into a later
|
|
149
|
+
// non-lazy build on the same ALS-backed Store.
|
|
150
|
+
Store.includeScope = lazyContext?.includeScope;
|
|
145
151
|
|
|
146
152
|
const handlerExecStart = performance.now();
|
|
147
153
|
const useItems = await getContext().runWithStore(
|
|
@@ -161,7 +167,10 @@ export async function loadManifest(
|
|
|
161
167
|
if (entry.lazy && entry.lazyPatterns) {
|
|
162
168
|
const lazyPatterns = entry.lazyPatterns as UrlPatterns<any>;
|
|
163
169
|
const includePrefix = (entry as any)._lazyPrefix || "";
|
|
164
|
-
|
|
170
|
+
// Slash-collapsing join so a trailing-slash parent prefix does not
|
|
171
|
+
// bake a double slash into the registered route patterns (must match
|
|
172
|
+
// the same join in evaluateLazyEntry / the build-time runWithPrefixes).
|
|
173
|
+
const fullPrefix = joinPrefix(lazyContext?.urlPrefix, includePrefix);
|
|
165
174
|
|
|
166
175
|
if (fullPrefix || lazyContext?.namePrefix) {
|
|
167
176
|
return runWithPrefixes(fullPrefix, lazyContext?.namePrefix, () =>
|
|
@@ -171,20 +180,16 @@ export async function loadManifest(
|
|
|
171
180
|
return lazyPatterns.handler();
|
|
172
181
|
}
|
|
173
182
|
|
|
174
|
-
// Wrap handler execution in root layout so routes get correct parent
|
|
175
|
-
// This ensures all routes are registered with the layout as their parent
|
|
176
183
|
let promiseResult: Promise<any> | null = null;
|
|
177
184
|
const wrappedItems = helpers.layout(MapRootLayout, () => {
|
|
178
185
|
const result = entry.handler();
|
|
179
186
|
if (result instanceof Promise) {
|
|
180
|
-
// Lazy handler detected - capture promise for async handling
|
|
181
187
|
promiseResult = result;
|
|
182
|
-
return [];
|
|
188
|
+
return [];
|
|
183
189
|
}
|
|
184
190
|
return result;
|
|
185
191
|
});
|
|
186
192
|
|
|
187
|
-
// Handle lazy (Promise-based) handlers
|
|
188
193
|
if (promiseResult !== null) {
|
|
189
194
|
const load = await (promiseResult as Promise<any>);
|
|
190
195
|
if (
|
|
@@ -218,7 +223,6 @@ export async function loadManifest(
|
|
|
218
223
|
);
|
|
219
224
|
}
|
|
220
225
|
|
|
221
|
-
// Inline handler - routes were registered with correct parent inside layout
|
|
222
226
|
return [wrappedItems].flat(3);
|
|
223
227
|
},
|
|
224
228
|
);
|