vinext 0.1.4 → 0.1.6

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 (176) hide show
  1. package/README.md +2 -2
  2. package/dist/build/css-url-assets.d.ts +1 -1
  3. package/dist/build/css-url-assets.js +9 -7
  4. package/dist/build/google-fonts/find-font-files-in-css.d.ts +16 -0
  5. package/dist/build/google-fonts/find-font-files-in-css.js +28 -0
  6. package/dist/build/prerender.js +2 -1
  7. package/dist/build/report.d.ts +1 -2
  8. package/dist/build/report.js +2 -11
  9. package/dist/build/run-prerender.js +1 -1
  10. package/dist/cache/cache-adapters-virtual.js +1 -1
  11. package/dist/check.js +3 -3
  12. package/dist/client/pages-router-link-navigation.d.ts +10 -0
  13. package/dist/client/pages-router-link-navigation.js +3 -3
  14. package/dist/cloudflare/src/cache/kv-data-adapter.runtime.d.ts +1 -1
  15. package/dist/deploy.js +3 -3
  16. package/dist/entries/app-rsc-entry.js +25 -20
  17. package/dist/entries/app-rsc-manifest.js +76 -46
  18. package/dist/entries/pages-server-entry.js +3 -1
  19. package/dist/index.js +254 -198
  20. package/dist/init.d.ts +5 -4
  21. package/dist/init.js +27 -8
  22. package/dist/plugins/css-data-url.js +30 -26
  23. package/dist/plugins/extensionless-dynamic-import.js +27 -24
  24. package/dist/plugins/fonts.js +3 -1
  25. package/dist/plugins/import-meta-url.js +21 -15
  26. package/dist/plugins/instrumentation-client.js +1 -1
  27. package/dist/plugins/middleware-export-validation.d.ts +5 -0
  28. package/dist/plugins/middleware-export-validation.js +36 -0
  29. package/dist/plugins/middleware-server-only.js +7 -6
  30. package/dist/plugins/og-asset-ownership.d.ts +27 -0
  31. package/dist/plugins/og-asset-ownership.js +285 -0
  32. package/dist/plugins/og-assets.js +66 -46
  33. package/dist/plugins/optimize-imports.js +9 -3
  34. package/dist/plugins/remove-console.d.ts +7 -1
  35. package/dist/plugins/remove-console.js +4 -1
  36. package/dist/plugins/require-context.js +21 -20
  37. package/dist/plugins/strip-server-exports.d.ts +7 -1
  38. package/dist/plugins/strip-server-exports.js +4 -1
  39. package/dist/server/accept-encoding.d.ts +29 -0
  40. package/dist/server/accept-encoding.js +95 -0
  41. package/dist/server/app-bfcache-identity.d.ts +26 -0
  42. package/dist/server/app-bfcache-identity.js +127 -0
  43. package/dist/server/app-browser-entry.js +17 -11
  44. package/dist/server/app-browser-rsc-redirect.d.ts +2 -1
  45. package/dist/server/app-browser-rsc-redirect.js +8 -1
  46. package/dist/server/app-browser-state.d.ts +2 -21
  47. package/dist/server/app-browser-state.js +4 -128
  48. package/dist/server/app-browser-stream.js +1 -1
  49. package/dist/server/app-browser-visible-commit.js +3 -2
  50. package/dist/server/app-fallback-renderer.d.ts +1 -1
  51. package/dist/server/app-layout-param-observation.d.ts +1 -1
  52. package/dist/server/app-layout-param-observation.js +1 -1
  53. package/dist/server/app-middleware.d.ts +1 -0
  54. package/dist/server/app-middleware.js +3 -1
  55. package/dist/server/app-page-boundary-render.d.ts +1 -1
  56. package/dist/server/app-page-boundary.js +1 -1
  57. package/dist/server/app-page-cache-finalizer.d.ts +62 -0
  58. package/dist/server/app-page-cache-finalizer.js +122 -0
  59. package/dist/server/app-page-cache-render.d.ts +2 -2
  60. package/dist/server/app-page-cache-render.js +1 -1
  61. package/dist/server/app-page-cache.d.ts +2 -53
  62. package/dist/server/app-page-cache.js +5 -131
  63. package/dist/server/app-page-dispatch.d.ts +4 -4
  64. package/dist/server/app-page-dispatch.js +10 -8
  65. package/dist/server/app-page-head.js +2 -2
  66. package/dist/server/app-page-probe.js +3 -2
  67. package/dist/server/app-page-render-observation.js +2 -2
  68. package/dist/server/app-page-render.d.ts +3 -3
  69. package/dist/server/app-page-render.js +3 -2
  70. package/dist/server/app-page-request.d.ts +7 -0
  71. package/dist/server/app-page-request.js +19 -1
  72. package/dist/server/app-page-stream.d.ts +2 -9
  73. package/dist/server/app-page-stream.js +1 -35
  74. package/dist/server/app-prerender-endpoints.d.ts +3 -3
  75. package/dist/server/app-prerender-endpoints.js +3 -4
  76. package/dist/server/app-request-context.d.ts +1 -2
  77. package/dist/server/app-request-context.js +2 -1
  78. package/dist/server/app-route-handler-dispatch.js +3 -2
  79. package/dist/server/app-route-handler-execution.d.ts +1 -1
  80. package/dist/server/app-route-handler-execution.js +1 -1
  81. package/dist/server/app-route-handler-response.d.ts +1 -1
  82. package/dist/server/app-route-module-loader.d.ts +62 -14
  83. package/dist/server/app-route-module-loader.js +61 -10
  84. package/dist/server/app-router-entry.js +2 -1
  85. package/dist/server/app-rsc-handler.d.ts +7 -6
  86. package/dist/server/app-rsc-handler.js +51 -40
  87. package/dist/server/app-rsc-response-finalizer.js +1 -1
  88. package/dist/server/app-rsc-route-matching.d.ts +9 -0
  89. package/dist/server/app-rsc-route-matching.js +13 -0
  90. package/dist/server/app-server-action-execution.d.ts +1 -1
  91. package/dist/server/app-server-action-execution.js +5 -4
  92. package/dist/server/app-ssr-entry.d.ts +1 -1
  93. package/dist/server/app-ssr-entry.js +9 -7
  94. package/dist/server/app-ssr-router-instance.d.ts +6 -0
  95. package/dist/server/app-ssr-router-instance.js +24 -0
  96. package/dist/server/app-ssr-stream.js +1 -1
  97. package/dist/server/artifact-compatibility.js +1 -1
  98. package/dist/server/client-reuse-manifest.js +1 -1
  99. package/dist/server/defer-until-stream-consumed.d.ts +7 -0
  100. package/dist/server/defer-until-stream-consumed.js +34 -0
  101. package/dist/server/dev-server.js +1 -1
  102. package/dist/server/headers.d.ts +5 -1
  103. package/dist/server/headers.js +5 -1
  104. package/dist/server/instrumentation.js +1 -1
  105. package/dist/server/isr-cache.d.ts +1 -1
  106. package/dist/server/isr-cache.js +1 -1
  107. package/dist/server/isr-decision.d.ts +1 -1
  108. package/dist/server/metadata-route-response.d.ts +3 -3
  109. package/dist/server/middleware-matcher.js +8 -6
  110. package/dist/server/middleware-runtime.d.ts +2 -1
  111. package/dist/server/middleware-runtime.js +21 -10
  112. package/dist/server/middleware.d.ts +2 -2
  113. package/dist/server/middleware.js +26 -11
  114. package/dist/server/open-redirect.d.ts +12 -0
  115. package/dist/server/open-redirect.js +21 -0
  116. package/dist/server/pages-page-data.d.ts +1 -1
  117. package/dist/server/pages-page-response.d.ts +1 -1
  118. package/dist/server/pages-page-response.js +2 -2
  119. package/dist/server/pages-request-pipeline.d.ts +3 -1
  120. package/dist/server/pages-request-pipeline.js +4 -2
  121. package/dist/server/prod-server.d.ts +1 -1
  122. package/dist/server/prod-server.js +87 -73
  123. package/dist/server/request-pipeline.d.ts +1 -24
  124. package/dist/server/request-pipeline.js +1 -33
  125. package/dist/server/seed-cache.d.ts +1 -1
  126. package/dist/shims/cache-handler.d.ts +106 -0
  127. package/dist/shims/cache-handler.js +176 -0
  128. package/dist/shims/cache-request-state.d.ts +47 -0
  129. package/dist/shims/cache-request-state.js +126 -0
  130. package/dist/shims/cache-runtime.d.ts +2 -2
  131. package/dist/shims/cache-runtime.js +3 -14
  132. package/dist/shims/cache.d.ts +3 -231
  133. package/dist/shims/cache.js +17 -383
  134. package/dist/shims/cdn-cache.d.ts +1 -1
  135. package/dist/shims/cdn-cache.js +1 -1
  136. package/dist/shims/error-boundary-navigation.d.ts +7 -0
  137. package/dist/shims/error-boundary-navigation.js +44 -0
  138. package/dist/shims/error-boundary.js +10 -8
  139. package/dist/shims/error.js +2 -1
  140. package/dist/shims/fetch-cache.js +1 -1
  141. package/dist/shims/font-google-base.d.ts +1 -0
  142. package/dist/shims/font-google-base.js +6 -21
  143. package/dist/shims/form.js +1 -1
  144. package/dist/shims/image.js +67 -9
  145. package/dist/shims/internal/app-page-props-cache-key.d.ts +5 -0
  146. package/dist/shims/internal/app-page-props-cache-key.js +16 -0
  147. package/dist/shims/internal/interpolate-as.d.ts +25 -0
  148. package/dist/shims/internal/interpolate-as.js +196 -0
  149. package/dist/shims/internal/navigation-untracked.js +2 -1
  150. package/dist/shims/layout-segment-context.d.ts +1 -1
  151. package/dist/shims/layout-segment-context.js +2 -1
  152. package/dist/shims/link.js +26 -3
  153. package/dist/shims/navigation-context-state.d.ts +40 -0
  154. package/dist/shims/navigation-context-state.js +116 -0
  155. package/dist/shims/navigation-errors.d.ts +55 -0
  156. package/dist/shims/navigation-errors.js +110 -0
  157. package/dist/shims/navigation-server.d.ts +3 -0
  158. package/dist/shims/navigation-server.js +3 -0
  159. package/dist/shims/navigation-state.d.ts +1 -2
  160. package/dist/shims/navigation-state.js +2 -1
  161. package/dist/shims/navigation.d.ts +3 -291
  162. package/dist/shims/navigation.js +13 -444
  163. package/dist/shims/navigation.react-server.d.ts +2 -2
  164. package/dist/shims/navigation.react-server.js +4 -2
  165. package/dist/shims/request-state-types.d.ts +3 -3
  166. package/dist/shims/router.d.ts +1 -3
  167. package/dist/shims/router.js +62 -18
  168. package/dist/shims/script.js +1 -1
  169. package/dist/shims/slot.js +3 -1
  170. package/dist/shims/unified-request-context.d.ts +2 -2
  171. package/dist/typegen.js +2 -10
  172. package/dist/utils/project.d.ts +7 -1
  173. package/dist/utils/project.js +16 -2
  174. package/dist/utils/virtual-module.d.ts +5 -0
  175. package/dist/utils/virtual-module.js +0 -0
  176. package/package.json +7 -3
@@ -1,10 +1,10 @@
1
1
  import { getRequestExecutionContext } from "../shims/request-context.js";
2
2
  import { reportRequestError } from "./instrumentation.js";
3
3
  import { setCacheStateHeaders } from "./cache-headers.js";
4
- import { fnv1a52 } from "../utils/hash.js";
5
- import { encodeCacheTag } from "../utils/encode-cache-tag.js";
6
4
  import { NEVER_CACHE_CONTROL, NO_STORE_CACHE_CONTROL, applyCdnResponseHeaders } from "./cache-control.js";
7
5
  import { buildMissIsrCacheControl } from "./isr-decision.js";
6
+ import { fnv1a52 } from "../utils/hash.js";
7
+ import { encodeCacheTag } from "../utils/encode-cache-tag.js";
8
8
  import { appendAssetDeploymentIdQuery } from "../utils/deployment-id.js";
9
9
  import { withScriptNonce } from "../shims/script-nonce-context.js";
10
10
  import { createNonceAttribute, escapeHtmlAttr } from "./html.js";
@@ -106,7 +106,9 @@ type PagesPipelineResult = {
106
106
  * ASSUMPTION: request already has internal headers filtered and basePath stripped.
107
107
  * The adapter is responsible for that pre-processing before calling runPagesRequest.
108
108
  * The adapter also handles: open-redirect guard, _next/static 404, image optimization,
109
- * _next/data normalization, Node decode/normalize/400, public-file serving.
109
+ * _next/data normalization and classification: adapters must rewrite the data
110
+ * URL to its page pathname and set `isDataReq` (the source of truth here), Node
111
+ * decode/normalize/400, public-file serving.
110
112
  * runPagesRequest receives a "clean" request with basePath-stripped URL.
111
113
  */
112
114
  declare function runPagesRequest(request: Request, deps: PagesPipelineDeps): Promise<PagesPipelineResult>;
@@ -40,7 +40,9 @@ function wrapMiddlewareWithBasePath(runMiddleware, basePath, hadBasePath) {
40
40
  * ASSUMPTION: request already has internal headers filtered and basePath stripped.
41
41
  * The adapter is responsible for that pre-processing before calling runPagesRequest.
42
42
  * The adapter also handles: open-redirect guard, _next/static 404, image optimization,
43
- * _next/data normalization, Node decode/normalize/400, public-file serving.
43
+ * _next/data normalization and classification: adapters must rewrite the data
44
+ * URL to its page pathname and set `isDataReq` (the source of truth here), Node
45
+ * decode/normalize/400, public-file serving.
44
46
  * runPagesRequest receives a "clean" request with basePath-stripped URL.
45
47
  */
46
48
  async function runPagesRequest(request, deps) {
@@ -54,7 +56,7 @@ async function runPagesRequest(request, deps) {
54
56
  hadBasePath
55
57
  };
56
58
  {
57
- const trailingSlashRedirect = normalizeTrailingSlash(pathname, basePath, trailingSlash, search);
59
+ const trailingSlashRedirect = isDataReq ? null : normalizeTrailingSlash(pathname, basePath, trailingSlash, search);
58
60
  if (trailingSlashRedirect) return {
59
61
  type: "response",
60
62
  response: trailingSlashRedirect
@@ -1,3 +1,4 @@
1
+ import { negotiateEncoding } from "./accept-encoding.js";
1
2
  import { StaticFileCache } from "./static-file-cache.js";
2
3
  import { resolveRequestHost, trustProxy, trustedHosts } from "./proxy-trust.js";
3
4
  import { IncomingMessage, ServerResponse } from "node:http";
@@ -59,7 +60,6 @@ type ProdServerOptions = {
59
60
  declare const COMPRESSIBLE_TYPES: Set<string>;
60
61
  /** Minimum size threshold for compression (in bytes). Below this, compression overhead isn't worth it. */
61
62
  declare const COMPRESS_THRESHOLD = 1024;
62
- declare function negotiateEncoding(req: IncomingMessage): "zstd" | "br" | "gzip" | "deflate" | null;
63
63
  /**
64
64
  * Merge middleware headers and a Web Response's headers into a single
65
65
  * record suitable for Node.js `res.writeHead()`. Uses `getSetCookie()`
@@ -3,7 +3,8 @@ import { hasBasePath, stripBasePath } from "../utils/base-path.js";
3
3
  import { VINEXT_PRERENDER_ROUTE_PARAMS_HEADER, VINEXT_PRERENDER_SECRET_HEADER, VINEXT_STATIC_FILE_HEADER } from "./headers.js";
4
4
  import { normalizePath } from "./normalize-path.js";
5
5
  import { notFoundResponse } from "./http-error-responses.js";
6
- import { filterInternalHeaders, isOpenRedirectShaped } from "./request-pipeline.js";
6
+ import { isOpenRedirectShaped } from "./open-redirect.js";
7
+ import { filterInternalHeaders } from "./request-pipeline.js";
7
8
  import { isUnknownRecord } from "../utils/record.js";
8
9
  import { resolveRequestHost, resolveRequestProtocol, trustProxy, trustedHosts } from "./proxy-trust.js";
9
10
  import { DEFAULT_DEVICE_SIZES, DEFAULT_IMAGE_SIZES, isImageOptimizationPath, isSafeImageContentType, parseImageParams } from "./image-optimization.js";
@@ -18,6 +19,7 @@ import { readTrustedPrerenderRouteParamsFromHeaders, serializePrerenderRoutePara
18
19
  import { computeClientRuntimeMetadata } from "../utils/client-runtime-metadata.js";
19
20
  import { readPrerenderSecret } from "../build/server-manifest.js";
20
21
  import { seedMemoryCacheFromPrerender } from "./seed-cache.js";
22
+ import { negotiateEncoding, parseAcceptedEncodings, selectContentEncoding } from "./accept-encoding.js";
21
23
  import fs from "node:fs";
22
24
  import path from "node:path";
23
25
  import fs$1 from "node:fs/promises";
@@ -166,25 +168,6 @@ const COMPRESSIBLE_TYPES = new Set([
166
168
  /** Minimum size threshold for compression (in bytes). Below this, compression overhead isn't worth it. */
167
169
  const COMPRESS_THRESHOLD = 1024;
168
170
  /**
169
- * Parse the Accept-Encoding header and return the best supported encoding.
170
- * Preference order: zstd > br > gzip > deflate > identity.
171
- *
172
- * zstd decompresses ~3-5x faster than brotli at similar compression ratios.
173
- * Supported in Chrome 123+, Firefox 126+. Safari can decompress but doesn't
174
- * send zstd in Accept-Encoding, so it transparently falls back to br/gzip.
175
- */
176
- const HAS_ZSTD = typeof zlib.createZstdCompress === "function";
177
- function negotiateEncoding(req) {
178
- const accept = req.headers["accept-encoding"];
179
- if (!accept || typeof accept !== "string") return null;
180
- const lower = accept.toLowerCase();
181
- if (HAS_ZSTD && lower.includes("zstd")) return "zstd";
182
- if (lower.includes("br")) return "br";
183
- if (lower.includes("gzip")) return "gzip";
184
- if (lower.includes("deflate")) return "deflate";
185
- return null;
186
- }
187
- /**
188
171
  * Create a compression stream for the given encoding.
189
172
  */
190
173
  function createCompressor(encoding, mode = "default") {
@@ -264,6 +247,23 @@ function omitHeadersCaseInsensitive(headersRecord, targets) {
264
247
  }
265
248
  return filtered;
266
249
  }
250
+ function mergeVaryHeader(headers, value) {
251
+ const merged = { ...headers };
252
+ const existingKey = Object.keys(merged).find((key) => key.toLowerCase() === "vary");
253
+ if (!existingKey) {
254
+ merged.Vary = value;
255
+ return merged;
256
+ }
257
+ const rawVary = merged[existingKey];
258
+ const existingVary = Array.isArray(rawVary) ? rawVary.join(", ") : rawVary;
259
+ if (existingVary.trim().length === 0) {
260
+ merged[existingKey] = value;
261
+ return merged;
262
+ }
263
+ const values = existingVary.split(",").map((entry) => entry.trim().toLowerCase());
264
+ if (!values.includes("*") && !values.includes(value.toLowerCase())) merged[existingKey] = `${existingVary}, ${value}`;
265
+ return merged;
266
+ }
267
267
  function matchesIfNoneMatchHeader(ifNoneMatch, etag) {
268
268
  if (!ifNoneMatch) return false;
269
269
  if (ifNoneMatch === "*") return true;
@@ -317,33 +317,37 @@ function mergeWebResponse(middlewareHeaders, response, statusOverride) {
317
317
  function sendCompressed(req, res, body, contentType, statusCode, extraHeaders = {}, compress = true, statusText) {
318
318
  const buf = typeof body === "string" ? Buffer.from(body) : body;
319
319
  const baseType = contentType.split(";")[0].trim();
320
- const encoding = compress ? negotiateEncoding(req) : null;
320
+ const varyByEncoding = compress && COMPRESSIBLE_TYPES.has(baseType);
321
+ const encoding = compress ? negotiateEncoding(req) : "identity";
321
322
  const headersWithoutBodyHeaders = omitHeadersCaseInsensitive(extraHeaders, OMIT_BODY_HEADERS);
322
- const writeHead = (headers) => {
323
- if (statusText) res.writeHead(statusCode, statusText, headers);
324
- else res.writeHead(statusCode, headers);
323
+ const writeHead = (headers, responseStatus = statusCode, responseStatusText = statusText) => {
324
+ if (responseStatusText) res.writeHead(responseStatus, responseStatusText, headers);
325
+ else res.writeHead(responseStatus, headers);
325
326
  };
326
- if (encoding && COMPRESSIBLE_TYPES.has(baseType) && buf.length >= 1024) {
327
- const compressor = createCompressor(encoding);
328
- const rawVary = extraHeaders["Vary"] ?? extraHeaders["vary"];
329
- const existingVary = Array.isArray(rawVary) ? rawVary.join(", ") : rawVary;
330
- let varyValue;
331
- if (existingVary) varyValue = existingVary.toLowerCase().includes("accept-encoding") ? existingVary : existingVary + ", Accept-Encoding";
332
- else varyValue = "Accept-Encoding";
333
- writeHead({
327
+ if (encoding !== "identity" && varyByEncoding && buf.length >= 1024) {
328
+ writeHead(mergeVaryHeader({
334
329
  ...headersWithoutBodyHeaders,
335
330
  "Content-Type": contentType,
336
- "Content-Encoding": encoding,
337
- Vary: varyValue
338
- });
331
+ "Content-Encoding": encoding
332
+ }, "Accept-Encoding"));
333
+ if (req.method === "HEAD") {
334
+ res.end();
335
+ return;
336
+ }
337
+ const compressor = createCompressor(encoding);
339
338
  compressor.end(buf);
340
339
  pipeline(compressor, res, () => {});
341
340
  } else {
342
- writeHead({
341
+ const identityHeaders = {
343
342
  ...headersWithoutBodyHeaders,
344
343
  "Content-Type": contentType,
345
344
  "Content-Length": String(buf.length)
346
- });
345
+ };
346
+ writeHead(varyByEncoding ? mergeVaryHeader(identityHeaders, "Accept-Encoding") : identityHeaders);
347
+ if (req.method === "HEAD") {
348
+ res.end();
349
+ return;
350
+ }
347
351
  res.end(buf);
348
352
  }
349
353
  }
@@ -377,24 +381,35 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
377
381
  }
378
382
  const entry = cache.lookup(lookupPath);
379
383
  if (!entry) return false;
384
+ const rawAe = compress ? req.headers["accept-encoding"] : void 0;
385
+ const parsed = typeof rawAe === "string" ? parseAcceptedEncodings(rawAe) : void 0;
386
+ const availableVariants = [
387
+ ...entry.zst ? ["zstd"] : [],
388
+ ...entry.br ? ["br"] : [],
389
+ ...entry.gz ? ["gzip"] : []
390
+ ];
391
+ const variesByEncoding = compress && availableVariants.length > 0;
392
+ const selected = parsed ? selectContentEncoding(parsed, availableVariants) : "identity";
393
+ const variant = selected === "zstd" ? entry.zst : selected === "br" ? entry.br : selected === "gzip" ? entry.gz : entry.original;
380
394
  const ifNoneMatch = req.headers["if-none-match"];
381
395
  if (responseStatus === 200 && typeof ifNoneMatch === "string" && matchesIfNoneMatchHeader(ifNoneMatch, entry.etag)) {
382
- if (extraHeaders) res.writeHead(304, {
396
+ const notModifiedHeaders = variesByEncoding ? mergeVaryHeader({
383
397
  ...entry.notModifiedHeaders,
384
398
  ...extraHeaders
385
- });
386
- else res.writeHead(304, entry.notModifiedHeaders);
399
+ }, "Accept-Encoding") : {
400
+ ...entry.notModifiedHeaders,
401
+ ...extraHeaders
402
+ };
403
+ if (selected !== "identity") notModifiedHeaders["Content-Encoding"] = selected;
404
+ res.writeHead(304, notModifiedHeaders);
387
405
  res.end();
388
406
  return true;
389
407
  }
390
- const rawAe = compress ? req.headers["accept-encoding"] : void 0;
391
- const ae = typeof rawAe === "string" ? rawAe.toLowerCase() : void 0;
392
- const variant = ae ? ae.includes("zstd") && entry.zst || ae.includes("br") && entry.br || ae.includes("gzip") && entry.gz || entry.original : entry.original;
393
- if (extraHeaders) res.writeHead(responseStatus, {
408
+ const responseHeaders = {
394
409
  ...variant.headers,
395
410
  ...extraHeaders
396
- });
397
- else res.writeHead(responseStatus, variant.headers);
411
+ };
412
+ res.writeHead(responseStatus, variesByEncoding ? mergeVaryHeader(responseHeaders, "Accept-Encoding") : responseHeaders);
398
413
  if (omitBody || req.method === "HEAD") {
399
414
  res.end();
400
415
  return true;
@@ -427,18 +442,6 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
427
442
  const etag = isHashed && etagFromFilenameHash(resolved.path, ext) || `W/"${resolved.size}-${Math.floor(resolved.mtimeMs / 1e3)}"`;
428
443
  const baseType = ct.split(";")[0].trim();
429
444
  const isCompressible = compress && COMPRESSIBLE_TYPES.has(baseType);
430
- const ifNoneMatch = req.headers["if-none-match"];
431
- if (responseStatus === 200 && typeof ifNoneMatch === "string" && matchesIfNoneMatchHeader(ifNoneMatch, etag)) {
432
- const notModifiedHeaders = {
433
- ETag: etag,
434
- "Cache-Control": cacheControl,
435
- ...isCompressible ? { Vary: "Accept-Encoding" } : void 0,
436
- ...extraHeaders
437
- };
438
- res.writeHead(304, notModifiedHeaders);
439
- res.end();
440
- return true;
441
- }
442
445
  const baseHeaders = {
443
446
  "Content-Type": ct,
444
447
  "Cache-Control": cacheControl,
@@ -447,12 +450,19 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
447
450
  };
448
451
  if (isCompressible) {
449
452
  const encoding = negotiateEncoding(req);
450
- if (encoding) {
451
- res.writeHead(responseStatus, {
453
+ const ifNoneMatch = req.headers["if-none-match"];
454
+ if (responseStatus === 200 && typeof ifNoneMatch === "string" && matchesIfNoneMatchHeader(ifNoneMatch, etag)) {
455
+ const notModifiedHeaders = mergeVaryHeader(baseHeaders, "Accept-Encoding");
456
+ if (encoding !== "identity") notModifiedHeaders["Content-Encoding"] = encoding;
457
+ res.writeHead(304, notModifiedHeaders);
458
+ res.end();
459
+ return true;
460
+ }
461
+ if (encoding !== "identity") {
462
+ res.writeHead(responseStatus, mergeVaryHeader({
452
463
  ...baseHeaders,
453
- "Content-Encoding": encoding,
454
- Vary: "Accept-Encoding"
455
- });
464
+ "Content-Encoding": encoding
465
+ }, "Accept-Encoding"));
456
466
  if (omitBody || req.method === "HEAD") {
457
467
  res.end();
458
468
  return true;
@@ -467,10 +477,17 @@ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, ex
467
477
  return true;
468
478
  }
469
479
  }
470
- res.writeHead(responseStatus, {
480
+ const ifNoneMatch = req.headers["if-none-match"];
481
+ if (responseStatus === 200 && typeof ifNoneMatch === "string" && matchesIfNoneMatchHeader(ifNoneMatch, etag)) {
482
+ res.writeHead(304, isCompressible ? mergeVaryHeader(baseHeaders, "Accept-Encoding") : baseHeaders);
483
+ res.end();
484
+ return true;
485
+ }
486
+ const identityHeaders = {
471
487
  ...baseHeaders,
472
488
  "Content-Length": String(resolved.size)
473
- });
489
+ };
490
+ res.writeHead(responseStatus, isCompressible ? mergeVaryHeader(identityHeaders, "Accept-Encoding") : identityHeaders);
474
491
  if (omitBody || req.method === "HEAD") {
475
492
  res.end();
476
493
  return true;
@@ -567,25 +584,22 @@ async function sendWebResponse(webResponse, req, res, compress) {
567
584
  if (existing !== void 0) nodeHeaders[key] = Array.isArray(existing) ? [...existing, value] : [existing, value];
568
585
  else nodeHeaders[key] = value;
569
586
  });
587
+ const alreadyEncoded = webResponse.headers.get("content-encoding") !== null;
570
588
  if (!webResponse.body) {
571
589
  writeHead(nodeHeaders);
572
590
  res.end();
573
591
  return;
574
592
  }
575
- const alreadyEncoded = webResponse.headers.has("content-encoding");
576
593
  const baseType = (webResponse.headers.get("content-type") ?? "").split(";")[0].trim();
577
- const encoding = compress && !alreadyEncoded ? negotiateEncoding(req) : null;
578
- const shouldCompress = !!(encoding && COMPRESSIBLE_TYPES.has(baseType));
594
+ const varyByEncoding = compress && !alreadyEncoded && COMPRESSIBLE_TYPES.has(baseType);
595
+ const encoding = compress && !alreadyEncoded ? negotiateEncoding(req) : "identity";
596
+ const shouldCompress = encoding !== "identity" && COMPRESSIBLE_TYPES.has(baseType);
579
597
  if (shouldCompress) {
580
598
  delete nodeHeaders["content-length"];
581
599
  delete nodeHeaders["Content-Length"];
582
600
  nodeHeaders["Content-Encoding"] = encoding;
583
- const existingVary = nodeHeaders["Vary"] ?? nodeHeaders["vary"];
584
- if (existingVary) {
585
- if (!String(existingVary).toLowerCase().includes("accept-encoding")) nodeHeaders["Vary"] = existingVary + ", Accept-Encoding";
586
- } else nodeHeaders["Vary"] = "Accept-Encoding";
587
601
  }
588
- writeHead(nodeHeaders);
602
+ writeHead(varyByEncoding ? mergeVaryHeader(nodeHeaders, "Accept-Encoding") : nodeHeaders);
589
603
  if (req.method === "HEAD") {
590
604
  cancelResponseBody(webResponse);
591
605
  res.end();
@@ -1,6 +1,7 @@
1
1
  import { NextHeader } from "../config/next-config.js";
2
2
  import { BasePathMatchState, RequestContext } from "../config/config-matchers.js";
3
3
  import { INTERNAL_HEADERS, VINEXT_INTERNAL_HEADERS } from "./headers.js";
4
+ import { isOpenRedirectShaped } from "./open-redirect.js";
4
5
  import { hasBasePath, stripBasePath } from "../utils/base-path.js";
5
6
 
6
7
  //#region src/server/request-pipeline.d.ts
@@ -42,30 +43,6 @@ import { hasBasePath, stripBasePath } from "../utils/base-path.js";
42
43
  * @returns A 404 Response if the path is protocol-relative, or null to continue
43
44
  */
44
45
  declare function guardProtocolRelativeUrl(rawPathname: string): Response | null;
45
- /**
46
- * Returns true if a request pathname looks like a protocol-relative open
47
- * redirect, in either literal or percent-encoded form.
48
- *
49
- * Exported for call sites that need to replicate the guard inline (Pages
50
- * Router worker codegen, Node production server) and for defense-in-depth
51
- * checks inside redirect emitters.
52
- *
53
- * A pathname is considered "open redirect shaped" when its first segment,
54
- * after decoding backslashes and encoded delimiters, would cause a browser
55
- * to resolve a `Location` containing the pathname as protocol-relative:
56
- *
57
- * - literal `//evil.com`
58
- * - literal `/\evil.com` (browsers normalize `\` to `/`)
59
- * - encoded `/%5Cevil.com` (`%5C` decodes to `\` in Location)
60
- * - encoded `/%2F/evil.com` (`%2F` decodes to `/` → `//`)
61
- * - mixed `/%5C%2F`, `/%5C%5C` (and other combinations)
62
- *
63
- * We explicitly do not require a valid percent sequence elsewhere in the
64
- * pathname — we only examine the leading bytes (up to the second real or
65
- * encoded delimiter) so malformed suffixes can still reach the normal
66
- * "400 Bad Request" decode path instead of being masked as "404".
67
- */
68
- declare function isOpenRedirectShaped(rawPathname: string): boolean;
69
46
  type HeaderRecord = Record<string, string | string[]>;
70
47
  type ApplyConfigHeadersOptions = {
71
48
  configHeaders: NextHeader[];
@@ -2,6 +2,7 @@ import { hasBasePath, removeTrailingSlash, stripBasePath } from "../utils/base-p
2
2
  import { INTERNAL_HEADERS, VINEXT_INTERNAL_HEADERS, VINEXT_STATIC_FILE_HEADER } from "./headers.js";
3
3
  import { matchHeaders } from "../config/config-matchers.js";
4
4
  import { forbiddenResponse, notFoundResponse } from "./http-error-responses.js";
5
+ import { isOpenRedirectShaped } from "./open-redirect.js";
5
6
  //#region src/server/request-pipeline.ts
6
7
  /**
7
8
  * Shared request pipeline utilities.
@@ -44,39 +45,6 @@ function guardProtocolRelativeUrl(rawPathname) {
44
45
  if (isOpenRedirectShaped(rawPathname)) return notFoundResponse();
45
46
  return null;
46
47
  }
47
- /**
48
- * Returns true if a request pathname looks like a protocol-relative open
49
- * redirect, in either literal or percent-encoded form.
50
- *
51
- * Exported for call sites that need to replicate the guard inline (Pages
52
- * Router worker codegen, Node production server) and for defense-in-depth
53
- * checks inside redirect emitters.
54
- *
55
- * A pathname is considered "open redirect shaped" when its first segment,
56
- * after decoding backslashes and encoded delimiters, would cause a browser
57
- * to resolve a `Location` containing the pathname as protocol-relative:
58
- *
59
- * - literal `//evil.com`
60
- * - literal `/\evil.com` (browsers normalize `\` to `/`)
61
- * - encoded `/%5Cevil.com` (`%5C` decodes to `\` in Location)
62
- * - encoded `/%2F/evil.com` (`%2F` decodes to `/` → `//`)
63
- * - mixed `/%5C%2F`, `/%5C%5C` (and other combinations)
64
- *
65
- * We explicitly do not require a valid percent sequence elsewhere in the
66
- * pathname — we only examine the leading bytes (up to the second real or
67
- * encoded delimiter) so malformed suffixes can still reach the normal
68
- * "400 Bad Request" decode path instead of being masked as "404".
69
- */
70
- function isOpenRedirectShaped(rawPathname) {
71
- if (!rawPathname.startsWith("/")) return false;
72
- const afterSlash = rawPathname.slice(1);
73
- if (afterSlash.startsWith("/") || afterSlash.startsWith("\\")) return true;
74
- if (afterSlash.length >= 3 && afterSlash[0] === "%") {
75
- const encoded = afterSlash.slice(0, 3).toLowerCase();
76
- if (encoded === "%5c" || encoded === "%2f") return true;
77
- }
78
- return false;
79
- }
80
48
  const FILE_LIKE_PATHNAME_RE = /\.[^/]+\/?$/;
81
49
  function isWellKnownPathname(pathname) {
82
50
  return pathname === "/.well-known" || pathname.startsWith("/.well-known/");
@@ -1,4 +1,4 @@
1
- import { CachedAppPageValue } from "../shims/cache.js";
1
+ import { CachedAppPageValue } from "../shims/cache-handler.js";
2
2
 
3
3
  //#region src/server/seed-cache.d.ts
4
4
  type PrerenderCacheSeedMetadata = {
@@ -0,0 +1,106 @@
1
+ import { RenderObservation } from "../server/cache-proof.js";
2
+
3
+ //#region src/shims/cache-handler.d.ts
4
+ type CacheHandlerValue = {
5
+ lastModified: number;
6
+ age?: number;
7
+ cacheState?: string;
8
+ cacheControl?: CacheControlMetadata;
9
+ value: IncrementalCacheValue | null;
10
+ };
11
+ type CacheControlMetadata = {
12
+ revalidate: number;
13
+ expire?: number;
14
+ };
15
+ type IncrementalCacheValue = CachedFetchValue | CachedAppPageValue | CachedPagesValue | CachedRouteValue | CachedRedirectValue | CachedImageValue;
16
+ type CachedFetchValue = {
17
+ kind: "FETCH";
18
+ data: {
19
+ headers: Record<string, string>;
20
+ body: string;
21
+ url: string;
22
+ status?: number;
23
+ };
24
+ tags?: string[];
25
+ revalidate: number | false;
26
+ };
27
+ type CachedAppPageValue = {
28
+ kind: "APP_PAGE";
29
+ html: string;
30
+ rscData: ArrayBuffer | undefined;
31
+ headers: Record<string, string | string[]> | undefined;
32
+ postponed: string | undefined;
33
+ renderObservation?: RenderObservation;
34
+ status: number | undefined;
35
+ };
36
+ type CachedPagesValue = {
37
+ kind: "PAGES";
38
+ html: string;
39
+ pageData: object;
40
+ generatedFromDataRequest?: boolean;
41
+ headers: Record<string, string | string[]> | undefined;
42
+ status: number | undefined;
43
+ };
44
+ type CachedRouteValue = {
45
+ kind: "APP_ROUTE";
46
+ body: ArrayBuffer;
47
+ status: number;
48
+ headers: Record<string, string | string[]>;
49
+ };
50
+ type CachedRedirectValue = {
51
+ kind: "REDIRECT";
52
+ props: object;
53
+ };
54
+ type CachedImageValue = {
55
+ kind: "IMAGE";
56
+ etag: string;
57
+ buffer: ArrayBuffer;
58
+ extension: string;
59
+ revalidate?: number;
60
+ };
61
+ type CacheHandlerContext = {
62
+ dev?: boolean;
63
+ maxMemoryCacheSize?: number;
64
+ revalidatedTags?: string[];
65
+ [key: string]: unknown;
66
+ };
67
+ type CacheHandler = {
68
+ get(key: string, ctx?: Record<string, unknown>): Promise<CacheHandlerValue | null>;
69
+ set(key: string, data: IncrementalCacheValue | null, ctx?: Record<string, unknown>): Promise<void>;
70
+ revalidateTag(tags: string | string[], durations?: {
71
+ expire?: number;
72
+ }): Promise<void>;
73
+ resetRequestCache?(): void;
74
+ };
75
+ declare class NoOpCacheHandler implements CacheHandler {
76
+ get(_key: string, _ctx?: Record<string, unknown>): Promise<CacheHandlerValue | null>;
77
+ set(_key: string, _data: IncrementalCacheValue | null, _ctx?: Record<string, unknown>): Promise<void>;
78
+ revalidateTag(_tags: string | string[], _durations?: {
79
+ expire?: number;
80
+ }): Promise<void>;
81
+ }
82
+ type MemoryCacheHandlerOptions = Pick<CacheHandlerContext, "maxMemoryCacheSize"> & {
83
+ cacheMaxMemorySize?: number;
84
+ };
85
+ declare class MemoryCacheHandler implements CacheHandler {
86
+ private store;
87
+ private tagRevalidatedAt;
88
+ private readonly maxMemoryCacheSize;
89
+ private currentMemoryCacheSize;
90
+ constructor(options?: number | MemoryCacheHandlerOptions);
91
+ private estimateEntrySize;
92
+ private deleteEntry;
93
+ private touchEntry;
94
+ private evictLeastRecentlyUsed;
95
+ get(key: string, ctx?: Record<string, unknown>): Promise<CacheHandlerValue | null>;
96
+ set(key: string, data: IncrementalCacheValue | null, ctx?: Record<string, unknown>): Promise<void>;
97
+ revalidateTag(tags: string | string[]): Promise<void>;
98
+ resetRequestCache(): void;
99
+ }
100
+ declare function configureMemoryCacheHandler(options?: MemoryCacheHandlerOptions): void;
101
+ declare function setDataCacheHandler(handler: CacheHandler): void;
102
+ declare function getDataCacheHandler(): CacheHandler;
103
+ declare function setCacheHandler(handler: CacheHandler): void;
104
+ declare function getCacheHandler(): CacheHandler;
105
+ //#endregion
106
+ export { CacheControlMetadata, CacheHandler, CacheHandlerContext, CacheHandlerValue, CachedAppPageValue, CachedFetchValue, CachedImageValue, CachedPagesValue, CachedRedirectValue, CachedRouteValue, IncrementalCacheValue, MemoryCacheHandler, NoOpCacheHandler, configureMemoryCacheHandler, getCacheHandler, getDataCacheHandler, setCacheHandler, setDataCacheHandler };