vinext 0.0.0 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -0
  3. package/dist/build/static-export.d.ts +78 -0
  4. package/dist/build/static-export.d.ts.map +1 -0
  5. package/dist/build/static-export.js +553 -0
  6. package/dist/build/static-export.js.map +1 -0
  7. package/dist/check.d.ts +52 -0
  8. package/dist/check.d.ts.map +1 -0
  9. package/dist/check.js +483 -0
  10. package/dist/check.js.map +1 -0
  11. package/dist/cli.d.ts +15 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +565 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/client/entry.d.ts +2 -0
  16. package/dist/client/entry.d.ts.map +1 -0
  17. package/dist/client/entry.js +85 -0
  18. package/dist/client/entry.js.map +1 -0
  19. package/dist/cloudflare/index.d.ts +8 -0
  20. package/dist/cloudflare/index.d.ts.map +1 -0
  21. package/dist/cloudflare/index.js +8 -0
  22. package/dist/cloudflare/index.js.map +1 -0
  23. package/dist/cloudflare/kv-cache-handler.d.ts +68 -0
  24. package/dist/cloudflare/kv-cache-handler.d.ts.map +1 -0
  25. package/dist/cloudflare/kv-cache-handler.js +304 -0
  26. package/dist/cloudflare/kv-cache-handler.js.map +1 -0
  27. package/dist/cloudflare/tpr.d.ts +78 -0
  28. package/dist/cloudflare/tpr.d.ts.map +1 -0
  29. package/dist/cloudflare/tpr.js +672 -0
  30. package/dist/cloudflare/tpr.js.map +1 -0
  31. package/dist/config/config-matchers.d.ts +106 -0
  32. package/dist/config/config-matchers.d.ts.map +1 -0
  33. package/dist/config/config-matchers.js +499 -0
  34. package/dist/config/config-matchers.js.map +1 -0
  35. package/dist/config/next-config.d.ts +153 -0
  36. package/dist/config/next-config.d.ts.map +1 -0
  37. package/dist/config/next-config.js +274 -0
  38. package/dist/config/next-config.js.map +1 -0
  39. package/dist/deploy.d.ts +87 -0
  40. package/dist/deploy.d.ts.map +1 -0
  41. package/dist/deploy.js +644 -0
  42. package/dist/deploy.js.map +1 -0
  43. package/dist/index.d.ts +156 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +3296 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/init.d.ts +55 -0
  48. package/dist/init.d.ts.map +1 -0
  49. package/dist/init.js +201 -0
  50. package/dist/init.js.map +1 -0
  51. package/dist/routing/app-router.d.ts +96 -0
  52. package/dist/routing/app-router.d.ts.map +1 -0
  53. package/dist/routing/app-router.js +815 -0
  54. package/dist/routing/app-router.js.map +1 -0
  55. package/dist/routing/pages-router.d.ts +52 -0
  56. package/dist/routing/pages-router.d.ts.map +1 -0
  57. package/dist/routing/pages-router.js +239 -0
  58. package/dist/routing/pages-router.js.map +1 -0
  59. package/dist/server/api-handler.d.ts +18 -0
  60. package/dist/server/api-handler.d.ts.map +1 -0
  61. package/dist/server/api-handler.js +169 -0
  62. package/dist/server/api-handler.js.map +1 -0
  63. package/dist/server/app-dev-server.d.ts +42 -0
  64. package/dist/server/app-dev-server.d.ts.map +1 -0
  65. package/dist/server/app-dev-server.js +2718 -0
  66. package/dist/server/app-dev-server.js.map +1 -0
  67. package/dist/server/app-router-entry.d.ts +18 -0
  68. package/dist/server/app-router-entry.d.ts.map +1 -0
  69. package/dist/server/app-router-entry.js +34 -0
  70. package/dist/server/app-router-entry.js.map +1 -0
  71. package/dist/server/dev-server.d.ts +40 -0
  72. package/dist/server/dev-server.d.ts.map +1 -0
  73. package/dist/server/dev-server.js +758 -0
  74. package/dist/server/dev-server.js.map +1 -0
  75. package/dist/server/html.d.ts +22 -0
  76. package/dist/server/html.d.ts.map +1 -0
  77. package/dist/server/html.js +29 -0
  78. package/dist/server/html.js.map +1 -0
  79. package/dist/server/image-optimization.d.ts +56 -0
  80. package/dist/server/image-optimization.d.ts.map +1 -0
  81. package/dist/server/image-optimization.js +103 -0
  82. package/dist/server/image-optimization.js.map +1 -0
  83. package/dist/server/instrumentation.d.ts +68 -0
  84. package/dist/server/instrumentation.d.ts.map +1 -0
  85. package/dist/server/instrumentation.js +90 -0
  86. package/dist/server/instrumentation.js.map +1 -0
  87. package/dist/server/isr-cache.d.ts +61 -0
  88. package/dist/server/isr-cache.d.ts.map +1 -0
  89. package/dist/server/isr-cache.js +134 -0
  90. package/dist/server/isr-cache.js.map +1 -0
  91. package/dist/server/metadata-routes.d.ts +103 -0
  92. package/dist/server/metadata-routes.d.ts.map +1 -0
  93. package/dist/server/metadata-routes.js +270 -0
  94. package/dist/server/metadata-routes.js.map +1 -0
  95. package/dist/server/middleware.d.ts +77 -0
  96. package/dist/server/middleware.d.ts.map +1 -0
  97. package/dist/server/middleware.js +228 -0
  98. package/dist/server/middleware.js.map +1 -0
  99. package/dist/server/prod-server.d.ts +78 -0
  100. package/dist/server/prod-server.d.ts.map +1 -0
  101. package/dist/server/prod-server.js +712 -0
  102. package/dist/server/prod-server.js.map +1 -0
  103. package/dist/shims/amp.d.ts +17 -0
  104. package/dist/shims/amp.d.ts.map +1 -0
  105. package/dist/shims/amp.js +21 -0
  106. package/dist/shims/amp.js.map +1 -0
  107. package/dist/shims/app.d.ts +12 -0
  108. package/dist/shims/app.d.ts.map +1 -0
  109. package/dist/shims/app.js +2 -0
  110. package/dist/shims/app.js.map +1 -0
  111. package/dist/shims/cache-runtime.d.ts +68 -0
  112. package/dist/shims/cache-runtime.d.ts.map +1 -0
  113. package/dist/shims/cache-runtime.js +437 -0
  114. package/dist/shims/cache-runtime.js.map +1 -0
  115. package/dist/shims/cache.d.ts +243 -0
  116. package/dist/shims/cache.d.ts.map +1 -0
  117. package/dist/shims/cache.js +415 -0
  118. package/dist/shims/cache.js.map +1 -0
  119. package/dist/shims/client-only.d.ts +18 -0
  120. package/dist/shims/client-only.d.ts.map +1 -0
  121. package/dist/shims/client-only.js +18 -0
  122. package/dist/shims/client-only.js.map +1 -0
  123. package/dist/shims/config.d.ts +27 -0
  124. package/dist/shims/config.d.ts.map +1 -0
  125. package/dist/shims/config.js +30 -0
  126. package/dist/shims/config.js.map +1 -0
  127. package/dist/shims/constants.d.ts +13 -0
  128. package/dist/shims/constants.d.ts.map +1 -0
  129. package/dist/shims/constants.js +13 -0
  130. package/dist/shims/constants.js.map +1 -0
  131. package/dist/shims/document.d.ts +33 -0
  132. package/dist/shims/document.d.ts.map +1 -0
  133. package/dist/shims/document.js +32 -0
  134. package/dist/shims/document.js.map +1 -0
  135. package/dist/shims/dynamic.d.ts +33 -0
  136. package/dist/shims/dynamic.d.ts.map +1 -0
  137. package/dist/shims/dynamic.js +149 -0
  138. package/dist/shims/dynamic.js.map +1 -0
  139. package/dist/shims/error-boundary.d.ts +33 -0
  140. package/dist/shims/error-boundary.d.ts.map +1 -0
  141. package/dist/shims/error-boundary.js +88 -0
  142. package/dist/shims/error-boundary.js.map +1 -0
  143. package/dist/shims/error.d.ts +16 -0
  144. package/dist/shims/error.d.ts.map +1 -0
  145. package/dist/shims/error.js +45 -0
  146. package/dist/shims/error.js.map +1 -0
  147. package/dist/shims/fetch-cache.d.ts +61 -0
  148. package/dist/shims/fetch-cache.d.ts.map +1 -0
  149. package/dist/shims/fetch-cache.js +307 -0
  150. package/dist/shims/fetch-cache.js.map +1 -0
  151. package/dist/shims/font-google.d.ts +122 -0
  152. package/dist/shims/font-google.d.ts.map +1 -0
  153. package/dist/shims/font-google.js +387 -0
  154. package/dist/shims/font-google.js.map +1 -0
  155. package/dist/shims/font-local.d.ts +61 -0
  156. package/dist/shims/font-local.d.ts.map +1 -0
  157. package/dist/shims/font-local.js +303 -0
  158. package/dist/shims/font-local.js.map +1 -0
  159. package/dist/shims/form.d.ts +30 -0
  160. package/dist/shims/form.d.ts.map +1 -0
  161. package/dist/shims/form.js +78 -0
  162. package/dist/shims/form.js.map +1 -0
  163. package/dist/shims/head-state.d.ts +11 -0
  164. package/dist/shims/head-state.d.ts.map +1 -0
  165. package/dist/shims/head-state.js +47 -0
  166. package/dist/shims/head-state.js.map +1 -0
  167. package/dist/shims/head.d.ts +28 -0
  168. package/dist/shims/head.d.ts.map +1 -0
  169. package/dist/shims/head.js +148 -0
  170. package/dist/shims/head.js.map +1 -0
  171. package/dist/shims/headers.d.ts +150 -0
  172. package/dist/shims/headers.d.ts.map +1 -0
  173. package/dist/shims/headers.js +412 -0
  174. package/dist/shims/headers.js.map +1 -0
  175. package/dist/shims/image-config.d.ts +30 -0
  176. package/dist/shims/image-config.d.ts.map +1 -0
  177. package/dist/shims/image-config.js +91 -0
  178. package/dist/shims/image-config.js.map +1 -0
  179. package/dist/shims/image.d.ts +63 -0
  180. package/dist/shims/image.d.ts.map +1 -0
  181. package/dist/shims/image.js +284 -0
  182. package/dist/shims/image.js.map +1 -0
  183. package/dist/shims/internal/api-utils.d.ts +12 -0
  184. package/dist/shims/internal/api-utils.d.ts.map +1 -0
  185. package/dist/shims/internal/api-utils.js +7 -0
  186. package/dist/shims/internal/api-utils.js.map +1 -0
  187. package/dist/shims/internal/app-router-context.d.ts +21 -0
  188. package/dist/shims/internal/app-router-context.d.ts.map +1 -0
  189. package/dist/shims/internal/app-router-context.js +15 -0
  190. package/dist/shims/internal/app-router-context.js.map +1 -0
  191. package/dist/shims/internal/cookies.d.ts +9 -0
  192. package/dist/shims/internal/cookies.d.ts.map +1 -0
  193. package/dist/shims/internal/cookies.js +9 -0
  194. package/dist/shims/internal/cookies.js.map +1 -0
  195. package/dist/shims/internal/router-context.d.ts +2 -0
  196. package/dist/shims/internal/router-context.d.ts.map +1 -0
  197. package/dist/shims/internal/router-context.js +9 -0
  198. package/dist/shims/internal/router-context.js.map +1 -0
  199. package/dist/shims/internal/utils.d.ts +48 -0
  200. package/dist/shims/internal/utils.d.ts.map +1 -0
  201. package/dist/shims/internal/utils.js +35 -0
  202. package/dist/shims/internal/utils.js.map +1 -0
  203. package/dist/shims/internal/work-unit-async-storage.d.ts +12 -0
  204. package/dist/shims/internal/work-unit-async-storage.d.ts.map +1 -0
  205. package/dist/shims/internal/work-unit-async-storage.js +13 -0
  206. package/dist/shims/internal/work-unit-async-storage.js.map +1 -0
  207. package/dist/shims/layout-segment-context.d.ts +21 -0
  208. package/dist/shims/layout-segment-context.d.ts.map +1 -0
  209. package/dist/shims/layout-segment-context.js +27 -0
  210. package/dist/shims/layout-segment-context.js.map +1 -0
  211. package/dist/shims/legacy-image.d.ts +52 -0
  212. package/dist/shims/legacy-image.d.ts.map +1 -0
  213. package/dist/shims/legacy-image.js +46 -0
  214. package/dist/shims/legacy-image.js.map +1 -0
  215. package/dist/shims/link.d.ts +48 -0
  216. package/dist/shims/link.d.ts.map +1 -0
  217. package/dist/shims/link.js +395 -0
  218. package/dist/shims/link.js.map +1 -0
  219. package/dist/shims/metadata.d.ts +184 -0
  220. package/dist/shims/metadata.d.ts.map +1 -0
  221. package/dist/shims/metadata.js +472 -0
  222. package/dist/shims/metadata.js.map +1 -0
  223. package/dist/shims/navigation-state.d.ts +14 -0
  224. package/dist/shims/navigation-state.d.ts.map +1 -0
  225. package/dist/shims/navigation-state.js +77 -0
  226. package/dist/shims/navigation-state.js.map +1 -0
  227. package/dist/shims/navigation.d.ts +201 -0
  228. package/dist/shims/navigation.d.ts.map +1 -0
  229. package/dist/shims/navigation.js +672 -0
  230. package/dist/shims/navigation.js.map +1 -0
  231. package/dist/shims/og.d.ts +20 -0
  232. package/dist/shims/og.d.ts.map +1 -0
  233. package/dist/shims/og.js +19 -0
  234. package/dist/shims/og.js.map +1 -0
  235. package/dist/shims/router-state.d.ts +11 -0
  236. package/dist/shims/router-state.d.ts.map +1 -0
  237. package/dist/shims/router-state.js +56 -0
  238. package/dist/shims/router-state.js.map +1 -0
  239. package/dist/shims/router.d.ts +103 -0
  240. package/dist/shims/router.d.ts.map +1 -0
  241. package/dist/shims/router.js +536 -0
  242. package/dist/shims/router.js.map +1 -0
  243. package/dist/shims/script.d.ts +58 -0
  244. package/dist/shims/script.d.ts.map +1 -0
  245. package/dist/shims/script.js +163 -0
  246. package/dist/shims/script.js.map +1 -0
  247. package/dist/shims/server-only.d.ts +19 -0
  248. package/dist/shims/server-only.d.ts.map +1 -0
  249. package/dist/shims/server-only.js +19 -0
  250. package/dist/shims/server-only.js.map +1 -0
  251. package/dist/shims/server.d.ts +178 -0
  252. package/dist/shims/server.d.ts.map +1 -0
  253. package/dist/shims/server.js +377 -0
  254. package/dist/shims/server.js.map +1 -0
  255. package/dist/shims/web-vitals.d.ts +24 -0
  256. package/dist/shims/web-vitals.d.ts.map +1 -0
  257. package/dist/shims/web-vitals.js +17 -0
  258. package/dist/shims/web-vitals.js.map +1 -0
  259. package/dist/utils/hash.d.ts +6 -0
  260. package/dist/utils/hash.d.ts.map +1 -0
  261. package/dist/utils/hash.js +20 -0
  262. package/dist/utils/hash.js.map +1 -0
  263. package/dist/utils/project.d.ts +36 -0
  264. package/dist/utils/project.d.ts.map +1 -0
  265. package/dist/utils/project.js +112 -0
  266. package/dist/utils/project.js.map +1 -0
  267. package/dist/utils/query.d.ts +10 -0
  268. package/dist/utils/query.d.ts.map +1 -0
  269. package/dist/utils/query.js +27 -0
  270. package/dist/utils/query.js.map +1 -0
  271. package/package.json +65 -7
  272. package/index.js +0 -1
@@ -0,0 +1,712 @@
1
+ /**
2
+ * Production server for vinext.
3
+ *
4
+ * Serves the built output from `vinext build`. Handles:
5
+ * - Static asset serving from client build output
6
+ * - Pages Router: SSR rendering + API route handling
7
+ * - App Router: RSC/SSR rendering, route handlers, server actions
8
+ * - Gzip/Brotli compression for text-based responses
9
+ * - Streaming SSR for App Router
10
+ *
11
+ * Build output for Pages Router:
12
+ * - dist/client/ — static assets (JS, CSS, images) + .vite/ssr-manifest.json
13
+ * - dist/server/entry.js — SSR entry point (virtual:vinext-server-entry)
14
+ *
15
+ * Build output for App Router:
16
+ * - dist/client/ — static assets (JS, CSS, images)
17
+ * - dist/server/index.js — RSC entry (default export: handler(Request) → Response)
18
+ * - dist/server/ssr/index.js — SSR entry (imported by RSC entry at runtime)
19
+ */
20
+ import { createServer } from "node:http";
21
+ import { Readable, pipeline } from "node:stream";
22
+ import { pathToFileURL } from "node:url";
23
+ import fs from "node:fs";
24
+ import path from "node:path";
25
+ import zlib from "node:zlib";
26
+ import { matchRedirect, matchRewrite, matchHeaders, requestContextFromRequest, isExternalUrl, proxyExternalRequest } from "../config/config-matchers.js";
27
+ import { IMAGE_OPTIMIZATION_PATH, parseImageParams } from "./image-optimization.js";
28
+ import { computeLazyChunks } from "../index.js";
29
+ /** Convert a Node.js IncomingMessage into a ReadableStream for Web Request body. */
30
+ function readNodeStream(req) {
31
+ return new ReadableStream({
32
+ start(controller) {
33
+ req.on("data", (chunk) => controller.enqueue(new Uint8Array(chunk)));
34
+ req.on("end", () => controller.close());
35
+ req.on("error", (err) => controller.error(err));
36
+ },
37
+ });
38
+ }
39
+ /** Content types that benefit from compression. */
40
+ const COMPRESSIBLE_TYPES = new Set([
41
+ "text/html",
42
+ "text/css",
43
+ "text/plain",
44
+ "text/xml",
45
+ "text/javascript",
46
+ "application/javascript",
47
+ "application/json",
48
+ "application/xml",
49
+ "application/xhtml+xml",
50
+ "application/rss+xml",
51
+ "application/atom+xml",
52
+ "image/svg+xml",
53
+ "application/manifest+json",
54
+ "application/wasm",
55
+ ]);
56
+ /** Minimum size threshold for compression (in bytes). Below this, compression overhead isn't worth it. */
57
+ const COMPRESS_THRESHOLD = 1024;
58
+ /**
59
+ * Parse the Accept-Encoding header and return the best supported encoding.
60
+ * Preference order: br > gzip > deflate > identity.
61
+ */
62
+ function negotiateEncoding(req) {
63
+ const accept = req.headers["accept-encoding"];
64
+ if (!accept || typeof accept !== "string")
65
+ return null;
66
+ const lower = accept.toLowerCase();
67
+ if (lower.includes("br"))
68
+ return "br";
69
+ if (lower.includes("gzip"))
70
+ return "gzip";
71
+ if (lower.includes("deflate"))
72
+ return "deflate";
73
+ return null;
74
+ }
75
+ /**
76
+ * Create a compression stream for the given encoding.
77
+ */
78
+ function createCompressor(encoding) {
79
+ switch (encoding) {
80
+ case "br":
81
+ return zlib.createBrotliCompress({
82
+ params: {
83
+ [zlib.constants.BROTLI_PARAM_QUALITY]: 4, // Fast compression (1-11, 4 is a good balance)
84
+ },
85
+ });
86
+ case "gzip":
87
+ return zlib.createGzip({ level: 6 }); // Default level, good balance
88
+ case "deflate":
89
+ return zlib.createDeflate({ level: 6 });
90
+ }
91
+ }
92
+ /**
93
+ * Send a compressed response if the content type is compressible and the
94
+ * client supports compression. Otherwise send uncompressed.
95
+ */
96
+ function sendCompressed(req, res, body, contentType, statusCode, extraHeaders = {}, compress = true) {
97
+ const buf = typeof body === "string" ? Buffer.from(body) : body;
98
+ const baseType = contentType.split(";")[0].trim();
99
+ const encoding = compress ? negotiateEncoding(req) : null;
100
+ if (encoding && COMPRESSIBLE_TYPES.has(baseType) && buf.length >= COMPRESS_THRESHOLD) {
101
+ const compressor = createCompressor(encoding);
102
+ res.writeHead(statusCode, {
103
+ ...extraHeaders,
104
+ "Content-Type": contentType,
105
+ "Content-Encoding": encoding,
106
+ Vary: "Accept-Encoding",
107
+ });
108
+ compressor.end(buf);
109
+ pipeline(compressor, res, () => { });
110
+ }
111
+ else {
112
+ res.writeHead(statusCode, {
113
+ ...extraHeaders,
114
+ "Content-Type": contentType,
115
+ "Content-Length": String(buf.length),
116
+ });
117
+ res.end(buf);
118
+ }
119
+ }
120
+ /** Content-type lookup for static assets. */
121
+ const CONTENT_TYPES = {
122
+ ".js": "application/javascript",
123
+ ".mjs": "application/javascript",
124
+ ".css": "text/css",
125
+ ".html": "text/html",
126
+ ".json": "application/json",
127
+ ".png": "image/png",
128
+ ".jpg": "image/jpeg",
129
+ ".jpeg": "image/jpeg",
130
+ ".gif": "image/gif",
131
+ ".svg": "image/svg+xml",
132
+ ".ico": "image/x-icon",
133
+ ".woff": "font/woff",
134
+ ".woff2": "font/woff2",
135
+ ".ttf": "font/ttf",
136
+ ".eot": "application/vnd.ms-fontobject",
137
+ ".webp": "image/webp",
138
+ ".avif": "image/avif",
139
+ ".map": "application/json",
140
+ };
141
+ /**
142
+ * Try to serve a static file from the client build directory.
143
+ * Returns true if the file was served, false otherwise.
144
+ */
145
+ function tryServeStatic(req, res, clientDir, pathname, compress) {
146
+ // Resolve the path and guard against directory traversal (e.g. /../../../etc/passwd)
147
+ const resolvedClient = path.resolve(clientDir);
148
+ let decodedPathname;
149
+ try {
150
+ decodedPathname = decodeURIComponent(pathname);
151
+ }
152
+ catch {
153
+ return false;
154
+ }
155
+ const staticFile = path.resolve(clientDir, "." + decodedPathname);
156
+ if (!staticFile.startsWith(resolvedClient + path.sep) && staticFile !== resolvedClient) {
157
+ return false;
158
+ }
159
+ if (pathname === "/" ||
160
+ !fs.existsSync(staticFile) ||
161
+ !fs.statSync(staticFile).isFile()) {
162
+ return false;
163
+ }
164
+ const ext = path.extname(staticFile);
165
+ const ct = CONTENT_TYPES[ext] ?? "application/octet-stream";
166
+ const isHashed = pathname.startsWith("/assets/");
167
+ const cacheControl = isHashed
168
+ ? "public, max-age=31536000, immutable"
169
+ : "public, max-age=3600";
170
+ const baseType = ct.split(";")[0].trim();
171
+ if (compress && COMPRESSIBLE_TYPES.has(baseType)) {
172
+ const encoding = negotiateEncoding(req);
173
+ if (encoding) {
174
+ const fileStream = fs.createReadStream(staticFile);
175
+ const compressor = createCompressor(encoding);
176
+ res.writeHead(200, {
177
+ "Content-Type": ct,
178
+ "Content-Encoding": encoding,
179
+ "Cache-Control": cacheControl,
180
+ Vary: "Accept-Encoding",
181
+ });
182
+ pipeline(fileStream, compressor, res, () => { });
183
+ return true;
184
+ }
185
+ }
186
+ res.writeHead(200, {
187
+ "Content-Type": ct,
188
+ "Cache-Control": cacheControl,
189
+ });
190
+ fs.createReadStream(staticFile).pipe(res);
191
+ return true;
192
+ }
193
+ /**
194
+ * Resolve the host for a request, ignoring X-Forwarded-Host to prevent
195
+ * host header poisoning attacks (open redirects, cache poisoning).
196
+ *
197
+ * X-Forwarded-Host is only trusted when the VINEXT_TRUSTED_HOSTS env var
198
+ * lists the forwarded host value. Without this, an attacker can send
199
+ * X-Forwarded-Host: evil.com and poison any redirect that resolves
200
+ * against request.url.
201
+ *
202
+ * On Cloudflare Workers, X-Forwarded-Host is always set by Cloudflare
203
+ * itself, so this is only a concern for the Node.js prod-server.
204
+ */
205
+ function resolveHost(req, fallback) {
206
+ const rawForwarded = req.headers["x-forwarded-host"];
207
+ const hostHeader = req.headers.host;
208
+ if (rawForwarded) {
209
+ // X-Forwarded-Host can be comma-separated when passing through
210
+ // multiple proxies — take only the first (client-facing) value.
211
+ const forwardedHost = rawForwarded.split(",")[0].trim().toLowerCase();
212
+ if (forwardedHost && trustedHosts.has(forwardedHost)) {
213
+ return forwardedHost;
214
+ }
215
+ }
216
+ return hostHeader || fallback;
217
+ }
218
+ /** Hosts that are allowed as X-Forwarded-Host values (stored lowercase). */
219
+ const trustedHosts = new Set((process.env.VINEXT_TRUSTED_HOSTS ?? "")
220
+ .split(",")
221
+ .map((h) => h.trim().toLowerCase())
222
+ .filter(Boolean));
223
+ /**
224
+ * Whether to trust X-Forwarded-Proto from upstream proxies.
225
+ * Enabled when VINEXT_TRUST_PROXY=1 or when VINEXT_TRUSTED_HOSTS is set
226
+ * (having trusted hosts implies a trusted proxy).
227
+ */
228
+ const trustProxy = process.env.VINEXT_TRUST_PROXY === "1" || trustedHosts.size > 0;
229
+ /**
230
+ * Convert a Node.js IncomingMessage to a Web Request object.
231
+ */
232
+ function nodeToWebRequest(req) {
233
+ const rawProto = trustProxy
234
+ ? req.headers["x-forwarded-proto"]?.split(",")[0]?.trim()
235
+ : undefined;
236
+ const proto = rawProto === "https" || rawProto === "http" ? rawProto : "http";
237
+ const host = resolveHost(req, "localhost");
238
+ const origin = `${proto}://${host}`;
239
+ const url = new URL(req.url ?? "/", origin);
240
+ const headers = new Headers();
241
+ for (const [key, value] of Object.entries(req.headers)) {
242
+ if (value === undefined)
243
+ continue;
244
+ if (Array.isArray(value)) {
245
+ for (const v of value)
246
+ headers.append(key, v);
247
+ }
248
+ else {
249
+ headers.set(key, value);
250
+ }
251
+ }
252
+ const method = req.method ?? "GET";
253
+ const hasBody = method !== "GET" && method !== "HEAD";
254
+ const init = {
255
+ method,
256
+ headers,
257
+ };
258
+ if (hasBody) {
259
+ // Convert Node.js readable stream to Web ReadableStream for request body.
260
+ // Readable.toWeb() is available since Node.js 17.
261
+ init.body = Readable.toWeb(req);
262
+ init.duplex = "half"; // Required for streaming request bodies
263
+ }
264
+ return new Request(url, init);
265
+ }
266
+ /**
267
+ * Stream a Web Response back to a Node.js ServerResponse.
268
+ * Supports streaming compression for SSR responses.
269
+ */
270
+ async function sendWebResponse(webResponse, req, res, compress) {
271
+ const status = webResponse.status;
272
+ // Collect headers, handling multi-value headers (e.g. Set-Cookie)
273
+ const nodeHeaders = {};
274
+ webResponse.headers.forEach((value, key) => {
275
+ const existing = nodeHeaders[key];
276
+ if (existing !== undefined) {
277
+ nodeHeaders[key] = Array.isArray(existing)
278
+ ? [...existing, value]
279
+ : [existing, value];
280
+ }
281
+ else {
282
+ nodeHeaders[key] = value;
283
+ }
284
+ });
285
+ if (!webResponse.body) {
286
+ res.writeHead(status, nodeHeaders);
287
+ res.end();
288
+ return;
289
+ }
290
+ // Check if we should compress the response.
291
+ // Skip if the upstream already compressed (avoid double-compression).
292
+ const alreadyEncoded = webResponse.headers.has("content-encoding");
293
+ const contentType = webResponse.headers.get("content-type") ?? "";
294
+ const baseType = contentType.split(";")[0].trim();
295
+ const encoding = (compress && !alreadyEncoded) ? negotiateEncoding(req) : null;
296
+ const shouldCompress = !!(encoding && COMPRESSIBLE_TYPES.has(baseType));
297
+ if (shouldCompress) {
298
+ delete nodeHeaders["content-length"];
299
+ delete nodeHeaders["Content-Length"];
300
+ nodeHeaders["Content-Encoding"] = encoding;
301
+ nodeHeaders["Vary"] = "Accept-Encoding";
302
+ }
303
+ res.writeHead(status, nodeHeaders);
304
+ // HEAD requests: send headers only, skip the body
305
+ if (req.method === "HEAD") {
306
+ res.end();
307
+ return;
308
+ }
309
+ // Convert Web ReadableStream to Node.js Readable and pipe to response.
310
+ // Readable.fromWeb() is available since Node.js 17.
311
+ const nodeStream = Readable.fromWeb(webResponse.body);
312
+ if (shouldCompress) {
313
+ const compressor = createCompressor(encoding);
314
+ pipeline(nodeStream, compressor, res, () => { });
315
+ }
316
+ else {
317
+ pipeline(nodeStream, res, () => { });
318
+ }
319
+ }
320
+ /**
321
+ * Start the production server.
322
+ *
323
+ * Automatically detects whether the build is App Router (dist/server/index.js) or
324
+ * Pages Router (dist/server/entry.js) and configures the appropriate handler.
325
+ */
326
+ export async function startProdServer(options = {}) {
327
+ const { port = process.env.PORT ? parseInt(process.env.PORT) : 3000, host = "0.0.0.0", outDir = path.resolve("dist"), noCompression = false, } = options;
328
+ const compress = !noCompression;
329
+ // Always resolve outDir to absolute to ensure dynamic import() works
330
+ const resolvedOutDir = path.resolve(outDir);
331
+ const clientDir = path.join(resolvedOutDir, "client");
332
+ // Detect build type
333
+ const rscEntryPath = path.join(resolvedOutDir, "server", "index.js");
334
+ const serverEntryPath = path.join(resolvedOutDir, "server", "entry.js");
335
+ const isAppRouter = fs.existsSync(rscEntryPath);
336
+ if (!isAppRouter && !fs.existsSync(serverEntryPath)) {
337
+ console.error(`[vinext] No build output found in ${outDir}`);
338
+ console.error("Run `vinext build` first.");
339
+ process.exit(1);
340
+ }
341
+ if (isAppRouter) {
342
+ return startAppRouterServer({ port, host, clientDir, rscEntryPath, compress });
343
+ }
344
+ return startPagesRouterServer({ port, host, clientDir, serverEntryPath, compress });
345
+ }
346
+ /**
347
+ * Start the App Router production server.
348
+ *
349
+ * The RSC entry (dist/server/index.js) exports a default handler function:
350
+ * handler(request: Request) → Promise<Response>
351
+ *
352
+ * This handler already does everything: route matching, RSC rendering,
353
+ * SSR HTML generation (via import("./ssr/index.js")), route handlers,
354
+ * server actions, ISR caching, 404s, redirects, etc.
355
+ *
356
+ * The production server's job is simply to:
357
+ * 1. Serve static assets from dist/client/
358
+ * 2. Convert Node.js IncomingMessage → Web Request
359
+ * 3. Call the RSC handler
360
+ * 4. Stream the Web Response back (with optional compression)
361
+ */
362
+ async function startAppRouterServer(options) {
363
+ const { port, host, clientDir, rscEntryPath, compress } = options;
364
+ // Import the RSC handler (use file:// URL for reliable dynamic import)
365
+ const rscModule = await import(pathToFileURL(rscEntryPath).href);
366
+ const rscHandler = rscModule.default;
367
+ if (typeof rscHandler !== "function") {
368
+ console.error("[vinext] RSC entry does not export a default handler function");
369
+ process.exit(1);
370
+ }
371
+ const server = createServer(async (req, res) => {
372
+ const url = req.url ?? "/";
373
+ const pathname = url.split("?")[0];
374
+ // Guard against protocol-relative URL open redirect attacks.
375
+ // See comment in app-dev-server.ts _handleRequest for full explanation.
376
+ if (pathname.startsWith("//")) {
377
+ res.writeHead(404);
378
+ res.end("404 Not Found");
379
+ return;
380
+ }
381
+ // Serve static assets from client build
382
+ if (pathname !== "/" && tryServeStatic(req, res, clientDir, pathname, compress)) {
383
+ return;
384
+ }
385
+ // Image optimization passthrough (Node.js prod server has no Images binding;
386
+ // serves the original file with cache headers)
387
+ if (pathname === IMAGE_OPTIMIZATION_PATH) {
388
+ const parsedUrl = new URL(url, "http://localhost");
389
+ const params = parseImageParams(parsedUrl);
390
+ if (!params) {
391
+ res.writeHead(400);
392
+ res.end("Bad Request");
393
+ return;
394
+ }
395
+ // Serve the original image from the client build directory
396
+ if (tryServeStatic(req, res, clientDir, params.imageUrl, false)) {
397
+ return;
398
+ }
399
+ res.writeHead(404);
400
+ res.end("Image not found");
401
+ return;
402
+ }
403
+ try {
404
+ // Convert Node.js request to Web Request and call the RSC handler
405
+ const request = nodeToWebRequest(req);
406
+ const response = await rscHandler(request);
407
+ // Stream the Web Response back to the Node.js response
408
+ await sendWebResponse(response, req, res, compress);
409
+ }
410
+ catch (e) {
411
+ console.error("[vinext] Server error:", e);
412
+ if (!res.headersSent) {
413
+ res.writeHead(500);
414
+ res.end("Internal Server Error");
415
+ }
416
+ }
417
+ });
418
+ await new Promise((resolve) => {
419
+ server.listen(port, host, () => {
420
+ const addr = server.address();
421
+ const actualPort = typeof addr === "object" && addr ? addr.port : port;
422
+ console.log(`[vinext] Production server running at http://${host}:${actualPort}`);
423
+ resolve();
424
+ });
425
+ });
426
+ return server;
427
+ }
428
+ /**
429
+ * Start the Pages Router production server.
430
+ *
431
+ * Uses the server entry (dist/server/entry.js) which exports:
432
+ * - renderPage(request, url, manifest) — SSR rendering (Web Request → Response)
433
+ * - handleApiRoute(request, url) — API route handling (Web Request → Response)
434
+ * - runMiddleware(request) — middleware execution
435
+ * - vinextConfig — embedded next.config.js settings
436
+ */
437
+ async function startPagesRouterServer(options) {
438
+ const { port, host, clientDir, serverEntryPath, compress } = options;
439
+ // Load the SSR manifest (maps module URLs to client asset URLs)
440
+ let ssrManifest = {};
441
+ const manifestPath = path.join(clientDir, ".vite", "ssr-manifest.json");
442
+ if (fs.existsSync(manifestPath)) {
443
+ ssrManifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
444
+ }
445
+ // Load the build manifest to compute lazy chunks — chunks only reachable via
446
+ // dynamic imports (React.lazy, next/dynamic). These should not be
447
+ // modulepreloaded since they are fetched on demand.
448
+ const buildManifestPath = path.join(clientDir, ".vite", "manifest.json");
449
+ if (fs.existsSync(buildManifestPath)) {
450
+ try {
451
+ const buildManifest = JSON.parse(fs.readFileSync(buildManifestPath, "utf-8"));
452
+ const lazyChunks = computeLazyChunks(buildManifest);
453
+ if (lazyChunks.length > 0) {
454
+ globalThis.__VINEXT_LAZY_CHUNKS__ = lazyChunks;
455
+ }
456
+ }
457
+ catch { /* ignore parse errors */ }
458
+ }
459
+ // Import the server entry module (use file:// URL for reliable dynamic import)
460
+ const serverEntry = await import(pathToFileURL(serverEntryPath).href);
461
+ const { renderPage, handleApiRoute: handleApi, runMiddleware, vinextConfig } = serverEntry;
462
+ // Extract config values (embedded at build time in the server entry)
463
+ const basePath = vinextConfig?.basePath ?? "";
464
+ const trailingSlash = vinextConfig?.trailingSlash ?? false;
465
+ const configRedirects = vinextConfig?.redirects ?? [];
466
+ const configRewrites = vinextConfig?.rewrites ?? { beforeFiles: [], afterFiles: [], fallback: [] };
467
+ const configHeaders = vinextConfig?.headers ?? [];
468
+ const server = createServer(async (req, res) => {
469
+ const rawUrl = req.url ?? "/";
470
+ let url = rawUrl;
471
+ let pathname = url.split("?")[0];
472
+ // Guard against protocol-relative URL open redirect attacks.
473
+ // See comment in app-dev-server.ts _handleRequest for full explanation.
474
+ if (pathname.startsWith("//")) {
475
+ res.writeHead(404);
476
+ res.end("404 Not Found");
477
+ return;
478
+ }
479
+ // ── 1. Static assets ──────────────────────────────────────────
480
+ // Serve static files from client build. When basePath is configured,
481
+ // Vite's `base` config ensures assets are under basePath/assets/.
482
+ // We check both with and without basePath.
483
+ const staticLookupPath = basePath && pathname.startsWith(basePath)
484
+ ? pathname.slice(basePath.length) || "/"
485
+ : pathname;
486
+ if (staticLookupPath !== "/" &&
487
+ !staticLookupPath.startsWith("/api/") &&
488
+ tryServeStatic(req, res, clientDir, staticLookupPath, compress)) {
489
+ return;
490
+ }
491
+ // ── Image optimization passthrough ──────────────────────────────
492
+ if (pathname === IMAGE_OPTIMIZATION_PATH || staticLookupPath === IMAGE_OPTIMIZATION_PATH) {
493
+ const parsedUrl = new URL(rawUrl, "http://localhost");
494
+ const params = parseImageParams(parsedUrl);
495
+ if (!params) {
496
+ res.writeHead(400);
497
+ res.end("Bad Request");
498
+ return;
499
+ }
500
+ if (tryServeStatic(req, res, clientDir, params.imageUrl, false)) {
501
+ return;
502
+ }
503
+ res.writeHead(404);
504
+ res.end("Image not found");
505
+ return;
506
+ }
507
+ try {
508
+ // ── 2. Strip basePath ─────────────────────────────────────────
509
+ if (basePath && pathname.startsWith(basePath)) {
510
+ const stripped = pathname.slice(basePath.length) || "/";
511
+ const qs = url.includes("?") ? url.slice(url.indexOf("?")) : "";
512
+ url = stripped + qs;
513
+ pathname = stripped;
514
+ }
515
+ // ── 3. Trailing slash normalization ───────────────────────────
516
+ if (pathname !== "/" && !pathname.startsWith("/api")) {
517
+ const hasTrailing = pathname.endsWith("/");
518
+ if (trailingSlash && !hasTrailing) {
519
+ const qs = url.includes("?") ? url.slice(url.indexOf("?")) : "";
520
+ res.writeHead(308, { Location: basePath + pathname + "/" + qs });
521
+ res.end();
522
+ return;
523
+ }
524
+ else if (!trailingSlash && hasTrailing) {
525
+ const qs = url.includes("?") ? url.slice(url.indexOf("?")) : "";
526
+ res.writeHead(308, { Location: basePath + pathname.replace(/\/+$/, "") + qs });
527
+ res.end();
528
+ return;
529
+ }
530
+ }
531
+ // Convert Node.js req to Web Request for the server entry
532
+ const rawProtocol = trustProxy
533
+ ? req.headers["x-forwarded-proto"]?.split(",")[0]?.trim()
534
+ : undefined;
535
+ const protocol = rawProtocol === "https" || rawProtocol === "http" ? rawProtocol : "http";
536
+ const hostHeader = resolveHost(req, `${host}:${port}`);
537
+ const reqHeaders = Object.entries(req.headers).reduce((h, [k, v]) => {
538
+ if (v)
539
+ h.set(k, Array.isArray(v) ? v.join(", ") : v);
540
+ return h;
541
+ }, new Headers());
542
+ const method = req.method ?? "GET";
543
+ const hasBody = method !== "GET" && method !== "HEAD";
544
+ const webRequest = new Request(`${protocol}://${hostHeader}${url}`, {
545
+ method,
546
+ headers: reqHeaders,
547
+ body: hasBody ? readNodeStream(req) : undefined,
548
+ // @ts-expect-error — duplex needed for streaming request bodies
549
+ duplex: hasBody ? "half" : undefined,
550
+ });
551
+ // Build request context for has/missing condition matching
552
+ const reqCtx = requestContextFromRequest(webRequest);
553
+ // ── 4. Run middleware ─────────────────────────────────────────
554
+ let resolvedUrl = url;
555
+ const middlewareHeaders = {};
556
+ let middlewareRewriteStatus;
557
+ if (typeof runMiddleware === "function") {
558
+ const result = await runMiddleware(webRequest);
559
+ if (!result.continue) {
560
+ if (result.redirectUrl) {
561
+ res.writeHead(result.redirectStatus ?? 307, {
562
+ Location: result.redirectUrl,
563
+ });
564
+ res.end();
565
+ return;
566
+ }
567
+ if (result.response) {
568
+ // Use arrayBuffer() to handle binary response bodies correctly
569
+ const body = Buffer.from(await result.response.arrayBuffer());
570
+ res.writeHead(result.response.status, Object.fromEntries(result.response.headers));
571
+ res.end(body);
572
+ return;
573
+ }
574
+ }
575
+ // Collect middleware response headers to merge into final response
576
+ if (result.responseHeaders) {
577
+ for (const [key, value] of result.responseHeaders) {
578
+ middlewareHeaders[key] = value;
579
+ }
580
+ }
581
+ // Apply middleware rewrite
582
+ if (result.rewriteUrl) {
583
+ resolvedUrl = result.rewriteUrl;
584
+ }
585
+ // Apply custom status code from middleware rewrite
586
+ // (e.g. NextResponse.rewrite(url, { status: 403 }))
587
+ middlewareRewriteStatus = result.rewriteStatus;
588
+ }
589
+ // Unpack x-middleware-request-* headers into the actual request so that
590
+ // renderPage / handleApiRoute see the middleware-modified headers.
591
+ // Also remove them from middlewareHeaders to prevent leaking as response headers.
592
+ const mwReqPrefix = "x-middleware-request-";
593
+ for (const key of Object.keys(middlewareHeaders)) {
594
+ if (key.startsWith(mwReqPrefix)) {
595
+ const realName = key.slice(mwReqPrefix.length);
596
+ webRequest.headers.set(realName, middlewareHeaders[key]);
597
+ delete middlewareHeaders[key];
598
+ }
599
+ }
600
+ let resolvedPathname = resolvedUrl.split("?")[0];
601
+ // ── 5. Apply custom headers from next.config.js ───────────────
602
+ if (configHeaders.length) {
603
+ const matched = matchHeaders(resolvedPathname, configHeaders);
604
+ for (const h of matched) {
605
+ middlewareHeaders[h.key.toLowerCase()] = h.value;
606
+ }
607
+ }
608
+ // ── 6. Apply redirects from next.config.js ────────────────────
609
+ if (configRedirects.length) {
610
+ const redirect = matchRedirect(resolvedPathname, configRedirects, reqCtx);
611
+ if (redirect) {
612
+ // Guard against double-prefixing: only add basePath if destination
613
+ // doesn't already start with it.
614
+ const dest = basePath && !redirect.destination.startsWith(basePath)
615
+ ? basePath + redirect.destination
616
+ : redirect.destination;
617
+ res.writeHead(redirect.permanent ? 308 : 307, { Location: dest });
618
+ res.end();
619
+ return;
620
+ }
621
+ }
622
+ // ── 7. Apply beforeFiles rewrites from next.config.js ─────────
623
+ if (configRewrites.beforeFiles?.length) {
624
+ const rewritten = matchRewrite(resolvedPathname, configRewrites.beforeFiles, reqCtx);
625
+ if (rewritten) {
626
+ if (isExternalUrl(rewritten)) {
627
+ const proxyResponse = await proxyExternalRequest(webRequest, rewritten);
628
+ await sendWebResponse(proxyResponse, req, res, compress);
629
+ return;
630
+ }
631
+ resolvedUrl = rewritten;
632
+ resolvedPathname = rewritten.split("?")[0];
633
+ }
634
+ }
635
+ // ── 8. API routes ─────────────────────────────────────────────
636
+ if (resolvedPathname.startsWith("/api/") || resolvedPathname === "/api") {
637
+ let response;
638
+ if (typeof handleApi === "function") {
639
+ response = await handleApi(webRequest, resolvedUrl);
640
+ }
641
+ else {
642
+ response = new Response("404 - API route not found", { status: 404 });
643
+ }
644
+ // Merge middleware + config headers into the response
645
+ const responseBody = await response.text();
646
+ const ct = response.headers.get("content-type") ?? "text/html";
647
+ const responseHeaders = { ...middlewareHeaders };
648
+ response.headers.forEach((v, k) => { responseHeaders[k] = v; });
649
+ sendCompressed(req, res, responseBody, ct, middlewareRewriteStatus ?? response.status, responseHeaders, compress);
650
+ return;
651
+ }
652
+ // ── 9. Apply afterFiles rewrites from next.config.js ──────────
653
+ if (configRewrites.afterFiles?.length) {
654
+ const rewritten = matchRewrite(resolvedPathname, configRewrites.afterFiles, reqCtx);
655
+ if (rewritten) {
656
+ if (isExternalUrl(rewritten)) {
657
+ const proxyResponse = await proxyExternalRequest(webRequest, rewritten);
658
+ await sendWebResponse(proxyResponse, req, res, compress);
659
+ return;
660
+ }
661
+ resolvedUrl = rewritten;
662
+ resolvedPathname = rewritten.split("?")[0];
663
+ }
664
+ }
665
+ // ── 10. SSR page rendering ────────────────────────────────────
666
+ let response;
667
+ if (typeof renderPage === "function") {
668
+ response = await renderPage(webRequest, resolvedUrl, ssrManifest);
669
+ // ── 11. Fallback rewrites (if SSR returned 404) ─────────────
670
+ if (response && response.status === 404 && configRewrites.fallback?.length) {
671
+ const fallbackRewrite = matchRewrite(resolvedPathname, configRewrites.fallback, reqCtx);
672
+ if (fallbackRewrite) {
673
+ if (isExternalUrl(fallbackRewrite)) {
674
+ const proxyResponse = await proxyExternalRequest(webRequest, fallbackRewrite);
675
+ await sendWebResponse(proxyResponse, req, res, compress);
676
+ return;
677
+ }
678
+ response = await renderPage(webRequest, fallbackRewrite, ssrManifest);
679
+ }
680
+ }
681
+ }
682
+ if (!response) {
683
+ res.writeHead(404);
684
+ res.end("404 - Not found");
685
+ return;
686
+ }
687
+ // Merge middleware + config headers into the response
688
+ const responseBody = await response.text();
689
+ const ct = response.headers.get("content-type") ?? "text/html";
690
+ const responseHeaders = { ...middlewareHeaders };
691
+ response.headers.forEach((v, k) => { responseHeaders[k] = v; });
692
+ sendCompressed(req, res, responseBody, ct, middlewareRewriteStatus ?? response.status, responseHeaders, compress);
693
+ }
694
+ catch (e) {
695
+ console.error("[vinext] Server error:", e);
696
+ res.writeHead(500);
697
+ res.end("Internal Server Error");
698
+ }
699
+ });
700
+ await new Promise((resolve) => {
701
+ server.listen(port, host, () => {
702
+ const addr = server.address();
703
+ const actualPort = typeof addr === "object" && addr ? addr.port : port;
704
+ console.log(`[vinext] Production server running at http://${host}:${actualPort}`);
705
+ resolve();
706
+ });
707
+ });
708
+ return server;
709
+ }
710
+ // Export helpers for testing
711
+ export { sendCompressed, negotiateEncoding, COMPRESSIBLE_TYPES, COMPRESS_THRESHOLD, resolveHost, trustedHosts, trustProxy, nodeToWebRequest };
712
+ //# sourceMappingURL=prod-server.js.map