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
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { loadEnv, parseAst } from "vite";
2
- import { pagesRouter, apiRouter, invalidateRouteCache, matchRoute } from "./routing/pages-router.js";
2
+ import { pagesRouter, apiRouter, invalidateRouteCache, matchRoute, } from "./routing/pages-router.js";
3
3
  import { generateServerEntry as _generateServerEntry } from "./entries/pages-server-entry.js";
4
4
  import { generateClientEntry as _generateClientEntry } from "./entries/pages-client-entry.js";
5
5
  import { appRouter, invalidateAppRouteCache } from "./routing/app-router.js";
@@ -19,7 +19,9 @@ import { PHASE_PRODUCTION_BUILD, PHASE_DEVELOPMENT_SERVER } from "./shims/consta
19
19
  import { validateDevRequest } from "./server/dev-origin-check.js";
20
20
  import { isExternalUrl, proxyExternalRequest, matchHeaders, matchRedirect, matchRewrite, requestContextFromRequest, sanitizeDestination, } from "./config/config-matchers.js";
21
21
  import { scanMetadataFiles } from "./server/metadata-routes.js";
22
+ import { buildRequestHeadersFromMiddlewareResponse } from "./server/middleware-request-headers.js";
22
23
  import { detectPackageManager } from "./utils/project.js";
24
+ import { hasBasePath } from "./utils/base-path.js";
23
25
  import { asyncHooksStubPlugin } from "./plugins/async-hooks-stub.js";
24
26
  import { hasWranglerConfig, formatMissingCloudflarePluginError } from "./deploy.js";
25
27
  import tsconfigPaths from "vite-tsconfig-paths";
@@ -66,7 +68,11 @@ async function fetchAndCacheFont(cssUrl, family, cacheDir) {
66
68
  while ((urlMatch = urlRe.exec(css)) !== null) {
67
69
  const fontUrl = urlMatch[1];
68
70
  if (!urls.has(fontUrl)) {
69
- const ext = fontUrl.includes(".woff2") ? ".woff2" : fontUrl.includes(".woff") ? ".woff" : ".ttf";
71
+ const ext = fontUrl.includes(".woff2")
72
+ ? ".woff2"
73
+ : fontUrl.includes(".woff")
74
+ ? ".woff"
75
+ : ".ttf";
70
76
  const fileHash = createHash("md5").update(fontUrl).digest("hex").slice(0, 8);
71
77
  urls.set(fontUrl, `${family.toLowerCase().replace(/\s+/g, "-")}-${fileHash}${ext}`);
72
78
  }
@@ -133,7 +139,9 @@ function extractStaticValue(node) {
133
139
  return node.value;
134
140
  case "UnaryExpression":
135
141
  // Handle negative numbers: -1, -3.14
136
- if (node.operator === "-" && node.argument?.type === "Literal" && typeof node.argument.value === "number") {
142
+ if (node.operator === "-" &&
143
+ node.argument?.type === "Literal" &&
144
+ typeof node.argument.value === "number") {
137
145
  return -node.argument.value;
138
146
  }
139
147
  return undefined;
@@ -244,7 +252,9 @@ async function resolvePostcssStringPlugins(projectRoot) {
244
252
  // Load the config file
245
253
  let config;
246
254
  try {
247
- if (configPath.endsWith(".json") || configPath.endsWith(".yaml") || configPath.endsWith(".yml")) {
255
+ if (configPath.endsWith(".json") ||
256
+ configPath.endsWith(".yaml") ||
257
+ configPath.endsWith(".yml")) {
248
258
  // JSON/YAML configs use object form — postcss-load-config handles these fine
249
259
  return undefined;
250
260
  }
@@ -267,8 +277,7 @@ async function resolvePostcssStringPlugins(projectRoot) {
267
277
  // (either bare strings or tuple form ["plugin-name", { options }])
268
278
  if (!config || !Array.isArray(config.plugins))
269
279
  return undefined;
270
- const hasStringPlugins = config.plugins.some((p) => typeof p === "string" ||
271
- (Array.isArray(p) && typeof p[0] === "string"));
280
+ const hasStringPlugins = config.plugins.some((p) => typeof p === "string" || (Array.isArray(p) && typeof p[0] === "string"));
272
281
  if (!hasStringPlugins)
273
282
  return undefined;
274
283
  // Resolve string plugin names to actual plugin functions
@@ -366,9 +375,7 @@ function clientManualChunks(id) {
366
375
  const pkg = getPackageName(id);
367
376
  if (!pkg)
368
377
  return undefined;
369
- if (pkg === "react" ||
370
- pkg === "react-dom" ||
371
- pkg === "scheduler") {
378
+ if (pkg === "react" || pkg === "react-dom" || pkg === "scheduler") {
372
379
  return "framework";
373
380
  }
374
381
  // Let Rollup handle all other vendor code via its default
@@ -566,11 +573,12 @@ export default function vinext(options = {}) {
566
573
  if (earlyAppDirExists && autoRsc) {
567
574
  if (!resolvedRscPath) {
568
575
  throw new Error("vinext: App Router detected but @vitejs/plugin-rsc is not installed.\n" +
569
- "Run: " + detectPackageManager(process.cwd()) + " @vitejs/plugin-rsc");
576
+ "Run: " +
577
+ detectPackageManager(process.cwd()) +
578
+ " @vitejs/plugin-rsc");
570
579
  }
571
580
  const rscImport = import(pathToFileURL(resolvedRscPath).href);
572
- rscPluginPromise = rscImport
573
- .then((mod) => {
581
+ rscPluginPromise = rscImport.then((mod) => {
574
582
  const rsc = mod.default;
575
583
  return rsc({
576
584
  entries: {
@@ -585,9 +593,7 @@ export default function vinext(options = {}) {
585
593
  // Shared state for the MDX proxy plugin. Populated during config() if MDX
586
594
  // files are detected and @mdx-js/rollup is installed.
587
595
  let mdxDelegate = null;
588
- const reactPlugin = options.react === false
589
- ? false
590
- : react(options.react === true ? undefined : options.react);
596
+ const reactPlugin = options.react === false ? false : react(options.react === true ? undefined : options.react);
591
597
  const plugins = [
592
598
  // Resolve tsconfig paths/baseUrl aliases so real-world Next.js repos
593
599
  // that use @/*, #/*, or baseUrl imports work out of the box.
@@ -689,7 +695,9 @@ export default function vinext(options = {}) {
689
695
  // server-side validation. Matches Next.js behavior: only configured
690
696
  // sizes are accepted by the image optimization endpoint.
691
697
  {
692
- const deviceSizes = nextConfig.images?.deviceSizes ?? [640, 750, 828, 1080, 1200, 1920, 2048, 3840];
698
+ const deviceSizes = nextConfig.images?.deviceSizes ?? [
699
+ 640, 750, 828, 1080, 1200, 1920, 2048, 3840,
700
+ ];
693
701
  const imageSizes = nextConfig.images?.imageSizes ?? [16, 32, 48, 64, 96, 128, 256, 384];
694
702
  defines["process.env.__VINEXT_IMAGE_DEVICE_SIZES"] = JSON.stringify(JSON.stringify(deviceSizes));
695
703
  defines["process.env.__VINEXT_IMAGE_SIZES"] = JSON.stringify(JSON.stringify(imageSizes));
@@ -701,6 +709,9 @@ export default function vinext(options = {}) {
701
709
  // __prerender_bypass cookie is consistent across all server
702
710
  // instances (e.g. multiple Cloudflare Workers isolates).
703
711
  defines["process.env.__VINEXT_DRAFT_SECRET"] = JSON.stringify(crypto.randomUUID());
712
+ // Build ID — resolved from next.config generateBuildId() or random UUID.
713
+ // Exposed so server entries and the next/server shim can inject it.
714
+ defines["process.env.__VINEXT_BUILD_ID"] = JSON.stringify(nextConfig.buildId);
704
715
  // Build the shim alias map — used by both resolve.alias and resolveId
705
716
  // (resolveId handles .js extension variants for libraries like nuqs)
706
717
  nextShimMap = {
@@ -770,8 +781,14 @@ export default function vinext(options = {}) {
770
781
  }
771
782
  }
772
783
  flattenPlugins(config.plugins ?? []);
773
- hasCloudflarePlugin = pluginsFlat.some((p) => p && typeof p === "object" && typeof p.name === "string" && (p.name === "vite-plugin-cloudflare" || p.name.startsWith("vite-plugin-cloudflare:")));
774
- hasNitroPlugin = pluginsFlat.some((p) => p && typeof p === "object" && typeof p.name === "string" && (p.name === "nitro" || p.name.startsWith("nitro:")));
784
+ hasCloudflarePlugin = pluginsFlat.some((p) => p &&
785
+ typeof p === "object" &&
786
+ typeof p.name === "string" &&
787
+ (p.name === "vite-plugin-cloudflare" || p.name.startsWith("vite-plugin-cloudflare:")));
788
+ hasNitroPlugin = pluginsFlat.some((p) => p &&
789
+ typeof p === "object" &&
790
+ typeof p.name === "string" &&
791
+ (p.name === "nitro" || p.name.startsWith("nitro:")));
775
792
  // Resolve PostCSS string plugin names that Vite can't handle.
776
793
  // Next.js projects commonly use array-form plugins like
777
794
  // `plugins: ["@tailwindcss/postcss"]` which postcss-load-config
@@ -785,9 +802,12 @@ export default function vinext(options = {}) {
785
802
  }
786
803
  // Auto-inject @mdx-js/rollup when MDX files exist and no MDX plugin is
787
804
  // already configured. Applies remark/rehype plugins from next.config.
788
- const hasMdxPlugin = pluginsFlat.some((p) => p && typeof p === "object" && typeof p.name === "string" &&
805
+ const hasMdxPlugin = pluginsFlat.some((p) => p &&
806
+ typeof p === "object" &&
807
+ typeof p.name === "string" &&
789
808
  (p.name === "@mdx-js/rollup" || p.name === "mdx"));
790
- if (!hasMdxPlugin && hasMdxFiles(root, hasAppDir ? appDir : null, hasPagesDir ? pagesDir : null)) {
809
+ if (!hasMdxPlugin &&
810
+ hasMdxFiles(root, hasAppDir ? appDir : null, hasPagesDir ? pagesDir : null)) {
791
811
  try {
792
812
  const mdxRollup = await import("@mdx-js/rollup");
793
813
  const mdxFactory = mdxRollup.default ?? mdxRollup;
@@ -811,7 +831,9 @@ export default function vinext(options = {}) {
811
831
  catch {
812
832
  // @mdx-js/rollup not installed — warn but don't fail
813
833
  console.warn("[vinext] MDX files detected but @mdx-js/rollup is not installed. " +
814
- "Install it with: " + detectPackageManager(process.cwd()) + " @mdx-js/rollup");
834
+ "Install it with: " +
835
+ detectPackageManager(process.cwd()) +
836
+ " @mdx-js/rollup");
815
837
  }
816
838
  }
817
839
  // Detect if this is a standalone SSR build (set by `vite build --ssr`
@@ -893,12 +915,14 @@ export default function vinext(options = {}) {
893
915
  // Skip when targeting bundled runtimes (Cloudflare/Nitro bundle everything).
894
916
  // This also resolves extensionless-import issues in packages like
895
917
  // `validator` (see #189) by routing them through Vite's resolver.
896
- ...(hasCloudflarePlugin || hasNitroPlugin ? {} : {
897
- ssr: {
898
- external: ["react", "react-dom", "react-dom/server"],
899
- noExternal: true,
900
- },
901
- }),
918
+ ...(hasCloudflarePlugin || hasNitroPlugin
919
+ ? {}
920
+ : {
921
+ ssr: {
922
+ external: ["react", "react-dom", "react-dom/server"],
923
+ noExternal: true,
924
+ },
925
+ }),
902
926
  resolve: {
903
927
  alias: nextShimMap,
904
928
  // Dedupe React packages to prevent dual-instance errors.
@@ -906,12 +930,7 @@ export default function vinext(options = {}) {
906
930
  // brings its own React copy, multiple React instances can load,
907
931
  // causing cryptic "Invalid hook call" errors. This is a no-op
908
932
  // when only one copy exists.
909
- dedupe: [
910
- "react",
911
- "react-dom",
912
- "react/jsx-runtime",
913
- "react/jsx-dev-runtime",
914
- ],
933
+ dedupe: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"],
915
934
  },
916
935
  // Exclude vinext from dependency optimization so esbuild doesn't
917
936
  // scan dist files containing virtual module imports (virtual:vinext-*)
@@ -954,34 +973,31 @@ export default function vinext(options = {}) {
954
973
  // "Invalid hook call" from duplicate React instances).
955
974
  // The entries must be relative to the project root.
956
975
  const relAppDir = path.relative(root, appDir);
957
- const appEntries = [
958
- `${relAppDir}/**/*.{tsx,ts,jsx,js}`,
959
- ];
976
+ const appEntries = [`${relAppDir}/**/*.{tsx,ts,jsx,js}`];
960
977
  viteConfig.environments = {
961
978
  rsc: {
962
- ...(hasCloudflarePlugin || hasNitroPlugin ? {} : {
963
- resolve: {
964
- // Externalize native/heavy packages so the RSC environment
965
- // loads them natively via Node rather than through Vite's
966
- // ESM module evaluator (which can't handle native addons).
967
- // Note: Do NOT externalize react/react-dom here they must
968
- // be bundled with the "react-server" condition for RSC.
969
- // Skip when targeting bundled runtimes (Cloudflare/Nitro).
970
- external: userSsrExternal === true ? true : [
971
- "satori",
972
- "@resvg/resvg-js",
973
- "yoga-wasm-web",
974
- ...userSsrExternal,
975
- ],
976
- // Force all node_modules through Vite's transform pipeline
977
- // so non-JS imports (CSS, images) don't hit Node's native
978
- // ESM loader. Matches Next.js behavior of bundling everything.
979
- // Packages in `external` above take precedence per Vite rules.
980
- // When user sets `ssr.external: true`, skip noExternal since
981
- // everything is already externalized.
982
- ...(userSsrExternal === true ? {} : { noExternal: true }),
983
- },
984
- }),
979
+ ...(hasCloudflarePlugin || hasNitroPlugin
980
+ ? {}
981
+ : {
982
+ resolve: {
983
+ // Externalize native/heavy packages so the RSC environment
984
+ // loads them natively via Node rather than through Vite's
985
+ // ESM module evaluator (which can't handle native addons).
986
+ // Note: Do NOT externalize react/react-dom here — they must
987
+ // be bundled with the "react-server" condition for RSC.
988
+ // Skip when targeting bundled runtimes (Cloudflare/Nitro).
989
+ external: userSsrExternal === true
990
+ ? true
991
+ : ["satori", "@resvg/resvg-js", "yoga-wasm-web", ...userSsrExternal],
992
+ // Force all node_modules through Vite's transform pipeline
993
+ // so non-JS imports (CSS, images) don't hit Node's native
994
+ // ESM loader. Matches Next.js behavior of bundling everything.
995
+ // Packages in `external` above take precedence per Vite rules.
996
+ // When user sets `ssr.external: true`, skip noExternal since
997
+ // everything is already externalized.
998
+ ...(userSsrExternal === true ? {} : { noExternal: true }),
999
+ },
1000
+ }),
985
1001
  optimizeDeps: {
986
1002
  exclude: ["vinext", "@vercel/og"],
987
1003
  entries: appEntries,
@@ -994,17 +1010,19 @@ export default function vinext(options = {}) {
994
1010
  },
995
1011
  },
996
1012
  ssr: {
997
- ...(hasCloudflarePlugin || hasNitroPlugin ? {} : {
998
- resolve: {
999
- external: userSsrExternal === true ? true : [...userSsrExternal],
1000
- // Force all node_modules through Vite's transform pipeline
1001
- // so non-JS imports (CSS, images) don't hit Node's native
1002
- // ESM loader. Matches Next.js behavior of bundling everything.
1003
- // When user sets `ssr.external: true`, skip noExternal since
1004
- // everything is already externalized.
1005
- ...(userSsrExternal === true ? {} : { noExternal: true }),
1006
- },
1007
- }),
1013
+ ...(hasCloudflarePlugin || hasNitroPlugin
1014
+ ? {}
1015
+ : {
1016
+ resolve: {
1017
+ external: userSsrExternal === true ? true : [...userSsrExternal],
1018
+ // Force all node_modules through Vite's transform pipeline
1019
+ // so non-JS imports (CSS, images) don't hit Node's native
1020
+ // ESM loader. Matches Next.js behavior of bundling everything.
1021
+ // When user sets `ssr.external: true`, skip noExternal since
1022
+ // everything is already externalized.
1023
+ ...(userSsrExternal === true ? {} : { noExternal: true }),
1024
+ },
1025
+ }),
1008
1026
  optimizeDeps: {
1009
1027
  exclude: ["vinext", "@vercel/og"],
1010
1028
  entries: appEntries,
@@ -1137,10 +1155,12 @@ export default function vinext(options = {}) {
1137
1155
  return RESOLVED_SERVER_ENTRY;
1138
1156
  if (cleanId === VIRTUAL_CLIENT_ENTRY)
1139
1157
  return RESOLVED_CLIENT_ENTRY;
1140
- if (cleanId.endsWith("/" + VIRTUAL_SERVER_ENTRY) || cleanId.endsWith("\\" + VIRTUAL_SERVER_ENTRY)) {
1158
+ if (cleanId.endsWith("/" + VIRTUAL_SERVER_ENTRY) ||
1159
+ cleanId.endsWith("\\" + VIRTUAL_SERVER_ENTRY)) {
1141
1160
  return RESOLVED_SERVER_ENTRY;
1142
1161
  }
1143
- if (cleanId.endsWith("/" + VIRTUAL_CLIENT_ENTRY) || cleanId.endsWith("\\" + VIRTUAL_CLIENT_ENTRY)) {
1162
+ if (cleanId.endsWith("/" + VIRTUAL_CLIENT_ENTRY) ||
1163
+ cleanId.endsWith("\\" + VIRTUAL_CLIENT_ENTRY)) {
1144
1164
  return RESOLVED_CLIENT_ENTRY;
1145
1165
  }
1146
1166
  // App Router virtual modules
@@ -1150,13 +1170,16 @@ export default function vinext(options = {}) {
1150
1170
  return RESOLVED_APP_SSR_ENTRY;
1151
1171
  if (cleanId === VIRTUAL_APP_BROWSER_ENTRY)
1152
1172
  return RESOLVED_APP_BROWSER_ENTRY;
1153
- if (cleanId.endsWith("/" + VIRTUAL_RSC_ENTRY) || cleanId.endsWith("\\" + VIRTUAL_RSC_ENTRY)) {
1173
+ if (cleanId.endsWith("/" + VIRTUAL_RSC_ENTRY) ||
1174
+ cleanId.endsWith("\\" + VIRTUAL_RSC_ENTRY)) {
1154
1175
  return RESOLVED_RSC_ENTRY;
1155
1176
  }
1156
- if (cleanId.endsWith("/" + VIRTUAL_APP_SSR_ENTRY) || cleanId.endsWith("\\" + VIRTUAL_APP_SSR_ENTRY)) {
1177
+ if (cleanId.endsWith("/" + VIRTUAL_APP_SSR_ENTRY) ||
1178
+ cleanId.endsWith("\\" + VIRTUAL_APP_SSR_ENTRY)) {
1157
1179
  return RESOLVED_APP_SSR_ENTRY;
1158
1180
  }
1159
- if (cleanId.endsWith("/" + VIRTUAL_APP_BROWSER_ENTRY) || cleanId.endsWith("\\" + VIRTUAL_APP_BROWSER_ENTRY)) {
1181
+ if (cleanId.endsWith("/" + VIRTUAL_APP_BROWSER_ENTRY) ||
1182
+ cleanId.endsWith("\\" + VIRTUAL_APP_BROWSER_ENTRY)) {
1160
1183
  return RESOLVED_APP_BROWSER_ENTRY;
1161
1184
  }
1162
1185
  },
@@ -1448,9 +1471,14 @@ export default function vinext(options = {}) {
1448
1471
  // loads the module. The gap between them is exactly the Vite
1449
1472
  // compile/transform cost.
1450
1473
  function _parseTiming(raw) {
1451
- const [handlerStart, inHandlerCompileMs, renderMs] = String(raw).split(",").map((v) => Number(v));
1452
- if (!Number.isNaN(handlerStart) && !Number.isNaN(inHandlerCompileMs) && inHandlerCompileMs !== -1) {
1453
- _compileMs = Math.max(0, Math.round(handlerStart - _reqStart)) + inHandlerCompileMs;
1474
+ const [handlerStart, inHandlerCompileMs, renderMs] = String(raw)
1475
+ .split(",")
1476
+ .map((v) => Number(v));
1477
+ if (!Number.isNaN(handlerStart) &&
1478
+ !Number.isNaN(inHandlerCompileMs) &&
1479
+ inHandlerCompileMs !== -1) {
1480
+ _compileMs =
1481
+ Math.max(0, Math.round(handlerStart - _reqStart)) + inHandlerCompileMs;
1454
1482
  }
1455
1483
  if (!Number.isNaN(renderMs) && renderMs !== -1) {
1456
1484
  _renderMs = renderMs;
@@ -1496,7 +1524,9 @@ export default function vinext(options = {}) {
1496
1524
  // matching what Next.js shows for soft navigations.
1497
1525
  const resolvedRenderMs = _renderMs !== undefined
1498
1526
  ? _renderMs
1499
- : (_compileMs !== undefined ? Math.max(0, Math.round(totalMs - _compileMs)) : undefined);
1527
+ : _compileMs !== undefined
1528
+ ? Math.max(0, Math.round(totalMs - _compileMs))
1529
+ : undefined;
1500
1530
  logRequest({
1501
1531
  method: req.method ?? "GET",
1502
1532
  url: logUrl,
@@ -1633,7 +1663,10 @@ export default function vinext(options = {}) {
1633
1663
  }
1634
1664
  // Normalize trailing slash based on next.config.js trailingSlash setting.
1635
1665
  // Redirect to the canonical form if needed.
1636
- if (nextConfig && pathname !== "/" && pathname !== "/api" && !pathname.startsWith("/api/")) {
1666
+ if (nextConfig &&
1667
+ pathname !== "/" &&
1668
+ pathname !== "/api" &&
1669
+ !pathname.startsWith("/api/")) {
1637
1670
  const hasTrailing = pathname.endsWith("/");
1638
1671
  if (nextConfig.trailingSlash && !hasTrailing) {
1639
1672
  // trailingSlash: true — redirect /about → /about/
@@ -1652,12 +1685,24 @@ export default function vinext(options = {}) {
1652
1685
  return;
1653
1686
  }
1654
1687
  }
1688
+ const applyRequestHeadersToNodeRequest = (nextRequestHeaders) => {
1689
+ for (const key of Object.keys(req.headers)) {
1690
+ delete req.headers[key];
1691
+ }
1692
+ for (const [key, value] of nextRequestHeaders) {
1693
+ req.headers[key] = value;
1694
+ }
1695
+ };
1696
+ let middlewareRequestHeaders = null;
1655
1697
  // Run middleware.ts if present
1656
1698
  if (middlewarePath) {
1657
1699
  // Only trust X-Forwarded-Proto when behind a trusted proxy
1658
- const devTrustProxy = process.env.VINEXT_TRUST_PROXY === "1" || (process.env.VINEXT_TRUSTED_HOSTS ?? "").split(",").some(h => h.trim());
1700
+ const devTrustProxy = process.env.VINEXT_TRUST_PROXY === "1" ||
1701
+ (process.env.VINEXT_TRUSTED_HOSTS ?? "").split(",").some((h) => h.trim());
1659
1702
  const rawProto = devTrustProxy
1660
- ? String(req.headers["x-forwarded-proto"] || "").split(",")[0].trim()
1703
+ ? String(req.headers["x-forwarded-proto"] || "")
1704
+ .split(",")[0]
1705
+ .trim()
1661
1706
  : "";
1662
1707
  const mwProto = rawProto === "https" || rawProto === "http" ? rawProto : "http";
1663
1708
  const origin = `${mwProto}://${req.headers.host || "localhost"}`;
@@ -1670,7 +1715,9 @@ export default function vinext(options = {}) {
1670
1715
  const result = await runMiddleware(getPagesRunner(), middlewarePath, middlewareRequest);
1671
1716
  if (!result.continue) {
1672
1717
  if (result.redirectUrl) {
1673
- const redirectHeaders = { Location: result.redirectUrl };
1718
+ const redirectHeaders = {
1719
+ Location: result.redirectUrl,
1720
+ };
1674
1721
  if (result.responseHeaders) {
1675
1722
  for (const [key, value] of result.responseHeaders) {
1676
1723
  const existing = redirectHeaders[key];
@@ -1704,13 +1751,21 @@ export default function vinext(options = {}) {
1704
1751
  // config has/missing conditions and downstream handlers
1705
1752
  // see middleware-modified cookies and headers.
1706
1753
  if (result.responseHeaders) {
1707
- const mwReqPrefix = "x-middleware-request-";
1708
- for (const [key, value] of result.responseHeaders) {
1709
- if (key.startsWith(mwReqPrefix)) {
1710
- const realName = key.slice(mwReqPrefix.length);
1711
- req.headers[realName] = value;
1754
+ const currentRequestHeaders = new Headers();
1755
+ for (const [key, value] of Object.entries(req.headers)) {
1756
+ if (Array.isArray(value)) {
1757
+ currentRequestHeaders.set(key, value.join(", "));
1758
+ }
1759
+ else if (value !== undefined) {
1760
+ currentRequestHeaders.set(key, value);
1712
1761
  }
1713
- else if (!key.startsWith("x-middleware-")) {
1762
+ }
1763
+ middlewareRequestHeaders = buildRequestHeadersFromMiddlewareResponse(currentRequestHeaders, result.responseHeaders);
1764
+ if (middlewareRequestHeaders && !hasAppDir) {
1765
+ applyRequestHeadersToNodeRequest(middlewareRequestHeaders);
1766
+ }
1767
+ for (const [key, value] of result.responseHeaders) {
1768
+ if (!key.startsWith("x-middleware-")) {
1714
1769
  res.appendHeader(key, value);
1715
1770
  }
1716
1771
  }
@@ -1758,9 +1813,10 @@ export default function vinext(options = {}) {
1758
1813
  // Convert Node.js IncomingMessage headers to a Web Request for
1759
1814
  // requestContextFromRequest(), which uses the standard Web API.
1760
1815
  const reqUrl = new URL(url, `http://${req.headers.host || "localhost"}`);
1761
- const reqCtxHeaders = new Headers(Object.fromEntries(Object.entries(req.headers)
1762
- .filter(([, v]) => v !== undefined)
1763
- .map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : String(v)])));
1816
+ const reqCtxHeaders = middlewareRequestHeaders ??
1817
+ new Headers(Object.fromEntries(Object.entries(req.headers)
1818
+ .filter(([, v]) => v !== undefined)
1819
+ .map(([k, v]) => [k, Array.isArray(v) ? v.join(", ") : String(v)])));
1764
1820
  const reqCtx = requestContextFromRequest(new Request(reqUrl, { headers: reqCtxHeaders }));
1765
1821
  // Apply custom headers from next.config.js
1766
1822
  if (nextConfig?.headers.length) {
@@ -1768,7 +1824,7 @@ export default function vinext(options = {}) {
1768
1824
  }
1769
1825
  // Apply redirects from next.config.js
1770
1826
  if (nextConfig?.redirects.length) {
1771
- const redirected = applyRedirects(pathname, res, nextConfig.redirects, reqCtx);
1827
+ const redirected = applyRedirects(pathname, res, nextConfig.redirects, reqCtx, nextConfig.basePath ?? "");
1772
1828
  if (redirected)
1773
1829
  return;
1774
1830
  }
@@ -1776,8 +1832,7 @@ export default function vinext(options = {}) {
1776
1832
  let resolvedUrl = url;
1777
1833
  if (nextConfig?.rewrites.beforeFiles.length) {
1778
1834
  resolvedUrl =
1779
- applyRewrites(pathname, nextConfig.rewrites.beforeFiles, reqCtx) ??
1780
- url;
1835
+ applyRewrites(pathname, nextConfig.rewrites.beforeFiles, reqCtx) ?? url;
1781
1836
  }
1782
1837
  // External rewrite from beforeFiles — proxy to external URL
1783
1838
  if (isExternalUrl(resolvedUrl)) {
@@ -1786,9 +1841,12 @@ export default function vinext(options = {}) {
1786
1841
  }
1787
1842
  // Handle API routes first (pages/api/*)
1788
1843
  const resolvedPathname = resolvedUrl.split("?")[0];
1789
- if (resolvedPathname.startsWith("/api/") ||
1790
- resolvedPathname === "/api") {
1844
+ if (resolvedPathname.startsWith("/api/") || resolvedPathname === "/api") {
1791
1845
  const apiRoutes = await apiRouter(pagesDir, nextConfig?.pageExtensions, fileMatcher);
1846
+ const apiMatch = matchRoute(resolvedUrl, apiRoutes);
1847
+ if (apiMatch && middlewareRequestHeaders) {
1848
+ applyRequestHeadersToNodeRequest(middlewareRequestHeaders);
1849
+ }
1792
1850
  const handled = await handleApiRoute(server, req, res, resolvedUrl, apiRoutes);
1793
1851
  if (handled)
1794
1852
  return;
@@ -1820,6 +1878,9 @@ export default function vinext(options = {}) {
1820
1878
  // Try rendering the resolved URL
1821
1879
  const match = matchRoute(resolvedUrl.split("?")[0], routes);
1822
1880
  if (match) {
1881
+ if (middlewareRequestHeaders) {
1882
+ applyRequestHeadersToNodeRequest(middlewareRequestHeaders);
1883
+ }
1823
1884
  await handler(req, res, resolvedUrl, mwStatus);
1824
1885
  return;
1825
1886
  }
@@ -1832,6 +1893,13 @@ export default function vinext(options = {}) {
1832
1893
  await proxyExternalRewriteNode(req, res, fallbackRewrite);
1833
1894
  return;
1834
1895
  }
1896
+ const fallbackMatch = matchRoute(fallbackRewrite.split("?")[0], routes);
1897
+ if (!fallbackMatch && hasAppDir) {
1898
+ return next();
1899
+ }
1900
+ if (middlewareRequestHeaders) {
1901
+ applyRequestHeadersToNodeRequest(middlewareRequestHeaders);
1902
+ }
1835
1903
  await handler(req, res, fallbackRewrite, mwStatus);
1836
1904
  return;
1837
1905
  }
@@ -2032,7 +2100,10 @@ export default function vinext(options = {}) {
2032
2100
  const importMatch = code.match(importRe);
2033
2101
  if (!importMatch)
2034
2102
  return null;
2035
- const importedNames = new Set(importMatch[1].split(",").map((s) => s.trim()).filter(Boolean));
2103
+ const importedNames = new Set(importMatch[1]
2104
+ .split(",")
2105
+ .map((s) => s.trim())
2106
+ .filter(Boolean));
2036
2107
  const s = new MagicString(code);
2037
2108
  let hasChanges = false;
2038
2109
  const cacheDir = this._cacheDir;
@@ -2058,10 +2129,14 @@ export default function vinext(options = {}) {
2058
2129
  }
2059
2130
  // Build the Google Fonts CSS URL
2060
2131
  const weights = options.weight
2061
- ? Array.isArray(options.weight) ? options.weight : [options.weight]
2132
+ ? Array.isArray(options.weight)
2133
+ ? options.weight
2134
+ : [options.weight]
2062
2135
  : [];
2063
2136
  const styles = options.style
2064
- ? Array.isArray(options.style) ? options.style : [options.style]
2137
+ ? Array.isArray(options.style)
2138
+ ? options.style
2139
+ : [options.style]
2065
2140
  : [];
2066
2141
  const display = options.display ?? "swap";
2067
2142
  let spec = family.replace(/\s+/g, "+");
@@ -2217,7 +2292,9 @@ export default function vinext(options = {}) {
2217
2292
  return null;
2218
2293
  if (!resolvedRscTransformsPath) {
2219
2294
  throw new Error("vinext: 'use cache' requires @vitejs/plugin-rsc to be installed.\n" +
2220
- "Run: " + detectPackageManager(process.cwd()) + " @vitejs/plugin-rsc");
2295
+ "Run: " +
2296
+ detectPackageManager(process.cwd()) +
2297
+ " @vitejs/plugin-rsc");
2221
2298
  }
2222
2299
  const { transformWrapExport, transformHoistInlineDirective } = await import(pathToFileURL(resolvedRscTransformsPath).href);
2223
2300
  const ast = parseAst(code);
@@ -2232,7 +2309,9 @@ export default function vinext(options = {}) {
2232
2309
  // (they're leaf components). Layout/template defaults are excluded
2233
2310
  // because they receive {children} from the framework.
2234
2311
  const directiveValue = cacheDirective.expression.value;
2235
- const variant = directiveValue === "use cache" ? "" : directiveValue.replace("use cache:", "").replace("use cache: ", "").trim();
2312
+ const variant = directiveValue === "use cache"
2313
+ ? ""
2314
+ : directiveValue.replace("use cache:", "").replace("use cache: ", "").trim();
2236
2315
  // Only skip default export wrapping for layouts and templates —
2237
2316
  // they receive {children} from the framework which requires
2238
2317
  // temporary reference handling that registerCachedFunction doesn't
@@ -2284,7 +2363,9 @@ export default function vinext(options = {}) {
2284
2363
  directive: /^use cache(:\s*\w+)?$/,
2285
2364
  runtime: (value, name, meta) => {
2286
2365
  const directiveMatch = meta.directiveMatch[0];
2287
- const variant = directiveMatch === "use cache" ? "" : directiveMatch.replace("use cache:", "").replace("use cache: ", "").trim();
2366
+ const variant = directiveMatch === "use cache"
2367
+ ? ""
2368
+ : directiveMatch.replace("use cache:", "").replace("use cache: ", "").trim();
2288
2369
  return `(await import(${JSON.stringify(runtimeModuleUrl2)})).registerCachedFunction(${value}, ${JSON.stringify(id + ":" + name)}, ${JSON.stringify(variant)})`;
2289
2370
  },
2290
2371
  rejectNonAsyncFunction: false,
@@ -2429,11 +2510,9 @@ export default function vinext(options = {}) {
2429
2510
  const content = fs.readFileSync(indexPath, "utf-8");
2430
2511
  // The font is inlined as base64 by vinext:og-inline-fetch-assets, so only
2431
2512
  // the WASM needs to be present as a file alongside the bundle.
2432
- const ogAssets = [
2433
- "resvg.wasm",
2434
- ];
2513
+ const ogAssets = ["resvg.wasm"];
2435
2514
  // Only copy if the bundle actually references these files
2436
- const referencedAssets = ogAssets.filter(asset => content.includes(asset));
2515
+ const referencedAssets = ogAssets.filter((asset) => content.includes(asset));
2437
2516
  if (referencedAssets.length === 0)
2438
2517
  return;
2439
2518
  // Find @vercel/og in node_modules
@@ -2533,7 +2612,9 @@ export default function vinext(options = {}) {
2533
2612
  if (lazy.length > 0)
2534
2613
  lazyChunksData = lazy;
2535
2614
  }
2536
- catch { /* ignore parse errors */ }
2615
+ catch {
2616
+ /* ignore parse errors */
2617
+ }
2537
2618
  }
2538
2619
  // Read SSR manifest for per-page CSS/JS injection
2539
2620
  let ssrManifestData = null;
@@ -2542,7 +2623,9 @@ export default function vinext(options = {}) {
2542
2623
  try {
2543
2624
  ssrManifestData = JSON.parse(fs.readFileSync(ssrManifestPath, "utf-8"));
2544
2625
  }
2545
- catch { /* ignore parse errors */ }
2626
+ catch {
2627
+ /* ignore parse errors */
2628
+ }
2546
2629
  }
2547
2630
  if (hasAppDir) {
2548
2631
  // App Router: the RSC plugin handles __VINEXT_CLIENT_ENTRY__
@@ -2589,7 +2672,8 @@ export default function vinext(options = {}) {
2589
2672
  const assetsDir = path.join(clientDir, "assets");
2590
2673
  if (fs.existsSync(assetsDir)) {
2591
2674
  const files = fs.readdirSync(assetsDir);
2592
- const entry = files.find((f) => (f.includes("vinext-client-entry") || f.includes("vinext-app-browser-entry")) && f.endsWith(".js"));
2675
+ const entry = files.find((f) => (f.includes("vinext-client-entry") || f.includes("vinext-app-browser-entry")) &&
2676
+ f.endsWith(".js"));
2593
2677
  if (entry)
2594
2678
  clientEntryFile = "assets/" + entry;
2595
2679
  }
@@ -2722,7 +2806,7 @@ export { matchConfigPattern } from "./config/config-matchers.js";
2722
2806
  */
2723
2807
  function stripServerExports(code) {
2724
2808
  const SERVER_EXPORTS = new Set(["getServerSideProps", "getStaticProps", "getStaticPaths"]);
2725
- if (![...SERVER_EXPORTS].some(name => code.includes(name)))
2809
+ if (![...SERVER_EXPORTS].some((name) => code.includes(name)))
2726
2810
  return null;
2727
2811
  let ast;
2728
2812
  try {
@@ -2773,11 +2857,13 @@ function stripServerExports(code) {
2773
2857
  // Build replacement: keep non-server specifiers, add stubs for stripped ones
2774
2858
  const parts = [];
2775
2859
  if (kept.length > 0) {
2776
- const keptStr = kept.map((sp) => {
2860
+ const keptStr = kept
2861
+ .map((sp) => {
2777
2862
  const local = sp.local.name;
2778
2863
  const exported = sp.exported?.name ?? sp.exported?.value;
2779
2864
  return local === exported ? local : `${local} as ${exported}`;
2780
- }).join(", ");
2865
+ })
2866
+ .join(", ");
2781
2867
  parts.push(`export { ${keptStr} };`);
2782
2868
  }
2783
2869
  for (const name of stripped) {
@@ -2796,11 +2882,13 @@ function stripServerExports(code) {
2796
2882
  * Apply redirect rules from next.config.js.
2797
2883
  * Returns true if a redirect was applied.
2798
2884
  */
2799
- function applyRedirects(pathname, res, redirects, ctx) {
2885
+ function applyRedirects(pathname, res, redirects, ctx, basePath = "") {
2800
2886
  const result = matchRedirect(pathname, redirects, ctx);
2801
2887
  if (result) {
2802
2888
  // Sanitize to prevent open redirect via protocol-relative URLs
2803
- const dest = sanitizeDestination(result.destination);
2889
+ const dest = sanitizeDestination(basePath && !isExternalUrl(result.destination) && !hasBasePath(result.destination, basePath)
2890
+ ? basePath + result.destination
2891
+ : result.destination);
2804
2892
  res.writeHead(result.permanent ? 308 : 307, { Location: dest });
2805
2893
  res.end();
2806
2894
  return true;
@@ -2838,9 +2926,7 @@ async function proxyExternalRewriteNode(req, res, externalUrl) {
2838
2926
  proxyResponse.headers.forEach((value, key) => {
2839
2927
  const existing = nodeHeaders[key];
2840
2928
  if (existing !== undefined) {
2841
- nodeHeaders[key] = Array.isArray(existing)
2842
- ? [...existing, value]
2843
- : [existing, value];
2929
+ nodeHeaders[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
2844
2930
  }
2845
2931
  else {
2846
2932
  nodeHeaders[key] = value;