-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathcli.mjs
More file actions
90 lines (82 loc) · 2.81 KB
/
Copy pathcli.mjs
File metadata and controls
90 lines (82 loc) · 2.81 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
#!/usr/bin/env node
import { readFileSync } from "node:fs";
import { createServer } from "node:http";
import { extname, isAbsolute, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const OUT_DIR = resolve(__dirname, "out");
const portFlag = process.argv.indexOf("--port");
const rawPort = portFlag !== -1 && portFlag + 1 < process.argv.length
? Number(process.argv[portFlag + 1])
: process.env.PORT
? Number(process.env.PORT)
: 3000;
const preferredPort = Number.isInteger(rawPort) && rawPort >= 0 && rawPort <= 65535 ? rawPort : 3000;
function findAvailablePort(startPort, maxRetries = 100) {
return new Promise((resolve, reject) => {
if (maxRetries <= 0) {
reject(new Error("Could not find an available port after 100 attempts."));
return;
}
const server = createServer();
server.listen(startPort, () => {
const { port } = server.address();
server.close(() => resolve(port));
});
server.on("error", () => {
const nextPort = startPort >= 65535 ? 1024 : startPort + 1;
findAvailablePort(nextPort, maxRetries - 1).then(resolve, reject);
});
});
}
let PORT;
try {
PORT = await findAvailablePort(preferredPort);
} catch (err) {
console.error(`Error: ${err.message}`);
process.exit(1);
}
const MIME = {
".html": "text/html",
".js": "application/javascript",
".css": "text/css",
".png": "image/png",
".svg": "image/svg+xml",
".json": "application/json",
".woff2": "font/woff2",
".glsl": "text/plain",
};
createServer((req, res) => {
let url = req.url ?? "/";
// strip query strings
url = url.split("?")[0];
// Next.js static export uses trailing slashes → serve index.html
if (url.endsWith("/")) url += "index.html";
const filePath = resolve(OUT_DIR, url.replace(/^\//, ""));
// path traversal guard
const rel = relative(OUT_DIR, filePath);
if (rel.startsWith("..") || isAbsolute(rel)) {
res.writeHead(403); res.end(); return;
}
try {
const data = readFileSync(filePath);
res.writeHead(200, { "Content-Type": MIME[extname(filePath)] ?? "application/octet-stream" });
res.end(data);
} catch {
// fallback to index.html for SPA-style routing
try {
const data = readFileSync(resolve(OUT_DIR, "index.html"));
res.writeHead(200, { "Content-Type": "text/html" });
res.end(data);
} catch {
res.writeHead(404); res.end("Not found");
}
}
}).listen(PORT, () => {
const url = `http://localhost:${PORT}`;
console.log(`Serving on ${url}`);
// open browser without any dependency
const { platform } = process;
const cmd = platform === "win32" ? "start" : platform === "darwin" ? "open" : "xdg-open";
import("node:child_process").then(({ exec }) => exec(`${cmd} ${url}`, () => {}));
});