vinext 0.0.26 → 0.0.28
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/README.md +89 -85
- package/dist/build/static-export.d.ts +1 -1
- package/dist/build/static-export.d.ts.map +1 -1
- package/dist/build/static-export.js +5 -9
- package/dist/build/static-export.js.map +1 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +152 -48
- package/dist/check.js.map +1 -1
- package/dist/cli.js +10 -11
- package/dist/cli.js.map +1 -1
- package/dist/cloudflare/kv-cache-handler.d.ts +43 -1
- package/dist/cloudflare/kv-cache-handler.d.ts.map +1 -1
- package/dist/cloudflare/kv-cache-handler.js +135 -44
- package/dist/cloudflare/kv-cache-handler.js.map +1 -1
- package/dist/cloudflare/tpr.d.ts.map +1 -1
- package/dist/cloudflare/tpr.js +15 -4
- package/dist/cloudflare/tpr.js.map +1 -1
- package/dist/config/config-matchers.d.ts +28 -0
- package/dist/config/config-matchers.d.ts.map +1 -1
- package/dist/config/config-matchers.js +353 -79
- package/dist/config/config-matchers.js.map +1 -1
- package/dist/config/dotenv.d.ts.map +1 -1
- package/dist/config/dotenv.js +1 -6
- package/dist/config/dotenv.js.map +1 -1
- package/dist/config/next-config.d.ts +7 -0
- package/dist/config/next-config.d.ts.map +1 -1
- package/dist/config/next-config.js +44 -19
- package/dist/config/next-config.js.map +1 -1
- package/dist/deploy.d.ts +1 -1
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +81 -48
- package/dist/deploy.js.map +1 -1
- package/dist/entries/app-rsc-entry.d.ts +3 -1
- package/dist/entries/app-rsc-entry.d.ts.map +1 -1
- package/dist/entries/app-rsc-entry.js +584 -113
- package/dist/entries/app-rsc-entry.js.map +1 -1
- package/dist/entries/pages-client-entry.d.ts.map +1 -1
- package/dist/entries/pages-client-entry.js +5 -3
- package/dist/entries/pages-client-entry.js.map +1 -1
- package/dist/entries/pages-server-entry.d.ts.map +1 -1
- package/dist/entries/pages-server-entry.js +100 -32
- package/dist/entries/pages-server-entry.js.map +1 -1
- package/dist/index.d.ts +24 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +327 -154
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +6 -5
- package/dist/init.js.map +1 -1
- package/dist/plugins/client-reference-dedup.d.ts +19 -0
- package/dist/plugins/client-reference-dedup.d.ts.map +1 -0
- package/dist/plugins/client-reference-dedup.js +96 -0
- package/dist/plugins/client-reference-dedup.js.map +1 -0
- package/dist/routing/app-router.d.ts +2 -0
- package/dist/routing/app-router.d.ts.map +1 -1
- package/dist/routing/app-router.js +70 -107
- package/dist/routing/app-router.js.map +1 -1
- package/dist/routing/file-matcher.d.ts.map +1 -1
- package/dist/routing/file-matcher.js.map +1 -1
- package/dist/routing/pages-router.d.ts +3 -1
- package/dist/routing/pages-router.d.ts.map +1 -1
- package/dist/routing/pages-router.js +33 -18
- package/dist/routing/pages-router.js.map +1 -1
- package/dist/routing/route-validation.d.ts +8 -0
- package/dist/routing/route-validation.d.ts.map +1 -0
- package/dist/routing/route-validation.js +124 -0
- package/dist/routing/route-validation.js.map +1 -0
- package/dist/routing/utils.d.ts.map +1 -1
- package/dist/routing/utils.js.map +1 -1
- package/dist/server/api-handler.d.ts.map +1 -1
- package/dist/server/api-handler.js +31 -9
- package/dist/server/api-handler.js.map +1 -1
- package/dist/server/app-router-entry.d.ts +3 -2
- package/dist/server/app-router-entry.d.ts.map +1 -1
- package/dist/server/app-router-entry.js +8 -4
- package/dist/server/app-router-entry.js.map +1 -1
- package/dist/server/dev-module-runner.d.ts.map +1 -1
- package/dist/server/dev-module-runner.js +1 -1
- package/dist/server/dev-module-runner.js.map +1 -1
- package/dist/server/dev-origin-check.d.ts.map +1 -1
- package/dist/server/dev-origin-check.js.map +1 -1
- package/dist/server/dev-server.d.ts.map +1 -1
- package/dist/server/dev-server.js +39 -21
- package/dist/server/dev-server.js.map +1 -1
- package/dist/server/image-optimization.d.ts.map +1 -1
- package/dist/server/image-optimization.js.map +1 -1
- package/dist/server/instrumentation.js +1 -1
- package/dist/server/instrumentation.js.map +1 -1
- package/dist/server/isr-cache.d.ts +5 -1
- package/dist/server/isr-cache.d.ts.map +1 -1
- package/dist/server/isr-cache.js +13 -3
- package/dist/server/isr-cache.js.map +1 -1
- package/dist/server/metadata-routes.d.ts +8 -2
- package/dist/server/metadata-routes.d.ts.map +1 -1
- package/dist/server/metadata-routes.js +78 -45
- package/dist/server/metadata-routes.js.map +1 -1
- package/dist/server/middleware-codegen.d.ts +1 -1
- package/dist/server/middleware-codegen.d.ts.map +1 -1
- package/dist/server/middleware-codegen.js +177 -22
- package/dist/server/middleware-codegen.js.map +1 -1
- package/dist/server/middleware-request-headers.d.ts +9 -0
- package/dist/server/middleware-request-headers.d.ts.map +1 -0
- package/dist/server/middleware-request-headers.js +77 -0
- package/dist/server/middleware-request-headers.js.map +1 -0
- package/dist/server/middleware.d.ts +9 -8
- package/dist/server/middleware.d.ts.map +1 -1
- package/dist/server/middleware.js +112 -32
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/normalize-path.js.map +1 -1
- package/dist/server/prod-server.d.ts +1 -1
- package/dist/server/prod-server.d.ts.map +1 -1
- package/dist/server/prod-server.js +127 -82
- package/dist/server/prod-server.js.map +1 -1
- package/dist/server/request-pipeline.d.ts +2 -1
- package/dist/server/request-pipeline.d.ts.map +1 -1
- package/dist/server/request-pipeline.js +5 -7
- package/dist/server/request-pipeline.js.map +1 -1
- package/dist/shims/cache-runtime.d.ts.map +1 -1
- package/dist/shims/cache-runtime.js +21 -16
- package/dist/shims/cache-runtime.js.map +1 -1
- package/dist/shims/cache.d.ts +2 -0
- package/dist/shims/cache.d.ts.map +1 -1
- package/dist/shims/cache.js +38 -25
- package/dist/shims/cache.js.map +1 -1
- package/dist/shims/constants.d.ts.map +1 -1
- package/dist/shims/constants.js +1 -6
- package/dist/shims/constants.js.map +1 -1
- package/dist/shims/dynamic.d.ts.map +1 -1
- package/dist/shims/dynamic.js +1 -1
- package/dist/shims/dynamic.js.map +1 -1
- package/dist/shims/error-boundary.d.ts.map +1 -1
- package/dist/shims/error-boundary.js +2 -3
- package/dist/shims/error-boundary.js.map +1 -1
- package/dist/shims/error.d.ts.map +1 -1
- package/dist/shims/error.js +1 -3
- package/dist/shims/error.js.map +1 -1
- package/dist/shims/fetch-cache.d.ts.map +1 -1
- package/dist/shims/fetch-cache.js +57 -30
- package/dist/shims/fetch-cache.js.map +1 -1
- package/dist/shims/font-google-base.d.ts.map +1 -1
- package/dist/shims/font-google-base.js +16 -4
- package/dist/shims/font-google-base.js.map +1 -1
- package/dist/shims/font-google.d.ts +1 -1
- package/dist/shims/font-google.d.ts.map +1 -1
- package/dist/shims/font-google.generated.d.ts.map +1 -1
- package/dist/shims/font-google.generated.js +412 -206
- package/dist/shims/font-google.generated.js.map +1 -1
- package/dist/shims/font-google.js +1 -1
- package/dist/shims/font-google.js.map +1 -1
- package/dist/shims/font-local.d.ts.map +1 -1
- package/dist/shims/font-local.js +13 -3
- package/dist/shims/font-local.js.map +1 -1
- package/dist/shims/form.d.ts.map +1 -1
- package/dist/shims/form.js +105 -10
- package/dist/shims/form.js.map +1 -1
- package/dist/shims/head.d.ts.map +1 -1
- package/dist/shims/head.js +10 -8
- package/dist/shims/head.js.map +1 -1
- package/dist/shims/headers.d.ts +34 -8
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/headers.js +268 -53
- package/dist/shims/headers.js.map +1 -1
- package/dist/shims/image.d.ts.map +1 -1
- package/dist/shims/image.js +35 -8
- package/dist/shims/image.js.map +1 -1
- package/dist/shims/internal/parse-cookie-header.d.ts +12 -0
- package/dist/shims/internal/parse-cookie-header.d.ts.map +1 -0
- package/dist/shims/internal/parse-cookie-header.js +32 -0
- package/dist/shims/internal/parse-cookie-header.js.map +1 -0
- package/dist/shims/legacy-image.d.ts.map +1 -1
- package/dist/shims/legacy-image.js +1 -1
- package/dist/shims/legacy-image.js.map +1 -1
- package/dist/shims/link.d.ts +2 -1
- package/dist/shims/link.d.ts.map +1 -1
- package/dist/shims/link.js +37 -17
- package/dist/shims/link.js.map +1 -1
- package/dist/shims/metadata.d.ts +12 -2
- package/dist/shims/metadata.d.ts.map +1 -1
- package/dist/shims/metadata.js +10 -8
- package/dist/shims/metadata.js.map +1 -1
- package/dist/shims/navigation-state.d.ts.map +1 -1
- package/dist/shims/navigation-state.js +3 -2
- package/dist/shims/navigation-state.js.map +1 -1
- package/dist/shims/navigation.d.ts +3 -7
- package/dist/shims/navigation.d.ts.map +1 -1
- package/dist/shims/navigation.js +46 -29
- package/dist/shims/navigation.js.map +1 -1
- package/dist/shims/readonly-url-search-params.d.ts +11 -0
- package/dist/shims/readonly-url-search-params.d.ts.map +1 -0
- package/dist/shims/readonly-url-search-params.js +24 -0
- package/dist/shims/readonly-url-search-params.js.map +1 -0
- package/dist/shims/request-context.d.ts +50 -0
- package/dist/shims/request-context.d.ts.map +1 -0
- package/dist/shims/request-context.js +59 -0
- package/dist/shims/request-context.js.map +1 -0
- package/dist/shims/router-state.d.ts.map +1 -1
- package/dist/shims/router-state.js +2 -1
- package/dist/shims/router-state.js.map +1 -1
- package/dist/shims/router.d.ts +4 -3
- package/dist/shims/router.d.ts.map +1 -1
- package/dist/shims/router.js +59 -53
- package/dist/shims/router.js.map +1 -1
- package/dist/shims/script.d.ts.map +1 -1
- package/dist/shims/script.js.map +1 -1
- package/dist/shims/server.d.ts +14 -1
- package/dist/shims/server.d.ts.map +1 -1
- package/dist/shims/server.js +107 -47
- package/dist/shims/server.js.map +1 -1
- package/dist/shims/url-utils.d.ts.map +1 -1
- package/dist/shims/url-utils.js +1 -3
- package/dist/shims/url-utils.js.map +1 -1
- package/dist/utils/base-path.d.ts +17 -0
- package/dist/utils/base-path.d.ts.map +1 -0
- package/dist/utils/base-path.js +25 -0
- package/dist/utils/base-path.js.map +1 -0
- package/dist/utils/manifest-paths.d.ts +4 -0
- package/dist/utils/manifest-paths.d.ts.map +1 -0
- package/dist/utils/manifest-paths.js +20 -0
- package/dist/utils/manifest-paths.js.map +1 -0
- package/dist/utils/project.d.ts.map +1 -1
- package/dist/utils/project.js +2 -4
- package/dist/utils/project.js.map +1 -1
- package/dist/utils/query.d.ts +9 -0
- package/dist/utils/query.d.ts.map +1 -1
- package/dist/utils/query.js +59 -7
- package/dist/utils/query.js.map +1 -1
- package/package.json +47 -33
|
@@ -4,6 +4,136 @@
|
|
|
4
4
|
* Shared between the dev server (index.ts) and the production server
|
|
5
5
|
* (prod-server.ts) so both apply next.config.js rules identically.
|
|
6
6
|
*/
|
|
7
|
+
import { buildRequestHeadersFromMiddlewareResponse } from "../server/middleware-request-headers.js";
|
|
8
|
+
/**
|
|
9
|
+
* Cache for compiled regex patterns in matchConfigPattern.
|
|
10
|
+
*
|
|
11
|
+
* Redirect/rewrite patterns are static — they come from next.config.js and
|
|
12
|
+
* never change at runtime. Without caching, every request that hits the regex
|
|
13
|
+
* branch re-runs the full tokeniser walk + isSafeRegex + new RegExp() for
|
|
14
|
+
* every rule in the array. On apps with many locale-prefixed rules (which all
|
|
15
|
+
* contain `(` and therefore enter the regex branch) this dominated profiling
|
|
16
|
+
* at ~2.4 seconds of CPU self-time.
|
|
17
|
+
*
|
|
18
|
+
* Value is `null` when safeRegExp rejected the pattern (ReDoS risk), so we
|
|
19
|
+
* skip it on subsequent requests too without re-running the scanner.
|
|
20
|
+
*/
|
|
21
|
+
const _compiledPatternCache = new Map();
|
|
22
|
+
/**
|
|
23
|
+
* Cache for compiled header source regexes in matchHeaders.
|
|
24
|
+
*
|
|
25
|
+
* Each NextHeader rule has a `source` that is run through escapeHeaderSource()
|
|
26
|
+
* then safeRegExp() to produce a RegExp. Both are pure functions of the source
|
|
27
|
+
* string and the result never changes. Without caching, every request
|
|
28
|
+
* re-runs the full escapeHeaderSource tokeniser + isSafeRegex scan + new RegExp()
|
|
29
|
+
* for every header rule.
|
|
30
|
+
*
|
|
31
|
+
* Value is `null` when safeRegExp rejected the pattern (ReDoS risk).
|
|
32
|
+
*/
|
|
33
|
+
const _compiledHeaderSourceCache = new Map();
|
|
34
|
+
/**
|
|
35
|
+
* Cache for compiled has/missing condition value regexes in checkSingleCondition.
|
|
36
|
+
*
|
|
37
|
+
* Each has/missing condition may carry a `value` string that is passed directly
|
|
38
|
+
* to safeRegExp() for matching against header/cookie/query/host values. The
|
|
39
|
+
* condition objects are static (from next.config.js) so the compiled RegExp
|
|
40
|
+
* never changes. Without caching, safeRegExp() is called on every request for
|
|
41
|
+
* every condition on every rule.
|
|
42
|
+
*
|
|
43
|
+
* Value is `null` when safeRegExp rejected the pattern, or `false` when the
|
|
44
|
+
* value string was undefined (no regex needed — use exact string comparison).
|
|
45
|
+
*/
|
|
46
|
+
const _compiledConditionCache = new Map();
|
|
47
|
+
/**
|
|
48
|
+
* Cache for destination substitution regexes in substituteDestinationParams.
|
|
49
|
+
*
|
|
50
|
+
* The regex depends only on the set of param keys captured from the matched
|
|
51
|
+
* source pattern. Caching by sorted key list avoids recompiling a new RegExp
|
|
52
|
+
* for repeated redirect/rewrite calls that use the same param shape.
|
|
53
|
+
*/
|
|
54
|
+
const _compiledDestinationParamCache = new Map();
|
|
55
|
+
/**
|
|
56
|
+
* Redirect index for O(1) locale-static rule lookup.
|
|
57
|
+
*
|
|
58
|
+
* Many Next.js apps generate 50-100 redirect rules of the form:
|
|
59
|
+
* /:locale(en|es|fr|...)?/some-static-path → /some-destination
|
|
60
|
+
*
|
|
61
|
+
* The compiled regex for each is like:
|
|
62
|
+
* ^/(en|es|fr|...)?/some-static-path$
|
|
63
|
+
*
|
|
64
|
+
* When no redirect matches (the common case for ordinary page loads),
|
|
65
|
+
* matchRedirect previously ran exec() on every one of those regexes —
|
|
66
|
+
* ~2ms per call, ~2992ms total self-time in profiles.
|
|
67
|
+
*
|
|
68
|
+
* The index splits rules into two buckets:
|
|
69
|
+
*
|
|
70
|
+
* localeStatic — rules whose source is exactly /:paramName(alt1|alt2|...)?/suffix
|
|
71
|
+
* where `suffix` is a static path with no further params or regex groups.
|
|
72
|
+
* These are indexed in a Map<suffix, entry[]> for O(1) lookup after a
|
|
73
|
+
* single fast strip of the optional locale prefix.
|
|
74
|
+
*
|
|
75
|
+
* linear — all other rules. Matched with the original O(n) loop.
|
|
76
|
+
*
|
|
77
|
+
* The index is stored in a WeakMap keyed by the redirects array so it is
|
|
78
|
+
* computed once per config load and GC'd when the array is no longer live.
|
|
79
|
+
*
|
|
80
|
+
* ## Ordering invariant
|
|
81
|
+
*
|
|
82
|
+
* Redirect rules must be evaluated in their original order (first match wins).
|
|
83
|
+
* Each locale-static entry stores its `originalIndex` so that, when a
|
|
84
|
+
* locale-static fast-path match is found, any linear rules that appear earlier
|
|
85
|
+
* in the array are still checked first.
|
|
86
|
+
*/
|
|
87
|
+
/** Matches `/:param(alternation)?/static/suffix` — the locale-static pattern. */
|
|
88
|
+
const _LOCALE_STATIC_RE = /^\/:[\w-]+\(([^)]+)\)\?\/([a-zA-Z0-9_~.%@!$&'*+,;=:/-]+)$/;
|
|
89
|
+
const _redirectIndexCache = new WeakMap();
|
|
90
|
+
/**
|
|
91
|
+
* Build (or retrieve from cache) the redirect index for a given redirects array.
|
|
92
|
+
*
|
|
93
|
+
* Called once per config load from matchRedirect. The WeakMap ensures the index
|
|
94
|
+
* is recomputed if the config is reloaded (new array reference) and GC'd when
|
|
95
|
+
* the array is collected.
|
|
96
|
+
*/
|
|
97
|
+
function _getRedirectIndex(redirects) {
|
|
98
|
+
let index = _redirectIndexCache.get(redirects);
|
|
99
|
+
if (index !== undefined)
|
|
100
|
+
return index;
|
|
101
|
+
const localeStatic = new Map();
|
|
102
|
+
const linear = [];
|
|
103
|
+
for (let i = 0; i < redirects.length; i++) {
|
|
104
|
+
const redirect = redirects[i];
|
|
105
|
+
const m = _LOCALE_STATIC_RE.exec(redirect.source);
|
|
106
|
+
if (m) {
|
|
107
|
+
const paramName = redirect.source.slice(2, redirect.source.indexOf("("));
|
|
108
|
+
const alternation = m[1];
|
|
109
|
+
const suffix = "/" + m[2]; // e.g. "/security"
|
|
110
|
+
// Build a small regex to validate the captured locale value against the
|
|
111
|
+
// alternation. Using anchored match to avoid partial matches.
|
|
112
|
+
// The alternation comes from user config; run it through safeRegExp to
|
|
113
|
+
// guard against ReDoS in pathological configs.
|
|
114
|
+
const altRe = safeRegExp("^(?:" + alternation + ")$");
|
|
115
|
+
if (!altRe) {
|
|
116
|
+
// Unsafe alternation — fall back to linear scan for this rule.
|
|
117
|
+
linear.push([i, redirect]);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const entry = { paramName, altRe, redirect, originalIndex: i };
|
|
121
|
+
const bucket = localeStatic.get(suffix);
|
|
122
|
+
if (bucket) {
|
|
123
|
+
bucket.push(entry);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
localeStatic.set(suffix, [entry]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
linear.push([i, redirect]);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
index = { localeStatic, linear };
|
|
134
|
+
_redirectIndexCache.set(redirects, index);
|
|
135
|
+
return index;
|
|
136
|
+
}
|
|
7
137
|
/** Hop-by-hop headers that should not be forwarded through a proxy. */
|
|
8
138
|
const HOP_BY_HOP_HEADERS = new Set([
|
|
9
139
|
"connection",
|
|
@@ -238,9 +368,13 @@ export function requestContextFromRequest(request) {
|
|
|
238
368
|
headers: request.headers,
|
|
239
369
|
cookies: parseCookies(request.headers.get("cookie")),
|
|
240
370
|
query: url.searchParams,
|
|
241
|
-
host: request.headers.get("host")
|
|
371
|
+
host: normalizeHost(request.headers.get("host"), url.hostname),
|
|
242
372
|
};
|
|
243
373
|
}
|
|
374
|
+
export function normalizeHost(hostHeader, fallbackHostname) {
|
|
375
|
+
const host = hostHeader ?? fallbackHostname;
|
|
376
|
+
return host.split(":", 1)[0].toLowerCase();
|
|
377
|
+
}
|
|
244
378
|
/**
|
|
245
379
|
* Unpack `x-middleware-request-*` headers from the collected middleware
|
|
246
380
|
* response headers into the actual request, and strip all `x-middleware-*`
|
|
@@ -259,27 +393,17 @@ export function requestContextFromRequest(request) {
|
|
|
259
393
|
* `middlewareHeaders` is safe to cast here.
|
|
260
394
|
*/
|
|
261
395
|
export function applyMiddlewareRequestHeaders(middlewareHeaders, request) {
|
|
262
|
-
const
|
|
263
|
-
const toApply = {};
|
|
396
|
+
const nextHeaders = buildRequestHeadersFromMiddlewareResponse(request.headers, middlewareHeaders);
|
|
264
397
|
for (const key of Object.keys(middlewareHeaders)) {
|
|
265
|
-
if (key.startsWith(
|
|
266
|
-
const realName = key.slice(mwReqPrefix.length);
|
|
267
|
-
toApply[realName] = middlewareHeaders[key];
|
|
268
|
-
delete middlewareHeaders[key];
|
|
269
|
-
}
|
|
270
|
-
else if (key.startsWith("x-middleware-")) {
|
|
398
|
+
if (key.startsWith("x-middleware-")) {
|
|
271
399
|
delete middlewareHeaders[key];
|
|
272
400
|
}
|
|
273
401
|
}
|
|
274
|
-
if (
|
|
402
|
+
if (nextHeaders) {
|
|
275
403
|
// Headers may be immutable (Workers), so always clone via new Headers().
|
|
276
|
-
const newHeaders = new Headers(request.headers);
|
|
277
|
-
for (const [k, v] of Object.entries(toApply)) {
|
|
278
|
-
newHeaders.set(k, v);
|
|
279
|
-
}
|
|
280
404
|
request = new Request(request.url, {
|
|
281
405
|
method: request.method,
|
|
282
|
-
headers:
|
|
406
|
+
headers: nextHeaders,
|
|
283
407
|
body: request.body,
|
|
284
408
|
// @ts-expect-error — duplex needed for streaming request bodies
|
|
285
409
|
duplex: request.body ? "half" : undefined,
|
|
@@ -298,7 +422,7 @@ function checkSingleCondition(condition, ctx) {
|
|
|
298
422
|
if (headerValue === null)
|
|
299
423
|
return false;
|
|
300
424
|
if (condition.value !== undefined) {
|
|
301
|
-
const re =
|
|
425
|
+
const re = _cachedConditionRegex(condition.value);
|
|
302
426
|
if (re)
|
|
303
427
|
return re.test(headerValue);
|
|
304
428
|
return headerValue === condition.value;
|
|
@@ -310,7 +434,7 @@ function checkSingleCondition(condition, ctx) {
|
|
|
310
434
|
if (cookieValue === undefined)
|
|
311
435
|
return false;
|
|
312
436
|
if (condition.value !== undefined) {
|
|
313
|
-
const re =
|
|
437
|
+
const re = _cachedConditionRegex(condition.value);
|
|
314
438
|
if (re)
|
|
315
439
|
return re.test(cookieValue);
|
|
316
440
|
return cookieValue === condition.value;
|
|
@@ -322,7 +446,7 @@ function checkSingleCondition(condition, ctx) {
|
|
|
322
446
|
if (queryValue === null)
|
|
323
447
|
return false;
|
|
324
448
|
if (condition.value !== undefined) {
|
|
325
|
-
const re =
|
|
449
|
+
const re = _cachedConditionRegex(condition.value);
|
|
326
450
|
if (re)
|
|
327
451
|
return re.test(queryValue);
|
|
328
452
|
return queryValue === condition.value;
|
|
@@ -331,7 +455,7 @@ function checkSingleCondition(condition, ctx) {
|
|
|
331
455
|
}
|
|
332
456
|
case "host": {
|
|
333
457
|
if (condition.value !== undefined) {
|
|
334
|
-
const re =
|
|
458
|
+
const re = _cachedConditionRegex(condition.value);
|
|
335
459
|
if (re)
|
|
336
460
|
return re.test(ctx.host);
|
|
337
461
|
return ctx.host === condition.value;
|
|
@@ -342,6 +466,19 @@ function checkSingleCondition(condition, ctx) {
|
|
|
342
466
|
return false;
|
|
343
467
|
}
|
|
344
468
|
}
|
|
469
|
+
/**
|
|
470
|
+
* Return a cached RegExp for a has/missing condition value string, compiling
|
|
471
|
+
* on first use. Returns null if safeRegExp rejected the pattern or if the
|
|
472
|
+
* value is not a valid regex (fall back to exact string comparison).
|
|
473
|
+
*/
|
|
474
|
+
function _cachedConditionRegex(value) {
|
|
475
|
+
let re = _compiledConditionCache.get(value);
|
|
476
|
+
if (re === undefined) {
|
|
477
|
+
re = safeRegExp(value);
|
|
478
|
+
_compiledConditionCache.set(value, re);
|
|
479
|
+
}
|
|
480
|
+
return re;
|
|
481
|
+
}
|
|
345
482
|
/**
|
|
346
483
|
* Check all has/missing conditions for a config rule.
|
|
347
484
|
* Returns true if the rule should be applied (all has conditions pass, all missing conditions pass).
|
|
@@ -412,55 +549,65 @@ export function matchConfigPattern(pathname, pattern) {
|
|
|
412
549
|
/:[\w-]+[*+][^/]/.test(pattern) ||
|
|
413
550
|
/:[\w-]+\./.test(pattern)) {
|
|
414
551
|
try {
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
552
|
+
// Look up the compiled regex in the module-level cache. Patterns come
|
|
553
|
+
// from next.config.js and are static, so we only need to compile each
|
|
554
|
+
// one once across the lifetime of the worker/server process.
|
|
555
|
+
let compiled = _compiledPatternCache.get(pattern);
|
|
556
|
+
if (compiled === undefined) {
|
|
557
|
+
// Cache miss — compile the pattern now and store the result.
|
|
558
|
+
// Param names may contain hyphens (e.g. :auth-method, :sign-in).
|
|
559
|
+
const paramNames = [];
|
|
560
|
+
// Single-pass conversion with procedural suffix handling. The tokenizer
|
|
561
|
+
// matches only simple, non-overlapping tokens; quantifier/constraint
|
|
562
|
+
// suffixes after :param are consumed procedurally to avoid polynomial
|
|
563
|
+
// backtracking in the regex engine.
|
|
564
|
+
let regexStr = "";
|
|
565
|
+
const tokenRe = /:([\w-]+)|[.]|[^:.]+/g; // lgtm[js/redos] — alternatives are non-overlapping (`:` and `.` excluded from `[^:.]+`)
|
|
566
|
+
let tok;
|
|
567
|
+
while ((tok = tokenRe.exec(pattern)) !== null) {
|
|
568
|
+
if (tok[1] !== undefined) {
|
|
569
|
+
const name = tok[1];
|
|
570
|
+
const rest = pattern.slice(tokenRe.lastIndex);
|
|
571
|
+
// Check for quantifier (* or +) with optional constraint
|
|
572
|
+
if (rest.startsWith("*") || rest.startsWith("+")) {
|
|
573
|
+
const quantifier = rest[0];
|
|
574
|
+
tokenRe.lastIndex += 1;
|
|
575
|
+
const constraint = extractConstraint(pattern, tokenRe);
|
|
576
|
+
paramNames.push(name);
|
|
577
|
+
if (constraint !== null) {
|
|
578
|
+
regexStr += `(${constraint})`;
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
regexStr += quantifier === "*" ? "(.*)" : "(.+)";
|
|
582
|
+
}
|
|
436
583
|
}
|
|
437
584
|
else {
|
|
438
|
-
|
|
585
|
+
// Check for inline constraint without quantifier
|
|
586
|
+
const constraint = extractConstraint(pattern, tokenRe);
|
|
587
|
+
paramNames.push(name);
|
|
588
|
+
regexStr += constraint !== null ? `(${constraint})` : "([^/]+)";
|
|
439
589
|
}
|
|
440
590
|
}
|
|
591
|
+
else if (tok[0] === ".") {
|
|
592
|
+
regexStr += "\\.";
|
|
593
|
+
}
|
|
441
594
|
else {
|
|
442
|
-
|
|
443
|
-
const constraint = extractConstraint(pattern, tokenRe);
|
|
444
|
-
paramNames.push(name);
|
|
445
|
-
regexStr += constraint !== null ? `(${constraint})` : "([^/]+)";
|
|
595
|
+
regexStr += tok[0];
|
|
446
596
|
}
|
|
447
597
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
regexStr += tok[0];
|
|
453
|
-
}
|
|
598
|
+
const re = safeRegExp("^" + regexStr + "$");
|
|
599
|
+
// Store null for rejected patterns so we don't re-run isSafeRegex.
|
|
600
|
+
compiled = re ? { re, paramNames } : null;
|
|
601
|
+
_compiledPatternCache.set(pattern, compiled);
|
|
454
602
|
}
|
|
455
|
-
|
|
456
|
-
if (!re)
|
|
603
|
+
if (!compiled)
|
|
457
604
|
return null;
|
|
458
|
-
const match = re.exec(pathname);
|
|
605
|
+
const match = compiled.re.exec(pathname);
|
|
459
606
|
if (!match)
|
|
460
607
|
return null;
|
|
461
608
|
const params = Object.create(null);
|
|
462
|
-
for (let i = 0; i < paramNames.length; i++) {
|
|
463
|
-
params[paramNames[i]] = match[i + 1] ?? "";
|
|
609
|
+
for (let i = 0; i < compiled.paramNames.length; i++) {
|
|
610
|
+
params[compiled.paramNames[i]] = match[i + 1] ?? "";
|
|
464
611
|
}
|
|
465
612
|
return params;
|
|
466
613
|
}
|
|
@@ -512,9 +659,115 @@ export function matchConfigPattern(pathname, pattern) {
|
|
|
512
659
|
* `ctx` provides the request context (cookies, headers, query, host) used
|
|
513
660
|
* to evaluate has/missing conditions. Next.js always has request context
|
|
514
661
|
* when evaluating redirects, so this parameter is required.
|
|
662
|
+
*
|
|
663
|
+
* ## Performance
|
|
664
|
+
*
|
|
665
|
+
* Rules with a locale-capture-group prefix (the dominant pattern in large
|
|
666
|
+
* Next.js apps — e.g. `/:locale(en|es|fr|...)?/some-path`) are handled via
|
|
667
|
+
* a pre-built index. Instead of running exec() on each locale regex
|
|
668
|
+
* individually, we:
|
|
669
|
+
*
|
|
670
|
+
* 1. Strip the optional locale prefix from the pathname with one cheap
|
|
671
|
+
* string-slice check (no regex exec on the hot path).
|
|
672
|
+
* 2. Look up the stripped suffix in a Map<suffix, entry[]>.
|
|
673
|
+
* 3. For each matching entry, validate the captured locale string against
|
|
674
|
+
* a small, anchored alternation regex.
|
|
675
|
+
*
|
|
676
|
+
* This reduces the per-request cost from O(n × regex) to O(1) map lookup +
|
|
677
|
+
* O(matches × tiny-regex), eliminating the ~2992ms self-time reported in
|
|
678
|
+
* profiles for apps with 63+ locale-prefixed rules.
|
|
679
|
+
*
|
|
680
|
+
* Rules that don't fit the locale-static pattern fall back to the original
|
|
681
|
+
* linear matchConfigPattern scan.
|
|
682
|
+
*
|
|
683
|
+
* ## Ordering invariant
|
|
684
|
+
*
|
|
685
|
+
* First match wins, preserving the original redirect array order. When a
|
|
686
|
+
* locale-static fast-path match is found at position N, all linear rules with
|
|
687
|
+
* an original index < N are checked via matchConfigPattern first — they are
|
|
688
|
+
* few in practice (typically zero) so this is not a hot-path concern.
|
|
515
689
|
*/
|
|
516
690
|
export function matchRedirect(pathname, redirects, ctx) {
|
|
517
|
-
|
|
691
|
+
if (redirects.length === 0)
|
|
692
|
+
return null;
|
|
693
|
+
const index = _getRedirectIndex(redirects);
|
|
694
|
+
// --- Locate the best locale-static candidate ---
|
|
695
|
+
//
|
|
696
|
+
// We look for the locale-static entry with the LOWEST originalIndex that
|
|
697
|
+
// matches this pathname (and passes has/missing conditions).
|
|
698
|
+
//
|
|
699
|
+
// Strategy: try both the full pathname (locale omitted, e.g. "/security")
|
|
700
|
+
// and the pathname with the first segment stripped (locale present, e.g.
|
|
701
|
+
// "/en/security" → suffix "/security", locale "en").
|
|
702
|
+
//
|
|
703
|
+
// We do NOT use a regex here — just a single indexOf('/') to locate the
|
|
704
|
+
// second slash, which is O(n) on the path length but far cheaper than
|
|
705
|
+
// running 63 compiled regexes.
|
|
706
|
+
let localeMatch = null;
|
|
707
|
+
let localeMatchIndex = Infinity;
|
|
708
|
+
if (index.localeStatic.size > 0) {
|
|
709
|
+
// Case 1: no locale prefix — pathname IS the suffix.
|
|
710
|
+
const noLocaleBucket = index.localeStatic.get(pathname);
|
|
711
|
+
if (noLocaleBucket) {
|
|
712
|
+
for (const entry of noLocaleBucket) {
|
|
713
|
+
if (entry.originalIndex >= localeMatchIndex)
|
|
714
|
+
continue; // already have a better match
|
|
715
|
+
const redirect = entry.redirect;
|
|
716
|
+
if (redirect.has || redirect.missing) {
|
|
717
|
+
if (!checkHasConditions(redirect.has, redirect.missing, ctx))
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
// Locale was omitted (the `?` made it optional) — param value is "".
|
|
721
|
+
let dest = substituteDestinationParams(redirect.destination, {
|
|
722
|
+
[entry.paramName]: "",
|
|
723
|
+
});
|
|
724
|
+
dest = sanitizeDestination(dest);
|
|
725
|
+
localeMatch = { destination: dest, permanent: redirect.permanent };
|
|
726
|
+
localeMatchIndex = entry.originalIndex;
|
|
727
|
+
break; // bucket entries are in insertion order = original order
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
// Case 2: locale prefix present — first path segment is the locale.
|
|
731
|
+
// Find the second slash: pathname = "/locale/rest/of/path"
|
|
732
|
+
// ^--- slashTwo
|
|
733
|
+
const slashTwo = pathname.indexOf("/", 1);
|
|
734
|
+
if (slashTwo !== -1) {
|
|
735
|
+
const suffix = pathname.slice(slashTwo); // e.g. "/security"
|
|
736
|
+
const localePart = pathname.slice(1, slashTwo); // e.g. "en"
|
|
737
|
+
const localeBucket = index.localeStatic.get(suffix);
|
|
738
|
+
if (localeBucket) {
|
|
739
|
+
for (const entry of localeBucket) {
|
|
740
|
+
if (entry.originalIndex >= localeMatchIndex)
|
|
741
|
+
continue;
|
|
742
|
+
// Validate that `localePart` is one of the allowed alternation values.
|
|
743
|
+
if (!entry.altRe.test(localePart))
|
|
744
|
+
continue;
|
|
745
|
+
const redirect = entry.redirect;
|
|
746
|
+
if (redirect.has || redirect.missing) {
|
|
747
|
+
if (!checkHasConditions(redirect.has, redirect.missing, ctx))
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
let dest = substituteDestinationParams(redirect.destination, {
|
|
751
|
+
[entry.paramName]: localePart,
|
|
752
|
+
});
|
|
753
|
+
dest = sanitizeDestination(dest);
|
|
754
|
+
localeMatch = { destination: dest, permanent: redirect.permanent };
|
|
755
|
+
localeMatchIndex = entry.originalIndex;
|
|
756
|
+
break; // bucket entries are in insertion order = original order
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// --- Linear fallback: all non-locale-static rules ---
|
|
762
|
+
//
|
|
763
|
+
// We only need to check linear rules whose originalIndex < localeMatchIndex.
|
|
764
|
+
// If localeMatchIndex is Infinity (no locale match), we check all of them.
|
|
765
|
+
for (const [origIdx, redirect] of index.linear) {
|
|
766
|
+
if (origIdx >= localeMatchIndex) {
|
|
767
|
+
// This linear rule comes after the best locale-static match —
|
|
768
|
+
// the locale-static match wins. Stop scanning.
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
518
771
|
const params = matchConfigPattern(pathname, redirect.source);
|
|
519
772
|
if (params) {
|
|
520
773
|
if (redirect.has || redirect.missing) {
|
|
@@ -522,20 +775,14 @@ export function matchRedirect(pathname, redirects, ctx) {
|
|
|
522
775
|
continue;
|
|
523
776
|
}
|
|
524
777
|
}
|
|
525
|
-
let dest = redirect.destination;
|
|
526
|
-
for (const [key, value] of Object.entries(params)) {
|
|
527
|
-
// Replace :param*, :param+, and :param forms in the destination.
|
|
528
|
-
// The catch-all suffixes (* and +) must be stripped along with the param name.
|
|
529
|
-
dest = dest.replace(`:${key}*`, value);
|
|
530
|
-
dest = dest.replace(`:${key}+`, value);
|
|
531
|
-
dest = dest.replace(`:${key}`, value);
|
|
532
|
-
}
|
|
778
|
+
let dest = substituteDestinationParams(redirect.destination, params);
|
|
533
779
|
// Collapse protocol-relative URLs (e.g. //evil.com from decoded %2F in catch-all params).
|
|
534
780
|
dest = sanitizeDestination(dest);
|
|
535
781
|
return { destination: dest, permanent: redirect.permanent };
|
|
536
782
|
}
|
|
537
783
|
}
|
|
538
|
-
|
|
784
|
+
// Return the locale-static match if found (no earlier linear rule matched).
|
|
785
|
+
return localeMatch;
|
|
539
786
|
}
|
|
540
787
|
/**
|
|
541
788
|
* Apply rewrite rules from next.config.js.
|
|
@@ -554,14 +801,7 @@ export function matchRewrite(pathname, rewrites, ctx) {
|
|
|
554
801
|
continue;
|
|
555
802
|
}
|
|
556
803
|
}
|
|
557
|
-
let dest = rewrite.destination;
|
|
558
|
-
for (const [key, value] of Object.entries(params)) {
|
|
559
|
-
// Replace :param*, :param+, and :param forms in the destination.
|
|
560
|
-
// The catch-all suffixes (* and +) must be stripped along with the param name.
|
|
561
|
-
dest = dest.replace(`:${key}*`, value);
|
|
562
|
-
dest = dest.replace(`:${key}+`, value);
|
|
563
|
-
dest = dest.replace(`:${key}`, value);
|
|
564
|
-
}
|
|
804
|
+
let dest = substituteDestinationParams(rewrite.destination, params);
|
|
565
805
|
// Collapse protocol-relative URLs (e.g. //evil.com from decoded %2F in catch-all params).
|
|
566
806
|
dest = sanitizeDestination(dest);
|
|
567
807
|
return dest;
|
|
@@ -569,6 +809,33 @@ export function matchRewrite(pathname, rewrites, ctx) {
|
|
|
569
809
|
}
|
|
570
810
|
return null;
|
|
571
811
|
}
|
|
812
|
+
/**
|
|
813
|
+
* Substitute all matched route params into a redirect/rewrite destination.
|
|
814
|
+
*
|
|
815
|
+
* Handles repeated params (e.g. `/api/:id/:id`) and catch-all suffix forms
|
|
816
|
+
* (`:path*`, `:path+`) in a single pass. Unknown params are left intact.
|
|
817
|
+
*/
|
|
818
|
+
function substituteDestinationParams(destination, params) {
|
|
819
|
+
const keys = Object.keys(params);
|
|
820
|
+
if (keys.length === 0)
|
|
821
|
+
return destination;
|
|
822
|
+
// Match only the concrete param keys captured from the source pattern.
|
|
823
|
+
// Sorting longest-first ensures hyphenated names like `auth-method`
|
|
824
|
+
// win over shorter prefixes like `auth`. The negative lookahead keeps
|
|
825
|
+
// alphanumeric/underscore suffixes attached, while allowing `-` to act
|
|
826
|
+
// as a literal delimiter in destinations like `:year-:month`.
|
|
827
|
+
const sortedKeys = [...keys].sort((a, b) => b.length - a.length);
|
|
828
|
+
const cacheKey = sortedKeys.join("\0");
|
|
829
|
+
let paramRe = _compiledDestinationParamCache.get(cacheKey);
|
|
830
|
+
if (!paramRe) {
|
|
831
|
+
const paramAlternation = sortedKeys
|
|
832
|
+
.map((key) => key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
833
|
+
.join("|");
|
|
834
|
+
paramRe = new RegExp(`:(${paramAlternation})([+*])?(?![A-Za-z0-9_])`, "g");
|
|
835
|
+
_compiledDestinationParamCache.set(cacheKey, paramRe);
|
|
836
|
+
}
|
|
837
|
+
return destination.replace(paramRe, (_token, key) => params[key]);
|
|
838
|
+
}
|
|
572
839
|
/**
|
|
573
840
|
* Sanitize a redirect/rewrite destination to collapse protocol-relative URLs.
|
|
574
841
|
*
|
|
@@ -613,12 +880,13 @@ export async function proxyExternalRequest(request, externalUrl) {
|
|
|
613
880
|
// Build the full external URL, preserving query parameters from the original request
|
|
614
881
|
const originalUrl = new URL(request.url);
|
|
615
882
|
const targetUrl = new URL(externalUrl);
|
|
883
|
+
const destinationKeys = new Set(targetUrl.searchParams.keys());
|
|
616
884
|
// If the rewrite destination already has query params, merge them.
|
|
617
885
|
// Destination params take precedence — original request params are only added
|
|
618
886
|
// when the destination doesn't already specify that key.
|
|
619
887
|
for (const [key, value] of originalUrl.searchParams) {
|
|
620
|
-
if (!
|
|
621
|
-
targetUrl.searchParams.
|
|
888
|
+
if (!destinationKeys.has(key)) {
|
|
889
|
+
targetUrl.searchParams.append(key, value);
|
|
622
890
|
}
|
|
623
891
|
}
|
|
624
892
|
// Forward the request with appropriate headers
|
|
@@ -674,7 +942,7 @@ export async function proxyExternalRequest(request, externalUrl) {
|
|
|
674
942
|
// decompression on the already-decoded body, resulting in
|
|
675
943
|
// ERR_CONTENT_DECODING_FAILED. Strip both headers on Node.js only.
|
|
676
944
|
// On Workers, fetch() preserves wire encoding, so the headers stay accurate.
|
|
677
|
-
const isNodeRuntime = typeof process !== "undefined" && !!
|
|
945
|
+
const isNodeRuntime = typeof process !== "undefined" && !!process.versions?.node;
|
|
678
946
|
const responseHeaders = new Headers();
|
|
679
947
|
upstreamResponse.headers.forEach((value, key) => {
|
|
680
948
|
const lower = key.toLowerCase();
|
|
@@ -701,8 +969,14 @@ export async function proxyExternalRequest(request, externalUrl) {
|
|
|
701
969
|
export function matchHeaders(pathname, headers, ctx) {
|
|
702
970
|
const result = [];
|
|
703
971
|
for (const rule of headers) {
|
|
704
|
-
|
|
705
|
-
|
|
972
|
+
// Cache the compiled source regex — escapeHeaderSource() + safeRegExp() are
|
|
973
|
+
// pure functions of rule.source and the result never changes between requests.
|
|
974
|
+
let sourceRegex = _compiledHeaderSourceCache.get(rule.source);
|
|
975
|
+
if (sourceRegex === undefined) {
|
|
976
|
+
const escaped = escapeHeaderSource(rule.source);
|
|
977
|
+
sourceRegex = safeRegExp("^" + escaped + "$");
|
|
978
|
+
_compiledHeaderSourceCache.set(rule.source, sourceRegex);
|
|
979
|
+
}
|
|
706
980
|
if (sourceRegex && sourceRegex.test(pathname)) {
|
|
707
981
|
if (rule.has || rule.missing) {
|
|
708
982
|
if (!checkHasConditions(rule.has, rule.missing, ctx)) {
|