chore: 引入统一日志模块,统一 cli 输出
This commit is contained in:
192
scripts/serve-dist.js
Normal file
192
scripts/serve-dist.js
Normal file
@@ -0,0 +1,192 @@
|
||||
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}` });
|
||||
|
||||
const shutdown = () => {
|
||||
log.info('正在关闭...');
|
||||
server.close(() => process.exit(0));
|
||||
};
|
||||
|
||||
process.on('SIGINT', shutdown);
|
||||
process.on('SIGTERM', shutdown);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user