diff --git a/changelog.md b/changelog.md index a7d1078..b59a974 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2026-03-06 - 4.3.0 - feat(mod_logger) +add centralized TsBuildLogger and replace ad-hoc console output with structured, colored logging + +- Add ts/mod_logger/classes.logger.ts providing header/step/detail/success/error/warn/indent utilities with ANSI color support +- Export logger from ts/mod_logger/index.ts and re-export from ts/index.ts +- Replace console.log/console.error/console.warn calls in mod_cli, mod_compiler, and mod_unpack with TsBuildLogger methods for consistent, hierarchical output +- Refactor error-summary, emit and type-check output to use logger separators, colors, and structured messages + ## 2026-03-05 - 4.2.6 - fix(meta) no changes diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 244762c..737e8d6 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@git.zone/tsbuild', - version: '4.2.6', + version: '4.3.0', description: 'A tool for compiling TypeScript files using the latest nightly features, offering flexible APIs and a CLI for streamlined development.' } diff --git a/ts/index.ts b/ts/index.ts index 10a1979..9ad0e6d 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -6,6 +6,7 @@ export * from './mod_fs/index.js'; export * from './mod_config/index.js'; export * from './mod_unpack/index.js'; export * from './mod_pathrewrite/index.js'; +export * from './mod_logger/index.js'; export * from './mod_compiler/index.js'; export * from './mod_cli/index.js'; diff --git a/ts/mod_cli/classes.tsbuildcli.ts b/ts/mod_cli/classes.tsbuildcli.ts index bfad526..ae1742a 100644 --- a/ts/mod_cli/classes.tsbuildcli.ts +++ b/ts/mod_cli/classes.tsbuildcli.ts @@ -5,6 +5,7 @@ import * as tspublish from '@git.zone/tspublish'; import { TsCompiler } from '../mod_compiler/index.js'; import { FsHelpers } from '../mod_fs/index.js'; +import { TsBuildLogger as log } from '../mod_logger/index.js'; /** * TsBuildCli handles all CLI commands for tsbuild. @@ -78,21 +79,20 @@ export class TsBuildCli { const patterns = argvArg._.slice(1); if (patterns.length === 0) { - console.error('\nโŒ Error: Please provide at least one TypeScript file path or glob pattern'); - console.error(' Usage: tsbuild emitcheck [additional_patterns ...]\n'); - console.error(' Example: tsbuild emitcheck "src/**/*.ts" "test/**/*.ts"\n'); + log.error('Please provide at least one TypeScript file path or glob pattern'); + log.indent('Usage: tsbuild emitcheck [...]'); + log.indent('Example: tsbuild emitcheck "src/**/*.ts" "test/**/*.ts"'); process.exit(1); } const allFiles = await this.collectFilesFromPatterns(patterns); if (allFiles.length === 0) { - console.error('\nโŒ Error: No TypeScript files found to check'); - console.error(' Please verify your file paths or glob patterns.\n'); + log.error('No TypeScript files found to check'); process.exit(1); } - console.log(`\n๐Ÿ”Ž Found ${allFiles.length} TypeScript file${allFiles.length !== 1 ? 's' : ''} to check`); + log.step('๐Ÿ”Ž', `Found ${allFiles.length} TypeScript file${allFiles.length !== 1 ? 's' : ''} to check`); const compiler = new TsCompiler(this.cwd, argvArg); const success = await compiler.checkEmit(allFiles); @@ -153,19 +153,12 @@ export class TsBuildCli { // Display compilation plan const folderCount = sortedTsFolders.length; - console.log(`\n๐Ÿ“‚ TypeScript Folder Compilation Plan (${folderCount} folder${folderCount !== 1 ? 's' : ''})`); - console.log('โ”Œ' + 'โ”€'.repeat(60) + 'โ”'); - console.log('โ”‚ ๐Ÿ”„ Compilation Order โ”‚'); - console.log('โ”œ' + 'โ”€'.repeat(60) + 'โ”ค'); + log.header('๐Ÿ“‚', `TypeScript Folder Compilation Plan (${folderCount} folder${folderCount !== 1 ? 's' : ''})`); sortedTsFolders.forEach((folder, index) => { - const prefix = index === folderCount - 1 ? 'โ””โ”€' : 'โ”œโ”€'; - const position = `${index + 1}/${folderCount}`; - console.log(`โ”‚ ${prefix} ${position.padStart(5)} ${folder.padEnd(46)} โ”‚`); + log.indent(`${index + 1}. ${folder}`); }); - console.log('โ””' + 'โ”€'.repeat(60) + 'โ”˜\n'); - // Build compilation object const compilationCommandObject: Record = {}; for (const tsFolder of sortedTsFolders) { @@ -198,12 +191,11 @@ export class TsBuildCli { const allFiles = await this.collectFilesFromPatterns(patterns); if (allFiles.length === 0) { - console.error('\nโŒ Error: No TypeScript files found to check'); - console.error(' Please verify your file paths or glob patterns.\n'); + log.error('No TypeScript files found to check'); process.exit(1); } - console.log(`\n๐Ÿ”Ž Found ${allFiles.length} TypeScript file${allFiles.length !== 1 ? 's' : ''} to check`); + log.step('๐Ÿ”Ž', `Found ${allFiles.length} TypeScript file${allFiles.length !== 1 ? 's' : ''} to check`); const compiler = new TsCompiler(this.cwd, argvArg); const success = await compiler.checkTypes(allFiles); @@ -216,34 +208,34 @@ export class TsBuildCli { * Run default type checks for ts/ and test/ directories */ private async runDefaultTypeChecks(argvArg: any): Promise { - console.log('\n๐Ÿ”ฌ Running default type checking sequence...\n'); + log.header('๐Ÿ”ฌ', 'Default type checking sequence'); // First check ts/**/* without skiplibcheck - console.log('๐Ÿ“‚ Checking ts/**/* files...'); + log.step('๐Ÿ“‚', 'Checking ts/**/* files...'); const tsTsFiles = await FsHelpers.listFilesWithGlob(this.cwd, 'ts/**/*.ts'); if (tsTsFiles.length > 0) { - console.log(` Found ${tsTsFiles.length} TypeScript files in ts/`); + log.detail('๐Ÿ“„', `${tsTsFiles.length} TypeScript files`); const tsAbsoluteFiles = smartpath.transform.toAbsolute(tsTsFiles, this.cwd) as string[]; const tsCompiler = new TsCompiler(this.cwd, argvArg); const tsSuccess = await tsCompiler.checkTypes(tsAbsoluteFiles); if (!tsSuccess) { - console.error('โŒ Type checking failed for ts/**/*'); + log.error('Type checking failed for ts/**/*'); process.exit(1); } - console.log('โœ… Type checking passed for ts/**/*\n'); + log.success('Type checking passed for ts/**/*'); } else { - console.log(' No TypeScript files found in ts/\n'); + log.detail('๐Ÿ“„', 'No TypeScript files found in ts/'); } // Then check test/**/* with skiplibcheck - console.log('๐Ÿ“‚ Checking test/**/* files with --skiplibcheck...'); + log.step('๐Ÿ“‚', 'Checking test/**/* files with --skiplibcheck...'); const testTsFiles = await FsHelpers.listFilesWithGlob(this.cwd, 'test/**/*.ts'); if (testTsFiles.length > 0) { - console.log(` Found ${testTsFiles.length} TypeScript files in test/`); + log.detail('๐Ÿ“„', `${testTsFiles.length} TypeScript files`); const testAbsoluteFiles = smartpath.transform.toAbsolute(testTsFiles, this.cwd) as string[]; const testArgvArg = { ...argvArg, skiplibcheck: true }; @@ -251,15 +243,15 @@ export class TsBuildCli { const testSuccess = await testCompiler.checkTypes(testAbsoluteFiles); if (!testSuccess) { - console.error('โŒ Type checking failed for test/**/*'); + log.error('Type checking failed for test/**/*'); process.exit(1); } - console.log('โœ… Type checking passed for test/**/*\n'); + log.success('Type checking passed for test/**/*'); } else { - console.log(' No TypeScript files found in test/\n'); + log.detail('๐Ÿ“„', 'No TypeScript files found in test/'); } - console.log('โœ… All default type checks passed!\n'); + log.success('All default type checks passed!'); process.exit(0); } @@ -272,19 +264,19 @@ export class TsBuildCli { for (const pattern of patterns) { if (pattern.includes('*') || pattern.includes('{') || pattern.includes('?')) { // Handle as glob pattern - console.log(`Processing glob pattern: ${pattern}`); + log.step('๐Ÿ”Ž', `Processing glob pattern: ${pattern}`); try { const stringMatchedFiles = await FsHelpers.listFilesWithGlob(this.cwd, pattern); if (stringMatchedFiles.length === 0) { - console.warn(`โš ๏ธ Warning: No files matched the pattern '${pattern}'`); + log.warn(`No files matched pattern '${pattern}'`); } else { - console.log(`๐Ÿ“‚ Found ${stringMatchedFiles.length} files matching pattern '${pattern}'`); + log.detail('๐Ÿ“‚', `${stringMatchedFiles.length} files matching '${pattern}'`); const absoluteMatchedFiles = smartpath.transform.toAbsolute(stringMatchedFiles, this.cwd) as string[]; allFiles = allFiles.concat(absoluteMatchedFiles); } } catch (err) { - console.error(`โŒ Error processing glob pattern '${pattern}': ${err}`); + log.error(`Error processing glob pattern '${pattern}': ${err}`); } } else { // Handle as direct file path @@ -293,7 +285,7 @@ export class TsBuildCli { if (fileExists) { allFiles.push(filePath); } else { - console.error(`โŒ Error: File not found: ${filePath}`); + log.error(`File not found: ${filePath}`); process.exit(1); } } diff --git a/ts/mod_compiler/classes.tscompiler.ts b/ts/mod_compiler/classes.tscompiler.ts index 23ce3ac..723a06f 100644 --- a/ts/mod_compiler/classes.tscompiler.ts +++ b/ts/mod_compiler/classes.tscompiler.ts @@ -8,6 +8,7 @@ import { TsConfig } from '../mod_config/index.js'; import { FsHelpers } from '../mod_fs/index.js'; import { performUnpack } from '../mod_unpack/index.js'; import { TsPathRewriter } from '../mod_pathrewrite/index.js'; +import { TsBuildLogger as log } from '../mod_logger/index.js'; /** * Interface for error summary data @@ -117,33 +118,23 @@ export class TsCompiler { } const { errorsByFile, generalErrors, totalErrors, totalFiles } = errorSummary; + const c = log.c; - // Print error summary header - console.log('\n' + '='.repeat(80)); + console.log(''); + console.log(c.dim + log.separator() + c.reset); console.log( - `โŒ Found ${totalErrors} error${totalErrors !== 1 ? 's' : ''} in ${totalFiles} file${totalFiles !== 1 ? 's' : ''}:` + `${c.red}โŒ Found ${totalErrors} error${totalErrors !== 1 ? 's' : ''} in ${totalFiles} file${totalFiles !== 1 ? 's' : ''}${c.reset}` ); - console.log('='.repeat(80)); - - // Color codes for error formatting - const colors = { - reset: '\x1b[0m', - red: '\x1b[31m', - yellow: '\x1b[33m', - cyan: '\x1b[36m', - white: '\x1b[37m', - brightRed: '\x1b[91m', - }; + console.log(c.dim + log.separator() + c.reset); // Print file-specific errors Object.entries(errorsByFile).forEach(([fileName, fileErrors]) => { - // Show relative path if possible for cleaner output const displayPath = fileName.replace(process.cwd(), '').replace(/^\//, ''); console.log( - `\n${colors.cyan}File: ${displayPath} ${colors.yellow}(${fileErrors.length} error${fileErrors.length !== 1 ? 's' : ''})${colors.reset}` + `\n ${c.cyan}File: ${displayPath}${c.reset} ${c.yellow}(${fileErrors.length} error${fileErrors.length !== 1 ? 's' : ''})${c.reset}` ); - console.log('-'.repeat(80)); + console.log(` ${c.dim}${'โ”€'.repeat(40)}${c.reset}`); fileErrors.forEach((diagnostic) => { if (diagnostic.file && diagnostic.start !== undefined) { @@ -152,19 +143,18 @@ export class TsCompiler { const errorCode = diagnostic.code ? `TS${diagnostic.code}` : 'Error'; console.log( - `${colors.white}Line ${line + 1}, Col ${character + 1}${colors.reset}: ${colors.brightRed}${errorCode}${colors.reset} - ${message}` + ` ${c.white}Line ${line + 1}, Col ${character + 1}${c.reset}: ${c.brightRed}${errorCode}${c.reset} - ${message}` ); - // Try to show the code snippet if possible try { const lineContent = diagnostic.file.text.split('\n')[line]; if (lineContent) { - console.log(` ${lineContent.trimEnd()}`); - const indicator = ' '.repeat(character) + `${colors.red}^${colors.reset}`; - console.log(` ${indicator}`); + console.log(` ${lineContent.trimEnd()}`); + const indicator = ' '.repeat(character) + `${c.red}^${c.reset}`; + console.log(` ${indicator}`); } } catch { - // Failed to get source text, skip showing the code snippet + // Failed to get source text } } }); @@ -172,17 +162,18 @@ export class TsCompiler { // Print general errors if (generalErrors.length > 0) { - console.log(`\n${colors.yellow}General Errors:${colors.reset}`); - console.log('-'.repeat(80)); + console.log(`\n ${c.yellow}General Errors:${c.reset}`); + console.log(` ${c.dim}${'โ”€'.repeat(40)}${c.reset}`); generalErrors.forEach((diagnostic) => { const message = typescript.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); const errorCode = diagnostic.code ? `TS${diagnostic.code}` : 'Error'; - console.log(`${colors.brightRed}${errorCode}${colors.reset}: ${message}`); + console.log(` ${c.brightRed}${errorCode}${c.reset}: ${message}`); }); } - console.log('\n' + '='.repeat(80) + '\n'); + console.log(''); + console.log(c.dim + log.separator() + c.reset); } /** @@ -190,12 +181,11 @@ export class TsCompiler { */ private async handleSkipLibCheckWarning(): Promise { if (this.argvArg?.confirmskiplibcheck) { - console.log('\nโš ๏ธ WARNING โš ๏ธ'); - console.log('You are skipping libcheck... Is that really wanted?'); - console.log('Continuing in 5 seconds...\n'); + log.warn('WARNING: You are skipping libcheck... Is that really wanted?'); + log.indent('Continuing in 5 seconds...'); await smartdelay.delayFor(5000); } else if (!this.argvArg?.quiet && !this.argvArg?.json) { - console.log('โš ๏ธ skipLibCheck enabled; use --confirmskiplibcheck to pause with warning.'); + log.warn('skipLibCheck enabled; use --confirmskiplibcheck to pause with warning.'); } } @@ -213,17 +203,14 @@ export class TsCompiler { await this.handleSkipLibCheckWarning(); } - // Enhanced logging with task info const startTime = Date.now(); if (taskInfo) { const { taskNumber, totalTasks, sourcePattern, fileCount } = taskInfo; const relativeDestDir = taskInfo.destDir.replace(process.cwd(), '').replace(/^\//, ''); - console.log( - `\n๐Ÿ”จ [${taskNumber}/${totalTasks}] Compiling ${fileCount} file${fileCount !== 1 ? 's' : ''} from ${sourcePattern}` - ); - console.log(` ๐Ÿ“ Output: ${relativeDestDir}`); + log.step('๐Ÿ”จ', `[${taskNumber}/${totalTasks}] Compiling ${fileCount} file${fileCount !== 1 ? 's' : ''} from ${sourcePattern}`); + log.detail('๐Ÿ“', `Output: ${relativeDestDir}`); } else { - console.log(`๐Ÿ”จ Compiling ${fileNames.length} files...`); + log.step('๐Ÿ”จ', `Compiling ${fileNames.length} files...`); } const done = smartpromise.defer(); @@ -233,11 +220,9 @@ export class TsCompiler { const preEmitDiagnostics = typescript.getPreEmitDiagnostics(program); const preEmitErrorSummary = this.processDiagnostics(preEmitDiagnostics); - // Only continue to emit phase if no pre-emit errors if (preEmitErrorSummary.totalErrors > 0) { this.displayErrorSummary(preEmitErrorSummary); - console.error('\nโŒ TypeScript pre-emit checks failed. Please fix the issues listed above before proceeding.'); - console.error(' Type errors must be resolved before the compiler can emit output files.\n'); + log.error('Pre-emit checks failed'); done.resolve({ emittedFiles: [], errorSummary: preEmitErrorSummary }); return done.promise; } @@ -267,9 +252,9 @@ export class TsCompiler { if (taskInfo) { const { taskNumber, totalTasks } = taskInfo; - console.log(`โœ… [${taskNumber}/${totalTasks}] Task completed in ${duration}ms`); + log.success(`[${taskNumber}/${totalTasks}] Completed in ${duration}ms`); } else { - console.log(`โœ… TypeScript emit succeeded! (${duration}ms)`); + log.success(`TypeScript emit succeeded (${duration}ms)`); } // Get count of emitted files by type @@ -278,16 +263,13 @@ export class TsCompiler { const mapFiles = emitResult.emittedFiles?.filter((f) => f.endsWith('.map')).length || 0; if (emitResult.emittedFiles && emitResult.emittedFiles.length > 0) { - console.log( - ` ๐Ÿ“„ Generated ${emitResult.emittedFiles.length} files: ${jsFiles} .js, ${dtsFiles} .d.ts, ${mapFiles} source maps` - ); + log.detail('๐Ÿ“„', `Generated ${emitResult.emittedFiles.length} files: ${jsFiles} .js, ${dtsFiles} .d.ts, ${mapFiles} source maps`); } done.resolve({ emittedFiles: emitResult.emittedFiles || [], errorSummary: combinedErrorSummary }); } else { this.displayErrorSummary(combinedErrorSummary); - console.error('\nโŒ TypeScript emit failed. Please investigate the errors listed above!'); - console.error(' No output files have been generated.\n'); + log.error('TypeScript emit failed'); done.resolve({ emittedFiles: [], errorSummary: combinedErrorSummary }); } @@ -323,16 +305,13 @@ export class TsCompiler { const isJson = this.argvArg?.json === true; if (!isQuiet && !isJson) { - console.log(`\n๐Ÿ‘ท TypeScript Compilation Tasks (${totalTasks} task${totalTasks !== 1 ? 's' : ''}):`); + log.header('๐Ÿ‘ท', `TypeScript Compilation Tasks (${totalTasks} task${totalTasks !== 1 ? 's' : ''})`); Object.entries(globPatterns).forEach(([source, dest]) => { - console.log(` ๐Ÿ“‚ ${source} โ†’ ${dest}`); + log.detail('๐Ÿ“‚', `${source} โ†’ ${dest}`); }); - console.log(''); } // Phase 1: Resolve glob patterns and clean ALL output directories upfront. - // This ensures no rm/rmSync activity overlaps with TypeScript compilation, - // preventing XFS metadata corruption from concurrent metadata operations. interface IResolvedTask { pattern: string; destPath: string; @@ -351,7 +330,7 @@ export class TsCompiler { if (await FsHelpers.directoryExists(destDir)) { if (!isQuiet && !isJson) { - console.log(`๐Ÿงน Clearing output directory: ${destPath}`); + log.step('๐Ÿงน', `Clearing output directory: ${destPath}`); } await FsHelpers.removeDirectory(destDir); } @@ -359,7 +338,7 @@ export class TsCompiler { resolvedTasks.push({ pattern, destPath, destDir, absoluteFiles }); } - // Phase 2: Compile all tasks. No filesystem cleanup happens during this phase. + // Phase 2: Compile all tasks. const pendingUnpacks: Array<{ pattern: string; destDir: string }> = []; for (const task of resolvedTasks) { @@ -389,15 +368,11 @@ export class TsCompiler { } // Phase 3: Perform all unpacks after all compilations are done. - // This ensures no output directory is modified while subsequent compilations - // are performing heavy filesystem writes to sibling directories. for (const { pattern, destDir } of pendingUnpacks) { await performUnpack(pattern, destDir, this.cwd); } - // Rewrite import paths in all output directories to handle cross-module references - // This must happen after ALL compilations so all destination folders exist - // Use fromProjectDirectory to detect ALL ts_* folders, not just the ones being compiled + // Rewrite import paths in all output directories if (successfulOutputDirs.length > 0) { const rewriter = await TsPathRewriter.fromProjectDirectory(this.cwd); let totalRewritten = 0; @@ -405,7 +380,7 @@ export class TsCompiler { totalRewritten += await rewriter.rewriteDirectory(outputDir); } if (totalRewritten > 0 && !isQuiet && !isJson) { - console.log(` ๐Ÿ”„ Rewrote import paths in ${totalRewritten} file${totalRewritten !== 1 ? 's' : ''}`); + log.detail('๐Ÿ”„', `Rewrote import paths in ${totalRewritten} file${totalRewritten !== 1 ? 's' : ''}`); } } @@ -454,7 +429,7 @@ export class TsCompiler { const options = { ...this.createOptions(customOptions), noEmit: true }; const fileCount = fileNames.length; - console.log(`\n๐Ÿ” Checking if ${fileCount} file${fileCount !== 1 ? 's' : ''} can be emitted...`); + log.step('๐Ÿ”', `Checking if ${fileCount} file${fileCount !== 1 ? 's' : ''} can be emitted...`); const program = this.createProgram(fileNames, options); @@ -474,12 +449,10 @@ export class TsCompiler { const success = combinedErrorSummary.totalErrors === 0 && !emitResult.emitSkipped; if (success) { - console.log('\nโœ… TypeScript emit check passed! All files can be emitted successfully.'); - console.log(` ${fileCount} file${fileCount !== 1 ? 's' : ''} ${fileCount !== 1 ? 'are' : 'is'} ready to be compiled.\n`); + log.success(`Emit check passed (${fileCount} file${fileCount !== 1 ? 's' : ''})`); } else { this.displayErrorSummary(combinedErrorSummary); - console.error('\nโŒ TypeScript emit check failed. Please fix the issues listed above.'); - console.error(' The compilation cannot proceed until these errors are resolved.\n'); + log.error('Emit check failed'); } return success; @@ -492,7 +465,7 @@ export class TsCompiler { const options = { ...this.createOptions(customOptions), noEmit: true }; const fileCount = fileNames.length; - console.log(`\n๐Ÿ” Type checking ${fileCount} TypeScript file${fileCount !== 1 ? 's' : ''}...`); + log.step('๐Ÿ”', `Type checking ${fileCount} file${fileCount !== 1 ? 's' : ''}...`); const program = this.createProgram(fileNames, options); const diagnostics = typescript.getPreEmitDiagnostics(program); @@ -501,12 +474,10 @@ export class TsCompiler { const success = errorSummary.totalErrors === 0; if (success) { - console.log('\nโœ… TypeScript type check passed! No type errors found.'); - console.log(` All ${fileCount} file${fileCount !== 1 ? 's' : ''} passed type checking successfully.\n`); + log.success(`Type check passed (${fileCount} file${fileCount !== 1 ? 's' : ''})`); } else { this.displayErrorSummary(errorSummary); - console.error('\nโŒ TypeScript type check failed. Please fix the type errors listed above.'); - console.error(' The type checker found issues that need to be resolved.\n'); + log.error('Type check failed'); } return success; @@ -544,42 +515,28 @@ export class TsCompiler { * Display final compilation summary */ private displayFinalSummary(errorSummary: IErrorSummary): void { + const c = log.c; + if (errorSummary.totalErrors === 0) { - console.log('\n๐Ÿ“Š \x1b[32mCompilation Summary: All tasks completed successfully! โœ…\x1b[0m\n'); + log.header('๐Ÿ“Š', `${c.green}Compilation Summary: All tasks completed successfully! โœ…${c.reset}`); return; } - const colors = { - reset: '\x1b[0m', - red: '\x1b[31m', - yellow: '\x1b[33m', - cyan: '\x1b[36m', - brightRed: '\x1b[91m', - brightYellow: '\x1b[93m', - }; - - console.log('\n' + '='.repeat(80)); - console.log(`๐Ÿ“Š ${colors.brightYellow}Final Compilation Summary${colors.reset}`); - console.log('='.repeat(80)); - - if (errorSummary.totalFiles > 0) { - console.log(`${colors.brightRed}โŒ Files with errors (${errorSummary.totalFiles}):${colors.reset}`); - - Object.entries(errorSummary.errorsByFile).forEach(([fileName, errors]) => { - const displayPath = fileName.replace(process.cwd(), '').replace(/^\//, ''); - console.log( - ` ${colors.red}โ€ข${colors.reset} ${colors.cyan}${displayPath}${colors.reset} ${colors.yellow}(${errors.length} error${errors.length !== 1 ? 's' : ''})${colors.reset}` - ); - }); - } - - if (errorSummary.generalErrors.length > 0) { - console.log(`${colors.brightRed}โŒ General errors: ${errorSummary.generalErrors.length}${colors.reset}`); - } + log.header('๐Ÿ“Š', 'Compilation Summary'); console.log( - `\n${colors.brightRed}Total: ${errorSummary.totalErrors} error${errorSummary.totalErrors !== 1 ? 's' : ''} across ${errorSummary.totalFiles} file${errorSummary.totalFiles !== 1 ? 's' : ''}${colors.reset}` + `${c.brightRed}โŒ ${errorSummary.totalErrors} error${errorSummary.totalErrors !== 1 ? 's' : ''} across ${errorSummary.totalFiles} file${errorSummary.totalFiles !== 1 ? 's' : ''}${c.reset}` ); - console.log('='.repeat(80) + '\n'); + + Object.entries(errorSummary.errorsByFile).forEach(([fileName, errors]) => { + const displayPath = fileName.replace(process.cwd(), '').replace(/^\//, ''); + log.indent( + `${c.red}โ€ข${c.reset} ${c.cyan}${displayPath}${c.reset} ${c.yellow}(${errors.length} error${errors.length !== 1 ? 's' : ''})${c.reset}` + ); + }); + + if (errorSummary.generalErrors.length > 0) { + console.log(`${c.brightRed}โŒ General errors: ${errorSummary.generalErrors.length}${c.reset}`); + } } } diff --git a/ts/mod_logger/classes.logger.ts b/ts/mod_logger/classes.logger.ts new file mode 100644 index 0000000..fe1139a --- /dev/null +++ b/ts/mod_logger/classes.logger.ts @@ -0,0 +1,72 @@ +/** + * Centralized console output for tsbuild. + * + * Visual hierarchy (4 levels): + * HEADER โ€” top-level section start (emoji + bold text + separator line) + * STEP โ€” major action within a section (emoji + text, no indent) + * DETAIL โ€” supplementary info under a step (3-space indent + emoji + text) + * SUCCESS/ERROR/WARN โ€” outcome indicators (emoji + text, no indent) + */ +export class TsBuildLogger { + static readonly c = { + reset: '\x1b[0m', + bold: '\x1b[1m', + dim: '\x1b[2m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + cyan: '\x1b[36m', + white: '\x1b[37m', + brightRed: '\x1b[91m', + brightGreen: '\x1b[92m', + brightYellow: '\x1b[93m', + }; + + static readonly SEPARATOR_WIDTH = 70; + + static separator(char = 'โ”€'): string { + return char.repeat(this.SEPARATOR_WIDTH); + } + + /** Level 1: Section header. Blank line before, separator after. */ + static header(emoji: string, text: string): void { + console.log(''); + console.log(`${emoji} ${this.c.bold}${text}${this.c.reset}`); + console.log(this.c.dim + this.separator() + this.c.reset); + } + + /** Level 2: Step within a section. No indent. */ + static step(emoji: string, text: string): void { + console.log(`${emoji} ${text}`); + } + + /** Level 3: Detail under a step. 3-space indent. */ + static detail(emoji: string, text: string): void { + console.log(` ${emoji} ${text}`); + } + + /** Outcome: success */ + static success(text: string): void { + console.log(`${this.c.green}โœ… ${text}${this.c.reset}`); + } + + /** Outcome: error (goes to stderr) */ + static error(text: string): void { + console.error(`${this.c.red}โŒ ${text}${this.c.reset}`); + } + + /** Outcome: warning */ + static warn(text: string): void { + console.log(`${this.c.yellow}โš ๏ธ ${text}${this.c.reset}`); + } + + /** Plain indented line (for code snippets, list items, etc.) */ + static indent(text: string, level = 1): void { + console.log(' '.repeat(level) + text); + } + + /** Blank line */ + static blank(): void { + console.log(''); + } +} diff --git a/ts/mod_logger/index.ts b/ts/mod_logger/index.ts new file mode 100644 index 0000000..8872439 --- /dev/null +++ b/ts/mod_logger/index.ts @@ -0,0 +1 @@ +export * from './classes.logger.js'; diff --git a/ts/mod_unpack/classes.tsunpacker.ts b/ts/mod_unpack/classes.tsunpacker.ts index 950810c..0adcbfe 100644 --- a/ts/mod_unpack/classes.tsunpacker.ts +++ b/ts/mod_unpack/classes.tsunpacker.ts @@ -2,6 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import { TsPublishConfig } from '../mod_config/index.js'; import { FsHelpers } from '../mod_fs/index.js'; +import { TsBuildLogger as log } from '../mod_logger/index.js'; /** * TsUnpacker handles flattening of nested TypeScript output directories. @@ -127,7 +128,7 @@ export class TsUnpacker { // Step 3: Remove the now-empty nested directory fs.rmdirSync(nestedPath); - console.log(` ๐Ÿ“ฆ Unpacked ${this.sourceFolderName}: ${nestedEntries.length} entries`); + log.detail('๐Ÿ“ฆ', `Unpacked ${this.sourceFolderName}: ${nestedEntries.length} entries`); return true; }