From e5fcbb9a097d557539a5431614fb5b685c6486c3 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sat, 13 Dec 2025 22:59:58 +0000 Subject: [PATCH] BREAKING(structure): modernize internal structure and support unpacking --- readme.hints.md | 226 +++++---- test/test.ts | 16 +- ts/index.ts | 13 +- ts/mod_cli/classes.tsbuildcli.ts | 320 ++++++++++++ ts/mod_cli/index.ts | 1 + ts/mod_compiler/classes.tscompiler.ts | 536 ++++++++++++++++++++ ts/mod_compiler/index.ts | 1 + ts/mod_config/classes.tsconfig.ts | 180 +++++++ ts/mod_config/classes.tspublishconfig.ts | 116 +++++ ts/mod_config/index.ts | 2 + ts/mod_fs/classes.fshelpers.ts | 144 ++++++ ts/mod_fs/index.ts | 1 + ts/mod_unpack/classes.tsunpacker.ts | 153 ++++++ ts/mod_unpack/index.ts | 1 + ts/paths.ts | 4 - ts/plugins.ts | 70 +-- ts/tsbuild.classes.tsbuild.ts | 594 ----------------------- ts/tsbuild.cli.ts | 327 ------------- ts/tsbuild.exports.ts | 218 --------- 19 files changed, 1605 insertions(+), 1318 deletions(-) create mode 100644 ts/mod_cli/classes.tsbuildcli.ts create mode 100644 ts/mod_cli/index.ts create mode 100644 ts/mod_compiler/classes.tscompiler.ts create mode 100644 ts/mod_compiler/index.ts create mode 100644 ts/mod_config/classes.tsconfig.ts create mode 100644 ts/mod_config/classes.tspublishconfig.ts create mode 100644 ts/mod_config/index.ts create mode 100644 ts/mod_fs/classes.fshelpers.ts create mode 100644 ts/mod_fs/index.ts create mode 100644 ts/mod_unpack/classes.tsunpacker.ts create mode 100644 ts/mod_unpack/index.ts delete mode 100644 ts/paths.ts delete mode 100644 ts/tsbuild.classes.tsbuild.ts delete mode 100644 ts/tsbuild.cli.ts delete mode 100644 ts/tsbuild.exports.ts diff --git a/readme.hints.md b/readme.hints.md index 4541c3e..6839e89 100644 --- a/readme.hints.md +++ b/readme.hints.md @@ -2,15 +2,13 @@ ## Quick Reference -### Public API (8 functions + 1 class) -1. **compileFileArray()** - Basic compilation, throws on error -2. **compileFileArrayWithErrorTracking()** - RECOMMENDED, returns IErrorSummary -3. **compileGlobStringObject()** - Most powerful, multiple patterns -4. **TsBuild Class** - Object-oriented API with compile, checkTypes, checkEmit methods -5. **mergeCompilerOptions()** - Utility for option merging -6. **compiler()** - Legacy function -7. **emitCheck()** - Validate emit capability -8. **checkTypes()** - Type checking only +### Public API - Classes +1. **TsCompiler** - Main compilation class with compileFiles, compileGlob, checkTypes, checkEmit methods +2. **TsConfig** - TypeScript configuration management (tsconfig.json handling) +3. **TsPublishConfig** - TsPublish configuration (tspublish.json handling) +4. **TsUnpacker** - Output directory flattening +5. **FsHelpers** - Filesystem utilities (static methods) +6. **TsBuildCli** - CLI command handler ### CLI Commands (5) 1. **tsbuild** (default) - Compiles ./ts/**/*.ts → ./dist_ts/ @@ -27,10 +25,71 @@ - `--quiet` - Suppress non-error output - `--json` - JSON output format -## Key Architecture Decisions +## Architecture - Modular Structure -### Configuration Priority (5 levels) -1. Default options (hardcoded) +### Module Organization +``` +ts/ + index.ts # Main entry, re-exports all modules + plugins.ts # Dependency imports only + + mod_fs/ + index.ts # exports + classes.fshelpers.ts # FsHelpers - static filesystem utilities + + mod_config/ + index.ts # exports + classes.tsconfig.ts # TsConfig - tsconfig.json handling + classes.tspublishconfig.ts # TsPublishConfig - tspublish.json handling + + mod_unpack/ + index.ts # exports + classes.tsunpacker.ts # TsUnpacker - output flattening + + mod_compiler/ + index.ts # exports + classes.tscompiler.ts # TsCompiler + legacy compatibility functions + + mod_cli/ + index.ts # exports + classes.tsbuildcli.ts # TsBuildCli - CLI command handler +``` + +### Class Responsibilities + +**TsCompiler** (`mod_compiler/classes.tscompiler.ts`) +- Core compilation with `compileFiles()`, `compileGlob()` +- Type checking with `checkTypes()`, `checkEmit()` +- Configuration via TsConfig +- Automatic unpacking via TsUnpacker + +**TsConfig** (`mod_config/classes.tsconfig.ts`) +- Load and parse tsconfig.json +- Merge configuration with priority order +- Protected defaults handling + +**TsPublishConfig** (`mod_config/classes.tspublishconfig.ts`) +- Load and parse tspublish.json +- `shouldUnpack` property (default true) +- `order` property for tsfolders ordering + +**TsUnpacker** (`mod_unpack/classes.tsunpacker.ts`) +- Detect nested output structure +- Flatten output directories +- Configurable via TsPublishConfig + +**FsHelpers** (`mod_fs/classes.fshelpers.ts`) +- `listFilesWithGlob()` - Glob pattern file listing +- `extractSourceFolder()` - Pattern parsing +- File/directory existence checks +- Directory operations + +**TsBuildCli** (`mod_cli/classes.tsbuildcli.ts`) +- All CLI commands registered +- Uses TsCompiler for compilation + +## Configuration Priority (5 levels) +1. Default options (hardcoded in TsConfig) 2. tsconfig.json (if exists) 3. Protected defaults (ensure integrity) 4. Programmatic options (function params) @@ -41,7 +100,6 @@ Cannot be overridden by tsconfig.json alone: - `outDir: 'dist_ts/'` - Path transformation logic - `noEmitOnError: true` - Build integrity - `declaration: true` - Library support -- `emitDecoratorMetadata: true` - DI frameworks - `inlineSourceMap: true` - Debugging ### Path Transformation @@ -51,12 +109,12 @@ Cannot be overridden by tsconfig.json alone: ## Default Compiler Options - Module: NodeNext (ESM with CommonJS fallback) - Target: ESNext (latest JavaScript) -- Decorators: ENABLED (experimentalDecorators + emitDecoratorMetadata) - Source Maps: Inline (no separate .map files) - Declaration Files: ALWAYS generated (protected) - Output: dist_ts/ - Implicit any: ALLOWED by default - esModuleInterop: true +- verbatimModuleSyntax: true ## Error Handling @@ -70,35 +128,59 @@ interface IErrorSummary { } ``` -### Three Error Patterns -1. **Throw Pattern** - compileFileArray: throws on error -2. **Tracking Pattern** - compileFileArrayWithErrorTracking: returns IErrorSummary, NO throw -3. **Boolean Pattern** - checkTypes/emitCheck: returns boolean - -RECOMMENDATION: Use compileFileArrayWithErrorTracking for production code - -## JSON Output Format -```json -{ - "success": boolean, - "totals": { - "errors": number, - "filesWithErrors": number, - "tasks": number - }, - "errorsByFile": { - "fileName": [ - { "code": number, "message": string } - ] - } +### Compile Result Structure +```typescript +interface ICompileResult { + emittedFiles: string[] + errorSummary: IErrorSummary } ``` +## Dependencies Used +- @git.zone/tspublish@^1.10.3 - Module ordering +- @push.rocks/smartcli@^4.0.19 - CLI framework +- @push.rocks/smartfile@^13.1.2 - File content handling +- @push.rocks/smartfs@^1.2.0 - Filesystem operations +- @push.rocks/smartpath@^6.0.0 - Path transformation utilities +- @push.rocks/smartpromise@^4.2.3 - Promise utilities +- @push.rocks/smartdelay@^3.0.5 - Delay utilities +- typescript@5.9.3 - TypeScript compiler + +### smartfs Usage +- File listing: `smartfs.directory(path).recursive().filter(pattern).list()` +- File existence: `smartfs.file(path).exists()` +- Directory existence: `smartfs.directory(path).exists()` +- FsHelpers wraps smartfs for glob pattern support + +## Unpack Feature + +When TypeScript compiles files that import from sibling directories, it creates nested output: +``` +dist_ts_core/ + ts_core/ ← nested output + ts_shared/ ← pulled-in dependency +``` + +The unpack feature automatically flattens this to: +``` +dist_ts_core/ + index.js ← flat +``` + +### Configuration +- Reads `tspublish.json` from source folder +- `unpack: true` (default if not present) → flatten output +- `unpack: false` → skip unpacking + +### Implementation +- `TsUnpacker` class in `mod_unpack/classes.tsunpacker.ts` +- Called automatically after each successful compilation in `TsCompiler.compileGlob()` + ## Special Behaviors ### tsfolders Command Ordering -1. Always: ts_interfaces first -2. Always: ts_shared second +1. Always: ts_interfaces first (if no tspublish.json) +2. Always: ts_shared second (if no tspublish.json) 3. Then: Other folders by `order` property in their tspublish.json 4. Finally: Folders without order property (Infinity) @@ -107,48 +189,12 @@ Two-phase check: 1. Phase 1: Type check ts/**/* (strict, include .d.ts) 2. Phase 2: Type check test/**/* (relaxed, skipLibCheck: true) -### Glob Pattern Support -- `*` single level -- `**` recursive -- `?` single char -- `{a,b}` alternation -- Duplicates: Files matching multiple patterns compile multiple times - -### Task Information Display -When compiling multiple files with taskInfo param: -Shows: `[1/3] Compiling 45 files from ./src/**/*.ts` -Plus: File counts, duration, and file type breakdown - -## File Structure -- **index.ts** - Main entry, re-exports all -- **tsbuild.exports.ts** - Core API functions -- **tsbuild.classes.tsbuild.ts** - TsBuild class + utility functions -- **tsbuild.cli.ts** - CLI command definitions -- **plugins.ts** - Dependency imports (smartfile, smartpath, smartcli, etc.) -- **paths.ts** - Path utilities (cwd, packageDir) - -## Dependencies Used -- @git.zone/tspublish@^1.10.3 - Module ordering -- @push.rocks/smartcli@^4.0.19 - CLI framework -- @push.rocks/smartfile@^13.1.2 - File content handling (SmartFile, StreamFile, VirtualDirectory) -- @push.rocks/smartfs@^1.2.0 - Filesystem operations (file listing, directory listing, etc.) -- @push.rocks/smartpath@^6.0.0 - Path transformation utilities -- @push.rocks/smartpromise@^4.2.3 - Promise utilities -- @push.rocks/smartdelay@^3.0.5 - Delay utilities -- typescript@5.9.3 - TypeScript compiler - -### smartfs Migration Notes -- smartfile v13+ split filesystem operations to a separate @push.rocks/smartfs package -- File listing uses smartfs fluent API: `smartfs.directory(path).recursive().filter(pattern).list()` -- File existence checks use: `smartfs.file(path).exists()` -- The `listFilesWithGlob()` helper in plugins.ts handles glob pattern parsing for smartfs - ## Edge Cases 1. **Empty file list** - Returns [], no error -2. **Glob duplicates** - Files compile multiple times, possible duplicate errors -3. **Non-existent files** - Handled by TypeScript "file not found" errors -4. **skipLibCheck warning** - 1-line default, 5-second pause with --confirmskiplibcheck +2. **Glob duplicates** - Files compile multiple times +3. **Non-existent files** - TypeScript "file not found" errors +4. **skipLibCheck warning** - 1-line default, 5s pause with --confirmskiplibcheck 5. **Missing tsconfig.json** - Graceful fallback, no error 6. **Module resolution** - --commonjs switches to NodeJs (not NodeNext) 7. **Source maps** - Inline only (not separate .map files) @@ -161,27 +207,9 @@ Plus: File counts, duration, and file type breakdown - Pre-emit checks before emit phase - CLI exit code handling (0=success, 1=error) -## Recent Changes (from git log) -- 3.1.3 - Current version +## Recent Changes +- 3.1.4+ - Major restructuring with mod_* modules +- Full OO architecture with TsCompiler, TsConfig, TsPublishConfig, TsUnpacker, TsBuildCli, FsHelpers +- Backward compatibility maintained for legacy functions +- Automatic "unpack" feature for nested output directories - Migrated filesystem operations from smartfile to smartfs -- Updated @git.zone/tsrun to ^2.0.1 -- Updated @git.zone/tstest to ^3.1.3 -- Updated @push.rocks/smartfile to ^13.1.2 -- Added @push.rocks/smartfs@^1.2.0 for filesystem operations -- Updated @types/node to ^25.0.1 - -## Configuration Example (tsconfig.json) -Paths get automatically transformed: -```json -{ - "compilerOptions": { - "paths": { - "@utils/*": ["./ts_utils/*"] // → ["./dist_ts_utils/*"] - } - } -} -``` - -## No ts_* folders found in project root -The project itself doesn't have ts_interfaces, ts_shared, etc. directories. -The tsfolders command is designed for OTHER projects using tsbuild. diff --git a/test/test.ts b/test/test.ts index a434596..7fa32a7 100644 --- a/test/test.ts +++ b/test/test.ts @@ -1,19 +1,21 @@ import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as tsbuild from '../ts/index.js'; +import { TsCompiler } from '../ts/index.js'; -let assetfiles: string[] = ['./test/assets/tocompile.ts', './test/assets/tocompile2.ts']; +const assetfiles: string[] = ['./test/assets/tocompile.ts', './test/assets/tocompile2.ts']; -let assetfiles2 = { +const assetfiles2 = { './test/assets/**/!(*.d.ts|*.js|output)': './test/assets/output', }; -tap.test('should convert files from an array with single files to output', async (tools) => { - tsbuild.compileFileArray(assetfiles, { outDir: './test/assets/output' }); +tap.test('should compile files from an array', async () => { + const compiler = new TsCompiler(); + await compiler.compileFilesOrThrow(assetfiles, { outDir: './test/assets/output' }); }); -tap.test('should convert files from an array with single files to output', async (tools) => { - tsbuild.compileGlobStringObject(assetfiles2); +tap.test('should compile files from glob pattern object', async () => { + const compiler = new TsCompiler(); + await compiler.compileGlob(assetfiles2); }); tap.start(); diff --git a/ts/index.ts b/ts/index.ts index 8372fe9..3704fc5 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -1,5 +1,14 @@ import * as early from '@push.rocks/early'; early.start('tsbuild'); -export * from './tsbuild.exports.js'; -export * from './tsbuild.cli.js'; + +// Export from new modular structure +export * from './mod_fs/index.js'; +export * from './mod_config/index.js'; +export * from './mod_unpack/index.js'; +export * from './mod_compiler/index.js'; +export * from './mod_cli/index.js'; + +// Re-export TypeScript types for convenience +export type { CompilerOptions, ScriptTarget, ModuleKind } from 'typescript'; + early.stop(); diff --git a/ts/mod_cli/classes.tsbuildcli.ts b/ts/mod_cli/classes.tsbuildcli.ts new file mode 100644 index 0000000..bfad526 --- /dev/null +++ b/ts/mod_cli/classes.tsbuildcli.ts @@ -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 = {}; + 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(); +}; diff --git a/ts/mod_cli/index.ts b/ts/mod_cli/index.ts new file mode 100644 index 0000000..4319953 --- /dev/null +++ b/ts/mod_cli/index.ts @@ -0,0 +1 @@ +export * from './classes.tsbuildcli.js'; diff --git a/ts/mod_compiler/classes.tscompiler.ts b/ts/mod_compiler/classes.tscompiler.ts new file mode 100644 index 0000000..6f8fe5c --- /dev/null +++ b/ts/mod_compiler/classes.tscompiler.ts @@ -0,0 +1,536 @@ +import type { CompilerOptions, Diagnostic, Program } from 'typescript'; +import typescript from 'typescript'; +import * as smartdelay from '@push.rocks/smartdelay'; +import * as smartpromise from '@push.rocks/smartpromise'; +import * as smartpath from '@push.rocks/smartpath'; + +import { TsConfig } from '../mod_config/index.js'; +import { FsHelpers } from '../mod_fs/index.js'; +import { performUnpack } from '../mod_unpack/index.js'; + +/** + * Interface for error summary data + */ +export interface IErrorSummary { + errorsByFile: Record; + generalErrors: Diagnostic[]; + totalErrors: number; + totalFiles: number; +} + +/** + * Interface for task information + */ +export interface ITaskInfo { + taskNumber: number; + totalTasks: number; + sourcePattern: string; + destDir: string; + fileCount: number; +} + +/** + * Interface for compilation result + */ +export interface ICompileResult { + emittedFiles: string[]; + errorSummary: IErrorSummary; +} + +/** + * TsCompiler handles TypeScript compilation with error tracking, + * configuration management, and output unpacking. + */ +export class TsCompiler { + private config: TsConfig; + private cwd: string; + private argvArg?: any; + + constructor(cwd: string = process.cwd(), argvArg?: any) { + this.cwd = cwd; + this.config = new TsConfig(cwd); + this.argvArg = argvArg; + } + + /** + * Get the current working directory + */ + public getCwd(): string { + return this.cwd; + } + + /** + * Get the TsConfig instance + */ + public getConfig(): TsConfig { + return this.config; + } + + /** + * Create compiler options by merging defaults, tsconfig.json, and custom options + */ + public createOptions(customOptions: CompilerOptions = {}): CompilerOptions { + return this.config.merge(customOptions, this.argvArg); + } + + /** + * Create a TypeScript program from file names and options + */ + private createProgram(fileNames: string[], options: CompilerOptions): Program { + return typescript.createProgram(fileNames, options); + } + + /** + * Process TypeScript diagnostics and return error summary + */ + private processDiagnostics(diagnostics: readonly Diagnostic[]): IErrorSummary { + const errorsByFile: Record = {}; + const generalErrors: Diagnostic[] = []; + + diagnostics.forEach((diagnostic) => { + if (diagnostic.file) { + const fileName = diagnostic.file.fileName; + if (!errorsByFile[fileName]) { + errorsByFile[fileName] = []; + } + errorsByFile[fileName].push(diagnostic); + } else { + generalErrors.push(diagnostic); + } + }); + + return { + errorsByFile, + generalErrors, + totalErrors: diagnostics.length, + totalFiles: Object.keys(errorsByFile).length, + }; + } + + /** + * Display error summary to console + */ + private displayErrorSummary(errorSummary: IErrorSummary): void { + if (errorSummary.totalErrors === 0) { + return; + } + + const { errorsByFile, generalErrors, totalErrors, totalFiles } = errorSummary; + + // Print error summary header + console.log('\n' + '='.repeat(80)); + console.log( + `❌ Found ${totalErrors} error${totalErrors !== 1 ? 's' : ''} in ${totalFiles} file${totalFiles !== 1 ? 's' : ''}:` + ); + 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', + }; + + // 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}` + ); + console.log('-'.repeat(80)); + + fileErrors.forEach((diagnostic) => { + if (diagnostic.file && diagnostic.start !== undefined) { + const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); + const message = typescript.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); + 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}` + ); + + // 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}`); + } + } catch { + // Failed to get source text, skip showing the code snippet + } + } + }); + }); + + // Print general errors + if (generalErrors.length > 0) { + console.log(`\n${colors.yellow}General Errors:${colors.reset}`); + console.log('-'.repeat(80)); + + 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('\n' + '='.repeat(80) + '\n'); + } + + /** + * Handle skipLibCheck warning display + */ + 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'); + await smartdelay.delayFor(5000); + } else if (!this.argvArg?.quiet && !this.argvArg?.json) { + console.log('⚠️ skipLibCheck enabled; use --confirmskiplibcheck to pause with warning.'); + } + } + + /** + * Compile files with error tracking (returns result instead of throwing) + */ + public async compileFiles( + fileNames: string[], + customOptions: CompilerOptions = {}, + taskInfo?: ITaskInfo + ): Promise { + const options = this.createOptions(customOptions); + + if (options.skipLibCheck) { + 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}`); + } else { + console.log(`🔨 Compiling ${fileNames.length} files...`); + } + + const done = smartpromise.defer(); + const program = this.createProgram(fileNames, options); + + // Check for pre-emit diagnostics first + 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'); + done.resolve({ emittedFiles: [], errorSummary: preEmitErrorSummary }); + return done.promise; + } + + // If no pre-emit errors, proceed with emit + const emitResult = program.emit(); + const emitErrorSummary = this.processDiagnostics(emitResult.diagnostics); + + // Combine error summaries + const combinedErrorSummary: IErrorSummary = { + errorsByFile: { ...preEmitErrorSummary.errorsByFile, ...emitErrorSummary.errorsByFile }, + generalErrors: [...preEmitErrorSummary.generalErrors, ...emitErrorSummary.generalErrors], + totalErrors: preEmitErrorSummary.totalErrors + emitErrorSummary.totalErrors, + totalFiles: Object.keys({ ...preEmitErrorSummary.errorsByFile, ...emitErrorSummary.errorsByFile }).length, + }; + + const exitCode = emitResult.emitSkipped ? 1 : 0; + if (exitCode === 0) { + const endTime = Date.now(); + const duration = endTime - startTime; + + if (taskInfo) { + const { taskNumber, totalTasks } = taskInfo; + console.log(`✅ [${taskNumber}/${totalTasks}] Task completed in ${duration}ms`); + } else { + console.log(`✅ TypeScript emit succeeded! (${duration}ms)`); + } + + // Get count of emitted files by type + const jsFiles = emitResult.emittedFiles?.filter((f) => f.endsWith('.js')).length || 0; + const dtsFiles = emitResult.emittedFiles?.filter((f) => f.endsWith('.d.ts')).length || 0; + 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` + ); + } + + 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'); + done.resolve({ emittedFiles: [], errorSummary: combinedErrorSummary }); + } + + return done.promise; + } + + /** + * Compile files (throws on error) + */ + public async compileFilesOrThrow(fileNames: string[], customOptions: CompilerOptions = {}): Promise { + const result = await this.compileFiles(fileNames, customOptions); + if (result.errorSummary.totalErrors > 0) { + throw new Error('TypeScript compilation failed.'); + } + return result.emittedFiles; + } + + /** + * Compile glob patterns with automatic unpacking + */ + public async compileGlob( + globPatterns: Record, + customOptions: CompilerOptions = {} + ): Promise { + const emittedFiles: string[] = []; + const errorSummaries: IErrorSummary[] = []; + + const totalTasks = Object.keys(globPatterns).length; + let currentTask = 0; + + const isQuiet = this.argvArg?.quiet === true; + const isJson = this.argvArg?.json === true; + + if (!isQuiet && !isJson) { + console.log(`\n👷 TypeScript Compilation Tasks (${totalTasks} task${totalTasks !== 1 ? 's' : ''}):`); + Object.entries(globPatterns).forEach(([source, dest]) => { + console.log(` 📂 ${source} → ${dest}`); + }); + console.log(''); + } + + for (const pattern of Object.keys(globPatterns)) { + const destPath = globPatterns[pattern]; + if (!pattern || !destPath) continue; + + // Get files matching the glob pattern + const files = await FsHelpers.listFilesWithGlob(this.cwd, pattern); + + // Transform to absolute paths + const absoluteFiles = smartpath.transform.toAbsolute(files, this.cwd) as string[]; + + // Get destination directory as absolute path + const destDir = smartpath.transform.toAbsolute(destPath, this.cwd) as string; + + // Update compiler options with the output directory + const options: CompilerOptions = { + ...customOptions, + outDir: destDir, + }; + + currentTask++; + const taskInfo: ITaskInfo = { + taskNumber: currentTask, + totalTasks, + sourcePattern: pattern, + destDir: destPath, + fileCount: absoluteFiles.length, + }; + + const result = await this.compileFiles(absoluteFiles, options, taskInfo); + emittedFiles.push(...result.emittedFiles); + errorSummaries.push(result.errorSummary); + + // Perform unpack if compilation succeeded + if (result.errorSummary.totalErrors === 0) { + await performUnpack(pattern, destDir, this.cwd); + } + } + + // Merge all error summaries + const finalErrorSummary = this.mergeErrorSummaries(errorSummaries); + + // Output summary based on mode + if (isJson) { + const result = { + success: finalErrorSummary.totalErrors === 0, + totals: { + errors: finalErrorSummary.totalErrors, + filesWithErrors: finalErrorSummary.totalFiles, + tasks: totalTasks, + }, + errorsByFile: Object.fromEntries( + Object.entries(finalErrorSummary.errorsByFile).map(([file, diags]) => [ + file, + diags.map((d) => ({ + code: d.code, + message: typescript.flattenDiagnosticMessageText(d.messageText as any, '\n'), + })), + ]) + ), + }; + console.log(JSON.stringify(result)); + } else if (!isQuiet) { + this.displayFinalSummary(finalErrorSummary); + } + + // Attach summary to argvArg for CLI exit behavior + if (this.argvArg && typeof this.argvArg === 'object') { + (this.argvArg as any).__tsbuildFinalErrorSummary = finalErrorSummary; + } + + return { + emittedFiles, + errorSummary: finalErrorSummary, + }; + } + + /** + * Check if files can be emitted without actually emitting + */ + public async checkEmit(fileNames: string[], customOptions: CompilerOptions = {}): Promise { + const options = { ...this.createOptions(customOptions), noEmit: true }; + const fileCount = fileNames.length; + + console.log(`\n🔍 Checking if ${fileCount} file${fileCount !== 1 ? 's' : ''} can be emitted...`); + + const program = this.createProgram(fileNames, options); + + const preEmitDiagnostics = typescript.getPreEmitDiagnostics(program); + const preEmitErrorSummary = this.processDiagnostics(preEmitDiagnostics); + + const emitResult = program.emit(undefined, undefined, undefined, true); + const emitErrorSummary = this.processDiagnostics(emitResult.diagnostics); + + const combinedErrorSummary: IErrorSummary = { + errorsByFile: { ...preEmitErrorSummary.errorsByFile, ...emitErrorSummary.errorsByFile }, + generalErrors: [...preEmitErrorSummary.generalErrors, ...emitErrorSummary.generalErrors], + totalErrors: preEmitErrorSummary.totalErrors + emitErrorSummary.totalErrors, + totalFiles: Object.keys({ ...preEmitErrorSummary.errorsByFile, ...emitErrorSummary.errorsByFile }).length, + }; + + 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`); + } 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'); + } + + return success; + } + + /** + * Check TypeScript files for type errors without emission + */ + public async checkTypes(fileNames: string[], customOptions: CompilerOptions = {}): Promise { + const options = { ...this.createOptions(customOptions), noEmit: true }; + const fileCount = fileNames.length; + + console.log(`\n🔍 Type checking ${fileCount} TypeScript file${fileCount !== 1 ? 's' : ''}...`); + + const program = this.createProgram(fileNames, options); + const diagnostics = typescript.getPreEmitDiagnostics(program); + const errorSummary = this.processDiagnostics(diagnostics); + + 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`); + } 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'); + } + + return success; + } + + /** + * Merge multiple error summaries into one + */ + private mergeErrorSummaries(summaries: IErrorSummary[]): IErrorSummary { + const mergedErrorsByFile: Record = {}; + const mergedGeneralErrors: Diagnostic[] = []; + let totalErrors = 0; + + summaries.forEach((summary) => { + Object.entries(summary.errorsByFile).forEach(([fileName, errors]) => { + if (!mergedErrorsByFile[fileName]) { + mergedErrorsByFile[fileName] = []; + } + mergedErrorsByFile[fileName] = mergedErrorsByFile[fileName].concat(errors); + }); + + mergedGeneralErrors.push(...summary.generalErrors); + totalErrors += summary.totalErrors; + }); + + return { + errorsByFile: mergedErrorsByFile, + generalErrors: mergedGeneralErrors, + totalErrors, + totalFiles: Object.keys(mergedErrorsByFile).length, + }; + } + + /** + * Display final compilation summary + */ + private displayFinalSummary(errorSummary: IErrorSummary): void { + if (errorSummary.totalErrors === 0) { + console.log('\n📊 \x1b[32mCompilation Summary: All tasks completed successfully! ✅\x1b[0m\n'); + 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}`); + } + + console.log( + `\n${colors.brightRed}Total: ${errorSummary.totalErrors} error${errorSummary.totalErrors !== 1 ? 's' : ''} across ${errorSummary.totalFiles} file${errorSummary.totalFiles !== 1 ? 's' : ''}${colors.reset}` + ); + console.log('='.repeat(80) + '\n'); + } +} diff --git a/ts/mod_compiler/index.ts b/ts/mod_compiler/index.ts new file mode 100644 index 0000000..6b492c7 --- /dev/null +++ b/ts/mod_compiler/index.ts @@ -0,0 +1 @@ +export * from './classes.tscompiler.js'; diff --git a/ts/mod_config/classes.tsconfig.ts b/ts/mod_config/classes.tsconfig.ts new file mode 100644 index 0000000..37a8597 --- /dev/null +++ b/ts/mod_config/classes.tsconfig.ts @@ -0,0 +1,180 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import type { CompilerOptions } from 'typescript'; +import typescript from 'typescript'; + +/** + * Default compiler options for TypeScript compilation + */ +export const compilerOptionsDefault: CompilerOptions = { + declaration: true, + inlineSourceMap: true, + noEmitOnError: true, + outDir: 'dist_ts/', + module: typescript.ModuleKind.NodeNext, + target: typescript.ScriptTarget.ESNext, + moduleResolution: typescript.ModuleResolutionKind.NodeNext, + lib: ['lib.dom.d.ts', 'lib.esnext.d.ts'], + noImplicitAny: false, + esModuleInterop: true, + verbatimModuleSyntax: true, + baseUrl: './', +}; + +/** + * TsConfig handles loading and merging TypeScript compiler configurations. + * It supports reading tsconfig.json and merging with defaults and custom options. + */ +export class TsConfig { + private cwd: string; + private cachedTsConfig: CompilerOptions | null = null; + + constructor(cwd: string = process.cwd()) { + this.cwd = cwd; + } + + /** + * Get the current working directory + */ + public getCwd(): string { + return this.cwd; + } + + /** + * Load and parse tsconfig.json from the current directory + */ + public load(): CompilerOptions { + if (this.cachedTsConfig !== null) { + return this.cachedTsConfig; + } + + let tsconfig: any; + + try { + const tsconfigPath = path.join(this.cwd, 'tsconfig.json'); + const tsconfigContent = fs.readFileSync(tsconfigPath, 'utf8'); + tsconfig = JSON.parse(tsconfigContent); + } catch { + this.cachedTsConfig = {}; + return {}; + } + + if (!tsconfig || !tsconfig.compilerOptions) { + this.cachedTsConfig = {}; + return {}; + } + + const returnObject: CompilerOptions = { ...tsconfig.compilerOptions }; + + // Convert target string to enum + if (tsconfig.compilerOptions.target && typeof tsconfig.compilerOptions.target === 'string') { + const targetKey = tsconfig.compilerOptions.target.toUpperCase(); + if (targetKey in typescript.ScriptTarget) { + returnObject.target = typescript.ScriptTarget[targetKey as keyof typeof typescript.ScriptTarget]; + } + } + + // Convert module string to enum + if (tsconfig.compilerOptions.module && typeof tsconfig.compilerOptions.module === 'string') { + const moduleKey = tsconfig.compilerOptions.module.toUpperCase(); + if (moduleKey in typescript.ModuleKind) { + returnObject.module = typescript.ModuleKind[moduleKey as keyof typeof typescript.ModuleKind]; + } else if (moduleKey === 'NODENEXT') { + returnObject.module = typescript.ModuleKind.NodeNext; + } + } + + // Convert moduleResolution string to enum + if (tsconfig.compilerOptions.moduleResolution && typeof tsconfig.compilerOptions.moduleResolution === 'string') { + const moduleResolutionKey = tsconfig.compilerOptions.moduleResolution.toUpperCase(); + if (moduleResolutionKey in typescript.ModuleResolutionKind) { + returnObject.moduleResolution = typescript.ModuleResolutionKind[ + moduleResolutionKey as keyof typeof typescript.ModuleResolutionKind + ]; + } else if (moduleResolutionKey === 'NODENEXT') { + returnObject.moduleResolution = typescript.ModuleResolutionKind.NodeNext; + } + } + + // Apply path transformations (ts_ → dist_ts_) + if (tsconfig.compilerOptions.paths) { + returnObject.paths = { ...tsconfig.compilerOptions.paths }; + for (const pathKey of Object.keys(returnObject.paths)) { + if (Array.isArray(returnObject.paths[pathKey]) && returnObject.paths[pathKey].length > 0) { + returnObject.paths[pathKey][0] = returnObject.paths[pathKey][0].replace('./ts_', './dist_ts_'); + } + } + } + + this.cachedTsConfig = returnObject; + return returnObject; + } + + /** + * Get default compiler options + */ + public getDefaultOptions(): CompilerOptions { + return { ...compilerOptionsDefault }; + } + + /** + * Get critical/protected default options that shouldn't be overridden by tsconfig.json + */ + public getProtectedDefaults(): CompilerOptions { + return { + outDir: 'dist_ts/', + noEmitOnError: true, + declaration: true, + inlineSourceMap: true, + }; + } + + /** + * Process command line arguments and return applicable compiler options + */ + public getCommandLineOptions(argvArg?: any): CompilerOptions { + if (!argvArg) return {}; + + const options: CompilerOptions = {}; + + if (argvArg.skiplibcheck) { + options.skipLibCheck = true; + } + + if (argvArg.disallowimplicitany) { + options.noImplicitAny = true; + } + + if (argvArg.commonjs) { + options.module = typescript.ModuleKind.CommonJS; + options.moduleResolution = typescript.ModuleResolutionKind.NodeJs; + } + + return options; + } + + /** + * Merge compiler options with proper priority order: + * 1. Default options + * 2. tsconfig.json options + * 3. Protected defaults (cannot be overridden by tsconfig) + * 4. Custom options (programmatic) + * 5. CLI options (highest priority) + */ + public merge(customOptions: CompilerOptions = {}, argvArg?: any): CompilerOptions { + return { + ...this.getDefaultOptions(), + ...this.load(), + ...this.getProtectedDefaults(), + ...customOptions, + ...this.getCommandLineOptions(argvArg), + }; + } + + /** + * Clear the cached tsconfig (useful for reloading) + */ + public clearCache(): void { + this.cachedTsConfig = null; + } +} diff --git a/ts/mod_config/classes.tspublishconfig.ts b/ts/mod_config/classes.tspublishconfig.ts new file mode 100644 index 0000000..6a53d3d --- /dev/null +++ b/ts/mod_config/classes.tspublishconfig.ts @@ -0,0 +1,116 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +/** + * Interface for tspublish.json configuration + */ +export interface ITsPublishJson { + order?: number; + unpack?: boolean; + [key: string]: any; +} + +/** + * TsPublishConfig handles loading and parsing tspublish.json files. + * These configuration files control module-specific settings like + * compilation order and output unpacking behavior. + */ +export class TsPublishConfig { + private folderPath: string; + private cachedConfig: ITsPublishJson | null | undefined = undefined; + + constructor(folderPath: string) { + this.folderPath = folderPath; + } + + /** + * Get the folder path this config is for + */ + public getFolderPath(): string { + return this.folderPath; + } + + /** + * Load and parse tspublish.json from the folder + * Returns null if file doesn't exist or is invalid + */ + public async load(): Promise { + if (this.cachedConfig !== undefined) { + return this.cachedConfig; + } + + try { + const configPath = path.join(this.folderPath, 'tspublish.json'); + const content = await fs.promises.readFile(configPath, 'utf8'); + this.cachedConfig = JSON.parse(content); + return this.cachedConfig; + } catch { + this.cachedConfig = null; + return null; + } + } + + /** + * Synchronously load and parse tspublish.json from the folder + * Returns null if file doesn't exist or is invalid + */ + public loadSync(): ITsPublishJson | null { + if (this.cachedConfig !== undefined) { + return this.cachedConfig; + } + + try { + const configPath = path.join(this.folderPath, 'tspublish.json'); + const content = fs.readFileSync(configPath, 'utf8'); + this.cachedConfig = JSON.parse(content); + return this.cachedConfig; + } catch { + this.cachedConfig = null; + return null; + } + } + + /** + * Check if output should be unpacked (flattened) + * Default is true if not specified + */ + public get shouldUnpack(): boolean { + const config = this.loadSync(); + if (!config || config.unpack === undefined) { + return true; // Default to true + } + return config.unpack === true; + } + + /** + * Get the compilation order for tsfolders command + * Returns Infinity if not specified (sorted last) + */ + public get order(): number { + const config = this.loadSync(); + if (!config || config.order === undefined) { + return Infinity; + } + return config.order; + } + + /** + * Check if tspublish.json exists in the folder + */ + public async exists(): Promise { + try { + const configPath = path.join(this.folderPath, 'tspublish.json'); + await fs.promises.access(configPath, fs.constants.F_OK); + return true; + } catch { + return false; + } + } + + /** + * Clear the cached config (useful for reloading) + */ + public clearCache(): void { + this.cachedConfig = undefined; + } +} diff --git a/ts/mod_config/index.ts b/ts/mod_config/index.ts new file mode 100644 index 0000000..6a7be47 --- /dev/null +++ b/ts/mod_config/index.ts @@ -0,0 +1,2 @@ +export * from './classes.tsconfig.js'; +export * from './classes.tspublishconfig.js'; diff --git a/ts/mod_fs/classes.fshelpers.ts b/ts/mod_fs/classes.fshelpers.ts new file mode 100644 index 0000000..eeb4126 --- /dev/null +++ b/ts/mod_fs/classes.fshelpers.ts @@ -0,0 +1,144 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as smartfsModule from '@push.rocks/smartfs'; + +// Create a smartfs instance with Node.js provider +const smartfs = new smartfsModule.SmartFs(new smartfsModule.SmartFsProviderNode()); + +/** + * FsHelpers provides filesystem utility methods for tsbuild. + * All methods are static for convenience. + */ +export class FsHelpers { + /** + * The smartfs instance for filesystem operations + */ + public static readonly smartfs = smartfs; + + /** + * List files matching a glob pattern like './ts/**\/*.ts' + * Parses the pattern to extract base directory and filter pattern + */ + public static async listFilesWithGlob(basePath: string, globPattern: string): Promise { + // Remove leading ./ if present + const pattern = globPattern.replace(/^\.\//, ''); + + // Find the first directory part before any glob characters + const globChars = ['*', '?', '{', '[']; + let baseDir = basePath; + + // Find where the glob pattern starts + const parts = pattern.split('/'); + const staticParts: string[] = []; + const filterParts: string[] = []; + let foundGlob = false; + + for (const part of parts) { + if (!foundGlob && !globChars.some(c => part.includes(c))) { + staticParts.push(part); + } else { + foundGlob = true; + filterParts.push(part); + } + } + + // Build the base directory + if (staticParts.length > 0) { + baseDir = path.join(basePath, ...staticParts); + } + + // Build the filter pattern (just the filename part, ignoring ** for directories) + // The recursive() handles the ** part + const fileFilter = filterParts[filterParts.length - 1] || '*'; + + // Check if we need recursive search + const needsRecursive = filterParts.some(p => p === '**' || p.includes('**')); + + let dirBuilder = smartfs.directory(baseDir); + if (needsRecursive) { + dirBuilder = dirBuilder.recursive(); + } + + try { + const entries = await dirBuilder.filter(fileFilter).list(); + return entries.filter(e => e.isFile).map(e => e.path); + } catch { + // Directory doesn't exist or other error + return []; + } + } + + /** + * Extract source folder name from a glob pattern + * './ts_core/**\/*.ts' → 'ts_core' + * 'ts_foo/**\/*.ts' → 'ts_foo' + */ + public static extractSourceFolder(pattern: string): string | null { + const match = pattern.match(/^\.?\/?([^\/\*]+)/); + return match ? match[1] : null; + } + + /** + * Get the current working directory + */ + public static getCwd(): string { + return process.cwd(); + } + + /** + * Get the package directory (where package.json is located) + */ + public static getPackageDir(): string { + return path.resolve(__dirname, '../../'); + } + + /** + * Check if a file exists + */ + public static async fileExists(filePath: string): Promise { + return smartfs.file(filePath).exists(); + } + + /** + * Check if a directory exists + */ + public static async directoryExists(dirPath: string): Promise { + return smartfs.directory(dirPath).exists(); + } + + /** + * Read a JSON file and parse it + */ + public static readJsonSync(filePath: string): T { + const content = fs.readFileSync(filePath, 'utf8'); + return JSON.parse(content); + } + + /** + * List directory contents + */ + public static async listDirectory(dirPath: string) { + return smartfs.directory(dirPath).list(); + } + + /** + * Remove a directory recursively + */ + public static async removeDirectory(dirPath: string): Promise { + await fs.promises.rm(dirPath, { recursive: true }); + } + + /** + * Move/rename a file or directory + */ + public static async move(src: string, dest: string): Promise { + await fs.promises.rename(src, dest); + } + + /** + * Remove an empty directory + */ + public static async removeEmptyDirectory(dirPath: string): Promise { + await fs.promises.rmdir(dirPath); + } +} diff --git a/ts/mod_fs/index.ts b/ts/mod_fs/index.ts new file mode 100644 index 0000000..85f96d3 --- /dev/null +++ b/ts/mod_fs/index.ts @@ -0,0 +1 @@ +export * from './classes.fshelpers.js'; diff --git a/ts/mod_unpack/classes.tsunpacker.ts b/ts/mod_unpack/classes.tsunpacker.ts new file mode 100644 index 0000000..17aa663 --- /dev/null +++ b/ts/mod_unpack/classes.tsunpacker.ts @@ -0,0 +1,153 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { TsPublishConfig } from '../mod_config/index.js'; +import { FsHelpers } from '../mod_fs/index.js'; + +/** + * TsUnpacker handles flattening of nested TypeScript output directories. + * + * When TypeScript compiles files that import from sibling directories, + * it creates a nested structure like: + * dist_ts_core/ts_core/index.js + * dist_ts_core/ts_shared/helper.js + * + * This class flattens it to: + * dist_ts_core/index.js + */ +export class TsUnpacker { + private sourceFolderName: string; + private destDir: string; + private cwd: string; + private config: TsPublishConfig; + + constructor(sourceFolderName: string, destDir: string, cwd: string = process.cwd()) { + this.sourceFolderName = sourceFolderName; + this.destDir = destDir; + this.cwd = cwd; + this.config = new TsPublishConfig(path.join(cwd, sourceFolderName)); + } + + /** + * Create an unpacker from a glob pattern + * './ts_core/**\/*.ts' → sourceFolderName = 'ts_core' + */ + public static fromGlobPattern( + sourcePattern: string, + destDir: string, + cwd: string = process.cwd() + ): TsUnpacker | null { + const sourceFolderName = FsHelpers.extractSourceFolder(sourcePattern); + if (!sourceFolderName) { + return null; + } + return new TsUnpacker(sourceFolderName, destDir, cwd); + } + + /** + * Get the source folder name + */ + public getSourceFolderName(): string { + return this.sourceFolderName; + } + + /** + * Get the destination directory + */ + public getDestDir(): string { + return this.destDir; + } + + /** + * Check if unpacking should be performed based on tspublish.json config + * Default is true if not specified + */ + public async shouldUnpack(): Promise { + return this.config.shouldUnpack; + } + + /** + * Check if nested structure exists in the destination directory + */ + public async detectNesting(): Promise { + const nestedPath = path.join(this.destDir, this.sourceFolderName); + return FsHelpers.directoryExists(nestedPath); + } + + /** + * Get the path to the nested directory + */ + public getNestedPath(): string { + return path.join(this.destDir, this.sourceFolderName); + } + + /** + * Perform the unpack operation - flatten nested output directories + * Returns true if unpacking was performed, false if skipped + */ + public async unpack(): Promise { + // Check if we should unpack based on config + if (!(await this.shouldUnpack())) { + return false; + } + + // Check if nested structure exists + if (!(await this.detectNesting())) { + return false; + } + + const nestedPath = this.getNestedPath(); + + // Delete sibling folders (not the source folder) + await this.removeSiblingDirectories(); + + // Move contents from nested folder up + await this.moveNestedContentsUp(); + + // Remove empty nested folder + await FsHelpers.removeEmptyDirectory(nestedPath); + + return true; + } + + /** + * Remove sibling directories in the destination folder + * (directories other than the source folder being unpacked) + */ + private async removeSiblingDirectories(): Promise { + const entries = await FsHelpers.listDirectory(this.destDir); + for (const entry of entries) { + if (entry.isDirectory && entry.name !== this.sourceFolderName) { + await FsHelpers.removeDirectory(path.join(this.destDir, entry.name)); + } + } + } + + /** + * Move contents from the nested folder up to the destination directory + */ + private async moveNestedContentsUp(): Promise { + const nestedPath = this.getNestedPath(); + const entries = await FsHelpers.listDirectory(nestedPath); + for (const entry of entries) { + const src = path.join(nestedPath, entry.name); + const dest = path.join(this.destDir, entry.name); + await FsHelpers.move(src, dest); + } + } +} + +/** + * Convenience function to perform unpack operation + * Can be used directly without instantiating the class + */ +export async function performUnpack( + sourcePattern: string, + destDir: string, + cwd: string = process.cwd() +): Promise { + const unpacker = TsUnpacker.fromGlobPattern(sourcePattern, destDir, cwd); + if (!unpacker) { + return false; + } + return unpacker.unpack(); +} diff --git a/ts/mod_unpack/index.ts b/ts/mod_unpack/index.ts new file mode 100644 index 0000000..20a929c --- /dev/null +++ b/ts/mod_unpack/index.ts @@ -0,0 +1 @@ +export * from './classes.tsunpacker.js'; diff --git a/ts/paths.ts b/ts/paths.ts deleted file mode 100644 index f8ade68..0000000 --- a/ts/paths.ts +++ /dev/null @@ -1,4 +0,0 @@ -import * as plugins from './plugins.js'; - -export const cwd = process.cwd(); -export const packageDir = plugins.path.join(plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url), '../'); diff --git a/ts/plugins.ts b/ts/plugins.ts index fec8f1d..591b3e1 100644 --- a/ts/plugins.ts +++ b/ts/plugins.ts @@ -2,87 +2,23 @@ import * as fs from 'fs'; import * as path from 'path'; -export { - fs, - path -} +export { fs, path }; // @git.zone scope import * as tspublish from '@git.zone/tspublish'; -export { - tspublish -} +export { tspublish }; // @push.rocks scope import * as smartcli from '@push.rocks/smartcli'; import * as smartdelay from '@push.rocks/smartdelay'; import * as smartfile from '@push.rocks/smartfile'; -import * as smartfsModule from '@push.rocks/smartfs'; import * as smartpath from '@push.rocks/smartpath'; import * as smartpromise from '@push.rocks/smartpromise'; -// Create a smartfs instance with Node.js provider -export const smartfs = new smartfsModule.SmartFs(new smartfsModule.SmartFsProviderNode()); - -/** - * Helper to list files matching a glob pattern like './ts/**\/*.ts' - * Parses the pattern to extract base directory and filter pattern - */ -export async function listFilesWithGlob(basePath: string, globPattern: string): Promise { - // Remove leading ./ if present - let pattern = globPattern.replace(/^\.\//, ''); - - // Find the first directory part before any glob characters - const globChars = ['*', '?', '{', '[']; - let baseDir = basePath; - - // Find where the glob pattern starts - const parts = pattern.split('/'); - const staticParts: string[] = []; - let filterParts: string[] = []; - let foundGlob = false; - - for (const part of parts) { - if (!foundGlob && !globChars.some(c => part.includes(c))) { - staticParts.push(part); - } else { - foundGlob = true; - filterParts.push(part); - } - } - - // Build the base directory - if (staticParts.length > 0) { - baseDir = path.join(basePath, ...staticParts); - } - - // Build the filter pattern (just the filename part, ignoring ** for directories) - // The recursive() handles the ** part - const fileFilter = filterParts[filterParts.length - 1] || '*'; - - // Check if we need recursive search - const needsRecursive = filterParts.some(p => p === '**' || p.includes('**')); - - let dirBuilder = smartfs.directory(baseDir); - if (needsRecursive) { - dirBuilder = dirBuilder.recursive(); - } - - try { - const entries = await dirBuilder.filter(fileFilter).list(); - return entries.filter(e => e.isFile).map(e => e.path); - } catch { - // Directory doesn't exist or other error - return []; - } -} - export { smartcli, smartdelay, smartfile, smartpath, smartpromise }; // third party scope import typescript from 'typescript'; -export { - typescript -} \ No newline at end of file +export { typescript }; diff --git a/ts/tsbuild.classes.tsbuild.ts b/ts/tsbuild.classes.tsbuild.ts deleted file mode 100644 index b3a7a4b..0000000 --- a/ts/tsbuild.classes.tsbuild.ts +++ /dev/null @@ -1,594 +0,0 @@ -// import all the stuff we need -import * as plugins from './plugins.js'; -import * as paths from './paths.js'; -import type { CompilerOptions, ScriptTarget, ModuleKind } from './tsbuild.exports.js'; - -/** - * Interface for error summary data - */ -export interface IErrorSummary { - errorsByFile: Record; - generalErrors: plugins.typescript.Diagnostic[]; - totalErrors: number; - totalFiles: number; -} - -/** - * Default compiler options for TypeScript compilation - */ -export const compilerOptionsDefault: CompilerOptions = { - declaration: true, - inlineSourceMap: true, - noEmitOnError: true, - outDir: 'dist_ts/', - module: plugins.typescript.ModuleKind.NodeNext, - target: plugins.typescript.ScriptTarget.ESNext, - moduleResolution: plugins.typescript.ModuleResolutionKind.NodeNext, - lib: ['lib.dom.d.ts', 'lib.esnext.d.ts'], - noImplicitAny: false, // Allow implicit any by default - esModuleInterop: true, - verbatimModuleSyntax: true, - baseUrl: './', -}; - -/** - * TsBuild class for handling TypeScript compilation - */ -export class TsBuild { - private fileNames: string[] = []; - private options: plugins.typescript.CompilerOptions; - private argvArg?: any; - private taskInfo?: any; - - /** - * Create a new TsBuild instance - */ - constructor( - fileNames: string[] = [], - customOptions: CompilerOptions = {}, - argvArg?: any, - taskInfo?: any - ) { - this.fileNames = fileNames; - this.argvArg = argvArg; - this.taskInfo = taskInfo; - this.options = this.mergeCompilerOptions(customOptions, argvArg); - } - - /** - * Helper function to read and process tsconfig.json - */ - private getTsConfigOptions(): CompilerOptions { - let tsconfig: any; - - // Try to read tsconfig.json, but don't fail if it doesn't exist - try { - const tsconfigPath = plugins.path.join(paths.cwd, 'tsconfig.json'); - const tsconfigContent = plugins.fs.readFileSync(tsconfigPath, 'utf8'); - tsconfig = JSON.parse(tsconfigContent); - } catch (error) { - // tsconfig.json doesn't exist or is invalid - use defaults - return {}; - } - - if (!tsconfig || !tsconfig.compilerOptions) { - return {}; - } - - // Start by copying ALL compiler options from tsconfig.json - const returnObject: CompilerOptions = { ...tsconfig.compilerOptions }; - - // Apply special transformations for string-to-enum conversions - - // Process target (convert string to enum) - if (tsconfig.compilerOptions.target && typeof tsconfig.compilerOptions.target === 'string') { - const targetKey = tsconfig.compilerOptions.target.toUpperCase(); - if (targetKey in plugins.typescript.ScriptTarget) { - returnObject.target = plugins.typescript.ScriptTarget[targetKey as keyof typeof plugins.typescript.ScriptTarget]; - } - } - - // Process module (convert string to enum) - if (tsconfig.compilerOptions.module && typeof tsconfig.compilerOptions.module === 'string') { - const moduleKey = tsconfig.compilerOptions.module.toUpperCase(); - if (moduleKey in plugins.typescript.ModuleKind) { - returnObject.module = plugins.typescript.ModuleKind[moduleKey as keyof typeof plugins.typescript.ModuleKind]; - } else if (moduleKey === 'NODENEXT') { - returnObject.module = plugins.typescript.ModuleKind.NodeNext; - } - } - - // Process moduleResolution (convert string to enum) - if (tsconfig.compilerOptions.moduleResolution && typeof tsconfig.compilerOptions.moduleResolution === 'string') { - const moduleResolutionKey = tsconfig.compilerOptions.moduleResolution.toUpperCase(); - if (moduleResolutionKey in plugins.typescript.ModuleResolutionKind) { - returnObject.moduleResolution = plugins.typescript.ModuleResolutionKind[ - moduleResolutionKey as keyof typeof plugins.typescript.ModuleResolutionKind - ]; - } else if (moduleResolutionKey === 'NODENEXT') { - returnObject.moduleResolution = plugins.typescript.ModuleResolutionKind.NodeNext; - } - } - - // Apply path transformations (ts_ → dist_ts_) - if (tsconfig.compilerOptions.paths) { - returnObject.paths = { ...tsconfig.compilerOptions.paths }; - for (const path of Object.keys(returnObject.paths)) { - if (Array.isArray(returnObject.paths[path]) && returnObject.paths[path].length > 0) { - returnObject.paths[path][0] = returnObject.paths[path][0].replace('./ts_', './dist_ts_'); - } - } - } - - return returnObject; - } - - /** - * Returns critical default options that should not be overridden by tsconfig.json - * These options are essential for tsbuild's functionality and build integrity - */ - private getCriticalDefaults(): CompilerOptions { - return { - outDir: 'dist_ts/', // Required for path transformation logic - noEmitOnError: true, // Build integrity - prevent broken builds - declaration: true, // Library consumers depend on .d.ts files - inlineSourceMap: true, // Consistent debugging experience - }; - } - - /** - * Process command line arguments and return applicable compiler options - */ - private getCommandLineOptions(argvArg?: any): CompilerOptions { - if (!argvArg) return {}; - - const options: CompilerOptions = {}; - - if (argvArg.skiplibcheck) { - options.skipLibCheck = true; - } - - // Changed behavior: --disallowimplicitany instead of --allowimplicitany - if (argvArg.disallowimplicitany) { - options.noImplicitAny = true; - } - - if (argvArg.commonjs) { - options.module = plugins.typescript.ModuleKind.CommonJS; - options.moduleResolution = plugins.typescript.ModuleResolutionKind.NodeJs; - } - - return options; - } - - /** - * Merges compilerOptions with the default compiler options - * - * Merge order (later overwrites earlier): - * 1. compilerOptionsDefault - Base defaults for all options - * 2. getTsConfigOptions() - User's tsconfig.json (all options) - * 3. getCriticalDefaults() - Protected options that shouldn't be overridden by tsconfig.json - * 4. customTsOptions - Programmatic options (can override critical defaults) - * 5. getCommandLineOptions() - CLI flags (highest priority) - */ - public mergeCompilerOptions( - customTsOptions: CompilerOptions = {}, - argvArg?: any - ): CompilerOptions { - // create merged options - const mergedOptions: CompilerOptions = { - ...compilerOptionsDefault, // 1. All defaults - ...this.getTsConfigOptions(), // 2. User's tsconfig.json (all options) - ...this.getCriticalDefaults(), // 3. Protected overrides - ...customTsOptions, // 4. Programmatic options - ...this.getCommandLineOptions(argvArg), // 5. CLI flags (highest priority) - }; - - return mergedOptions; - } - - /** - * Helper function to process TypeScript diagnostics and return error summary - */ - private processDiagnostics(diagnostics: readonly plugins.typescript.Diagnostic[]): IErrorSummary { - const errorsByFile: Record = {}; - const generalErrors: plugins.typescript.Diagnostic[] = []; - - // Categorize diagnostics - diagnostics.forEach((diagnostic) => { - if (diagnostic.file) { - const fileName = diagnostic.file.fileName; - if (!errorsByFile[fileName]) { - errorsByFile[fileName] = []; - } - errorsByFile[fileName].push(diagnostic); - } else { - generalErrors.push(diagnostic); - } - }); - - return { - errorsByFile, - generalErrors, - totalErrors: diagnostics.length, - totalFiles: Object.keys(errorsByFile).length - }; - } - - /** - * Helper function to display error summary - */ - private displayErrorSummary(errorSummary: IErrorSummary): void { - if (errorSummary.totalErrors === 0) { - return; - } - - const { errorsByFile, generalErrors, totalErrors, totalFiles } = errorSummary; - - // Print error summary header - console.log('\n' + '='.repeat(80)); - console.log(`❌ Found ${totalErrors} error${totalErrors !== 1 ? 's' : ''} in ${totalFiles} file${totalFiles !== 1 ? 's' : ''}:`); - 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' - }; - - // 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}`); - console.log('-'.repeat(80)); - - fileErrors.forEach((diagnostic) => { - if (diagnostic.file && diagnostic.start !== undefined) { - const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); - const message = plugins.typescript.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); - 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}`); - - // Try to show the code snippet if possible - try { - const lineContent = diagnostic.file.text.split('\n')[line]; - if (lineContent) { - // Show the line of code - console.log(` ${lineContent.trimRight()}`); - - // Show the error position indicator - const indicator = ' '.repeat(character) + `${colors.red}^${colors.reset}`; - console.log(` ${indicator}`); - } - } catch (e) { - // Failed to get source text, skip showing the code snippet - } - } - }); - }); - - // Print general errors - if (generalErrors.length > 0) { - console.log(`\n${colors.yellow}General Errors:${colors.reset}`); - console.log('-'.repeat(80)); - - generalErrors.forEach((diagnostic) => { - const message = plugins.typescript.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); - const errorCode = diagnostic.code ? `TS${diagnostic.code}` : 'Error'; - console.log(`${colors.brightRed}${errorCode}${colors.reset}: ${message}`); - }); - } - - console.log('\n' + '='.repeat(80) + '\n'); - } - - - /** - * Creates a TypeScript program from file names and options - */ - private createProgram( - options: plugins.typescript.CompilerOptions = this.options - ): plugins.typescript.Program { - return plugins.typescript.createProgram(this.fileNames, options); - } - - /** - * Set file names to be compiled - */ - public setFileNames(fileNames: string[]): void { - this.fileNames = fileNames; - } - - /** - * Set compiler options - */ - public setOptions(options: CompilerOptions): void { - this.options = { ...this.options, ...options }; - } - - /** - * The main compiler function that compiles the files and returns error summary - */ - public async compileWithErrorTracking(): Promise<{ emittedFiles: any[], errorSummary: IErrorSummary }> { - if (this.options.skipLibCheck) { - 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'); - await plugins.smartdelay.delayFor(5000); - } else { - // No delay by default; keep a short note unless in quiet/json modes - if (!this.argvArg?.quiet && !this.argvArg?.json) { - console.log('⚠️ skipLibCheck enabled; use --confirmskiplibcheck to pause with warning.'); - } - } - } - - // Enhanced logging with task info - const startTime = Date.now(); - if (this.taskInfo) { - const { taskNumber, totalTasks, sourcePattern, destDir, fileCount } = this.taskInfo; - const relativeDestDir = destDir.replace(process.cwd(), '').replace(/^\//, ''); - console.log(`\n🔨 [${taskNumber}/${totalTasks}] Compiling ${fileCount} file${fileCount !== 1 ? 's' : ''} from ${sourcePattern}`); - console.log(` 📁 Output: ${relativeDestDir}`); - } else { - console.log(`🔨 Compiling ${this.fileNames.length} files...`); - } - - const done = plugins.smartpromise.defer<{ emittedFiles: any[], errorSummary: IErrorSummary }>(); - const program = this.createProgram(); - - // Check for pre-emit diagnostics first - const preEmitDiagnostics = plugins.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'); - // Return error summary instead of exiting to allow final summary display - done.resolve({ emittedFiles: [], errorSummary: preEmitErrorSummary }); - return done.promise; - } - - // If no pre-emit errors, proceed with emit - const emitResult = program.emit(); - const emitErrorSummary = this.processDiagnostics(emitResult.diagnostics); - - // Combine error summaries - const combinedErrorSummary: IErrorSummary = { - errorsByFile: { ...preEmitErrorSummary.errorsByFile, ...emitErrorSummary.errorsByFile }, - generalErrors: [...preEmitErrorSummary.generalErrors, ...emitErrorSummary.generalErrors], - totalErrors: preEmitErrorSummary.totalErrors + emitErrorSummary.totalErrors, - totalFiles: Object.keys({ ...preEmitErrorSummary.errorsByFile, ...emitErrorSummary.errorsByFile }).length - }; - - const exitCode = emitResult.emitSkipped ? 1 : 0; - if (exitCode === 0) { - const endTime = Date.now(); - const duration = endTime - startTime; - - if (this.taskInfo) { - const { taskNumber, totalTasks } = this.taskInfo; - console.log(`✅ [${taskNumber}/${totalTasks}] Task completed in ${duration}ms`); - } else { - console.log(`✅ TypeScript emit succeeded! (${duration}ms)`); - } - - // Get count of emitted files by type - const jsFiles = emitResult.emittedFiles?.filter(f => f.endsWith('.js')).length || 0; - const dtsFiles = emitResult.emittedFiles?.filter(f => f.endsWith('.d.ts')).length || 0; - const mapFiles = emitResult.emittedFiles?.filter(f => f.endsWith('.map')).length || 0; - - // If we have emitted files, show a summary - if (emitResult.emittedFiles && emitResult.emittedFiles.length > 0) { - console.log(` 📄 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'); - // Do not exit here; return error summary so caller can decide - done.resolve({ emittedFiles: [], errorSummary: combinedErrorSummary }); - return done.promise; - } - - return done.promise; - } - - /** - * The main compiler function that compiles the files - */ - public async compile(): Promise { - if (this.options.skipLibCheck) { - 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'); - await plugins.smartdelay.delayFor(5000); - } else { - if (!this.argvArg?.quiet && !this.argvArg?.json) { - console.log('⚠️ skipLibCheck enabled; use --confirmskiplibcheck to pause with warning.'); - } - } - } - - console.log(`🔨 Compiling ${this.fileNames.length} files...`); - const done = plugins.smartpromise.defer(); - const program = this.createProgram(); - - // Check for pre-emit diagnostics first - const preEmitDiagnostics = plugins.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'); - // Throw instead of exiting to keep library pure - throw new Error('TypeScript pre-emit checks failed.'); - } - - // If no pre-emit errors, proceed with emit - const emitResult = program.emit(); - const emitErrorSummary = this.processDiagnostics(emitResult.diagnostics); - - const exitCode = emitResult.emitSkipped ? 1 : 0; - if (exitCode === 0) { - console.log('\n✅ TypeScript emit succeeded!'); - - // Get count of emitted files by type - const jsFiles = emitResult.emittedFiles?.filter(f => f.endsWith('.js')).length || 0; - const dtsFiles = emitResult.emittedFiles?.filter(f => f.endsWith('.d.ts')).length || 0; - const mapFiles = emitResult.emittedFiles?.filter(f => f.endsWith('.map')).length || 0; - - // If we have emitted files, show a summary - if (emitResult.emittedFiles && emitResult.emittedFiles.length > 0) { - console.log(` Generated ${emitResult.emittedFiles.length} files: ${jsFiles} .js, ${dtsFiles} .d.ts, ${mapFiles} source maps`); - } - - done.resolve(emitResult.emittedFiles); - } else { - this.displayErrorSummary(emitErrorSummary); - console.error('\n❌ TypeScript emit failed. Please investigate the errors listed above!'); - console.error(' No output files have been generated.\n'); - // Throw instead of exiting to keep library pure - throw new Error('TypeScript emit failed.'); - } - - return done.promise; - } - - /** - * Function to check if files can be emitted without actually emitting them - */ - public async checkEmit(): Promise { - const fileCount = this.fileNames.length; - console.log(`\n🔍 Checking if ${fileCount} file${fileCount !== 1 ? 's' : ''} can be emitted...`); - - // Create a program with noEmit option - const program = this.createProgram({ - ...this.options, - noEmit: true - }); - - // Check for pre-emit diagnostics - const preEmitDiagnostics = plugins.typescript.getPreEmitDiagnostics(program); - const preEmitErrorSummary = this.processDiagnostics(preEmitDiagnostics); - - // Run the emit phase but with noEmit: true to check for emit errors without producing files - const emitResult = program.emit(undefined, undefined, undefined, true); - const emitErrorSummary = this.processDiagnostics(emitResult.diagnostics); - - // Combine error summaries - const combinedErrorSummary: IErrorSummary = { - errorsByFile: { ...preEmitErrorSummary.errorsByFile, ...emitErrorSummary.errorsByFile }, - generalErrors: [...preEmitErrorSummary.generalErrors, ...emitErrorSummary.generalErrors], - totalErrors: preEmitErrorSummary.totalErrors + emitErrorSummary.totalErrors, - totalFiles: Object.keys({ ...preEmitErrorSummary.errorsByFile, ...emitErrorSummary.errorsByFile }).length - }; - - 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`); - } 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'); - } - - return success; - } - - /** - * Function to check TypeScript files for type errors without emission - */ - public async checkTypes(): Promise { - const fileCount = this.fileNames.length; - console.log(`\n🔍 Type checking ${fileCount} TypeScript file${fileCount !== 1 ? 's' : ''}...`); - - // Create a program with noEmit option explicitly set - const program = this.createProgram({ - ...this.options, - noEmit: true - }); - - // Check for type errors - const diagnostics = plugins.typescript.getPreEmitDiagnostics(program); - const errorSummary = this.processDiagnostics(diagnostics); - - // Set success flag - 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`); - } 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'); - } - - return success; - } -} - -/** - * Merges compilerOptions with the default compiler options (backward compatibility) - */ -export const mergeCompilerOptions = ( - customTsOptions: CompilerOptions, - argvArg?: any -): CompilerOptions => { - const tsBuild = new TsBuild(); - return tsBuild.mergeCompilerOptions(customTsOptions, argvArg); -}; - -/** - * The internal main compiler function that compiles the files (backward compatibility) - */ -export const compiler = async ( - fileNames: string[], - options: plugins.typescript.CompilerOptions, - argvArg?: any -): Promise => { - const tsBuild = new TsBuild(fileNames, options, argvArg); - return tsBuild.compile(); -}; - -/** - * Function to check if a TypeScript file can be emitted without actually emitting it (backward compatibility) - */ -export const emitCheck = async ( - fileNames: string[], - options: plugins.typescript.CompilerOptions = {}, - argvArg?: any -): Promise => { - const tsBuild = new TsBuild(fileNames, options, argvArg); - return tsBuild.checkEmit(); -}; - -/** - * Function to check TypeScript files for type errors without emission (backward compatibility) - */ -export const checkTypes = async ( - fileNames: string[], - options: plugins.typescript.CompilerOptions = {}, - argvArg?: any -): Promise => { - const tsBuild = new TsBuild(fileNames, options, argvArg); - return tsBuild.checkTypes(); -}; diff --git a/ts/tsbuild.cli.ts b/ts/tsbuild.cli.ts deleted file mode 100644 index 5259a82..0000000 --- a/ts/tsbuild.cli.ts +++ /dev/null @@ -1,327 +0,0 @@ -import * as plugins from './plugins.js'; -import * as paths from './paths.js'; -import * as tsbuild from './tsbuild.exports.js'; - -export const runCli = async () => { - const tsbuildCli = new plugins.smartcli.Smartcli(); - - /** - * the standard task compiles anything in ts/ directory to dist directory - */ - tsbuildCli.standardCommand().subscribe(async (argvArg) => { - await tsbuild.compileGlobStringObject( - { - './ts/**/*.ts': './dist_ts', - }, - {}, - process.cwd(), - argvArg - ); - const summary = (argvArg as any)?.__tsbuildFinalErrorSummary; - if (summary && summary.totalErrors > 0) { - process.exit(1); - } - }); - - /** - * the custom command compiles any customDir to dist_customDir - */ - tsbuildCli.addCommand('custom').subscribe(async (argvArg) => { - const listedDirectories = argvArg._; - listedDirectories.shift(); // removes the first element that is "custom" - const compilationCommandObject: { [key: string]: string } = {}; - for (const directory of listedDirectories) { - compilationCommandObject[`./${directory}/**/*.ts`] = `./dist_${directory}`; - } - await tsbuild.compileGlobStringObject(compilationCommandObject, {}, process.cwd(), argvArg); - const summary = (argvArg as any)?.__tsbuildFinalErrorSummary; - if (summary && summary.totalErrors > 0) { - process.exit(1); - } - }); - - /** - * the emitcheck command checks if a TypeScript file can be emitted without actually emitting it - */ - tsbuildCli.addCommand('emitcheck').subscribe(async (argvArg) => { - const patterns = argvArg._.slice(1); // Remove the first element which is 'emitcheck' - - 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 cwd = process.cwd(); - let allFiles: string[] = []; - - // Process each pattern - could be a direct file path or a glob pattern - for (const pattern of patterns) { - // Check if the pattern looks like a glob pattern - if (pattern.includes('*') || pattern.includes('{') || pattern.includes('?')) { - // Handle as glob pattern - console.log(`Processing glob pattern: ${pattern}`); - try { - const stringMatchedFiles = await plugins.listFilesWithGlob(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}'`); - - // Transform to absolute paths - const absoluteMatchedFiles = plugins.smartpath.transform.toAbsolute( - stringMatchedFiles, - cwd - ) as string[]; - - // Add to the list of all files to check - allFiles = allFiles.concat(absoluteMatchedFiles); - } - } catch (err) { - console.error(`❌ Error processing glob pattern '${pattern}': ${err}`); - } - } else { - // Handle as direct file path - const filePath = plugins.path.isAbsolute(pattern) - ? pattern - : plugins.path.join(cwd, pattern); - - const fileExists = await plugins.smartfs.file(filePath).exists(); - if (fileExists) { - allFiles.push(filePath); - } else { - console.error(`❌ Error: File not found: ${filePath}`); - process.exit(1); - } - } - } - - // Filter to only TypeScript files - allFiles = allFiles.filter(file => file.endsWith('.ts') || file.endsWith('.tsx')); - - 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`); - - // Process compiler options - const compilerOptions = tsbuild.mergeCompilerOptions({}, argvArg); - - // Run emit check - const success = await tsbuild.emitCheck(allFiles, compilerOptions, argvArg); - - // Exit with appropriate code - process.exit(success ? 0 : 1); - }); - - /** - * the custom command compiles any customDir to dist_customDir - */ - tsbuildCli.addCommand('tsfolders').subscribe(async (argvArg) => { - // List folders matching /^ts/ regex - const allEntries = await plugins.smartfs.directory(paths.cwd).list(); - const tsFolders = allEntries - .filter(e => e.isDirectory && /^ts/.test(e.name)) - .map(e => e.name); - - // Now tsFolders contains all other folders except 'ts_shared' and 'ts_interfaces' - - // We've established a base order. Now let's look at tspublish.json based ranking. - const tsPublishInstance = new plugins.tspublish.TsPublish(); - const tsPublishModules = await tsPublishInstance.getModuleSubDirs(paths.cwd); - // tsPublishModules is an object: { [folderName]: tspublishJsonData } - - // Create an array with folder names and their ranks - const foldersWithOrder = []; - - for (const folder of tsFolders) { - let rank = Infinity; // Default rank if not specified - 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 = []; - - // Add the rest of the folders in sorted order - for (const item of foldersWithOrder) { - sortedTsFolders.push(item.folder); - } - - // Let's make sure 'ts_shared' is always transpiled first - const ensurePosition = (folderNameArg: string, ensuredPosition: number) => { - if (tsFolders.indexOf(folderNameArg) > -1 && Object.keys(tsPublishModules).indexOf(folderNameArg) === -1) { - sortedTsFolders.splice(tsFolders.indexOf(folderNameArg), 1); - sortedTsFolders.splice(ensuredPosition, 0, folderNameArg); - } - } - - ensurePosition('ts_interfaces', 0); - ensurePosition('ts_shared', 1); - - - const compilationCommandObject: { [key: string]: string } = {}; - 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'); - - for (const tsFolder of sortedTsFolders) { - compilationCommandObject[`./${tsFolder}/**/*.ts`] = `./dist_${tsFolder}`; - } - await tsbuild.compileGlobStringObject(compilationCommandObject, {}, process.cwd(), argvArg); - const summary = (argvArg as any)?.__tsbuildFinalErrorSummary; - if (summary && summary.totalErrors > 0) { - process.exit(1); - } - }); - - /** - * the check command checks TypeScript files against a glob pattern without emitting them - */ - tsbuildCli.addCommand('check').subscribe(async (argvArg) => { - const patterns = argvArg._.slice(1); // Remove the first element which is 'check' - - // If no patterns provided, default to checking ts/**/* and then test/**/* - if (patterns.length === 0) { - console.log('\n🔬 Running default type checking sequence...\n'); - - // First check ts/**/* without skiplibcheck - console.log('📂 Checking ts/**/* files...'); - const tsTsFiles = await plugins.listFilesWithGlob(process.cwd(), 'ts/**/*.ts'); - - if (tsTsFiles.length > 0) { - console.log(` Found ${tsTsFiles.length} TypeScript files in ts/`); - const tsAbsoluteFiles = plugins.smartpath.transform.toAbsolute( - tsTsFiles, - process.cwd() - ) as string[]; - - const tsCompilerOptions = tsbuild.mergeCompilerOptions({}, argvArg); - const tsSuccess = await tsbuild.checkTypes(tsAbsoluteFiles, tsCompilerOptions, argvArg); - - 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 plugins.listFilesWithGlob(process.cwd(), 'test/**/*.ts'); - - if (testTsFiles.length > 0) { - console.log(` Found ${testTsFiles.length} TypeScript files in test/`); - const testAbsoluteFiles = plugins.smartpath.transform.toAbsolute( - testTsFiles, - process.cwd() - ) as string[]; - - // Create new argvArg with skiplibcheck for test files - const testArgvArg = { ...argvArg, skiplibcheck: true }; - const testCompilerOptions = tsbuild.mergeCompilerOptions({}, testArgvArg); - const testSuccess = await tsbuild.checkTypes(testAbsoluteFiles, testCompilerOptions, testArgvArg); - - 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); - } - - const cwd = process.cwd(); - let allFiles: string[] = []; - - // Process each pattern - could be a direct file path or a glob pattern - for (const pattern of patterns) { - // Check if the pattern looks like a glob pattern - if (pattern.includes('*') || pattern.includes('{') || pattern.includes('?')) { - // Handle as glob pattern - console.log(`Processing glob pattern: ${pattern}`); - try { - const stringMatchedFiles = await plugins.listFilesWithGlob(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}'`); - - // Transform to absolute paths - const absoluteMatchedFiles = plugins.smartpath.transform.toAbsolute( - stringMatchedFiles, - cwd - ) as string[]; - - // Add to the list of all files to check - allFiles = allFiles.concat(absoluteMatchedFiles); - } - } catch (err) { - console.error(`❌ Error processing glob pattern '${pattern}': ${err}`); - } - } else { - // Handle as direct file path - const filePath = plugins.path.isAbsolute(pattern) - ? pattern - : plugins.path.join(cwd, pattern); - - const fileExists = await plugins.smartfs.file(filePath).exists(); - if (fileExists) { - allFiles.push(filePath); - } else { - console.error(`❌ Error: File not found: ${filePath}`); - process.exit(1); - } - } - } - - // Filter to only TypeScript files - allFiles = allFiles.filter(file => file.endsWith('.ts') || file.endsWith('.tsx')); - - 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`); - - // Process compiler options - const compilerOptions = tsbuild.mergeCompilerOptions({}, argvArg); - - // Run type check without emitting - const success = await tsbuild.checkTypes(allFiles, compilerOptions, argvArg); - - // Exit with appropriate code - process.exit(success ? 0 : 1); - }); - - tsbuildCli.startParse(); -}; diff --git a/ts/tsbuild.exports.ts b/ts/tsbuild.exports.ts deleted file mode 100644 index de6807b..0000000 --- a/ts/tsbuild.exports.ts +++ /dev/null @@ -1,218 +0,0 @@ -import * as plugins from './plugins.js'; -import type { CompilerOptions, ScriptTarget, ModuleKind } from 'typescript'; -import { compiler, mergeCompilerOptions, emitCheck, checkTypes } from './tsbuild.classes.tsbuild.js'; - -export type { CompilerOptions, ScriptTarget, ModuleKind }; - -export * from './tsbuild.classes.tsbuild.js'; - -/** - * Interface for task information - */ -export interface ITaskInfo { - taskNumber: number; - totalTasks: number; - sourcePattern: string; - destDir: string; - fileCount: number; -} - -/** - * compile an array of absolute file paths with error tracking - */ -export let compileFileArrayWithErrorTracking = async ( - fileStringArrayArg: string[], - compilerOptionsArg: CompilerOptions = {}, - argvArg?: any, - taskInfo?: ITaskInfo -): Promise<{ emittedFiles: any[], errorSummary: import('./tsbuild.classes.tsbuild.js').IErrorSummary }> => { - const { TsBuild } = await import('./tsbuild.classes.tsbuild.js'); - const tsBuild = new TsBuild(fileStringArrayArg, compilerOptionsArg, argvArg, taskInfo); - return tsBuild.compileWithErrorTracking(); -}; - -/** - * compile am array of absolute file paths - */ -export let compileFileArray = ( - fileStringArrayArg: string[], - compilerOptionsArg: CompilerOptions = {}, - argvArg?: any -): Promise => { - return compiler(fileStringArrayArg, mergeCompilerOptions(compilerOptionsArg, argvArg), argvArg); -}; - -/** - * Helper function to merge error summaries - */ -function mergeErrorSummaries(summaries: import('./tsbuild.classes.tsbuild.js').IErrorSummary[]): import('./tsbuild.classes.tsbuild.js').IErrorSummary { - const mergedErrorsByFile: Record = {}; - const mergedGeneralErrors: plugins.typescript.Diagnostic[] = []; - let totalErrors = 0; - - summaries.forEach(summary => { - // Merge errors by file - Object.entries(summary.errorsByFile).forEach(([fileName, errors]) => { - if (!mergedErrorsByFile[fileName]) { - mergedErrorsByFile[fileName] = []; - } - mergedErrorsByFile[fileName] = mergedErrorsByFile[fileName].concat(errors); - }); - - // Merge general errors - mergedGeneralErrors.push(...summary.generalErrors); - totalErrors += summary.totalErrors; - }); - - return { - errorsByFile: mergedErrorsByFile, - generalErrors: mergedGeneralErrors, - totalErrors, - totalFiles: Object.keys(mergedErrorsByFile).length - }; -} - -/** - * Helper function to display final compilation summary - */ -function displayFinalErrorSummary(errorSummary: import('./tsbuild.classes.tsbuild.js').IErrorSummary): void { - if (errorSummary.totalErrors === 0) { - console.log('\n📊 \x1b[32mCompilation Summary: All tasks completed successfully! ✅\x1b[0m\n'); - 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}`); - } - - console.log(`\n${colors.brightRed}Total: ${errorSummary.totalErrors} error${errorSummary.totalErrors !== 1 ? 's' : ''} across ${errorSummary.totalFiles} file${errorSummary.totalFiles !== 1 ? 's' : ''}${colors.reset}`); - console.log('='.repeat(80) + '\n'); -} - -/** - * compile advanced glob configurations - * @param globStringArrayArg a array of glob strings - * { - * './some/origin/folder/**\/*.ts': './some/destination/folder' - * } - */ -export let compileGlobStringObject = async ( - globStringObjectArg: Record, - tsOptionsArg: CompilerOptions = {}, - cwdArg: string = process.cwd(), - argvArg?: any -) => { - let compiledFiles: any[] = []; - const errorSummaries: import('./tsbuild.classes.tsbuild.js').IErrorSummary[] = []; - - const totalTasks = Object.keys(globStringObjectArg).length; - let currentTask = 0; - - // Log the compilation tasks in a nice format (skip for --quiet or --json) - const isQuiet = argvArg?.quiet === true; - const isJson = argvArg?.json === true; - if (!isQuiet && !isJson) { - console.log(`\n👷 TypeScript Compilation Tasks (${totalTasks} task${totalTasks !== 1 ? 's' : ''}):`); - Object.entries(globStringObjectArg).forEach(([source, dest]) => { - console.log(` 📂 ${source} → ${dest}`); - }); - console.log(''); - } - - for (const keyArg in globStringObjectArg) { - // Type safety check for key - if (keyArg && typeof keyArg === 'string' && globStringObjectArg[keyArg]) { - - // Get files matching the glob pattern using helper function - const stringFileTreeArray = await plugins.listFilesWithGlob(cwdArg, keyArg); - - // Transform to absolute paths - const absoluteFilePathArray = plugins.smartpath.transform.toAbsolute( - stringFileTreeArray, - cwdArg - ) as string[]; - - // Get destination directory as absolute path - const destDir: string = plugins.smartpath.transform.toAbsolute( - globStringObjectArg[keyArg], - cwdArg - ) as string; - - // Update compiler options with the output directory - const updatedTsOptions: CompilerOptions = { - ...tsOptionsArg, - outDir: destDir, - }; - - // Compile with error tracking - currentTask++; - const taskInfo = { - taskNumber: currentTask, - totalTasks, - sourcePattern: keyArg, - destDir: globStringObjectArg[keyArg], - fileCount: absoluteFilePathArray.length - }; - - const result = await compileFileArrayWithErrorTracking(absoluteFilePathArray, updatedTsOptions, argvArg, taskInfo); - compiledFiles = compiledFiles.concat(result.emittedFiles); - errorSummaries.push(result.errorSummary); - } - } - - // Display final error summary after all compilation tasks - const finalErrorSummary = mergeErrorSummaries(errorSummaries); - - // Output summary based on mode - if (isJson) { - const result = { - success: finalErrorSummary.totalErrors === 0, - totals: { - errors: finalErrorSummary.totalErrors, - filesWithErrors: finalErrorSummary.totalFiles, - tasks: totalTasks, - }, - errorsByFile: Object.fromEntries( - Object.entries(finalErrorSummary.errorsByFile).map(([file, diags]) => [ - file, - diags.map(d => ({ - code: d.code, - message: plugins.typescript.flattenDiagnosticMessageText(d.messageText as any, '\n'), - })) - ]) - ), - }; - console.log(JSON.stringify(result)); - } else if (!isQuiet) { - displayFinalErrorSummary(finalErrorSummary); - } - - // Attach summary to argvArg so CLI can decide exit behavior - if (argvArg && typeof argvArg === 'object') { - (argvArg as any).__tsbuildFinalErrorSummary = finalErrorSummary; - } - - return compiledFiles; -};