@rangojs/router 0.0.0-experimental.19 → 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 +17 -0
- package/README.md +291 -61
- package/dist/bin/rango.js +544 -143
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3744 -1329
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +67 -13
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +312 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +247 -23
- package/skills/caching/SKILL.md +322 -19
- 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 +282 -60
- package/skills/host-router/SKILL.md +278 -0
- 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 +297 -31
- 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 +155 -111
- package/skills/rango/SKILL.md +251 -23
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +123 -48
- package/skills/route/SKILL.md +104 -9
- package/skills/router-setup/SKILL.md +124 -11
- 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 +125 -222
- 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 +357 -52
- 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 +67 -40
- package/src/bin/rango.ts +18 -0
- 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/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +200 -30
- package/src/browser/navigation-client.ts +217 -58
- 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 +312 -30
- package/src/browser/prefetch/fetch.ts +344 -47
- 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 +125 -18
- 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 +226 -75
- 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 +619 -442
- package/src/browser/types.ts +115 -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 +7 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +182 -37
- 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 +392 -106
- 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 +45 -21
- package/src/client.tsx +120 -336
- 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 +95 -0
- 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 -5
- 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 +79 -29
- package/src/index.ts +149 -65
- 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 +63 -26
- 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 -3
- 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 +110 -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 +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 +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 -49
- package/src/router/metrics.ts +217 -26
- package/src/router/middleware-types.ts +24 -110
- package/src/router/middleware.ts +384 -197
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +148 -91
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +199 -56
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +144 -74
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +8 -28
- package/src/router/router-interfaces.ts +129 -36
- package/src/router/router-options.ts +185 -23
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +281 -76
- package/src/router/segment-resolution/helpers.ts +116 -31
- package/src/router/segment-resolution/loader-cache.ts +63 -37
- package/src/router/segment-resolution/revalidation.ts +493 -391
- 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 +180 -58
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +182 -54
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +702 -460
- package/src/rsc/helpers.ts +168 -46
- package/src/rsc/index.ts +2 -25
- 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 +98 -19
- 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 +126 -106
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +269 -114
- package/src/rsc/ssr-setup.ts +144 -0
- package/src/rsc/types.ts +34 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +49 -41
- 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 +317 -63
- 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 +425 -177
- package/src/server.ts +6 -0
- 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 +3 -19
- 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 +236 -88
- 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 +10 -45
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +37 -19
- 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 +64 -91
- package/src/vite/discovery/discover-routers.ts +147 -88
- 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 +247 -145
- 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 +61 -13
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +10 -3
- 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 +120 -0
- 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 +127 -0
- 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 +241 -287
- package/src/vite/router-discovery.ts +956 -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 +141 -34
- package/src/vite/utils/shared-utils.ts +92 -42
- package/CLAUDE.md +0 -5
- 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 -6
- package/src/network-error-thrower.tsx +0 -23
- package/src/route-definition/route-function.ts +0 -119
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -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
|
+
}
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import type { RouteEntry, TrailingSlashMode } from "../types";
|
|
8
8
|
import type { EntryData } from "../server/context";
|
|
9
9
|
import { debugLog, isRouterDebugEnabled } from "./logging.js";
|
|
10
|
+
import { escapeRegExp } from "../regex-escape.js";
|
|
11
|
+
import { safeDecodeURIComponent } from "./url-params.js";
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Parsed segment info
|
|
@@ -16,6 +18,7 @@ export interface ParsedSegment {
|
|
|
16
18
|
value: string; // static text, param name, or "*"
|
|
17
19
|
optional: boolean;
|
|
18
20
|
constraint?: string[]; // enum values like ["en", "gb"]
|
|
21
|
+
suffix?: string; // literal text after param in same segment (e.g., ".html")
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
/**
|
|
@@ -31,19 +34,22 @@ export interface ParsedSegment {
|
|
|
31
34
|
*/
|
|
32
35
|
export function parsePattern(pattern: string): ParsedSegment[] {
|
|
33
36
|
const segments: ParsedSegment[] = [];
|
|
34
|
-
// Match: /segment where segment can be:
|
|
35
|
-
// - static text
|
|
36
|
-
// - :param
|
|
37
|
-
// - :param?
|
|
38
|
-
// - :param(a|b)
|
|
39
|
-
// - :param(a|b)?
|
|
40
|
-
// - *
|
|
41
37
|
const segmentRegex =
|
|
42
|
-
/\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)
|
|
38
|
+
/\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?([^/]*)|(\*)|([^/]+))/g;
|
|
43
39
|
|
|
44
40
|
let match;
|
|
45
41
|
while ((match = segmentRegex.exec(pattern)) !== null) {
|
|
46
|
-
const [
|
|
42
|
+
const [
|
|
43
|
+
,
|
|
44
|
+
,
|
|
45
|
+
paramName,
|
|
46
|
+
,
|
|
47
|
+
constraint,
|
|
48
|
+
optional,
|
|
49
|
+
suffix,
|
|
50
|
+
wildcard,
|
|
51
|
+
staticText,
|
|
52
|
+
] = match;
|
|
47
53
|
|
|
48
54
|
if (wildcard) {
|
|
49
55
|
segments.push({ type: "wildcard", value: "*", optional: false });
|
|
@@ -53,6 +59,7 @@ export function parsePattern(pattern: string): ParsedSegment[] {
|
|
|
53
59
|
value: paramName,
|
|
54
60
|
optional: optional === "?",
|
|
55
61
|
constraint: constraint ? constraint.split("|") : undefined,
|
|
62
|
+
suffix: suffix || undefined,
|
|
56
63
|
});
|
|
57
64
|
} else if (staticText) {
|
|
58
65
|
segments.push({ type: "static", value: staticText, optional: false });
|
|
@@ -68,8 +75,14 @@ export function parsePattern(pattern: string): ParsedSegment[] {
|
|
|
68
75
|
export interface CompiledPattern {
|
|
69
76
|
regex: RegExp;
|
|
70
77
|
paramNames: string[];
|
|
71
|
-
optionalParams: Set<string>;
|
|
72
78
|
hasTrailingSlash: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Param-name → allowed values for constrained params (e.g. `:lang(en|gb)`).
|
|
81
|
+
* Validated against the **decoded** param value after regex extraction so
|
|
82
|
+
* a URL like `/en%20GB` still matches `:lang(en GB)` — matching the trie
|
|
83
|
+
* path's behavior (trie-matching.ts:validateAndBuild).
|
|
84
|
+
*/
|
|
85
|
+
constraints?: Record<string, string[]>;
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
// Module-level cache for compiled patterns. Route patterns are a finite set
|
|
@@ -129,7 +142,7 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
129
142
|
|
|
130
143
|
const segments = parsePattern(normalizedPattern);
|
|
131
144
|
const paramNames: string[] = [];
|
|
132
|
-
|
|
145
|
+
let constraints: Record<string, string[]> | undefined;
|
|
133
146
|
|
|
134
147
|
let regexPattern = "";
|
|
135
148
|
|
|
@@ -139,28 +152,46 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
139
152
|
regexPattern += "/(.*)";
|
|
140
153
|
} else if (segment.type === "param") {
|
|
141
154
|
paramNames.push(segment.value);
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
155
|
+
const suffixPattern = segment.suffix ? escapeRegExp(segment.suffix) : "";
|
|
156
|
+
// Constrained params capture anything here; the allowed values are
|
|
157
|
+
// checked post-decode in findMatch so URL-encoded constraint values
|
|
158
|
+
// (e.g. `:lang(en GB)` via `/en%20GB`) still match.
|
|
159
|
+
const valuePattern = segment.suffix ? "([^/]+?)" : "([^/]+)";
|
|
160
|
+
|
|
161
|
+
if (segment.constraint) {
|
|
162
|
+
(constraints ??= {})[segment.value] = segment.constraint;
|
|
163
|
+
}
|
|
145
164
|
|
|
146
165
|
if (segment.optional) {
|
|
147
|
-
optionalParams.add(segment.value);
|
|
148
166
|
// Optional: make the whole /segment optional
|
|
149
|
-
regexPattern += `(?:/${valuePattern})?`;
|
|
167
|
+
regexPattern += `(?:/${valuePattern}${suffixPattern})?`;
|
|
150
168
|
} else {
|
|
151
|
-
regexPattern += `/${valuePattern}`;
|
|
169
|
+
regexPattern += `/${valuePattern}${suffixPattern}`;
|
|
152
170
|
}
|
|
153
171
|
} else {
|
|
154
172
|
// Static segment
|
|
155
|
-
regexPattern += `/${
|
|
173
|
+
regexPattern += `/${escapeRegExp(segment.value)}`;
|
|
156
174
|
}
|
|
157
175
|
}
|
|
158
176
|
|
|
159
|
-
// Handle root path
|
|
160
177
|
if (regexPattern === "") {
|
|
161
178
|
regexPattern = "/";
|
|
162
179
|
}
|
|
163
180
|
|
|
181
|
+
// Patterns of only optional segments (e.g. `/:locale?`, `/:a?/:b?`) need
|
|
182
|
+
// an explicit `/` alternative so a bare `/` matches the absent form. The
|
|
183
|
+
// optional template `(?:/X)?` matches `/X` or empty string, but pathnames
|
|
184
|
+
// are never empty. Arises from `include("/:locale?", routes)` + inner
|
|
185
|
+
// `path("/")`. Skip when an explicit trailing slash already anchors the
|
|
186
|
+
// match.
|
|
187
|
+
const hasOnlyOptionalSegments =
|
|
188
|
+
!hasTrailingSlash &&
|
|
189
|
+
segments.length > 0 &&
|
|
190
|
+
segments.every((segment) => segment.type === "param" && segment.optional);
|
|
191
|
+
if (hasOnlyOptionalSegments) {
|
|
192
|
+
regexPattern = `(?:/|${regexPattern})`;
|
|
193
|
+
}
|
|
194
|
+
|
|
164
195
|
// Add trailing slash to regex if pattern has one
|
|
165
196
|
if (hasTrailingSlash) {
|
|
166
197
|
regexPattern += "/";
|
|
@@ -169,16 +200,55 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
169
200
|
return {
|
|
170
201
|
regex: new RegExp(`^${regexPattern}$`),
|
|
171
202
|
paramNames,
|
|
172
|
-
optionalParams,
|
|
173
203
|
hasTrailingSlash,
|
|
204
|
+
...(constraints ? { constraints } : {}),
|
|
174
205
|
};
|
|
175
206
|
}
|
|
176
207
|
|
|
177
208
|
/**
|
|
178
|
-
*
|
|
209
|
+
* Validate decoded params against a compiled pattern's constraints.
|
|
210
|
+
* Returns false if any constrained param has a non-empty value not in the
|
|
211
|
+
* allowed list. Absent optionals (key missing or `undefined`) are allowed;
|
|
212
|
+
* `""` is also tolerated as "absent" so user-provided params or fixtures
|
|
213
|
+
* that pass empty strings explicitly behave the same way.
|
|
179
214
|
*/
|
|
180
|
-
function
|
|
181
|
-
|
|
215
|
+
function satisfiesConstraints(
|
|
216
|
+
params: Record<string, string>,
|
|
217
|
+
constraints: Record<string, string[]> | undefined,
|
|
218
|
+
): boolean {
|
|
219
|
+
if (!constraints) return true;
|
|
220
|
+
for (const name in constraints) {
|
|
221
|
+
const value = params[name];
|
|
222
|
+
if (
|
|
223
|
+
value !== undefined &&
|
|
224
|
+
value !== "" &&
|
|
225
|
+
!constraints[name].includes(value)
|
|
226
|
+
) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Build the named-params record from a regex match. Optional segments that
|
|
235
|
+
* didn't capture leave the corresponding group `undefined`; we skip those
|
|
236
|
+
* keys so `ctx.params.<name>` reads as `undefined` rather than `""`. This
|
|
237
|
+
* keeps the runtime aligned with the `ExtractParams` type and matches the
|
|
238
|
+
* trie matcher's contract (see `trie-matching.ts:validateAndBuild`).
|
|
239
|
+
*/
|
|
240
|
+
function buildParamsFromMatch(
|
|
241
|
+
match: RegExpExecArray,
|
|
242
|
+
paramNames: string[],
|
|
243
|
+
): Record<string, string> {
|
|
244
|
+
const params: Record<string, string> = {};
|
|
245
|
+
paramNames.forEach((name, index) => {
|
|
246
|
+
const captured = match[index + 1];
|
|
247
|
+
if (captured !== undefined) {
|
|
248
|
+
params[name] = safeDecodeURIComponent(captured);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
return params;
|
|
182
252
|
}
|
|
183
253
|
|
|
184
254
|
/**
|
|
@@ -197,43 +267,58 @@ function escapeRegex(str: string): string {
|
|
|
197
267
|
export function extractStaticPrefix(pattern: string): string {
|
|
198
268
|
if (!pattern || pattern === "/") return "";
|
|
199
269
|
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
270
|
+
// Walk segments and stop at the first that is a real param (`:name`) or a
|
|
271
|
+
// wildcard (`*`). A literal `:` or `*` not at a segment boundary (e.g. the
|
|
272
|
+
// `a:b` in `/a:b/c/:id`, or `tel:+1`) is a STATIC segment and must NOT
|
|
273
|
+
// terminate the prefix — `pattern.indexOf(":")` misread it as a param marker,
|
|
274
|
+
// returning "" and dropping the findMatch fast-skip optimization for that
|
|
275
|
+
// entry on every request. Classification mirrors parsePattern: a leading `:`
|
|
276
|
+
// marks a param, a leading `*` marks a wildcard.
|
|
277
|
+
const hasLeadingSlash = pattern.startsWith("/");
|
|
278
|
+
const body = hasLeadingSlash ? pattern.slice(1) : pattern;
|
|
279
|
+
const segments = body.split("/");
|
|
280
|
+
|
|
281
|
+
const staticSegments: string[] = [];
|
|
282
|
+
for (const segment of segments) {
|
|
283
|
+
if (segment.startsWith(":") || segment.startsWith("*")) {
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
staticSegments.push(segment);
|
|
216
287
|
}
|
|
217
288
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
289
|
+
// No leading static segment (first segment is a param/wildcard) -> no prefix.
|
|
290
|
+
if (staticSegments.length === 0) return "";
|
|
291
|
+
// Every segment was static (no param/wildcard) -> the whole pattern is the
|
|
292
|
+
// prefix. Preserve a trailing slash only when it existed in the input; a
|
|
293
|
+
// split of "/a/b" yields ["a","b"] (no empty tail) so a re-join is exact.
|
|
294
|
+
if (staticSegments.length === segments.length) return pattern;
|
|
222
295
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return "";
|
|
227
|
-
}
|
|
296
|
+
const prefix = staticSegments.join("/");
|
|
297
|
+
return hasLeadingSlash ? "/" + prefix : prefix;
|
|
298
|
+
}
|
|
228
299
|
|
|
229
|
-
|
|
300
|
+
/**
|
|
301
|
+
* Join a URL prefix to a sub-prefix, collapsing the duplicate slash when the
|
|
302
|
+
* base ends with "/" and the sub-prefix starts with "/". This mirrors the
|
|
303
|
+
* canonical join in `include()` (urls/include-helper.ts) and `runWithPrefixes`
|
|
304
|
+
* (server/context.ts) so a nested lazy include's runtime staticPrefix matches
|
|
305
|
+
* the build-time trie's `sp` (e.g. `include("/parent/", …)` containing
|
|
306
|
+
* `include("/child", …)` resolves to `/parent/child`, not `/parent//child`).
|
|
307
|
+
*/
|
|
308
|
+
export function joinPrefix(base: string | undefined, prefix: string): string {
|
|
309
|
+
if (!base) return prefix;
|
|
310
|
+
return base.endsWith("/") && prefix.startsWith("/")
|
|
311
|
+
? base + prefix.slice(1)
|
|
312
|
+
: base + prefix;
|
|
230
313
|
}
|
|
231
314
|
|
|
232
315
|
/**
|
|
233
316
|
* Match a pathname against registered routes
|
|
234
317
|
*
|
|
235
|
-
* Note: Optional params that are absent in the path
|
|
236
|
-
*
|
|
318
|
+
* Note: Optional params that are absent in the path are omitted from the
|
|
319
|
+
* returned `params` (read as `undefined`), matching the trie matcher and
|
|
320
|
+
* the `ExtractParams<"/:locale?/...">` type. Use the pattern definition to
|
|
321
|
+
* determine which keys are optional.
|
|
237
322
|
*
|
|
238
323
|
* Trailing slash handling (priority order):
|
|
239
324
|
* 1. Per-route `trailingSlash` config from route()
|
|
@@ -251,10 +336,7 @@ export interface RouteMatchResult<TEnv = any> {
|
|
|
251
336
|
entry: RouteEntry<TEnv>;
|
|
252
337
|
routeKey: string;
|
|
253
338
|
params: Record<string, string>;
|
|
254
|
-
optionalParams: Set<string>;
|
|
255
339
|
redirectTo?: string;
|
|
256
|
-
/** Ancestry shortCodes for layout pruning (from trie match) */
|
|
257
|
-
ancestry?: string[];
|
|
258
340
|
/** Route has pre-rendered data available (from trie) */
|
|
259
341
|
pr?: true;
|
|
260
342
|
/** Passthrough: handler kept for live fallback on unknown params (from trie) */
|
|
@@ -332,8 +414,6 @@ export function findMatch<TEnv>(
|
|
|
332
414
|
: pathname + "/";
|
|
333
415
|
|
|
334
416
|
for (const entry of routesEntries) {
|
|
335
|
-
// Short-circuit: skip entry if pathname doesn't start with static prefix
|
|
336
|
-
// staticPrefix is pre-computed at registration time, so this is O(1)
|
|
337
417
|
if (entry.staticPrefix && !pathname.startsWith(entry.staticPrefix)) {
|
|
338
418
|
if (effectiveDebug) {
|
|
339
419
|
debugStats.entriesSkipped++;
|
|
@@ -345,8 +425,6 @@ export function findMatch<TEnv>(
|
|
|
345
425
|
continue;
|
|
346
426
|
}
|
|
347
427
|
|
|
348
|
-
// Check if this is a lazy entry that needs evaluation
|
|
349
|
-
// When staticPrefix matches but routes are not yet populated, signal caller to evaluate
|
|
350
428
|
if (entry.lazy && !entry.lazyEvaluated) {
|
|
351
429
|
if (effectiveDebug) {
|
|
352
430
|
debugLog("findMatch", "lazy entry requires evaluation", {
|
|
@@ -367,7 +445,6 @@ export function findMatch<TEnv>(
|
|
|
367
445
|
debugStats.routesChecked++;
|
|
368
446
|
}
|
|
369
447
|
|
|
370
|
-
// Join prefix and pattern, handling edge cases
|
|
371
448
|
let fullPattern: string;
|
|
372
449
|
if (entry.prefix === "" || entry.prefix === "/") {
|
|
373
450
|
fullPattern = pattern;
|
|
@@ -377,14 +454,12 @@ export function findMatch<TEnv>(
|
|
|
377
454
|
fullPattern = entry.prefix + pattern;
|
|
378
455
|
}
|
|
379
456
|
|
|
380
|
-
const { regex, paramNames,
|
|
457
|
+
const { regex, paramNames, hasTrailingSlash, constraints } =
|
|
381
458
|
getCompiledPattern(fullPattern);
|
|
382
459
|
|
|
383
|
-
// Get trailing slash mode for this route (per-route config or pattern-based)
|
|
384
460
|
const trailingSlashMode: TrailingSlashMode | undefined =
|
|
385
461
|
entry.trailingSlash?.[routeKey];
|
|
386
462
|
|
|
387
|
-
// Prerender flag from entry metadata (set by urls() for prerender handlers)
|
|
388
463
|
const prFlag = entry.prerenderRouteKeys?.has(routeKey)
|
|
389
464
|
? { pr: true as const }
|
|
390
465
|
: {};
|
|
@@ -392,13 +467,13 @@ export function findMatch<TEnv>(
|
|
|
392
467
|
? { pt: true as const }
|
|
393
468
|
: {};
|
|
394
469
|
|
|
395
|
-
// Try exact match first
|
|
396
470
|
const match = regex.exec(pathname);
|
|
397
471
|
if (match) {
|
|
398
|
-
const params
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
472
|
+
const params = buildParamsFromMatch(match, paramNames);
|
|
473
|
+
|
|
474
|
+
if (!satisfiesConstraints(params, constraints)) {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
402
477
|
|
|
403
478
|
if (effectiveDebug) {
|
|
404
479
|
debugLog("findMatch", "matched route", {
|
|
@@ -408,29 +483,24 @@ export function findMatch<TEnv>(
|
|
|
408
483
|
});
|
|
409
484
|
}
|
|
410
485
|
|
|
411
|
-
// Check if trailing slash mode requires redirect even on exact match
|
|
412
486
|
if (
|
|
413
487
|
trailingSlashMode === "always" &&
|
|
414
488
|
!pathnameHasTrailingSlash &&
|
|
415
489
|
pathname !== "/"
|
|
416
490
|
) {
|
|
417
|
-
// Mode says always have trailing slash, but pathname doesn't have it
|
|
418
491
|
return {
|
|
419
492
|
entry,
|
|
420
493
|
routeKey,
|
|
421
494
|
params,
|
|
422
|
-
optionalParams,
|
|
423
495
|
redirectTo: pathname + "/",
|
|
424
496
|
...prFlag,
|
|
425
497
|
...ptFlag,
|
|
426
498
|
};
|
|
427
499
|
} else if (trailingSlashMode === "never" && pathnameHasTrailingSlash) {
|
|
428
|
-
// Mode says never have trailing slash, but pathname has it
|
|
429
500
|
return {
|
|
430
501
|
entry,
|
|
431
502
|
routeKey,
|
|
432
503
|
params,
|
|
433
|
-
optionalParams,
|
|
434
504
|
redirectTo: pathname.slice(0, -1),
|
|
435
505
|
...prFlag,
|
|
436
506
|
...ptFlag,
|
|
@@ -441,39 +511,33 @@ export function findMatch<TEnv>(
|
|
|
441
511
|
entry,
|
|
442
512
|
routeKey,
|
|
443
513
|
params,
|
|
444
|
-
optionalParams,
|
|
445
514
|
...prFlag,
|
|
446
515
|
...ptFlag,
|
|
447
516
|
};
|
|
448
517
|
}
|
|
449
518
|
|
|
450
|
-
// Try alternate pathname (opposite trailing slash)
|
|
451
519
|
const altMatch = regex.exec(alternatePathname);
|
|
452
520
|
if (altMatch) {
|
|
453
|
-
const params
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
521
|
+
const params = buildParamsFromMatch(altMatch, paramNames);
|
|
522
|
+
|
|
523
|
+
if (!satisfiesConstraints(params, constraints)) {
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
457
526
|
|
|
458
|
-
// Determine redirect behavior based on mode
|
|
459
527
|
if (trailingSlashMode === "ignore") {
|
|
460
|
-
// Match without redirect
|
|
461
528
|
return {
|
|
462
529
|
entry,
|
|
463
530
|
routeKey,
|
|
464
531
|
params,
|
|
465
|
-
optionalParams,
|
|
466
532
|
...prFlag,
|
|
467
533
|
...ptFlag,
|
|
468
534
|
};
|
|
469
535
|
} else if (trailingSlashMode === "never") {
|
|
470
|
-
// Redirect to no trailing slash
|
|
471
536
|
if (pathnameHasTrailingSlash) {
|
|
472
537
|
return {
|
|
473
538
|
entry,
|
|
474
539
|
routeKey,
|
|
475
540
|
params,
|
|
476
|
-
optionalParams,
|
|
477
541
|
redirectTo: alternatePathname,
|
|
478
542
|
...prFlag,
|
|
479
543
|
...ptFlag,
|
|
@@ -483,18 +547,15 @@ export function findMatch<TEnv>(
|
|
|
483
547
|
entry,
|
|
484
548
|
routeKey,
|
|
485
549
|
params,
|
|
486
|
-
optionalParams,
|
|
487
550
|
...prFlag,
|
|
488
551
|
...ptFlag,
|
|
489
552
|
};
|
|
490
553
|
} else if (trailingSlashMode === "always") {
|
|
491
|
-
// Redirect to with trailing slash
|
|
492
554
|
if (!pathnameHasTrailingSlash) {
|
|
493
555
|
return {
|
|
494
556
|
entry,
|
|
495
557
|
routeKey,
|
|
496
558
|
params,
|
|
497
|
-
optionalParams,
|
|
498
559
|
redirectTo: alternatePathname,
|
|
499
560
|
...prFlag,
|
|
500
561
|
...ptFlag,
|
|
@@ -504,13 +565,10 @@ export function findMatch<TEnv>(
|
|
|
504
565
|
entry,
|
|
505
566
|
routeKey,
|
|
506
567
|
params,
|
|
507
|
-
optionalParams,
|
|
508
568
|
...prFlag,
|
|
509
569
|
...ptFlag,
|
|
510
570
|
};
|
|
511
571
|
} else {
|
|
512
|
-
// No explicit mode - use pattern-based detection
|
|
513
|
-
// Redirect to canonical form (what the pattern defines)
|
|
514
572
|
const canonicalPath = hasTrailingSlash
|
|
515
573
|
? alternatePathname
|
|
516
574
|
: pathname.slice(0, -1);
|
|
@@ -518,7 +576,6 @@ export function findMatch<TEnv>(
|
|
|
518
576
|
entry,
|
|
519
577
|
routeKey,
|
|
520
578
|
params,
|
|
521
|
-
optionalParams,
|
|
522
579
|
redirectTo: canonicalPath,
|
|
523
580
|
...prFlag,
|
|
524
581
|
...ptFlag,
|
|
@@ -539,7 +596,7 @@ export function* traverseBack(entry: EntryData): Generator<EntryData> {
|
|
|
539
596
|
let current: EntryData | null = entry;
|
|
540
597
|
const items = [] as EntryData[];
|
|
541
598
|
while (current !== null) {
|
|
542
|
-
items.push(current);
|
|
599
|
+
items.push(current);
|
|
543
600
|
current = current.parent;
|
|
544
601
|
}
|
|
545
602
|
for (let i = items.length - 1; i >= 0; i--) {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the prefetch cache TTL once, at router init, into the three derived
|
|
3
|
+
* values the rest of the router consumes: seconds (for the Cache-Control
|
|
4
|
+
* max-age), milliseconds (for the client-side in-memory cache), and the
|
|
5
|
+
* Cache-Control header string (or false when caching is disabled).
|
|
6
|
+
*
|
|
7
|
+
* `false` disables prefetch caching (seconds 0). A non-finite input
|
|
8
|
+
* (NaN/Infinity, e.g. from an env-derived number that failed to parse) is
|
|
9
|
+
* treated as the default rather than producing a malformed
|
|
10
|
+
* `Cache-Control: max-age=NaN` header; CDNs/browsers ignore such a directive,
|
|
11
|
+
* which would silently disable caching on EVERY prefetch response. Negative
|
|
12
|
+
* finite values clamp to 0 (disabled).
|
|
13
|
+
*
|
|
14
|
+
* Policy note: this is the prefetch-TTL policy specifically. Other finite-number
|
|
15
|
+
* guards in the codebase deliberately differ — defer.ts treats Infinity as an
|
|
16
|
+
* intentional disable, and profile-registry.ts THROWS on non-finite/negative ttl
|
|
17
|
+
* at config time. They are NOT the same guard, so don't unify them.
|
|
18
|
+
*/
|
|
19
|
+
export interface ResolvedPrefetchCacheTTL {
|
|
20
|
+
/** TTL in seconds for the Cache-Control max-age directive. */
|
|
21
|
+
seconds: number;
|
|
22
|
+
/** TTL in milliseconds for the client-side in-memory prefetch cache. */
|
|
23
|
+
ms: number;
|
|
24
|
+
/** Cache-Control header value, or false when caching is disabled. */
|
|
25
|
+
cacheControl: string | false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DEFAULT_PREFETCH_CACHE_TTL_SECONDS = 300;
|
|
29
|
+
|
|
30
|
+
export function resolvePrefetchCacheTTL(
|
|
31
|
+
rawTTL: number | false | undefined,
|
|
32
|
+
): ResolvedPrefetchCacheTTL {
|
|
33
|
+
let seconds: number;
|
|
34
|
+
if (rawTTL === false) {
|
|
35
|
+
seconds = 0;
|
|
36
|
+
} else if (rawTTL === undefined) {
|
|
37
|
+
seconds = DEFAULT_PREFETCH_CACHE_TTL_SECONDS;
|
|
38
|
+
} else if (typeof rawTTL === "number" && Number.isFinite(rawTTL)) {
|
|
39
|
+
seconds = Math.max(0, Math.floor(rawTTL));
|
|
40
|
+
} else {
|
|
41
|
+
// Non-finite (NaN/Infinity): fall back to the default rather than emit a
|
|
42
|
+
// malformed max-age=NaN/Infinity that CDNs and browsers reject.
|
|
43
|
+
seconds = DEFAULT_PREFETCH_CACHE_TTL_SECONDS;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
seconds,
|
|
48
|
+
ms: seconds * 1000,
|
|
49
|
+
cacheControl: seconds === 0 ? false : `private, max-age=${seconds}`,
|
|
50
|
+
};
|
|
51
|
+
}
|