chore: 引入统一日志模块,统一 cli 输出

This commit is contained in:
rbetree
2026-01-16 17:29:05 +08:00
parent f2f59108a0
commit 87d1f0244c
26 changed files with 903 additions and 150 deletions

View File

@@ -6,19 +6,19 @@
"homepage": "https://rbetree.github.io/menav", "homepage": "https://rbetree.github.io/menav",
"scripts": { "scripts": {
"generate": "node src/generator.js", "generate": "node src/generator.js",
"dev": "npm run sync-projects && npm run sync-articles && node src/generator.js && serve dist -l 5173", "dev": "node ./scripts/dev.js",
"dev:offline": "node src/generator.js && serve dist -l 5173", "dev:offline": "node ./scripts/dev-offline.js",
"clean": "node ./scripts/clean.js", "clean": "node ./scripts/clean.js",
"build": "npm run clean && npm run generate", "build": "node ./scripts/build.js",
"sync-articles": "node ./scripts/sync-articles.js", "sync-articles": "node ./scripts/sync-articles.js",
"sync-projects": "node ./scripts/sync-projects.js", "sync-projects": "node ./scripts/sync-projects.js",
"import-bookmarks": "node src/bookmark-processor.js", "import-bookmarks": "node src/bookmark-processor.js",
"test": "node --test test/*.js", "test": "node ./scripts/test.js",
"lint": "node ./scripts/lint.js", "lint": "node ./scripts/lint.js",
"format": "prettier --write \"src/**/*.js\" \"scripts/**/*.js\" \"test/**/*.js\" \".github/**/*.yml\" \"*.{md,json}\" \"config/**/*.md\" \"config/**/*.yml\"", "format": "node ./scripts/format.js --write",
"format:check": "prettier --check \"src/**/*.js\" \"scripts/**/*.js\" \"test/**/*.js\" \".github/**/*.yml\" \"*.{md,json}\" \"config/**/*.md\" \"config/**/*.yml\"", "format:check": "node ./scripts/format.js --check",
"format:check:changed": "node ./scripts/format-check-changed.js", "format:check:changed": "node ./scripts/format-check-changed.js",
"check": "npm run lint && npm test && npm run build", "check": "node ./scripts/check.js",
"prepare": "husky" "prepare": "husky"
}, },
"keywords": [ "keywords": [

View File

@@ -1,6 +1,10 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('node:fs'); const fs = require('node:fs');
const { createLogger, isVerbose, startTimer } = require('../src/generator/utils/logger');
const log = createLogger('bundle');
function ensureDir(dirPath) { function ensureDir(dirPath) {
if (!fs.existsSync(dirPath)) { if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true }); fs.mkdirSync(dirPath, { recursive: true });
@@ -12,7 +16,7 @@ async function main() {
try { try {
esbuild = require('esbuild'); esbuild = require('esbuild');
} catch (error) { } catch (error) {
console.error('未找到 esbuild请先执行 npm install。'); log.error('未找到 esbuild请先执行 npm install。');
process.exitCode = 1; process.exitCode = 1;
return; return;
} }
@@ -22,7 +26,7 @@ async function main() {
const outFile = path.join(projectRoot, 'dist', 'script.js'); const outFile = path.join(projectRoot, 'dist', 'script.js');
if (!fs.existsSync(entry)) { if (!fs.existsSync(entry)) {
console.error(`运行时入口不存在${path.relative(projectRoot, entry)}`); log.error('运行时入口不存在', { path: path.relative(projectRoot, entry) });
process.exitCode = 1; process.exitCode = 1;
return; return;
} }
@@ -30,6 +34,7 @@ async function main() {
ensureDir(path.dirname(outFile)); ensureDir(path.dirname(outFile));
try { try {
const elapsedMs = startTimer();
const result = await esbuild.build({ const result = await esbuild.build({
entryPoints: [entry], entryPoints: [entry],
outfile: outFile, outfile: outFile,
@@ -41,23 +46,25 @@ async function main() {
minify: true, minify: true,
legalComments: 'none', legalComments: 'none',
metafile: true, metafile: true,
logLevel: 'info', logLevel: 'silent',
}); });
const ms = elapsedMs();
const outputs = const outputs =
result && result.metafile && result.metafile.outputs ? result.metafile.outputs : null; result && result.metafile && result.metafile.outputs ? result.metafile.outputs : null;
const outKey = outputs ? Object.keys(outputs).find((k) => k.endsWith('dist/script.js')) : ''; const outKey = outputs ? Object.keys(outputs).find((k) => k.endsWith('dist/script.js')) : '';
const bytes = outKey && outputs && outputs[outKey] ? outputs[outKey].bytes : 0; const bytes = outKey && outputs && outputs[outKey] ? outputs[outKey].bytes : 0;
if (bytes) {
console.log(`✅ runtime bundle 完成dist/script.js (${bytes} bytes)`); const meta = { ms };
} else { if (bytes) meta.bytes = bytes;
console.log('✅ runtime bundle 完成:dist/script.js'); log.ok('输出 dist/script.js', meta);
}
} catch (error) { } catch (error) {
console.error( log.error('构建 dist/script.js 失败', {
'❌ runtime bundle 失败(禁止回退旧产物):', message: error && error.message ? error.message : String(error),
error && error.message ? error.message : error });
); if (isVerbose() && error && error.stack) {
console.error(error.stack);
}
process.exitCode = 1; process.exitCode = 1;
} }
} }

42
scripts/build.js Normal file
View 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
View 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;
});
}

View File

@@ -1,13 +1,19 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { createLogger } = require('../src/generator/utils/logger');
const log = createLogger('clean');
const distPath = path.resolve(__dirname, '..', 'dist'); const distPath = path.resolve(__dirname, '..', 'dist');
try { try {
fs.rmSync(distPath, { recursive: true, force: true }); fs.rmSync(distPath, { recursive: true, force: true });
console.log(`Removed ${distPath}`); log.ok('删除 dist 目录', { path: distPath });
} catch (error) { } catch (error) {
console.error(`Failed to remove ${distPath}`); log.error('删除 dist 目录失败', {
console.error(error); path: distPath,
message: error && error.message ? error.message : String(error),
});
process.exitCode = 1; process.exitCode = 1;
} }

55
scripts/dev-offline.js Normal file
View 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
View 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;
});
}

View File

@@ -2,6 +2,10 @@ const fs = require('node:fs');
const path = require('node:path'); const path = require('node:path');
const { execFileSync } = require('node:child_process'); 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 = {}) { function runGit(args, cwd, options = {}) {
const { allowFailure = false, stdio } = options; const { allowFailure = false, stdio } = options;
try { try {
@@ -103,17 +107,13 @@ function collectChangedFiles(repoRoot, range) {
const baseExists = gitObjectExists(repoRoot, range.base); const baseExists = gitObjectExists(repoRoot, range.base);
const headExists = gitObjectExists(repoRoot, range.head); const headExists = gitObjectExists(repoRoot, range.head);
if (!baseExists || !headExists) { if (!baseExists || !headExists) {
console.warn( log.warn('检测到 diff range 所需提交缺失,尝试补全 git 历史(避免浅克隆导致失败)');
'格式检查:检测到 diff range 所需提交缺失,尝试补全 git 历史(避免浅克隆导致失败)'
);
tryFetchMoreHistory(repoRoot); tryFetchMoreHistory(repoRoot);
} }
const output = runGit(diffArgs, repoRoot, { allowFailure: true }); const output = runGit(diffArgs, repoRoot, { allowFailure: true });
if (!output) { if (!output) {
console.warn( log.warn('无法计算 revision range回退为 HEAD 变更文件(可能仅覆盖最后一次提交)');
'格式检查:无法计算 revision range回退为 HEAD 变更文件(可能仅覆盖最后一次提交)'
);
return collectHeadChangedFiles(repoRoot); return collectHeadChangedFiles(repoRoot);
} }
@@ -175,6 +175,9 @@ function resolvePrettierBin(repoRoot) {
function main() { function main() {
const repoRoot = path.resolve(__dirname, '..'); const repoRoot = path.resolve(__dirname, '..');
const elapsedMs = startTimer();
log.info('开始');
const event = tryReadGithubEvent(process.env.GITHUB_EVENT_PATH); const event = tryReadGithubEvent(process.env.GITHUB_EVENT_PATH);
const range = getDiffRangeFromGithubEvent(event); const range = getDiffRangeFromGithubEvent(event);
@@ -186,21 +189,33 @@ function main() {
const filesToCheck = candidateFiles.filter(shouldCheckFile); const filesToCheck = candidateFiles.filter(shouldCheckFile);
if (filesToCheck.length === 0) { if (filesToCheck.length === 0) {
console.log('格式检查:未发现需要检查的文件,跳过'); log.ok('未发现需要检查的文件,跳过');
return; return;
} }
const prettierBin = resolvePrettierBin(repoRoot); const prettierBin = resolvePrettierBin(repoRoot);
if (!prettierBin) { if (!prettierBin) {
console.error('格式检查失败:未找到 prettier可先运行 npm ci / npm install'); log.error('未找到 prettier可先运行 npm ci / npm install');
process.exitCode = 1; process.exitCode = 1;
return; return;
} }
console.log(`格式检查:共 ${filesToCheck.length} 个文件`); log.info('准备检查文件格式', { files: filesToCheck.length });
filesToCheck.forEach((filePath) => console.log(`- ${filePath}`)); 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(); main();

67
scripts/format.js Normal file
View 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;
});
}

View File

@@ -2,6 +2,10 @@ const fs = require('node:fs');
const path = require('node:path'); const path = require('node:path');
const { execFileSync } = require('node:child_process'); const { execFileSync } = require('node:child_process');
const { createLogger } = require('../src/generator/utils/logger');
const log = createLogger('lint');
function collectJsFiles(rootDir) { function collectJsFiles(rootDir) {
const files = []; const files = [];
@@ -39,7 +43,7 @@ function main() {
const jsFiles = targetDirs.flatMap((dir) => collectJsFiles(dir)).sort(); const jsFiles = targetDirs.flatMap((dir) => collectJsFiles(dir)).sort();
if (jsFiles.length === 0) { if (jsFiles.length === 0) {
console.log('未发现需要检查的 .js 文件,跳过'); log.ok('未发现需要检查的 .js 文件,跳过');
return; return;
} }
@@ -50,17 +54,15 @@ function main() {
execFileSync(process.execPath, ['--check', filePath], { stdio: 'inherit' }); execFileSync(process.execPath, ['--check', filePath], { stdio: 'inherit' });
} catch (error) { } catch (error) {
hasError = true; hasError = true;
console.error(`\n语法检查失败:${relativePath}`); log.error('语法检查失败', { file: relativePath, exit: error && error.status });
if (error && error.status) {
console.error(`退出码:${error.status}`);
}
} }
}); });
if (hasError) { if (hasError) {
log.error('语法检查未通过', { files: jsFiles.length });
process.exitCode = 1; process.exitCode = 1;
} else { } else {
console.log(`语法检查通过${jsFiles.length} 个文件`); log.ok('语法检查通过', { files: jsFiles.length });
} }
} }

192
scripts/serve-dist.js Normal file
View 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,
};

View File

@@ -6,6 +6,9 @@ const net = require('node:net');
const Parser = require('rss-parser'); const Parser = require('rss-parser');
const { loadConfig } = require('../src/generator.js'); const { loadConfig } = require('../src/generator.js');
const { createLogger, isVerbose, startTimer } = require('../src/generator/utils/logger');
const log = createLogger('sync:articles');
const DEFAULT_RSS_SETTINGS = { const DEFAULT_RSS_SETTINGS = {
enabled: true, enabled: true,
@@ -488,7 +491,7 @@ async function processSourceSite(sourceSite, settings, parser, deadlineTs) {
throw lastError || new Error('未找到可用 Feed'); throw lastError || new Error('未找到可用 Feed');
}; };
const startedAt = Date.now(); const elapsedMs = startTimer();
for (let i = 0; i <= settings.fetch.maxRetries; i += 1) { for (let i = 0; i <= settings.fetch.maxRetries; i += 1) {
try { try {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
@@ -501,7 +504,7 @@ async function processSourceSite(sourceSite, settings, parser, deadlineTs) {
status: 'success', status: 'success',
error: '', error: '',
fetchedAt: new Date().toISOString(), fetchedAt: new Date().toISOString(),
durationMs: Date.now() - startedAt, durationMs: elapsedMs(),
}, },
articles: res.articles, articles: res.articles,
}; };
@@ -518,7 +521,7 @@ async function processSourceSite(sourceSite, settings, parser, deadlineTs) {
status: 'failed', status: 'failed',
error: lastError ? String(lastError.message || lastError) : '未知错误', error: lastError ? String(lastError.message || lastError) : '未知错误',
fetchedAt: new Date().toISOString(), fetchedAt: new Date().toISOString(),
durationMs: Date.now() - startedAt, durationMs: elapsedMs(),
}, },
articles: [], articles: [],
}; };
@@ -582,6 +585,7 @@ async function syncArticlesForPage(pageId, pageConfig, config, settings) {
pageConfig && Array.isArray(pageConfig.categories) ? pageConfig.categories : [] pageConfig && Array.isArray(pageConfig.categories) ? pageConfig.categories : []
); );
const elapsedMs = startTimer();
const startedAt = Date.now(); const startedAt = Date.now();
const deadlineTs = startedAt + settings.fetch.totalTimeoutMs; const deadlineTs = startedAt + settings.fetch.totalTimeoutMs;
@@ -635,7 +639,7 @@ async function syncArticlesForPage(pageId, pageConfig, config, settings) {
failedSites, failedSites,
skippedSites, skippedSites,
totalArticles: limitedArticles.length, totalArticles: limitedArticles.length,
durationMs: Date.now() - startedAt, durationMs: elapsedMs(),
}, },
}; };
@@ -670,43 +674,63 @@ function pickArticlesPages(config, onlyPageId) {
} }
async function main() { async function main() {
const elapsedMs = startTimer();
const args = process.argv.slice(2); const args = process.argv.slice(2);
const pageArgIndex = args.findIndex((a) => a === '--page'); const pageArgIndex = args.findIndex((a) => a === '--page');
const onlyPageId = pageArgIndex >= 0 ? args[pageArgIndex + 1] : null; const onlyPageId = pageArgIndex >= 0 ? args[pageArgIndex + 1] : null;
log.info('开始', { page: onlyPageId || '' });
const config = loadConfig(); const config = loadConfig();
const settings = getRssSettings(config); const settings = getRssSettings(config);
if (!settings.enabled) { if (!settings.enabled) {
console.log('[INFO] RSS 已禁用(RSS_ENABLED=false),跳过。'); log.ok('RSS 已禁用,跳过', { env: 'RSS_ENABLED=false' });
return; return;
} }
const pages = pickArticlesPages(config, onlyPageId); const pages = pickArticlesPages(config, onlyPageId);
if (pages.length === 0) { if (pages.length === 0) {
console.log('[INFO] 未找到需要同步的 articles 页面'); log.ok('未找到需要同步的 articles 页面,跳过');
return; 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) { for (const { pageId, pageConfig } of pages) {
try { try {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const { cachePath, cache } = await syncArticlesForPage(pageId, pageConfig, config, settings); const { cachePath, cache } = await syncArticlesForPage(pageId, pageConfig, config, settings);
console.log( success += 1;
`[INFO] 已生成缓存:${cachePath}articles=${cache.stats.totalArticles}, sites=${cache.stats.totalSites}` log.ok('已生成缓存', {
); page: pageId,
cache: cachePath,
articles: cache && cache.stats ? cache.stats.totalArticles : '',
sites: cache && cache.stats ? cache.stats.totalSites : '',
});
} catch (e) { } 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 // best-effort不阻断其他页面/后续 build
} }
} }
log.ok('完成', { ms: elapsedMs(), pages: pages.length, success, failed });
} }
if (require.main === module) { if (require.main === module) {
main().catch((err) => { 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错误已输出到日志便于排查 // best-effort不阻断后续 build/deploy错误已输出到日志便于排查
process.exitCode = 0; process.exitCode = 0;
}); });

View File

@@ -3,6 +3,9 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const { loadConfig } = require('../src/generator.js'); const { loadConfig } = require('../src/generator.js');
const { createLogger, isVerbose, startTimer } = require('../src/generator/utils/logger');
const log = createLogger('sync:projects');
const DEFAULT_SETTINGS = { const DEFAULT_SETTINGS = {
enabled: true, enabled: true,
@@ -153,9 +156,9 @@ async function loadLanguageColors(settings, cacheBaseDir) {
return colors; return colors;
} }
} catch (error) { } catch (error) {
console.warn( log.warn('获取语言颜色表失败(将不输出 languageColor', {
`[WARN] 获取语言颜色表失败(将不输出 languageColor${String(error && error.message ? error.message : error)}` message: String(error && error.message ? error.message : error),
); });
} }
return {}; return {};
@@ -209,11 +212,14 @@ async function runPool(items, concurrency, worker) {
} }
async function main() { async function main() {
const elapsedMs = startTimer();
const config = loadConfig(); const config = loadConfig();
const settings = getSettings(config); const settings = getSettings(config);
log.info('开始');
if (!settings.enabled) { if (!settings.enabled) {
console.log('[INFO] projects 仓库同步已禁用PROJECTS_ENABLED=false'); log.ok('projects 仓库同步已禁用,跳过', { env: 'PROJECTS_ENABLED=false' });
return; return;
} }
@@ -226,10 +232,15 @@ async function main() {
const pages = findProjectsPages(config); const pages = findProjectsPages(config);
if (!pages.length) { if (!pages.length) {
console.log('[INFO] 未找到 template=projects 的页面,跳过同步'); log.ok('未找到 template=projects 的页面,跳过同步');
return; return;
} }
log.info('准备同步 projects 页面缓存', { pages: pages.length });
let pageSuccess = 0;
let pageFailed = 0;
for (const { pageId, page } of pages) { for (const { pageId, page } of pages) {
const categories = Array.isArray(page.categories) ? page.categories : []; const categories = Array.isArray(page.categories) ? page.categories : [];
const sites = []; const sites = [];
@@ -244,7 +255,7 @@ async function main() {
const repoList = Array.from(unique.values()); const repoList = Array.from(unique.values());
if (!repoList.length) { if (!repoList.length) {
console.log(`[INFO] 页面 ${pageId}:未发现 GitHub 仓库链接,跳过`); log.ok('页面未发现 GitHub 仓库链接,跳过', { page: pageId });
continue; continue;
} }
@@ -258,9 +269,10 @@ async function main() {
return meta; return meta;
} catch (error) { } catch (error) {
failed += 1; failed += 1;
console.warn( log.warn('拉取仓库元信息失败best-effort', {
`[WARN] 拉取失败:${repo.canonicalUrl}${String(error && error.message ? error.message : error)}` repo: repo.canonicalUrl,
); message: String(error && error.message ? error.message : error),
});
return null; return null;
} }
}); });
@@ -280,13 +292,24 @@ async function main() {
const cachePath = path.join(cacheBaseDir, `${pageId}.repo-cache.json`); const cachePath = path.join(cacheBaseDir, `${pageId}.repo-cache.json`);
fs.writeFileSync(cachePath, JSON.stringify(payload, null, 2), 'utf8'); fs.writeFileSync(cachePath, JSON.stringify(payload, null, 2), 'utf8');
console.log( if (failed === 0) pageSuccess += 1;
`[INFO] 页面 ${pageId}:同步完成(成功 ${success} / 失败 ${failed}),写入缓存 ${cachePath}` else pageFailed += 1;
);
log.ok('页面同步完成', {
page: pageId,
success,
failed,
cache: cachePath,
});
} }
log.ok('完成', { ms: elapsedMs(), pages: pages.length, pageSuccess, pageFailed });
} }
main().catch((error) => { 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 process.exitCode = 0; // best-effort不阻断后续 build
}); });

52
scripts/test.js Normal file
View 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;
});
}

View File

@@ -2,6 +2,9 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
const { FileError, wrapAsyncError } = require('./generator/utils/errors'); const { FileError, wrapAsyncError } = require('./generator/utils/errors');
const { createLogger, isVerbose, startTimer } = require('./generator/utils/logger');
const log = createLogger('import-bookmarks');
// 书签文件夹路径 - 使用相对路径 // 书签文件夹路径 - 使用相对路径
const BOOKMARKS_DIR = 'bookmarks'; const BOOKMARKS_DIR = 'bookmarks';
@@ -26,16 +29,12 @@ function ensureUserConfigInitialized() {
if (fs.existsSync(CONFIG_DEFAULT_DIR)) { if (fs.existsSync(CONFIG_DEFAULT_DIR)) {
fs.cpSync(CONFIG_DEFAULT_DIR, CONFIG_USER_DIR, { recursive: true }); fs.cpSync(CONFIG_DEFAULT_DIR, CONFIG_USER_DIR, { recursive: true });
console.log( log.info('config/user 不存在,已从 config/_default 初始化用户配置(完全替换策略)');
'[INFO] 检测到 config/user 不存在,已从 config/_default 初始化用户配置(完全替换策略需要完整配置)。'
);
return { initialized: true, source: '_default' }; return { initialized: true, source: '_default' };
} }
fs.mkdirSync(CONFIG_USER_DIR, { recursive: true }); fs.mkdirSync(CONFIG_USER_DIR, { recursive: true });
console.log( log.warn('未找到 config/_default已创建空的 config/user建议补齐 site.yml 与 pages/*.yml');
'[WARN] 未找到默认配置目录 config/_default已创建空的 config/user 目录;建议补齐 site.yml 与 pages/*.yml。'
);
return { initialized: true, source: 'empty' }; return { initialized: true, source: 'empty' };
} }
@@ -49,12 +48,12 @@ function ensureUserSiteYmlExists() {
fs.mkdirSync(CONFIG_USER_DIR, { recursive: true }); fs.mkdirSync(CONFIG_USER_DIR, { recursive: true });
} }
fs.copyFileSync(DEFAULT_SITE_YML, USER_SITE_YML); fs.copyFileSync(DEFAULT_SITE_YML, USER_SITE_YML);
console.log('[INFO] 未找到 config/user/site.yml已从 config/_default/site.yml 复制一份。'); log.info('未找到 config/user/site.yml已从 config/_default/site.yml 复制');
return true; return true;
} }
console.log( log.warn(
'[WARN] 未找到可用的 site.yml无法自动更新导航手动在 config/user/site.yml 添加 navigation含 id: bookmarks' '未找到可用的 site.yml无法自动更新导航请在 config/user/site.yml 添加 navigation含 id: bookmarks'
); );
return false; return false;
} }
@@ -185,7 +184,7 @@ function getLatestBookmarkFile() {
// 确保书签目录存在 // 确保书签目录存在
if (!fs.existsSync(BOOKMARKS_DIR)) { if (!fs.existsSync(BOOKMARKS_DIR)) {
fs.mkdirSync(BOOKMARKS_DIR, { recursive: true }); fs.mkdirSync(BOOKMARKS_DIR, { recursive: true });
console.log('[WARN] 书签目录为空,未找到HTML文件'); log.warn('bookmarks 目录不存在,已创建;未找到 HTML 书签文件', { dir: BOOKMARKS_DIR });
return null; return null;
} }
@@ -195,7 +194,7 @@ function getLatestBookmarkFile() {
.filter((file) => file.toLowerCase().endsWith('.html')); .filter((file) => file.toLowerCase().endsWith('.html'));
if (files.length === 0) { if (files.length === 0) {
console.log('[WARN] 未找到任何HTML书签文件'); log.warn('未找到任何 HTML 书签文件', { dir: BOOKMARKS_DIR });
return null; return null;
} }
@@ -210,11 +209,12 @@ function getLatestBookmarkFile() {
const latestFile = fileStats[0].file; const latestFile = fileStats[0].file;
const latestFilePath = path.join(BOOKMARKS_DIR, latestFile); const latestFilePath = path.join(BOOKMARKS_DIR, latestFile);
console.log('[INFO] 选择最新的书签文件:', latestFile); log.info('选择最新的书签文件', { file: latestFile });
return latestFilePath; return latestFilePath;
} catch (error) { } catch (error) {
console.error('[ERROR] 查找书签文件时出错:', error); log.error('查找书签文件时出错', { message: error && error.message ? error.message : error });
if (isVerbose() && error && error.stack) console.error(error.stack);
return null; return null;
} }
} }
@@ -612,11 +612,11 @@ function parseBookmarks(htmlContent) {
/<DT><H3[^>]*PERSONAL_TOOLBAR_FOLDER[^>]*>([^<]+)<\/H3>/i /<DT><H3[^>]*PERSONAL_TOOLBAR_FOLDER[^>]*>([^<]+)<\/H3>/i
); );
if (!bookmarkBarMatch) { if (!bookmarkBarMatch) {
console.log('[WARN] 未找到书签栏文件夹PERSONAL_TOOLBAR_FOLDER使用备用方案'); log.warn('未找到书签栏文件夹PERSONAL_TOOLBAR_FOLDER使用备用方案');
// 备用方案:使用第一个 <DL><p> 标签 // 备用方案:使用第一个 <DL><p> 标签
const firstDLMatch = htmlContent.match(/<DL><p>/i); const firstDLMatch = htmlContent.match(/<DL><p>/i);
if (!firstDLMatch) { if (!firstDLMatch) {
console.log('[ERROR] 未找到任何书签容器'); log.error('未找到任何书签容器');
bookmarks.categories = []; bookmarks.categories = [];
} else { } else {
const dlStart = firstDLMatch.index + firstDLMatch[0].length; const dlStart = firstDLMatch.index + firstDLMatch[0].length;
@@ -651,7 +651,7 @@ function parseBookmarks(htmlContent) {
const remainingAfterBar = htmlContent.substring(bookmarkBarStart); const remainingAfterBar = htmlContent.substring(bookmarkBarStart);
const dlMatch = remainingAfterBar.match(/<DL><p>/i); const dlMatch = remainingAfterBar.match(/<DL><p>/i);
if (!dlMatch) { if (!dlMatch) {
console.log('[ERROR] 未找到书签栏的内容容器 <DL><p>'); log.error('未找到书签栏的内容容器 <DL><p>');
bookmarks.categories = []; bookmarks.categories = [];
} else { } else {
const bookmarkBarContentStart = bookmarkBarStart + dlMatch.index + dlMatch[0].length; const bookmarkBarContentStart = bookmarkBarStart + dlMatch.index + dlMatch[0].length;
@@ -691,11 +691,11 @@ function parseBookmarks(htmlContent) {
} }
} }
console.log(`[INFO] 解析完成 - 共找到 ${bookmarks.categories.length} 个顶层分类`); log.info('解析完成', { categories: bookmarks.categories.length });
// 如果存在根路径书签,创建"根目录书签"特殊分类并插入到首位 // 如果存在根路径书签,创建"根目录书签"特殊分类并插入到首位
if (rootSites.length > 0) { if (rootSites.length > 0) {
console.log(`[INFO] 创建"根目录书签"特殊分类,包含 ${rootSites.length} 个书签`); log.info('创建"根目录书签"特殊分类', { sites: rootSites.length });
const rootCategory = { const rootCategory = {
name: '根目录书签', name: '根目录书签',
icon: 'fas fa-star', icon: 'fas fa-star',
@@ -705,7 +705,7 @@ function parseBookmarks(htmlContent) {
// 插入到数组首位 // 插入到数组首位
bookmarks.categories.unshift(rootCategory); bookmarks.categories.unshift(rootCategory);
console.log(`[INFO] "根目录书签"已插入到分类列表首位`); log.info('"根目录书签"已插入到分类列表首位');
} }
return bookmarks; return bookmarks;
@@ -742,7 +742,10 @@ ${yamlString}`;
return yamlWithComment; return yamlWithComment;
} catch (error) { } catch (error) {
console.error('Error generating YAML:', error); log.error('生成 YAML 失败', {
message: error && error.message ? error.message : String(error),
});
if (isVerbose() && error && error.stack) console.error(error.stack);
return null; return null;
} }
} }
@@ -768,46 +771,44 @@ function updateNavigationWithBookmarks() {
// 主函数 // 主函数
async function main() { async function main() {
console.log('========================================'); const elapsedMs = startTimer();
console.log('[INFO] 书签处理脚本启动'); log.info('开始', { version: process.env.npm_package_version });
console.log('[INFO] 时间:', new Date().toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }));
console.log('========================================\n');
// 获取最新的书签文件 // 获取最新的书签文件
console.log('[步骤 1/5] 查找书签文件...'); log.info('查找书签文件', { dir: BOOKMARKS_DIR });
const bookmarkFile = getLatestBookmarkFile(); const bookmarkFile = getLatestBookmarkFile();
if (!bookmarkFile) { if (!bookmarkFile) {
console.log('[WARN] 未找到书签文件,跳过处理将HTML书签文件放入 bookmarks/ 后再运行)。'); log.ok('未找到书签文件,跳过', { dir: BOOKMARKS_DIR });
return; return;
} }
console.log('[SUCCESS] 找到书签文件\n'); log.ok('找到书签文件', { file: bookmarkFile });
try { try {
// 读取文件内容 // 读取文件内容
console.log('[步骤 2/5] 读取书签文件...'); log.info('读取书签文件', { file: bookmarkFile });
const htmlContent = fs.readFileSync(bookmarkFile, 'utf8'); const htmlContent = fs.readFileSync(bookmarkFile, 'utf8');
console.log('[SUCCESS] 文件读取成功,大小:', htmlContent.length, '字符\n'); log.ok('读取成功', { chars: htmlContent.length });
// 解析书签 // 解析书签
console.log('[步骤 3/5] 解析书签结构...'); log.info('解析书签结构');
const bookmarks = parseBookmarks(htmlContent); const bookmarks = parseBookmarks(htmlContent);
if (bookmarks.categories.length === 0) { if (bookmarks.categories.length === 0) {
console.error('[ERROR] HTML文件中未找到书签分类处理终止'); log.error('HTML 文件中未找到书签分类,处理终止');
return; return;
} }
console.log('[SUCCESS] 解析完成\n'); log.ok('解析完成', { categories: bookmarks.categories.length });
// 生成YAML // 生成YAML
console.log('[步骤 4/5] 生成YAML配置...'); log.info('生成 YAML 配置');
const yamlContent = generateBookmarksYaml(bookmarks); const yamlContent = generateBookmarksYaml(bookmarks);
if (!yamlContent) { if (!yamlContent) {
console.error('[ERROR] YAML生成失败处理终止'); log.error('YAML 生成失败,处理终止');
return; return;
} }
console.log('[SUCCESS] YAML生成成功\n'); log.ok('YAML 生成成功');
// 保存文件 // 保存文件
console.log('[步骤 5/5] 保存配置文件...'); log.info('写入配置文件', { path: MODULAR_OUTPUT_FILE });
try { try {
// 完全替换策略:若尚未初始化用户配置,则先从默认配置初始化一份完整配置 // 完全替换策略:若尚未初始化用户配置,则先从默认配置初始化一份完整配置
ensureUserConfigInitialized(); ensureUserConfigInitialized();
@@ -829,25 +830,35 @@ async function main() {
]); ]);
} }
console.log('[SUCCESS] 文件保存成功'); log.ok('写入成功', { path: MODULAR_OUTPUT_FILE });
console.log('[INFO] 输出文件:', MODULAR_OUTPUT_FILE, '\n');
// 更新导航 // 更新导航
console.log('[附加步骤] 更新导航配置...'); log.info('更新导航配置(确保包含 bookmarks 入口)');
const navUpdateResult = updateNavigationWithBookmarks(); const navUpdateResult = updateNavigationWithBookmarks();
if (navUpdateResult.updated) { if (navUpdateResult.updated) {
console.log(`[SUCCESS] 导航配置已更新(${navUpdateResult.target}\n`); log.ok('导航配置已更新', {
target: navUpdateResult.target,
reason: navUpdateResult.reason,
});
} else if (navUpdateResult.reason === 'already_present') { } else if (navUpdateResult.reason === 'already_present') {
console.log('[INFO] 导航配置已包含书签入口,无需更新\n'); log.ok('导航配置已包含书签入口,无需更新', { target: navUpdateResult.target });
} else if (navUpdateResult.reason === 'no_navigation_config') { } else if (navUpdateResult.reason === 'no_site_yml') {
console.log( log.warn('未找到可用的 site.yml无法自动更新导航', { path: USER_SITE_YML });
'[WARN] 未找到可更新的导航配置文件;请确认 config/user/site.yml 存在且包含 navigation\n' } else if (navUpdateResult.reason === 'navigation_not_array') {
); log.warn('site.yml 中 navigation 不是数组,无法自动更新导航', { path: USER_SITE_YML });
} else if (navUpdateResult.reason === 'error') { } else if (navUpdateResult.reason === 'error') {
console.log('[WARN] 导航更新失败,请手动检查配置文件格式(详见错误信息)\n'); log.warn('导航更新失败,请手动检查配置文件格式(详见错误信息)');
console.error(navUpdateResult.error); if (navUpdateResult.error) {
log.warn('导航更新错误详情', {
message: navUpdateResult.error.message
? navUpdateResult.error.message
: String(navUpdateResult.error),
});
if (isVerbose() && navUpdateResult.error.stack)
console.error(navUpdateResult.error.stack);
}
} else { } else {
console.log('[INFO] 导航配置无需更新\n'); log.info('导航配置无需更新', { reason: navUpdateResult.reason });
} }
} catch (writeError) { } catch (writeError) {
throw new FileError('写入文件时出错', MODULAR_OUTPUT_FILE, [ throw new FileError('写入文件时出错', MODULAR_OUTPUT_FILE, [
@@ -857,9 +868,7 @@ async function main() {
]); ]);
} }
console.log('========================================'); log.ok('完成', { ms: elapsedMs(), output: MODULAR_OUTPUT_FILE });
console.log('[SUCCESS] 书签处理完成!');
console.log('========================================');
} catch (error) { } catch (error) {
// 如果是自定义错误,直接抛出 // 如果是自定义错误,直接抛出
if (error instanceof FileError) { if (error instanceof FileError) {

View File

@@ -2,6 +2,9 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const { collectSitesRecursively, normalizeUrlKey } = require('../utils/sites'); const { collectSitesRecursively, normalizeUrlKey } = require('../utils/sites');
const { createLogger } = require('../utils/logger');
const log = createLogger('cache:articles');
/** /**
* 读取 articles 页面 RSS 缓存Phase 2 * 读取 articles 页面 RSS 缓存Phase 2
@@ -68,7 +71,7 @@ function tryLoadArticlesFeedCache(pageId, config) {
}, },
}; };
} catch (e) { } catch (e) {
console.warn(`[WARN] articles 缓存读取失败${cachePath}(将回退 Phase 1`); log.warn('articles 缓存读取失败,将回退 Phase 1', { path: cachePath });
return null; return null;
} }
} }

View File

@@ -1,6 +1,10 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { createLogger } = require('../utils/logger');
const log = createLogger('cache:projects');
function tryLoadProjectsRepoCache(pageId, config) { function tryLoadProjectsRepoCache(pageId, config) {
if (!pageId) return null; if (!pageId) return null;
@@ -43,7 +47,7 @@ function tryLoadProjectsRepoCache(pageId, config) {
}, },
}; };
} catch (e) { } catch (e) {
console.warn(`[WARN] projects 缓存读取失败${cachePath}(将仅展示标题与描述)`); log.warn('projects 缓存读取失败,将仅展示标题与描述', { path: cachePath });
return null; return null;
} }
} }

View File

@@ -10,6 +10,9 @@ const {
} = require('./resolver'); } = require('./resolver');
const { assignCategorySlugs } = require('./slugs'); const { assignCategorySlugs } = require('./slugs');
const { ConfigError } = require('../utils/errors'); const { ConfigError } = require('../utils/errors');
const { createLogger } = require('../utils/logger');
const log = createLogger('config');
function loadConfig() { function loadConfig() {
let config = { let config = {
@@ -33,10 +36,8 @@ function loadConfig() {
} }
if (!fs.existsSync('config/user/pages')) { if (!fs.existsSync('config/user/pages')) {
console.warn( log.warn('检测到 config/user/pages/ 缺失,部分页面内容可能为空');
'[WARN] 检测到 config/user/ 目录,但缺少 config/user/pages/。部分页面内容可能为空。' log.warn('建议复制 config/_default/pages/ 到 config/user/pages/,再按需修改');
);
console.warn('[WARN] 建议:复制 config/_default/pages/ 到 config/user/pages/,再按需修改。');
} }
config = loadModularConfig('config/user'); config = loadModularConfig('config/user');

View File

@@ -2,8 +2,18 @@ const fs = require('node:fs');
const path = require('node:path'); const path = require('node:path');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
const { createLogger, isVerbose } = require('../utils/logger');
const log = createLogger('config');
function handleConfigLoadError(filePath, error) { function handleConfigLoadError(filePath, error) {
console.error(`Error loading configuration from ${filePath}:`, error); log.error('加载配置失败', {
path: filePath,
message: error && error.message ? error.message : String(error),
});
if (isVerbose() && error && error.stack) {
console.error(error.stack);
}
} }
function safeLoadYamlConfig(filePath) { function safeLoadYamlConfig(filePath) {
@@ -20,9 +30,7 @@ function safeLoadYamlConfig(filePath) {
} }
if (docs.length > 1) { if (docs.length > 1) {
console.warn( log.warn('检测到 YAML 多文档,仅使用第一个', { path: filePath });
`Warning: Multiple documents found in ${filePath}. Using the first document only.`
);
return docs[0]; return docs[0];
} }
@@ -59,7 +67,7 @@ function loadModularConfig(dirPath) {
if (siteConfig.navigation) { if (siteConfig.navigation) {
config.navigation = siteConfig.navigation; config.navigation = siteConfig.navigation;
console.log('使用 site.yml 中的导航配置'); if (isVerbose()) log.info('使用 site.yml 中的 navigation 配置');
} }
} }

View File

@@ -77,7 +77,8 @@ function ensureConfigDefaults(config) {
function validateConfig(config) { function validateConfig(config) {
if (!config || typeof config !== 'object') { if (!config || typeof config !== 'object') {
console.error('配置无效: 配置必须是一个对象'); const { createLogger } = require('../utils/logger');
createLogger('config').error('配置无效:配置必须是对象');
return false; return false;
} }

View File

@@ -4,6 +4,9 @@ const path = require('path');
const { handlebars } = require('../template/engine'); const { handlebars } = require('../template/engine');
const { getSubmenuForNavItem } = require('../config'); const { getSubmenuForNavItem } = require('../config');
const { escapeHtml } = require('../utils/html'); const { escapeHtml } = require('../utils/html');
const { createLogger, isVerbose } = require('../utils/logger');
const log = createLogger('render');
// 生成导航菜单 // 生成导航菜单
function generateNavigation(navigation, config) { function generateNavigation(navigation, config) {
@@ -104,7 +107,10 @@ function generateSocialLinks(social) {
return template(social); // 社交链接模板直接接收数组 return template(social); // 社交链接模板直接接收数组
} }
} catch (error) { } catch (error) {
console.error('Error rendering social-links template:', error); log.warn('渲染 social-links 模板失败,已回退到内置渲染', {
message: error && error.message ? error.message : String(error),
});
if (isVerbose() && error && error.stack) console.error(error.stack);
// 出错时回退到原始生成方法 // 出错时回退到原始生成方法
} }
@@ -124,7 +130,7 @@ function generateSocialLinks(social) {
function generatePageContent(pageId, data) { function generatePageContent(pageId, data) {
// 确保数据对象存在 // 确保数据对象存在
if (!data) { if (!data) {
console.error(`Missing data for page: ${pageId}`); log.warn('页面数据缺失,已回退为占位页面', { page: pageId });
return ` return `
<div class="welcome-section"> <div class="welcome-section">
<div class="welcome-section-main"> <div class="welcome-section-main">

View File

@@ -11,10 +11,13 @@ const {
buildProjectsMeta, buildProjectsMeta,
} = require('../cache/projects'); } = require('../cache/projects');
const { getPageConfigUpdatedAtMeta } = require('../utils/pageMeta'); const { getPageConfigUpdatedAtMeta } = require('../utils/pageMeta');
const { createLogger, isVerbose } = require('../utils/logger');
const log = createLogger('render');
function prepareNavigationData(pageId, config) { function prepareNavigationData(pageId, config) {
if (!Array.isArray(config.navigation)) { if (!Array.isArray(config.navigation)) {
console.warn('Warning: config.navigation is not an array in renderPage. Using empty array.'); log.warn('config.navigation 不是数组,已降级为空数组');
return []; return [];
} }
@@ -162,7 +165,7 @@ function preparePageData(pageId, config) {
} }
if (config[pageId] && config[pageId].template) { if (config[pageId] && config[pageId].template) {
console.log(`页面 ${pageId} 使用指定模板: ${templateName}`); if (isVerbose()) log.info(`页面 ${pageId} 使用指定模板`, { template: templateName });
} }
return { data, templateName }; return { data, templateName };

View File

@@ -26,6 +26,7 @@ const {
TemplateError, TemplateError,
wrapAsyncError, wrapAsyncError,
} = require('./utils/errors'); } = require('./utils/errors');
const { createLogger, isVerbose, startTimer } = require('./utils/logger');
/** /**
* 渲染单个页面 * 渲染单个页面
@@ -120,7 +121,6 @@ function generateHTML(config) {
// 渲染模板 // 渲染模板
return layoutTemplate(layoutData); return layoutTemplate(layoutData);
} catch (error) { } catch (error) {
console.error('Error rendering main HTML template:', error);
throw error; throw error;
} }
} }
@@ -143,13 +143,20 @@ function tryMinifyStaticAsset(srcPath, destPath, loader) {
fs.writeFileSync(destPath, result.code); fs.writeFileSync(destPath, result.code);
return true; return true;
} catch (error) { } catch (error) {
console.error(`Error minifying ${srcPath}:`, error); const log = createLogger('assets');
log.warn('压缩静态资源失败,已降级为原文件', {
path: srcPath,
message: error && error.message ? error.message : String(error),
});
if (isVerbose() && error && error.stack) console.error(error.stack);
return false; return false;
} }
} }
// 复制静态文件 // 复制静态文件
function copyStaticFiles(config) { function copyStaticFiles(config) {
const log = createLogger('assets');
// 确保 dist 目录存在 // 确保 dist 目录存在
if (!fs.existsSync('dist')) { if (!fs.existsSync('dist')) {
fs.mkdirSync('dist', { recursive: true }); fs.mkdirSync('dist', { recursive: true });
@@ -161,7 +168,8 @@ function copyStaticFiles(config) {
fs.copyFileSync('assets/style.css', 'dist/style.css'); fs.copyFileSync('assets/style.css', 'dist/style.css');
} }
} catch (e) { } catch (e) {
console.error('Error copying style.css:', e); log.error('复制 style.css 失败', { message: e && e.message ? e.message : String(e) });
if (isVerbose() && e && e.stack) console.error(e.stack);
} }
try { try {
@@ -169,7 +177,8 @@ function copyStaticFiles(config) {
fs.copyFileSync('assets/pinyin-match.js', 'dist/pinyin-match.js'); fs.copyFileSync('assets/pinyin-match.js', 'dist/pinyin-match.js');
} }
} catch (e) { } catch (e) {
console.error('Error copying pinyin-match.js:', e); log.error('复制 pinyin-match.js 失败', { message: e && e.message ? e.message : String(e) });
if (isVerbose() && e && e.stack) console.error(e.stack);
} }
// dist/script.js 由构建阶段 runtime bundle 产出scripts/build-runtime.js这里不再复制/覆盖 // dist/script.js 由构建阶段 runtime bundle 产出scripts/build-runtime.js这里不再复制/覆盖
@@ -194,7 +203,7 @@ function copyStaticFiles(config) {
const srcPath = path.join(process.cwd(), normalized); const srcPath = path.join(process.cwd(), normalized);
const destPath = path.join(process.cwd(), 'dist', normalized); const destPath = path.join(process.cwd(), 'dist', normalized);
if (!fs.existsSync(srcPath)) { if (!fs.existsSync(srcPath)) {
console.warn(`[WARN] faviconUrl 本地文件不存在${normalized}`); log.warn('faviconUrl 本地文件不存在', { path: normalized });
return; return;
} }
@@ -227,7 +236,8 @@ function copyStaticFiles(config) {
}); });
} }
} catch (e) { } catch (e) {
console.error('Error copying faviconUrl assets:', e); log.error('复制 faviconUrl 本地资源失败', { message: e && e.message ? e.message : String(e) });
if (isVerbose() && e && e.stack) console.error(e.stack);
} }
// 如果配置了 favicon确保文件存在并复制 // 如果配置了 favicon确保文件存在并复制
@@ -241,16 +251,29 @@ function copyStaticFiles(config) {
} else if (fs.existsSync(config.site.favicon)) { } else if (fs.existsSync(config.site.favicon)) {
fs.copyFileSync(config.site.favicon, `dist/${path.basename(config.site.favicon)}`); fs.copyFileSync(config.site.favicon, `dist/${path.basename(config.site.favicon)}`);
} else { } else {
console.warn(`Warning: Favicon file not found: ${config.site.favicon}`); log.warn('favicon 文件不存在', { path: config.site.favicon });
} }
} catch (e) { } catch (e) {
console.error('Error copying favicon:', e); log.error('复制 favicon 失败', { message: e && e.message ? e.message : String(e) });
if (isVerbose() && e && e.stack) console.error(e.stack);
} }
} }
} }
// 主函数 // 主函数
function main() { function main() {
const cmdLog = createLogger('generate');
const configLog = createLogger('config');
const renderLog = createLogger('render');
const elapsedMs = startTimer();
cmdLog.info('开始', { version: process.env.npm_package_version });
let source = 'unknown';
if (fs.existsSync('config/user')) source = 'config/user';
else if (fs.existsSync('config/_default')) source = 'config/_default';
configLog.info('加载模块化配置', { source });
const config = loadConfig(); const config = loadConfig();
try { try {
@@ -259,6 +282,10 @@ function main() {
fs.mkdirSync('dist', { recursive: true }); fs.mkdirSync('dist', { recursive: true });
} }
renderLog.info('生成页面', {
pages: Array.isArray(config.navigation) ? config.navigation.length : 0,
});
// 初始化 Handlebars 模板系统 // 初始化 Handlebars 模板系统
loadHandlebarsTemplates(); loadHandlebarsTemplates();
@@ -276,7 +303,10 @@ function main() {
fs.writeFileSync(path.join('dist', MENAV_EXTENSION_CONFIG_FILE), extensionConfig); fs.writeFileSync(path.join('dist', MENAV_EXTENSION_CONFIG_FILE), extensionConfig);
} }
} catch (error) { } catch (error) {
console.error('Error writing extension config file:', error); cmdLog.warn('写入扩展配置文件失败(不影响页面渲染)', {
message: error && error.message ? error.message : String(error),
});
if (isVerbose() && error && error.stack) console.error(error.stack);
} }
// GitHub Pages 静态路由回退:用于支持 /<id> 形式的路径深链接 // GitHub Pages 静态路由回退:用于支持 /<id> 形式的路径深链接
@@ -296,6 +326,8 @@ function main() {
// 复制静态文件 // 复制静态文件
copyStaticFiles(config); copyStaticFiles(config);
cmdLog.ok('完成', { ms: elapsedMs(), dist: 'dist/' });
} catch (e) { } catch (e) {
// 如果是自定义错误,直接抛出,保留上下文/路径信息 // 如果是自定义错误,直接抛出,保留上下文/路径信息
if ( if (

View File

@@ -3,6 +3,9 @@ const path = require('node:path');
const Handlebars = require('handlebars'); const Handlebars = require('handlebars');
const { registerAllHelpers } = require('../../helpers'); const { registerAllHelpers } = require('../../helpers');
const { createLogger, isVerbose } = require('../utils/logger');
const log = createLogger('template');
const handlebars = Handlebars.create(); const handlebars = Handlebars.create();
registerAllHelpers(handlebars); registerAllHelpers(handlebars);
@@ -82,7 +85,11 @@ function renderTemplate(templateName, data, useLayout = true) {
); );
} }
console.log(`模板 ${templateName}.hbs 不存在,使用通用模板 page.hbs 代替`); if (isVerbose()) {
log.info('页面模板不存在,已回退到通用模板 page.hbs', { template: `${templateName}.hbs` });
} else {
log.warn('页面模板不存在,已回退到通用模板 page.hbs', { template: `${templateName}.hbs` });
}
const genericTemplateContent = fs.readFileSync(genericTemplatePath, 'utf8'); const genericTemplateContent = fs.readFileSync(genericTemplatePath, 'utf8');
const genericTemplate = handlebars.compile(genericTemplateContent); const genericTemplate = handlebars.compile(genericTemplateContent);

View File

@@ -49,38 +49,42 @@ class FileError extends Error {
* @param {number} exitCode - 退出码,默认为 1 * @param {number} exitCode - 退出码,默认为 1
*/ */
function handleError(error, exitCode = 1) { function handleError(error, exitCode = 1) {
const { formatPrefix, isVerbose } = require('./logger');
// 错误标题行 // 错误标题行
console.error(`\n ${error.name}: ${error.message}`); console.error(`\n${formatPrefix('ERROR')} ${error.name}: ${error.message}`);
// 文件路径(如果有) // 文件路径(如果有)
if (error.filePath || error.templatePath) { if (error.filePath || error.templatePath) {
const path = error.filePath || error.templatePath; const path = error.filePath || error.templatePath;
console.error(` 位置: ${path}`); console.error(`位置: ${path}`);
} }
// 上下文信息(如果有) // 上下文信息(如果有)
if (error.context && Object.keys(error.context).length > 0) { if (error.context && Object.keys(error.context).length > 0) {
console.error(''); console.error('上下文:');
for (const [key, value] of Object.entries(error.context)) { for (const [key, value] of Object.entries(error.context)) {
console.error(` ${key}: ${value}`); console.error(` ${key}: ${value}`);
} }
} }
// 修复建议(如果有) // 修复建议(如果有)
if (error.suggestions && error.suggestions.length > 0) { if (error.suggestions && error.suggestions.length > 0) {
console.error(''); console.error('建议:');
console.error('➜ 解决方案:');
error.suggestions.forEach((suggestion, index) => { error.suggestions.forEach((suggestion, index) => {
console.error(` ${index + 1}. ${suggestion}`); console.error(` ${index + 1}) ${suggestion}`);
}); });
} }
// DEBUG 提示(仅在非 DEBUG 模式下显示) // DEBUG 提示(仅在非 DEBUG 模式下显示)
if (process.env.DEBUG) { if (process.env.DEBUG) {
console.error('\n堆栈跟踪:'); console.error('\n堆栈:');
console.error(error.stack || String(error));
} else if (isVerbose() && error && error.stack) {
console.error('\n堆栈:');
console.error(error.stack); console.error(error.stack);
} else { } else {
console.error('\n(设置 DEBUG=1 查看堆栈跟踪)'); console.error('\n提示: DEBUG=1 查看堆栈');
} }
console.error(); // 空行结束 console.error(); // 空行结束

View File

@@ -0,0 +1,78 @@
function parseBooleanEnv(value) {
if (value === undefined || value === null || value === '') return false;
const v = String(value).trim().toLowerCase();
return v === '1' || v === 'true' || v === 'yes' || v === 'y' || v === 'on';
}
function isVerbose() {
return parseBooleanEnv(process.env.MENAV_VERBOSE) || parseBooleanEnv(process.env.DEBUG);
}
function isColorEnabled() {
if (process.env.NO_COLOR) return false;
if (parseBooleanEnv(process.env.FORCE_COLOR)) return true;
return Boolean(
(process.stdout && process.stdout.isTTY) || (process.stderr && process.stderr.isTTY)
);
}
function colorize(text, ansiCode) {
if (!ansiCode || !isColorEnabled()) return text;
return `\x1b[${ansiCode}m${text}\x1b[0m`;
}
function formatMeta(meta) {
if (!meta || typeof meta !== 'object') return '';
const entries = Object.entries(meta)
.filter(([, value]) => value !== undefined && value !== null && value !== '')
.map(([key, value]) => `${key}=${String(value)}`);
if (entries.length === 0) return '';
return ` (${entries.join(', ')})`;
}
function formatPrefix(level) {
const base = `[${level}]`;
if (level === 'ERROR') return colorize(base, 31);
if (level === 'WARN') return colorize(base, 33);
if (level === 'OK') return colorize(base, 32);
return base;
}
function writeLine(level, scope, message, meta) {
const prefix = formatPrefix(level);
const scopePart = scope ? ` ${scope}:` : '';
const line = `${prefix}${scopePart} ${message}${formatMeta(meta)}`;
if (level === 'ERROR') {
console.error(line);
} else if (level === 'WARN') {
console.warn(line);
} else {
console.log(line);
}
}
function createLogger(scope) {
const normalized = scope ? String(scope) : '';
return {
info: (message, meta) => writeLine('INFO', normalized, message, meta),
warn: (message, meta) => writeLine('WARN', normalized, message, meta),
error: (message, meta) => writeLine('ERROR', normalized, message, meta),
ok: (message, meta) => writeLine('OK', normalized, message, meta),
};
}
function startTimer() {
const startedAt = process.hrtime.bigint();
return () => Number((process.hrtime.bigint() - startedAt) / 1_000_000n);
}
module.exports = {
createLogger,
formatMeta,
formatPrefix,
isColorEnabled,
isVerbose,
parseBooleanEnv,
startTimer,
};