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.
Files changed (196) hide show
  1. package/README.md +89 -85
  2. package/dist/build/static-export.d.ts.map +1 -1
  3. package/dist/build/static-export.js +3 -8
  4. package/dist/build/static-export.js.map +1 -1
  5. package/dist/check.d.ts.map +1 -1
  6. package/dist/check.js +152 -48
  7. package/dist/check.js.map +1 -1
  8. package/dist/cli.js +10 -11
  9. package/dist/cli.js.map +1 -1
  10. package/dist/cloudflare/kv-cache-handler.d.ts +32 -1
  11. package/dist/cloudflare/kv-cache-handler.d.ts.map +1 -1
  12. package/dist/cloudflare/kv-cache-handler.js +47 -21
  13. package/dist/cloudflare/kv-cache-handler.js.map +1 -1
  14. package/dist/cloudflare/tpr.d.ts.map +1 -1
  15. package/dist/cloudflare/tpr.js +15 -4
  16. package/dist/cloudflare/tpr.js.map +1 -1
  17. package/dist/config/config-matchers.d.ts +27 -0
  18. package/dist/config/config-matchers.d.ts.map +1 -1
  19. package/dist/config/config-matchers.js +306 -60
  20. package/dist/config/config-matchers.js.map +1 -1
  21. package/dist/config/dotenv.d.ts.map +1 -1
  22. package/dist/config/dotenv.js +1 -6
  23. package/dist/config/dotenv.js.map +1 -1
  24. package/dist/config/next-config.d.ts +7 -0
  25. package/dist/config/next-config.d.ts.map +1 -1
  26. package/dist/config/next-config.js +44 -19
  27. package/dist/config/next-config.js.map +1 -1
  28. package/dist/deploy.d.ts.map +1 -1
  29. package/dist/deploy.js +36 -19
  30. package/dist/deploy.js.map +1 -1
  31. package/dist/entries/app-rsc-entry.d.ts.map +1 -1
  32. package/dist/entries/app-rsc-entry.js +89 -38
  33. package/dist/entries/app-rsc-entry.js.map +1 -1
  34. package/dist/entries/pages-client-entry.d.ts.map +1 -1
  35. package/dist/entries/pages-client-entry.js +5 -3
  36. package/dist/entries/pages-client-entry.js.map +1 -1
  37. package/dist/entries/pages-server-entry.d.ts.map +1 -1
  38. package/dist/entries/pages-server-entry.js +32 -10
  39. package/dist/entries/pages-server-entry.js.map +1 -1
  40. package/dist/index.d.ts +1 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +204 -118
  43. package/dist/index.js.map +1 -1
  44. package/dist/init.d.ts.map +1 -1
  45. package/dist/init.js +6 -5
  46. package/dist/init.js.map +1 -1
  47. package/dist/routing/app-router.d.ts +2 -0
  48. package/dist/routing/app-router.d.ts.map +1 -1
  49. package/dist/routing/app-router.js +10 -18
  50. package/dist/routing/app-router.js.map +1 -1
  51. package/dist/routing/file-matcher.d.ts.map +1 -1
  52. package/dist/routing/file-matcher.js.map +1 -1
  53. package/dist/routing/pages-router.d.ts +2 -0
  54. package/dist/routing/pages-router.d.ts.map +1 -1
  55. package/dist/routing/pages-router.js +8 -5
  56. package/dist/routing/pages-router.js.map +1 -1
  57. package/dist/routing/utils.d.ts.map +1 -1
  58. package/dist/routing/utils.js.map +1 -1
  59. package/dist/server/api-handler.d.ts.map +1 -1
  60. package/dist/server/api-handler.js +7 -2
  61. package/dist/server/api-handler.js.map +1 -1
  62. package/dist/server/app-router-entry.d.ts +3 -2
  63. package/dist/server/app-router-entry.d.ts.map +1 -1
  64. package/dist/server/app-router-entry.js +8 -4
  65. package/dist/server/app-router-entry.js.map +1 -1
  66. package/dist/server/dev-module-runner.d.ts.map +1 -1
  67. package/dist/server/dev-module-runner.js +1 -1
  68. package/dist/server/dev-module-runner.js.map +1 -1
  69. package/dist/server/dev-origin-check.d.ts.map +1 -1
  70. package/dist/server/dev-origin-check.js.map +1 -1
  71. package/dist/server/dev-server.d.ts.map +1 -1
  72. package/dist/server/dev-server.js +30 -18
  73. package/dist/server/dev-server.js.map +1 -1
  74. package/dist/server/image-optimization.d.ts.map +1 -1
  75. package/dist/server/image-optimization.js.map +1 -1
  76. package/dist/server/instrumentation.js +1 -1
  77. package/dist/server/instrumentation.js.map +1 -1
  78. package/dist/server/isr-cache.d.ts +13 -1
  79. package/dist/server/isr-cache.d.ts.map +1 -1
  80. package/dist/server/isr-cache.js +10 -1
  81. package/dist/server/isr-cache.js.map +1 -1
  82. package/dist/server/metadata-routes.d.ts.map +1 -1
  83. package/dist/server/metadata-routes.js +6 -18
  84. package/dist/server/metadata-routes.js.map +1 -1
  85. package/dist/server/middleware-codegen.d.ts.map +1 -1
  86. package/dist/server/middleware-codegen.js +12 -10
  87. package/dist/server/middleware-codegen.js.map +1 -1
  88. package/dist/server/middleware-request-headers.d.ts +9 -0
  89. package/dist/server/middleware-request-headers.d.ts.map +1 -0
  90. package/dist/server/middleware-request-headers.js +77 -0
  91. package/dist/server/middleware-request-headers.js.map +1 -0
  92. package/dist/server/middleware.d.ts.map +1 -1
  93. package/dist/server/middleware.js +38 -19
  94. package/dist/server/middleware.js.map +1 -1
  95. package/dist/server/normalize-path.js.map +1 -1
  96. package/dist/server/prod-server.d.ts +1 -1
  97. package/dist/server/prod-server.d.ts.map +1 -1
  98. package/dist/server/prod-server.js +53 -38
  99. package/dist/server/prod-server.js.map +1 -1
  100. package/dist/server/request-pipeline.d.ts +2 -1
  101. package/dist/server/request-pipeline.d.ts.map +1 -1
  102. package/dist/server/request-pipeline.js +5 -7
  103. package/dist/server/request-pipeline.js.map +1 -1
  104. package/dist/shims/cache-runtime.d.ts.map +1 -1
  105. package/dist/shims/cache-runtime.js +21 -16
  106. package/dist/shims/cache-runtime.js.map +1 -1
  107. package/dist/shims/cache.d.ts.map +1 -1
  108. package/dist/shims/cache.js +18 -17
  109. package/dist/shims/cache.js.map +1 -1
  110. package/dist/shims/constants.d.ts.map +1 -1
  111. package/dist/shims/constants.js +1 -6
  112. package/dist/shims/constants.js.map +1 -1
  113. package/dist/shims/dynamic.d.ts.map +1 -1
  114. package/dist/shims/dynamic.js +1 -1
  115. package/dist/shims/dynamic.js.map +1 -1
  116. package/dist/shims/error-boundary.d.ts.map +1 -1
  117. package/dist/shims/error-boundary.js +2 -3
  118. package/dist/shims/error-boundary.js.map +1 -1
  119. package/dist/shims/error.d.ts.map +1 -1
  120. package/dist/shims/error.js +1 -3
  121. package/dist/shims/error.js.map +1 -1
  122. package/dist/shims/fetch-cache.d.ts.map +1 -1
  123. package/dist/shims/fetch-cache.js +53 -29
  124. package/dist/shims/fetch-cache.js.map +1 -1
  125. package/dist/shims/font-google-base.d.ts.map +1 -1
  126. package/dist/shims/font-google-base.js +16 -4
  127. package/dist/shims/font-google-base.js.map +1 -1
  128. package/dist/shims/font-google.d.ts +1 -1
  129. package/dist/shims/font-google.d.ts.map +1 -1
  130. package/dist/shims/font-google.generated.d.ts.map +1 -1
  131. package/dist/shims/font-google.generated.js +412 -206
  132. package/dist/shims/font-google.generated.js.map +1 -1
  133. package/dist/shims/font-google.js +1 -1
  134. package/dist/shims/font-google.js.map +1 -1
  135. package/dist/shims/font-local.d.ts.map +1 -1
  136. package/dist/shims/font-local.js +13 -3
  137. package/dist/shims/font-local.js.map +1 -1
  138. package/dist/shims/form.d.ts.map +1 -1
  139. package/dist/shims/form.js +2 -2
  140. package/dist/shims/form.js.map +1 -1
  141. package/dist/shims/head.d.ts.map +1 -1
  142. package/dist/shims/head.js +10 -8
  143. package/dist/shims/head.js.map +1 -1
  144. package/dist/shims/headers.d.ts +23 -5
  145. package/dist/shims/headers.d.ts.map +1 -1
  146. package/dist/shims/headers.js +97 -37
  147. package/dist/shims/headers.js.map +1 -1
  148. package/dist/shims/image.d.ts.map +1 -1
  149. package/dist/shims/image.js +35 -8
  150. package/dist/shims/image.js.map +1 -1
  151. package/dist/shims/legacy-image.d.ts.map +1 -1
  152. package/dist/shims/legacy-image.js +1 -1
  153. package/dist/shims/legacy-image.js.map +1 -1
  154. package/dist/shims/link.d.ts.map +1 -1
  155. package/dist/shims/link.js +29 -15
  156. package/dist/shims/link.js.map +1 -1
  157. package/dist/shims/metadata.d.ts +12 -2
  158. package/dist/shims/metadata.d.ts.map +1 -1
  159. package/dist/shims/metadata.js +10 -8
  160. package/dist/shims/metadata.js.map +1 -1
  161. package/dist/shims/navigation-state.d.ts.map +1 -1
  162. package/dist/shims/navigation-state.js +3 -2
  163. package/dist/shims/navigation-state.js.map +1 -1
  164. package/dist/shims/navigation.d.ts.map +1 -1
  165. package/dist/shims/navigation.js +26 -19
  166. package/dist/shims/navigation.js.map +1 -1
  167. package/dist/shims/request-context.d.ts +50 -0
  168. package/dist/shims/request-context.d.ts.map +1 -0
  169. package/dist/shims/request-context.js +59 -0
  170. package/dist/shims/request-context.js.map +1 -0
  171. package/dist/shims/router-state.d.ts.map +1 -1
  172. package/dist/shims/router-state.js +2 -1
  173. package/dist/shims/router-state.js.map +1 -1
  174. package/dist/shims/router.d.ts.map +1 -1
  175. package/dist/shims/router.js +18 -25
  176. package/dist/shims/router.js.map +1 -1
  177. package/dist/shims/script.d.ts.map +1 -1
  178. package/dist/shims/script.js.map +1 -1
  179. package/dist/shims/server.d.ts +13 -0
  180. package/dist/shims/server.d.ts.map +1 -1
  181. package/dist/shims/server.js +100 -34
  182. package/dist/shims/server.js.map +1 -1
  183. package/dist/shims/url-utils.d.ts.map +1 -1
  184. package/dist/shims/url-utils.js +1 -3
  185. package/dist/shims/url-utils.js.map +1 -1
  186. package/dist/utils/base-path.d.ts +17 -0
  187. package/dist/utils/base-path.d.ts.map +1 -0
  188. package/dist/utils/base-path.js +25 -0
  189. package/dist/utils/base-path.js.map +1 -0
  190. package/dist/utils/project.d.ts.map +1 -1
  191. package/dist/utils/project.js +2 -4
  192. package/dist/utils/project.js.map +1 -1
  193. package/dist/utils/query.d.ts.map +1 -1
  194. package/dist/utils/query.js +3 -1
  195. package/dist/utils/query.js.map +1 -1
  196. 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 mwReqPrefix = "x-middleware-request-";
263
- const toApply = {};
384
+ const nextHeaders = buildRequestHeadersFromMiddlewareResponse(request.headers, middlewareHeaders);
264
385
  for (const key of Object.keys(middlewareHeaders)) {
265
- if (key.startsWith(mwReqPrefix)) {
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 (Object.keys(toApply).length > 0) {
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: newHeaders,
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 = safeRegExp(condition.value);
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 = safeRegExp(condition.value);
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 = safeRegExp(condition.value);
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 = safeRegExp(condition.value);
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
- // Param names may contain hyphens (e.g. :auth-method, :sign-in).
416
- const paramNames = [];
417
- // Single-pass conversion with procedural suffix handling. The tokenizer
418
- // matches only simple, non-overlapping tokens; quantifier/constraint
419
- // suffixes after :param are consumed procedurally to avoid polynomial
420
- // backtracking in the regex engine.
421
- let regexStr = "";
422
- const tokenRe = /:([\w-]+)|[.]|[^:.]+/g; // lgtm[js/redos] — alternatives are non-overlapping (`:` and `.` excluded from `[^:.]+`)
423
- let tok;
424
- while ((tok = tokenRe.exec(pattern)) !== null) {
425
- if (tok[1] !== undefined) {
426
- const name = tok[1];
427
- const rest = pattern.slice(tokenRe.lastIndex);
428
- // Check for quantifier (* or +) with optional constraint
429
- if (rest.startsWith("*") || rest.startsWith("+")) {
430
- const quantifier = rest[0];
431
- tokenRe.lastIndex += 1;
432
- const constraint = extractConstraint(pattern, tokenRe);
433
- paramNames.push(name);
434
- if (constraint !== null) {
435
- regexStr += `(${constraint})`;
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
- regexStr += quantifier === "*" ? "(.*)" : "(.+)";
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
- // Check for inline constraint without quantifier
443
- const constraint = extractConstraint(pattern, tokenRe);
444
- paramNames.push(name);
445
- regexStr += constraint !== null ? `(${constraint})` : "([^/]+)";
583
+ regexStr += tok[0];
446
584
  }
447
585
  }
448
- else if (tok[0] === ".") {
449
- regexStr += "\\.";
450
- }
451
- else {
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
- const re = safeRegExp("^" + regexStr + "$");
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
- for (const redirect of redirects) {
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
- return null;
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" && !!(process.versions?.node);
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
- const escaped = escapeHeaderSource(rule.source);
705
- const sourceRegex = safeRegExp("^" + escaped + "$");
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)) {