chore: 引入统一日志模块,统一 cli 输出
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
const path = require('node:path');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const { createLogger, isVerbose, startTimer } = require('../src/generator/utils/logger');
|
||||
|
||||
const log = createLogger('bundle');
|
||||
|
||||
function ensureDir(dirPath) {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
@@ -12,7 +16,7 @@ async function main() {
|
||||
try {
|
||||
esbuild = require('esbuild');
|
||||
} catch (error) {
|
||||
console.error('未找到 esbuild,请先执行 npm install。');
|
||||
log.error('未找到 esbuild,请先执行 npm install。');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
@@ -22,7 +26,7 @@ async function main() {
|
||||
const outFile = path.join(projectRoot, 'dist', 'script.js');
|
||||
|
||||
if (!fs.existsSync(entry)) {
|
||||
console.error(`运行时入口不存在:${path.relative(projectRoot, entry)}`);
|
||||
log.error('运行时入口不存在', { path: path.relative(projectRoot, entry) });
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
@@ -30,6 +34,7 @@ async function main() {
|
||||
ensureDir(path.dirname(outFile));
|
||||
|
||||
try {
|
||||
const elapsedMs = startTimer();
|
||||
const result = await esbuild.build({
|
||||
entryPoints: [entry],
|
||||
outfile: outFile,
|
||||
@@ -41,23 +46,25 @@ async function main() {
|
||||
minify: true,
|
||||
legalComments: 'none',
|
||||
metafile: true,
|
||||
logLevel: 'info',
|
||||
logLevel: 'silent',
|
||||
});
|
||||
|
||||
const ms = elapsedMs();
|
||||
const outputs =
|
||||
result && result.metafile && result.metafile.outputs ? result.metafile.outputs : null;
|
||||
const outKey = outputs ? Object.keys(outputs).find((k) => k.endsWith('dist/script.js')) : '';
|
||||
const bytes = outKey && outputs && outputs[outKey] ? outputs[outKey].bytes : 0;
|
||||
if (bytes) {
|
||||
console.log(`✅ runtime bundle 完成:dist/script.js (${bytes} bytes)`);
|
||||
} else {
|
||||
console.log('✅ runtime bundle 完成:dist/script.js');
|
||||
}
|
||||
|
||||
const meta = { ms };
|
||||
if (bytes) meta.bytes = bytes;
|
||||
log.ok('输出 dist/script.js', meta);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'❌ runtime bundle 失败(禁止回退旧产物):',
|
||||
error && error.message ? error.message : error
|
||||
);
|
||||
log.error('构建 dist/script.js 失败', {
|
||||
message: error && error.message ? error.message : String(error),
|
||||
});
|
||||
if (isVerbose() && error && error.stack) {
|
||||
console.error(error.stack);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
42
scripts/build.js
Normal file
42
scripts/build.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const { createLogger, isVerbose, startTimer } = require('../src/generator/utils/logger');
|
||||
|
||||
const log = createLogger('build');
|
||||
|
||||
function runNode(scriptPath) {
|
||||
const result = spawnSync(process.execPath, [scriptPath], { stdio: 'inherit' });
|
||||
return result && Number.isFinite(result.status) ? result.status : 1;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const elapsedMs = startTimer();
|
||||
log.info('开始', { version: process.env.npm_package_version });
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
|
||||
const cleanExit = runNode(path.join(repoRoot, 'scripts', 'clean.js'));
|
||||
if (cleanExit !== 0) {
|
||||
log.error('clean 失败', { exit: cleanExit });
|
||||
process.exitCode = cleanExit;
|
||||
return;
|
||||
}
|
||||
|
||||
const generatorExit = runNode(path.join(repoRoot, 'src', 'generator.js'));
|
||||
if (generatorExit !== 0) {
|
||||
log.error('generate 失败', { exit: generatorExit });
|
||||
process.exitCode = generatorExit;
|
||||
return;
|
||||
}
|
||||
|
||||
log.ok('完成', { ms: elapsedMs(), dist: 'dist/' });
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
49
scripts/check.js
Normal file
49
scripts/check.js
Normal file
@@ -0,0 +1,49 @@
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const { createLogger, isVerbose, startTimer } = require('../src/generator/utils/logger');
|
||||
|
||||
const log = createLogger('check');
|
||||
|
||||
function runNode(scriptPath) {
|
||||
const result = spawnSync(process.execPath, [scriptPath], { stdio: 'inherit' });
|
||||
return result && Number.isFinite(result.status) ? result.status : 1;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const elapsedMs = startTimer();
|
||||
log.info('开始', { version: process.env.npm_package_version });
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
|
||||
const lintExit = runNode(path.join(repoRoot, 'scripts', 'lint.js'));
|
||||
if (lintExit !== 0) {
|
||||
log.error('lint 失败', { exit: lintExit });
|
||||
process.exitCode = lintExit;
|
||||
return;
|
||||
}
|
||||
|
||||
const testExit = runNode(path.join(repoRoot, 'scripts', 'test.js'));
|
||||
if (testExit !== 0) {
|
||||
log.error('test 失败', { exit: testExit });
|
||||
process.exitCode = testExit;
|
||||
return;
|
||||
}
|
||||
|
||||
const buildExit = runNode(path.join(repoRoot, 'scripts', 'build.js'));
|
||||
if (buildExit !== 0) {
|
||||
log.error('build 失败', { exit: buildExit });
|
||||
process.exitCode = buildExit;
|
||||
return;
|
||||
}
|
||||
|
||||
log.ok('完成', { ms: elapsedMs() });
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const { createLogger } = require('../src/generator/utils/logger');
|
||||
|
||||
const log = createLogger('clean');
|
||||
|
||||
const distPath = path.resolve(__dirname, '..', 'dist');
|
||||
|
||||
try {
|
||||
fs.rmSync(distPath, { recursive: true, force: true });
|
||||
console.log(`Removed ${distPath}`);
|
||||
log.ok('删除 dist 目录', { path: distPath });
|
||||
} catch (error) {
|
||||
console.error(`Failed to remove ${distPath}`);
|
||||
console.error(error);
|
||||
log.error('删除 dist 目录失败', {
|
||||
path: distPath,
|
||||
message: error && error.message ? error.message : String(error),
|
||||
});
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
55
scripts/dev-offline.js
Normal file
55
scripts/dev-offline.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const { createLogger, isVerbose, startTimer } = require('../src/generator/utils/logger');
|
||||
const { startServer } = require('./serve-dist');
|
||||
|
||||
const log = createLogger('dev:offline');
|
||||
let serverRef = null;
|
||||
|
||||
function runNode(scriptPath) {
|
||||
const result = spawnSync(process.execPath, [scriptPath], { stdio: 'inherit' });
|
||||
return result && Number.isFinite(result.status) ? result.status : 1;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const elapsedMs = startTimer();
|
||||
log.info('开始', { version: process.env.npm_package_version });
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const generatorPath = path.join(repoRoot, 'src', 'generator.js');
|
||||
|
||||
const exitCode = runNode(generatorPath);
|
||||
if (exitCode !== 0) {
|
||||
log.error('生成失败', { exit: exitCode });
|
||||
process.exitCode = exitCode;
|
||||
return;
|
||||
}
|
||||
|
||||
const portRaw = process.env.PORT || process.env.MENAV_PORT || '5173';
|
||||
const port = Number.parseInt(portRaw, 10) || 5173;
|
||||
const { server, port: actualPort } = await startServer({
|
||||
rootDir: path.join(repoRoot, 'dist'),
|
||||
host: process.env.HOST || '0.0.0.0',
|
||||
port,
|
||||
});
|
||||
serverRef = server;
|
||||
|
||||
log.ok('就绪', { ms: elapsedMs(), url: `http://localhost:${actualPort}` });
|
||||
|
||||
const shutdown = () => {
|
||||
log.info('正在关闭...');
|
||||
if (!serverRef) process.exit(0);
|
||||
serverRef.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;
|
||||
});
|
||||
}
|
||||
63
scripts/dev.js
Normal file
63
scripts/dev.js
Normal file
@@ -0,0 +1,63 @@
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const { createLogger, isVerbose, startTimer } = require('../src/generator/utils/logger');
|
||||
const { startServer } = require('./serve-dist');
|
||||
|
||||
const log = createLogger('dev');
|
||||
let serverRef = null;
|
||||
|
||||
function runNode(scriptPath) {
|
||||
const result = spawnSync(process.execPath, [scriptPath], { stdio: 'inherit' });
|
||||
return result && Number.isFinite(result.status) ? result.status : 1;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const elapsedMs = startTimer();
|
||||
log.info('开始', { version: process.env.npm_package_version });
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
|
||||
// best-effort:同步失败不阻断 dev
|
||||
const syncProjectsExit = runNode(path.join(repoRoot, 'scripts', 'sync-projects.js'));
|
||||
if (syncProjectsExit !== 0)
|
||||
log.warn('sync-projects 异常退出,已继续(best-effort)', { exit: syncProjectsExit });
|
||||
|
||||
const syncArticlesExit = runNode(path.join(repoRoot, 'scripts', 'sync-articles.js'));
|
||||
if (syncArticlesExit !== 0)
|
||||
log.warn('sync-articles 异常退出,已继续(best-effort)', { exit: syncArticlesExit });
|
||||
|
||||
const generatorExit = runNode(path.join(repoRoot, 'src', 'generator.js'));
|
||||
if (generatorExit !== 0) {
|
||||
log.error('生成失败', { exit: generatorExit });
|
||||
process.exitCode = generatorExit;
|
||||
return;
|
||||
}
|
||||
|
||||
const portRaw = process.env.PORT || process.env.MENAV_PORT || '5173';
|
||||
const port = Number.parseInt(portRaw, 10) || 5173;
|
||||
const { server, port: actualPort } = await startServer({
|
||||
rootDir: path.join(repoRoot, 'dist'),
|
||||
host: process.env.HOST || '0.0.0.0',
|
||||
port,
|
||||
});
|
||||
serverRef = server;
|
||||
|
||||
log.ok('就绪', { ms: elapsedMs(), url: `http://localhost:${actualPort}` });
|
||||
|
||||
const shutdown = () => {
|
||||
log.info('正在关闭...');
|
||||
if (!serverRef) process.exit(0);
|
||||
serverRef.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;
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,10 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
|
||||
const { createLogger, isVerbose, startTimer } = require('../src/generator/utils/logger');
|
||||
|
||||
const log = createLogger('format:check:changed');
|
||||
|
||||
function runGit(args, cwd, options = {}) {
|
||||
const { allowFailure = false, stdio } = options;
|
||||
try {
|
||||
@@ -103,17 +107,13 @@ function collectChangedFiles(repoRoot, range) {
|
||||
const baseExists = gitObjectExists(repoRoot, range.base);
|
||||
const headExists = gitObjectExists(repoRoot, range.head);
|
||||
if (!baseExists || !headExists) {
|
||||
console.warn(
|
||||
'格式检查:检测到 diff range 所需提交缺失,尝试补全 git 历史(避免浅克隆导致失败)'
|
||||
);
|
||||
log.warn('检测到 diff range 所需提交缺失,尝试补全 git 历史(避免浅克隆导致失败)');
|
||||
tryFetchMoreHistory(repoRoot);
|
||||
}
|
||||
|
||||
const output = runGit(diffArgs, repoRoot, { allowFailure: true });
|
||||
if (!output) {
|
||||
console.warn(
|
||||
'格式检查:无法计算 revision range,回退为 HEAD 变更文件(可能仅覆盖最后一次提交)'
|
||||
);
|
||||
log.warn('无法计算 revision range,回退为 HEAD 变更文件(可能仅覆盖最后一次提交)');
|
||||
return collectHeadChangedFiles(repoRoot);
|
||||
}
|
||||
|
||||
@@ -175,6 +175,9 @@ function resolvePrettierBin(repoRoot) {
|
||||
|
||||
function main() {
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const elapsedMs = startTimer();
|
||||
|
||||
log.info('开始');
|
||||
|
||||
const event = tryReadGithubEvent(process.env.GITHUB_EVENT_PATH);
|
||||
const range = getDiffRangeFromGithubEvent(event);
|
||||
@@ -186,21 +189,33 @@ function main() {
|
||||
const filesToCheck = candidateFiles.filter(shouldCheckFile);
|
||||
|
||||
if (filesToCheck.length === 0) {
|
||||
console.log('格式检查:未发现需要检查的文件,跳过。');
|
||||
log.ok('未发现需要检查的文件,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
const prettierBin = resolvePrettierBin(repoRoot);
|
||||
if (!prettierBin) {
|
||||
console.error('格式检查失败:未找到 prettier,可先运行 npm ci / npm install。');
|
||||
log.error('未找到 prettier,可先运行 npm ci / npm install');
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`格式检查:共 ${filesToCheck.length} 个文件`);
|
||||
filesToCheck.forEach((filePath) => console.log(`- ${filePath}`));
|
||||
log.info('准备检查文件格式', { files: filesToCheck.length });
|
||||
if (isVerbose()) {
|
||||
filesToCheck.forEach((filePath) => log.info('待检查', { file: filePath }));
|
||||
}
|
||||
|
||||
execFileSync(prettierBin, ['--check', ...filesToCheck], { cwd: repoRoot, stdio: 'inherit' });
|
||||
try {
|
||||
execFileSync(prettierBin, ['--check', ...filesToCheck], { cwd: repoRoot, stdio: 'inherit' });
|
||||
log.ok('通过', { ms: elapsedMs(), files: filesToCheck.length });
|
||||
} catch (error) {
|
||||
log.error('未通过', {
|
||||
ms: elapsedMs(),
|
||||
files: filesToCheck.length,
|
||||
exit: error && error.status ? error.status : 1,
|
||||
});
|
||||
process.exitCode = error && error.status ? error.status : 1;
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
67
scripts/format.js
Normal file
67
scripts/format.js
Normal file
@@ -0,0 +1,67 @@
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const { createLogger, isVerbose, startTimer } = require('../src/generator/utils/logger');
|
||||
|
||||
const lifecycleEvent = process.env.npm_lifecycle_event
|
||||
? String(process.env.npm_lifecycle_event)
|
||||
: '';
|
||||
const scope =
|
||||
lifecycleEvent === 'format' || lifecycleEvent.startsWith('format:') ? lifecycleEvent : 'format';
|
||||
const log = createLogger(scope);
|
||||
|
||||
const PATTERNS = [
|
||||
'src/**/*.js',
|
||||
'scripts/**/*.js',
|
||||
'test/**/*.js',
|
||||
'.github/**/*.yml',
|
||||
'*.{md,json}',
|
||||
'config/**/*.md',
|
||||
'config/**/*.yml',
|
||||
];
|
||||
|
||||
function parseMode(argv) {
|
||||
if (argv.includes('--check')) return 'check';
|
||||
if (argv.includes('--write')) return 'write';
|
||||
return 'check';
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const elapsedMs = startTimer();
|
||||
const mode = parseMode(process.argv.slice(2));
|
||||
log.info('开始', { mode, version: process.env.npm_package_version });
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const prettierCli = path.join(repoRoot, 'node_modules', 'prettier', 'bin', 'prettier.cjs');
|
||||
|
||||
const args = [];
|
||||
if (mode === 'write') args.push('--write');
|
||||
else args.push('--check');
|
||||
|
||||
// Prettier 本身会根据 .prettierignore 过滤;这里不额外做 file list,保持输出简洁
|
||||
if (isVerbose()) {
|
||||
log.info('检查范围', { patterns: PATTERNS.join(' ') });
|
||||
}
|
||||
|
||||
const result = spawnSync(process.execPath, [prettierCli, ...args, ...PATTERNS], {
|
||||
cwd: repoRoot,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
const exitCode = result && Number.isFinite(result.status) ? result.status : 1;
|
||||
if (exitCode !== 0) {
|
||||
log.error('失败', { ms: elapsedMs(), exit: exitCode });
|
||||
process.exitCode = exitCode;
|
||||
return;
|
||||
}
|
||||
|
||||
log.ok('完成', { ms: elapsedMs(), mode });
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,10 @@ const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
|
||||
const { createLogger } = require('../src/generator/utils/logger');
|
||||
|
||||
const log = createLogger('lint');
|
||||
|
||||
function collectJsFiles(rootDir) {
|
||||
const files = [];
|
||||
|
||||
@@ -39,7 +43,7 @@ function main() {
|
||||
const jsFiles = targetDirs.flatMap((dir) => collectJsFiles(dir)).sort();
|
||||
|
||||
if (jsFiles.length === 0) {
|
||||
console.log('未发现需要检查的 .js 文件,跳过。');
|
||||
log.ok('未发现需要检查的 .js 文件,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -50,17 +54,15 @@ function main() {
|
||||
execFileSync(process.execPath, ['--check', filePath], { stdio: 'inherit' });
|
||||
} catch (error) {
|
||||
hasError = true;
|
||||
console.error(`\n语法检查失败:${relativePath}`);
|
||||
if (error && error.status) {
|
||||
console.error(`退出码:${error.status}`);
|
||||
}
|
||||
log.error('语法检查失败', { file: relativePath, exit: error && error.status });
|
||||
}
|
||||
});
|
||||
|
||||
if (hasError) {
|
||||
log.error('语法检查未通过', { files: jsFiles.length });
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(`语法检查通过:${jsFiles.length} 个文件`);
|
||||
log.ok('语法检查通过', { files: jsFiles.length });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -6,6 +6,9 @@ const net = require('node:net');
|
||||
const Parser = require('rss-parser');
|
||||
|
||||
const { loadConfig } = require('../src/generator.js');
|
||||
const { createLogger, isVerbose, startTimer } = require('../src/generator/utils/logger');
|
||||
|
||||
const log = createLogger('sync:articles');
|
||||
|
||||
const DEFAULT_RSS_SETTINGS = {
|
||||
enabled: true,
|
||||
@@ -488,7 +491,7 @@ async function processSourceSite(sourceSite, settings, parser, deadlineTs) {
|
||||
throw lastError || new Error('未找到可用 Feed');
|
||||
};
|
||||
|
||||
const startedAt = Date.now();
|
||||
const elapsedMs = startTimer();
|
||||
for (let i = 0; i <= settings.fetch.maxRetries; i += 1) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
@@ -501,7 +504,7 @@ async function processSourceSite(sourceSite, settings, parser, deadlineTs) {
|
||||
status: 'success',
|
||||
error: '',
|
||||
fetchedAt: new Date().toISOString(),
|
||||
durationMs: Date.now() - startedAt,
|
||||
durationMs: elapsedMs(),
|
||||
},
|
||||
articles: res.articles,
|
||||
};
|
||||
@@ -518,7 +521,7 @@ async function processSourceSite(sourceSite, settings, parser, deadlineTs) {
|
||||
status: 'failed',
|
||||
error: lastError ? String(lastError.message || lastError) : '未知错误',
|
||||
fetchedAt: new Date().toISOString(),
|
||||
durationMs: Date.now() - startedAt,
|
||||
durationMs: elapsedMs(),
|
||||
},
|
||||
articles: [],
|
||||
};
|
||||
@@ -582,6 +585,7 @@ async function syncArticlesForPage(pageId, pageConfig, config, settings) {
|
||||
pageConfig && Array.isArray(pageConfig.categories) ? pageConfig.categories : []
|
||||
);
|
||||
|
||||
const elapsedMs = startTimer();
|
||||
const startedAt = Date.now();
|
||||
const deadlineTs = startedAt + settings.fetch.totalTimeoutMs;
|
||||
|
||||
@@ -635,7 +639,7 @@ async function syncArticlesForPage(pageId, pageConfig, config, settings) {
|
||||
failedSites,
|
||||
skippedSites,
|
||||
totalArticles: limitedArticles.length,
|
||||
durationMs: Date.now() - startedAt,
|
||||
durationMs: elapsedMs(),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -670,43 +674,63 @@ function pickArticlesPages(config, onlyPageId) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const elapsedMs = startTimer();
|
||||
const args = process.argv.slice(2);
|
||||
const pageArgIndex = args.findIndex((a) => a === '--page');
|
||||
const onlyPageId = pageArgIndex >= 0 ? args[pageArgIndex + 1] : null;
|
||||
|
||||
log.info('开始', { page: onlyPageId || '' });
|
||||
|
||||
const config = loadConfig();
|
||||
const settings = getRssSettings(config);
|
||||
|
||||
if (!settings.enabled) {
|
||||
console.log('[INFO] RSS 已禁用(RSS_ENABLED=false),跳过。');
|
||||
log.ok('RSS 已禁用,跳过', { env: 'RSS_ENABLED=false' });
|
||||
return;
|
||||
}
|
||||
|
||||
const pages = pickArticlesPages(config, onlyPageId);
|
||||
if (pages.length === 0) {
|
||||
console.log('[INFO] 未找到需要同步的 articles 页面。');
|
||||
log.ok('未找到需要同步的 articles 页面,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`[INFO] 准备同步 ${pages.length} 个 articles 页面缓存…`);
|
||||
log.info('准备同步 articles 页面缓存', { pages: pages.length });
|
||||
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (const { pageId, pageConfig } of pages) {
|
||||
try {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
const { cachePath, cache } = await syncArticlesForPage(pageId, pageConfig, config, settings);
|
||||
console.log(
|
||||
`[INFO] 已生成缓存:${cachePath}(articles=${cache.stats.totalArticles}, sites=${cache.stats.totalSites})`
|
||||
);
|
||||
success += 1;
|
||||
log.ok('已生成缓存', {
|
||||
page: pageId,
|
||||
cache: cachePath,
|
||||
articles: cache && cache.stats ? cache.stats.totalArticles : '',
|
||||
sites: cache && cache.stats ? cache.stats.totalSites : '',
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn(`[WARN] 页面 ${pageId} 同步失败:${e.message || e}`);
|
||||
failed += 1;
|
||||
log.warn('页面同步失败,已跳过(best-effort)', {
|
||||
page: pageId,
|
||||
message: e && e.message ? e.message : String(e),
|
||||
});
|
||||
if (isVerbose() && e && e.stack) console.error(e.stack);
|
||||
// best-effort:不阻断其他页面/后续 build
|
||||
}
|
||||
}
|
||||
|
||||
log.ok('完成', { ms: elapsedMs(), pages: pages.length, success, failed });
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch((err) => {
|
||||
console.error('[ERROR] sync-articles 执行失败:', err);
|
||||
log.error('执行失败(best-effort,不阻断后续 build/deploy)', {
|
||||
message: err && err.message ? err.message : String(err),
|
||||
});
|
||||
if (isVerbose() && err && err.stack) console.error(err.stack);
|
||||
// best-effort:不阻断后续 build/deploy(错误已输出到日志,便于排查)
|
||||
process.exitCode = 0;
|
||||
});
|
||||
|
||||
@@ -3,6 +3,9 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const { loadConfig } = require('../src/generator.js');
|
||||
const { createLogger, isVerbose, startTimer } = require('../src/generator/utils/logger');
|
||||
|
||||
const log = createLogger('sync:projects');
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
enabled: true,
|
||||
@@ -153,9 +156,9 @@ async function loadLanguageColors(settings, cacheBaseDir) {
|
||||
return colors;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[WARN] 获取语言颜色表失败(将不输出 languageColor):${String(error && error.message ? error.message : error)}`
|
||||
);
|
||||
log.warn('获取语言颜色表失败(将不输出 languageColor)', {
|
||||
message: String(error && error.message ? error.message : error),
|
||||
});
|
||||
}
|
||||
|
||||
return {};
|
||||
@@ -209,11 +212,14 @@ async function runPool(items, concurrency, worker) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const elapsedMs = startTimer();
|
||||
const config = loadConfig();
|
||||
const settings = getSettings(config);
|
||||
|
||||
log.info('开始');
|
||||
|
||||
if (!settings.enabled) {
|
||||
console.log('[INFO] projects 仓库同步已禁用(PROJECTS_ENABLED=false)');
|
||||
log.ok('projects 仓库同步已禁用,跳过', { env: 'PROJECTS_ENABLED=false' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -226,10 +232,15 @@ async function main() {
|
||||
const pages = findProjectsPages(config);
|
||||
|
||||
if (!pages.length) {
|
||||
console.log('[INFO] 未找到 template=projects 的页面,跳过同步');
|
||||
log.ok('未找到 template=projects 的页面,跳过同步');
|
||||
return;
|
||||
}
|
||||
|
||||
log.info('准备同步 projects 页面缓存', { pages: pages.length });
|
||||
|
||||
let pageSuccess = 0;
|
||||
let pageFailed = 0;
|
||||
|
||||
for (const { pageId, page } of pages) {
|
||||
const categories = Array.isArray(page.categories) ? page.categories : [];
|
||||
const sites = [];
|
||||
@@ -244,7 +255,7 @@ async function main() {
|
||||
const repoList = Array.from(unique.values());
|
||||
|
||||
if (!repoList.length) {
|
||||
console.log(`[INFO] 页面 ${pageId}:未发现 GitHub 仓库链接,跳过`);
|
||||
log.ok('页面未发现 GitHub 仓库链接,跳过', { page: pageId });
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -258,9 +269,10 @@ async function main() {
|
||||
return meta;
|
||||
} catch (error) {
|
||||
failed += 1;
|
||||
console.warn(
|
||||
`[WARN] 拉取失败:${repo.canonicalUrl}(${String(error && error.message ? error.message : error)})`
|
||||
);
|
||||
log.warn('拉取仓库元信息失败(best-effort)', {
|
||||
repo: repo.canonicalUrl,
|
||||
message: String(error && error.message ? error.message : error),
|
||||
});
|
||||
return null;
|
||||
}
|
||||
});
|
||||
@@ -280,13 +292,24 @@ async function main() {
|
||||
const cachePath = path.join(cacheBaseDir, `${pageId}.repo-cache.json`);
|
||||
fs.writeFileSync(cachePath, JSON.stringify(payload, null, 2), 'utf8');
|
||||
|
||||
console.log(
|
||||
`[INFO] 页面 ${pageId}:同步完成(成功 ${success} / 失败 ${failed}),写入缓存 ${cachePath}`
|
||||
);
|
||||
if (failed === 0) pageSuccess += 1;
|
||||
else pageFailed += 1;
|
||||
|
||||
log.ok('页面同步完成', {
|
||||
page: pageId,
|
||||
success,
|
||||
failed,
|
||||
cache: cachePath,
|
||||
});
|
||||
}
|
||||
|
||||
log.ok('完成', { ms: elapsedMs(), pages: pages.length, pageSuccess, pageFailed });
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('[ERROR] projects 同步异常:', error);
|
||||
log.error('执行异常(best-effort,不阻断后续 build)', {
|
||||
message: error && error.message ? error.message : String(error),
|
||||
});
|
||||
if (isVerbose() && error && error.stack) console.error(error.stack);
|
||||
process.exitCode = 0; // best-effort:不阻断后续 build
|
||||
});
|
||||
|
||||
52
scripts/test.js
Normal file
52
scripts/test.js
Normal file
@@ -0,0 +1,52 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const { createLogger, isVerbose, startTimer } = require('../src/generator/utils/logger');
|
||||
|
||||
const log = createLogger('test');
|
||||
|
||||
function collectTestFiles(repoRoot) {
|
||||
const testDir = path.join(repoRoot, 'test');
|
||||
if (!fs.existsSync(testDir)) return [];
|
||||
|
||||
return fs
|
||||
.readdirSync(testDir)
|
||||
.filter((name) => name.endsWith('.js'))
|
||||
.map((name) => path.join('test', name))
|
||||
.sort();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const elapsedMs = startTimer();
|
||||
log.info('开始', { version: process.env.npm_package_version });
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '..');
|
||||
const files = collectTestFiles(repoRoot);
|
||||
if (files.length === 0) {
|
||||
log.ok('未发现测试文件,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = spawnSync(process.execPath, ['--test', ...files], {
|
||||
cwd: repoRoot,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
const exitCode = result && Number.isFinite(result.status) ? result.status : 1;
|
||||
if (exitCode !== 0) {
|
||||
log.error('失败', { ms: elapsedMs(), exit: exitCode });
|
||||
process.exitCode = exitCode;
|
||||
return;
|
||||
}
|
||||
|
||||
log.ok('完成', { ms: elapsedMs(), files: files.length });
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user