vinext 0.0.27 → 0.0.29

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 (151) hide show
  1. package/dist/build/report.d.ts +117 -0
  2. package/dist/build/report.d.ts.map +1 -0
  3. package/dist/build/report.js +303 -0
  4. package/dist/build/report.js.map +1 -0
  5. package/dist/build/static-export.d.ts +1 -1
  6. package/dist/build/static-export.d.ts.map +1 -1
  7. package/dist/build/static-export.js +2 -1
  8. package/dist/build/static-export.js.map +1 -1
  9. package/dist/cli.js +106 -9
  10. package/dist/cli.js.map +1 -1
  11. package/dist/cloudflare/kv-cache-handler.d.ts +28 -17
  12. package/dist/cloudflare/kv-cache-handler.d.ts.map +1 -1
  13. package/dist/cloudflare/kv-cache-handler.js +109 -42
  14. package/dist/cloudflare/kv-cache-handler.js.map +1 -1
  15. package/dist/cloudflare/tpr.d.ts +10 -0
  16. package/dist/cloudflare/tpr.d.ts.map +1 -1
  17. package/dist/cloudflare/tpr.js +36 -41
  18. package/dist/cloudflare/tpr.js.map +1 -1
  19. package/dist/config/config-matchers.d.ts +1 -0
  20. package/dist/config/config-matchers.d.ts.map +1 -1
  21. package/dist/config/config-matchers.js +51 -23
  22. package/dist/config/config-matchers.js.map +1 -1
  23. package/dist/config/next-config.d.ts.map +1 -1
  24. package/dist/config/next-config.js +16 -0
  25. package/dist/config/next-config.js.map +1 -1
  26. package/dist/deploy.d.ts +1 -1
  27. package/dist/deploy.d.ts.map +1 -1
  28. package/dist/deploy.js +48 -32
  29. package/dist/deploy.js.map +1 -1
  30. package/dist/entries/app-rsc-entry.d.ts +3 -1
  31. package/dist/entries/app-rsc-entry.d.ts.map +1 -1
  32. package/dist/entries/app-rsc-entry.js +514 -99
  33. package/dist/entries/app-rsc-entry.js.map +1 -1
  34. package/dist/entries/pages-server-entry.d.ts.map +1 -1
  35. package/dist/entries/pages-server-entry.js +154 -58
  36. package/dist/entries/pages-server-entry.js.map +1 -1
  37. package/dist/index.d.ts +40 -7
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +239 -79
  40. package/dist/index.js.map +1 -1
  41. package/dist/plugins/client-reference-dedup.d.ts +19 -0
  42. package/dist/plugins/client-reference-dedup.d.ts.map +1 -0
  43. package/dist/plugins/client-reference-dedup.js +96 -0
  44. package/dist/plugins/client-reference-dedup.js.map +1 -0
  45. package/dist/routing/app-router.d.ts +2 -0
  46. package/dist/routing/app-router.d.ts.map +1 -1
  47. package/dist/routing/app-router.js +145 -161
  48. package/dist/routing/app-router.js.map +1 -1
  49. package/dist/routing/pages-router.d.ts +1 -1
  50. package/dist/routing/pages-router.d.ts.map +1 -1
  51. package/dist/routing/pages-router.js +37 -65
  52. package/dist/routing/pages-router.js.map +1 -1
  53. package/dist/routing/route-trie.d.ts +57 -0
  54. package/dist/routing/route-trie.d.ts.map +1 -0
  55. package/dist/routing/route-trie.js +160 -0
  56. package/dist/routing/route-trie.js.map +1 -0
  57. package/dist/routing/route-validation.d.ts +8 -0
  58. package/dist/routing/route-validation.d.ts.map +1 -0
  59. package/dist/routing/route-validation.js +136 -0
  60. package/dist/routing/route-validation.js.map +1 -0
  61. package/dist/routing/utils.d.ts +19 -0
  62. package/dist/routing/utils.d.ts.map +1 -1
  63. package/dist/routing/utils.js +47 -0
  64. package/dist/routing/utils.js.map +1 -1
  65. package/dist/server/api-handler.d.ts.map +1 -1
  66. package/dist/server/api-handler.js +52 -20
  67. package/dist/server/api-handler.js.map +1 -1
  68. package/dist/server/dev-server.d.ts.map +1 -1
  69. package/dist/server/dev-server.js +67 -9
  70. package/dist/server/dev-server.js.map +1 -1
  71. package/dist/server/image-optimization.d.ts.map +1 -1
  72. package/dist/server/image-optimization.js +1 -1
  73. package/dist/server/image-optimization.js.map +1 -1
  74. package/dist/server/instrumentation.d.ts.map +1 -1
  75. package/dist/server/instrumentation.js +17 -8
  76. package/dist/server/instrumentation.js.map +1 -1
  77. package/dist/server/isr-cache.d.ts +5 -13
  78. package/dist/server/isr-cache.d.ts.map +1 -1
  79. package/dist/server/isr-cache.js +13 -12
  80. package/dist/server/isr-cache.js.map +1 -1
  81. package/dist/server/metadata-routes.d.ts +8 -2
  82. package/dist/server/metadata-routes.d.ts.map +1 -1
  83. package/dist/server/metadata-routes.js +73 -28
  84. package/dist/server/metadata-routes.js.map +1 -1
  85. package/dist/server/middleware-codegen.d.ts +11 -1
  86. package/dist/server/middleware-codegen.d.ts.map +1 -1
  87. package/dist/server/middleware-codegen.js +204 -12
  88. package/dist/server/middleware-codegen.js.map +1 -1
  89. package/dist/server/middleware.d.ts +9 -8
  90. package/dist/server/middleware.d.ts.map +1 -1
  91. package/dist/server/middleware.js +76 -14
  92. package/dist/server/middleware.js.map +1 -1
  93. package/dist/server/prod-server.d.ts +8 -2
  94. package/dist/server/prod-server.d.ts.map +1 -1
  95. package/dist/server/prod-server.js +144 -74
  96. package/dist/server/prod-server.js.map +1 -1
  97. package/dist/shims/cache.d.ts +2 -0
  98. package/dist/shims/cache.d.ts.map +1 -1
  99. package/dist/shims/cache.js +20 -8
  100. package/dist/shims/cache.js.map +1 -1
  101. package/dist/shims/fetch-cache.d.ts.map +1 -1
  102. package/dist/shims/fetch-cache.js +5 -2
  103. package/dist/shims/fetch-cache.js.map +1 -1
  104. package/dist/shims/form.d.ts.map +1 -1
  105. package/dist/shims/form.js +103 -8
  106. package/dist/shims/form.js.map +1 -1
  107. package/dist/shims/headers.d.ts +11 -3
  108. package/dist/shims/headers.d.ts.map +1 -1
  109. package/dist/shims/headers.js +182 -30
  110. package/dist/shims/headers.js.map +1 -1
  111. package/dist/shims/internal/parse-cookie-header.d.ts +12 -0
  112. package/dist/shims/internal/parse-cookie-header.d.ts.map +1 -0
  113. package/dist/shims/internal/parse-cookie-header.js +32 -0
  114. package/dist/shims/internal/parse-cookie-header.js.map +1 -0
  115. package/dist/shims/link.d.ts +2 -1
  116. package/dist/shims/link.d.ts.map +1 -1
  117. package/dist/shims/link.js +19 -45
  118. package/dist/shims/link.js.map +1 -1
  119. package/dist/shims/metadata.d.ts +56 -0
  120. package/dist/shims/metadata.d.ts.map +1 -1
  121. package/dist/shims/metadata.js +66 -0
  122. package/dist/shims/metadata.js.map +1 -1
  123. package/dist/shims/navigation.d.ts +5 -7
  124. package/dist/shims/navigation.d.ts.map +1 -1
  125. package/dist/shims/navigation.js +61 -39
  126. package/dist/shims/navigation.js.map +1 -1
  127. package/dist/shims/readonly-url-search-params.d.ts +11 -0
  128. package/dist/shims/readonly-url-search-params.d.ts.map +1 -0
  129. package/dist/shims/readonly-url-search-params.js +24 -0
  130. package/dist/shims/readonly-url-search-params.js.map +1 -0
  131. package/dist/shims/router.d.ts +4 -3
  132. package/dist/shims/router.d.ts.map +1 -1
  133. package/dist/shims/router.js +55 -48
  134. package/dist/shims/router.js.map +1 -1
  135. package/dist/shims/server.d.ts +1 -1
  136. package/dist/shims/server.d.ts.map +1 -1
  137. package/dist/shims/server.js +7 -13
  138. package/dist/shims/server.js.map +1 -1
  139. package/dist/shims/url-utils.d.ts +20 -6
  140. package/dist/shims/url-utils.d.ts.map +1 -1
  141. package/dist/shims/url-utils.js +79 -0
  142. package/dist/shims/url-utils.js.map +1 -1
  143. package/dist/utils/manifest-paths.d.ts +4 -0
  144. package/dist/utils/manifest-paths.d.ts.map +1 -0
  145. package/dist/utils/manifest-paths.js +20 -0
  146. package/dist/utils/manifest-paths.js.map +1 -0
  147. package/dist/utils/query.d.ts +9 -0
  148. package/dist/utils/query.d.ts.map +1 -1
  149. package/dist/utils/query.js +59 -9
  150. package/dist/utils/query.js.map +1 -1
  151. package/package.json +2 -2
@@ -28,6 +28,8 @@ import { IMAGE_OPTIMIZATION_PATH, IMAGE_CONTENT_SECURITY_POLICY, parseImageParam
28
28
  import { normalizePath } from "./normalize-path.js";
29
29
  import { hasBasePath, stripBasePath } from "../utils/base-path.js";
30
30
  import { computeLazyChunks } from "../index.js";
31
+ import { manifestFileWithBase } from "../utils/manifest-paths.js";
32
+ import { normalizePathnameForRouteMatchStrict } from "../routing/utils.js";
31
33
  /** Convert a Node.js IncomingMessage into a ReadableStream for Web Request body. */
32
34
  function readNodeStream(req) {
33
35
  return new ReadableStream({
@@ -118,10 +120,18 @@ function mergeResponseHeaders(middlewareHeaders, response) {
118
120
  * Send a compressed response if the content type is compressible and the
119
121
  * client supports compression. Otherwise send uncompressed.
120
122
  */
121
- function sendCompressed(req, res, body, contentType, statusCode, extraHeaders = {}, compress = true) {
123
+ function sendCompressed(req, res, body, contentType, statusCode, extraHeaders = {}, compress = true, statusText = undefined) {
122
124
  const buf = typeof body === "string" ? Buffer.from(body) : body;
123
125
  const baseType = contentType.split(";")[0].trim();
124
126
  const encoding = compress ? negotiateEncoding(req) : null;
127
+ const writeHead = (headers) => {
128
+ if (statusText) {
129
+ res.writeHead(statusCode, statusText, headers);
130
+ }
131
+ else {
132
+ res.writeHead(statusCode, headers);
133
+ }
134
+ };
125
135
  if (encoding && COMPRESSIBLE_TYPES.has(baseType) && buf.length >= COMPRESS_THRESHOLD) {
126
136
  const compressor = createCompressor(encoding);
127
137
  // Merge Accept-Encoding into existing Vary header from extraHeaders instead
@@ -139,7 +149,7 @@ function sendCompressed(req, res, body, contentType, statusCode, extraHeaders =
139
149
  else {
140
150
  varyValue = "Accept-Encoding";
141
151
  }
142
- res.writeHead(statusCode, {
152
+ writeHead({
143
153
  ...extraHeaders,
144
154
  "Content-Type": contentType,
145
155
  "Content-Encoding": encoding,
@@ -151,8 +161,11 @@ function sendCompressed(req, res, body, contentType, statusCode, extraHeaders =
151
161
  });
152
162
  }
153
163
  else {
154
- res.writeHead(statusCode, {
155
- ...extraHeaders,
164
+ // Strip any pre-existing content-length (from the Web Response constructor)
165
+ // before setting our own — avoids duplicate Content-Length headers.
166
+ const { "content-length": _cl, "Content-Length": _CL, ...headersWithoutLength } = extraHeaders;
167
+ writeHead({
168
+ ...headersWithoutLength,
156
169
  "Content-Type": contentType,
157
170
  "Content-Length": String(buf.length),
158
171
  });
@@ -276,15 +289,21 @@ const trustedHosts = new Set((process.env.VINEXT_TRUSTED_HOSTS ?? "")
276
289
  const trustProxy = process.env.VINEXT_TRUST_PROXY === "1" || trustedHosts.size > 0;
277
290
  /**
278
291
  * Convert a Node.js IncomingMessage to a Web Request object.
292
+ *
293
+ * When `urlOverride` is provided, it is used as the path + query string
294
+ * instead of `req.url`. This avoids redundant path normalization when the
295
+ * caller has already decoded and normalized the pathname (e.g. the App
296
+ * Router prod server normalizes before static-asset lookup, and can pass
297
+ * the result here so the downstream RSC handler doesn't re-normalize).
279
298
  */
280
- function nodeToWebRequest(req) {
299
+ function nodeToWebRequest(req, urlOverride) {
281
300
  const rawProto = trustProxy
282
301
  ? req.headers["x-forwarded-proto"]?.split(",")[0]?.trim()
283
302
  : undefined;
284
303
  const proto = rawProto === "https" || rawProto === "http" ? rawProto : "http";
285
304
  const host = resolveHost(req, "localhost");
286
305
  const origin = `${proto}://${host}`;
287
- const url = new URL(req.url ?? "/", origin);
306
+ const url = new URL(urlOverride ?? req.url ?? "/", origin);
288
307
  const headers = new Headers();
289
308
  for (const [key, value] of Object.entries(req.headers)) {
290
309
  if (value === undefined)
@@ -317,6 +336,15 @@ function nodeToWebRequest(req) {
317
336
  */
318
337
  async function sendWebResponse(webResponse, req, res, compress) {
319
338
  const status = webResponse.status;
339
+ const statusText = webResponse.statusText || undefined;
340
+ const writeHead = (headers) => {
341
+ if (statusText) {
342
+ res.writeHead(status, statusText, headers);
343
+ }
344
+ else {
345
+ res.writeHead(status, headers);
346
+ }
347
+ };
320
348
  // Collect headers, handling multi-value headers (e.g. Set-Cookie)
321
349
  const nodeHeaders = {};
322
350
  webResponse.headers.forEach((value, key) => {
@@ -329,7 +357,7 @@ async function sendWebResponse(webResponse, req, res, compress) {
329
357
  }
330
358
  });
331
359
  if (!webResponse.body) {
332
- res.writeHead(status, nodeHeaders);
360
+ writeHead(nodeHeaders);
333
361
  res.end();
334
362
  return;
335
363
  }
@@ -358,7 +386,7 @@ async function sendWebResponse(webResponse, req, res, compress) {
358
386
  nodeHeaders["Vary"] = "Accept-Encoding";
359
387
  }
360
388
  }
361
- res.writeHead(status, nodeHeaders);
389
+ writeHead(nodeHeaders);
362
390
  // HEAD requests: send headers only, skip the body
363
391
  if (req.method === "HEAD") {
364
392
  res.end();
@@ -405,11 +433,36 @@ export async function startProdServer(options = {}) {
405
433
  }
406
434
  return startPagesRouterServer({ port, host, clientDir, serverEntryPath, compress });
407
435
  }
436
+ function createNodeExecutionContext() {
437
+ return {
438
+ waitUntil(promise) {
439
+ // Node doesn't provide a Workers lifecycle, but we still attach a
440
+ // rejection handler so background waitUntil work doesn't surface as an
441
+ // unhandled rejection when a Worker-style entry is used with vinext start.
442
+ void Promise.resolve(promise).catch(() => { });
443
+ },
444
+ passThroughOnException() { },
445
+ };
446
+ }
447
+ function resolveAppRouterHandler(entry) {
448
+ if (typeof entry === "function") {
449
+ return (request) => Promise.resolve(entry(request));
450
+ }
451
+ if (entry && typeof entry === "object" && "fetch" in entry) {
452
+ const workerEntry = entry;
453
+ if (typeof workerEntry.fetch === "function") {
454
+ return (request) => Promise.resolve(workerEntry.fetch(request, undefined, createNodeExecutionContext()));
455
+ }
456
+ }
457
+ console.error("[vinext] App Router entry must export either a default handler function or a Worker-style default export with fetch()");
458
+ process.exit(1);
459
+ }
408
460
  /**
409
461
  * Start the App Router production server.
410
462
  *
411
- * The RSC entry (dist/server/index.js) exports a default handler function:
412
- * handler(request: Request) → Promise<Response>
463
+ * The App Router entry (dist/server/index.js) can export either:
464
+ * - a default handler function: handler(request: Request) → Promise<Response>
465
+ * - a Worker-style object: { fetch(request, env, ctx) → Promise<Response> }
413
466
  *
414
467
  * This handler already does everything: route matching, RSC rendering,
415
468
  * SSR HTML generation (via import("./ssr/index.js")), route handlers,
@@ -437,18 +490,14 @@ async function startAppRouterServer(options) {
437
490
  }
438
491
  // Import the RSC handler (use file:// URL for reliable dynamic import)
439
492
  const rscModule = await import(pathToFileURL(rscEntryPath).href);
440
- const rscHandler = rscModule.default;
441
- if (typeof rscHandler !== "function") {
442
- console.error("[vinext] RSC entry does not export a default handler function");
443
- process.exit(1);
444
- }
493
+ const rscHandler = resolveAppRouterHandler(rscModule.default);
445
494
  const server = createServer(async (req, res) => {
446
- const url = req.url ?? "/";
495
+ const rawUrl = req.url ?? "/";
447
496
  // Normalize backslashes (browsers treat /\ as //), then decode and normalize path.
448
- const rawPathname = url.split("?")[0].replaceAll("\\", "/");
497
+ const rawPathname = rawUrl.split("?")[0].replaceAll("\\", "/");
449
498
  let pathname;
450
499
  try {
451
- pathname = normalizePath(decodeURIComponent(rawPathname));
500
+ pathname = normalizePath(normalizePathnameForRouteMatchStrict(rawPathname));
452
501
  }
453
502
  catch {
454
503
  // Malformed percent-encoding (e.g. /%E0%A4%A) — return 400 instead of crashing.
@@ -470,7 +519,7 @@ async function startAppRouterServer(options) {
470
519
  // Image optimization passthrough (Node.js prod server has no Images binding;
471
520
  // serves the original file with cache headers and security headers)
472
521
  if (pathname === IMAGE_OPTIMIZATION_PATH) {
473
- const parsedUrl = new URL(url, "http://localhost");
522
+ const parsedUrl = new URL(rawUrl, "http://localhost");
474
523
  const defaultAllowedWidths = [...DEFAULT_DEVICE_SIZES, ...DEFAULT_IMAGE_SIZES];
475
524
  const params = parseImageParams(parsedUrl, defaultAllowedWidths);
476
525
  if (!params) {
@@ -491,7 +540,7 @@ async function startAppRouterServer(options) {
491
540
  const imageSecurityHeaders = {
492
541
  "Content-Security-Policy": imageConfig?.contentSecurityPolicy ?? IMAGE_CONTENT_SECURITY_POLICY,
493
542
  "X-Content-Type-Options": "nosniff",
494
- "Content-Disposition": imageConfig?.contentDispositionType ?? "inline",
543
+ "Content-Disposition": imageConfig?.contentDispositionType === "attachment" ? "attachment" : "inline",
495
544
  };
496
545
  if (tryServeStatic(req, res, clientDir, params.imageUrl, false, imageSecurityHeaders)) {
497
546
  return;
@@ -501,8 +550,13 @@ async function startAppRouterServer(options) {
501
550
  return;
502
551
  }
503
552
  try {
553
+ // Build the normalized URL (pathname + original query string) so the
554
+ // RSC handler receives an already-canonical path and doesn't need to
555
+ // re-normalize. This deduplicates the normalizePath work done above.
556
+ const qs = rawUrl.includes("?") ? rawUrl.slice(rawUrl.indexOf("?")) : "";
557
+ const normalizedUrl = pathname + qs;
504
558
  // Convert Node.js request to Web Request and call the RSC handler
505
- const request = nodeToWebRequest(req);
559
+ const request = nodeToWebRequest(req, normalizedUrl);
506
560
  const response = await rscHandler(request);
507
561
  // Stream the Web Response back to the Node.js response
508
562
  await sendWebResponse(response, req, res, compress);
@@ -536,33 +590,12 @@ async function startAppRouterServer(options) {
536
590
  */
537
591
  async function startPagesRouterServer(options) {
538
592
  const { port, host, clientDir, serverEntryPath, compress } = options;
539
- // Load the SSR manifest (maps module URLs to client asset URLs)
540
- let ssrManifest = {};
541
- const manifestPath = path.join(clientDir, ".vite", "ssr-manifest.json");
542
- if (fs.existsSync(manifestPath)) {
543
- ssrManifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
544
- }
545
- // Load the build manifest to compute lazy chunks — chunks only reachable via
546
- // dynamic imports (React.lazy, next/dynamic). These should not be
547
- // modulepreloaded since they are fetched on demand.
548
- const buildManifestPath = path.join(clientDir, ".vite", "manifest.json");
549
- if (fs.existsSync(buildManifestPath)) {
550
- try {
551
- const buildManifest = JSON.parse(fs.readFileSync(buildManifestPath, "utf-8"));
552
- const lazyChunks = computeLazyChunks(buildManifest);
553
- if (lazyChunks.length > 0) {
554
- globalThis.__VINEXT_LAZY_CHUNKS__ = lazyChunks;
555
- }
556
- }
557
- catch {
558
- /* ignore parse errors */
559
- }
560
- }
561
593
  // Import the server entry module (use file:// URL for reliable dynamic import)
562
594
  const serverEntry = await import(pathToFileURL(serverEntryPath).href);
563
595
  const { renderPage, handleApiRoute: handleApi, runMiddleware, vinextConfig } = serverEntry;
564
596
  // Extract config values (embedded at build time in the server entry)
565
597
  const basePath = vinextConfig?.basePath ?? "";
598
+ const assetBase = basePath ? `${basePath}/` : "/";
566
599
  const trailingSlash = vinextConfig?.trailingSlash ?? false;
567
600
  const configRedirects = vinextConfig?.redirects ?? [];
568
601
  const configRewrites = vinextConfig?.rewrites ?? {
@@ -584,6 +617,28 @@ async function startPagesRouterServer(options) {
584
617
  contentSecurityPolicy: vinextConfig.images.contentSecurityPolicy,
585
618
  }
586
619
  : undefined;
620
+ // Load the SSR manifest (maps module URLs to client asset URLs)
621
+ let ssrManifest = {};
622
+ const manifestPath = path.join(clientDir, ".vite", "ssr-manifest.json");
623
+ if (fs.existsSync(manifestPath)) {
624
+ ssrManifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
625
+ }
626
+ // Load the build manifest to compute lazy chunks — chunks only reachable via
627
+ // dynamic imports (React.lazy, next/dynamic). These should not be
628
+ // modulepreloaded since they are fetched on demand.
629
+ const buildManifestPath = path.join(clientDir, ".vite", "manifest.json");
630
+ if (fs.existsSync(buildManifestPath)) {
631
+ try {
632
+ const buildManifest = JSON.parse(fs.readFileSync(buildManifestPath, "utf-8"));
633
+ const lazyChunks = computeLazyChunks(buildManifest).map((file) => manifestFileWithBase(file, assetBase));
634
+ if (lazyChunks.length > 0) {
635
+ globalThis.__VINEXT_LAZY_CHUNKS__ = lazyChunks;
636
+ }
637
+ }
638
+ catch {
639
+ /* ignore parse errors */
640
+ }
641
+ }
587
642
  const server = createServer(async (req, res) => {
588
643
  const rawUrl = req.url ?? "/";
589
644
  // Normalize backslashes (browsers treat /\ as //), then decode and normalize path.
@@ -594,7 +649,7 @@ async function startPagesRouterServer(options) {
594
649
  const rawQs = rawUrl.includes("?") ? rawUrl.slice(rawUrl.indexOf("?")) : "";
595
650
  let pathname;
596
651
  try {
597
- pathname = normalizePath(decodeURIComponent(rawPagesPathname));
652
+ pathname = normalizePath(normalizePathnameForRouteMatchStrict(rawPagesPathname));
598
653
  }
599
654
  catch {
600
655
  // Malformed percent-encoding (e.g. /%E0%A4%A) — return 400 instead of crashing.
@@ -641,7 +696,7 @@ async function startPagesRouterServer(options) {
641
696
  const imageSecurityHeaders = {
642
697
  "Content-Security-Policy": pagesImageConfig?.contentSecurityPolicy ?? IMAGE_CONTENT_SECURITY_POLICY,
643
698
  "X-Content-Type-Options": "nosniff",
644
- "Content-Disposition": pagesImageConfig?.contentDispositionType ?? "inline",
699
+ "Content-Disposition": pagesImageConfig?.contentDispositionType === "attachment" ? "attachment" : "inline",
645
700
  };
646
701
  if (tryServeStatic(req, res, clientDir, params.imageUrl, false, imageSecurityHeaders)) {
647
702
  return;
@@ -696,12 +751,31 @@ async function startPagesRouterServer(options) {
696
751
  // @ts-expect-error — duplex needed for streaming request bodies
697
752
  duplex: hasBody ? "half" : undefined,
698
753
  });
699
- // Build request context for has/missing condition matching.
700
- // headers and redirects run before middleware and use this pre-middleware
701
- // snapshot. beforeFiles, afterFiles, and fallback all run after middleware
702
- // per the Next.js execution order, so they use postMwReqCtx below.
754
+ // Build request context for pre-middleware config matching. Redirects
755
+ // run before middleware in Next.js. Header match conditions also use the
756
+ // original request snapshot even though header merging happens later so
757
+ // middleware response headers can still take precedence.
758
+ // beforeFiles, afterFiles, and fallback all run after middleware per the
759
+ // Next.js execution order, so they use postMwReqCtx below.
703
760
  const reqCtx = requestContextFromRequest(webRequest);
704
- // ── 4. Run middleware ─────────────────────────────────────────
761
+ // ── 4. Apply redirects from next.config.js ────────────────────
762
+ if (configRedirects.length) {
763
+ const redirect = matchRedirect(pathname, configRedirects, reqCtx);
764
+ if (redirect) {
765
+ // Guard against double-prefixing: only add basePath if destination
766
+ // doesn't already start with it.
767
+ // Sanitize the final destination to prevent protocol-relative URL open redirects.
768
+ const dest = sanitizeDestination(basePath &&
769
+ !isExternalUrl(redirect.destination) &&
770
+ !hasBasePath(redirect.destination, basePath)
771
+ ? basePath + redirect.destination
772
+ : redirect.destination);
773
+ res.writeHead(redirect.permanent ? 308 : 307, { Location: dest });
774
+ res.end();
775
+ return;
776
+ }
777
+ }
778
+ // ── 5. Run middleware ─────────────────────────────────────────
705
779
  let resolvedUrl = url;
706
780
  const middlewareHeaders = {};
707
781
  let middlewareRewriteStatus;
@@ -744,7 +818,12 @@ async function startPagesRouterServer(options) {
744
818
  const setCookies = result.response.headers.getSetCookie?.() ?? [];
745
819
  if (setCookies.length > 0)
746
820
  respHeaders["set-cookie"] = setCookies;
747
- res.writeHead(result.response.status, respHeaders);
821
+ if (result.response.statusText) {
822
+ res.writeHead(result.response.status, result.response.statusText, respHeaders);
823
+ }
824
+ else {
825
+ res.writeHead(result.response.status, respHeaders);
826
+ }
748
827
  res.end(body);
749
828
  return;
750
829
  }
@@ -784,15 +863,17 @@ async function startPagesRouterServer(options) {
784
863
  // middleware per the Next.js execution order).
785
864
  const { postMwReqCtx, request: postMwReq } = applyMiddlewareRequestHeaders(middlewareHeaders, webRequest);
786
865
  webRequest = postMwReq;
866
+ // Config header matching must keep using the original normalized pathname
867
+ // even if middleware rewrites the downstream route/render target.
787
868
  let resolvedPathname = resolvedUrl.split("?")[0];
788
- // ── 5. Apply custom headers from next.config.js ───────────────
869
+ // ── 6. Apply custom headers from next.config.js ───────────────
789
870
  // Config headers are additive for multi-value headers (Vary,
790
871
  // Set-Cookie) and override for everything else. Set-Cookie values
791
872
  // are stored as arrays (RFC 6265 forbids comma-joining cookies).
792
873
  // Middleware headers take precedence: skip config keys already set
793
874
  // by middleware so middleware always wins for the same key.
794
875
  if (configHeaders.length) {
795
- const matched = matchHeaders(resolvedPathname, configHeaders, reqCtx);
876
+ const matched = matchHeaders(pathname, configHeaders, reqCtx);
796
877
  for (const h of matched) {
797
878
  const lk = h.key.toLowerCase();
798
879
  if (lk === "set-cookie") {
@@ -817,23 +898,6 @@ async function startPagesRouterServer(options) {
817
898
  }
818
899
  }
819
900
  }
820
- // ── 6. Apply redirects from next.config.js ────────────────────
821
- if (configRedirects.length) {
822
- const redirect = matchRedirect(resolvedPathname, configRedirects, reqCtx);
823
- if (redirect) {
824
- // Guard against double-prefixing: only add basePath if destination
825
- // doesn't already start with it.
826
- // Sanitize the final destination to prevent protocol-relative URL open redirects.
827
- const dest = sanitizeDestination(basePath &&
828
- !isExternalUrl(redirect.destination) &&
829
- !hasBasePath(redirect.destination, basePath)
830
- ? basePath + redirect.destination
831
- : redirect.destination);
832
- res.writeHead(redirect.permanent ? 308 : 307, { Location: dest });
833
- res.end();
834
- return;
835
- }
836
- }
837
901
  // ── 7. Apply beforeFiles rewrites from next.config.js ─────────
838
902
  if (configRewrites.beforeFiles?.length) {
839
903
  const rewritten = matchRewrite(resolvedPathname, configRewrites.beforeFiles, postMwReqCtx);
@@ -863,7 +927,9 @@ async function startPagesRouterServer(options) {
863
927
  // the handler doesn't set an explicit Content-Type.
864
928
  const ct = response.headers.get("content-type") ?? "application/octet-stream";
865
929
  const responseHeaders = mergeResponseHeaders(middlewareHeaders, response);
866
- sendCompressed(req, res, responseBody, ct, middlewareRewriteStatus ?? response.status, responseHeaders, compress);
930
+ const finalStatus = middlewareRewriteStatus ?? response.status;
931
+ const finalStatusText = finalStatus === response.status ? response.statusText || undefined : undefined;
932
+ sendCompressed(req, res, responseBody, ct, finalStatus, responseHeaders, compress, finalStatusText);
867
933
  return;
868
934
  }
869
935
  // ── 9. Apply afterFiles rewrites from next.config.js ──────────
@@ -905,12 +971,16 @@ async function startPagesRouterServer(options) {
905
971
  const responseBody = Buffer.from(await response.arrayBuffer());
906
972
  const ct = response.headers.get("content-type") ?? "text/html";
907
973
  const responseHeaders = mergeResponseHeaders(middlewareHeaders, response);
908
- sendCompressed(req, res, responseBody, ct, middlewareRewriteStatus ?? response.status, responseHeaders, compress);
974
+ const finalStatus = middlewareRewriteStatus ?? response.status;
975
+ const finalStatusText = finalStatus === response.status ? response.statusText || undefined : undefined;
976
+ sendCompressed(req, res, responseBody, ct, finalStatus, responseHeaders, compress, finalStatusText);
909
977
  }
910
978
  catch (e) {
911
979
  console.error("[vinext] Server error:", e);
912
- res.writeHead(500);
913
- res.end("Internal Server Error");
980
+ if (!res.headersSent) {
981
+ res.writeHead(500);
982
+ res.end("Internal Server Error");
983
+ }
914
984
  }
915
985
  });
916
986
  await new Promise((resolve) => {