// 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'; /** * 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: true, 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 { console.log(`looking at tsconfig.json at ${paths.cwd}`); const tsconfig = plugins.smartfile.fs.toObjectSync(plugins.path.join(paths.cwd, 'tsconfig.json')); const returnObject: CompilerOptions = {}; if (!tsconfig || !tsconfig.compilerOptions) { return returnObject; } if (tsconfig.compilerOptions.baseUrl) { console.log('baseUrl found in tsconfig.json'); returnObject.baseUrl = tsconfig.compilerOptions.baseUrl; } if (tsconfig.compilerOptions.paths) { console.log('paths found in tsconfig.json'); returnObject.paths = tsconfig.compilerOptions.paths; for (const path of Object.keys(tsconfig.compilerOptions.paths)) { returnObject.paths[path][0] = returnObject.paths[path][0].replace('./ts_', './dist_ts_'); } } 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; } if (argvArg.allowimplicitany) { options.noImplicitAny = false; } 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(), }; console.log(mergedOptions); return mergedOptions; } /** * Helper function to handle and log TypeScript diagnostics */ private handleDiagnostics(diagnostics: readonly plugins.typescript.Diagnostic[]): boolean { let hasErrors = false; diagnostics.forEach((diagnostic) => { hasErrors = true; if (diagnostic.file) { const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!); const message = plugins.typescript.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); console.log(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`); } else { console.log( `${plugins.typescript.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}` ); } }); return hasErrors; } /** * 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 */ public async compile(): Promise { if (this.options.skipLibCheck) { console.log('? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?'); console.log('You are skipping libcheck... Is that really wanted?'); console.log('continuing in 5 seconds...'); console.log('? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?'); 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('TypeScript pre-emit checks failed. Please fix the issues above.'); 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('TypeScript emit succeeded!'); done.resolve(emitResult.emittedFiles); } else { console.error('TypeScript emit failed. Please investigate!'); process.exit(exitCode); } return done.promise; } /** * Function to check if files can be emitted without actually emitting them */ public async checkEmit(): Promise { console.log(`Checking if ${this.fileNames.length} files 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('TypeScript emit check passed! File can be emitted successfully.'); } else { console.error('TypeScript emit check failed. Please fix the issues above.'); } 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(); };