// 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, emitDecoratorMetadata: true, experimentalDecorators: 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.es2022.d.ts'], noImplicitAny: false, // Allow implicit any by default esModuleInterop: true, useDefineForClassFields: false, verbatimModuleSyntax: true, baseUrl: './', }; /** * TsBuild class for handling TypeScript compilation */ export class TsBuild { private fileNames: string[] = []; private options: plugins.typescript.CompilerOptions; private argvArg?: any; /** * Create a new TsBuild instance */ constructor( fileNames: string[] = [], customOptions: CompilerOptions = {}, argvArg?: any ) { this.fileNames = fileNames; this.argvArg = argvArg; this.options = this.mergeCompilerOptions(customOptions, argvArg); } /** * Helper function to read and process tsconfig.json */ private getTsConfigOptions(): CompilerOptions { const tsconfig = plugins.smartfile.fs.toObjectSync(plugins.path.join(paths.cwd, 'tsconfig.json')); const returnObject: CompilerOptions = {}; if (!tsconfig || !tsconfig.compilerOptions) { return returnObject; } // Process baseUrl if (tsconfig.compilerOptions.baseUrl) { returnObject.baseUrl = tsconfig.compilerOptions.baseUrl; } // Process paths 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_'); } } } // Process target if (tsconfig.compilerOptions.target) { if (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 if (tsconfig.compilerOptions.module) { if (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 if (tsconfig.compilerOptions.moduleResolution) { if (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; } } } // Copy boolean options directly const booleanOptions = [ 'experimentalDecorators', 'useDefineForClassFields', 'esModuleInterop', 'verbatimModuleSyntax' ]; for (const option of booleanOptions) { if (option in tsconfig.compilerOptions) { (returnObject as any)[option] = (tsconfig.compilerOptions as any)[option]; } } return returnObject; } /** * 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 */ public mergeCompilerOptions( customTsOptions: CompilerOptions = {}, argvArg?: any ): CompilerOptions { // create merged options const mergedOptions: CompilerOptions = { ...compilerOptionsDefault, ...customTsOptions, ...this.getCommandLineOptions(argvArg), ...this.getTsConfigOptions(), }; 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'); } /** * Helper function to handle and log TypeScript diagnostics (legacy method) */ private handleDiagnostics(diagnostics: readonly plugins.typescript.Diagnostic[]): boolean { const errorSummary = this.processDiagnostics(diagnostics); this.displayErrorSummary(errorSummary); return errorSummary.totalErrors > 0; } /** * 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) { 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); } 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) { 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({ 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'); process.exit(exitCode); } return done.promise; } /** * The main compiler function that compiles the files */ public async compile(): Promise { if (this.options.skipLibCheck) { 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); } 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 hasPreEmitErrors = this.handleDiagnostics(preEmitDiagnostics); // Only continue to emit phase if no pre-emit errors if (hasPreEmitErrors) { 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'); process.exit(1); } // If no pre-emit errors, proceed with emit const emitResult = program.emit(); const hasEmitErrors = this.handleDiagnostics(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 { console.error('\n❌ TypeScript emit failed. Please investigate the errors listed above!'); console.error(' No output files have been generated.\n'); process.exit(exitCode); } 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 hasPreEmitErrors = this.handleDiagnostics(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 hasEmitErrors = this.handleDiagnostics(emitResult.diagnostics); const success = !hasPreEmitErrors && !hasEmitErrors && !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 { 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 hasErrors = this.handleDiagnostics(diagnostics); // Set success flag const success = !hasErrors; 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 { 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(); };