213 lines
6.3 KiB
JavaScript
213 lines
6.3 KiB
JavaScript
const fs = require('node:fs');
|
|
const http = require('node:http');
|
|
const path = require('node:path');
|
|
|
|
const { createLogger, isVerbose } = require('../src/generator/utils/logger');
|
|
|
|
const log = createLogger('serve');
|
|
|
|
function parseInteger(value, fallback) {
|
|
const n = Number.parseInt(String(value), 10);
|
|
return Number.isFinite(n) ? n : fallback;
|
|
}
|
|
|
|
function parseArgs(argv) {
|
|
const args = Array.isArray(argv) ? argv.slice() : [];
|
|
|
|
const getValue = (keys) => {
|
|
const idx = args.findIndex((a) => keys.includes(a));
|
|
if (idx === -1) return null;
|
|
const next = args[idx + 1];
|
|
return next ? String(next) : null;
|
|
};
|
|
|
|
const portArg = getValue(['--port', '-p']);
|
|
const hostArg = getValue(['--host', '-h']);
|
|
const rootArg = getValue(['--root']);
|
|
|
|
return {
|
|
port: portArg ? parseInteger(portArg, null) : null,
|
|
host: hostArg ? String(hostArg) : null,
|
|
root: rootArg ? String(rootArg) : null,
|
|
};
|
|
}
|
|
|
|
function getContentType(filePath) {
|
|
const ext = path.extname(filePath).toLowerCase();
|
|
if (ext === '.html') return 'text/html; charset=utf-8';
|
|
if (ext === '.js') return 'text/javascript; charset=utf-8';
|
|
if (ext === '.css') return 'text/css; charset=utf-8';
|
|
if (ext === '.json') return 'application/json; charset=utf-8';
|
|
if (ext === '.svg') return 'image/svg+xml';
|
|
if (ext === '.png') return 'image/png';
|
|
if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg';
|
|
if (ext === '.gif') return 'image/gif';
|
|
if (ext === '.webp') return 'image/webp';
|
|
if (ext === '.ico') return 'image/x-icon';
|
|
if (ext === '.txt') return 'text/plain; charset=utf-8';
|
|
if (ext === '.xml') return 'application/xml; charset=utf-8';
|
|
if (ext === '.map') return 'application/json; charset=utf-8';
|
|
return 'application/octet-stream';
|
|
}
|
|
|
|
function sendNotFound(res, rootDir) {
|
|
const fallback404 = path.join(rootDir, '404.html');
|
|
if (fs.existsSync(fallback404)) {
|
|
res.statusCode = 404;
|
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
fs.createReadStream(fallback404).pipe(res);
|
|
return;
|
|
}
|
|
|
|
res.statusCode = 404;
|
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
res.end('Not Found');
|
|
}
|
|
|
|
function sendFile(req, res, filePath, rootDir) {
|
|
try {
|
|
const stat = fs.statSync(filePath);
|
|
if (!stat.isFile()) return sendNotFound(res, rootDir);
|
|
|
|
res.statusCode = 200;
|
|
res.setHeader('Content-Type', getContentType(filePath));
|
|
res.setHeader('Content-Length', String(stat.size));
|
|
|
|
if (req.method === 'HEAD') {
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
fs.createReadStream(filePath).pipe(res);
|
|
} catch (error) {
|
|
log.warn('读取文件失败', {
|
|
path: filePath,
|
|
message: error && error.message ? error.message : String(error),
|
|
});
|
|
if (isVerbose() && error && error.stack) console.error(error.stack);
|
|
sendNotFound(res, rootDir);
|
|
}
|
|
}
|
|
|
|
function buildHandler(rootDir) {
|
|
const normalizedRoot = path.resolve(rootDir);
|
|
|
|
return (req, res) => {
|
|
const rawUrl = req.url || '/';
|
|
const rawPath = rawUrl.split('?')[0] || '/';
|
|
|
|
let decodedPath = '/';
|
|
try {
|
|
decodedPath = decodeURIComponent(rawPath);
|
|
} catch {
|
|
res.statusCode = 400;
|
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
res.end('Bad Request');
|
|
return;
|
|
}
|
|
|
|
const safePath = decodedPath.replace(/\\/g, '/');
|
|
const resolved = path.resolve(normalizedRoot, `.${safePath}`);
|
|
if (!resolved.startsWith(`${normalizedRoot}${path.sep}`) && resolved !== normalizedRoot) {
|
|
res.statusCode = 403;
|
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
res.end('Forbidden');
|
|
return;
|
|
}
|
|
|
|
let target = resolved;
|
|
try {
|
|
if (fs.existsSync(target) && fs.statSync(target).isDirectory()) {
|
|
target = path.join(target, 'index.html');
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
|
|
if (!fs.existsSync(target)) {
|
|
sendNotFound(res, normalizedRoot);
|
|
return;
|
|
}
|
|
|
|
sendFile(req, res, target, normalizedRoot);
|
|
};
|
|
}
|
|
|
|
function startServer(options = {}) {
|
|
const { rootDir, host, port } = options;
|
|
const normalizedRoot = path.resolve(rootDir);
|
|
|
|
if (!fs.existsSync(normalizedRoot)) {
|
|
throw new Error(`dist 目录不存在:${normalizedRoot}`);
|
|
}
|
|
|
|
const handler = buildHandler(normalizedRoot);
|
|
const server = http.createServer(handler);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
server.on('error', reject);
|
|
server.listen(port, host, () => {
|
|
const addr = server.address();
|
|
const actualPort = addr && typeof addr === 'object' ? addr.port : port;
|
|
resolve({ server, port: actualPort, host });
|
|
});
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
const repoRoot = path.resolve(__dirname, '..');
|
|
const defaultRoot = path.join(repoRoot, 'dist');
|
|
const args = parseArgs(process.argv.slice(2));
|
|
|
|
const port =
|
|
args.port ?? parseInteger(process.env.PORT || process.env.MENAV_PORT || '5173', 5173);
|
|
const host = args.host || process.env.HOST || '0.0.0.0';
|
|
const rootDir = args.root ? path.resolve(repoRoot, args.root) : defaultRoot;
|
|
|
|
log.info('启动静态服务', { root: path.relative(repoRoot, rootDir) || '.', host, port });
|
|
|
|
const { server, port: actualPort } = await startServer({ rootDir, host, port });
|
|
|
|
log.ok('就绪', { url: `http://localhost:${actualPort}` });
|
|
|
|
let shuttingDown = false;
|
|
const shutdown = (signal) => {
|
|
if (shuttingDown) return;
|
|
shuttingDown = true;
|
|
|
|
process.stdout.write('\n');
|
|
log.info('正在关闭...', { signal });
|
|
|
|
try {
|
|
if (typeof server.closeIdleConnections === 'function') server.closeIdleConnections();
|
|
if (typeof server.closeAllConnections === 'function') server.closeAllConnections();
|
|
} catch {
|
|
// ignore
|
|
}
|
|
|
|
const exit = signal === 'SIGINT' ? 130 : 0;
|
|
const forceTimer = setTimeout(() => process.exit(exit), 2000);
|
|
if (typeof forceTimer.unref === 'function') forceTimer.unref();
|
|
|
|
server.close(() => {
|
|
clearTimeout(forceTimer);
|
|
process.exit(exit);
|
|
});
|
|
};
|
|
|
|
process.once('SIGINT', () => shutdown('SIGINT'));
|
|
process.once('SIGTERM', () => shutdown('SIGTERM'));
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main().catch((error) => {
|
|
log.error('启动失败', { message: error && error.message ? error.message : String(error) });
|
|
if (isVerbose() && error && error.stack) console.error(error.stack);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
startServer,
|
|
};
|