vinext 0.0.26 → 0.0.27
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.map +1 -1
- package/dist/build/static-export.js +3 -8
- 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 +32 -1
- package/dist/cloudflare/kv-cache-handler.d.ts.map +1 -1
- package/dist/cloudflare/kv-cache-handler.js +47 -21
- 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 +27 -0
- package/dist/config/config-matchers.d.ts.map +1 -1
- package/dist/config/config-matchers.js +306 -60
- 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.map +1 -1
- package/dist/deploy.js +36 -19
- package/dist/deploy.js.map +1 -1
- package/dist/entries/app-rsc-entry.d.ts.map +1 -1
- package/dist/entries/app-rsc-entry.js +89 -38
- 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 +32 -10
- package/dist/entries/pages-server-entry.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +204 -118
- 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/routing/app-router.d.ts +2 -0
- package/dist/routing/app-router.d.ts.map +1 -1
- package/dist/routing/app-router.js +10 -18
- 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 +2 -0
- package/dist/routing/pages-router.d.ts.map +1 -1
- package/dist/routing/pages-router.js +8 -5
- package/dist/routing/pages-router.js.map +1 -1
- 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 +7 -2
- 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 +30 -18
- 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 +13 -1
- package/dist/server/isr-cache.d.ts.map +1 -1
- package/dist/server/isr-cache.js +10 -1
- package/dist/server/isr-cache.js.map +1 -1
- package/dist/server/metadata-routes.d.ts.map +1 -1
- package/dist/server/metadata-routes.js +6 -18
- package/dist/server/metadata-routes.js.map +1 -1
- package/dist/server/middleware-codegen.d.ts.map +1 -1
- package/dist/server/middleware-codegen.js +12 -10
- 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.map +1 -1
- package/dist/server/middleware.js +38 -19
- 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 +53 -38
- 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.map +1 -1
- package/dist/shims/cache.js +18 -17
- 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 +53 -29
- 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 +2 -2
- 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 +23 -5
- package/dist/shims/headers.d.ts.map +1 -1
- package/dist/shims/headers.js +97 -37
- 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/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.map +1 -1
- package/dist/shims/link.js +29 -15
- 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.map +1 -1
- package/dist/shims/navigation.js +26 -19
- package/dist/shims/navigation.js.map +1 -1
- 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.map +1 -1
- package/dist/shims/router.js +18 -25
- 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 +13 -0
- package/dist/shims/server.d.ts.map +1 -1
- package/dist/shims/server.js +100 -34
- 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/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.map +1 -1
- package/dist/utils/query.js +3 -1
- package/dist/utils/query.js.map +1 -1
- package/package.json +47 -33
|
@@ -4,6 +4,128 @@
|
|
|
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
|
+
* Redirect index for O(1) locale-static rule lookup.
|
|
49
|
+
*
|
|
50
|
+
* Many Next.js apps generate 50-100 redirect rules of the form:
|
|
51
|
+
* /:locale(en|es|fr|...)?/some-static-path → /some-destination
|
|
52
|
+
*
|
|
53
|
+
* The compiled regex for each is like:
|
|
54
|
+
* ^/(en|es|fr|...)?/some-static-path$
|
|
55
|
+
*
|
|
56
|
+
* When no redirect matches (the common case for ordinary page loads),
|
|
57
|
+
* matchRedirect previously ran exec() on every one of those regexes —
|
|
58
|
+
* ~2ms per call, ~2992ms total self-time in profiles.
|
|
59
|
+
*
|
|
60
|
+
* The index splits rules into two buckets:
|
|
61
|
+
*
|
|
62
|
+
* localeStatic — rules whose source is exactly /:paramName(alt1|alt2|...)?/suffix
|
|
63
|
+
* where `suffix` is a static path with no further params or regex groups.
|
|
64
|
+
* These are indexed in a Map<suffix, entry[]> for O(1) lookup after a
|
|
65
|
+
* single fast strip of the optional locale prefix.
|
|
66
|
+
*
|
|
67
|
+
* linear — all other rules. Matched with the original O(n) loop.
|
|
68
|
+
*
|
|
69
|
+
* The index is stored in a WeakMap keyed by the redirects array so it is
|
|
70
|
+
* computed once per config load and GC'd when the array is no longer live.
|
|
71
|
+
*
|
|
72
|
+
* ## Ordering invariant
|
|
73
|
+
*
|
|
74
|
+
* Redirect rules must be evaluated in their original order (first match wins).
|
|
75
|
+
* Each locale-static entry stores its `originalIndex` so that, when a
|
|
76
|
+
* locale-static fast-path match is found, any linear rules that appear earlier
|
|
77
|
+
* in the array are still checked first.
|
|
78
|
+
*/
|
|
79
|
+
/** Matches `/:param(alternation)?/static/suffix` — the locale-static pattern. */
|
|
80
|
+
const _LOCALE_STATIC_RE = /^\/:[\w-]+\(([^)]+)\)\?\/([a-zA-Z0-9_~.%@!$&'*+,;=:/-]+)$/;
|
|
81
|
+
const _redirectIndexCache = new WeakMap();
|
|
82
|
+
/**
|
|
83
|
+
* Build (or retrieve from cache) the redirect index for a given redirects array.
|
|
84
|
+
*
|
|
85
|
+
* Called once per config load from matchRedirect. The WeakMap ensures the index
|
|
86
|
+
* is recomputed if the config is reloaded (new array reference) and GC'd when
|
|
87
|
+
* the array is collected.
|
|
88
|
+
*/
|
|
89
|
+
function _getRedirectIndex(redirects) {
|
|
90
|
+
let index = _redirectIndexCache.get(redirects);
|
|
91
|
+
if (index !== undefined)
|
|
92
|
+
return index;
|
|
93
|
+
const localeStatic = new Map();
|
|
94
|
+
const linear = [];
|
|
95
|
+
for (let i = 0; i < redirects.length; i++) {
|
|
96
|
+
const redirect = redirects[i];
|
|
97
|
+
const m = _LOCALE_STATIC_RE.exec(redirect.source);
|
|
98
|
+
if (m) {
|
|
99
|
+
const paramName = redirect.source.slice(2, redirect.source.indexOf("("));
|
|
100
|
+
const alternation = m[1];
|
|
101
|
+
const suffix = "/" + m[2]; // e.g. "/security"
|
|
102
|
+
// Build a small regex to validate the captured locale value against the
|
|
103
|
+
// alternation. Using anchored match to avoid partial matches.
|
|
104
|
+
// The alternation comes from user config; run it through safeRegExp to
|
|
105
|
+
// guard against ReDoS in pathological configs.
|
|
106
|
+
const altRe = safeRegExp("^(?:" + alternation + ")$");
|
|
107
|
+
if (!altRe) {
|
|
108
|
+
// Unsafe alternation — fall back to linear scan for this rule.
|
|
109
|
+
linear.push([i, redirect]);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const entry = { paramName, altRe, redirect, originalIndex: i };
|
|
113
|
+
const bucket = localeStatic.get(suffix);
|
|
114
|
+
if (bucket) {
|
|
115
|
+
bucket.push(entry);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
localeStatic.set(suffix, [entry]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
linear.push([i, redirect]);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
index = { localeStatic, linear };
|
|
126
|
+
_redirectIndexCache.set(redirects, index);
|
|
127
|
+
return index;
|
|
128
|
+
}
|
|
7
129
|
/** Hop-by-hop headers that should not be forwarded through a proxy. */
|
|
8
130
|
const HOP_BY_HOP_HEADERS = new Set([
|
|
9
131
|
"connection",
|
|
@@ -259,27 +381,17 @@ export function requestContextFromRequest(request) {
|
|
|
259
381
|
* `middlewareHeaders` is safe to cast here.
|
|
260
382
|
*/
|
|
261
383
|
export function applyMiddlewareRequestHeaders(middlewareHeaders, request) {
|
|
262
|
-
const
|
|
263
|
-
const toApply = {};
|
|
384
|
+
const nextHeaders = buildRequestHeadersFromMiddlewareResponse(request.headers, middlewareHeaders);
|
|
264
385
|
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-")) {
|
|
386
|
+
if (key.startsWith("x-middleware-")) {
|
|
271
387
|
delete middlewareHeaders[key];
|
|
272
388
|
}
|
|
273
389
|
}
|
|
274
|
-
if (
|
|
390
|
+
if (nextHeaders) {
|
|
275
391
|
// 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
392
|
request = new Request(request.url, {
|
|
281
393
|
method: request.method,
|
|
282
|
-
headers:
|
|
394
|
+
headers: nextHeaders,
|
|
283
395
|
body: request.body,
|
|
284
396
|
// @ts-expect-error — duplex needed for streaming request bodies
|
|
285
397
|
duplex: request.body ? "half" : undefined,
|
|
@@ -298,7 +410,7 @@ function checkSingleCondition(condition, ctx) {
|
|
|
298
410
|
if (headerValue === null)
|
|
299
411
|
return false;
|
|
300
412
|
if (condition.value !== undefined) {
|
|
301
|
-
const re =
|
|
413
|
+
const re = _cachedConditionRegex(condition.value);
|
|
302
414
|
if (re)
|
|
303
415
|
return re.test(headerValue);
|
|
304
416
|
return headerValue === condition.value;
|
|
@@ -310,7 +422,7 @@ function checkSingleCondition(condition, ctx) {
|
|
|
310
422
|
if (cookieValue === undefined)
|
|
311
423
|
return false;
|
|
312
424
|
if (condition.value !== undefined) {
|
|
313
|
-
const re =
|
|
425
|
+
const re = _cachedConditionRegex(condition.value);
|
|
314
426
|
if (re)
|
|
315
427
|
return re.test(cookieValue);
|
|
316
428
|
return cookieValue === condition.value;
|
|
@@ -322,7 +434,7 @@ function checkSingleCondition(condition, ctx) {
|
|
|
322
434
|
if (queryValue === null)
|
|
323
435
|
return false;
|
|
324
436
|
if (condition.value !== undefined) {
|
|
325
|
-
const re =
|
|
437
|
+
const re = _cachedConditionRegex(condition.value);
|
|
326
438
|
if (re)
|
|
327
439
|
return re.test(queryValue);
|
|
328
440
|
return queryValue === condition.value;
|
|
@@ -331,7 +443,7 @@ function checkSingleCondition(condition, ctx) {
|
|
|
331
443
|
}
|
|
332
444
|
case "host": {
|
|
333
445
|
if (condition.value !== undefined) {
|
|
334
|
-
const re =
|
|
446
|
+
const re = _cachedConditionRegex(condition.value);
|
|
335
447
|
if (re)
|
|
336
448
|
return re.test(ctx.host);
|
|
337
449
|
return ctx.host === condition.value;
|
|
@@ -342,6 +454,19 @@ function checkSingleCondition(condition, ctx) {
|
|
|
342
454
|
return false;
|
|
343
455
|
}
|
|
344
456
|
}
|
|
457
|
+
/**
|
|
458
|
+
* Return a cached RegExp for a has/missing condition value string, compiling
|
|
459
|
+
* on first use. Returns null if safeRegExp rejected the pattern or if the
|
|
460
|
+
* value is not a valid regex (fall back to exact string comparison).
|
|
461
|
+
*/
|
|
462
|
+
function _cachedConditionRegex(value) {
|
|
463
|
+
let re = _compiledConditionCache.get(value);
|
|
464
|
+
if (re === undefined) {
|
|
465
|
+
re = safeRegExp(value);
|
|
466
|
+
_compiledConditionCache.set(value, re);
|
|
467
|
+
}
|
|
468
|
+
return re;
|
|
469
|
+
}
|
|
345
470
|
/**
|
|
346
471
|
* Check all has/missing conditions for a config rule.
|
|
347
472
|
* Returns true if the rule should be applied (all has conditions pass, all missing conditions pass).
|
|
@@ -412,55 +537,65 @@ export function matchConfigPattern(pathname, pattern) {
|
|
|
412
537
|
/:[\w-]+[*+][^/]/.test(pattern) ||
|
|
413
538
|
/:[\w-]+\./.test(pattern)) {
|
|
414
539
|
try {
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
540
|
+
// Look up the compiled regex in the module-level cache. Patterns come
|
|
541
|
+
// from next.config.js and are static, so we only need to compile each
|
|
542
|
+
// one once across the lifetime of the worker/server process.
|
|
543
|
+
let compiled = _compiledPatternCache.get(pattern);
|
|
544
|
+
if (compiled === undefined) {
|
|
545
|
+
// Cache miss — compile the pattern now and store the result.
|
|
546
|
+
// Param names may contain hyphens (e.g. :auth-method, :sign-in).
|
|
547
|
+
const paramNames = [];
|
|
548
|
+
// Single-pass conversion with procedural suffix handling. The tokenizer
|
|
549
|
+
// matches only simple, non-overlapping tokens; quantifier/constraint
|
|
550
|
+
// suffixes after :param are consumed procedurally to avoid polynomial
|
|
551
|
+
// backtracking in the regex engine.
|
|
552
|
+
let regexStr = "";
|
|
553
|
+
const tokenRe = /:([\w-]+)|[.]|[^:.]+/g; // lgtm[js/redos] — alternatives are non-overlapping (`:` and `.` excluded from `[^:.]+`)
|
|
554
|
+
let tok;
|
|
555
|
+
while ((tok = tokenRe.exec(pattern)) !== null) {
|
|
556
|
+
if (tok[1] !== undefined) {
|
|
557
|
+
const name = tok[1];
|
|
558
|
+
const rest = pattern.slice(tokenRe.lastIndex);
|
|
559
|
+
// Check for quantifier (* or +) with optional constraint
|
|
560
|
+
if (rest.startsWith("*") || rest.startsWith("+")) {
|
|
561
|
+
const quantifier = rest[0];
|
|
562
|
+
tokenRe.lastIndex += 1;
|
|
563
|
+
const constraint = extractConstraint(pattern, tokenRe);
|
|
564
|
+
paramNames.push(name);
|
|
565
|
+
if (constraint !== null) {
|
|
566
|
+
regexStr += `(${constraint})`;
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
regexStr += quantifier === "*" ? "(.*)" : "(.+)";
|
|
570
|
+
}
|
|
436
571
|
}
|
|
437
572
|
else {
|
|
438
|
-
|
|
573
|
+
// Check for inline constraint without quantifier
|
|
574
|
+
const constraint = extractConstraint(pattern, tokenRe);
|
|
575
|
+
paramNames.push(name);
|
|
576
|
+
regexStr += constraint !== null ? `(${constraint})` : "([^/]+)";
|
|
439
577
|
}
|
|
440
578
|
}
|
|
579
|
+
else if (tok[0] === ".") {
|
|
580
|
+
regexStr += "\\.";
|
|
581
|
+
}
|
|
441
582
|
else {
|
|
442
|
-
|
|
443
|
-
const constraint = extractConstraint(pattern, tokenRe);
|
|
444
|
-
paramNames.push(name);
|
|
445
|
-
regexStr += constraint !== null ? `(${constraint})` : "([^/]+)";
|
|
583
|
+
regexStr += tok[0];
|
|
446
584
|
}
|
|
447
585
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
regexStr += tok[0];
|
|
453
|
-
}
|
|
586
|
+
const re = safeRegExp("^" + regexStr + "$");
|
|
587
|
+
// Store null for rejected patterns so we don't re-run isSafeRegex.
|
|
588
|
+
compiled = re ? { re, paramNames } : null;
|
|
589
|
+
_compiledPatternCache.set(pattern, compiled);
|
|
454
590
|
}
|
|
455
|
-
|
|
456
|
-
if (!re)
|
|
591
|
+
if (!compiled)
|
|
457
592
|
return null;
|
|
458
|
-
const match = re.exec(pathname);
|
|
593
|
+
const match = compiled.re.exec(pathname);
|
|
459
594
|
if (!match)
|
|
460
595
|
return null;
|
|
461
596
|
const params = Object.create(null);
|
|
462
|
-
for (let i = 0; i < paramNames.length; i++) {
|
|
463
|
-
params[paramNames[i]] = match[i + 1] ?? "";
|
|
597
|
+
for (let i = 0; i < compiled.paramNames.length; i++) {
|
|
598
|
+
params[compiled.paramNames[i]] = match[i + 1] ?? "";
|
|
464
599
|
}
|
|
465
600
|
return params;
|
|
466
601
|
}
|
|
@@ -512,9 +647,113 @@ export function matchConfigPattern(pathname, pattern) {
|
|
|
512
647
|
* `ctx` provides the request context (cookies, headers, query, host) used
|
|
513
648
|
* to evaluate has/missing conditions. Next.js always has request context
|
|
514
649
|
* when evaluating redirects, so this parameter is required.
|
|
650
|
+
*
|
|
651
|
+
* ## Performance
|
|
652
|
+
*
|
|
653
|
+
* Rules with a locale-capture-group prefix (the dominant pattern in large
|
|
654
|
+
* Next.js apps — e.g. `/:locale(en|es|fr|...)?/some-path`) are handled via
|
|
655
|
+
* a pre-built index. Instead of running exec() on each locale regex
|
|
656
|
+
* individually, we:
|
|
657
|
+
*
|
|
658
|
+
* 1. Strip the optional locale prefix from the pathname with one cheap
|
|
659
|
+
* string-slice check (no regex exec on the hot path).
|
|
660
|
+
* 2. Look up the stripped suffix in a Map<suffix, entry[]>.
|
|
661
|
+
* 3. For each matching entry, validate the captured locale string against
|
|
662
|
+
* a small, anchored alternation regex.
|
|
663
|
+
*
|
|
664
|
+
* This reduces the per-request cost from O(n × regex) to O(1) map lookup +
|
|
665
|
+
* O(matches × tiny-regex), eliminating the ~2992ms self-time reported in
|
|
666
|
+
* profiles for apps with 63+ locale-prefixed rules.
|
|
667
|
+
*
|
|
668
|
+
* Rules that don't fit the locale-static pattern fall back to the original
|
|
669
|
+
* linear matchConfigPattern scan.
|
|
670
|
+
*
|
|
671
|
+
* ## Ordering invariant
|
|
672
|
+
*
|
|
673
|
+
* First match wins, preserving the original redirect array order. When a
|
|
674
|
+
* locale-static fast-path match is found at position N, all linear rules with
|
|
675
|
+
* an original index < N are checked via matchConfigPattern first — they are
|
|
676
|
+
* few in practice (typically zero) so this is not a hot-path concern.
|
|
515
677
|
*/
|
|
516
678
|
export function matchRedirect(pathname, redirects, ctx) {
|
|
517
|
-
|
|
679
|
+
if (redirects.length === 0)
|
|
680
|
+
return null;
|
|
681
|
+
const index = _getRedirectIndex(redirects);
|
|
682
|
+
// --- Locate the best locale-static candidate ---
|
|
683
|
+
//
|
|
684
|
+
// We look for the locale-static entry with the LOWEST originalIndex that
|
|
685
|
+
// matches this pathname (and passes has/missing conditions).
|
|
686
|
+
//
|
|
687
|
+
// Strategy: try both the full pathname (locale omitted, e.g. "/security")
|
|
688
|
+
// and the pathname with the first segment stripped (locale present, e.g.
|
|
689
|
+
// "/en/security" → suffix "/security", locale "en").
|
|
690
|
+
//
|
|
691
|
+
// We do NOT use a regex here — just a single indexOf('/') to locate the
|
|
692
|
+
// second slash, which is O(n) on the path length but far cheaper than
|
|
693
|
+
// running 63 compiled regexes.
|
|
694
|
+
let localeMatch = null;
|
|
695
|
+
let localeMatchIndex = Infinity;
|
|
696
|
+
if (index.localeStatic.size > 0) {
|
|
697
|
+
// Case 1: no locale prefix — pathname IS the suffix.
|
|
698
|
+
const noLocaleBucket = index.localeStatic.get(pathname);
|
|
699
|
+
if (noLocaleBucket) {
|
|
700
|
+
for (const entry of noLocaleBucket) {
|
|
701
|
+
if (entry.originalIndex >= localeMatchIndex)
|
|
702
|
+
continue; // already have a better match
|
|
703
|
+
const redirect = entry.redirect;
|
|
704
|
+
if (redirect.has || redirect.missing) {
|
|
705
|
+
if (!checkHasConditions(redirect.has, redirect.missing, ctx))
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
// Locale was omitted (the `?` made it optional) — param value is "".
|
|
709
|
+
let dest = redirect.destination;
|
|
710
|
+
dest = dest.replace(`:${entry.paramName}`, "");
|
|
711
|
+
dest = sanitizeDestination(dest);
|
|
712
|
+
localeMatch = { destination: dest, permanent: redirect.permanent };
|
|
713
|
+
localeMatchIndex = entry.originalIndex;
|
|
714
|
+
break; // bucket entries are in insertion order = original order
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
// Case 2: locale prefix present — first path segment is the locale.
|
|
718
|
+
// Find the second slash: pathname = "/locale/rest/of/path"
|
|
719
|
+
// ^--- slashTwo
|
|
720
|
+
const slashTwo = pathname.indexOf("/", 1);
|
|
721
|
+
if (slashTwo !== -1) {
|
|
722
|
+
const suffix = pathname.slice(slashTwo); // e.g. "/security"
|
|
723
|
+
const localePart = pathname.slice(1, slashTwo); // e.g. "en"
|
|
724
|
+
const localeBucket = index.localeStatic.get(suffix);
|
|
725
|
+
if (localeBucket) {
|
|
726
|
+
for (const entry of localeBucket) {
|
|
727
|
+
if (entry.originalIndex >= localeMatchIndex)
|
|
728
|
+
continue;
|
|
729
|
+
// Validate that `localePart` is one of the allowed alternation values.
|
|
730
|
+
if (!entry.altRe.test(localePart))
|
|
731
|
+
continue;
|
|
732
|
+
const redirect = entry.redirect;
|
|
733
|
+
if (redirect.has || redirect.missing) {
|
|
734
|
+
if (!checkHasConditions(redirect.has, redirect.missing, ctx))
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
let dest = redirect.destination;
|
|
738
|
+
dest = dest.replace(`:${entry.paramName}`, localePart);
|
|
739
|
+
dest = sanitizeDestination(dest);
|
|
740
|
+
localeMatch = { destination: dest, permanent: redirect.permanent };
|
|
741
|
+
localeMatchIndex = entry.originalIndex;
|
|
742
|
+
break; // bucket entries are in insertion order = original order
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
// --- Linear fallback: all non-locale-static rules ---
|
|
748
|
+
//
|
|
749
|
+
// We only need to check linear rules whose originalIndex < localeMatchIndex.
|
|
750
|
+
// If localeMatchIndex is Infinity (no locale match), we check all of them.
|
|
751
|
+
for (const [origIdx, redirect] of index.linear) {
|
|
752
|
+
if (origIdx >= localeMatchIndex) {
|
|
753
|
+
// This linear rule comes after the best locale-static match —
|
|
754
|
+
// the locale-static match wins. Stop scanning.
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
518
757
|
const params = matchConfigPattern(pathname, redirect.source);
|
|
519
758
|
if (params) {
|
|
520
759
|
if (redirect.has || redirect.missing) {
|
|
@@ -535,7 +774,8 @@ export function matchRedirect(pathname, redirects, ctx) {
|
|
|
535
774
|
return { destination: dest, permanent: redirect.permanent };
|
|
536
775
|
}
|
|
537
776
|
}
|
|
538
|
-
|
|
777
|
+
// Return the locale-static match if found (no earlier linear rule matched).
|
|
778
|
+
return localeMatch;
|
|
539
779
|
}
|
|
540
780
|
/**
|
|
541
781
|
* Apply rewrite rules from next.config.js.
|
|
@@ -674,7 +914,7 @@ export async function proxyExternalRequest(request, externalUrl) {
|
|
|
674
914
|
// decompression on the already-decoded body, resulting in
|
|
675
915
|
// ERR_CONTENT_DECODING_FAILED. Strip both headers on Node.js only.
|
|
676
916
|
// On Workers, fetch() preserves wire encoding, so the headers stay accurate.
|
|
677
|
-
const isNodeRuntime = typeof process !== "undefined" && !!
|
|
917
|
+
const isNodeRuntime = typeof process !== "undefined" && !!process.versions?.node;
|
|
678
918
|
const responseHeaders = new Headers();
|
|
679
919
|
upstreamResponse.headers.forEach((value, key) => {
|
|
680
920
|
const lower = key.toLowerCase();
|
|
@@ -701,8 +941,14 @@ export async function proxyExternalRequest(request, externalUrl) {
|
|
|
701
941
|
export function matchHeaders(pathname, headers, ctx) {
|
|
702
942
|
const result = [];
|
|
703
943
|
for (const rule of headers) {
|
|
704
|
-
|
|
705
|
-
|
|
944
|
+
// Cache the compiled source regex — escapeHeaderSource() + safeRegExp() are
|
|
945
|
+
// pure functions of rule.source and the result never changes between requests.
|
|
946
|
+
let sourceRegex = _compiledHeaderSourceCache.get(rule.source);
|
|
947
|
+
if (sourceRegex === undefined) {
|
|
948
|
+
const escaped = escapeHeaderSource(rule.source);
|
|
949
|
+
sourceRegex = safeRegExp("^" + escaped + "$");
|
|
950
|
+
_compiledHeaderSourceCache.set(rule.source, sourceRegex);
|
|
951
|
+
}
|
|
706
952
|
if (sourceRegex && sourceRegex.test(pathname)) {
|
|
707
953
|
if (rule.has || rule.missing) {
|
|
708
954
|
if (!checkHasConditions(rule.has, rule.missing, ctx)) {
|