vinext 1.2.0 → 1.3.0

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 (283) hide show
  1. package/README.md +13 -10
  2. package/dist/build/client-build-config.d.ts +64 -3
  3. package/dist/build/client-build-config.js +131 -4
  4. package/dist/build/client-reference-groups.d.ts +163 -0
  5. package/dist/build/client-reference-groups.js +534 -0
  6. package/dist/build/module-dependency-cache.d.ts +31 -0
  7. package/dist/build/module-dependency-cache.js +51 -1
  8. package/dist/build/next-client-runtime-manifests.d.ts +10 -1
  9. package/dist/build/next-client-runtime-manifests.js +19 -4
  10. package/dist/build/nitro-public-files.d.ts +24 -0
  11. package/dist/build/nitro-public-files.js +60 -0
  12. package/dist/build/output-root-externals.d.ts +21 -0
  13. package/dist/build/output-root-externals.js +40 -0
  14. package/dist/build/pages-client-assets-module.js +1 -1
  15. package/dist/build/prerender-paths.js +6 -3
  16. package/dist/build/prerender.js +1 -1
  17. package/dist/build/server-actions-encryption-key.d.ts +23 -0
  18. package/dist/build/server-actions-encryption-key.js +101 -0
  19. package/dist/build/ssr-manifest.d.ts +3 -0
  20. package/dist/build/ssr-manifest.js +10 -1
  21. package/dist/check.js +5 -6
  22. package/dist/cli-dev-config.js +3 -3
  23. package/dist/cli.js +1 -1
  24. package/dist/client/client-rewrite-params.d.ts +9 -0
  25. package/dist/client/client-rewrite-params.js +20 -0
  26. package/dist/client/dev-error-overlay.js +2 -1
  27. package/dist/client/navigation-runtime.d.ts +25 -2
  28. package/dist/client/navigation-runtime.js +42 -11
  29. package/dist/client/vinext-next-data.d.ts +0 -1
  30. package/dist/config/config-matchers.d.ts +15 -40
  31. package/dist/config/config-matchers.js +195 -262
  32. package/dist/config/config-source.d.ts +34 -0
  33. package/dist/config/config-source.js +141 -0
  34. package/dist/config/header-params.d.ts +29 -0
  35. package/dist/config/header-params.js +106 -0
  36. package/dist/config/next-config.d.ts +7 -5
  37. package/dist/config/next-config.js +120 -7
  38. package/dist/entries/app-browser-entry.d.ts +14 -1
  39. package/dist/entries/app-browser-entry.js +26 -36
  40. package/dist/entries/app-rsc-entry.d.ts +2 -0
  41. package/dist/entries/app-rsc-entry.js +92 -57
  42. package/dist/entries/app-rsc-manifest.d.ts +10 -0
  43. package/dist/entries/app-rsc-manifest.js +20 -1
  44. package/dist/entries/app-ssr-entry.d.ts +4 -1
  45. package/dist/entries/app-ssr-entry.js +9 -2
  46. package/dist/entries/pages-client-entry.d.ts +1 -0
  47. package/dist/entries/pages-client-entry.js +6 -6
  48. package/dist/entries/pages-server-entry.js +17 -14
  49. package/dist/index.d.ts +11 -0
  50. package/dist/index.js +350 -175
  51. package/dist/init.d.ts +1 -1
  52. package/dist/init.js +103 -6
  53. package/dist/plugins/ast-utils.d.ts +53 -2
  54. package/dist/plugins/ast-utils.js +90 -2
  55. package/dist/plugins/cloudflare-dev-module-urls.d.ts +41 -0
  56. package/dist/plugins/cloudflare-dev-module-urls.js +271 -0
  57. package/dist/plugins/commonjs-esm-facade.js +2 -2
  58. package/dist/plugins/css-module-imports.js +13 -20
  59. package/dist/plugins/dynamic-preload-metadata.js +2 -3
  60. package/dist/plugins/extensionless-dynamic-import.js +2 -3
  61. package/dist/plugins/fonts.d.ts +1 -1
  62. package/dist/plugins/fonts.js +3 -3
  63. package/dist/plugins/ignore-dynamic-requests.js +2 -3
  64. package/dist/plugins/import-meta-url.js +3 -4
  65. package/dist/plugins/instrumentation-client.js +3 -2
  66. package/dist/plugins/middleware-export-validation.js +2 -3
  67. package/dist/plugins/og-harfbuzz.js +2 -3
  68. package/dist/plugins/optimize-imports.d.ts +25 -2
  69. package/dist/plugins/optimize-imports.js +109 -24
  70. package/dist/plugins/pages-node-externals.js +2 -3
  71. package/dist/plugins/remove-console.js +2 -18
  72. package/dist/plugins/require-condition-resolution.js +3 -3
  73. package/dist/plugins/require-context.js +2 -3
  74. package/dist/plugins/rsc-client-reference-loaders.d.ts +45 -1
  75. package/dist/plugins/rsc-client-reference-loaders.js +336 -18
  76. package/dist/plugins/strip-server-exports.js +3 -4
  77. package/dist/plugins/styled-jsx.js +2 -3
  78. package/dist/plugins/use-cache-callable.d.ts +26 -2
  79. package/dist/plugins/use-cache-callable.js +101 -36
  80. package/dist/plugins/worker-image-imports.d.ts +1 -0
  81. package/dist/plugins/worker-image-imports.js +31 -34
  82. package/dist/plugins/worker-use-server.js +38 -14
  83. package/dist/routing/app-route-graph.d.ts +1 -1
  84. package/dist/routing/browser-route-manifest.d.ts +43 -0
  85. package/dist/routing/browser-route-manifest.js +195 -0
  86. package/dist/routing/hashed-route-patterns.d.ts +4 -0
  87. package/dist/routing/hashed-route-patterns.js +22 -0
  88. package/dist/routing/optimistic-route-trie.d.ts +27 -0
  89. package/dist/routing/optimistic-route-trie.js +102 -0
  90. package/dist/routing/route-validation.d.ts +6 -1
  91. package/dist/routing/route-validation.js +26 -10
  92. package/dist/server/api-handler.js +1 -1
  93. package/dist/server/app-action-forwarding.d.ts +8 -0
  94. package/dist/server/app-action-forwarding.js +17 -3
  95. package/dist/server/app-browser-entry.js +149 -62
  96. package/dist/server/app-browser-error.d.ts +3 -1
  97. package/dist/server/app-browser-error.js +7 -2
  98. package/dist/server/app-browser-interception-context.d.ts +2 -2
  99. package/dist/server/app-browser-interception-context.js +1 -1
  100. package/dist/server/app-browser-mpa-navigation.d.ts +1 -0
  101. package/dist/server/app-browser-mpa-navigation.js +6 -5
  102. package/dist/server/app-browser-navigation-controller.d.ts +17 -6
  103. package/dist/server/app-browser-navigation-controller.js +88 -26
  104. package/dist/server/app-browser-prefetch-response.js +2 -2
  105. package/dist/server/app-browser-state.d.ts +2 -2
  106. package/dist/server/app-browser-state.js +1 -1
  107. package/dist/server/app-browser-supplemental-refresh.js +1 -1
  108. package/dist/server/app-browser-visible-commit.d.ts +4 -4
  109. package/dist/server/app-client-reference-preloader.d.ts +23 -0
  110. package/dist/server/app-client-reference-preloader.js +30 -7
  111. package/dist/server/app-fallback-renderer.js +2 -1
  112. package/dist/server/app-middleware.d.ts +6 -0
  113. package/dist/server/app-middleware.js +8 -10
  114. package/dist/server/app-optimistic-routing.d.ts +8 -10
  115. package/dist/server/app-optimistic-routing.js +4 -95
  116. package/dist/server/app-page-boundary-render.js +8 -1
  117. package/dist/server/app-page-cache-render.js +1 -0
  118. package/dist/server/app-page-dispatch.js +3 -1
  119. package/dist/server/app-page-render.js +4 -2
  120. package/dist/server/app-page-rsc-render-status.d.ts +1 -0
  121. package/dist/server/app-page-rsc-render-status.js +1 -0
  122. package/dist/server/app-page-search-params-observation.d.ts +1 -4
  123. package/dist/server/app-page-search-params-observation.js +8 -10
  124. package/dist/server/app-page-stream.d.ts +7 -0
  125. package/dist/server/app-page-stream.js +1 -0
  126. package/dist/server/app-pages-bridge.d.ts +2 -1
  127. package/dist/server/app-pages-bridge.js +12 -8
  128. package/dist/server/app-prerender-endpoints.js +1 -1
  129. package/dist/server/app-prerender-static-params.js +3 -2
  130. package/dist/server/app-render-abort-error.d.ts +19 -0
  131. package/dist/server/app-render-abort-error.js +31 -1
  132. package/dist/server/app-render-dependency.js +42 -2
  133. package/dist/server/app-request-stage-independent-entry.js +7 -7
  134. package/dist/server/app-response-stage-entry.js +5 -5
  135. package/dist/server/app-route-handler-dispatch.js +3 -1
  136. package/dist/server/app-route-handler-execution.d.ts +2 -0
  137. package/dist/server/app-route-handler-execution.js +9 -2
  138. package/dist/server/app-route-handler-middleware-context.d.ts +2 -1
  139. package/dist/server/app-route-handler-middleware-context.js +3 -3
  140. package/dist/server/app-route-handler-response.d.ts +2 -1
  141. package/dist/server/app-route-handler-response.js +2 -2
  142. package/dist/server/app-route-handler-runtime.d.ts +2 -0
  143. package/dist/server/app-route-handler-runtime.js +21 -4
  144. package/dist/server/app-route-module-loader.js +7 -6
  145. package/dist/server/app-route-tree.d.ts +20 -0
  146. package/dist/server/app-route-tree.js +18 -0
  147. package/dist/server/app-router-entry.js +7 -7
  148. package/dist/server/app-rsc-cache-busting.d.ts +16 -0
  149. package/dist/server/app-rsc-cache-busting.js +29 -12
  150. package/dist/server/app-rsc-combined-handler.js +8 -1
  151. package/dist/server/app-rsc-errors.d.ts +7 -0
  152. package/dist/server/app-rsc-errors.js +9 -2
  153. package/dist/server/app-rsc-handler.d.ts +4 -1
  154. package/dist/server/app-rsc-handler.js +151 -135
  155. package/dist/server/app-rsc-request-normalization.js +3 -3
  156. package/dist/server/app-rsc-response-finalizer.d.ts +5 -0
  157. package/dist/server/app-rsc-response-finalizer.js +4 -1
  158. package/dist/server/app-rsc-response-stage.js +5 -3
  159. package/dist/server/app-rsc-route-matching.d.ts +1 -0
  160. package/dist/server/app-rsc-route-matching.js +1 -1
  161. package/dist/server/app-server-action-execution.d.ts +23 -6
  162. package/dist/server/app-server-action-execution.js +51 -57
  163. package/dist/server/app-ssr-entry.d.ts +6 -0
  164. package/dist/server/app-ssr-entry.js +21 -11
  165. package/dist/server/app-ssr-stream.d.ts +7 -0
  166. package/dist/server/app-ssr-stream.js +13 -1
  167. package/dist/server/app-visited-response-cache.js +2 -2
  168. package/dist/server/config-headers.js +13 -4
  169. package/dist/server/defer-until-stream-consumed.d.ts +2 -1
  170. package/dist/server/defer-until-stream-consumed.js +3 -1
  171. package/dist/server/dev-server.js +1 -1
  172. package/dist/server/image-optimization.js +1 -0
  173. package/dist/server/isolate-shared-promise.d.ts +10 -0
  174. package/dist/server/isolate-shared-promise.js +33 -0
  175. package/dist/server/isr-cache.js +3 -2
  176. package/dist/server/metadata-route-response.js +2 -2
  177. package/dist/server/middleware-matcher-pattern.d.ts +0 -1
  178. package/dist/server/middleware-matcher-pattern.js +11 -140
  179. package/dist/server/middleware-matcher.js +5 -6
  180. package/dist/server/middleware-response-headers.d.ts +31 -0
  181. package/dist/server/middleware-response-headers.js +57 -1
  182. package/dist/server/middleware-runtime.js +30 -17
  183. package/dist/server/navigation-planner.d.ts +2 -2
  184. package/dist/server/navigation-planner.js +1 -1
  185. package/dist/server/nitro-public-files-plugin.d.ts +38 -0
  186. package/dist/server/nitro-public-files-plugin.js +106 -0
  187. package/dist/server/nitro-public-files.d.ts +21 -0
  188. package/dist/server/nitro-public-files.js +39 -0
  189. package/dist/server/open-redirect.d.ts +11 -4
  190. package/dist/server/open-redirect.js +11 -4
  191. package/dist/server/pages-api-route.js +2 -2
  192. package/dist/server/pages-asset-tags.d.ts +5 -0
  193. package/dist/server/pages-asset-tags.js +9 -2
  194. package/dist/server/pages-client-assets.d.ts +4 -1
  195. package/dist/server/pages-data-route.js +2 -1
  196. package/dist/server/pages-i18n.d.ts +18 -0
  197. package/dist/server/pages-i18n.js +18 -2
  198. package/dist/server/pages-node-compat.d.ts +5 -3
  199. package/dist/server/pages-node-compat.js +35 -16
  200. package/dist/server/pages-page-data.js +1 -1
  201. package/dist/server/pages-page-handler.js +1 -1
  202. package/dist/server/pages-page-response.js +1 -1
  203. package/dist/server/pages-request-pipeline.d.ts +6 -1
  204. package/dist/server/pages-request-pipeline.js +35 -39
  205. package/dist/server/pages-request-stage-entry.js +20 -10
  206. package/dist/server/pages-response-stage-entry.js +1 -1
  207. package/dist/server/pages-revalidate.js +3 -2
  208. package/dist/server/pages-router-entry.js +2 -2
  209. package/dist/server/prod-server.d.ts +2 -2
  210. package/dist/server/prod-server.js +74 -36
  211. package/dist/server/proxy-trust.d.ts +6 -0
  212. package/dist/server/proxy-trust.js +12 -6
  213. package/dist/server/request-pipeline.d.ts +5 -0
  214. package/dist/server/request-pipeline.js +12 -1
  215. package/dist/server/rsc-stream-hints.d.ts +1 -1
  216. package/dist/server/rsc-stream-hints.js +21 -3
  217. package/dist/server/server-reference-loader.js +10 -1
  218. package/dist/server/static-file-cache.js +15 -3
  219. package/dist/server/worker-image-optimization.js +19 -6
  220. package/dist/server/worker-utils.js +1 -1
  221. package/dist/shims/cache-handler.js +3 -2
  222. package/dist/shims/cache-request-state.d.ts +40 -3
  223. package/dist/shims/cache-request-state.js +70 -5
  224. package/dist/shims/cache-runtime.js +47 -19
  225. package/dist/shims/cache.d.ts +2 -2
  226. package/dist/shims/cache.js +17 -8
  227. package/dist/shims/cdn-purge-routes.d.ts +24 -0
  228. package/dist/shims/cdn-purge-routes.js +77 -0
  229. package/dist/shims/dynamic.js +17 -4
  230. package/dist/shims/form.js +8 -3
  231. package/dist/shims/head.js +1 -1
  232. package/dist/shims/headers.js +2 -2
  233. package/dist/shims/image-props.d.ts +93 -0
  234. package/dist/shims/image-props.js +323 -0
  235. package/dist/shims/image.d.ts +4 -18
  236. package/dist/shims/image.js +4 -319
  237. package/dist/shims/internal/headers-state.d.ts +7 -0
  238. package/dist/shims/internal/headers-state.js +14 -2
  239. package/dist/shims/internal/hybrid-client-route-owner-direct.d.ts +4 -1
  240. package/dist/shims/internal/hybrid-client-route-owner-direct.js +32 -15
  241. package/dist/shims/internal/hybrid-client-route-owner.js +9 -8
  242. package/dist/shims/link-prefetch.js +5 -4
  243. package/dist/shims/link.js +7 -5
  244. package/dist/shims/navigation.d.ts +1 -1
  245. package/dist/shims/navigation.js +21 -10
  246. package/dist/shims/og.js +35 -10
  247. package/dist/shims/request-context.d.ts +13 -0
  248. package/dist/shims/router.js +28 -52
  249. package/dist/shims/server.d.ts +42 -8
  250. package/dist/shims/server.js +99 -15
  251. package/dist/shims/thenable-params.d.ts +6 -1
  252. package/dist/shims/thenable-params.js +11 -2
  253. package/dist/shims/unified-request-context.d.ts +5 -0
  254. package/dist/shims/unified-request-context.js +2 -0
  255. package/dist/shims/url-utils.d.ts +7 -0
  256. package/dist/shims/url-utils.js +12 -3
  257. package/dist/utils/asset-prefix.d.ts +7 -0
  258. package/dist/utils/asset-prefix.js +10 -1
  259. package/dist/utils/client-build-manifest.js +12 -5
  260. package/dist/utils/client-runtime-metadata.d.ts +1 -0
  261. package/dist/utils/client-runtime-metadata.js +15 -2
  262. package/dist/utils/domain-locale.js +1 -1
  263. package/dist/utils/lazy-chunks.d.ts +4 -0
  264. package/dist/utils/manifest-paths.d.ts +14 -0
  265. package/dist/utils/manifest-paths.js +22 -1
  266. package/dist/utils/path-to-regexp.d.ts +21 -0
  267. package/dist/{server/middleware-path-to-regexp.js → utils/path-to-regexp.js} +14 -11
  268. package/dist/utils/plugin-react-version.d.ts +15 -0
  269. package/dist/utils/plugin-react-version.js +64 -0
  270. package/dist/utils/plugin-rsc-version.d.ts +2 -0
  271. package/dist/utils/plugin-rsc-version.js +28 -2
  272. package/dist/utils/public-routes.d.ts +2 -0
  273. package/dist/utils/public-routes.js +18 -7
  274. package/dist/utils/react-version.d.ts +7 -0
  275. package/dist/utils/react-version.js +39 -16
  276. package/package.json +6 -6
  277. package/dist/build/nitro-route-rules.d.ts +0 -47
  278. package/dist/build/nitro-route-rules.js +0 -79
  279. package/dist/plugins/cache-flight-codec.d.ts +0 -19
  280. package/dist/plugins/cache-flight-codec.js +0 -68
  281. package/dist/server/middleware-path-to-regexp.d.ts +0 -13
  282. package/dist/utils/regex-safety.d.ts +0 -17
  283. package/dist/utils/regex-safety.js +0 -789
@@ -1,59 +1,32 @@
1
1
  import { PRERENDER_REVALIDATE_HEADER, PRERENDER_REVALIDATE_ONLY_GENERATED_HEADER, VINEXT_MW_CTX_HEADER, VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, VINEXT_PRERENDER_SECRET_HEADER, VINEXT_REVALIDATE_HOST_HEADER } from "../utils/protocol-headers.js";
2
2
  import { buildRequestHeadersFromMiddlewareResponse, getUnconsumedMiddlewareRequestHeaders } from "../utils/middleware-request-headers.js";
3
- import { analyzeRegexSafety } from "../utils/regex-safety.js";
3
+ import { compileConfigSource, configSourceError, execConfigSource, matchConfigPattern } from "./config-source.js";
4
4
  import { normalizeHost, parseCookies, requestContextFromRequest } from "./request-context.js";
5
5
  import { isExternalUrl } from "../utils/external-url.js";
6
+ import { attachRequestCfMetadata } from "../server/request-pipeline.js";
6
7
  import { substituteDestinationParams, substituteRedirectDestinationQuery } from "./destination-params.js";
8
+ import { compileHeaderParamTemplate, isValidHeaderPair } from "./header-params.js";
7
9
  //#region src/config/config-matchers.ts
8
10
  /**
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 = /* @__PURE__ */ 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 = /* @__PURE__ */ new Map();
34
- /**
35
11
  * Cache for compiled has/missing condition value regexes in checkSingleCondition.
36
12
  *
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.
13
+ * Each has/missing condition may carry a `value` string that is compiled as a
14
+ * regex for matching against header/cookie/query/host values. The condition
15
+ * objects are static (from next.config.js) so the compiled RegExp never
16
+ * changes. Without caching, the value is recompiled on every request for every
17
+ * condition on every rule.
42
18
  *
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).
19
+ * Value is `null` when the value is not a valid regex (use exact string
20
+ * comparison instead).
45
21
  */
46
22
  const _compiledConditionCache = /* @__PURE__ */ new Map();
47
23
  /**
48
- * Generic helper for the regex compilation caches above.
24
+ * Generic helper for the regex compilation cache above.
49
25
  *
50
- * Each cache stores the compiled artifact (or `null` when safeRegExp rejected
51
- * the pattern) the first time a key is seen, and reuses it forever. The
26
+ * The cache stores the compiled artifact (or `null` when the pattern could
27
+ * not be compiled) the first time a key is seen, and reuses it forever. The
52
28
  * `undefined` sentinel distinguishes "not yet seen" from "seen and rejected"
53
- * so we never re-run isSafeRegex on the same input.
54
- *
55
- * Keep the security path intact: `compile()` is responsible for calling
56
- * safeRegExp(); this helper only handles caching.
29
+ * so we never recompile the same input.
57
30
  */
58
31
  function getCachedRegex(cache, key, compile) {
59
32
  let value = cache.get(key);
@@ -105,8 +78,36 @@ function getCachedRegex(cache, key, compile) {
105
78
  * by `applyLocaleToRoutes` for the locale-capture variant)
106
79
  * Both forms benefit from O(1) suffix lookup; the optionality is recorded
107
80
  * on the entry so we know whether to try the no-locale-prefix bucket.
81
+ *
82
+ * The index must match exactly what the compiled source would. So the param
83
+ * name follows path-to-regexp's `\w` name lexer, the alternatives are plain
84
+ * words, possibly empty (a `.*` alternative could span several segments), and
85
+ * the suffix class only admits characters path-to-regexp treats as literals:
86
+ * `:` (param), `*` and `+` (modifiers), `(`, `{`, `?` and `\` are excluded.
108
87
  */
109
- const _LOCALE_STATIC_RE = /^\/:[\w-]+\(([^)]+)\)(\??)\/([a-zA-Z0-9_~.%@!$&'*+,;=:/-]+)$/;
88
+ const _LOCALE_STATIC_RE = /^\/:\w+\(([\w-]*(?:\|[\w-]*)*)\)(\??)\/([a-zA-Z0-9_~.%@!$&',;=/-]+)$/;
89
+ /**
90
+ * ECMAScript's non-Unicode Canonicalize operation, which a RegExp with `i` and
91
+ * without `u` compares characters by: it keeps the original UTF-16 code unit
92
+ * when uppercasing expands it or maps a non-ASCII character to ASCII.
93
+ */
94
+ function canonicalizeIgnoreCase(character) {
95
+ const upper = character.toUpperCase();
96
+ if (upper.length !== 1) return character;
97
+ if (character.charCodeAt(0) >= 128 && upper.charCodeAt(0) < 128) return character;
98
+ return upper;
99
+ }
100
+ /**
101
+ * Folds case as a RegExp with `i` and without `u` compares characters, as
102
+ * the compiled source does: `toLowerCase()` would also read the Kelvin sign
103
+ * as `k`.
104
+ */
105
+ function foldCase(value) {
106
+ if (!/[^ -~]/.test(value)) return value.toUpperCase();
107
+ let folded = "";
108
+ for (let i = 0; i < value.length; i++) folded += canonicalizeIgnoreCase(value[i]);
109
+ return folded;
110
+ }
110
111
  const _redirectIndexCache = /* @__PURE__ */ new WeakMap();
111
112
  /**
112
113
  * Build (or retrieve from cache) the redirect index for a given redirects array.
@@ -123,12 +124,12 @@ function _getRedirectIndex(redirects) {
123
124
  for (let i = 0; i < redirects.length; i++) {
124
125
  const redirect = redirects[i];
125
126
  const m = _LOCALE_STATIC_RE.exec(redirect.source);
126
- if (m) {
127
+ if (m && m[1] !== "" && !m[3].endsWith("/")) {
127
128
  const paramName = redirect.source.slice(2, redirect.source.indexOf("("));
128
129
  const alternation = m[1];
129
130
  const optional = m[2] === "?";
130
131
  const suffix = "/" + m[3];
131
- const altRe = safeRegExp("^(?:" + alternation + ")$", "i");
132
+ const altRe = compileRegExp("^(?:" + alternation + ")$", "i");
132
133
  if (!altRe) {
133
134
  linear.push([i, redirect]);
134
135
  continue;
@@ -140,7 +141,7 @@ function _getRedirectIndex(redirects) {
140
141
  redirect,
141
142
  originalIndex: i
142
143
  };
143
- const bucketKey = suffix.toLowerCase();
144
+ const bucketKey = foldCase(suffix);
144
145
  const bucket = localeStatic.get(bucketKey);
145
146
  if (bucket) bucket.push(entry);
146
147
  else localeStatic.set(bucketKey, [entry]);
@@ -184,77 +185,18 @@ function stripHopByHopRequestHeaders(headers) {
184
185
  for (const token of connectionTokens) headers.delete(token);
185
186
  }
186
187
  /**
187
- * Detect regex patterns vulnerable to catastrophic backtracking (ReDoS).
188
- *
189
- * Uses the same deterministic structural analysis as middleware matcher
190
- * validation. Nested bounded repetition is accepted only when its repeated
191
- * language has fixed width and unambiguous branches; a fixed outer count can
192
- * otherwise still cause polynomially catastrophic backtracking on long near
193
- * misses.
194
- *
195
- * Returns true if the pattern appears safe, false if it's potentially dangerous.
196
- */
197
- function isSafeRegex(pattern, flags) {
198
- return analyzeRegexSafety(pattern, { ignoreCase: flags?.includes("i") }) === null;
199
- }
200
- /**
201
- * Compile a regex pattern safely. Returns the compiled RegExp or null if the
202
- * pattern is invalid or vulnerable to ReDoS.
188
+ * Compile a regex from next.config, or return null when it is invalid.
203
189
  *
204
- * Logs a warning when a pattern is rejected so developers can fix their config.
190
+ * Like Next.js, patterns are not checked for catastrophic backtracking: they
191
+ * run as written.
205
192
  */
206
- function safeRegExp(pattern, flags) {
207
- if (!isSafeRegex(pattern, flags)) {
208
- console.warn(`[vinext] Rejecting potentially unsafe regex pattern (ReDoS risk): ${pattern}\n Nested or ambiguous repetition can cause catastrophic backtracking.\n Simplify the pattern to make repeated matches fixed and unambiguous.`);
209
- return null;
210
- }
193
+ function compileRegExp(pattern, flags) {
211
194
  try {
212
195
  return new RegExp(pattern, flags);
213
196
  } catch {
214
197
  return null;
215
198
  }
216
199
  }
217
- /**
218
- * Convert a Next.js header/rewrite/redirect source pattern into a regex string.
219
- *
220
- * Regex groups in the source (e.g. `(\d+)`) are extracted first, the remaining
221
- * text is escaped/converted in a **single pass** (avoiding chained `.replace()`
222
- * which CodeQL flags as incomplete sanitization), then groups are restored.
223
- */
224
- function escapeHeaderSource(source) {
225
- const S = "";
226
- const groups = [];
227
- const withPlaceholders = source.replace(/\(([^)]+)\)/g, (_m, inner) => {
228
- groups.push(inner);
229
- return `${S}G${groups.length - 1}${S}`;
230
- });
231
- let result = "";
232
- const re = new RegExp(`${S}G(\\d+)${S}|:[\\w-]+|[.+?*]|[^.+?*:\\uE000]+`, "g");
233
- let m;
234
- while ((m = re.exec(withPlaceholders)) !== null) if (m[1] !== void 0) result += `(${groups[Number(m[1])]})`;
235
- else if (m[0].startsWith(":")) {
236
- const constraintMatch = withPlaceholders.slice(re.lastIndex).match(new RegExp(`^${S}G(\\d+)${S}`));
237
- if (constraintMatch) {
238
- re.lastIndex += constraintMatch[0].length;
239
- result += `(${groups[Number(constraintMatch[1])]})`;
240
- } else result += "[^/]+";
241
- } else switch (m[0]) {
242
- case ".":
243
- result += "\\.";
244
- break;
245
- case "+":
246
- result += "\\+";
247
- break;
248
- case "?":
249
- result += "\\?";
250
- break;
251
- case "*":
252
- result += ".*";
253
- break;
254
- default: result += m[0];
255
- }
256
- return result;
257
- }
258
200
  const _BASEPATH_DEFAULT = {
259
201
  basePath: "",
260
202
  hadBasePath: true
@@ -296,13 +238,17 @@ function applyMiddlewareRequestHeaders(middlewareHeaders, request) {
296
238
  const nextHeaders = buildRequestHeadersFromMiddlewareResponse(request.headers, middlewareHeaders);
297
239
  const unconsumedRequestHeaders = getUnconsumedMiddlewareRequestHeaders(middlewareHeaders);
298
240
  for (const key of Object.keys(middlewareHeaders)) if (key.startsWith("x-middleware-") && key !== "x-middleware-cache" && !unconsumedRequestHeaders.has(key)) delete middlewareHeaders[key];
299
- if (nextHeaders) request = new Request(request.url, {
300
- method: request.method,
301
- headers: nextHeaders,
302
- body: request.body,
303
- signal: request.signal,
304
- duplex: request.body ? "half" : void 0
305
- });
241
+ if (nextHeaders) {
242
+ const source = request;
243
+ request = new Request(request.url, {
244
+ method: request.method,
245
+ headers: nextHeaders,
246
+ body: request.body,
247
+ signal: request.signal,
248
+ duplex: request.body ? "half" : void 0
249
+ });
250
+ attachRequestCfMetadata(request, source);
251
+ }
306
252
  return {
307
253
  request,
308
254
  postMwReqCtx: requestContextFromRequest(request)
@@ -355,11 +301,11 @@ function matchSingleCondition(condition, ctx) {
355
301
  }
356
302
  /**
357
303
  * Return a cached RegExp for a has/missing condition value string, compiling
358
- * on first use. Returns null if safeRegExp rejected the pattern or if the
359
- * value is not a valid regex (fall back to exact string comparison).
304
+ * on first use. Returns null if the value is not a valid regex (fall back to
305
+ * exact string comparison).
360
306
  */
361
307
  function _cachedConditionRegex(value) {
362
- return getCachedRegex(_compiledConditionCache, value, () => safeRegExp(`^${value}$`));
308
+ return getCachedRegex(_compiledConditionCache, value, () => compileRegExp(`^${value}$`));
363
309
  }
364
310
  /**
365
311
  * Check all has/missing conditions for a config rule.
@@ -384,125 +330,13 @@ function checkHasConditions(has, missing, ctx) {
384
330
  return collectConditionParams(has, missing, ctx) !== null;
385
331
  }
386
332
  /**
387
- * If the current position in `str` starts with a parenthesized group, consume
388
- * it and advance `re.lastIndex` past the closing `)`. Returns the group
389
- * contents or null if no group is present.
390
- */
391
- function extractConstraint(str, re) {
392
- if (str[re.lastIndex] !== "(") return null;
393
- const start = re.lastIndex + 1;
394
- let depth = 1;
395
- let i = start;
396
- while (i < str.length && depth > 0) {
397
- if (str[i] === "(") depth++;
398
- else if (str[i] === ")") depth--;
399
- i++;
400
- }
401
- if (depth !== 0) return null;
402
- re.lastIndex = i;
403
- return str.slice(start, i - 1);
404
- }
405
- /**
406
- * Match a Next.js config pattern (from redirects/rewrites sources) against a pathname.
407
- * Returns matched params or null.
408
- *
409
- * Supports:
410
- * :param - matches a single path segment
411
- * :param* - matches zero or more segments (catch-all)
412
- * :param+ - matches one or more segments
413
- * (regex) - inline regex patterns in the source
414
- * :param(constraint) - named param with inline regex constraint
415
- */
416
- /**
417
- * Strip a single trailing slash from a pathname for config-source matching.
418
- *
419
- * Next.js conditionally appends `(/)?` to rewrite/redirect/header source
420
- * regexes when `trailingSlash: true` (see Next.js
421
- * `resolve-rewrites.ts` and `server-utils.ts:checkRewrite`). Rather than
422
- * threading the trailingSlash flag through every matcher, we unconditionally
423
- * strip a trailing slash from the incoming pathname. When `trailingSlash: false`
424
- * the request pipeline emits a normalizing redirect (step 3) before config
425
- * rewrites/redirects (step 6) ever run, so the pathname is already slash-free;
426
- * the unconditional strip is defense-in-depth for that ordering. When
427
- * `trailingSlash: true` it bridges the gap between the canonicalized request
428
- * path (`/rewrite-1/`) and source patterns written without a trailing slash
429
- * (`/rewrite-1`).
430
- *
431
- * The root path `"/"` is preserved as-is.
333
+ * Strip one trailing slash, preserving the root path `"/"`. Compiled sources
334
+ * accept an optional trailing slash, so the redirect index looks up the
335
+ * slash-free form.
432
336
  */
433
337
  function stripTrailingSlashForConfigMatch(value) {
434
338
  return value.length > 1 && value.endsWith("/") ? value.slice(0, -1) : value;
435
339
  }
436
- function configPathEquals(a, b) {
437
- return a.toLowerCase() === b.toLowerCase();
438
- }
439
- function configPathStartsWith(pathname, prefix) {
440
- return pathname.slice(0, prefix.length).toLowerCase() === prefix.toLowerCase();
441
- }
442
- function matchConfigPattern(pathname, pattern) {
443
- const pathnameHadTrailingSlash = pathname.length > 1 && pathname.endsWith("/");
444
- pathname = stripTrailingSlashForConfigMatch(pathname);
445
- if (pathnameHadTrailingSlash) pattern = stripTrailingSlashForConfigMatch(pattern);
446
- const catchAllAnchor = /:[\w-]+[*+]/.test(pattern);
447
- const namedParamCount = (pattern.match(/:[\w-]+/g) || []).length;
448
- if (pattern.includes("(") || pattern.includes("\\") || /:[\w-]+[*+][^/]/.test(pattern) || /:[\w-]+\./.test(pattern) || /[^/]:[\w-]+/.test(pattern) || catchAllAnchor && namedParamCount > 1) try {
449
- const compiled = getCachedRegex(_compiledPatternCache, pattern, () => {
450
- const paramNames = [];
451
- let regexStr = "";
452
- const tokenRe = /:([\w-]+)|[.]|[^:.]+/g;
453
- let tok;
454
- while ((tok = tokenRe.exec(pattern)) !== null) if (tok[1] !== void 0) {
455
- const name = tok[1];
456
- const rest = pattern.slice(tokenRe.lastIndex);
457
- if (rest.startsWith("*") || rest.startsWith("+")) {
458
- const quantifier = rest[0];
459
- tokenRe.lastIndex += 1;
460
- const constraint = extractConstraint(pattern, tokenRe);
461
- paramNames.push(name);
462
- if (constraint !== null) regexStr += `(${constraint})`;
463
- else regexStr += quantifier === "*" ? "(.*)" : "(.+)";
464
- } else {
465
- const constraint = extractConstraint(pattern, tokenRe);
466
- paramNames.push(name);
467
- regexStr += constraint !== null ? `(${constraint})` : "([^/]+)";
468
- }
469
- } else if (tok[0] === ".") regexStr += "\\.";
470
- else regexStr += tok[0];
471
- const re = safeRegExp("^" + regexStr + "$", "i");
472
- return re ? {
473
- re,
474
- paramNames
475
- } : null;
476
- });
477
- if (!compiled) return null;
478
- const match = compiled.re.exec(pathname);
479
- if (!match) return null;
480
- const params = Object.create(null);
481
- for (let i = 0; i < compiled.paramNames.length; i++) params[compiled.paramNames[i]] = match[i + 1] ?? "";
482
- return params;
483
- } catch {}
484
- const catchAllMatch = pattern.match(/:([\w-]+)(\*|\+)$/);
485
- if (catchAllMatch) {
486
- const prefix = pattern.slice(0, pattern.lastIndexOf(":"));
487
- const paramName = catchAllMatch[1];
488
- const isPlus = catchAllMatch[2] === "+";
489
- const prefixNoSlash = prefix.replace(/\/$/, "");
490
- if (!configPathStartsWith(pathname, prefixNoSlash)) return null;
491
- const charAfter = pathname[prefixNoSlash.length];
492
- if (charAfter !== void 0 && charAfter !== "/") return null;
493
- const rest = pathname.slice(prefixNoSlash.length);
494
- if (isPlus && (!rest || rest === "/")) return null;
495
- let restValue = rest.startsWith("/") ? rest.slice(1) : rest;
496
- return { [paramName]: restValue };
497
- }
498
- const parts = pattern.split("/");
499
- const pathParts = pathname.split("/");
500
- if (parts.length !== pathParts.length) return null;
501
- const params = Object.create(null);
502
- for (let i = 0; i < parts.length; i++) if (parts[i].startsWith(":")) params[parts[i].slice(1)] = pathParts[i];
503
- else if (!configPathEquals(parts[i], pathParts[i])) return null;
504
- return params;
505
- }
506
340
  /**
507
341
  * Apply redirect rules from next.config.js.
508
342
  * Returns the redirect info if a redirect was matched, or null.
@@ -545,7 +379,7 @@ function matchRedirect(pathname, redirects, ctx, basePathState = _BASEPATH_DEFAU
545
379
  let localeMatch = null;
546
380
  let localeMatchIndex = Infinity;
547
381
  if (index.localeStatic.size > 0) {
548
- const noLocaleBucket = index.localeStatic.get(normalizedPathname.toLowerCase());
382
+ const noLocaleBucket = index.localeStatic.get(foldCase(normalizedPathname));
549
383
  if (noLocaleBucket) for (const entry of noLocaleBucket) {
550
384
  if (!entry.optional) continue;
551
385
  if (entry.originalIndex >= localeMatchIndex) continue;
@@ -565,7 +399,7 @@ function matchRedirect(pathname, redirects, ctx, basePathState = _BASEPATH_DEFAU
565
399
  if (slashTwo !== -1) {
566
400
  const suffix = normalizedPathname.slice(slashTwo);
567
401
  const localePart = normalizedPathname.slice(1, slashTwo);
568
- const localeBucket = index.localeStatic.get(suffix.toLowerCase());
402
+ const localeBucket = index.localeStatic.get(foldCase(suffix));
569
403
  if (localeBucket) for (const entry of localeBucket) {
570
404
  if (entry.originalIndex >= localeMatchIndex) continue;
571
405
  if (!entry.altRe.test(localePart)) continue;
@@ -610,17 +444,17 @@ function matchRedirect(pathname, redirects, ctx, basePathState = _BASEPATH_DEFAU
610
444
  function matchRewrite(pathname, rewrites, ctx, basePathState = _BASEPATH_DEFAULT, paramsPathname = pathname, onRuleSourceMatch) {
611
445
  for (const rewrite of rewrites) {
612
446
  if (!shouldEvaluateRule(rewrite.basePath, basePathState)) continue;
613
- const matchedParams = matchConfigPattern(pathname, rewrite.source);
614
- if (matchedParams) {
447
+ const matched = execConfigSource(pathname, rewrite.source);
448
+ if (matched) {
615
449
  onRuleSourceMatch?.(rewrite);
616
- const params = paramsPathname === pathname ? matchedParams : matchConfigPattern(paramsPathname, rewrite.source) ?? matchedParams;
450
+ const { params, unmatched } = paramsPathname === pathname ? matched : execConfigSource(paramsPathname, rewrite.source) ?? matched;
617
451
  const conditionParams = rewrite.has || rewrite.missing ? collectConditionParams(rewrite.has, rewrite.missing, ctx) : _emptyParams();
618
452
  if (!conditionParams) continue;
619
453
  const rewriteParams = {
620
454
  ...params,
621
455
  ...conditionParams
622
456
  };
623
- return substituteAndSanitizeRewriteDestination(serializeRewriteDestinationTemplate(rewrite.destination), rewriteParams);
457
+ return substituteAndSanitizeRewriteDestination(serializeRewriteDestinationTemplate(rewrite.destination), rewriteParams, unmatched.filter((key) => !(key in conditionParams)));
624
458
  }
625
459
  }
626
460
  return null;
@@ -668,16 +502,18 @@ function resolveRedirectMatch(redirect, params) {
668
502
  /**
669
503
  * Match Next.js's rewrite-specific prepareDestination behavior: source params
670
504
  * that are not consumed by the destination path/host are exposed to the target
671
- * page through query.
505
+ * page through query. Optional params that did not match are not params at
506
+ * all in Next.js, so they are neither appended nor count as consumed.
672
507
  *
673
508
  * https://github.com/vercel/next.js/blob/canary/packages/next/src/shared/lib/router/utils/prepare-destination.ts
674
509
  */
675
- function substituteAndSanitizeRewriteDestination(destination, params) {
510
+ function substituteAndSanitizeRewriteDestination(destination, params, unmatchedParams) {
676
511
  const rewritten = substituteAndSanitizeDestination(destination, params, "rewrite");
677
- if (!shouldAppendRewriteParamsToQuery(destination, params)) return rewritten;
512
+ const matchedParams = unmatchedParams.length === 0 ? params : Object.fromEntries(Object.entries(params).filter(([key]) => !unmatchedParams.includes(key)));
513
+ if (!shouldAppendRewriteParamsToQuery(destination, matchedParams)) return rewritten;
678
514
  const existingQueryKeys = getDestinationQueryKeys(destination);
679
515
  const paramsToAppend = [];
680
- for (const [key, value] of Object.entries(params)) {
516
+ for (const [key, value] of Object.entries(matchedParams)) {
681
517
  if (key === "nextInternalLocale" || existingQueryKeys.has(key)) continue;
682
518
  paramsToAppend.push([key, value]);
683
519
  }
@@ -893,23 +729,118 @@ async function proxyExternalRequest(request, externalUrl) {
893
729
  * when evaluating headers, so this parameter is required.
894
730
  */
895
731
  function matchHeaders(pathname, headers, ctx, basePathState = _BASEPATH_DEFAULT, onRuleSourceMatch) {
896
- const pathnameHadTrailingSlash = pathname.length > 1 && pathname.endsWith("/");
897
- pathname = stripTrailingSlashForConfigMatch(pathname);
898
732
  const result = [];
899
733
  for (const rule of headers) {
900
734
  if (!shouldEvaluateRule(rule.basePath, basePathState)) continue;
901
- const source = pathnameHadTrailingSlash ? stripTrailingSlashForConfigMatch(rule.source) : rule.source;
902
- const sourceRegex = getCachedRegex(_compiledHeaderSourceCache, source, () => safeRegExp("^" + escapeHeaderSource(source) + "$", "i"));
903
- if (sourceRegex && sourceRegex.test(pathname)) {
735
+ const compiledSource = compileConfigSource(rule.source);
736
+ const sourceMatch = compiledSource?.re.exec(pathname);
737
+ if (compiledSource && sourceMatch) {
904
738
  onRuleSourceMatch?.(rule);
905
739
  if (rule.has || rule.missing) {
906
740
  if (!checkHasConditions(rule.has, rule.missing, ctx)) continue;
907
741
  }
908
- result.push(...rule.headers);
742
+ if (!rule.headers.some((header) => header.key.includes(":") || header.value.includes(":"))) {
743
+ result.push(...rule.headers);
744
+ continue;
745
+ }
746
+ const params = collectHeaderSourceParams(sourceMatch, compiledSource.keys);
747
+ if (rule.has) collectHeaderHasParams(rule.has, ctx, params);
748
+ result.push(...substituteHeaderParams(rule.headers, params));
749
+ }
750
+ }
751
+ return result;
752
+ }
753
+ /**
754
+ * Collect source params like Next.js's `getPathMatch` with
755
+ * `removeUnnamedParams`: path-to-regexp's `regexpToFunction` keeps captures
756
+ * raw, splits `*`/`+` captures on the key's prefix and suffix, and omits
757
+ * groups that did not participate.
758
+ */
759
+ function collectHeaderSourceParams(match, keys) {
760
+ const params = _emptyParams();
761
+ for (let index = 1; index < match.length; index++) {
762
+ const capture = match[index];
763
+ const key = keys[index - 1];
764
+ if (capture === void 0 || !key || typeof key.name === "number") continue;
765
+ params[key.name] = key.modifier === "*" || key.modifier === "+" ? capture.split(key.prefix + key.suffix) : capture;
766
+ }
767
+ return params;
768
+ }
769
+ /**
770
+ * Add `has` captures like Next.js's `matchHas`, after checkHasConditions has
771
+ * accepted the rule: a condition without a value exposes the matched value
772
+ * under its key (letters only), a value regex exposes its named groups, and a
773
+ * host regex without named groups exposes the matched host as `host`.
774
+ *
775
+ * https://github.com/vercel/next.js/blob/v16.2.7/packages/next/src/shared/lib/router/utils/prepare-destination.ts
776
+ */
777
+ function collectHeaderHasParams(has, ctx, params) {
778
+ for (const condition of has) {
779
+ let value;
780
+ switch (condition.type) {
781
+ case "header":
782
+ value = ctx.headers.get(condition.key) ?? void 0;
783
+ break;
784
+ case "cookie":
785
+ value = Object.hasOwn(ctx.cookies, condition.key) ? ctx.cookies[condition.key] : void 0;
786
+ break;
787
+ case "query": {
788
+ const values = ctx.query.getAll(condition.key);
789
+ value = values.length > 1 ? values : values[0];
790
+ break;
791
+ }
792
+ case "host": value = ctx.host;
793
+ }
794
+ if (!value) continue;
795
+ if (!condition.value) {
796
+ if (condition.type !== "host") params[condition.key.replace(/[^A-Za-z]/g, "")] = value;
797
+ continue;
909
798
  }
799
+ const match = _cachedConditionRegex(condition.value)?.exec(Array.isArray(value) ? value[value.length - 1] : value);
800
+ if (!match) continue;
801
+ if (match.groups) Object.assign(params, match.groups);
802
+ else if (condition.type === "host" && match[0]) params.host = match[0];
803
+ }
804
+ }
805
+ const _warnedHeaderParamTemplates = /* @__PURE__ */ new WeakSet();
806
+ /**
807
+ * Substitute params into header keys and values like Next.js's
808
+ * `compileNonPath`, with params inserted verbatim. Source captures are the raw
809
+ * percent-encoded path text, as in Next.js, but `has` captures from decoded
810
+ * cookies or query values can carry characters a header cannot.
811
+ *
812
+ * Next.js fails the whole request when compiling throws (a repeated param
813
+ * used without `*`/`+`, or a has group that did not participate) or when the
814
+ * result is not a valid header. Requests can trigger both, so vinext instead
815
+ * omits only the affected header and logs once per header template.
816
+ */
817
+ function substituteHeaderParams(headers, params) {
818
+ if (Object.keys(params).length === 0) return headers;
819
+ const result = [];
820
+ for (const header of headers) {
821
+ let key;
822
+ let value;
823
+ try {
824
+ key = compileHeaderParamTemplate(header.key, params);
825
+ value = compileHeaderParamTemplate(header.value, params);
826
+ } catch (error) {
827
+ warnHeaderParamTemplate(header, error instanceof Error ? error.message : String(error));
828
+ continue;
829
+ }
830
+ if (key === header.key && value === header.value) result.push(header);
831
+ else if (isValidHeaderPair(key, value)) result.push({
832
+ key,
833
+ value
834
+ });
835
+ else warnHeaderParamTemplate(header, "the substituted header is not a valid HTTP header");
910
836
  }
911
837
  return result;
912
838
  }
839
+ function warnHeaderParamTemplate(header, reason) {
840
+ if (_warnedHeaderParamTemplates.has(header)) return;
841
+ _warnedHeaderParamTemplates.add(header);
842
+ console.warn(`[vinext] Omitting headers() header "${header.key}: ${header.value}" from a response: ${reason}.`);
843
+ }
913
844
  /**
914
845
  * Escape a string for inclusion in a regex character class / alternation.
915
846
  * Mirrors `escape-string-regexp` semantics used by Next.js's processRoutes.
@@ -931,17 +862,19 @@ function _escapeRegexString(value) {
931
862
  * starts with `/:nextInternalLocale(en|sv|nl)` so that locale-prefixed
932
863
  * URLs match. For redirects only, a second variant prefixed with
933
864
  * `/${defaultLocale}` is also emitted, matching Next.js exactly.
934
- * - **Vinext divergence**: we ALSO retain the original (unprefixed) source
935
- * so that requests for the default locale that arrive without a prefix
936
- * still match. Next.js solves this upstream by path-normalising every
937
- * incoming default-locale request to include the prefix
938
- * (`resolve-routes.ts` lines ~251-263); vinext currently does that
939
- * normalisation only inside the pages-server-entry route matcher, so
940
- * the rewrite/redirect matcher would otherwise miss unprefixed paths.
941
- * Keeping the unprefixed variant gives functionally identical behaviour
942
- * without requiring a server-wide path normalisation pass. The original
943
- * source is appended LAST so the locale-aware variants win when both
944
- * forms could match.
865
+ * - Headers get only that variant, exactly like Next.js. Every header
866
+ * matcher caller splices the (domain-aware) default locale into
867
+ * unprefixed request paths first (`normalizeDefaultLocalePathname`,
868
+ * mirroring `resolve-routes.ts`), so `/about` is matched as `/en/about`.
869
+ * `/_next/*` paths are left unprefixed and therefore only match
870
+ * `locale: false` rules, as in Next.js.
871
+ * - **Vinext divergence (redirects/rewrites only)**: we ALSO retain the
872
+ * original (unprefixed) source, appended LAST so the locale-aware
873
+ * variants win when both forms could match. Their callers normalise
874
+ * default-locale paths too, so this is a legacy fallback. Headers never
875
+ * get it: it would apply rules Next.js skips (`/((?!api).*)` would match
876
+ * `/fr/api/x` as `fr/api/x`), and default-locale requests would match
877
+ * both variants, appending additive headers such as `Set-Cookie` twice.
945
878
  *
946
879
  * Destinations that are local (start with `/`) are similarly rewritten with
947
880
  * `/:nextInternalLocale` for the locale-capture variant so the locale
@@ -980,9 +913,9 @@ function applyLocaleToRoutes(routes, i18n, type, options = {}) {
980
913
  };
981
914
  if ("destination" in internalRoute && internalDestination !== void 0) internalRoute.destination = internalDestination;
982
915
  out.push(internalRoute);
983
- out.push(r);
916
+ if (type !== "header") out.push(r);
984
917
  }
985
918
  return out;
986
919
  }
987
920
  //#endregion
988
- export { applyLocaleToRoutes, applyMiddlewareRequestHeaders, checkHasConditions, escapeHeaderSource, isExternalUrl, isSafeRegex, matchConfigPattern, matchHeaders, matchRedirect, matchRewrite, matchesRewriteSource, normalizeHost, parseCookies, preserveRedirectDestinationQuery, proxyExternalRequest, requestContextFromRequest, safeRegExp, sanitizeDestination };
921
+ export { applyLocaleToRoutes, applyMiddlewareRequestHeaders, checkHasConditions, configSourceError, isExternalUrl, matchConfigPattern, matchHeaders, matchRedirect, matchRewrite, matchesRewriteSource, normalizeHost, parseCookies, preserveRedirectDestinationQuery, proxyExternalRequest, requestContextFromRequest, sanitizeDestination };
@@ -0,0 +1,34 @@
1
+ import { PathKey } from "../utils/path-to-regexp.js";
2
+ //#region src/config/config-source.d.ts
3
+ export type CompiledConfigSource = {
4
+ re: RegExp;
5
+ /** Keys in capture-group order; unnamed groups have numeric names. */
6
+ keys: PathKey[];
7
+ };
8
+ /**
9
+ * Why `source` is not a valid redirects/rewrites/headers source, or null.
10
+ * Mirrors the source checks in Next.js's checkCustomRoutes, which fail the
11
+ * build; config loading calls this to do the same.
12
+ *
13
+ * https://github.com/vercel/next.js/blob/v16.2.7/packages/next/src/lib/load-custom-routes.ts
14
+ */
15
+ export declare function configSourceError(source: unknown): string | null;
16
+ export declare function compileConfigSource(source: string): CompiledConfigSource | null;
17
+ /**
18
+ * Match a Next.js config source (from redirects/rewrites/headers) against a
19
+ * pathname. Returns the named params or null.
20
+ *
21
+ * Unnamed groups are dropped, as Next.js does with `removeUnnamedParams`.
22
+ * Repeated params (`:path*`, `:path+`) keep the captured string (`a/b`), and
23
+ * an optional param that did not match is `""`.
24
+ */
25
+ export declare function matchConfigPattern(pathname: string, pattern: string): Record<string, string> | null;
26
+ /**
27
+ * matchConfigPattern, also reporting the optional params that did not match.
28
+ * Next.js leaves those out of its params entirely.
29
+ */
30
+ export declare function execConfigSource(pathname: string, source: string): {
31
+ params: Record<string, string>;
32
+ unmatched: string[];
33
+ } | null;
34
+ //#endregion