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.
- package/LICENSE +21 -0
- package/README.md +1 -0
- package/dist/build/static-export.d.ts +78 -0
- package/dist/build/static-export.d.ts.map +1 -0
- package/dist/build/static-export.js +553 -0
- package/dist/build/static-export.js.map +1 -0
- package/dist/check.d.ts +52 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +483 -0
- package/dist/check.js.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +565 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/entry.d.ts +2 -0
- package/dist/client/entry.d.ts.map +1 -0
- package/dist/client/entry.js +85 -0
- package/dist/client/entry.js.map +1 -0
- package/dist/cloudflare/index.d.ts +8 -0
- package/dist/cloudflare/index.d.ts.map +1 -0
- package/dist/cloudflare/index.js +8 -0
- package/dist/cloudflare/index.js.map +1 -0
- package/dist/cloudflare/kv-cache-handler.d.ts +68 -0
- package/dist/cloudflare/kv-cache-handler.d.ts.map +1 -0
- package/dist/cloudflare/kv-cache-handler.js +304 -0
- package/dist/cloudflare/kv-cache-handler.js.map +1 -0
- package/dist/cloudflare/tpr.d.ts +78 -0
- package/dist/cloudflare/tpr.d.ts.map +1 -0
- package/dist/cloudflare/tpr.js +672 -0
- package/dist/cloudflare/tpr.js.map +1 -0
- package/dist/config/config-matchers.d.ts +106 -0
- package/dist/config/config-matchers.d.ts.map +1 -0
- package/dist/config/config-matchers.js +499 -0
- package/dist/config/config-matchers.js.map +1 -0
- package/dist/config/next-config.d.ts +153 -0
- package/dist/config/next-config.d.ts.map +1 -0
- package/dist/config/next-config.js +274 -0
- package/dist/config/next-config.js.map +1 -0
- package/dist/deploy.d.ts +87 -0
- package/dist/deploy.d.ts.map +1 -0
- package/dist/deploy.js +644 -0
- package/dist/deploy.js.map +1 -0
- package/dist/index.d.ts +156 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3296 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +55 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +201 -0
- package/dist/init.js.map +1 -0
- package/dist/routing/app-router.d.ts +96 -0
- package/dist/routing/app-router.d.ts.map +1 -0
- package/dist/routing/app-router.js +815 -0
- package/dist/routing/app-router.js.map +1 -0
- package/dist/routing/pages-router.d.ts +52 -0
- package/dist/routing/pages-router.d.ts.map +1 -0
- package/dist/routing/pages-router.js +239 -0
- package/dist/routing/pages-router.js.map +1 -0
- package/dist/server/api-handler.d.ts +18 -0
- package/dist/server/api-handler.d.ts.map +1 -0
- package/dist/server/api-handler.js +169 -0
- package/dist/server/api-handler.js.map +1 -0
- package/dist/server/app-dev-server.d.ts +42 -0
- package/dist/server/app-dev-server.d.ts.map +1 -0
- package/dist/server/app-dev-server.js +2718 -0
- package/dist/server/app-dev-server.js.map +1 -0
- package/dist/server/app-router-entry.d.ts +18 -0
- package/dist/server/app-router-entry.d.ts.map +1 -0
- package/dist/server/app-router-entry.js +34 -0
- package/dist/server/app-router-entry.js.map +1 -0
- package/dist/server/dev-server.d.ts +40 -0
- package/dist/server/dev-server.d.ts.map +1 -0
- package/dist/server/dev-server.js +758 -0
- package/dist/server/dev-server.js.map +1 -0
- package/dist/server/html.d.ts +22 -0
- package/dist/server/html.d.ts.map +1 -0
- package/dist/server/html.js +29 -0
- package/dist/server/html.js.map +1 -0
- package/dist/server/image-optimization.d.ts +56 -0
- package/dist/server/image-optimization.d.ts.map +1 -0
- package/dist/server/image-optimization.js +103 -0
- package/dist/server/image-optimization.js.map +1 -0
- package/dist/server/instrumentation.d.ts +68 -0
- package/dist/server/instrumentation.d.ts.map +1 -0
- package/dist/server/instrumentation.js +90 -0
- package/dist/server/instrumentation.js.map +1 -0
- package/dist/server/isr-cache.d.ts +61 -0
- package/dist/server/isr-cache.d.ts.map +1 -0
- package/dist/server/isr-cache.js +134 -0
- package/dist/server/isr-cache.js.map +1 -0
- package/dist/server/metadata-routes.d.ts +103 -0
- package/dist/server/metadata-routes.d.ts.map +1 -0
- package/dist/server/metadata-routes.js +270 -0
- package/dist/server/metadata-routes.js.map +1 -0
- package/dist/server/middleware.d.ts +77 -0
- package/dist/server/middleware.d.ts.map +1 -0
- package/dist/server/middleware.js +228 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/prod-server.d.ts +78 -0
- package/dist/server/prod-server.d.ts.map +1 -0
- package/dist/server/prod-server.js +712 -0
- package/dist/server/prod-server.js.map +1 -0
- package/dist/shims/amp.d.ts +17 -0
- package/dist/shims/amp.d.ts.map +1 -0
- package/dist/shims/amp.js +21 -0
- package/dist/shims/amp.js.map +1 -0
- package/dist/shims/app.d.ts +12 -0
- package/dist/shims/app.d.ts.map +1 -0
- package/dist/shims/app.js +2 -0
- package/dist/shims/app.js.map +1 -0
- package/dist/shims/cache-runtime.d.ts +68 -0
- package/dist/shims/cache-runtime.d.ts.map +1 -0
- package/dist/shims/cache-runtime.js +437 -0
- package/dist/shims/cache-runtime.js.map +1 -0
- package/dist/shims/cache.d.ts +243 -0
- package/dist/shims/cache.d.ts.map +1 -0
- package/dist/shims/cache.js +415 -0
- package/dist/shims/cache.js.map +1 -0
- package/dist/shims/client-only.d.ts +18 -0
- package/dist/shims/client-only.d.ts.map +1 -0
- package/dist/shims/client-only.js +18 -0
- package/dist/shims/client-only.js.map +1 -0
- package/dist/shims/config.d.ts +27 -0
- package/dist/shims/config.d.ts.map +1 -0
- package/dist/shims/config.js +30 -0
- package/dist/shims/config.js.map +1 -0
- package/dist/shims/constants.d.ts +13 -0
- package/dist/shims/constants.d.ts.map +1 -0
- package/dist/shims/constants.js +13 -0
- package/dist/shims/constants.js.map +1 -0
- package/dist/shims/document.d.ts +33 -0
- package/dist/shims/document.d.ts.map +1 -0
- package/dist/shims/document.js +32 -0
- package/dist/shims/document.js.map +1 -0
- package/dist/shims/dynamic.d.ts +33 -0
- package/dist/shims/dynamic.d.ts.map +1 -0
- package/dist/shims/dynamic.js +149 -0
- package/dist/shims/dynamic.js.map +1 -0
- package/dist/shims/error-boundary.d.ts +33 -0
- package/dist/shims/error-boundary.d.ts.map +1 -0
- package/dist/shims/error-boundary.js +88 -0
- package/dist/shims/error-boundary.js.map +1 -0
- package/dist/shims/error.d.ts +16 -0
- package/dist/shims/error.d.ts.map +1 -0
- package/dist/shims/error.js +45 -0
- package/dist/shims/error.js.map +1 -0
- package/dist/shims/fetch-cache.d.ts +61 -0
- package/dist/shims/fetch-cache.d.ts.map +1 -0
- package/dist/shims/fetch-cache.js +307 -0
- package/dist/shims/fetch-cache.js.map +1 -0
- package/dist/shims/font-google.d.ts +122 -0
- package/dist/shims/font-google.d.ts.map +1 -0
- package/dist/shims/font-google.js +387 -0
- package/dist/shims/font-google.js.map +1 -0
- package/dist/shims/font-local.d.ts +61 -0
- package/dist/shims/font-local.d.ts.map +1 -0
- package/dist/shims/font-local.js +303 -0
- package/dist/shims/font-local.js.map +1 -0
- package/dist/shims/form.d.ts +30 -0
- package/dist/shims/form.d.ts.map +1 -0
- package/dist/shims/form.js +78 -0
- package/dist/shims/form.js.map +1 -0
- package/dist/shims/head-state.d.ts +11 -0
- package/dist/shims/head-state.d.ts.map +1 -0
- package/dist/shims/head-state.js +47 -0
- package/dist/shims/head-state.js.map +1 -0
- package/dist/shims/head.d.ts +28 -0
- package/dist/shims/head.d.ts.map +1 -0
- package/dist/shims/head.js +148 -0
- package/dist/shims/head.js.map +1 -0
- package/dist/shims/headers.d.ts +150 -0
- package/dist/shims/headers.d.ts.map +1 -0
- package/dist/shims/headers.js +412 -0
- package/dist/shims/headers.js.map +1 -0
- package/dist/shims/image-config.d.ts +30 -0
- package/dist/shims/image-config.d.ts.map +1 -0
- package/dist/shims/image-config.js +91 -0
- package/dist/shims/image-config.js.map +1 -0
- package/dist/shims/image.d.ts +63 -0
- package/dist/shims/image.d.ts.map +1 -0
- package/dist/shims/image.js +284 -0
- package/dist/shims/image.js.map +1 -0
- package/dist/shims/internal/api-utils.d.ts +12 -0
- package/dist/shims/internal/api-utils.d.ts.map +1 -0
- package/dist/shims/internal/api-utils.js +7 -0
- package/dist/shims/internal/api-utils.js.map +1 -0
- package/dist/shims/internal/app-router-context.d.ts +21 -0
- package/dist/shims/internal/app-router-context.d.ts.map +1 -0
- package/dist/shims/internal/app-router-context.js +15 -0
- package/dist/shims/internal/app-router-context.js.map +1 -0
- package/dist/shims/internal/cookies.d.ts +9 -0
- package/dist/shims/internal/cookies.d.ts.map +1 -0
- package/dist/shims/internal/cookies.js +9 -0
- package/dist/shims/internal/cookies.js.map +1 -0
- package/dist/shims/internal/router-context.d.ts +2 -0
- package/dist/shims/internal/router-context.d.ts.map +1 -0
- package/dist/shims/internal/router-context.js +9 -0
- package/dist/shims/internal/router-context.js.map +1 -0
- package/dist/shims/internal/utils.d.ts +48 -0
- package/dist/shims/internal/utils.d.ts.map +1 -0
- package/dist/shims/internal/utils.js +35 -0
- package/dist/shims/internal/utils.js.map +1 -0
- package/dist/shims/internal/work-unit-async-storage.d.ts +12 -0
- package/dist/shims/internal/work-unit-async-storage.d.ts.map +1 -0
- package/dist/shims/internal/work-unit-async-storage.js +13 -0
- package/dist/shims/internal/work-unit-async-storage.js.map +1 -0
- package/dist/shims/layout-segment-context.d.ts +21 -0
- package/dist/shims/layout-segment-context.d.ts.map +1 -0
- package/dist/shims/layout-segment-context.js +27 -0
- package/dist/shims/layout-segment-context.js.map +1 -0
- package/dist/shims/legacy-image.d.ts +52 -0
- package/dist/shims/legacy-image.d.ts.map +1 -0
- package/dist/shims/legacy-image.js +46 -0
- package/dist/shims/legacy-image.js.map +1 -0
- package/dist/shims/link.d.ts +48 -0
- package/dist/shims/link.d.ts.map +1 -0
- package/dist/shims/link.js +395 -0
- package/dist/shims/link.js.map +1 -0
- package/dist/shims/metadata.d.ts +184 -0
- package/dist/shims/metadata.d.ts.map +1 -0
- package/dist/shims/metadata.js +472 -0
- package/dist/shims/metadata.js.map +1 -0
- package/dist/shims/navigation-state.d.ts +14 -0
- package/dist/shims/navigation-state.d.ts.map +1 -0
- package/dist/shims/navigation-state.js +77 -0
- package/dist/shims/navigation-state.js.map +1 -0
- package/dist/shims/navigation.d.ts +201 -0
- package/dist/shims/navigation.d.ts.map +1 -0
- package/dist/shims/navigation.js +672 -0
- package/dist/shims/navigation.js.map +1 -0
- package/dist/shims/og.d.ts +20 -0
- package/dist/shims/og.d.ts.map +1 -0
- package/dist/shims/og.js +19 -0
- package/dist/shims/og.js.map +1 -0
- package/dist/shims/router-state.d.ts +11 -0
- package/dist/shims/router-state.d.ts.map +1 -0
- package/dist/shims/router-state.js +56 -0
- package/dist/shims/router-state.js.map +1 -0
- package/dist/shims/router.d.ts +103 -0
- package/dist/shims/router.d.ts.map +1 -0
- package/dist/shims/router.js +536 -0
- package/dist/shims/router.js.map +1 -0
- package/dist/shims/script.d.ts +58 -0
- package/dist/shims/script.d.ts.map +1 -0
- package/dist/shims/script.js +163 -0
- package/dist/shims/script.js.map +1 -0
- package/dist/shims/server-only.d.ts +19 -0
- package/dist/shims/server-only.d.ts.map +1 -0
- package/dist/shims/server-only.js +19 -0
- package/dist/shims/server-only.js.map +1 -0
- package/dist/shims/server.d.ts +178 -0
- package/dist/shims/server.d.ts.map +1 -0
- package/dist/shims/server.js +377 -0
- package/dist/shims/server.js.map +1 -0
- package/dist/shims/web-vitals.d.ts +24 -0
- package/dist/shims/web-vitals.d.ts.map +1 -0
- package/dist/shims/web-vitals.js +17 -0
- package/dist/shims/web-vitals.js.map +1 -0
- package/dist/utils/hash.d.ts +6 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +20 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/project.d.ts +36 -0
- package/dist/utils/project.d.ts.map +1 -0
- package/dist/utils/project.js +112 -0
- package/dist/utils/project.js.map +1 -0
- package/dist/utils/query.d.ts +10 -0
- package/dist/utils/query.d.ts.map +1 -0
- package/dist/utils/query.js +27 -0
- package/dist/utils/query.js.map +1 -0
- package/package.json +65 -7
- 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
|