chore: 引入统一日志模块,统一 cli 输出
This commit is contained in:
14
package.json
14
package.json
@@ -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": [
|
||||||
|
|||||||
@@ -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
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 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
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 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
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 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
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 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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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) {
|
||||||
|
|||||||
5
src/generator/cache/articles.js
vendored
5
src/generator/cache/articles.js
vendored
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
src/generator/cache/projects.js
vendored
6
src/generator/cache/projects.js
vendored
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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 配置');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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(); // 空行结束
|
||||||
|
|||||||
78
src/generator/utils/logger.js
Normal file
78
src/generator/utils/logger.js
Normal 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,
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user