vinext 0.0.38 → 0.0.40

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 (245) hide show
  1. package/README.md +33 -20
  2. package/dist/build/nitro-route-rules.d.ts +50 -0
  3. package/dist/build/nitro-route-rules.js +81 -0
  4. package/dist/build/nitro-route-rules.js.map +1 -0
  5. package/dist/build/precompress.d.ts +17 -0
  6. package/dist/build/precompress.js +102 -0
  7. package/dist/build/precompress.js.map +1 -0
  8. package/dist/build/prerender.d.ts +27 -22
  9. package/dist/build/prerender.js +17 -17
  10. package/dist/build/prerender.js.map +1 -1
  11. package/dist/build/report.d.ts +3 -4
  12. package/dist/build/report.js.map +1 -1
  13. package/dist/build/run-prerender.d.ts +3 -4
  14. package/dist/build/run-prerender.js.map +1 -1
  15. package/dist/build/standalone.d.ts +32 -0
  16. package/dist/build/standalone.js +206 -0
  17. package/dist/build/standalone.js.map +1 -0
  18. package/dist/build/static-export.d.ts +17 -29
  19. package/dist/build/static-export.js.map +1 -1
  20. package/dist/check.d.ts +4 -4
  21. package/dist/check.js +1 -1
  22. package/dist/check.js.map +1 -1
  23. package/dist/cli.js +31 -4
  24. package/dist/cli.js.map +1 -1
  25. package/dist/client/instrumentation-client.d.ts +2 -2
  26. package/dist/client/instrumentation-client.js.map +1 -1
  27. package/dist/client/vinext-next-data.d.ts +5 -8
  28. package/dist/cloudflare/index.js +1 -1
  29. package/dist/cloudflare/kv-cache-handler.d.ts +5 -3
  30. package/dist/cloudflare/kv-cache-handler.js +1 -1
  31. package/dist/cloudflare/kv-cache-handler.js.map +1 -1
  32. package/dist/cloudflare/tpr.d.ts +35 -27
  33. package/dist/cloudflare/tpr.js +36 -12
  34. package/dist/cloudflare/tpr.js.map +1 -1
  35. package/dist/config/config-matchers.d.ts +2 -2
  36. package/dist/config/config-matchers.js +1 -1
  37. package/dist/config/config-matchers.js.map +1 -1
  38. package/dist/config/dotenv.d.ts +4 -4
  39. package/dist/config/dotenv.js.map +1 -1
  40. package/dist/config/next-config.d.ts +40 -61
  41. package/dist/config/next-config.js +5 -4
  42. package/dist/config/next-config.js.map +1 -1
  43. package/dist/deploy.d.ts +25 -41
  44. package/dist/deploy.js +1 -1
  45. package/dist/deploy.js.map +1 -1
  46. package/dist/entries/app-rsc-entry.d.ts +8 -11
  47. package/dist/entries/app-rsc-entry.js +133 -249
  48. package/dist/entries/app-rsc-entry.js.map +1 -1
  49. package/dist/entries/pages-server-entry.js +1 -3
  50. package/dist/entries/pages-server-entry.js.map +1 -1
  51. package/dist/index.d.ts +49 -28
  52. package/dist/index.js +238 -83
  53. package/dist/index.js.map +1 -1
  54. package/dist/init.d.ts +14 -26
  55. package/dist/init.js +8 -2
  56. package/dist/init.js.map +1 -1
  57. package/dist/plugins/client-reference-dedup.js.map +1 -1
  58. package/dist/plugins/fix-use-server-closure-collision.js.map +1 -1
  59. package/dist/plugins/fonts.d.ts +18 -1
  60. package/dist/plugins/fonts.js +107 -8
  61. package/dist/plugins/fonts.js.map +1 -1
  62. package/dist/plugins/optimize-imports.d.ts +2 -2
  63. package/dist/plugins/optimize-imports.js +4 -4
  64. package/dist/plugins/optimize-imports.js.map +1 -1
  65. package/dist/plugins/server-externals-manifest.d.ts +37 -0
  66. package/dist/plugins/server-externals-manifest.js +83 -0
  67. package/dist/plugins/server-externals-manifest.js.map +1 -0
  68. package/dist/routing/app-router.d.ts +37 -55
  69. package/dist/routing/app-router.js +37 -22
  70. package/dist/routing/app-router.js.map +1 -1
  71. package/dist/routing/file-matcher.d.ts +2 -2
  72. package/dist/routing/file-matcher.js.map +1 -1
  73. package/dist/routing/pages-router.d.ts +6 -11
  74. package/dist/routing/pages-router.js.map +1 -1
  75. package/dist/routing/route-trie.d.ts +2 -2
  76. package/dist/routing/route-trie.js.map +1 -1
  77. package/dist/server/api-handler.js.map +1 -1
  78. package/dist/server/app-browser-entry.js +270 -39
  79. package/dist/server/app-browser-entry.js.map +1 -1
  80. package/dist/server/app-browser-stream.d.ts +6 -6
  81. package/dist/server/app-browser-stream.js.map +1 -1
  82. package/dist/server/app-page-boundary-render.d.ts +8 -8
  83. package/dist/server/app-page-boundary-render.js +2 -2
  84. package/dist/server/app-page-boundary-render.js.map +1 -1
  85. package/dist/server/app-page-boundary.d.ts +13 -11
  86. package/dist/server/app-page-boundary.js +1 -1
  87. package/dist/server/app-page-boundary.js.map +1 -1
  88. package/dist/server/app-page-cache.d.ts +10 -10
  89. package/dist/server/app-page-cache.js.map +1 -1
  90. package/dist/server/app-page-execution.d.ts +10 -10
  91. package/dist/server/app-page-execution.js.map +1 -1
  92. package/dist/server/app-page-probe.d.ts +2 -2
  93. package/dist/server/app-page-probe.js.map +1 -1
  94. package/dist/server/app-page-render.d.ts +4 -4
  95. package/dist/server/app-page-render.js.map +1 -1
  96. package/dist/server/app-page-request.d.ts +12 -12
  97. package/dist/server/app-page-request.js.map +1 -1
  98. package/dist/server/app-page-response.d.ts +30 -19
  99. package/dist/server/app-page-response.js +26 -7
  100. package/dist/server/app-page-response.js.map +1 -1
  101. package/dist/server/app-page-route-wiring.d.ts +79 -0
  102. package/dist/server/app-page-route-wiring.js +165 -0
  103. package/dist/server/app-page-route-wiring.js.map +1 -0
  104. package/dist/server/app-page-stream.d.ts +18 -18
  105. package/dist/server/app-page-stream.js +3 -0
  106. package/dist/server/app-page-stream.js.map +1 -1
  107. package/dist/server/app-route-handler-cache.d.ts +2 -2
  108. package/dist/server/app-route-handler-cache.js.map +1 -1
  109. package/dist/server/app-route-handler-execution.d.ts +6 -6
  110. package/dist/server/app-route-handler-execution.js.map +1 -1
  111. package/dist/server/app-route-handler-policy.d.ts +8 -8
  112. package/dist/server/app-route-handler-policy.js.map +1 -1
  113. package/dist/server/app-route-handler-response.d.ts +6 -6
  114. package/dist/server/app-route-handler-response.js +4 -1
  115. package/dist/server/app-route-handler-response.js.map +1 -1
  116. package/dist/server/app-route-handler-runtime.d.ts +4 -4
  117. package/dist/server/app-route-handler-runtime.js.map +1 -1
  118. package/dist/server/app-router-entry.d.ts +6 -1
  119. package/dist/server/app-router-entry.js +9 -2
  120. package/dist/server/app-router-entry.js.map +1 -1
  121. package/dist/server/app-ssr-entry.d.ts +4 -4
  122. package/dist/server/app-ssr-entry.js.map +1 -1
  123. package/dist/server/app-ssr-stream.d.ts +2 -2
  124. package/dist/server/app-ssr-stream.js +1 -3
  125. package/dist/server/app-ssr-stream.js.map +1 -1
  126. package/dist/server/dev-module-runner.d.ts +2 -2
  127. package/dist/server/dev-module-runner.js.map +1 -1
  128. package/dist/server/dev-server.js +5 -7
  129. package/dist/server/dev-server.js.map +1 -1
  130. package/dist/server/image-optimization.d.ts +7 -12
  131. package/dist/server/image-optimization.js.map +1 -1
  132. package/dist/server/instrumentation.d.ts +8 -12
  133. package/dist/server/instrumentation.js +1 -1
  134. package/dist/server/instrumentation.js.map +1 -1
  135. package/dist/server/isr-cache.d.ts +2 -2
  136. package/dist/server/isr-cache.js.map +1 -1
  137. package/dist/server/metadata-routes.d.ts +14 -19
  138. package/dist/server/metadata-routes.js.map +1 -1
  139. package/dist/server/middleware.d.ts +9 -17
  140. package/dist/server/middleware.js +1 -1
  141. package/dist/server/middleware.js.map +1 -1
  142. package/dist/server/pages-api-route.d.ts +6 -6
  143. package/dist/server/pages-api-route.js.map +1 -1
  144. package/dist/server/pages-i18n.d.ts +4 -4
  145. package/dist/server/pages-i18n.js.map +1 -1
  146. package/dist/server/pages-node-compat.d.ts +10 -10
  147. package/dist/server/pages-node-compat.js.map +1 -1
  148. package/dist/server/pages-page-data.d.ts +22 -22
  149. package/dist/server/pages-page-data.js.map +1 -1
  150. package/dist/server/pages-page-response.d.ts +8 -8
  151. package/dist/server/pages-page-response.js.map +1 -1
  152. package/dist/server/prod-server.d.ts +20 -15
  153. package/dist/server/prod-server.js +198 -55
  154. package/dist/server/prod-server.js.map +1 -1
  155. package/dist/server/seed-cache.js.map +1 -1
  156. package/dist/server/static-file-cache.d.ts +57 -0
  157. package/dist/server/static-file-cache.js +219 -0
  158. package/dist/server/static-file-cache.js.map +1 -0
  159. package/dist/server/worker-utils.d.ts +4 -1
  160. package/dist/server/worker-utils.js +31 -1
  161. package/dist/server/worker-utils.js.map +1 -1
  162. package/dist/shims/app.d.ts +2 -2
  163. package/dist/shims/cache-runtime.d.ts +6 -9
  164. package/dist/shims/cache-runtime.js.map +1 -1
  165. package/dist/shims/cache.d.ts +28 -31
  166. package/dist/shims/cache.js.map +1 -1
  167. package/dist/shims/config.d.ts +2 -2
  168. package/dist/shims/config.js.map +1 -1
  169. package/dist/shims/dynamic.d.ts +2 -2
  170. package/dist/shims/dynamic.js +5 -7
  171. package/dist/shims/dynamic.js.map +1 -1
  172. package/dist/shims/error-boundary.d.ts +19 -10
  173. package/dist/shims/error-boundary.js +23 -3
  174. package/dist/shims/error-boundary.js.map +1 -1
  175. package/dist/shims/error.d.ts +2 -2
  176. package/dist/shims/error.js.map +1 -1
  177. package/dist/shims/fetch-cache.d.ts +4 -4
  178. package/dist/shims/fetch-cache.js.map +1 -1
  179. package/dist/shims/font-google-base.d.ts +4 -4
  180. package/dist/shims/font-google-base.js.map +1 -1
  181. package/dist/shims/font-local.d.ts +6 -6
  182. package/dist/shims/font-local.js.map +1 -1
  183. package/dist/shims/form.d.ts +4 -8
  184. package/dist/shims/form.js +4 -6
  185. package/dist/shims/form.js.map +1 -1
  186. package/dist/shims/head-state.d.ts +2 -2
  187. package/dist/shims/head-state.js.map +1 -1
  188. package/dist/shims/head.d.ts +2 -2
  189. package/dist/shims/head.js +18 -20
  190. package/dist/shims/head.js.map +1 -1
  191. package/dist/shims/headers.d.ts +4 -4
  192. package/dist/shims/headers.js.map +1 -1
  193. package/dist/shims/i18n-context.d.ts +2 -2
  194. package/dist/shims/i18n-context.js.map +1 -1
  195. package/dist/shims/i18n-state.d.ts +2 -2
  196. package/dist/shims/i18n-state.js.map +1 -1
  197. package/dist/shims/image-config.d.ts +2 -2
  198. package/dist/shims/image-config.js.map +1 -1
  199. package/dist/shims/image.d.ts +5 -6
  200. package/dist/shims/image.js.map +1 -1
  201. package/dist/shims/internal/app-router-context.d.ts +6 -6
  202. package/dist/shims/internal/app-router-context.js.map +1 -1
  203. package/dist/shims/internal/utils.d.ts +2 -2
  204. package/dist/shims/internal/utils.js.map +1 -1
  205. package/dist/shims/layout-segment-context.d.ts +12 -5
  206. package/dist/shims/layout-segment-context.js +9 -4
  207. package/dist/shims/layout-segment-context.js.map +1 -1
  208. package/dist/shims/legacy-image.d.ts +5 -8
  209. package/dist/shims/legacy-image.js.map +1 -1
  210. package/dist/shims/link.d.ts +21 -31
  211. package/dist/shims/link.js +4 -58
  212. package/dist/shims/link.js.map +1 -1
  213. package/dist/shims/metadata.d.ts +23 -31
  214. package/dist/shims/metadata.js.map +1 -1
  215. package/dist/shims/navigation-state.d.ts +2 -2
  216. package/dist/shims/navigation-state.js.map +1 -1
  217. package/dist/shims/navigation.d.ts +118 -18
  218. package/dist/shims/navigation.js +377 -116
  219. package/dist/shims/navigation.js.map +1 -1
  220. package/dist/shims/request-context.d.ts +2 -2
  221. package/dist/shims/request-context.js.map +1 -1
  222. package/dist/shims/router-state.d.ts +4 -4
  223. package/dist/shims/router-state.js.map +1 -1
  224. package/dist/shims/router.d.ts +28 -47
  225. package/dist/shims/router.js +127 -38
  226. package/dist/shims/router.js.map +1 -1
  227. package/dist/shims/script.d.ts +16 -31
  228. package/dist/shims/script.js.map +1 -1
  229. package/dist/shims/server.d.ts +27 -14
  230. package/dist/shims/server.js +91 -73
  231. package/dist/shims/server.js.map +1 -1
  232. package/dist/shims/slot.d.ts +28 -0
  233. package/dist/shims/slot.js +49 -0
  234. package/dist/shims/slot.js.map +1 -0
  235. package/dist/shims/unified-request-context.d.ts +3 -5
  236. package/dist/shims/unified-request-context.js.map +1 -1
  237. package/dist/shims/web-vitals.d.ts +2 -2
  238. package/dist/shims/web-vitals.js.map +1 -1
  239. package/dist/utils/lazy-chunks.d.ts +34 -0
  240. package/dist/utils/lazy-chunks.js +50 -0
  241. package/dist/utils/lazy-chunks.js.map +1 -0
  242. package/dist/utils/vinext-root.d.ts +24 -0
  243. package/dist/utils/vinext-root.js +31 -0
  244. package/dist/utils/vinext-root.js.map +1 -0
  245. package/package.json +1 -2
@@ -3,16 +3,18 @@ import { applyMiddlewareRequestHeaders, isExternalUrl, matchHeaders, matchRedire
3
3
  import { normalizePath } from "./normalize-path.js";
4
4
  import { hasBasePath, stripBasePath } from "../utils/base-path.js";
5
5
  import { manifestFileWithBase } from "../utils/manifest-paths.js";
6
+ import { CONTENT_TYPES, StaticFileCache, etagFromFilenameHash } from "./static-file-cache.js";
6
7
  import { DEFAULT_DEVICE_SIZES, DEFAULT_IMAGE_SIZES, isSafeImageContentType, parseImageParams } from "./image-optimization.js";
8
+ import { computeLazyChunks } from "../utils/lazy-chunks.js";
7
9
  import { readPrerenderSecret } from "../build/server-manifest.js";
8
10
  import { seedMemoryCacheFromPrerender } from "./seed-cache.js";
9
- import { computeLazyChunks } from "../index.js";
10
11
  import fs from "node:fs";
11
12
  import path from "node:path";
13
+ import fsp from "node:fs/promises";
12
14
  import { pathToFileURL } from "node:url";
15
+ import zlib from "node:zlib";
13
16
  import { createServer } from "node:http";
14
17
  import { Readable, pipeline } from "node:stream";
15
- import zlib from "node:zlib";
16
18
  //#region src/server/prod-server.ts
17
19
  /**
18
20
  * Production server for vinext.
@@ -21,7 +23,7 @@ import zlib from "node:zlib";
21
23
  * - Static asset serving from client build output
22
24
  * - Pages Router: SSR rendering + API route handling
23
25
  * - App Router: RSC/SSR rendering, route handlers, server actions
24
- * - Gzip/Brotli compression for text-based responses
26
+ * - Zstd/Brotli/Gzip compression for text-based responses
25
27
  * - Streaming SSR for App Router
26
28
  *
27
29
  * Build output for Pages Router:
@@ -62,12 +64,18 @@ const COMPRESSIBLE_TYPES = new Set([
62
64
  const COMPRESS_THRESHOLD = 1024;
63
65
  /**
64
66
  * Parse the Accept-Encoding header and return the best supported encoding.
65
- * Preference order: br > gzip > deflate > identity.
67
+ * Preference order: zstd > br > gzip > deflate > identity.
68
+ *
69
+ * zstd decompresses ~3-5x faster than brotli at similar compression ratios.
70
+ * Supported in Chrome 123+, Firefox 126+. Safari can decompress but doesn't
71
+ * send zstd in Accept-Encoding, so it transparently falls back to br/gzip.
66
72
  */
73
+ const HAS_ZSTD = typeof zlib.createZstdCompress === "function";
67
74
  function negotiateEncoding(req) {
68
75
  const accept = req.headers["accept-encoding"];
69
76
  if (!accept || typeof accept !== "string") return null;
70
77
  const lower = accept.toLowerCase();
78
+ if (HAS_ZSTD && lower.includes("zstd")) return "zstd";
71
79
  if (lower.includes("br")) return "br";
72
80
  if (lower.includes("gzip")) return "gzip";
73
81
  if (lower.includes("deflate")) return "deflate";
@@ -78,6 +86,10 @@ function negotiateEncoding(req) {
78
86
  */
79
87
  function createCompressor(encoding, mode = "default") {
80
88
  switch (encoding) {
89
+ case "zstd": return zlib.createZstdCompress({
90
+ ...mode === "streaming" ? { flush: zlib.constants.ZSTD_e_flush } : {},
91
+ params: { [zlib.constants.ZSTD_c_compressionLevel]: 3 }
92
+ });
81
93
  case "br": return zlib.createBrotliCompress({
82
94
  ...mode === "streaming" ? { flush: zlib.constants.BROTLI_OPERATION_FLUSH } : {},
83
95
  params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 4 }
@@ -134,6 +146,11 @@ function omitHeadersCaseInsensitive(headersRecord, names) {
134
146
  }
135
147
  return filtered;
136
148
  }
149
+ function matchesIfNoneMatchHeader(ifNoneMatch, etag) {
150
+ if (!ifNoneMatch) return false;
151
+ if (ifNoneMatch === "*") return true;
152
+ return ifNoneMatch.split(",").map((value) => value.trim()).some((value) => value === etag);
153
+ }
137
154
  function stripHeaders(headersRecord, names) {
138
155
  const targets = new Set(names.map((name) => name.toLowerCase()));
139
156
  for (const key of Object.keys(headersRecord)) if (targets.has(key.toLowerCase())) delete headersRecord[key];
@@ -220,33 +237,67 @@ function sendCompressed(req, res, body, contentType, statusCode, extraHeaders =
220
237
  res.end(buf);
221
238
  }
222
239
  }
223
- /** Content-type lookup for static assets. */
224
- const CONTENT_TYPES = {
225
- ".js": "application/javascript",
226
- ".mjs": "application/javascript",
227
- ".css": "text/css",
228
- ".html": "text/html",
229
- ".json": "application/json",
230
- ".png": "image/png",
231
- ".jpg": "image/jpeg",
232
- ".jpeg": "image/jpeg",
233
- ".gif": "image/gif",
234
- ".svg": "image/svg+xml",
235
- ".ico": "image/x-icon",
236
- ".woff": "font/woff",
237
- ".woff2": "font/woff2",
238
- ".ttf": "font/ttf",
239
- ".eot": "application/vnd.ms-fontobject",
240
- ".webp": "image/webp",
241
- ".avif": "image/avif",
242
- ".map": "application/json",
243
- ".rsc": "text/x-component"
244
- };
245
240
  /**
246
241
  * Try to serve a static file from the client build directory.
247
- * Returns true if the file was served, false otherwise.
242
+ *
243
+ * When a `StaticFileCache` is provided, lookups are pure in-memory Map.get()
244
+ * with zero filesystem calls. Precompressed .br/.gz/.zst variants (generated at
245
+ * build time) are served directly — no per-request compression needed for
246
+ * hashed assets.
247
+ *
248
+ * Without a cache, falls back to async filesystem probing (still non-blocking,
249
+ * unlike the old sync existsSync/statSync approach).
248
250
  */
249
- function tryServeStatic(req, res, clientDir, pathname, compress, extraHeaders) {
251
+ async function tryServeStatic(req, res, clientDir, pathname, compress, cache, extraHeaders, statusCode) {
252
+ if (pathname === "/") return false;
253
+ const responseStatus = statusCode ?? 200;
254
+ const omitBody = isNoBodyResponseStatus(responseStatus);
255
+ if (cache) {
256
+ let lookupPath;
257
+ if (pathname.includes("%")) {
258
+ try {
259
+ lookupPath = decodeURIComponent(pathname);
260
+ } catch {
261
+ return false;
262
+ }
263
+ if (lookupPath.startsWith("/.vite/") || lookupPath === "/.vite") return false;
264
+ } else {
265
+ if (pathname.startsWith("/.vite/") || pathname === "/.vite") return false;
266
+ lookupPath = pathname;
267
+ }
268
+ const entry = cache.lookup(lookupPath);
269
+ if (!entry) return false;
270
+ const ifNoneMatch = req.headers["if-none-match"];
271
+ if (responseStatus === 200 && typeof ifNoneMatch === "string" && matchesIfNoneMatchHeader(ifNoneMatch, entry.etag)) {
272
+ if (extraHeaders) res.writeHead(304, {
273
+ ...entry.notModifiedHeaders,
274
+ ...extraHeaders
275
+ });
276
+ else res.writeHead(304, entry.notModifiedHeaders);
277
+ res.end();
278
+ return true;
279
+ }
280
+ const rawAe = compress ? req.headers["accept-encoding"] : void 0;
281
+ const ae = typeof rawAe === "string" ? rawAe.toLowerCase() : void 0;
282
+ const variant = ae ? ae.includes("zstd") && entry.zst || ae.includes("br") && entry.br || ae.includes("gzip") && entry.gz || entry.original : entry.original;
283
+ if (extraHeaders) res.writeHead(responseStatus, {
284
+ ...variant.headers,
285
+ ...extraHeaders
286
+ });
287
+ else res.writeHead(responseStatus, variant.headers);
288
+ if (omitBody || req.method === "HEAD") {
289
+ res.end();
290
+ return true;
291
+ }
292
+ if (variant.buffer) res.end(variant.buffer);
293
+ else pipeline(fs.createReadStream(variant.path), res, (err) => {
294
+ if (err) {
295
+ console.warn(`[vinext] Static file stream error for ${variant.path}:`, err.message);
296
+ res.destroy(err);
297
+ }
298
+ });
299
+ return true;
300
+ }
250
301
  const resolvedClient = path.resolve(clientDir);
251
302
  let decodedPathname;
252
303
  try {
@@ -257,44 +308,110 @@ function tryServeStatic(req, res, clientDir, pathname, compress, extraHeaders) {
257
308
  if (decodedPathname.startsWith("/.vite/") || decodedPathname === "/.vite") return false;
258
309
  const staticFile = path.resolve(clientDir, "." + decodedPathname);
259
310
  if (!staticFile.startsWith(resolvedClient + path.sep) && staticFile !== resolvedClient) return false;
260
- let resolvedStaticFile = staticFile;
261
- if (pathname === "/") return false;
262
- if (!fs.existsSync(resolvedStaticFile) || !fs.statSync(resolvedStaticFile).isFile()) {
263
- const htmlFallback = staticFile + ".html";
264
- if (fs.existsSync(htmlFallback) && fs.statSync(htmlFallback).isFile()) resolvedStaticFile = htmlFallback;
265
- else {
266
- const indexFallback = path.join(staticFile, "index.html");
267
- if (fs.existsSync(indexFallback) && fs.statSync(indexFallback).isFile()) resolvedStaticFile = indexFallback;
268
- else return false;
269
- }
311
+ const resolved = await resolveStaticFile(staticFile);
312
+ if (!resolved) return false;
313
+ const ext = path.extname(resolved.path);
314
+ const ct = CONTENT_TYPES[ext] ?? "application/octet-stream";
315
+ const isHashed = pathname.startsWith("/assets/");
316
+ const cacheControl = isHashed ? "public, max-age=31536000, immutable" : "public, max-age=3600";
317
+ const etag = isHashed && etagFromFilenameHash(resolved.path, ext) || `W/"${resolved.size}-${Math.floor(resolved.mtimeMs / 1e3)}"`;
318
+ const baseType = ct.split(";")[0].trim();
319
+ const isCompressible = compress && COMPRESSIBLE_TYPES.has(baseType);
320
+ const ifNoneMatch = req.headers["if-none-match"];
321
+ if (responseStatus === 200 && typeof ifNoneMatch === "string" && matchesIfNoneMatchHeader(ifNoneMatch, etag)) {
322
+ const notModifiedHeaders = {
323
+ ETag: etag,
324
+ "Cache-Control": cacheControl,
325
+ ...isCompressible ? { Vary: "Accept-Encoding" } : void 0,
326
+ ...extraHeaders
327
+ };
328
+ res.writeHead(304, notModifiedHeaders);
329
+ res.end();
330
+ return true;
270
331
  }
271
- const ct = CONTENT_TYPES[path.extname(resolvedStaticFile)] ?? "application/octet-stream";
272
- const cacheControl = pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "public, max-age=3600";
273
332
  const baseHeaders = {
274
333
  "Content-Type": ct,
275
334
  "Cache-Control": cacheControl,
335
+ ETag: etag,
276
336
  ...extraHeaders
277
337
  };
278
- const baseType = ct.split(";")[0].trim();
279
- if (compress && COMPRESSIBLE_TYPES.has(baseType)) {
338
+ if (isCompressible) {
280
339
  const encoding = negotiateEncoding(req);
281
340
  if (encoding) {
282
- const fileStream = fs.createReadStream(resolvedStaticFile);
283
- const compressor = createCompressor(encoding);
284
- res.writeHead(200, {
341
+ res.writeHead(responseStatus, {
285
342
  ...baseHeaders,
286
343
  "Content-Encoding": encoding,
287
344
  Vary: "Accept-Encoding"
288
345
  });
289
- pipeline(fileStream, compressor, res, () => {});
346
+ if (omitBody || req.method === "HEAD") {
347
+ res.end();
348
+ return true;
349
+ }
350
+ const compressor = createCompressor(encoding);
351
+ pipeline(fs.createReadStream(resolved.path), compressor, res, (err) => {
352
+ if (err) {
353
+ console.warn(`[vinext] Static file stream error for ${resolved.path}:`, err.message);
354
+ res.destroy(err);
355
+ }
356
+ });
290
357
  return true;
291
358
  }
292
359
  }
293
- res.writeHead(200, baseHeaders);
294
- fs.createReadStream(resolvedStaticFile).pipe(res);
360
+ res.writeHead(responseStatus, {
361
+ ...baseHeaders,
362
+ "Content-Length": String(resolved.size)
363
+ });
364
+ if (omitBody || req.method === "HEAD") {
365
+ res.end();
366
+ return true;
367
+ }
368
+ pipeline(fs.createReadStream(resolved.path), res, (err) => {
369
+ if (err) {
370
+ console.warn(`[vinext] Static file stream error for ${resolved.path}:`, err.message);
371
+ res.destroy(err);
372
+ }
373
+ });
295
374
  return true;
296
375
  }
297
376
  /**
377
+ * Resolve the actual file to serve, trying extension-less HTML fallbacks.
378
+ * Returns the resolved path + size + mtime, or null if not found.
379
+ */
380
+ async function resolveStaticFile(staticFile) {
381
+ const stat = await statIfFile(staticFile);
382
+ if (stat) return {
383
+ path: staticFile,
384
+ size: stat.size,
385
+ mtimeMs: stat.mtimeMs
386
+ };
387
+ const htmlFallback = staticFile + ".html";
388
+ const htmlStat = await statIfFile(htmlFallback);
389
+ if (htmlStat) return {
390
+ path: htmlFallback,
391
+ size: htmlStat.size,
392
+ mtimeMs: htmlStat.mtimeMs
393
+ };
394
+ const indexFallback = path.join(staticFile, "index.html");
395
+ const indexStat = await statIfFile(indexFallback);
396
+ if (indexStat) return {
397
+ path: indexFallback,
398
+ size: indexStat.size,
399
+ mtimeMs: indexStat.mtimeMs
400
+ };
401
+ return null;
402
+ }
403
+ async function statIfFile(filePath) {
404
+ try {
405
+ const stat = await fsp.stat(filePath);
406
+ return stat.isFile() ? {
407
+ size: stat.size,
408
+ mtimeMs: stat.mtimeMs
409
+ } : null;
410
+ } catch {
411
+ return null;
412
+ }
413
+ }
414
+ /**
298
415
  * Resolve the host for a request, ignoring X-Forwarded-Host to prevent
299
416
  * host header poisoning attacks (open redirects, cache poisoning).
300
417
  *
@@ -479,6 +596,7 @@ async function startAppRouterServer(options) {
479
596
  const rscHandler = resolveAppRouterHandler((await import(`${pathToFileURL(rscEntryPath).href}?t=${rscMtime}`)).default);
480
597
  const seededRoutes = await seedMemoryCacheFromPrerender(path.dirname(rscEntryPath));
481
598
  if (seededRoutes > 0) console.log(`[vinext] Seeded ${seededRoutes} pre-rendered route${seededRoutes !== 1 ? "s" : ""} into memory cache`);
599
+ const staticCache = await StaticFileCache.create(clientDir);
482
600
  const server = createServer(async (req, res) => {
483
601
  const rawUrl = req.url ?? "/";
484
602
  const rawPathname = rawUrl.split("?")[0].replaceAll("\\", "/");
@@ -503,7 +621,7 @@ async function startAppRouterServer(options) {
503
621
  return;
504
622
  }
505
623
  }
506
- if (pathname.startsWith("/assets/") && tryServeStatic(req, res, clientDir, pathname, compress)) return;
624
+ if (pathname.startsWith("/assets/") && await tryServeStatic(req, res, clientDir, pathname, compress, staticCache)) return;
507
625
  if (pathname === "/_vinext/image") {
508
626
  const params = parseImageParams(new URL(rawUrl, "http://localhost"), [...DEFAULT_DEVICE_SIZES, ...DEFAULT_IMAGE_SIZES]);
509
627
  if (!params) {
@@ -521,14 +639,38 @@ async function startAppRouterServer(options) {
521
639
  "X-Content-Type-Options": "nosniff",
522
640
  "Content-Disposition": imageConfig?.contentDispositionType === "attachment" ? "attachment" : "inline"
523
641
  };
524
- if (tryServeStatic(req, res, clientDir, params.imageUrl, false, imageSecurityHeaders)) return;
642
+ if (await tryServeStatic(req, res, clientDir, params.imageUrl, false, staticCache, imageSecurityHeaders)) return;
525
643
  res.writeHead(404);
526
644
  res.end("Image not found");
527
645
  return;
528
646
  }
529
647
  try {
530
648
  const qs = rawUrl.includes("?") ? rawUrl.slice(rawUrl.indexOf("?")) : "";
531
- await sendWebResponse(await rscHandler(nodeToWebRequest(req, pathname + qs)), req, res, compress);
649
+ const response = await rscHandler(nodeToWebRequest(req, pathname + qs));
650
+ const staticFileSignal = response.headers.get("x-vinext-static-file");
651
+ if (staticFileSignal) {
652
+ let staticFilePath = "/";
653
+ try {
654
+ staticFilePath = decodeURIComponent(staticFileSignal);
655
+ } catch {
656
+ staticFilePath = staticFileSignal;
657
+ }
658
+ const staticResponseHeaders = omitHeadersCaseInsensitive(mergeResponseHeaders({}, response), [
659
+ "x-vinext-static-file",
660
+ "content-encoding",
661
+ "content-length",
662
+ "content-type"
663
+ ]);
664
+ const served = await tryServeStatic(req, res, clientDir, staticFilePath, compress, staticCache, staticResponseHeaders, response.status);
665
+ cancelResponseBody(response);
666
+ if (served) return;
667
+ await sendWebResponse(new Response("Not Found", {
668
+ status: 404,
669
+ headers: toWebHeaders(staticResponseHeaders)
670
+ }), req, res, compress);
671
+ return;
672
+ }
673
+ await sendWebResponse(response, req, res, compress);
532
674
  } catch (e) {
533
675
  console.error("[vinext] Server error:", e);
534
676
  if (!res.headersSent) {
@@ -590,6 +732,7 @@ async function startPagesRouterServer(options) {
590
732
  const lazyChunks = computeLazyChunks(JSON.parse(fs.readFileSync(buildManifestPath, "utf-8"))).map((file) => manifestFileWithBase(file, assetBase));
591
733
  if (lazyChunks.length > 0) globalThis.__VINEXT_LAZY_CHUNKS__ = lazyChunks;
592
734
  } catch {}
735
+ const staticCache = await StaticFileCache.create(clientDir);
593
736
  const server = createServer(async (req, res) => {
594
737
  const rawUrl = req.url ?? "/";
595
738
  const rawPagesPathname = rawUrl.split("?")[0].replaceAll("\\", "/");
@@ -640,7 +783,7 @@ async function startPagesRouterServer(options) {
640
783
  return;
641
784
  }
642
785
  const staticLookupPath = stripBasePath(pathname, basePath);
643
- if (staticLookupPath.startsWith("/assets/") && tryServeStatic(req, res, clientDir, staticLookupPath, compress)) return;
786
+ if (staticLookupPath.startsWith("/assets/") && await tryServeStatic(req, res, clientDir, staticLookupPath, compress, staticCache)) return;
644
787
  if (pathname === "/_vinext/image" || staticLookupPath === "/_vinext/image") {
645
788
  const params = parseImageParams(new URL(rawUrl, "http://localhost"), allowedImageWidths);
646
789
  if (!params) {
@@ -658,7 +801,7 @@ async function startPagesRouterServer(options) {
658
801
  "X-Content-Type-Options": "nosniff",
659
802
  "Content-Disposition": pagesImageConfig?.contentDispositionType === "attachment" ? "attachment" : "inline"
660
803
  };
661
- if (tryServeStatic(req, res, clientDir, params.imageUrl, false, imageSecurityHeaders)) return;
804
+ if (await tryServeStatic(req, res, clientDir, params.imageUrl, false, staticCache, imageSecurityHeaders)) return;
662
805
  res.writeHead(404);
663
806
  res.end("Image not found");
664
807
  return;
@@ -769,7 +912,7 @@ async function startPagesRouterServer(options) {
769
912
  else if (!(lk in middlewareHeaders)) middlewareHeaders[lk] = h.value;
770
913
  }
771
914
  }
772
- if (staticLookupPath !== "/" && !staticLookupPath.startsWith("/api/") && !staticLookupPath.startsWith("/assets/") && tryServeStatic(req, res, clientDir, staticLookupPath, compress, middlewareHeaders)) return;
915
+ if (staticLookupPath !== "/" && !staticLookupPath.startsWith("/api/") && !staticLookupPath.startsWith("/assets/") && await tryServeStatic(req, res, clientDir, staticLookupPath, compress, staticCache, middlewareHeaders)) return;
773
916
  if (configRewrites.beforeFiles?.length) {
774
917
  const rewritten = matchRewrite(resolvedPathname, configRewrites.beforeFiles, postMwReqCtx);
775
918
  if (rewritten) {
@@ -861,6 +1004,6 @@ async function startPagesRouterServer(options) {
861
1004
  };
862
1005
  }
863
1006
  //#endregion
864
- export { COMPRESSIBLE_TYPES, COMPRESS_THRESHOLD, mergeResponseHeaders, mergeWebResponse, negotiateEncoding, nodeToWebRequest, resolveHost, sendCompressed, sendWebResponse, startProdServer, trustProxy, trustedHosts };
1007
+ export { COMPRESSIBLE_TYPES, COMPRESS_THRESHOLD, mergeResponseHeaders, mergeWebResponse, negotiateEncoding, nodeToWebRequest, resolveHost, sendCompressed, sendWebResponse, startProdServer, trustProxy, trustedHosts, tryServeStatic };
865
1008
 
866
1009
  //# sourceMappingURL=prod-server.js.map