vinext 1.0.0-beta.1 → 1.0.0-beta.2

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 (175) hide show
  1. package/README.md +4 -1
  2. package/dist/build/client-build-config.js +1 -0
  3. package/dist/build/report.d.ts +8 -1
  4. package/dist/build/report.js +12 -2
  5. package/dist/check.js +15 -2
  6. package/dist/cli.js +3 -2
  7. package/dist/config/config-matchers.d.ts +13 -37
  8. package/dist/config/config-matchers.js +31 -121
  9. package/dist/config/next-config.d.ts +3 -2
  10. package/dist/config/next-config.js +3 -1
  11. package/dist/config/request-context.d.ts +14 -0
  12. package/dist/config/request-context.js +26 -0
  13. package/dist/entries/app-rsc-entry.js +27 -9
  14. package/dist/entries/app-rsc-manifest.js +17 -0
  15. package/dist/entries/pages-server-entry.js +3 -2
  16. package/dist/index.js +82 -58
  17. package/dist/init.d.ts +6 -4
  18. package/dist/init.js +20 -22
  19. package/dist/plugins/og-assets.js +1 -1
  20. package/dist/routing/app-route-graph.d.ts +13 -5
  21. package/dist/routing/app-route-graph.js +59 -13
  22. package/dist/routing/app-router.js +12 -6
  23. package/dist/routing/file-matcher.d.ts +11 -22
  24. package/dist/routing/file-matcher.js +21 -33
  25. package/dist/routing/pages-router.js +23 -29
  26. package/dist/routing/route-matching.d.ts +14 -1
  27. package/dist/routing/route-matching.js +13 -1
  28. package/dist/routing/route-pattern.d.ts +2 -1
  29. package/dist/routing/route-pattern.js +7 -4
  30. package/dist/routing/route-trie.d.ts +5 -1
  31. package/dist/routing/route-trie.js +5 -2
  32. package/dist/server/app-browser-entry.js +7 -6
  33. package/dist/server/app-browser-history-controller.d.ts +1 -1
  34. package/dist/server/app-browser-history-controller.js +3 -3
  35. package/dist/server/app-browser-navigation-controller.js +1 -1
  36. package/dist/server/app-browser-state.js +1 -1
  37. package/dist/server/app-browser-visible-commit.js +1 -1
  38. package/dist/server/app-fallback-renderer.d.ts +3 -0
  39. package/dist/server/app-fallback-renderer.js +4 -2
  40. package/dist/server/app-middleware.js +2 -1
  41. package/dist/server/app-page-boundary-render.d.ts +12 -1
  42. package/dist/server/app-page-boundary-render.js +84 -2
  43. package/dist/server/app-page-cache-finalizer.d.ts +1 -1
  44. package/dist/server/app-page-cache.d.ts +1 -1
  45. package/dist/server/app-page-dispatch.d.ts +11 -2
  46. package/dist/server/app-page-dispatch.js +14 -17
  47. package/dist/server/app-page-element-builder.d.ts +7 -2
  48. package/dist/server/app-page-element-builder.js +112 -27
  49. package/dist/server/app-page-head.d.ts +54 -2
  50. package/dist/server/app-page-head.js +164 -78
  51. package/dist/server/app-page-http-access-fallback-metadata.d.ts +29 -0
  52. package/dist/server/app-page-http-access-fallback-metadata.js +106 -0
  53. package/dist/server/app-page-render.d.ts +2 -2
  54. package/dist/server/app-page-render.js +1 -1
  55. package/dist/server/app-page-request.d.ts +12 -0
  56. package/dist/server/app-page-request.js +133 -13
  57. package/dist/server/app-page-route-wiring.d.ts +9 -0
  58. package/dist/server/app-page-route-wiring.js +43 -3
  59. package/dist/server/app-post-middleware-context.d.ts +1 -1
  60. package/dist/server/app-post-middleware-context.js +1 -1
  61. package/dist/server/app-route-handler-dispatch.js +31 -17
  62. package/dist/server/app-route-handler-execution.d.ts +6 -1
  63. package/dist/server/app-route-handler-execution.js +42 -16
  64. package/dist/server/app-route-handler-policy.d.ts +2 -0
  65. package/dist/server/app-route-handler-policy.js +3 -3
  66. package/dist/server/app-route-module-loader.d.ts +3 -1
  67. package/dist/server/app-route-module-loader.js +1 -0
  68. package/dist/server/app-rsc-cache-busting.d.ts +1 -1
  69. package/dist/server/app-rsc-errors.d.ts +1 -1
  70. package/dist/server/app-rsc-errors.js +12 -2
  71. package/dist/server/app-rsc-handler.d.ts +10 -5
  72. package/dist/server/app-rsc-handler.js +52 -24
  73. package/dist/server/app-rsc-request-normalization.d.ts +3 -2
  74. package/dist/server/app-rsc-request-normalization.js +8 -2
  75. package/dist/server/app-rsc-response-finalizer.d.ts +2 -2
  76. package/dist/server/app-rsc-response-finalizer.js +5 -11
  77. package/dist/server/app-rsc-route-matching.d.ts +20 -0
  78. package/dist/server/app-rsc-route-matching.js +87 -11
  79. package/dist/server/app-segment-config.js +13 -3
  80. package/dist/server/app-server-action-execution.d.ts +3 -1
  81. package/dist/server/app-server-action-execution.js +9 -7
  82. package/dist/server/app-ssr-entry.js +2 -2
  83. package/dist/server/app-ssr-stream.d.ts +1 -7
  84. package/dist/server/app-ssr-stream.js +2 -10
  85. package/dist/server/config-headers.d.ts +24 -0
  86. package/dist/server/config-headers.js +52 -0
  87. package/dist/server/dev-initial-server-error.d.ts +1 -1
  88. package/dist/server/dev-server.js +29 -13
  89. package/dist/server/isr-cache.d.ts +1 -1
  90. package/dist/server/middleware-matcher-pattern.d.ts +22 -0
  91. package/dist/server/middleware-matcher-pattern.js +219 -0
  92. package/dist/server/middleware-matcher.d.ts +2 -7
  93. package/dist/server/middleware-matcher.js +13 -60
  94. package/dist/server/middleware-path-to-regexp.d.ts +14 -0
  95. package/dist/server/middleware-path-to-regexp.js +228 -0
  96. package/dist/server/middleware-runtime.js +4 -3
  97. package/dist/server/middleware.d.ts +1 -1
  98. package/dist/server/middleware.js +2 -1
  99. package/dist/server/pages-data-route.d.ts +11 -1
  100. package/dist/server/pages-data-route.js +16 -1
  101. package/dist/server/pages-get-initial-props.d.ts +15 -2
  102. package/dist/server/pages-get-initial-props.js +16 -6
  103. package/dist/server/pages-page-data.d.ts +3 -1
  104. package/dist/server/pages-page-data.js +16 -7
  105. package/dist/server/pages-page-handler.d.ts +4 -2
  106. package/dist/server/pages-page-handler.js +16 -17
  107. package/dist/server/pages-page-response.js +1 -1
  108. package/dist/server/pages-readiness.d.ts +3 -3
  109. package/dist/server/pages-request-pipeline.d.ts +1 -0
  110. package/dist/server/pages-request-pipeline.js +25 -11
  111. package/dist/server/pages-router-entry.js +6 -0
  112. package/dist/server/prerender-route-params.js +2 -2
  113. package/dist/server/prod-server.d.ts +1 -4
  114. package/dist/server/prod-server.js +36 -23
  115. package/dist/server/request-pipeline.d.ts +15 -29
  116. package/dist/server/request-pipeline.js +24 -59
  117. package/dist/server/rsc-stream-hints.js +122 -18
  118. package/dist/shims/app.d.ts +1 -1
  119. package/dist/shims/cache-request-state.d.ts +27 -1
  120. package/dist/shims/cache-request-state.js +60 -1
  121. package/dist/shims/cache-runtime.d.ts +12 -1
  122. package/dist/shims/cache-runtime.js +22 -7
  123. package/dist/shims/cache.d.ts +5 -5
  124. package/dist/shims/cache.js +26 -10
  125. package/dist/shims/document.d.ts +42 -83
  126. package/dist/shims/document.js +62 -53
  127. package/dist/shims/dynamic.d.ts +5 -25
  128. package/dist/shims/dynamic.js +29 -8
  129. package/dist/shims/error.d.ts +1 -1
  130. package/dist/shims/error.js +1 -1
  131. package/dist/shims/fetch-cache.js +12 -3
  132. package/dist/shims/font-google-base.d.ts +11 -4
  133. package/dist/shims/font-local.d.ts +10 -3
  134. package/dist/shims/head.d.ts +13 -2
  135. package/dist/shims/head.js +3 -3
  136. package/dist/shims/headers.d.ts +28 -31
  137. package/dist/shims/headers.js +109 -49
  138. package/dist/shims/image.d.ts +27 -37
  139. package/dist/shims/image.js +21 -14
  140. package/dist/shims/internal/cookie-serialize.d.ts +3 -1
  141. package/dist/shims/internal/cookie-serialize.js +3 -1
  142. package/dist/shims/internal/hybrid-client-route-owner.js +3 -1
  143. package/dist/shims/internal/utils.d.ts +3 -3
  144. package/dist/shims/legacy-image.d.ts +3 -37
  145. package/dist/shims/legacy-image.js +6 -16
  146. package/dist/shims/link.d.ts +15 -22
  147. package/dist/shims/link.js +20 -11
  148. package/dist/shims/navigation-context-state.d.ts +1 -1
  149. package/dist/shims/navigation.d.ts +7 -3
  150. package/dist/shims/navigation.js +24 -13
  151. package/dist/shims/navigation.react-server.js +1 -1
  152. package/dist/shims/next-shims-augmentations.d.ts +1 -0
  153. package/dist/shims/next-shims-public.d.ts +1 -0
  154. package/dist/shims/public-shim-map.json.js +103 -0
  155. package/dist/shims/readonly-url-search-params.d.ts +3 -3
  156. package/dist/shims/readonly-url-search-params.js +3 -3
  157. package/dist/shims/request-state-types.d.ts +1 -1
  158. package/dist/shims/router.d.ts +55 -21
  159. package/dist/shims/router.js +162 -37
  160. package/dist/shims/script.d.ts +6 -10
  161. package/dist/shims/script.js +6 -3
  162. package/dist/shims/server.d.ts +45 -22
  163. package/dist/shims/server.js +44 -5
  164. package/dist/shims/unified-request-context.d.ts +1 -1
  165. package/dist/shims/unified-request-context.js +2 -0
  166. package/dist/shims/url-safety.d.ts +1 -1
  167. package/dist/shims/url-safety.js +2 -2
  168. package/dist/typegen.d.ts +8 -2
  169. package/dist/typegen.js +76 -15
  170. package/dist/utils/domain-locale.d.ts +2 -2
  171. package/dist/utils/external-url.d.ts +5 -0
  172. package/dist/utils/external-url.js +7 -0
  173. package/dist/utils/regex-safety.d.ts +8 -0
  174. package/dist/utils/regex-safety.js +737 -0
  175. package/package.json +13 -2
package/README.md CHANGED
@@ -206,7 +206,10 @@ vinext works everywhere. It natively supports Cloudflare Workers (with `npx @vin
206
206
  vinext is a Vite plugin that reimplements the public Next.js API — routing, server rendering, `next/*` module imports, the CLI — so you can run Next.js applications on Vite instead of the Next.js compiler toolchain. It can be deployed anywhere: Cloudflare Workers is the first natively supported target, with other platforms available via Nitro. Native adapters for more platforms are [planned](https://github.com/cloudflare/vinext/issues/80).
207
207
 
208
208
  **Is this a fork of Next.js?**
209
- No. vinext is an alternative implementation of the Next.js API surface built on Vite. It does import some Next.js types and utilities, but the core is written from scratch. The goal is not to create a competing framework or add features beyond what Next.js offers; it is to provide the same well-defined API surface on Vite's toolchain.
209
+ No. vinext is an alternative implementation of the Next.js API surface built on Vite. The core is written from scratch. The goal is not to create a competing framework or add features beyond what Next.js offers; it is to provide the same well-defined API surface on Vite's toolchain.
210
+
211
+ **Does vinext require Next.js to be installed?**
212
+ No. vinext ships fallback declarations for the supported `next` and `next/*` APIs, so applications can run and type-check without the `next` package. If both packages are installed, vinext keeps using Next.js's authoritative types and adds only its own extensions. Compatibility features that consume Next.js internals, such as `styled-jsx`, may still require a matching Next.js installation when used.
210
213
 
211
214
  **How is this different from OpenNext?**
212
215
  [OpenNext](https://opennext.js.org/) adapts the _output_ of a standard `next build` to run on various platforms. Because it builds on Next.js's own output, it inherits broad API coverage and has been well-tested for much longer. vinext takes a different approach: it reimplements the Next.js APIs on Vite from scratch, which means faster builds and smaller bundles, but less coverage of the long tail of Next.js features. If you need a mature, well-tested way to run Next.js outside Vercel, OpenNext is the safer choice. If you want a lighter Vite-based toolchain and do not need every Next.js API, vinext may be a good fit.
@@ -6,6 +6,7 @@ const ROUTE_OWNED_CLIENT_SHIMS = /* @__PURE__ */ new Set([
6
6
  "dynamic-preload-chunks",
7
7
  "form",
8
8
  "image",
9
+ "internal/hybrid-client-route-owner",
9
10
  "layout-segment-context",
10
11
  "legacy-image",
11
12
  "link",
@@ -44,6 +44,13 @@ declare function hasExportedName(code: string, name: string): boolean;
44
44
  */
45
45
  declare function extractExportConstString(code: string, name: string): string | null;
46
46
  declare function extractMiddlewareMatcherConfig(filePath: string): StaticMiddlewareMatcher | undefined;
47
+ /**
48
+ * Extract the statically analyzable `config.matcher` value without first
49
+ * narrowing it to vinext's runtime matcher type. Build validation needs the
50
+ * raw value so malformed matcher objects are rejected instead of disappearing
51
+ * as though no matcher had been configured.
52
+ */
53
+ declare function extractMiddlewareMatcherConfigValue(filePath: string): unknown;
47
54
  /**
48
55
  * Extracts the numeric value of `export const <name> = <number|false>`.
49
56
  * Supports integers, decimals, negative values, `Infinity`, and `false`.
@@ -137,4 +144,4 @@ declare function printBuildReport(options: {
137
144
  prerenderResult?: PrerenderResult;
138
145
  }): Promise<void>;
139
146
  //#endregion
140
- export { RouteRow, RouteType, StaticMiddlewareMatcher, buildReportRows, classifyAppRoute, classifyLayoutSegmentConfig, classifyPagesRoute, extractExportConstNumber, extractExportConstString, extractGetStaticPropsRevalidate, extractMiddlewareMatcherConfig, formatBuildReport, getAppRouteRenderEntryPath, hasExportedName, hasNamedExport, printBuildReport };
147
+ export { RouteRow, RouteType, StaticMiddlewareMatcher, buildReportRows, classifyAppRoute, classifyLayoutSegmentConfig, classifyPagesRoute, extractExportConstNumber, extractExportConstString, extractGetStaticPropsRevalidate, extractMiddlewareMatcherConfig, extractMiddlewareMatcherConfigValue, formatBuildReport, getAppRouteRenderEntryPath, hasExportedName, hasNamedExport, printBuildReport };
@@ -135,6 +135,16 @@ function extractStringFromConstInitializer(initializer) {
135
135
  return null;
136
136
  }
137
137
  function extractMiddlewareMatcherConfig(filePath) {
138
+ const value = extractMiddlewareMatcherConfigValue(filePath);
139
+ return isStaticMiddlewareMatcher(value) ? value : void 0;
140
+ }
141
+ /**
142
+ * Extract the statically analyzable `config.matcher` value without first
143
+ * narrowing it to vinext's runtime matcher type. Build validation needs the
144
+ * raw value so malformed matcher objects are rejected instead of disappearing
145
+ * as though no matcher had been configured.
146
+ */
147
+ function extractMiddlewareMatcherConfigValue(filePath) {
138
148
  let code;
139
149
  try {
140
150
  code = fs.readFileSync(filePath, "utf8");
@@ -148,7 +158,7 @@ function extractMiddlewareMatcherConfig(filePath) {
148
158
  const matcherExpression = objectPropertyValue(config, "matcher");
149
159
  if (!matcherExpression) return void 0;
150
160
  const value = extractStaticJsonValue(matcherExpression);
151
- return isStaticMiddlewareMatcher(value) ? value : void 0;
161
+ return value === UNSUPPORTED_STATIC_VALUE ? void 0 : value;
152
162
  }
153
163
  function objectPropertyValue(object, key) {
154
164
  for (const property of object.properties) {
@@ -596,4 +606,4 @@ async function printBuildReport(options) {
596
606
  }
597
607
  }
598
608
  //#endregion
599
- export { buildReportRows, classifyAppRoute, classifyLayoutSegmentConfig, classifyPagesRoute, extractExportConstNumber, extractExportConstString, extractGetStaticPropsRevalidate, extractMiddlewareMatcherConfig, formatBuildReport, getAppRouteRenderEntryPath, hasExportedName, hasNamedExport, printBuildReport };
609
+ export { buildReportRows, classifyAppRoute, classifyLayoutSegmentConfig, classifyPagesRoute, extractExportConstNumber, extractExportConstString, extractGetStaticPropsRevalidate, extractMiddlewareMatcherConfig, extractMiddlewareMatcherConfigValue, formatBuildReport, getAppRouteRenderEntryPath, hasExportedName, hasNamedExport, printBuildReport };
package/dist/check.js CHANGED
@@ -390,6 +390,19 @@ function findSourceFiles(dir, extensions = [
390
390
  }
391
391
  return results;
392
392
  }
393
+ /**
394
+ * Find files that can contribute to the application compatibility surface.
395
+ * Test modules and test-runner configuration are executed by their own runners
396
+ * rather than bundled into the vinext application, so reporting their imports
397
+ * or CJS globals as migration blockers produces false positives.
398
+ */
399
+ function findRuntimeSourceFiles(root) {
400
+ return findSourceFiles(root).filter((file) => {
401
+ const basename = path.basename(file);
402
+ const isTestRunnerConfig = /^(?:jest|playwright|vitest)\.config\.[cm]?[jt]sx?$/.test(basename);
403
+ return !/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(basename) && !isTestRunnerConfig;
404
+ });
405
+ }
393
406
  function isIdentStart(c) {
394
407
  return c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "_" || c === "$";
395
408
  }
@@ -588,7 +601,7 @@ function hasFreeCjsGlobal(content) {
588
601
  * Scan source files for `import ... from 'next/...'` statements.
589
602
  */
590
603
  function scanImports(root) {
591
- const files = findSourceFiles(root);
604
+ const files = findRuntimeSourceFiles(root);
592
605
  const importUsage = /* @__PURE__ */ new Map();
593
606
  const importRegex = /(?:import\s+(?:[\w{},\s*]+\s+from\s+)?|require\s*\()['"]([^'"]+)['"]\)?/g;
594
607
  const typeOnlyImportRegex = /import\s+type\s+/;
@@ -911,7 +924,7 @@ function checkConventions(root) {
911
924
  detail: "required for Vite — vinext init will add it automatically"
912
925
  });
913
926
  }
914
- const allSourceFiles = findSourceFiles(root);
927
+ const allSourceFiles = findRuntimeSourceFiles(root);
915
928
  const viewTransitionRegex = /import\s+\{[^}]*\bViewTransition\b[^}]*\}\s+from\s+['"]react['"]/;
916
929
  const viewTransitionFiles = [];
917
930
  const cjsGlobalFiles = [];
package/dist/cli.js CHANGED
@@ -482,11 +482,12 @@ async function typegen() {
482
482
  root,
483
483
  mode: "production"
484
484
  });
485
- const outputPath = await generateRouteTypes({
485
+ const result = await generateRouteTypes({
486
486
  root,
487
487
  pageExtensions: (await resolveNextConfig(await loadNextConfig(root, PHASE_PRODUCTION_BUILD), root)).pageExtensions
488
488
  });
489
- console.log(`\n Generated route types at ${path.relative(root, outputPath)}\n`);
489
+ const nextEnvMessage = result.nextEnvStatus === "unchanged" ? `${path.relative(root, result.nextEnvPath)} is up to date` : `${result.nextEnvStatus === "created" ? "Created" : "Updated"} ${path.relative(root, result.nextEnvPath)}`;
490
+ console.log(`\n Generated route types at ${path.relative(root, result.routeTypesPath)}\n ${nextEnvMessage}\n`);
490
491
  }
491
492
  async function initCommand() {
492
493
  const parsed = parseArgs(rawArgs);
@@ -1,17 +1,20 @@
1
1
  import { HasCondition, NextHeader, NextI18nConfig, NextRedirect, NextRewrite } from "./next-config.js";
2
+ import { RequestContext, normalizeHost, parseCookies, requestContextFromRequest } from "./request-context.js";
3
+ import { isExternalUrl } from "../utils/external-url.js";
2
4
 
3
5
  //#region src/config/config-matchers.d.ts
4
6
  /**
5
7
  * Detect regex patterns vulnerable to catastrophic backtracking (ReDoS).
6
8
  *
7
- * Uses a lightweight heuristic: scans the pattern string for nested quantifiers
8
- * (a quantifier applied to a group that itself contains a quantifier). This
9
- * catches the most common pathological patterns like `(a+)+`, `(.*)*`,
10
- * `([^/]+)+`, `(a|a+)+` without needing a full regex parser.
9
+ * Uses the same deterministic structural analysis as middleware matcher
10
+ * validation. Nested bounded repetition is accepted only when its repeated
11
+ * language has fixed width and unambiguous branches; a fixed outer count can
12
+ * otherwise still cause polynomially catastrophic backtracking on long near
13
+ * misses.
11
14
  *
12
15
  * Returns true if the pattern appears safe, false if it's potentially dangerous.
13
16
  */
14
- declare function isSafeRegex(pattern: string): boolean;
17
+ declare function isSafeRegex(pattern: string, flags?: string): boolean;
15
18
  /**
16
19
  * Compile a regex pattern safely. Returns the compiled RegExp or null if the
17
20
  * pattern is invalid or vulnerable to ReDoS.
@@ -27,16 +30,6 @@ declare function safeRegExp(pattern: string, flags?: string): RegExp | null;
27
30
  * which CodeQL flags as incomplete sanitization), then groups are restored.
28
31
  */
29
32
  declare function escapeHeaderSource(source: string): string;
30
- /**
31
- * Request context needed for evaluating has/missing conditions.
32
- * Callers extract the relevant parts from the incoming Request.
33
- */
34
- type RequestContext = {
35
- readonly headers: Headers;
36
- readonly cookies: Record<string, string>;
37
- readonly query: URLSearchParams;
38
- readonly host: string;
39
- };
40
33
  /**
41
34
  * basePath gating state passed alongside the pathname to every matcher.
42
35
  *
@@ -59,22 +52,6 @@ type BasePathMatchState = {
59
52
  */
60
53
  hadBasePath: boolean;
61
54
  };
62
- /**
63
- * Parse a Cookie header string into a key-value record.
64
- */
65
- declare function parseCookies(cookieHeader: string | null): Record<string, string>;
66
- /**
67
- * Build a RequestContext from a Web Request object.
68
- *
69
- * `cookies` and `query` are lazy memoized getters: they are consumed only by
70
- * `has`/`missing` condition evaluation (`checkHasConditions` /
71
- * `matchesRuleConditions`), and most apps configure no such conditions. The
72
- * cookie split and `searchParams` access are therefore deferred until first
73
- * read and computed at most once. Mirrors `headersContextFromRequest` in
74
- * `shims/headers.ts`.
75
- */
76
- declare function requestContextFromRequest(request: Request): RequestContext;
77
- declare function normalizeHost(hostHeader: string | null, fallbackHostname: string): string;
78
55
  /**
79
56
  * Unpack `x-middleware-request-*` headers from the collected middleware
80
57
  * response headers into the actual request, and strip all `x-middleware-*`
@@ -147,7 +124,7 @@ declare function matchRedirect(pathname: string, redirects: NextRedirect[], ctx:
147
124
  * to evaluate has/missing conditions. Next.js always has request context
148
125
  * when evaluating rewrites, so this parameter is required.
149
126
  */
150
- declare function matchRewrite(pathname: string, rewrites: NextRewrite[], ctx: RequestContext, basePathState?: BasePathMatchState): string | null;
127
+ declare function matchRewrite(pathname: string, rewrites: NextRewrite[], ctx: RequestContext, basePathState?: BasePathMatchState, paramsPathname?: string): string | null;
151
128
  /**
152
129
  * Check whether a rewrite source can match a pathname without evaluating its
153
130
  * request-dependent `has` / `missing` conditions.
@@ -175,7 +152,6 @@ declare function sanitizeDestination(dest: string): string;
175
152
  * Detects any URL scheme (http:, https:, data:, javascript:, blob:, etc.)
176
153
  * per RFC 3986, plus protocol-relative URLs (//).
177
154
  */
178
- declare function isExternalUrl(url: string): boolean;
179
155
  /**
180
156
  * Merge the original request's query params into a config-redirect
181
157
  * destination, preserving them on the resulting `Location`.
@@ -218,8 +194,8 @@ declare function matchHeaders(pathname: string, headers: NextHeader[], ctx: Requ
218
194
  value: string;
219
195
  }>;
220
196
  /**
221
- * Apply Next.js i18n locale-prefix transformation to a set of redirect or
222
- * rewrite rules. Mirrors the relevant slice of Next.js's `processRoutes`
197
+ * Apply Next.js i18n locale-prefix transformation to a set of redirect,
198
+ * rewrite, or header rules. Mirrors the relevant slice of Next.js's `processRoutes`
223
199
  * (load-custom-routes.ts) with one deliberate divergence noted below.
224
200
  *
225
201
  * For each rule:
@@ -250,8 +226,8 @@ declare function matchHeaders(pathname: string, headers: NextHeader[], ctx: Requ
250
226
  * Mirrors the Next.js reference in
251
227
  * packages/next/src/lib/load-custom-routes.ts — see `processRoutes`.
252
228
  */
253
- declare function applyLocaleToRoutes<T extends NextRedirect | NextRewrite>(routes: T[], i18n: NextI18nConfig | null | undefined, type: "redirect" | "rewrite", options?: {
229
+ declare function applyLocaleToRoutes<T extends NextRedirect | NextRewrite | NextHeader>(routes: T[], i18n: NextI18nConfig | null | undefined, type: "redirect" | "rewrite" | "header", options?: {
254
230
  trailingSlash?: boolean;
255
231
  }): T[];
256
232
  //#endregion
257
- export { BasePathMatchState, RequestContext, applyLocaleToRoutes, applyMiddlewareRequestHeaders, checkHasConditions, escapeHeaderSource, isExternalUrl, isSafeRegex, matchConfigPattern, matchHeaders, matchRedirect, matchRewrite, matchesRewriteSource, normalizeHost, parseCookies, preserveRedirectDestinationQuery, proxyExternalRequest, requestContextFromRequest, safeRegExp, sanitizeDestination };
233
+ export { BasePathMatchState, type RequestContext, applyLocaleToRoutes, applyMiddlewareRequestHeaders, checkHasConditions, escapeHeaderSource, isExternalUrl, isSafeRegex, matchConfigPattern, matchHeaders, matchRedirect, matchRewrite, matchesRewriteSource, normalizeHost, parseCookies, preserveRedirectDestinationQuery, proxyExternalRequest, requestContextFromRequest, safeRegExp, sanitizeDestination };
@@ -1,6 +1,8 @@
1
1
  import { VINEXT_MW_CTX_HEADER, VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, VINEXT_PRERENDER_SECRET_HEADER } from "../utils/protocol-headers.js";
2
2
  import { buildRequestHeadersFromMiddlewareResponse } from "../utils/middleware-request-headers.js";
3
- import { parseCookieHeader } from "../utils/parse-cookie.js";
3
+ import { analyzeRegexSafety } from "../utils/regex-safety.js";
4
+ import { normalizeHost, parseCookies, requestContextFromRequest } from "./request-context.js";
5
+ import { isExternalUrl } from "../utils/external-url.js";
4
6
  //#region src/config/config-matchers.ts
5
7
  /**
6
8
  * Cache for compiled regex patterns in matchConfigPattern.
@@ -191,75 +193,16 @@ function stripHopByHopRequestHeaders(headers) {
191
193
  /**
192
194
  * Detect regex patterns vulnerable to catastrophic backtracking (ReDoS).
193
195
  *
194
- * Uses a lightweight heuristic: scans the pattern string for nested quantifiers
195
- * (a quantifier applied to a group that itself contains a quantifier). This
196
- * catches the most common pathological patterns like `(a+)+`, `(.*)*`,
197
- * `([^/]+)+`, `(a|a+)+` without needing a full regex parser.
196
+ * Uses the same deterministic structural analysis as middleware matcher
197
+ * validation. Nested bounded repetition is accepted only when its repeated
198
+ * language has fixed width and unambiguous branches; a fixed outer count can
199
+ * otherwise still cause polynomially catastrophic backtracking on long near
200
+ * misses.
198
201
  *
199
202
  * Returns true if the pattern appears safe, false if it's potentially dangerous.
200
203
  */
201
- function isSafeRegex(pattern) {
202
- const quantifierAtDepth = [];
203
- let depth = 0;
204
- let i = 0;
205
- while (i < pattern.length) {
206
- const ch = pattern[i];
207
- if (ch === "\\") {
208
- i += 2;
209
- continue;
210
- }
211
- if (ch === "[") {
212
- i++;
213
- while (i < pattern.length && pattern[i] !== "]") {
214
- if (pattern[i] === "\\") i++;
215
- i++;
216
- }
217
- i++;
218
- continue;
219
- }
220
- if (ch === "(") {
221
- depth++;
222
- if (quantifierAtDepth.length <= depth) quantifierAtDepth.push(false);
223
- else quantifierAtDepth[depth] = false;
224
- i++;
225
- continue;
226
- }
227
- if (ch === ")") {
228
- const hadQuantifier = depth > 0 && quantifierAtDepth[depth];
229
- if (depth > 0) depth--;
230
- const next = pattern[i + 1];
231
- if (next === "+" || next === "*" || next === "{") {
232
- if (hadQuantifier) return false;
233
- if (depth >= 0 && depth < quantifierAtDepth.length) quantifierAtDepth[depth] = true;
234
- }
235
- i++;
236
- continue;
237
- }
238
- if (ch === "+" || ch === "*") {
239
- if (depth > 0) quantifierAtDepth[depth] = true;
240
- i++;
241
- continue;
242
- }
243
- if (ch === "?") {
244
- const prev = i > 0 ? pattern[i - 1] : "";
245
- if (prev !== "+" && prev !== "*" && prev !== "?" && prev !== "}") {
246
- if (depth > 0) quantifierAtDepth[depth] = true;
247
- }
248
- i++;
249
- continue;
250
- }
251
- if (ch === "{") {
252
- let j = i + 1;
253
- while (j < pattern.length && /[\d,]/.test(pattern[j])) j++;
254
- if (j < pattern.length && pattern[j] === "}" && j > i + 1) {
255
- if (depth > 0) quantifierAtDepth[depth] = true;
256
- i = j + 1;
257
- continue;
258
- }
259
- }
260
- i++;
261
- }
262
- return true;
204
+ function isSafeRegex(pattern, flags) {
205
+ return analyzeRegexSafety(pattern, { ignoreCase: flags?.includes("i") }) === null;
263
206
  }
264
207
  /**
265
208
  * Compile a regex pattern safely. Returns the compiled RegExp or null if the
@@ -268,8 +211,8 @@ function isSafeRegex(pattern) {
268
211
  * Logs a warning when a pattern is rejected so developers can fix their config.
269
212
  */
270
213
  function safeRegExp(pattern, flags) {
271
- if (!isSafeRegex(pattern)) {
272
- console.warn(`[vinext] Rejecting potentially unsafe regex pattern (ReDoS risk): ${pattern}\n Patterns with nested quantifiers (e.g. (a+)+) can cause catastrophic backtracking.\n Simplify the pattern to avoid nested repetition.`);
214
+ if (!isSafeRegex(pattern, flags)) {
215
+ 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.`);
273
216
  return null;
274
217
  }
275
218
  try {
@@ -342,40 +285,6 @@ function shouldEvaluateRule(ruleBasePath, state) {
342
285
  return ruleBasePath === false ? !state.hadBasePath : state.hadBasePath;
343
286
  }
344
287
  /**
345
- * Parse a Cookie header string into a key-value record.
346
- */
347
- function parseCookies(cookieHeader) {
348
- return parseCookieHeader(cookieHeader);
349
- }
350
- /**
351
- * Build a RequestContext from a Web Request object.
352
- *
353
- * `cookies` and `query` are lazy memoized getters: they are consumed only by
354
- * `has`/`missing` condition evaluation (`checkHasConditions` /
355
- * `matchesRuleConditions`), and most apps configure no such conditions. The
356
- * cookie split and `searchParams` access are therefore deferred until first
357
- * read and computed at most once. Mirrors `headersContextFromRequest` in
358
- * `shims/headers.ts`.
359
- */
360
- function requestContextFromRequest(request) {
361
- const url = new URL(request.url);
362
- let cookies;
363
- let query;
364
- return {
365
- headers: request.headers,
366
- get cookies() {
367
- return cookies ??= parseCookies(request.headers.get("cookie"));
368
- },
369
- get query() {
370
- return query ??= url.searchParams;
371
- },
372
- host: normalizeHost(request.headers.get("host"), url.hostname)
373
- };
374
- }
375
- function normalizeHost(hostHeader, fallbackHostname) {
376
- return (hostHeader ?? fallbackHostname).split(":", 1)[0].toLowerCase();
377
- }
378
- /**
379
288
  * Unpack `x-middleware-request-*` headers from the collected middleware
380
289
  * response headers into the actual request, and strip all `x-middleware-*`
381
290
  * internal signals so they never reach clients.
@@ -410,7 +319,7 @@ function _emptyParams() {
410
319
  return Object.create(null);
411
320
  }
412
321
  function _matchConditionValue(actualValue, expectedValue) {
413
- if (expectedValue === void 0) return _emptyParams();
322
+ if (!expectedValue) return actualValue ? _emptyParams() : null;
414
323
  const re = _cachedConditionRegex(expectedValue);
415
324
  if (re) {
416
325
  const match = re.exec(actualValue);
@@ -440,9 +349,10 @@ function matchSingleCondition(condition, ctx) {
440
349
  return _matchConditionValue(cookieValue, condition.value);
441
350
  }
442
351
  case "query": {
443
- const queryValue = ctx.query.get(condition.key);
444
- if (queryValue === null) return null;
445
- return _matchConditionValue(queryValue, condition.value);
352
+ const queryValues = ctx.query.getAll(condition.key);
353
+ if (queryValues.length === 0) return null;
354
+ if (!condition.value && queryValues.length > 1) return _emptyParams();
355
+ return _matchConditionValue(queryValues[queryValues.length - 1], condition.value);
446
356
  }
447
357
  case "host":
448
358
  if (condition.value !== void 0) return _matchConditionValue(ctx.host, condition.value);
@@ -710,11 +620,12 @@ function matchRedirect(pathname, redirects, ctx, basePathState = _BASEPATH_DEFAU
710
620
  * to evaluate has/missing conditions. Next.js always has request context
711
621
  * when evaluating rewrites, so this parameter is required.
712
622
  */
713
- function matchRewrite(pathname, rewrites, ctx, basePathState = _BASEPATH_DEFAULT) {
623
+ function matchRewrite(pathname, rewrites, ctx, basePathState = _BASEPATH_DEFAULT, paramsPathname = pathname) {
714
624
  for (const rewrite of rewrites) {
715
625
  if (!shouldEvaluateRule(rewrite.basePath, basePathState)) continue;
716
- const params = matchConfigPattern(pathname, rewrite.source);
717
- if (params) {
626
+ const matchedParams = matchConfigPattern(pathname, rewrite.source);
627
+ if (matchedParams) {
628
+ const params = paramsPathname === pathname ? matchedParams : matchConfigPattern(paramsPathname, rewrite.source) ?? matchedParams;
718
629
  const conditionParams = rewrite.has || rewrite.missing ? collectConditionParams(rewrite.has, rewrite.missing, ctx) : _emptyParams();
719
630
  if (!conditionParams) continue;
720
631
  const rewriteParams = {
@@ -868,9 +779,6 @@ function sanitizeDestination(dest) {
868
779
  * Detects any URL scheme (http:, https:, data:, javascript:, blob:, etc.)
869
780
  * per RFC 3986, plus protocol-relative URLs (//).
870
781
  */
871
- function isExternalUrl(url) {
872
- return /^[a-z][a-z0-9+.-]*:/i.test(url) || url.startsWith("//");
873
- }
874
782
  /**
875
783
  * Merge the original request's query params into a config-redirect
876
784
  * destination, preserving them on the resulting `Location`.
@@ -1004,8 +912,8 @@ function _escapeRegexString(value) {
1004
912
  return value.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
1005
913
  }
1006
914
  /**
1007
- * Apply Next.js i18n locale-prefix transformation to a set of redirect or
1008
- * rewrite rules. Mirrors the relevant slice of Next.js's `processRoutes`
915
+ * Apply Next.js i18n locale-prefix transformation to a set of redirect,
916
+ * rewrite, or header rules. Mirrors the relevant slice of Next.js's `processRoutes`
1009
917
  * (load-custom-routes.ts) with one deliberate divergence noted below.
1010
918
  *
1011
919
  * For each rule:
@@ -1048,7 +956,8 @@ function applyLocaleToRoutes(routes, i18n, type, options = {}) {
1048
956
  out.push(r);
1049
957
  continue;
1050
958
  }
1051
- const isExternal = !!r.destination && !r.destination.startsWith("/");
959
+ const destination = "destination" in r ? r.destination : void 0;
960
+ const isExternal = !!destination && !destination.startsWith("/");
1052
961
  if (!isExternal) for (const locale of defaultLocales) {
1053
962
  const localizedSource = `/${locale}${suffixFor(r.source)}`;
1054
963
  out.push({
@@ -1057,13 +966,14 @@ function applyLocaleToRoutes(routes, i18n, type, options = {}) {
1057
966
  });
1058
967
  }
1059
968
  const internalSource = `${internalLocale}${suffixFor(r.source)}`;
1060
- let internalDestination = r.destination;
969
+ let internalDestination = destination;
1061
970
  if (internalDestination && internalDestination.startsWith("/") && !isExternal) internalDestination = `/:nextInternalLocale${internalDestination === "/" && !trailingSlash ? "" : internalDestination}`;
1062
- out.push({
971
+ const internalRoute = {
1063
972
  ...r,
1064
- source: internalSource,
1065
- destination: internalDestination
1066
- });
973
+ source: internalSource
974
+ };
975
+ if ("destination" in internalRoute && internalDestination !== void 0) internalRoute.destination = internalDestination;
976
+ out.push(internalRoute);
1067
977
  out.push(r);
1068
978
  }
1069
979
  return out;
@@ -54,7 +54,8 @@ type NextHeader = {
54
54
  key: string;
55
55
  value: string;
56
56
  }>; /** See {@link NextRedirect.basePath}. */
57
- basePath?: false;
57
+ basePath?: false; /** See {@link NextRedirect.locale}. */
58
+ locale?: false;
58
59
  };
59
60
  type NextI18nConfig = {
60
61
  /** List of supported locales */locales: string[]; /** The default locale (used when no locale prefix is in the URL) */
@@ -71,7 +72,7 @@ type NextI18nConfig = {
71
72
  domain: string;
72
73
  defaultLocale: string;
73
74
  locales?: string[];
74
- http?: boolean;
75
+ http?: true;
75
76
  }>;
76
77
  };
77
78
  /**
@@ -1,6 +1,7 @@
1
1
  import path, { toSlash } from "../deps/.pnpm/pathslash@0.1.0/deps/pathslash/dist/index.js";
2
2
  import { normalizePageExtensions } from "../routing/file-matcher.js";
3
- import { applyLocaleToRoutes, isExternalUrl } from "./config-matchers.js";
3
+ import { isExternalUrl } from "../utils/external-url.js";
4
+ import { applyLocaleToRoutes } from "./config-matchers.js";
4
5
  import { isUnknownRecord } from "../utils/record.js";
5
6
  import { getHtmlLimitedBotRegex } from "../utils/html-limited-bots.js";
6
7
  import { flattenPluginOptions } from "../utils/plugin-options.js";
@@ -815,6 +816,7 @@ async function resolveNextConfig(config, root = toSlash(process.cwd()), options
815
816
  afterFiles: applyLocaleToRoutes(rewrites.afterFiles, i18n, "rewrite", opts),
816
817
  fallback: applyLocaleToRoutes(rewrites.fallback, i18n, "rewrite", opts)
817
818
  };
819
+ headers = applyLocaleToRoutes(headers, i18n, "header", opts);
818
820
  }
819
821
  const images = config.images ? {
820
822
  ...config.images,
@@ -0,0 +1,14 @@
1
+ //#region src/config/request-context.d.ts
2
+ /** Request context needed for evaluating has/missing conditions. */
3
+ type RequestContext = {
4
+ readonly headers: Headers;
5
+ readonly cookies: Record<string, string>;
6
+ readonly query: URLSearchParams;
7
+ readonly host: string;
8
+ };
9
+ declare function parseCookies(cookieHeader: string | null): Record<string, string>;
10
+ declare function normalizeHost(hostHeader: string | null, fallbackHostname: string): string;
11
+ /** Build a lazily parsed request context from a Web Request. */
12
+ declare function requestContextFromRequest(request: Request): RequestContext;
13
+ //#endregion
14
+ export { RequestContext, normalizeHost, parseCookies, requestContextFromRequest };
@@ -0,0 +1,26 @@
1
+ import { parseCookieHeader } from "../utils/parse-cookie.js";
2
+ //#region src/config/request-context.ts
3
+ function parseCookies(cookieHeader) {
4
+ return parseCookieHeader(cookieHeader);
5
+ }
6
+ function normalizeHost(hostHeader, fallbackHostname) {
7
+ return (hostHeader ?? fallbackHostname).split(":", 1)[0].toLowerCase();
8
+ }
9
+ /** Build a lazily parsed request context from a Web Request. */
10
+ function requestContextFromRequest(request) {
11
+ const url = new URL(request.url);
12
+ let cookies;
13
+ let query;
14
+ return {
15
+ headers: request.headers,
16
+ get cookies() {
17
+ return cookies ??= parseCookies(request.headers.get("cookie"));
18
+ },
19
+ get query() {
20
+ return query ??= url.searchParams;
21
+ },
22
+ host: normalizeHost(request.headers.get("host"), url.hostname)
23
+ };
24
+ }
25
+ //#endregion
26
+ export { normalizeHost, parseCookies, requestContextFromRequest };