450 lines
16 KiB
TypeScript
450 lines
16 KiB
TypeScript
// 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();
|
||
}; |