-
Notifications
You must be signed in to change notification settings - Fork 961
Expand file tree
/
Copy pathserve.ts
More file actions
230 lines (196 loc) · 7.8 KB
/
serve.ts
File metadata and controls
230 lines (196 loc) · 7.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
import { createServer } from "http";
import { parse } from "url";
import path from "path";
import fs from "fs";
import { createRequire } from "module";
const require = createRequire(import.meta.url);
// --- 1. ENV SETUP ---
// @ts-ignore
process.env['NODE_ENV'] = "production";
process.env['__NEXT_PRIVATE_PREBUNDLED_REACT'] = 'experimental';
async function start() {
// --- 2. CONFIGURATION ---
const serverDir = process.argv[2] ? path.resolve(process.argv[2]) : process.cwd();
const PORT = parseInt(process.env.PORT || "8080");
console.log(`> Starting server from: ${serverDir}`);
// Import Next.js internals
const nextMetaPath = require.resolve("next/dist/server/request-meta", { paths: [serverDir] });
const { NEXT_REQUEST_META } = require(nextMetaPath);
// Load build config
let configPath = path.join(serverDir, "output.json");
if (!fs.existsSync(configPath)) {
configPath = path.join(process.cwd(), ".apphosting", "output.json");
}
if (!fs.existsSync(configPath)) {
console.error(`❌ Config not found at: ${configPath}`);
process.exit(1);
}
const rawConfig = fs.readFileSync(configPath, 'utf-8');
const buildContext = JSON.parse(rawConfig);
// --- HELPER: EDGE ROUTING ENGINE ---
function applyRoutingRules(req: any, res: any, pathname: string) {
const routing = buildContext.routing;
if (!routing) return false;
// 1. Headers (beforeMiddleware)
if (routing.beforeMiddleware) {
for (const rule of routing.beforeMiddleware) {
if (rule.sourceRegex && new RegExp(rule.sourceRegex).test(pathname)) {
if (rule.headers) {
for (const [key, value] of Object.entries(rule.headers)) {
res.setHeader(key, value);
}
console.log(`[EDGE] 🔧 Applied headers for: ${pathname}`);
}
// Handle Redirects
if (rule.status && (rule.status >= 300 && rule.status < 400) && rule.headers?.Location) {
console.log(`[EDGE] ↪️ Redirecting ${pathname} -> ${rule.headers.Location} (${rule.status})`);
res.statusCode = rule.status;
res.setHeader('Location', rule.headers.Location);
res.end();
return true;
}
}
}
}
return false;
}
// --- HELPER: PPR STATE ---
const getPostponedState = (path: string) => {
let prerender = buildContext.outputs?.prerenders?.find((it: any) => it.pathname === path);
if (!prerender && buildContext.routes?.dynamicRoutes) {
const dynamicMatch = buildContext.routes.dynamicRoutes.find((it: any) =>
path.match(new RegExp(it.sourceRegex))
)?.source;
if (dynamicMatch) {
prerender = buildContext.outputs?.prerenders?.find((it: any) => it.pathname === dynamicMatch);
}
}
return prerender?.fallback?.postponedState;
};
// --- 3. SERVER INSTANTIATION ---
const nextPath = require.resolve("next/dist/server/next-server", { paths: [serverDir] });
const NextServer = require(nextPath).default;
// Minimal Server (PPR)
const minimalServer = new NextServer({
dir: serverDir,
hostname: '0.0.0.0',
port: PORT,
conf: buildContext.config,
minimalMode: true,
customServer: false
});
// Full Server (Actions/API/Standard)
const fullServer = new NextServer({
dir: serverDir,
hostname: '0.0.0.0',
port: PORT,
conf: buildContext.config,
minimalMode: false,
customServer: false
});
console.log("Hydrating Next.js servers...");
try {
await minimalServer.prepare();
await fullServer.prepare();
} catch (e) {
console.error("Failed to prepare Next.js servers:", e);
process.exit(1);
}
const minimalRequestHandler = minimalServer.getRequestHandler();
const fullRequestHandler = fullServer.getRequestHandler();
// --- 4. REQUEST HANDLER ---
const server = createServer(async (req: any, res: any) => {
try {
const parsedUrl = parse(req.url, true);
const { pathname } = parsedUrl;
// [A] ROUTING ENGINE
const handled = applyRoutingRules(req, res, pathname || "/");
if (handled) return;
// [B] STATIC FILE HANDLING
// We check if the request matches a file on disk.
// Since we aren't changing directories, we must construct the full path manually.
if (pathname) {
let filePath = null;
// 1. _next/static assets -> map to .next/static
if (pathname.startsWith("/_next/static/")) {
filePath = path.join(serverDir, ".next/static", pathname.replace(/^\/_next\/static\//, ""));
}
// 2. Explicit /public/ request -> map to public folder
else if (pathname.startsWith("/public/")) {
filePath = path.join(serverDir, pathname);
}
// 3. Root assets (favicon.ico, sparky3d.gif) -> Check inside 'public' folder
// This was the missing piece! Root URLs often map to the public folder.
else if (!pathname.startsWith("/_next/")) {
const publicFilePath = path.join(serverDir, "public", pathname);
if (fs.existsSync(publicFilePath) && fs.statSync(publicFilePath).isFile()) {
filePath = publicFilePath;
}
}
// If we calculated a path AND it exists, serve it
if (filePath && fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
const ext = path.extname(filePath).toLowerCase();
const mimeTypes: Record<string, string> = {
'.js': 'application/javascript', '.css': 'text/css', '.png': 'image/png',
'.jpg': 'image/jpeg', '.svg': 'image/svg+xml', '.ico': 'image/x-icon', '.gif': 'image/gif'
};
res.setHeader("Content-Type", mimeTypes[ext] || 'application/octet-stream');
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
fs.createReadStream(filePath).pipe(res);
return;
}
}
// [C] SERVER ACTIONS (POST)
if (req.method === 'POST') {
console.log(`[ACTION] ⚡️ Routing POST to Full Server: ${pathname}`);
await fullRequestHandler(req, res, parsedUrl);
return;
}
// [D] RESUME REQUESTS (PPR Stream)
if (req.headers['next-resume'] === '1' && pathname) {
console.log(`[RESUME] 🌊 Intercepted Resume request for: ${pathname}`);
const postponed = getPostponedState(pathname);
if (postponed) {
req[NEXT_REQUEST_META] = { postponed };
}
return minimalRequestHandler(req, res, parsedUrl);
}
if (!req.headers['x-matched-path']) {
req.headers['x-matched-path'] = pathname;
}
// [E] ROUTING DECISION
const match = await minimalServer.matchers.match(pathname, {});
let isPPR = false;
if (match) {
const manifest = minimalServer.getPrerenderManifest();
const safePathname = pathname || "";
const routeData = manifest.routes[safePathname] ||
(match.definition && manifest.dynamicRoutes[match.definition.pathname]);
if (routeData && routeData.experimentalPPR) {
isPPR = true;
}
}
if (isPPR) {
console.log(`[MINIMAL] 🟢 Routing to Minimal Server (PPR): ${pathname}`);
await minimalRequestHandler(req, res, parsedUrl);
} else {
console.log(`[FULL] 🔵 Routing to Full Server: ${pathname}`);
await fullRequestHandler(req, res, parsedUrl);
}
} catch (err) {
console.error(err);
res.statusCode = 500;
res.end("Internal Server Error");
}
});
server.on('error', (e: any) => {
if (e.code === 'EADDRINUSE') {
console.error(`\n❌ FATAL: Port ${PORT} is already in use.`);
process.exit(1);
}
});
server.listen(PORT, () => {
console.log(`> Ready on http://localhost:${PORT}`);
});
}
start();