import * as path from 'path'; import * as smartcli from '@push.rocks/smartcli'; import * as smartpath from '@push.rocks/smartpath'; import * as tspublish from '@git.zone/tspublish'; import { TsCompiler } from '../mod_compiler/index.js'; import { FsHelpers } from '../mod_fs/index.js'; /** * TsBuildCli handles all CLI commands for tsbuild. * Provides commands for compiling, type checking, and emit validation. */ export class TsBuildCli { private cli: smartcli.Smartcli; private cwd: string; constructor(cwd: string = process.cwd()) { this.cwd = cwd; this.cli = new smartcli.Smartcli(); this.registerCommands(); } /** * Register all CLI commands */ private registerCommands(): void { this.registerStandardCommand(); this.registerCustomCommand(); this.registerEmitCheckCommand(); this.registerTsFoldersCommand(); this.registerCheckCommand(); } /** * Standard command: compiles ts folder to dist_ts */ private registerStandardCommand(): void { this.cli.standardCommand().subscribe(async (argvArg) => { const compiler = new TsCompiler(this.cwd, argvArg); await compiler.compileGlob({ './ts/**/*.ts': './dist_ts', }); const summary = (argvArg as any)?.__tsbuildFinalErrorSummary; if (summary && summary.totalErrors > 0) { process.exit(1); } }); } /** * Custom command: compiles specified directories to dist_ prefixed directories */ private registerCustomCommand(): void { this.cli.addCommand('custom').subscribe(async (argvArg) => { const listedDirectories = argvArg._; listedDirectories.shift(); // removes the first element that is "custom" const compilationCommandObject: Record = {}; for (const directory of listedDirectories) { compilationCommandObject[`./${directory}/**/*.ts`] = `./dist_${directory}`; } const compiler = new TsCompiler(this.cwd, argvArg); await compiler.compileGlob(compilationCommandObject); const summary = (argvArg as any)?.__tsbuildFinalErrorSummary; if (summary && summary.totalErrors > 0) { process.exit(1); } }); } /** * Emit check command: validates files can be emitted without producing output */ private registerEmitCheckCommand(): void { this.cli.addCommand('emitcheck').subscribe(async (argvArg) => { 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'); 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'); process.exit(1); } console.log(`\nšŸ”Ž Found ${allFiles.length} TypeScript file${allFiles.length !== 1 ? 's' : ''} to check`); const compiler = new TsCompiler(this.cwd, argvArg); const success = await compiler.checkEmit(allFiles); process.exit(success ? 0 : 1); }); } /** * TsFolders command: compiles all ts_* directories in order */ private registerTsFoldersCommand(): void { this.cli.addCommand('tsfolders').subscribe(async (argvArg) => { // List folders matching /^ts/ regex const allEntries = await FsHelpers.listDirectory(this.cwd); const tsFolders = allEntries .filter((e) => e.isDirectory && /^ts/.test(e.name)) .map((e) => e.name); // Get tspublish.json based ranking const tsPublishInstance = new tspublish.TsPublish(); const tsPublishModules = await tsPublishInstance.getModuleSubDirs(this.cwd); // Create an array with folder names and their ranks const foldersWithOrder: Array<{ folder: string; rank: number }> = []; for (const folder of tsFolders) { let rank = Infinity; if (tsPublishModules[folder] && tsPublishModules[folder].order !== undefined) { rank = tsPublishModules[folder].order; } foldersWithOrder.push({ folder, rank }); } // Sort the folders based on rank foldersWithOrder.sort((a, b) => a.rank - b.rank); // Construct the sorted list of folders const sortedTsFolders: string[] = []; for (const item of foldersWithOrder) { sortedTsFolders.push(item.folder); } // Ensure ts_interfaces is first and ts_shared is second if they exist const ensurePosition = (folderName: string, position: number) => { if (tsFolders.indexOf(folderName) > -1 && Object.keys(tsPublishModules).indexOf(folderName) === -1) { const currentIndex = sortedTsFolders.indexOf(folderName); if (currentIndex > -1) { sortedTsFolders.splice(currentIndex, 1); } sortedTsFolders.splice(position, 0, folderName); } }; ensurePosition('ts_interfaces', 0); ensurePosition('ts_shared', 1); // 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) + '┤'); sortedTsFolders.forEach((folder, index) => { const prefix = index === folderCount - 1 ? '└─' : 'ā”œā”€'; const position = `${index + 1}/${folderCount}`; console.log(`│ ${prefix} ${position.padStart(5)} ${folder.padEnd(46)} │`); }); console.log('ā””' + '─'.repeat(60) + 'ā”˜\n'); // Build compilation object const compilationCommandObject: Record = {}; for (const tsFolder of sortedTsFolders) { compilationCommandObject[`./${tsFolder}/**/*.ts`] = `./dist_${tsFolder}`; } const compiler = new TsCompiler(this.cwd, argvArg); await compiler.compileGlob(compilationCommandObject); const summary = (argvArg as any)?.__tsbuildFinalErrorSummary; if (summary && summary.totalErrors > 0) { process.exit(1); } }); } /** * Check command: type checks files without emitting */ private registerCheckCommand(): void { this.cli.addCommand('check').subscribe(async (argvArg) => { const patterns = argvArg._.slice(1); // If no patterns provided, default to checking ts/**/* and then test/**/* if (patterns.length === 0) { await this.runDefaultTypeChecks(argvArg); return; } 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'); process.exit(1); } console.log(`\nšŸ”Ž Found ${allFiles.length} TypeScript file${allFiles.length !== 1 ? 's' : ''} to check`); const compiler = new TsCompiler(this.cwd, argvArg); const success = await compiler.checkTypes(allFiles); process.exit(success ? 0 : 1); }); } /** * Run default type checks for ts/ and test/ directories */ private async runDefaultTypeChecks(argvArg: any): Promise { console.log('\nšŸ”¬ Running default type checking sequence...\n'); // First check ts/**/* without skiplibcheck console.log('šŸ“‚ 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/`); 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/**/*'); process.exit(1); } console.log('āœ… Type checking passed for ts/**/*\n'); } else { console.log(' No TypeScript files found in ts/\n'); } // Then check test/**/* with skiplibcheck console.log('šŸ“‚ 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/`); const testAbsoluteFiles = smartpath.transform.toAbsolute(testTsFiles, this.cwd) as string[]; const testArgvArg = { ...argvArg, skiplibcheck: true }; const testCompiler = new TsCompiler(this.cwd, testArgvArg); const testSuccess = await testCompiler.checkTypes(testAbsoluteFiles); if (!testSuccess) { console.error('āŒ Type checking failed for test/**/*'); process.exit(1); } console.log('āœ… Type checking passed for test/**/*\n'); } else { console.log(' No TypeScript files found in test/\n'); } console.log('āœ… All default type checks passed!\n'); process.exit(0); } /** * Collect files from patterns (glob or direct paths) */ private async collectFilesFromPatterns(patterns: string[]): Promise { let allFiles: string[] = []; for (const pattern of patterns) { if (pattern.includes('*') || pattern.includes('{') || pattern.includes('?')) { // Handle as glob pattern console.log(`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}'`); } else { console.log(`šŸ“‚ Found ${stringMatchedFiles.length} files matching pattern '${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}`); } } else { // Handle as direct file path const filePath = path.isAbsolute(pattern) ? pattern : path.join(this.cwd, pattern); const fileExists = await FsHelpers.fileExists(filePath); if (fileExists) { allFiles.push(filePath); } else { console.error(`āŒ Error: File not found: ${filePath}`); process.exit(1); } } } // Filter to only TypeScript files return allFiles.filter((file) => file.endsWith('.ts') || file.endsWith('.tsx')); } /** * Start parsing CLI arguments */ public run(): void { this.cli.startParse(); } } /** * Run the CLI */ export const runCli = async (): Promise => { const cli = new TsBuildCli(); cli.run(); };