BREAKING(structure): modernize internal structure and support unpacking
This commit is contained in:
320
ts/mod_cli/classes.tsbuildcli.ts
Normal file
320
ts/mod_cli/classes.tsbuildcli.ts
Normal file
@@ -0,0 +1,320 @@
|
||||
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<string, string> = {};
|
||||
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 <file_or_glob_pattern> [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<string, string> = {};
|
||||
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<void> {
|
||||
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<string[]> {
|
||||
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<void> => {
|
||||
const cli = new TsBuildCli();
|
||||
cli.run();
|
||||
};
|
||||
Reference in New Issue
Block a user