feat(cli): Add new check command for type checking and update compiler options handling

This commit is contained in:
2025-05-15 09:31:57 +00:00
parent 6d68f35a9a
commit 960fc5f213
9 changed files with 402 additions and 49 deletions

View File

@ -17,7 +17,7 @@ export const compilerOptionsDefault: CompilerOptions = {
target: plugins.typescript.ScriptTarget.ESNext,
moduleResolution: plugins.typescript.ModuleResolutionKind.NodeNext,
lib: ['lib.dom.d.ts', 'lib.es2022.d.ts'],
noImplicitAny: true,
noImplicitAny: false, // Allow implicit any by default
esModuleInterop: true,
useDefineForClassFields: false,
verbatimModuleSyntax: true,
@ -49,7 +49,6 @@ export class TsBuild {
* 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 = {};
@ -57,16 +56,66 @@ export class TsBuild {
return returnObject;
}
// Process baseUrl
if (tsconfig.compilerOptions.baseUrl) {
console.log('baseUrl found in tsconfig.json');
returnObject.baseUrl = tsconfig.compilerOptions.baseUrl;
}
// Process paths
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_');
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];
}
}
@ -85,8 +134,9 @@ export class TsBuild {
options.skipLibCheck = true;
}
if (argvArg.allowimplicitany) {
options.noImplicitAny = false;
// Changed behavior: --disallowimplicitany instead of --allowimplicitany
if (argvArg.disallowimplicitany) {
options.noImplicitAny = true;
}
if (argvArg.commonjs) {
@ -112,7 +162,6 @@ export class TsBuild {
...this.getTsConfigOptions(),
};
console.log(mergedOptions);
return mergedOptions;
}
@ -120,22 +169,94 @@ export class TsBuild {
* Helper function to handle and log TypeScript diagnostics
*/
private handleDiagnostics(diagnostics: readonly plugins.typescript.Diagnostic[]): boolean {
let hasErrors = false;
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) => {
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}`);
const fileName = diagnostic.file.fileName;
if (!errorsByFile[fileName]) {
errorsByFile[fileName] = [];
}
errorsByFile[fileName].push(diagnostic);
} else {
console.log(
`${plugins.typescript.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`
);
generalErrors.push(diagnostic);
}
});
return hasErrors;
// 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;
}
/**
@ -166,14 +287,13 @@ export class TsBuild {
*/
public async compile(): Promise<any[]> {
if (this.options.skipLibCheck) {
console.log('? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?');
console.log('\n⚠ WARNING ⚠️');
console.log('You are skipping libcheck... Is that really wanted?');
console.log('continuing in 5 seconds...');
console.log('? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?');
console.log('Continuing in 5 seconds...\n');
await plugins.smartdelay.delayFor(5000);
}
console.log(`Compiling ${this.fileNames.length} files...`);
console.log(`🔨 Compiling ${this.fileNames.length} files...`);
const done = plugins.smartpromise.defer<any[]>();
const program = this.createProgram();
@ -183,7 +303,8 @@ export class TsBuild {
// Only continue to emit phase if no pre-emit errors
if (hasPreEmitErrors) {
console.error('TypeScript pre-emit checks failed. Please fix the issues above.');
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);
}
@ -193,10 +314,22 @@ export class TsBuild {
const exitCode = emitResult.emitSkipped ? 1 : 0;
if (exitCode === 0) {
console.log('TypeScript emit succeeded!');
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('TypeScript emit failed. Please investigate!');
console.error('\n❌ TypeScript emit failed. Please investigate the errors listed above!');
console.error(' No output files have been generated.\n');
process.exit(exitCode);
}
@ -207,7 +340,8 @@ export class TsBuild {
* Function to check if files can be emitted without actually emitting them
*/
public async checkEmit(): Promise<boolean> {
console.log(`Checking if ${this.fileNames.length} files can be emitted...`);
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({
@ -226,9 +360,42 @@ export class TsBuild {
const success = !hasPreEmitErrors && !hasEmitErrors && !emitResult.emitSkipped;
if (success) {
console.log('TypeScript emit check passed! File can be emitted successfully.');
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('TypeScript emit check failed. Please fix the issues above.');
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;
@ -268,4 +435,16 @@ export const emitCheck = async (
): 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();
};