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

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

View File

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

View File

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

View File

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

View File

@@ -2,8 +2,18 @@ const fs = require('node:fs');
const path = require('node:path');
const yaml = require('js-yaml');
const { createLogger, isVerbose } = require('../utils/logger');
const log = createLogger('config');
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) {
@@ -20,9 +30,7 @@ function safeLoadYamlConfig(filePath) {
}
if (docs.length > 1) {
console.warn(
`Warning: Multiple documents found in ${filePath}. Using the first document only.`
);
log.warn('检测到 YAML 多文档,仅使用第一个', { path: filePath });
return docs[0];
}
@@ -59,7 +67,7 @@ function loadModularConfig(dirPath) {
if (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) {
if (!config || typeof config !== 'object') {
console.error('配置无效: 配置必须是一个对象');
const { createLogger } = require('../utils/logger');
createLogger('config').error('配置无效:配置必须是对象');
return false;
}

View File

@@ -4,6 +4,9 @@ const path = require('path');
const { handlebars } = require('../template/engine');
const { getSubmenuForNavItem } = require('../config');
const { escapeHtml } = require('../utils/html');
const { createLogger, isVerbose } = require('../utils/logger');
const log = createLogger('render');
// 生成导航菜单
function generateNavigation(navigation, config) {
@@ -104,7 +107,10 @@ function generateSocialLinks(social) {
return template(social); // 社交链接模板直接接收数组
}
} 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) {
// 确保数据对象存在
if (!data) {
console.error(`Missing data for page: ${pageId}`);
log.warn('页面数据缺失,已回退为占位页面', { page: pageId });
return `
<div class="welcome-section">
<div class="welcome-section-main">

View File

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

View File

@@ -26,6 +26,7 @@ const {
TemplateError,
wrapAsyncError,
} = require('./utils/errors');
const { createLogger, isVerbose, startTimer } = require('./utils/logger');
/**
* 渲染单个页面
@@ -120,7 +121,6 @@ function generateHTML(config) {
// 渲染模板
return layoutTemplate(layoutData);
} catch (error) {
console.error('Error rendering main HTML template:', error);
throw error;
}
}
@@ -143,13 +143,20 @@ function tryMinifyStaticAsset(srcPath, destPath, loader) {
fs.writeFileSync(destPath, result.code);
return true;
} 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;
}
}
// 复制静态文件
function copyStaticFiles(config) {
const log = createLogger('assets');
// 确保 dist 目录存在
if (!fs.existsSync('dist')) {
fs.mkdirSync('dist', { recursive: true });
@@ -161,7 +168,8 @@ function copyStaticFiles(config) {
fs.copyFileSync('assets/style.css', 'dist/style.css');
}
} 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 {
@@ -169,7 +177,8 @@ function copyStaticFiles(config) {
fs.copyFileSync('assets/pinyin-match.js', 'dist/pinyin-match.js');
}
} 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这里不再复制/覆盖
@@ -194,7 +203,7 @@ function copyStaticFiles(config) {
const srcPath = path.join(process.cwd(), normalized);
const destPath = path.join(process.cwd(), 'dist', normalized);
if (!fs.existsSync(srcPath)) {
console.warn(`[WARN] faviconUrl 本地文件不存在${normalized}`);
log.warn('faviconUrl 本地文件不存在', { path: normalized });
return;
}
@@ -227,7 +236,8 @@ function copyStaticFiles(config) {
});
}
} 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确保文件存在并复制
@@ -241,16 +251,29 @@ function copyStaticFiles(config) {
} else if (fs.existsSync(config.site.favicon)) {
fs.copyFileSync(config.site.favicon, `dist/${path.basename(config.site.favicon)}`);
} else {
console.warn(`Warning: Favicon file not found: ${config.site.favicon}`);
log.warn('favicon 文件不存在', { path: config.site.favicon });
}
} 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() {
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();
try {
@@ -259,6 +282,10 @@ function main() {
fs.mkdirSync('dist', { recursive: true });
}
renderLog.info('生成页面', {
pages: Array.isArray(config.navigation) ? config.navigation.length : 0,
});
// 初始化 Handlebars 模板系统
loadHandlebarsTemplates();
@@ -276,7 +303,10 @@ function main() {
fs.writeFileSync(path.join('dist', MENAV_EXTENSION_CONFIG_FILE), extensionConfig);
}
} 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> 形式的路径深链接
@@ -296,6 +326,8 @@ function main() {
// 复制静态文件
copyStaticFiles(config);
cmdLog.ok('完成', { ms: elapsedMs(), dist: 'dist/' });
} catch (e) {
// 如果是自定义错误,直接抛出,保留上下文/路径信息
if (

View File

@@ -3,6 +3,9 @@ const path = require('node:path');
const Handlebars = require('handlebars');
const { registerAllHelpers } = require('../../helpers');
const { createLogger, isVerbose } = require('../utils/logger');
const log = createLogger('template');
const handlebars = Handlebars.create();
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 genericTemplate = handlebars.compile(genericTemplateContent);

View File

@@ -49,38 +49,42 @@ class FileError extends Error {
* @param {number} 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) {
const path = error.filePath || error.templatePath;
console.error(` 位置: ${path}`);
console.error(`位置: ${path}`);
}
// 上下文信息(如果有)
if (error.context && Object.keys(error.context).length > 0) {
console.error('');
console.error('上下文:');
for (const [key, value] of Object.entries(error.context)) {
console.error(` ${key}: ${value}`);
console.error(` ${key}: ${value}`);
}
}
// 修复建议(如果有)
if (error.suggestions && error.suggestions.length > 0) {
console.error('');
console.error('➜ 解决方案:');
console.error('建议:');
error.suggestions.forEach((suggestion, index) => {
console.error(` ${index + 1}. ${suggestion}`);
console.error(` ${index + 1}) ${suggestion}`);
});
}
// DEBUG 提示(仅在非 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);
} else {
console.error('\n(设置 DEBUG=1 查看堆栈跟踪)');
console.error('\n提示: DEBUG=1 查看堆栈');
}
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,
};