@rangojs/router 0.0.0-experimental.31 → 0.0.0-experimental.3232cd17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -0
- package/README.md +198 -44
- package/dist/bin/rango.js +287 -105
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3248 -1117
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +73 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +107 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +245 -21
- package/skills/caching/SKILL.md +302 -6
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +270 -30
- package/skills/host-router/SKILL.md +82 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +49 -5
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +294 -30
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +203 -7
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +250 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +97 -5
- package/skills/router-setup/SKILL.md +90 -5
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +121 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +329 -27
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +67 -40
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +86 -147
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +148 -19
- package/src/browser/navigation-client.ts +187 -67
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +18 -66
- package/src/browser/partial-update.ts +123 -94
- package/src/browser/prefetch/cache.ts +214 -36
- package/src/browser/prefetch/fetch.ts +260 -38
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +126 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +93 -11
- package/src/browser/react/NavigationProvider.tsx +115 -34
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +49 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +23 -69
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +22 -5
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +52 -1
- package/src/browser/rsc-router.tsx +215 -76
- package/src/browser/scroll-restoration.ts +46 -39
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +176 -50
- package/src/browser/types.ts +95 -11
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +137 -32
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -96
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +149 -43
- package/src/cache/cache-scope.ts +148 -81
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2550 -93
- package/src/cache/cf/index.ts +11 -17
- package/src/cache/document-cache.ts +78 -27
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +108 -290
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +84 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +52 -26
- package/src/index.ts +100 -38
- package/src/internal-debug.ts +2 -4
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +437 -274
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +113 -37
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +52 -10
- package/src/route-definition/resolve-handler-use.ts +161 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -17
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +108 -9
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +45 -22
- package/src/router/handler-context.ts +83 -41
- package/src/router/intercept-resolution.ts +25 -23
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +213 -30
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +49 -45
- package/src/router/match-api.ts +121 -205
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +27 -6
- package/src/router/match-middleware/cache-lookup.ts +205 -249
- package/src/router/match-middleware/cache-store.ts +45 -32
- package/src/router/match-middleware/intercept-resolution.ts +8 -28
- package/src/router/match-middleware/segment-resolution.ts +52 -18
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +104 -40
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +173 -143
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +109 -63
- package/src/router/prerender-match.ts +192 -54
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +63 -55
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +6 -28
- package/src/router/router-interfaces.ts +100 -35
- package/src/router/router-options.ts +91 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +242 -75
- package/src/router/segment-resolution/helpers.ts +64 -25
- package/src/router/segment-resolution/loader-cache.ts +41 -37
- package/src/router/segment-resolution/revalidation.ts +456 -372
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +2 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +91 -46
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +134 -43
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +492 -383
- package/src/rsc/helpers.ts +162 -46
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +30 -3
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +90 -63
- package/src/rsc/rsc-rendering.ts +56 -54
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +74 -67
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +25 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +134 -0
- package/src/segment-system.tsx +272 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +309 -61
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +26 -24
- package/src/server/loader-registry.ts +10 -28
- package/src/server/request-context.ts +348 -128
- package/src/ssr/index.tsx +23 -15
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +618 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +233 -81
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +107 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +37 -14
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +58 -11
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/discover-routers.ts +130 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +192 -99
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +51 -6
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +8 -0
- package/src/vite/plugin-types.ts +187 -69
- package/src/vite/plugins/cjs-to-esm.ts +8 -18
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
- package/src/vite/plugins/expose-action-id.ts +49 -98
- package/src/vite/plugins/expose-id-utils.ts +11 -50
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
- package/src/vite/plugins/expose-internal-ids.ts +554 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +41 -20
- package/src/vite/plugins/virtual-entries.ts +2 -17
- package/src/vite/rango.ts +257 -289
- package/src/vite/router-discovery.ts +930 -140
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +27 -29
- package/src/vite/utils/shared-utils.ts +92 -42
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -7,6 +7,7 @@
|
|
|
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 { safeDecodeURIComponent } from "./url-params.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Parsed segment info
|
|
@@ -32,13 +33,6 @@ export interface ParsedSegment {
|
|
|
32
33
|
*/
|
|
33
34
|
export function parsePattern(pattern: string): ParsedSegment[] {
|
|
34
35
|
const segments: ParsedSegment[] = [];
|
|
35
|
-
// Match: /segment where segment can be:
|
|
36
|
-
// - static text
|
|
37
|
-
// - :param
|
|
38
|
-
// - :param?
|
|
39
|
-
// - :param(a|b)
|
|
40
|
-
// - :param(a|b)?
|
|
41
|
-
// - *
|
|
42
36
|
const segmentRegex =
|
|
43
37
|
/\/(:([a-zA-Z_][a-zA-Z0-9_]*)(\(([^)]+)\))?(\?)?([^/]*)|(\*)|([^/]+))/g;
|
|
44
38
|
|
|
@@ -80,8 +74,14 @@ export function parsePattern(pattern: string): ParsedSegment[] {
|
|
|
80
74
|
export interface CompiledPattern {
|
|
81
75
|
regex: RegExp;
|
|
82
76
|
paramNames: string[];
|
|
83
|
-
optionalParams: Set<string>;
|
|
84
77
|
hasTrailingSlash: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Param-name → allowed values for constrained params (e.g. `:lang(en|gb)`).
|
|
80
|
+
* Validated against the **decoded** param value after regex extraction so
|
|
81
|
+
* a URL like `/en%20GB` still matches `:lang(en GB)` — matching the trie
|
|
82
|
+
* path's behavior (trie-matching.ts:validateAndBuild).
|
|
83
|
+
*/
|
|
84
|
+
constraints?: Record<string, string[]>;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
// Module-level cache for compiled patterns. Route patterns are a finite set
|
|
@@ -141,7 +141,7 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
141
141
|
|
|
142
142
|
const segments = parsePattern(normalizedPattern);
|
|
143
143
|
const paramNames: string[] = [];
|
|
144
|
-
|
|
144
|
+
let constraints: Record<string, string[]> | undefined;
|
|
145
145
|
|
|
146
146
|
let regexPattern = "";
|
|
147
147
|
|
|
@@ -152,14 +152,16 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
152
152
|
} else if (segment.type === "param") {
|
|
153
153
|
paramNames.push(segment.value);
|
|
154
154
|
const suffixPattern = segment.suffix ? escapeRegex(segment.suffix) : "";
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
155
|
+
// Constrained params capture anything here; the allowed values are
|
|
156
|
+
// checked post-decode in findMatch so URL-encoded constraint values
|
|
157
|
+
// (e.g. `:lang(en GB)` via `/en%20GB`) still match.
|
|
158
|
+
const valuePattern = segment.suffix ? "([^/]+?)" : "([^/]+)";
|
|
159
|
+
|
|
160
|
+
if (segment.constraint) {
|
|
161
|
+
(constraints ??= {})[segment.value] = segment.constraint;
|
|
162
|
+
}
|
|
160
163
|
|
|
161
164
|
if (segment.optional) {
|
|
162
|
-
optionalParams.add(segment.value);
|
|
163
165
|
// Optional: make the whole /segment optional
|
|
164
166
|
regexPattern += `(?:/${valuePattern}${suffixPattern})?`;
|
|
165
167
|
} else {
|
|
@@ -171,11 +173,24 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
171
173
|
}
|
|
172
174
|
}
|
|
173
175
|
|
|
174
|
-
// Handle root path
|
|
175
176
|
if (regexPattern === "") {
|
|
176
177
|
regexPattern = "/";
|
|
177
178
|
}
|
|
178
179
|
|
|
180
|
+
// Patterns of only optional segments (e.g. `/:locale?`, `/:a?/:b?`) need
|
|
181
|
+
// an explicit `/` alternative so a bare `/` matches the absent form. The
|
|
182
|
+
// optional template `(?:/X)?` matches `/X` or empty string, but pathnames
|
|
183
|
+
// are never empty. Arises from `include("/:locale?", routes)` + inner
|
|
184
|
+
// `path("/")`. Skip when an explicit trailing slash already anchors the
|
|
185
|
+
// match.
|
|
186
|
+
const hasOnlyOptionalSegments =
|
|
187
|
+
!hasTrailingSlash &&
|
|
188
|
+
segments.length > 0 &&
|
|
189
|
+
segments.every((segment) => segment.type === "param" && segment.optional);
|
|
190
|
+
if (hasOnlyOptionalSegments) {
|
|
191
|
+
regexPattern = `(?:/|${regexPattern})`;
|
|
192
|
+
}
|
|
193
|
+
|
|
179
194
|
// Add trailing slash to regex if pattern has one
|
|
180
195
|
if (hasTrailingSlash) {
|
|
181
196
|
regexPattern += "/";
|
|
@@ -184,11 +199,36 @@ export function compilePattern(pattern: string): CompiledPattern {
|
|
|
184
199
|
return {
|
|
185
200
|
regex: new RegExp(`^${regexPattern}$`),
|
|
186
201
|
paramNames,
|
|
187
|
-
optionalParams,
|
|
188
202
|
hasTrailingSlash,
|
|
203
|
+
...(constraints ? { constraints } : {}),
|
|
189
204
|
};
|
|
190
205
|
}
|
|
191
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Validate decoded params against a compiled pattern's constraints.
|
|
209
|
+
* Returns false if any constrained param has a non-empty value not in the
|
|
210
|
+
* allowed list. Absent optionals (key missing or `undefined`) are allowed;
|
|
211
|
+
* `""` is also tolerated as "absent" so user-provided params or fixtures
|
|
212
|
+
* that pass empty strings explicitly behave the same way.
|
|
213
|
+
*/
|
|
214
|
+
function satisfiesConstraints(
|
|
215
|
+
params: Record<string, string>,
|
|
216
|
+
constraints: Record<string, string[]> | undefined,
|
|
217
|
+
): boolean {
|
|
218
|
+
if (!constraints) return true;
|
|
219
|
+
for (const name in constraints) {
|
|
220
|
+
const value = params[name];
|
|
221
|
+
if (
|
|
222
|
+
value !== undefined &&
|
|
223
|
+
value !== "" &&
|
|
224
|
+
!constraints[name].includes(value)
|
|
225
|
+
) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
|
|
192
232
|
/**
|
|
193
233
|
* Escape special regex characters in a string
|
|
194
234
|
*/
|
|
@@ -196,6 +236,27 @@ function escapeRegex(str: string): string {
|
|
|
196
236
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
197
237
|
}
|
|
198
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Build the named-params record from a regex match. Optional segments that
|
|
241
|
+
* didn't capture leave the corresponding group `undefined`; we skip those
|
|
242
|
+
* keys so `ctx.params.<name>` reads as `undefined` rather than `""`. This
|
|
243
|
+
* keeps the runtime aligned with the `ExtractParams` type and matches the
|
|
244
|
+
* trie matcher's contract (see `trie-matching.ts:validateAndBuild`).
|
|
245
|
+
*/
|
|
246
|
+
function buildParamsFromMatch(
|
|
247
|
+
match: RegExpExecArray,
|
|
248
|
+
paramNames: string[],
|
|
249
|
+
): Record<string, string> {
|
|
250
|
+
const params: Record<string, string> = {};
|
|
251
|
+
paramNames.forEach((name, index) => {
|
|
252
|
+
const captured = match[index + 1];
|
|
253
|
+
if (captured !== undefined) {
|
|
254
|
+
params[name] = safeDecodeURIComponent(captured);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
return params;
|
|
258
|
+
}
|
|
259
|
+
|
|
199
260
|
/**
|
|
200
261
|
* Extract the static prefix from a route pattern.
|
|
201
262
|
* Returns everything before the first param/wildcard.
|
|
@@ -212,7 +273,6 @@ function escapeRegex(str: string): string {
|
|
|
212
273
|
export function extractStaticPrefix(pattern: string): string {
|
|
213
274
|
if (!pattern || pattern === "/") return "";
|
|
214
275
|
|
|
215
|
-
// Find the first occurrence of : or *
|
|
216
276
|
const paramIndex = pattern.indexOf(":");
|
|
217
277
|
const wildcardIndex = pattern.indexOf("*");
|
|
218
278
|
|
|
@@ -226,16 +286,13 @@ export function extractStaticPrefix(pattern: string): string {
|
|
|
226
286
|
}
|
|
227
287
|
|
|
228
288
|
if (cutIndex === -1) {
|
|
229
|
-
// No params or wildcards - entire pattern is static
|
|
230
289
|
return pattern;
|
|
231
290
|
}
|
|
232
291
|
|
|
233
292
|
if (cutIndex === 0) {
|
|
234
|
-
// Pattern starts with : or * - no static prefix
|
|
235
293
|
return "";
|
|
236
294
|
}
|
|
237
295
|
|
|
238
|
-
// Find the last / before the param
|
|
239
296
|
const lastSlash = pattern.lastIndexOf("/", cutIndex - 1);
|
|
240
297
|
if (lastSlash === -1 || lastSlash === 0) {
|
|
241
298
|
return "";
|
|
@@ -244,11 +301,28 @@ export function extractStaticPrefix(pattern: string): string {
|
|
|
244
301
|
return pattern.slice(0, lastSlash);
|
|
245
302
|
}
|
|
246
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Join a URL prefix to a sub-prefix, collapsing the duplicate slash when the
|
|
306
|
+
* base ends with "/" and the sub-prefix starts with "/". This mirrors the
|
|
307
|
+
* canonical join in `include()` (urls/include-helper.ts) and `runWithPrefixes`
|
|
308
|
+
* (server/context.ts) so a nested lazy include's runtime staticPrefix matches
|
|
309
|
+
* the build-time trie's `sp` (e.g. `include("/parent/", …)` containing
|
|
310
|
+
* `include("/child", …)` resolves to `/parent/child`, not `/parent//child`).
|
|
311
|
+
*/
|
|
312
|
+
export function joinPrefix(base: string | undefined, prefix: string): string {
|
|
313
|
+
if (!base) return prefix;
|
|
314
|
+
return base.endsWith("/") && prefix.startsWith("/")
|
|
315
|
+
? base + prefix.slice(1)
|
|
316
|
+
: base + prefix;
|
|
317
|
+
}
|
|
318
|
+
|
|
247
319
|
/**
|
|
248
320
|
* Match a pathname against registered routes
|
|
249
321
|
*
|
|
250
|
-
* Note: Optional params that are absent in the path
|
|
251
|
-
*
|
|
322
|
+
* Note: Optional params that are absent in the path are omitted from the
|
|
323
|
+
* returned `params` (read as `undefined`), matching the trie matcher and
|
|
324
|
+
* the `ExtractParams<"/:locale?/...">` type. Use the pattern definition to
|
|
325
|
+
* determine which keys are optional.
|
|
252
326
|
*
|
|
253
327
|
* Trailing slash handling (priority order):
|
|
254
328
|
* 1. Per-route `trailingSlash` config from route()
|
|
@@ -266,10 +340,7 @@ export interface RouteMatchResult<TEnv = any> {
|
|
|
266
340
|
entry: RouteEntry<TEnv>;
|
|
267
341
|
routeKey: string;
|
|
268
342
|
params: Record<string, string>;
|
|
269
|
-
optionalParams: Set<string>;
|
|
270
343
|
redirectTo?: string;
|
|
271
|
-
/** Ancestry shortCodes for layout pruning (from trie match) */
|
|
272
|
-
ancestry?: string[];
|
|
273
344
|
/** Route has pre-rendered data available (from trie) */
|
|
274
345
|
pr?: true;
|
|
275
346
|
/** Passthrough: handler kept for live fallback on unknown params (from trie) */
|
|
@@ -347,8 +418,6 @@ export function findMatch<TEnv>(
|
|
|
347
418
|
: pathname + "/";
|
|
348
419
|
|
|
349
420
|
for (const entry of routesEntries) {
|
|
350
|
-
// Short-circuit: skip entry if pathname doesn't start with static prefix
|
|
351
|
-
// staticPrefix is pre-computed at registration time, so this is O(1)
|
|
352
421
|
if (entry.staticPrefix && !pathname.startsWith(entry.staticPrefix)) {
|
|
353
422
|
if (effectiveDebug) {
|
|
354
423
|
debugStats.entriesSkipped++;
|
|
@@ -360,8 +429,6 @@ export function findMatch<TEnv>(
|
|
|
360
429
|
continue;
|
|
361
430
|
}
|
|
362
431
|
|
|
363
|
-
// Check if this is a lazy entry that needs evaluation
|
|
364
|
-
// When staticPrefix matches but routes are not yet populated, signal caller to evaluate
|
|
365
432
|
if (entry.lazy && !entry.lazyEvaluated) {
|
|
366
433
|
if (effectiveDebug) {
|
|
367
434
|
debugLog("findMatch", "lazy entry requires evaluation", {
|
|
@@ -382,7 +449,6 @@ export function findMatch<TEnv>(
|
|
|
382
449
|
debugStats.routesChecked++;
|
|
383
450
|
}
|
|
384
451
|
|
|
385
|
-
// Join prefix and pattern, handling edge cases
|
|
386
452
|
let fullPattern: string;
|
|
387
453
|
if (entry.prefix === "" || entry.prefix === "/") {
|
|
388
454
|
fullPattern = pattern;
|
|
@@ -392,14 +458,12 @@ export function findMatch<TEnv>(
|
|
|
392
458
|
fullPattern = entry.prefix + pattern;
|
|
393
459
|
}
|
|
394
460
|
|
|
395
|
-
const { regex, paramNames,
|
|
461
|
+
const { regex, paramNames, hasTrailingSlash, constraints } =
|
|
396
462
|
getCompiledPattern(fullPattern);
|
|
397
463
|
|
|
398
|
-
// Get trailing slash mode for this route (per-route config or pattern-based)
|
|
399
464
|
const trailingSlashMode: TrailingSlashMode | undefined =
|
|
400
465
|
entry.trailingSlash?.[routeKey];
|
|
401
466
|
|
|
402
|
-
// Prerender flag from entry metadata (set by urls() for prerender handlers)
|
|
403
467
|
const prFlag = entry.prerenderRouteKeys?.has(routeKey)
|
|
404
468
|
? { pr: true as const }
|
|
405
469
|
: {};
|
|
@@ -407,13 +471,13 @@ export function findMatch<TEnv>(
|
|
|
407
471
|
? { pt: true as const }
|
|
408
472
|
: {};
|
|
409
473
|
|
|
410
|
-
// Try exact match first
|
|
411
474
|
const match = regex.exec(pathname);
|
|
412
475
|
if (match) {
|
|
413
|
-
const params
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
476
|
+
const params = buildParamsFromMatch(match, paramNames);
|
|
477
|
+
|
|
478
|
+
if (!satisfiesConstraints(params, constraints)) {
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
417
481
|
|
|
418
482
|
if (effectiveDebug) {
|
|
419
483
|
debugLog("findMatch", "matched route", {
|
|
@@ -423,29 +487,24 @@ export function findMatch<TEnv>(
|
|
|
423
487
|
});
|
|
424
488
|
}
|
|
425
489
|
|
|
426
|
-
// Check if trailing slash mode requires redirect even on exact match
|
|
427
490
|
if (
|
|
428
491
|
trailingSlashMode === "always" &&
|
|
429
492
|
!pathnameHasTrailingSlash &&
|
|
430
493
|
pathname !== "/"
|
|
431
494
|
) {
|
|
432
|
-
// Mode says always have trailing slash, but pathname doesn't have it
|
|
433
495
|
return {
|
|
434
496
|
entry,
|
|
435
497
|
routeKey,
|
|
436
498
|
params,
|
|
437
|
-
optionalParams,
|
|
438
499
|
redirectTo: pathname + "/",
|
|
439
500
|
...prFlag,
|
|
440
501
|
...ptFlag,
|
|
441
502
|
};
|
|
442
503
|
} else if (trailingSlashMode === "never" && pathnameHasTrailingSlash) {
|
|
443
|
-
// Mode says never have trailing slash, but pathname has it
|
|
444
504
|
return {
|
|
445
505
|
entry,
|
|
446
506
|
routeKey,
|
|
447
507
|
params,
|
|
448
|
-
optionalParams,
|
|
449
508
|
redirectTo: pathname.slice(0, -1),
|
|
450
509
|
...prFlag,
|
|
451
510
|
...ptFlag,
|
|
@@ -456,39 +515,33 @@ export function findMatch<TEnv>(
|
|
|
456
515
|
entry,
|
|
457
516
|
routeKey,
|
|
458
517
|
params,
|
|
459
|
-
optionalParams,
|
|
460
518
|
...prFlag,
|
|
461
519
|
...ptFlag,
|
|
462
520
|
};
|
|
463
521
|
}
|
|
464
522
|
|
|
465
|
-
// Try alternate pathname (opposite trailing slash)
|
|
466
523
|
const altMatch = regex.exec(alternatePathname);
|
|
467
524
|
if (altMatch) {
|
|
468
|
-
const params
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
525
|
+
const params = buildParamsFromMatch(altMatch, paramNames);
|
|
526
|
+
|
|
527
|
+
if (!satisfiesConstraints(params, constraints)) {
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
472
530
|
|
|
473
|
-
// Determine redirect behavior based on mode
|
|
474
531
|
if (trailingSlashMode === "ignore") {
|
|
475
|
-
// Match without redirect
|
|
476
532
|
return {
|
|
477
533
|
entry,
|
|
478
534
|
routeKey,
|
|
479
535
|
params,
|
|
480
|
-
optionalParams,
|
|
481
536
|
...prFlag,
|
|
482
537
|
...ptFlag,
|
|
483
538
|
};
|
|
484
539
|
} else if (trailingSlashMode === "never") {
|
|
485
|
-
// Redirect to no trailing slash
|
|
486
540
|
if (pathnameHasTrailingSlash) {
|
|
487
541
|
return {
|
|
488
542
|
entry,
|
|
489
543
|
routeKey,
|
|
490
544
|
params,
|
|
491
|
-
optionalParams,
|
|
492
545
|
redirectTo: alternatePathname,
|
|
493
546
|
...prFlag,
|
|
494
547
|
...ptFlag,
|
|
@@ -498,18 +551,15 @@ export function findMatch<TEnv>(
|
|
|
498
551
|
entry,
|
|
499
552
|
routeKey,
|
|
500
553
|
params,
|
|
501
|
-
optionalParams,
|
|
502
554
|
...prFlag,
|
|
503
555
|
...ptFlag,
|
|
504
556
|
};
|
|
505
557
|
} else if (trailingSlashMode === "always") {
|
|
506
|
-
// Redirect to with trailing slash
|
|
507
558
|
if (!pathnameHasTrailingSlash) {
|
|
508
559
|
return {
|
|
509
560
|
entry,
|
|
510
561
|
routeKey,
|
|
511
562
|
params,
|
|
512
|
-
optionalParams,
|
|
513
563
|
redirectTo: alternatePathname,
|
|
514
564
|
...prFlag,
|
|
515
565
|
...ptFlag,
|
|
@@ -519,13 +569,10 @@ export function findMatch<TEnv>(
|
|
|
519
569
|
entry,
|
|
520
570
|
routeKey,
|
|
521
571
|
params,
|
|
522
|
-
optionalParams,
|
|
523
572
|
...prFlag,
|
|
524
573
|
...ptFlag,
|
|
525
574
|
};
|
|
526
575
|
} else {
|
|
527
|
-
// No explicit mode - use pattern-based detection
|
|
528
|
-
// Redirect to canonical form (what the pattern defines)
|
|
529
576
|
const canonicalPath = hasTrailingSlash
|
|
530
577
|
? alternatePathname
|
|
531
578
|
: pathname.slice(0, -1);
|
|
@@ -533,7 +580,6 @@ export function findMatch<TEnv>(
|
|
|
533
580
|
entry,
|
|
534
581
|
routeKey,
|
|
535
582
|
params,
|
|
536
|
-
optionalParams,
|
|
537
583
|
redirectTo: canonicalPath,
|
|
538
584
|
...prFlag,
|
|
539
585
|
...ptFlag,
|
|
@@ -554,7 +600,7 @@ export function* traverseBack(entry: EntryData): Generator<EntryData> {
|
|
|
554
600
|
let current: EntryData | null = entry;
|
|
555
601
|
const items = [] as EntryData[];
|
|
556
602
|
while (current !== null) {
|
|
557
|
-
items.push(current);
|
|
603
|
+
items.push(current);
|
|
558
604
|
current = current.parent;
|
|
559
605
|
}
|
|
560
606
|
for (let i = items.length - 1; i >= 0; i--) {
|