@rangojs/router 0.0.0-experimental.31 → 0.0.0-experimental.3232cd17
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 +4 -0
- package/README.md +198 -44
- package/dist/bin/rango.js +287 -105
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3248 -1117
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +73 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +107 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +245 -21
- package/skills/caching/SKILL.md +302 -6
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +270 -30
- package/skills/host-router/SKILL.md +82 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +49 -5
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +294 -30
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +203 -7
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +250 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +97 -5
- package/skills/router-setup/SKILL.md +90 -5
- 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 +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -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 +120 -0
- package/skills/typesafety/SKILL.md +329 -27
- package/skills/use-cache/SKILL.md +36 -5
- 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 +67 -40
- 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/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +86 -147
- 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/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +148 -19
- package/src/browser/navigation-client.ts +187 -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/partial-update.ts +123 -94
- package/src/browser/prefetch/cache.ts +214 -36
- package/src/browser/prefetch/fetch.ts +260 -38
- package/src/browser/prefetch/policy.ts +6 -0
- 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 +93 -11
- package/src/browser/react/NavigationProvider.tsx +115 -34
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +49 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -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-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +22 -5
- 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 +52 -1
- package/src/browser/rsc-router.tsx +215 -76
- package/src/browser/scroll-restoration.ts +46 -39
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +176 -50
- package/src/browser/types.ts +95 -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 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +137 -32
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -96
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +149 -43
- package/src/cache/cache-scope.ts +148 -81
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2550 -93
- package/src/cache/cf/index.ts +11 -17
- package/src/cache/document-cache.ts +78 -27
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +108 -290
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +84 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- 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 +52 -26
- package/src/index.ts +100 -38
- package/src/internal-debug.ts +2 -4
- 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/network-error-thrower.tsx +1 -6
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +37 -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 +437 -274
- 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 +52 -10
- 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 +108 -9
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +45 -22
- package/src/router/handler-context.ts +83 -41
- package/src/router/intercept-resolution.ts +25 -23
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +213 -30
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +49 -45
- package/src/router/match-api.ts +121 -205
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +27 -6
- package/src/router/match-middleware/cache-lookup.ts +205 -249
- package/src/router/match-middleware/cache-store.ts +45 -32
- 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 +173 -143
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +109 -63
- package/src/router/prerender-match.ts +192 -54
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +63 -55
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +6 -28
- package/src/router/router-interfaces.ts +100 -35
- package/src/router/router-options.ts +91 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +242 -75
- package/src/router/segment-resolution/helpers.ts +64 -25
- package/src/router/segment-resolution/loader-cache.ts +41 -37
- package/src/router/segment-resolution/revalidation.ts +456 -372
- 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 +4 -1
- package/src/router/segment-wrappers.ts +2 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +91 -46
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +134 -43
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +492 -383
- package/src/rsc/helpers.ts +162 -46
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +30 -3
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +90 -63
- package/src/rsc/rsc-rendering.ts +56 -54
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +74 -67
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +25 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +134 -0
- package/src/segment-system.tsx +272 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +309 -61
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +26 -24
- package/src/server/loader-registry.ts +10 -28
- package/src/server/request-context.ts +348 -128
- package/src/ssr/index.tsx +23 -15
- 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 +618 -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 +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -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 +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -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 +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- 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 +107 -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 +192 -99
- 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 +51 -6
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +8 -0
- package/src/vite/plugin-types.ts +187 -69
- package/src/vite/plugins/cjs-to-esm.ts +8 -18
- 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 +11 -50
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
- package/src/vite/plugins/expose-internal-ids.ts +554 -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/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +41 -20
- package/src/vite/plugins/virtual-entries.ts +2 -17
- package/src/vite/rango.ts +257 -289
- package/src/vite/router-discovery.ts +930 -140
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- 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/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 +27 -29
- 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/router/middleware-cookies.ts +0 -55
package/src/router/middleware.ts
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
/// <reference types="vite/types/importMeta.d.ts" />
|
|
2
|
-
/**
|
|
3
|
-
* Middleware Execution
|
|
4
|
-
*
|
|
5
|
-
* True middleware that wraps the entire RSC handler.
|
|
6
|
-
* - `await next()` returns actual Response
|
|
7
|
-
* - Can modify response headers
|
|
8
|
-
* - Can catch errors from RSC rendering
|
|
9
|
-
* - Forgiving API: if middleware doesn't return, original response is used
|
|
10
|
-
*/
|
|
11
2
|
|
|
12
3
|
import { contextGet, contextSet } from "../context-var.js";
|
|
4
|
+
import { safeDecodeURIComponent } from "./url-params.js";
|
|
5
|
+
import { fireAndForgetWaitUntil } from "../types/request-scope.js";
|
|
13
6
|
import type {
|
|
14
7
|
CollectedMiddleware,
|
|
15
8
|
MiddlewareCollectableEntry,
|
|
@@ -19,26 +12,28 @@ import type {
|
|
|
19
12
|
ResponseHolder,
|
|
20
13
|
} from "./middleware-types.js";
|
|
21
14
|
import { _getRequestContext } from "../server/request-context.js";
|
|
15
|
+
import {
|
|
16
|
+
EXTERNAL_REDIRECT_MARKER,
|
|
17
|
+
isExternalRedirect,
|
|
18
|
+
markExternalRedirect,
|
|
19
|
+
} from "../redirect-origin.js";
|
|
22
20
|
import { isAutoGeneratedRouteName } from "../route-name.js";
|
|
23
21
|
import { appendMetric, createMetricsStore } from "./metrics.js";
|
|
22
|
+
import { stripInternalParams } from "./handler-context.js";
|
|
23
|
+
import { isWebSocketUpgradeResponse } from "../response-utils.js";
|
|
24
24
|
|
|
25
|
-
// Re-export types
|
|
25
|
+
// Re-export types consumed through this module's path.
|
|
26
26
|
export type {
|
|
27
27
|
CookieOptions,
|
|
28
|
-
CollectedMiddleware,
|
|
29
|
-
MiddlewareCollectableEntry,
|
|
30
28
|
MiddlewareContext,
|
|
31
29
|
MiddlewareEntry,
|
|
32
30
|
MiddlewareFn,
|
|
33
|
-
ResponseHolder,
|
|
34
31
|
} from "./middleware-types.js";
|
|
35
|
-
export { parseCookies, serializeCookie } from "./middleware-cookies.js";
|
|
36
32
|
|
|
37
33
|
const MIDDLEWARE_METRIC_DEPTH = 1;
|
|
38
|
-
/** Ignore post-next() durations below this threshold (measurement noise). */
|
|
39
34
|
const POST_METRIC_MIN_DURATION_MS = 0.01;
|
|
40
35
|
|
|
41
|
-
function
|
|
36
|
+
function getMiddlewareMetricLabel<TEnv>(
|
|
42
37
|
entry: MiddlewareEntry<TEnv>,
|
|
43
38
|
ordinal: number,
|
|
44
39
|
): string {
|
|
@@ -46,23 +41,12 @@ function getMiddlewareMetricBase<TEnv>(
|
|
|
46
41
|
const scope = entry.pattern ?? "*";
|
|
47
42
|
|
|
48
43
|
if (handlerName) {
|
|
49
|
-
return
|
|
44
|
+
return `middleware:${handlerName}@${scope}`;
|
|
50
45
|
}
|
|
51
46
|
|
|
52
|
-
return
|
|
47
|
+
return `middleware:${scope}#${ordinal + 1}`;
|
|
53
48
|
}
|
|
54
49
|
|
|
55
|
-
function getMiddlewareMetricLabel<TEnv>(
|
|
56
|
-
entry: MiddlewareEntry<TEnv>,
|
|
57
|
-
ordinal: number,
|
|
58
|
-
): string {
|
|
59
|
-
return `middleware:${getMiddlewareMetricBase(entry, ordinal)}`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Parse a route pattern into regex and param names
|
|
64
|
-
* Supports: *, /path, /path/*, /path/:param, /path/:param/*
|
|
65
|
-
*/
|
|
66
50
|
export function parsePattern(pattern: string): {
|
|
67
51
|
regex: RegExp;
|
|
68
52
|
paramNames: string[];
|
|
@@ -111,7 +95,12 @@ function escapeRegex(str: string): string {
|
|
|
111
95
|
}
|
|
112
96
|
|
|
113
97
|
/**
|
|
114
|
-
* Extract params from a pathname using a pattern's regex and param names
|
|
98
|
+
* Extract params from a pathname using a pattern's regex and param names.
|
|
99
|
+
*
|
|
100
|
+
* Values are URL-decoded so apps see the raw string (e.g. "ivo@example.com")
|
|
101
|
+
* instead of the percent-encoded form ("ivo%40example.com"). This matches the
|
|
102
|
+
* contract assumed by ctx.reverse (which re-encodes) and aligns with
|
|
103
|
+
* Express/React Router/Fastify/Koa.
|
|
115
104
|
*/
|
|
116
105
|
export function extractParams(
|
|
117
106
|
pathname: string,
|
|
@@ -123,7 +112,7 @@ export function extractParams(
|
|
|
123
112
|
|
|
124
113
|
const params: Record<string, string> = {};
|
|
125
114
|
for (let i = 0; i < paramNames.length; i++) {
|
|
126
|
-
params[paramNames[i]] = match[i + 1] || "";
|
|
115
|
+
params[paramNames[i]] = safeDecodeURIComponent(match[i + 1] || "");
|
|
127
116
|
}
|
|
128
117
|
return params;
|
|
129
118
|
}
|
|
@@ -147,7 +136,7 @@ export function createMiddlewareContext<TEnv>(
|
|
|
147
136
|
search?: Record<string, unknown>,
|
|
148
137
|
) => string,
|
|
149
138
|
): MiddlewareContext<TEnv> {
|
|
150
|
-
const url = new URL(request.url);
|
|
139
|
+
const url = stripInternalParams(new URL(request.url));
|
|
151
140
|
|
|
152
141
|
// Track the initial response to detect pre/post-next() phase.
|
|
153
142
|
// Before next(): responseHolder.response === initialResponse (the stub).
|
|
@@ -178,14 +167,22 @@ export function createMiddlewareContext<TEnv>(
|
|
|
178
167
|
return responseHolder.response;
|
|
179
168
|
};
|
|
180
169
|
|
|
170
|
+
// Capture reqCtx once: the request-scoped platform fields
|
|
171
|
+
// (originalUrl, executionContext, waitUntil) are immutable per request,
|
|
172
|
+
// so snapshotting beats re-reading ALS on every access. The lazy getters
|
|
173
|
+
// below (routeName, theme, setTheme) stay lazy because those can change
|
|
174
|
+
// during `await next()`.
|
|
175
|
+
const reqCtx = _getRequestContext();
|
|
181
176
|
return {
|
|
182
177
|
request,
|
|
183
178
|
url,
|
|
184
|
-
originalUrl: new URL(request.url),
|
|
179
|
+
originalUrl: reqCtx?.originalUrl ?? new URL(request.url),
|
|
185
180
|
pathname: url.pathname,
|
|
186
181
|
searchParams: url.searchParams,
|
|
187
182
|
env: env as MiddlewareContext<TEnv>["env"],
|
|
188
183
|
params,
|
|
184
|
+
executionContext: reqCtx?.executionContext,
|
|
185
|
+
waitUntil: reqCtx ? reqCtx.waitUntil.bind(reqCtx) : fireAndForgetWaitUntil,
|
|
189
186
|
// Getter: re-derives from request context on each access so that global
|
|
190
187
|
// middleware sees the matched route name after await next().
|
|
191
188
|
get routeName(): MiddlewareContext<TEnv>["routeName"] {
|
|
@@ -203,12 +200,9 @@ export function createMiddlewareContext<TEnv>(
|
|
|
203
200
|
get: ((keyOrVar: any) =>
|
|
204
201
|
contextGet(variables, keyOrVar)) as MiddlewareContext<TEnv>["get"],
|
|
205
202
|
|
|
206
|
-
set: ((keyOrVar: any, value: unknown) => {
|
|
207
|
-
contextSet(variables, keyOrVar, value);
|
|
203
|
+
set: ((keyOrVar: any, value: unknown, options?: any) => {
|
|
204
|
+
contextSet(variables, keyOrVar, value, options);
|
|
208
205
|
}) as MiddlewareContext<TEnv>["set"],
|
|
209
|
-
|
|
210
|
-
var: variables as MiddlewareContext<TEnv>["var"],
|
|
211
|
-
|
|
212
206
|
header(name: string, value: string): void {
|
|
213
207
|
// Before next(): delegate to shared RequestContext stub
|
|
214
208
|
if (isPreNext()) {
|
|
@@ -247,9 +241,13 @@ export function createMiddlewareContext<TEnv>(
|
|
|
247
241
|
|
|
248
242
|
reverse:
|
|
249
243
|
reverse ??
|
|
250
|
-
((
|
|
244
|
+
((
|
|
245
|
+
name: string,
|
|
246
|
+
_params?: Record<string, string>,
|
|
247
|
+
_search?: Record<string, unknown>,
|
|
248
|
+
) => {
|
|
251
249
|
throw new Error(
|
|
252
|
-
`ctx.reverse() is not available
|
|
250
|
+
`ctx.reverse(${JSON.stringify(name)}) is not available: no route map is bound to this middleware context.`,
|
|
253
251
|
);
|
|
254
252
|
}),
|
|
255
253
|
|
|
@@ -293,6 +291,88 @@ export function matchMiddleware<TEnv>(
|
|
|
293
291
|
return matches;
|
|
294
292
|
}
|
|
295
293
|
|
|
294
|
+
// Set-Cookie is appended; for other headers stubOverridesNonCookie=true
|
|
295
|
+
// overwrites (chain ran to completion), false fills only missing slots (an
|
|
296
|
+
// explicit short-circuit Response's own headers win).
|
|
297
|
+
function mergeStubHeaders(
|
|
298
|
+
target: Headers,
|
|
299
|
+
stub: Headers,
|
|
300
|
+
stubOverridesNonCookie: boolean,
|
|
301
|
+
): void {
|
|
302
|
+
stub.forEach((value, name) => {
|
|
303
|
+
// The reserved external-redirect marker is internal and never a trust
|
|
304
|
+
// signal; never copy a stub value (e.g. a stray ctx.header() call) onto a
|
|
305
|
+
// browser-facing response. The opt-in is the out-of-band brand.
|
|
306
|
+
if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
|
|
307
|
+
if (name.toLowerCase() === "set-cookie") {
|
|
308
|
+
target.append(name, value);
|
|
309
|
+
} else if (stubOverridesNonCookie || !target.has(name)) {
|
|
310
|
+
target.set(name, value);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Set-Cookie is deduped so a nested inner executeMiddleware that already merged
|
|
316
|
+
// the same reqCtx cookies does not duplicate them; other headers fill if missing.
|
|
317
|
+
function mergeReqCtxStub(
|
|
318
|
+
target: Headers,
|
|
319
|
+
reqCtx: ReturnType<typeof _getRequestContext>,
|
|
320
|
+
): void {
|
|
321
|
+
if (!reqCtx) return;
|
|
322
|
+
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
323
|
+
if (stubCookies.length > 0) {
|
|
324
|
+
const existing = new Set(target.getSetCookie());
|
|
325
|
+
for (const cookie of stubCookies) {
|
|
326
|
+
if (!existing.has(cookie)) {
|
|
327
|
+
target.append("set-cookie", cookie);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
reqCtx.res.headers.forEach((value, name) => {
|
|
332
|
+
// Never propagate the reserved external-redirect marker (see mergeStubHeaders).
|
|
333
|
+
if (name.toLowerCase() === EXTERNAL_REDIRECT_MARKER) return;
|
|
334
|
+
if (name !== "set-cookie" && !target.has(name)) {
|
|
335
|
+
target.set(name, value);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Clone `base` with stub headers merged into a fresh Headers (the clone keeps
|
|
341
|
+
// the body mutable for post-next() modifications). Set-Cookie is always
|
|
342
|
+
// appended; other headers obey stubOverridesNonCookie (see mergeStubHeaders).
|
|
343
|
+
// mergeReqCtx folds in RequestContext stub cookies/headers; the intercept
|
|
344
|
+
// short-circuit path passes false (its reqCtx headers are not merged here),
|
|
345
|
+
// which is the one deliberate divergence between the call sites.
|
|
346
|
+
function mergeResponse(
|
|
347
|
+
base: Response,
|
|
348
|
+
stub: Headers,
|
|
349
|
+
opts: { stubOverridesNonCookie: boolean; mergeReqCtx: boolean },
|
|
350
|
+
): Response {
|
|
351
|
+
const mergedHeaders = new Headers(base.headers);
|
|
352
|
+
// The reserved external-redirect marker is never a trust signal and must never
|
|
353
|
+
// reach the browser. The guard strips it on 3xx redirects; strip it here too so
|
|
354
|
+
// a forged value cannot ride a non-3xx middleware response (which the 3xx-only
|
|
355
|
+
// guard would not touch) to the client. The opt-in is the out-of-band brand.
|
|
356
|
+
mergedHeaders.delete(EXTERNAL_REDIRECT_MARKER);
|
|
357
|
+
mergeStubHeaders(mergedHeaders, stub, opts.stubOverridesNonCookie);
|
|
358
|
+
if (opts.mergeReqCtx) {
|
|
359
|
+
mergeReqCtxStub(mergedHeaders, _getRequestContext());
|
|
360
|
+
}
|
|
361
|
+
const merged = new Response(base.body, {
|
|
362
|
+
status: base.status,
|
|
363
|
+
statusText: base.statusText,
|
|
364
|
+
headers: mergedHeaders,
|
|
365
|
+
});
|
|
366
|
+
// Transfer the out-of-band external-redirect brand across this rebuild: a
|
|
367
|
+
// middleware short-circuit `return redirect(url, { external: true })` reaches
|
|
368
|
+
// the open-redirect guard only after this merge, and the brand lives on the
|
|
369
|
+
// Response object, not in its headers.
|
|
370
|
+
if (isExternalRedirect(base)) {
|
|
371
|
+
markExternalRedirect(merged);
|
|
372
|
+
}
|
|
373
|
+
return merged;
|
|
374
|
+
}
|
|
375
|
+
|
|
296
376
|
/**
|
|
297
377
|
* Execute middleware chain
|
|
298
378
|
*
|
|
@@ -301,7 +381,7 @@ export function matchMiddleware<TEnv>(
|
|
|
301
381
|
* - `ctx.headers` available before and after `await next()`
|
|
302
382
|
* - `ctx.header()` shorthand for setting a single header
|
|
303
383
|
* - Forgiving: if middleware doesn't return, uses the downstream response
|
|
304
|
-
* - Short-circuit: return Response to stop chain
|
|
384
|
+
* - Short-circuit: return OR throw a Response to stop chain
|
|
305
385
|
* - Error catching: try/catch around `next()` works
|
|
306
386
|
*/
|
|
307
387
|
export async function executeMiddleware<TEnv>(
|
|
@@ -331,42 +411,16 @@ export async function executeMiddleware<TEnv>(
|
|
|
331
411
|
// End of chain - call actual RSC handler
|
|
332
412
|
const response = await finalHandler();
|
|
333
413
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
stubResponse.headers.forEach((value, name) => {
|
|
338
|
-
if (name.toLowerCase() === "set-cookie") {
|
|
339
|
-
mergedHeaders.append(name, value);
|
|
340
|
-
} else {
|
|
341
|
-
mergedHeaders.set(name, value);
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
// Also merge shared RequestContext stub (cookies written via cookies().set()).
|
|
345
|
-
// Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
|
|
346
|
-
// may have already merged the same reqCtx cookies into the response.
|
|
347
|
-
const reqCtx = _getRequestContext();
|
|
348
|
-
if (reqCtx) {
|
|
349
|
-
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
350
|
-
if (stubCookies.length > 0) {
|
|
351
|
-
const existing = new Set(mergedHeaders.getSetCookie());
|
|
352
|
-
for (const cookie of stubCookies) {
|
|
353
|
-
if (!existing.has(cookie)) {
|
|
354
|
-
mergedHeaders.append("set-cookie", cookie);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
reqCtx.res.headers.forEach((value, name) => {
|
|
359
|
-
if (name !== "set-cookie" && !mergedHeaders.has(name)) {
|
|
360
|
-
mergedHeaders.set(name, value);
|
|
361
|
-
}
|
|
362
|
-
});
|
|
414
|
+
if (isWebSocketUpgradeResponse(response)) {
|
|
415
|
+
responseHolder.response = response;
|
|
416
|
+
return response;
|
|
363
417
|
}
|
|
364
418
|
|
|
365
|
-
//
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
419
|
+
// Chain ran to completion: stub headers overwrite (stubOverridesNonCookie)
|
|
420
|
+
// and reqCtx stub headers are merged in.
|
|
421
|
+
responseHolder.response = mergeResponse(response, stubResponse.headers, {
|
|
422
|
+
stubOverridesNonCookie: true,
|
|
423
|
+
mergeReqCtx: true,
|
|
370
424
|
});
|
|
371
425
|
|
|
372
426
|
return responseHolder.response;
|
|
@@ -428,8 +482,16 @@ export async function executeMiddleware<TEnv>(
|
|
|
428
482
|
try {
|
|
429
483
|
result = await entry.handler(ctx, wrappedNext);
|
|
430
484
|
} catch (error) {
|
|
431
|
-
|
|
432
|
-
|
|
485
|
+
// Thrown Response is short-circuit control flow, not an error.
|
|
486
|
+
// Fall through to the `if (result instanceof Response)` branch below
|
|
487
|
+
// so stub headers and request-context cookies merge as they do for
|
|
488
|
+
// an explicit `return new Response(...)`. Real errors propagate.
|
|
489
|
+
if (error instanceof Response) {
|
|
490
|
+
result = error;
|
|
491
|
+
} else {
|
|
492
|
+
finishMiddleware();
|
|
493
|
+
throw error;
|
|
494
|
+
}
|
|
433
495
|
}
|
|
434
496
|
finishMiddleware();
|
|
435
497
|
|
|
@@ -450,41 +512,18 @@ export async function executeMiddleware<TEnv>(
|
|
|
450
512
|
|
|
451
513
|
// Explicit return takes precedence (middleware short-circuit).
|
|
452
514
|
// Merge stub headers (from ctx.header before this point) and
|
|
453
|
-
// RequestContext stub headers (from
|
|
515
|
+
// RequestContext stub headers (from cookies().set()) into the
|
|
454
516
|
// returned Response so they are not lost.
|
|
455
517
|
if (result instanceof Response) {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
mergedHeaders.append(name, value);
|
|
460
|
-
} else if (!mergedHeaders.has(name)) {
|
|
461
|
-
mergedHeaders.set(name, value);
|
|
462
|
-
}
|
|
463
|
-
});
|
|
464
|
-
// Also merge shared RequestContext stub (cookies written via setCookie).
|
|
465
|
-
// Dedup Set-Cookie: an inner executeMiddleware (route-level middleware)
|
|
466
|
-
// may have already merged the same reqCtx cookies into the response.
|
|
467
|
-
const reqCtx = _getRequestContext();
|
|
468
|
-
if (reqCtx) {
|
|
469
|
-
const stubCookies = reqCtx.res.headers.getSetCookie();
|
|
470
|
-
if (stubCookies.length > 0) {
|
|
471
|
-
const existing = new Set(mergedHeaders.getSetCookie());
|
|
472
|
-
for (const cookie of stubCookies) {
|
|
473
|
-
if (!existing.has(cookie)) {
|
|
474
|
-
mergedHeaders.append("set-cookie", cookie);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
reqCtx.res.headers.forEach((value, name) => {
|
|
479
|
-
if (name !== "set-cookie" && !mergedHeaders.has(name)) {
|
|
480
|
-
mergedHeaders.set(name, value);
|
|
481
|
-
}
|
|
482
|
-
});
|
|
518
|
+
if (isWebSocketUpgradeResponse(result)) {
|
|
519
|
+
responseHolder.response = result;
|
|
520
|
+
return result;
|
|
483
521
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
522
|
+
// Explicit short-circuit: the returned Response's own headers win
|
|
523
|
+
// (stubOverridesNonCookie=false); reqCtx stub headers still merge in.
|
|
524
|
+
const merged = mergeResponse(result, stubResponse.headers, {
|
|
525
|
+
stubOverridesNonCookie: false,
|
|
526
|
+
mergeReqCtx: true,
|
|
488
527
|
});
|
|
489
528
|
responseHolder.response = merged;
|
|
490
529
|
return merged;
|
|
@@ -529,23 +568,12 @@ export async function executeMiddleware<TEnv>(
|
|
|
529
568
|
// last merge point (e.g. cookies().set() called after await next()).
|
|
530
569
|
// The reqCtx stub may have already been partially merged during finalHandler
|
|
531
570
|
// or early-return paths; only append *new* Set-Cookie entries to avoid dupes.
|
|
571
|
+
//
|
|
572
|
+
// Skip for upgrade responses: upgrade headers are semantically immutable and
|
|
573
|
+
// set-cookie on an upgrade is not meaningful.
|
|
532
574
|
const reqCtx = _getRequestContext();
|
|
533
|
-
if (reqCtx) {
|
|
534
|
-
|
|
535
|
-
if (stubCookies.length > 0) {
|
|
536
|
-
const existingCookies = new Set(finalResponse.headers.getSetCookie());
|
|
537
|
-
for (const cookie of stubCookies) {
|
|
538
|
-
if (!existingCookies.has(cookie)) {
|
|
539
|
-
finalResponse.headers.append("set-cookie", cookie);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
// Fill in non-cookie headers that aren't already on the response
|
|
544
|
-
reqCtx.res.headers.forEach((value, name) => {
|
|
545
|
-
if (name !== "set-cookie" && !finalResponse.headers.has(name)) {
|
|
546
|
-
finalResponse.headers.set(name, value);
|
|
547
|
-
}
|
|
548
|
-
});
|
|
575
|
+
if (reqCtx && !isWebSocketUpgradeResponse(finalResponse)) {
|
|
576
|
+
mergeReqCtxStub(finalResponse.headers, reqCtx);
|
|
549
577
|
}
|
|
550
578
|
|
|
551
579
|
return finalResponse;
|
|
@@ -556,7 +584,7 @@ export async function executeMiddleware<TEnv>(
|
|
|
556
584
|
*
|
|
557
585
|
* Intercepts use a shared stubResponse from the request context. This function:
|
|
558
586
|
* - Runs middleware in sequence with a simple next() chain
|
|
559
|
-
* - Returns Response if any middleware short-circuits (returns Response or redirects BEFORE next())
|
|
587
|
+
* - Returns Response if any middleware short-circuits (returns OR throws a Response, or redirects, BEFORE next())
|
|
560
588
|
* - Returns null if all middleware calls next() - headers set after next() remain on stubResponse
|
|
561
589
|
*
|
|
562
590
|
* @param middlewares - Array of middleware functions
|
|
@@ -615,7 +643,18 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
615
643
|
return next();
|
|
616
644
|
};
|
|
617
645
|
|
|
618
|
-
|
|
646
|
+
let result: Response | void;
|
|
647
|
+
try {
|
|
648
|
+
result = await middleware(ctx, guardedNext);
|
|
649
|
+
} catch (error) {
|
|
650
|
+
// Thrown Response is short-circuit control flow, parity with the
|
|
651
|
+
// explicit-return path below. Real errors propagate.
|
|
652
|
+
if (error instanceof Response) {
|
|
653
|
+
result = error;
|
|
654
|
+
} else {
|
|
655
|
+
throw error;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
619
658
|
|
|
620
659
|
if (result instanceof Response) {
|
|
621
660
|
earlyResponse = result;
|
|
@@ -639,21 +678,13 @@ export async function executeInterceptMiddleware<TEnv>(
|
|
|
639
678
|
});
|
|
640
679
|
|
|
641
680
|
if (hasStubHeaders) {
|
|
642
|
-
//
|
|
643
|
-
//
|
|
644
|
-
//
|
|
645
|
-
|
|
646
|
-
stubResponse.headers
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
} else if (!mergedHeaders.has(name)) {
|
|
650
|
-
mergedHeaders.set(name, value);
|
|
651
|
-
}
|
|
652
|
-
});
|
|
653
|
-
return new Response(response.body, {
|
|
654
|
-
status: response.status,
|
|
655
|
-
statusText: response.statusText,
|
|
656
|
-
headers: mergedHeaders,
|
|
681
|
+
// Only fill in missing headers — the returned Response's explicit headers
|
|
682
|
+
// take precedence (stubOverridesNonCookie=false), matching executeMiddleware.
|
|
683
|
+
// mergeReqCtx=false: the intercept path deliberately does NOT merge reqCtx
|
|
684
|
+
// stub headers here (pinned by intercept-middleware-headers.test.ts).
|
|
685
|
+
return mergeResponse(response, stubResponse.headers, {
|
|
686
|
+
stubOverridesNonCookie: false,
|
|
687
|
+
mergeReqCtx: false,
|
|
657
688
|
});
|
|
658
689
|
}
|
|
659
690
|
return response;
|
|
@@ -694,7 +725,6 @@ export async function executeLoaderMiddleware<TEnv>(
|
|
|
694
725
|
regex: null,
|
|
695
726
|
paramNames: [],
|
|
696
727
|
handler,
|
|
697
|
-
mountPrefix: null,
|
|
698
728
|
} as MiddlewareEntry<TEnv>,
|
|
699
729
|
params,
|
|
700
730
|
}));
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { RouteMatchResult } from "./pattern-matching.js";
|
|
2
|
+
|
|
3
|
+
export interface NavigationSnapshot {
|
|
4
|
+
prevUrl: URL;
|
|
5
|
+
prevParams: Record<string, string>;
|
|
6
|
+
prevMatch: RouteMatchResult | null;
|
|
7
|
+
|
|
8
|
+
interceptContextUrl: URL;
|
|
9
|
+
interceptContextMatch: RouteMatchResult | null;
|
|
10
|
+
|
|
11
|
+
clientSegmentIds: string[];
|
|
12
|
+
clientSegmentSet: Set<string>;
|
|
13
|
+
filteredSegmentIds: string[];
|
|
14
|
+
|
|
15
|
+
stale: boolean;
|
|
16
|
+
|
|
17
|
+
isSameRouteNavigation: boolean;
|
|
18
|
+
|
|
19
|
+
effectiveFromUrl: URL;
|
|
20
|
+
effectiveFromMatch: RouteMatchResult | null;
|
|
21
|
+
|
|
22
|
+
hasInterceptSource: boolean;
|
|
23
|
+
|
|
24
|
+
isHmr: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ResolveNavigationDeps {
|
|
28
|
+
findMatch: (pathname: string) => RouteMatchResult | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolveNavigation(
|
|
32
|
+
request: Request,
|
|
33
|
+
url: URL,
|
|
34
|
+
currentRouteKey: string,
|
|
35
|
+
deps: ResolveNavigationDeps,
|
|
36
|
+
): NavigationSnapshot | null {
|
|
37
|
+
const clientSegmentIds =
|
|
38
|
+
url.searchParams.get("_rsc_segments")?.split(",").filter(Boolean) || [];
|
|
39
|
+
const stale = url.searchParams.get("_rsc_stale") === "true";
|
|
40
|
+
const previousUrl =
|
|
41
|
+
request.headers.get("X-RSC-Router-Client-Path") ||
|
|
42
|
+
request.headers.get("Referer");
|
|
43
|
+
const interceptSourceUrl = request.headers.get(
|
|
44
|
+
"X-RSC-Router-Intercept-Source",
|
|
45
|
+
);
|
|
46
|
+
const isHmr = !!request.headers.get("X-RSC-HMR");
|
|
47
|
+
|
|
48
|
+
if (!previousUrl) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let prevUrl: URL;
|
|
53
|
+
try {
|
|
54
|
+
prevUrl = new URL(previousUrl, url.origin);
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let interceptContextUrl: URL;
|
|
60
|
+
try {
|
|
61
|
+
interceptContextUrl = interceptSourceUrl
|
|
62
|
+
? new URL(interceptSourceUrl, url.origin)
|
|
63
|
+
: prevUrl;
|
|
64
|
+
} catch {
|
|
65
|
+
interceptContextUrl = prevUrl;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const prevMatch = deps.findMatch(prevUrl.pathname);
|
|
69
|
+
const prevParams = prevMatch?.params || {};
|
|
70
|
+
const interceptContextMatch = interceptSourceUrl
|
|
71
|
+
? deps.findMatch(interceptContextUrl.pathname)
|
|
72
|
+
: prevMatch;
|
|
73
|
+
|
|
74
|
+
const isSameRouteNavigation = !!(
|
|
75
|
+
interceptContextMatch && interceptContextMatch.routeKey === currentRouteKey
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const hasInterceptSource = !!interceptSourceUrl;
|
|
79
|
+
const effectiveFromUrl = hasInterceptSource ? interceptContextUrl : prevUrl;
|
|
80
|
+
const effectiveFromMatch = hasInterceptSource
|
|
81
|
+
? interceptContextMatch
|
|
82
|
+
: prevMatch;
|
|
83
|
+
|
|
84
|
+
const filteredSegmentIds = clientSegmentIds.filter((id) => {
|
|
85
|
+
if (id.includes(".@")) return false;
|
|
86
|
+
if (/D\d+\./.test(id)) return false;
|
|
87
|
+
return true;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const clientSegmentSet = new Set(clientSegmentIds);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
prevUrl,
|
|
94
|
+
prevParams,
|
|
95
|
+
prevMatch,
|
|
96
|
+
interceptContextUrl,
|
|
97
|
+
interceptContextMatch,
|
|
98
|
+
clientSegmentIds,
|
|
99
|
+
clientSegmentSet,
|
|
100
|
+
filteredSegmentIds,
|
|
101
|
+
stale,
|
|
102
|
+
isSameRouteNavigation,
|
|
103
|
+
effectiveFromUrl,
|
|
104
|
+
effectiveFromMatch,
|
|
105
|
+
hasInterceptSource,
|
|
106
|
+
isHmr,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function createNavigationSnapshot(
|
|
111
|
+
overrides?: Partial<NavigationSnapshot>,
|
|
112
|
+
): NavigationSnapshot {
|
|
113
|
+
const defaultUrl = new URL("http://localhost/");
|
|
114
|
+
return {
|
|
115
|
+
prevUrl: defaultUrl,
|
|
116
|
+
prevParams: {},
|
|
117
|
+
prevMatch: null,
|
|
118
|
+
interceptContextUrl: defaultUrl,
|
|
119
|
+
interceptContextMatch: null,
|
|
120
|
+
clientSegmentIds: [],
|
|
121
|
+
clientSegmentSet: new Set(),
|
|
122
|
+
filteredSegmentIds: [],
|
|
123
|
+
stale: false,
|
|
124
|
+
isSameRouteNavigation: false,
|
|
125
|
+
effectiveFromUrl: defaultUrl,
|
|
126
|
+
effectiveFromMatch: null,
|
|
127
|
+
hasInterceptSource: false,
|
|
128
|
+
isHmr: false,
|
|
129
|
+
...overrides,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared route-param comparison helpers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Shallow equality for two route-param records. Same-reference is a fast path;
|
|
7
|
+
* otherwise compares key count then each value.
|
|
8
|
+
*/
|
|
9
|
+
export function paramsEqual(
|
|
10
|
+
a: Record<string, string>,
|
|
11
|
+
b: Record<string, string>,
|
|
12
|
+
): boolean {
|
|
13
|
+
if (a === b) return true;
|
|
14
|
+
|
|
15
|
+
const keysA = Object.keys(a);
|
|
16
|
+
if (keysA.length !== Object.keys(b).length) return false;
|
|
17
|
+
|
|
18
|
+
for (const key of keysA) {
|
|
19
|
+
if (a[key] !== b[key]) return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return true;
|
|
23
|
+
}
|