@rangojs/router 0.0.0-experimental.bd6e11bc → 0.0.0-experimental.bdaf10aa
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -4
- package/README.md +296 -887
- package/dist/bin/rango.js +459 -91
- package/dist/testing/vitest.js +36 -2
- package/dist/vite/index.js +1708 -414
- package/package.json +35 -10
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +82 -5
- package/skills/bundle-analysis/SKILL.md +2 -2
- package/skills/cache-guide/SKILL.md +14 -9
- package/skills/caching/SKILL.md +221 -12
- package/skills/catalog.json +271 -0
- package/skills/comparison/SKILL.md +50 -0
- package/skills/comparison/agents/openai.yaml +4 -0
- package/skills/comparison/references/framework-comparison.md +837 -0
- package/skills/composability/SKILL.md +83 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +5 -3
- package/skills/defer-hydration/SKILL.md +235 -0
- package/skills/document-cache/SKILL.md +11 -3
- package/skills/fonts/SKILL.md +1 -1
- package/skills/handler-use/SKILL.md +9 -9
- package/skills/hooks/SKILL.md +73 -900
- package/skills/hooks/data.md +273 -0
- package/skills/hooks/handle-and-actions.md +103 -0
- package/skills/hooks/navigation.md +110 -0
- package/skills/hooks/outlets.md +41 -0
- package/skills/hooks/state.md +228 -0
- package/skills/hooks/urls.md +135 -0
- package/skills/host-router/SKILL.md +84 -7
- package/skills/i18n/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +51 -17
- package/skills/layout/SKILL.md +38 -16
- package/skills/links/SKILL.md +1 -1
- package/skills/loader/SKILL.md +48 -20
- package/skills/middleware/SKILL.md +11 -5
- package/skills/migrate-nextjs/SKILL.md +203 -20
- package/skills/migrate-react-router/SKILL.md +59 -675
- package/skills/migrate-react-router/cloudflare-workers.md +129 -0
- package/skills/migrate-react-router/component-migration.md +196 -0
- package/skills/migrate-react-router/data-and-actions.md +225 -0
- package/skills/migrate-react-router/route-mapping.md +271 -0
- package/skills/mime-routes/SKILL.md +3 -3
- package/skills/observability/SKILL.md +70 -5
- package/skills/parallel/SKILL.md +32 -8
- package/skills/ppr/SKILL.md +622 -0
- package/skills/prerender/SKILL.md +59 -28
- package/skills/rango/SKILL.md +124 -50
- package/skills/response-routes/SKILL.md +78 -46
- package/skills/route/SKILL.md +85 -6
- package/skills/router-setup/SKILL.md +41 -6
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +28 -3
- package/skills/shell-manifest/SKILL.md +185 -0
- package/skills/streams-and-websockets/SKILL.md +1 -1
- package/skills/tailwind/SKILL.md +28 -4
- package/skills/testing/SKILL.md +68 -654
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -0
- package/skills/testing/handles.md +131 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +122 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +85 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +123 -0
- package/skills/theme/SKILL.md +1 -1
- package/skills/typesafety/SKILL.md +45 -918
- package/skills/typesafety/env-and-bindings.md +254 -0
- package/skills/typesafety/generated-files-and-cli.md +335 -0
- package/skills/typesafety/params-and-search.md +153 -0
- package/skills/typesafety/route-types.md +209 -0
- package/skills/use-cache/SKILL.md +47 -17
- package/skills/vercel/SKILL.md +128 -0
- package/skills/view-transitions/SKILL.md +44 -1
- package/src/__augment-tests__/augmented.check.ts +2 -3
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +14 -27
- package/src/browser/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +178 -100
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/logging.ts +28 -0
- package/src/browser/merge-segment-loaders.ts +6 -4
- package/src/browser/navigation-bridge.ts +81 -68
- package/src/browser/navigation-client.ts +115 -70
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +153 -88
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +157 -144
- package/src/browser/prefetch/cache.ts +148 -81
- package/src/browser/prefetch/fetch.ts +231 -51
- package/src/browser/prefetch/queue.ts +25 -7
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +40 -7
- package/src/browser/react/NavigationProvider.tsx +140 -99
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +17 -2
- package/src/browser/react/index.ts +0 -51
- package/src/browser/react/location-state-shared.ts +14 -15
- package/src/browser/react/location-state.ts +0 -1
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +0 -5
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +10 -5
- package/src/browser/react/use-params.ts +0 -2
- package/src/browser/react/use-router.ts +6 -4
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/response-adapter.ts +74 -8
- package/src/browser/rsc-router.tsx +97 -22
- package/src/browser/scroll-restoration.ts +15 -8
- package/src/browser/segment-reconciler.ts +31 -21
- package/src/browser/server-action-bridge.ts +216 -38
- package/src/browser/types.ts +94 -22
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/generate-manifest.ts +155 -131
- package/src/build/generate-route-types.ts +1 -1
- package/src/build/index.ts +11 -5
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +152 -22
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +12 -1
- package/src/build/route-types/include-resolution.ts +455 -61
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +15 -2
- package/src/build/route-types/router-processing.ts +77 -41
- package/src/build/route-types/source-scan.ts +105 -7
- package/src/build/runtime-discovery.ts +4 -1
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +58 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +454 -101
- package/src/cache/cache-scope.ts +159 -54
- package/src/cache/cache-tag.ts +149 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +2170 -377
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +126 -41
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +243 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +13 -21
- package/src/cache/shell-snapshot.ts +417 -0
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +194 -99
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +1132 -0
- package/src/client.rsc.tsx +39 -22
- package/src/client.tsx +28 -58
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +108 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +13 -1
- package/src/decode-loader-results.ts +18 -2
- package/src/defer.ts +185 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +0 -3
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +57 -40
- package/src/handles/MetaTags.tsx +24 -53
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +35 -8
- package/src/handles/deferred-resolution.ts +127 -0
- package/src/handles/is-thenable.ts +18 -0
- package/src/handles/meta.ts +14 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +13 -22
- package/src/host/index.ts +7 -0
- package/src/host/pattern-matcher.ts +23 -52
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +7 -12
- package/src/index.rsc.ts +88 -8
- package/src/index.ts +90 -16
- package/src/internal-debug.ts +11 -10
- package/src/loader.rsc.ts +19 -9
- package/src/loader.ts +12 -4
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +75 -7
- package/src/redirect-origin.ts +114 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +25 -0
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +13 -49
- package/src/route-definition/dsl-helpers.ts +60 -53
- package/src/route-definition/helper-factories.ts +0 -2
- package/src/route-definition/helpers-types.ts +46 -46
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +6 -1
- package/src/route-definition/use-item-types.ts +3 -6
- package/src/route-map-builder.ts +41 -20
- package/src/route-types.ts +0 -5
- package/src/router/content-negotiation.ts +58 -23
- package/src/router/error-handling.ts +44 -17
- package/src/router/find-match.ts +129 -30
- package/src/router/handler-context.ts +6 -1
- package/src/router/instrument.ts +355 -0
- package/src/router/intercept-resolution.ts +35 -2
- package/src/router/lazy-includes.ts +79 -56
- package/src/router/loader-resolution.ts +151 -73
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +74 -40
- package/src/router/match-api.ts +76 -52
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +181 -178
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +115 -194
- package/src/router/match-middleware/cache-store.ts +61 -50
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +36 -67
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +0 -116
- package/src/router/middleware.ts +231 -120
- package/src/router/navigation-snapshot.ts +7 -56
- package/src/router/params-util.ts +23 -0
- package/src/router/parse-pattern.ts +115 -0
- package/src/router/pattern-matching.ts +99 -152
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prefetch-limits.ts +37 -0
- package/src/router/prerender-match.ts +111 -66
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +47 -42
- package/src/router/revalidation.ts +75 -81
- package/src/router/route-snapshot.ts +14 -3
- package/src/router/router-context.ts +6 -29
- package/src/router/router-interfaces.ts +70 -8
- package/src/router/router-options.ts +126 -4
- package/src/router/segment-resolution/fresh.ts +104 -80
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +155 -39
- package/src/router/segment-resolution/loader-mask.ts +60 -0
- package/src/router/segment-resolution/loader-snapshot.ts +259 -0
- package/src/router/segment-resolution/mask-nested.ts +83 -0
- package/src/router/segment-resolution/revalidation.ts +215 -304
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +35 -15
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +6 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +54 -35
- package/src/router/telemetry-otel.ts +160 -200
- package/src/router/telemetry.ts +9 -23
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +215 -0
- package/src/router/trie-matching.ts +171 -64
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +13 -5
- package/src/router.ts +119 -48
- package/src/rsc/full-payload.ts +70 -0
- package/src/rsc/handler-context.ts +1 -0
- package/src/rsc/handler.ts +267 -152
- package/src/rsc/helpers.ts +78 -4
- package/src/rsc/index.ts +1 -4
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +29 -42
- package/src/rsc/nonce.ts +10 -1
- package/src/rsc/origin-guard.ts +11 -15
- package/src/rsc/progressive-enhancement.ts +120 -13
- package/src/rsc/redirect-guard.ts +100 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +58 -141
- package/src/rsc/rsc-rendering.ts +492 -49
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +268 -82
- package/src/rsc/shell-capture.ts +1190 -0
- package/src/rsc/shell-serve.ts +181 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +45 -3
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +31 -26
- package/src/segment-loader-promise.ts +49 -4
- package/src/segment-system.tsx +260 -95
- package/src/server/context.ts +99 -9
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +125 -2
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +379 -138
- package/src/ssr/index.tsx +491 -182
- package/src/ssr/inject-rsc-eager.ts +167 -0
- package/src/ssr/ssr-root.tsx +228 -0
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +44 -48
- package/src/testing/collect-handle.ts +14 -31
- package/src/testing/dispatch.ts +533 -160
- package/src/testing/e2e/fixture.ts +45 -11
- package/src/testing/e2e/index.ts +1 -22
- package/src/testing/e2e/matchers.ts +0 -16
- package/src/testing/e2e/parity.ts +85 -4
- package/src/testing/e2e/server.ts +12 -0
- package/src/testing/flight-matchers.ts +7 -14
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +36 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +30 -0
- package/src/testing/flight.ts +145 -70
- package/src/testing/generated-routes.ts +26 -50
- package/src/testing/index.ts +18 -19
- package/src/testing/internal/context.ts +184 -68
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +357 -0
- package/src/testing/render-route.tsx +134 -115
- package/src/testing/run-loader.ts +140 -51
- package/src/testing/run-middleware.ts +59 -33
- package/src/testing/run-transition-when.ts +164 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +1 -1
- package/src/testing/vitest-stubs/cloudflare-workers.ts +1 -1
- package/src/testing/vitest.ts +138 -16
- package/src/theme/ThemeProvider.tsx +56 -84
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +15 -15
- package/src/types/handler-context.ts +45 -15
- package/src/types/index.ts +2 -10
- package/src/types/loader-types.ts +6 -3
- package/src/types/request-scope.ts +8 -22
- package/src/types/route-config.ts +20 -52
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +100 -13
- package/src/urls/include-helper.ts +10 -12
- package/src/urls/include-provider.ts +71 -0
- package/src/urls/index.ts +2 -8
- package/src/urls/path-helper-types.ts +52 -14
- package/src/urls/path-helper.ts +5 -54
- package/src/urls/pattern-types.ts +36 -0
- package/src/urls/type-extraction.ts +76 -42
- package/src/urls/urls-function.ts +0 -14
- package/src/use-loader.tsx +0 -186
- package/src/vercel/index.ts +11 -0
- package/src/vercel/tracing.ts +88 -0
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/dev-prerender-cache.ts +117 -0
- package/src/vite/discovery/discover-routers.ts +34 -43
- package/src/vite/discovery/discovery-errors.ts +61 -0
- package/src/vite/discovery/prerender-collection.ts +33 -46
- package/src/vite/discovery/state.ts +12 -1
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/index.ts +9 -0
- package/src/vite/inject-client-debug.ts +88 -0
- package/src/vite/plugin-types.ts +143 -10
- package/src/vite/plugins/cjs-to-esm.ts +8 -12
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +0 -10
- package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
- package/src/vite/plugins/expose-action-id.ts +2 -73
- package/src/vite/plugins/expose-id-utils.ts +85 -56
- package/src/vite/plugins/expose-ids/export-analysis.ts +30 -43
- package/src/vite/plugins/expose-ids/handler-transform.ts +5 -31
- package/src/vite/plugins/expose-ids/loader-transform.ts +12 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +98 -26
- package/src/vite/plugins/expose-internal-ids.ts +10 -1
- package/src/vite/plugins/performance-tracks.ts +0 -3
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +21 -46
- package/src/vite/plugins/vercel-output.ts +384 -0
- package/src/vite/plugins/version-injector.ts +22 -27
- package/src/vite/plugins/version-plugin.ts +6 -66
- package/src/vite/plugins/virtual-entries.ts +137 -26
- package/src/vite/rango.ts +146 -135
- package/src/vite/router-discovery.ts +189 -48
- package/src/vite/utils/ast-handler-extract.ts +11 -20
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +4 -75
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +71 -44
- package/src/vite/utils/shared-utils.ts +55 -37
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -7,73 +7,15 @@
|
|
|
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";
|
|
10
11
|
import { safeDecodeURIComponent } from "./url-params.js";
|
|
12
|
+
import { parsePattern, type ParsedSegment } from "./parse-pattern.js";
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
value: string; // static text, param name, or "*"
|
|
18
|
-
optional: boolean;
|
|
19
|
-
constraint?: string[]; // enum values like ["en", "gb"]
|
|
20
|
-
suffix?: string; // literal text after param in same segment (e.g., ".html")
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Parse a route pattern into segments
|
|
25
|
-
*
|
|
26
|
-
* Supports:
|
|
27
|
-
* - Static: /blog, /about
|
|
28
|
-
* - Params: /:slug, /:id
|
|
29
|
-
* - Optional: /:locale?, /:page?
|
|
30
|
-
* - Constrained: /:locale(en|gb), /:type(post|page)
|
|
31
|
-
* - Optional + Constrained: /:locale(en|gb)?
|
|
32
|
-
* - Wildcard: /*
|
|
33
|
-
*/
|
|
34
|
-
export function parsePattern(pattern: string): ParsedSegment[] {
|
|
35
|
-
const segments: ParsedSegment[] = [];
|
|
36
|
-
// Match: /segment where segment can be:
|
|
37
|
-
// - static text
|
|
38
|
-
// - :param
|
|
39
|
-
// - :param?
|
|
40
|
-
// - :param(a|b)
|
|
41
|
-
// - :param(a|b)?
|
|
42
|
-
// - *
|
|
43
|
-
const segmentRegex =
|
|
44
|
-
/\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?([^/]*)|(\*)|([^/]+))/g;
|
|
45
|
-
|
|
46
|
-
let match;
|
|
47
|
-
while ((match = segmentRegex.exec(pattern)) !== null) {
|
|
48
|
-
const [
|
|
49
|
-
,
|
|
50
|
-
,
|
|
51
|
-
paramName,
|
|
52
|
-
,
|
|
53
|
-
constraint,
|
|
54
|
-
optional,
|
|
55
|
-
suffix,
|
|
56
|
-
wildcard,
|
|
57
|
-
staticText,
|
|
58
|
-
] = match;
|
|
59
|
-
|
|
60
|
-
if (wildcard) {
|
|
61
|
-
segments.push({ type: "wildcard", value: "*", optional: false });
|
|
62
|
-
} else if (paramName) {
|
|
63
|
-
segments.push({
|
|
64
|
-
type: "param",
|
|
65
|
-
value: paramName,
|
|
66
|
-
optional: optional === "?",
|
|
67
|
-
constraint: constraint ? constraint.split("|") : undefined,
|
|
68
|
-
suffix: suffix || undefined,
|
|
69
|
-
});
|
|
70
|
-
} else if (staticText) {
|
|
71
|
-
segments.push({ type: "static", value: staticText, optional: false });
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return segments;
|
|
76
|
-
}
|
|
14
|
+
// `parsePattern`/`ParsedSegment` live in the dependency-free `parse-pattern.ts`
|
|
15
|
+
// so the client reverse helper can import them without dragging this module's
|
|
16
|
+
// server-only deps into the browser bundle. Re-exported here for existing
|
|
17
|
+
// importers (build/route-trie, middleware, tests).
|
|
18
|
+
export { parsePattern, type ParsedSegment };
|
|
77
19
|
|
|
78
20
|
/**
|
|
79
21
|
* Compiled pattern result containing regex, param metadata, and trailing slash info.
|
|
@@ -81,7 +23,6 @@ export function parsePattern(pattern: string): ParsedSegment[] {
|
|
|
81
23
|
export interface CompiledPattern {
|
|
82
24
|
regex: RegExp;
|
|
83
25
|
paramNames: string[];
|
|
84
|
-
optionalParams: Set<string>;
|
|
85
26
|
hasTrailingSlash: boolean;
|
|
86
27
|
/**
|
|
87
28
|
* Param-name → allowed values for constrained params (e.g. `:lang(en|gb)`).
|
|
@@ -90,6 +31,14 @@ export interface CompiledPattern {
|
|
|
90
31
|
* path's behavior (trie-matching.ts:validateAndBuild).
|
|
91
32
|
*/
|
|
92
33
|
constraints?: Record<string, string[]>;
|
|
34
|
+
/**
|
|
35
|
+
* The pattern's catch-all param, if any (`*` for bare `/*`, the name for a
|
|
36
|
+
* named `:name+`/`:name*`). A zero-or-more catch-all (`oneOrMore: false`)
|
|
37
|
+
* whose optional group is absent binds "" rather than being omitted — so
|
|
38
|
+
* `/docs` matches `/docs/:slug*` with `slug === ""`. `oneOrMore` keeps the
|
|
39
|
+
* same polarity as `ParsedSegment.oneOrMore` and the trie's `w1`.
|
|
40
|
+
*/
|
|
41
|
+
catchAll?: { name: string; oneOrMore: boolean };
|
|
93
42
|
}
|
|
94
43
|
|
|
95
44
|
// Module-level cache for compiled patterns. Route patterns are a finite set
|
|
@@ -149,18 +98,36 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
149
98
|
|
|
150
99
|
const segments = parsePattern(normalizedPattern);
|
|
151
100
|
const paramNames: string[] = [];
|
|
152
|
-
const optionalParams = new Set<string>();
|
|
153
101
|
let constraints: Record<string, string[]> | undefined;
|
|
102
|
+
let catchAll: { name: string; oneOrMore: boolean } | undefined;
|
|
154
103
|
|
|
155
104
|
let regexPattern = "";
|
|
156
105
|
|
|
157
106
|
for (const segment of segments) {
|
|
158
107
|
if (segment.type === "wildcard") {
|
|
159
|
-
|
|
160
|
-
|
|
108
|
+
// Wildcards capture the remainder under `segment.value` ("*" for the bare
|
|
109
|
+
// form, the param name for a named catch-all).
|
|
110
|
+
paramNames.push(segment.value);
|
|
111
|
+
catchAll = { name: segment.value, oneOrMore: Boolean(segment.oneOrMore) };
|
|
112
|
+
if (segment.oneOrMore) {
|
|
113
|
+
// `:name+` — one-or-more, rejects the zero-segment (bare-prefix) case.
|
|
114
|
+
regexPattern += "/(.+)";
|
|
115
|
+
} else {
|
|
116
|
+
// Zero-or-more catch-all: named `:name*` OR the bare `/*` (both parse to
|
|
117
|
+
// `oneOrMore: false`). The whole `/segment` is optional so the bare
|
|
118
|
+
// prefix matches directly, aligning the regex fallback with the trie
|
|
119
|
+
// (which already matches the bare prefix binding "" — trie-matching.ts);
|
|
120
|
+
// buildParamsFromMatch binds "" when the optional group is absent.
|
|
121
|
+
//
|
|
122
|
+
// The bare `/*` previously used a required `/(.*)`, so `/files/*` failed
|
|
123
|
+
// to match `/files` and fell through to trailing-slash normalization,
|
|
124
|
+
// emitting a corrupt `/file` redirect instead of a match (issue #636,
|
|
125
|
+
// parity row C1). It is the same alignment #635 made for named `:name*`.
|
|
126
|
+
regexPattern += "(?:/(.*))?";
|
|
127
|
+
}
|
|
161
128
|
} else if (segment.type === "param") {
|
|
162
129
|
paramNames.push(segment.value);
|
|
163
|
-
const suffixPattern = segment.suffix ?
|
|
130
|
+
const suffixPattern = segment.suffix ? escapeRegExp(segment.suffix) : "";
|
|
164
131
|
// Constrained params capture anything here; the allowed values are
|
|
165
132
|
// checked post-decode in findMatch so URL-encoded constraint values
|
|
166
133
|
// (e.g. `:lang(en GB)` via `/en%20GB`) still match.
|
|
@@ -171,7 +138,6 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
171
138
|
}
|
|
172
139
|
|
|
173
140
|
if (segment.optional) {
|
|
174
|
-
optionalParams.add(segment.value);
|
|
175
141
|
// Optional: make the whole /segment optional
|
|
176
142
|
regexPattern += `(?:/${valuePattern}${suffixPattern})?`;
|
|
177
143
|
} else {
|
|
@@ -179,11 +145,10 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
179
145
|
}
|
|
180
146
|
} else {
|
|
181
147
|
// Static segment
|
|
182
|
-
regexPattern += `/${
|
|
148
|
+
regexPattern += `/${escapeRegExp(segment.value)}`;
|
|
183
149
|
}
|
|
184
150
|
}
|
|
185
151
|
|
|
186
|
-
// Handle root path
|
|
187
152
|
if (regexPattern === "") {
|
|
188
153
|
regexPattern = "/";
|
|
189
154
|
}
|
|
@@ -210,9 +175,9 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
210
175
|
return {
|
|
211
176
|
regex: new RegExp(`^${regexPattern}$`),
|
|
212
177
|
paramNames,
|
|
213
|
-
optionalParams,
|
|
214
178
|
hasTrailingSlash,
|
|
215
179
|
...(constraints ? { constraints } : {}),
|
|
180
|
+
...(catchAll ? { catchAll } : {}),
|
|
216
181
|
};
|
|
217
182
|
}
|
|
218
183
|
|
|
@@ -241,29 +206,35 @@ function satisfiesConstraints(
|
|
|
241
206
|
return true;
|
|
242
207
|
}
|
|
243
208
|
|
|
244
|
-
/**
|
|
245
|
-
* Escape special regex characters in a string
|
|
246
|
-
*/
|
|
247
|
-
function escapeRegex(str: string): string {
|
|
248
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
249
|
-
}
|
|
250
|
-
|
|
251
209
|
/**
|
|
252
210
|
* Build the named-params record from a regex match. Optional segments that
|
|
253
211
|
* didn't capture leave the corresponding group `undefined`; we skip those
|
|
254
212
|
* keys so `ctx.params.<name>` reads as `undefined` rather than `""`. This
|
|
255
213
|
* keeps the runtime aligned with the `ExtractParams` type and matches the
|
|
256
214
|
* trie matcher's contract (see `trie-matching.ts:validateAndBuild`).
|
|
215
|
+
*
|
|
216
|
+
* A zero-or-more catch-all (`compiled.catchAll`, `oneOrMore: false`) whose
|
|
217
|
+
* optional group didn't capture binds "" instead of being omitted, so `/docs`
|
|
218
|
+
* matches `/docs/:slug*` with `slug === ""`. Exported so the `renderRoute`
|
|
219
|
+
* testing harness (`matchLeaf`) shares this exact logic instead of forking it.
|
|
257
220
|
*/
|
|
258
|
-
function buildParamsFromMatch(
|
|
221
|
+
export function buildParamsFromMatch(
|
|
259
222
|
match: RegExpExecArray,
|
|
260
223
|
paramNames: string[],
|
|
224
|
+
catchAll?: { name: string; oneOrMore: boolean },
|
|
261
225
|
): Record<string, string> {
|
|
262
226
|
const params: Record<string, string> = {};
|
|
263
227
|
paramNames.forEach((name, index) => {
|
|
264
228
|
const captured = match[index + 1];
|
|
265
229
|
if (captured !== undefined) {
|
|
230
|
+
// A catch-all remainder decodes identically whether split-per-segment or
|
|
231
|
+
// whole-string (a literal `/` never lives inside a `%XX` escape), so a
|
|
232
|
+
// single decode is correct and cheapest.
|
|
266
233
|
params[name] = safeDecodeURIComponent(captured);
|
|
234
|
+
} else if (catchAll && name === catchAll.name && !catchAll.oneOrMore) {
|
|
235
|
+
// A zero-or-more catch-all (`:name*` or the bare `/*`) whose optional
|
|
236
|
+
// group was absent binds "" rather than being omitted.
|
|
237
|
+
params[name] = "";
|
|
267
238
|
}
|
|
268
239
|
});
|
|
269
240
|
return params;
|
|
@@ -285,36 +256,49 @@ function buildParamsFromMatch(
|
|
|
285
256
|
export function extractStaticPrefix(pattern: string): string {
|
|
286
257
|
if (!pattern || pattern === "/") return "";
|
|
287
258
|
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
259
|
+
// Walk segments and stop at the first that is a real param (`:name`) or a
|
|
260
|
+
// wildcard (`*`). A literal `:` or `*` not at a segment boundary (e.g. the
|
|
261
|
+
// `a:b` in `/a:b/c/:id`, or `tel:+1`) is a STATIC segment and must NOT
|
|
262
|
+
// terminate the prefix — `pattern.indexOf(":")` misread it as a param marker,
|
|
263
|
+
// returning "" and dropping the findMatch fast-skip optimization for that
|
|
264
|
+
// entry on every request. Classification mirrors parsePattern: a leading `:`
|
|
265
|
+
// marks a param, a leading `*` marks a wildcard.
|
|
266
|
+
const hasLeadingSlash = pattern.startsWith("/");
|
|
267
|
+
const body = hasLeadingSlash ? pattern.slice(1) : pattern;
|
|
268
|
+
const segments = body.split("/");
|
|
269
|
+
|
|
270
|
+
const staticSegments: string[] = [];
|
|
271
|
+
for (const segment of segments) {
|
|
272
|
+
if (segment.startsWith(":") || segment.startsWith("*")) {
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
staticSegments.push(segment);
|
|
304
276
|
}
|
|
305
277
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
278
|
+
// No leading static segment (first segment is a param/wildcard) -> no prefix.
|
|
279
|
+
if (staticSegments.length === 0) return "";
|
|
280
|
+
// Every segment was static (no param/wildcard) -> the whole pattern is the
|
|
281
|
+
// prefix. Preserve a trailing slash only when it existed in the input; a
|
|
282
|
+
// split of "/a/b" yields ["a","b"] (no empty tail) so a re-join is exact.
|
|
283
|
+
if (staticSegments.length === segments.length) return pattern;
|
|
310
284
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
return "";
|
|
315
|
-
}
|
|
285
|
+
const prefix = staticSegments.join("/");
|
|
286
|
+
return hasLeadingSlash ? "/" + prefix : prefix;
|
|
287
|
+
}
|
|
316
288
|
|
|
317
|
-
|
|
289
|
+
/**
|
|
290
|
+
* Join a URL prefix to a sub-prefix, collapsing the duplicate slash when the
|
|
291
|
+
* base ends with "/" and the sub-prefix starts with "/". This mirrors the
|
|
292
|
+
* canonical join in `include()` (urls/include-helper.ts) and `runWithPrefixes`
|
|
293
|
+
* (server/context.ts) so a nested lazy include's runtime staticPrefix matches
|
|
294
|
+
* the build-time trie's `sp` (e.g. `include("/parent/", …)` containing
|
|
295
|
+
* `include("/child", …)` resolves to `/parent/child`, not `/parent//child`).
|
|
296
|
+
*/
|
|
297
|
+
export function joinPrefix(base: string | undefined, prefix: string): string {
|
|
298
|
+
if (!base) return prefix;
|
|
299
|
+
return base.endsWith("/") && prefix.startsWith("/")
|
|
300
|
+
? base + prefix.slice(1)
|
|
301
|
+
: base + prefix;
|
|
318
302
|
}
|
|
319
303
|
|
|
320
304
|
/**
|
|
@@ -322,8 +306,8 @@ export function extractStaticPrefix(pattern: string): string {
|
|
|
322
306
|
*
|
|
323
307
|
* Note: Optional params that are absent in the path are omitted from the
|
|
324
308
|
* returned `params` (read as `undefined`), matching the trie matcher and
|
|
325
|
-
* the `ExtractParams<"/:locale?/...">` type. Use the pattern definition
|
|
326
|
-
*
|
|
309
|
+
* the `ExtractParams<"/:locale?/...">` type. Use the pattern definition to
|
|
310
|
+
* determine which keys are optional.
|
|
327
311
|
*
|
|
328
312
|
* Trailing slash handling (priority order):
|
|
329
313
|
* 1. Per-route `trailingSlash` config from route()
|
|
@@ -341,10 +325,7 @@ export interface RouteMatchResult<TEnv = any> {
|
|
|
341
325
|
entry: RouteEntry<TEnv>;
|
|
342
326
|
routeKey: string;
|
|
343
327
|
params: Record<string, string>;
|
|
344
|
-
optionalParams: Set<string>;
|
|
345
328
|
redirectTo?: string;
|
|
346
|
-
/** Ancestry shortCodes for layout pruning (from trie match) */
|
|
347
|
-
ancestry?: string[];
|
|
348
329
|
/** Route has pre-rendered data available (from trie) */
|
|
349
330
|
pr?: true;
|
|
350
331
|
/** Passthrough: handler kept for live fallback on unknown params (from trie) */
|
|
@@ -422,8 +403,6 @@ export function findMatch<TEnv>(
|
|
|
422
403
|
: pathname + "/";
|
|
423
404
|
|
|
424
405
|
for (const entry of routesEntries) {
|
|
425
|
-
// Short-circuit: skip entry if pathname doesn't start with static prefix
|
|
426
|
-
// staticPrefix is pre-computed at registration time, so this is O(1)
|
|
427
406
|
if (entry.staticPrefix && !pathname.startsWith(entry.staticPrefix)) {
|
|
428
407
|
if (effectiveDebug) {
|
|
429
408
|
debugStats.entriesSkipped++;
|
|
@@ -435,8 +414,6 @@ export function findMatch<TEnv>(
|
|
|
435
414
|
continue;
|
|
436
415
|
}
|
|
437
416
|
|
|
438
|
-
// Check if this is a lazy entry that needs evaluation
|
|
439
|
-
// When staticPrefix matches but routes are not yet populated, signal caller to evaluate
|
|
440
417
|
if (entry.lazy && !entry.lazyEvaluated) {
|
|
441
418
|
if (effectiveDebug) {
|
|
442
419
|
debugLog("findMatch", "lazy entry requires evaluation", {
|
|
@@ -457,7 +434,6 @@ export function findMatch<TEnv>(
|
|
|
457
434
|
debugStats.routesChecked++;
|
|
458
435
|
}
|
|
459
436
|
|
|
460
|
-
// Join prefix and pattern, handling edge cases
|
|
461
437
|
let fullPattern: string;
|
|
462
438
|
if (entry.prefix === "" || entry.prefix === "/") {
|
|
463
439
|
fullPattern = pattern;
|
|
@@ -467,19 +443,12 @@ export function findMatch<TEnv>(
|
|
|
467
443
|
fullPattern = entry.prefix + pattern;
|
|
468
444
|
}
|
|
469
445
|
|
|
470
|
-
const {
|
|
471
|
-
|
|
472
|
-
paramNames,
|
|
473
|
-
optionalParams,
|
|
474
|
-
hasTrailingSlash,
|
|
475
|
-
constraints,
|
|
476
|
-
} = getCompiledPattern(fullPattern);
|
|
446
|
+
const { regex, paramNames, hasTrailingSlash, constraints, catchAll } =
|
|
447
|
+
getCompiledPattern(fullPattern);
|
|
477
448
|
|
|
478
|
-
// Get trailing slash mode for this route (per-route config or pattern-based)
|
|
479
449
|
const trailingSlashMode: TrailingSlashMode | undefined =
|
|
480
450
|
entry.trailingSlash?.[routeKey];
|
|
481
451
|
|
|
482
|
-
// Prerender flag from entry metadata (set by urls() for prerender handlers)
|
|
483
452
|
const prFlag = entry.prerenderRouteKeys?.has(routeKey)
|
|
484
453
|
? { pr: true as const }
|
|
485
454
|
: {};
|
|
@@ -487,13 +456,10 @@ export function findMatch<TEnv>(
|
|
|
487
456
|
? { pt: true as const }
|
|
488
457
|
: {};
|
|
489
458
|
|
|
490
|
-
// Try exact match first
|
|
491
459
|
const match = regex.exec(pathname);
|
|
492
460
|
if (match) {
|
|
493
|
-
const params = buildParamsFromMatch(match, paramNames);
|
|
461
|
+
const params = buildParamsFromMatch(match, paramNames, catchAll);
|
|
494
462
|
|
|
495
|
-
// Validate constraints against decoded values; a failure falls
|
|
496
|
-
// through to the next route so other patterns can still match.
|
|
497
463
|
if (!satisfiesConstraints(params, constraints)) {
|
|
498
464
|
continue;
|
|
499
465
|
}
|
|
@@ -506,29 +472,24 @@ export function findMatch<TEnv>(
|
|
|
506
472
|
});
|
|
507
473
|
}
|
|
508
474
|
|
|
509
|
-
// Check if trailing slash mode requires redirect even on exact match
|
|
510
475
|
if (
|
|
511
476
|
trailingSlashMode === "always" &&
|
|
512
477
|
!pathnameHasTrailingSlash &&
|
|
513
478
|
pathname !== "/"
|
|
514
479
|
) {
|
|
515
|
-
// Mode says always have trailing slash, but pathname doesn't have it
|
|
516
480
|
return {
|
|
517
481
|
entry,
|
|
518
482
|
routeKey,
|
|
519
483
|
params,
|
|
520
|
-
optionalParams,
|
|
521
484
|
redirectTo: pathname + "/",
|
|
522
485
|
...prFlag,
|
|
523
486
|
...ptFlag,
|
|
524
487
|
};
|
|
525
488
|
} else if (trailingSlashMode === "never" && pathnameHasTrailingSlash) {
|
|
526
|
-
// Mode says never have trailing slash, but pathname has it
|
|
527
489
|
return {
|
|
528
490
|
entry,
|
|
529
491
|
routeKey,
|
|
530
492
|
params,
|
|
531
|
-
optionalParams,
|
|
532
493
|
redirectTo: pathname.slice(0, -1),
|
|
533
494
|
...prFlag,
|
|
534
495
|
...ptFlag,
|
|
@@ -539,40 +500,33 @@ export function findMatch<TEnv>(
|
|
|
539
500
|
entry,
|
|
540
501
|
routeKey,
|
|
541
502
|
params,
|
|
542
|
-
optionalParams,
|
|
543
503
|
...prFlag,
|
|
544
504
|
...ptFlag,
|
|
545
505
|
};
|
|
546
506
|
}
|
|
547
507
|
|
|
548
|
-
// Try alternate pathname (opposite trailing slash)
|
|
549
508
|
const altMatch = regex.exec(alternatePathname);
|
|
550
509
|
if (altMatch) {
|
|
551
|
-
const params = buildParamsFromMatch(altMatch, paramNames);
|
|
510
|
+
const params = buildParamsFromMatch(altMatch, paramNames, catchAll);
|
|
552
511
|
|
|
553
512
|
if (!satisfiesConstraints(params, constraints)) {
|
|
554
513
|
continue;
|
|
555
514
|
}
|
|
556
515
|
|
|
557
|
-
// Determine redirect behavior based on mode
|
|
558
516
|
if (trailingSlashMode === "ignore") {
|
|
559
|
-
// Match without redirect
|
|
560
517
|
return {
|
|
561
518
|
entry,
|
|
562
519
|
routeKey,
|
|
563
520
|
params,
|
|
564
|
-
optionalParams,
|
|
565
521
|
...prFlag,
|
|
566
522
|
...ptFlag,
|
|
567
523
|
};
|
|
568
524
|
} else if (trailingSlashMode === "never") {
|
|
569
|
-
// Redirect to no trailing slash
|
|
570
525
|
if (pathnameHasTrailingSlash) {
|
|
571
526
|
return {
|
|
572
527
|
entry,
|
|
573
528
|
routeKey,
|
|
574
529
|
params,
|
|
575
|
-
optionalParams,
|
|
576
530
|
redirectTo: alternatePathname,
|
|
577
531
|
...prFlag,
|
|
578
532
|
...ptFlag,
|
|
@@ -582,18 +536,15 @@ export function findMatch<TEnv>(
|
|
|
582
536
|
entry,
|
|
583
537
|
routeKey,
|
|
584
538
|
params,
|
|
585
|
-
optionalParams,
|
|
586
539
|
...prFlag,
|
|
587
540
|
...ptFlag,
|
|
588
541
|
};
|
|
589
542
|
} else if (trailingSlashMode === "always") {
|
|
590
|
-
// Redirect to with trailing slash
|
|
591
543
|
if (!pathnameHasTrailingSlash) {
|
|
592
544
|
return {
|
|
593
545
|
entry,
|
|
594
546
|
routeKey,
|
|
595
547
|
params,
|
|
596
|
-
optionalParams,
|
|
597
548
|
redirectTo: alternatePathname,
|
|
598
549
|
...prFlag,
|
|
599
550
|
...ptFlag,
|
|
@@ -603,13 +554,10 @@ export function findMatch<TEnv>(
|
|
|
603
554
|
entry,
|
|
604
555
|
routeKey,
|
|
605
556
|
params,
|
|
606
|
-
optionalParams,
|
|
607
557
|
...prFlag,
|
|
608
558
|
...ptFlag,
|
|
609
559
|
};
|
|
610
560
|
} else {
|
|
611
|
-
// No explicit mode - use pattern-based detection
|
|
612
|
-
// Redirect to canonical form (what the pattern defines)
|
|
613
561
|
const canonicalPath = hasTrailingSlash
|
|
614
562
|
? alternatePathname
|
|
615
563
|
: pathname.slice(0, -1);
|
|
@@ -617,7 +565,6 @@ export function findMatch<TEnv>(
|
|
|
617
565
|
entry,
|
|
618
566
|
routeKey,
|
|
619
567
|
params,
|
|
620
|
-
optionalParams,
|
|
621
568
|
redirectTo: canonicalPath,
|
|
622
569
|
...prFlag,
|
|
623
570
|
...ptFlag,
|
|
@@ -638,7 +585,7 @@ export function* traverseBack(entry: EntryData): Generator<EntryData> {
|
|
|
638
585
|
let current: EntryData | null = entry;
|
|
639
586
|
const items = [] as EntryData[];
|
|
640
587
|
while (current !== null) {
|
|
641
|
-
items.push(current);
|
|
588
|
+
items.push(current);
|
|
642
589
|
current = current.parent;
|
|
643
590
|
}
|
|
644
591
|
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
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the client-side prefetch limits — the in-memory prefetch cache size
|
|
3
|
+
* and the speculative-prefetch queue concurrency — once, at router init. Both
|
|
4
|
+
* are positive-integer counts shipped to the browser in payload metadata; the
|
|
5
|
+
* browser entry feeds them to the prefetch cache (`initPrefetchCache`) and the
|
|
6
|
+
* prefetch queue (`setPrefetchConcurrency`) at startup.
|
|
7
|
+
*
|
|
8
|
+
* Policy note: this is a SEPARATE policy from resolvePrefetchCacheTTL
|
|
9
|
+
* (prefetch-cache-ttl.ts). These are counts, not durations, so the disable
|
|
10
|
+
* paths differ: a TTL of `false`/0 intentionally turns prefetching off, but a
|
|
11
|
+
* cache size or concurrency of 0 would silently break prefetching while leaving
|
|
12
|
+
* it nominally enabled — never the intent of passing a number. So any value
|
|
13
|
+
* that is not a finite integer >= 1 (0, negative, fractional below 1, NaN,
|
|
14
|
+
* Infinity, undefined) falls back to the default. Finite values >= 1 are
|
|
15
|
+
* floored. Do not unify this guard with the TTL guard.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_PREFETCH_CACHE_SIZE = 100;
|
|
19
|
+
export const DEFAULT_PREFETCH_CONCURRENCY = 2;
|
|
20
|
+
|
|
21
|
+
export function resolvePrefetchCacheSize(raw: number | undefined): number {
|
|
22
|
+
return resolvePositiveCount(raw, DEFAULT_PREFETCH_CACHE_SIZE);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolvePrefetchConcurrency(raw: number | undefined): number {
|
|
26
|
+
return resolvePositiveCount(raw, DEFAULT_PREFETCH_CONCURRENCY);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolvePositiveCount(
|
|
30
|
+
raw: number | undefined,
|
|
31
|
+
fallback: number,
|
|
32
|
+
): number {
|
|
33
|
+
if (typeof raw === "number" && Number.isFinite(raw) && raw >= 1) {
|
|
34
|
+
return Math.floor(raw);
|
|
35
|
+
}
|
|
36
|
+
return fallback;
|
|
37
|
+
}
|