tsbuild/ts/tsbuild.classes.tsbuild.ts

450 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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: 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 handle and log TypeScript diagnostics
*/
private handleDiagnostics(diagnostics: readonly plugins.typescript.Diagnostic[]): boolean {
if (diagnostics.length === 0) {
return false;
}
// Group errors by file for better readability
const errorsByFile: Record<string, plugins.typescript.Diagnostic[]> = {};
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);
}
});
// Print error summary header
const totalErrorCount = diagnostics.length;
const fileCount = Object.keys(errorsByFile).length;
console.log('\n' + '='.repeat(80));
console.log(`❌ Found ${totalErrorCount} error${totalErrorCount !== 1 ? 's' : ''} in ${fileCount} file${fileCount !== 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');
return diagnostics.length > 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
*/
public async compile(): Promise<any[]> {
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<any[]>();
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<boolean> {
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<boolean> {
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<any[]> => {
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<boolean> => {
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<boolean> => {
const tsBuild = new TsBuild(fileNames, options, argvArg);
return tsBuild.checkTypes();
};