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

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@git.zone/tsbuild',
version: '2.3.2',
version: '2.4.0',
description: 'A tool for compiling TypeScript files using the latest nightly features, offering flexible APIs and a CLI for streamlined development.'
}

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();
};

View File

@ -39,7 +39,9 @@ export const runCli = async () => {
const patterns = argvArg._.slice(1); // Remove the first element which is 'emitcheck'
if (patterns.length === 0) {
console.error('Error: Please provide at least one TypeScript file path or glob pattern');
console.error('\n❌ Error: Please provide at least one TypeScript file path or glob pattern');
console.error(' Usage: tsbuild emitcheck <file_or_glob_pattern> [additional_patterns ...]\n');
console.error(' Example: tsbuild emitcheck "src/**/*.ts" "test/**/*.ts"\n');
process.exit(1);
}
@ -61,9 +63,9 @@ export const runCli = async () => {
: [];
if (stringMatchedFiles.length === 0) {
console.warn(`Warning: No files matched the pattern '${pattern}'`);
console.warn(`⚠️ Warning: No files matched the pattern '${pattern}'`);
} else {
console.log(`Found ${stringMatchedFiles.length} files matching pattern '${pattern}'`);
console.log(`📂 Found ${stringMatchedFiles.length} files matching pattern '${pattern}'`);
// Transform to absolute paths
const absoluteMatchedFiles = plugins.smartpath.transform.toAbsolute(
@ -75,7 +77,7 @@ export const runCli = async () => {
allFiles = allFiles.concat(absoluteMatchedFiles);
}
} catch (err) {
console.error(`Error processing glob pattern '${pattern}': ${err}`);
console.error(`Error processing glob pattern '${pattern}': ${err}`);
}
} else {
// Handle as direct file path
@ -87,7 +89,7 @@ export const runCli = async () => {
await plugins.smartfile.fs.fileExists(filePath);
allFiles.push(filePath);
} catch (err) {
console.error(`Error: File not found: ${filePath}`);
console.error(`Error: File not found: ${filePath}`);
process.exit(1);
}
}
@ -97,11 +99,12 @@ export const runCli = async () => {
allFiles = allFiles.filter(file => file.endsWith('.ts') || file.endsWith('.tsx'));
if (allFiles.length === 0) {
console.error('Error: No TypeScript files found to check');
console.error('\n❌ Error: No TypeScript files found to check');
console.error(' Please verify your file paths or glob patterns.\n');
process.exit(1);
}
console.log(`Found ${allFiles.length} TypeScript files to check`);
console.log(`\n🔎 Found ${allFiles.length} TypeScript file${allFiles.length !== 1 ? 's' : ''} to check`);
// Process compiler options
const compilerOptions = tsbuild.mergeCompilerOptions({}, argvArg);
@ -161,13 +164,97 @@ export const runCli = async () => {
const compilationCommandObject: { [key: string]: string } = {};
console.log(`compiling in this order:`);
console.log(sortedTsFolders);
console.log(`\n🔄 Compiling TS folders in this order:`);
console.log(' ' + sortedTsFolders.join('\n ') + '\n');
for (const tsFolder of sortedTsFolders) {
compilationCommandObject[`./${tsFolder}/**/*.ts`] = `./dist_${tsFolder}`;
}
await tsbuild.compileGlobStringObject(compilationCommandObject, {}, process.cwd(), argvArg);
});
/**
* the check command checks TypeScript files against a glob pattern without emitting them
*/
tsbuildCli.addCommand('check').subscribe(async (argvArg) => {
const patterns = argvArg._.slice(1); // Remove the first element which is 'check'
if (patterns.length === 0) {
console.error('\n❌ Error: Please provide at least one TypeScript file path or glob pattern');
console.error(' Usage: tsbuild check <file_or_glob_pattern> [additional_patterns ...]\n');
console.error(' Example: tsbuild check "src/**/*.ts" "test/**/*.ts"\n');
process.exit(1);
}
const cwd = process.cwd();
let allFiles: string[] = [];
// Process each pattern - could be a direct file path or a glob pattern
for (const pattern of patterns) {
// Check if the pattern looks like a glob pattern
if (pattern.includes('*') || pattern.includes('{') || pattern.includes('?')) {
// Handle as glob pattern
console.log(`Processing glob pattern: ${pattern}`);
try {
const matchedFiles = await plugins.smartfile.fs.listFileTree(cwd, pattern);
// Ensure matchedFiles contains only strings
const stringMatchedFiles = Array.isArray(matchedFiles)
? matchedFiles.filter((item): item is string => typeof item === 'string')
: [];
if (stringMatchedFiles.length === 0) {
console.warn(`⚠️ Warning: No files matched the pattern '${pattern}'`);
} else {
console.log(`📂 Found ${stringMatchedFiles.length} files matching pattern '${pattern}'`);
// Transform to absolute paths
const absoluteMatchedFiles = plugins.smartpath.transform.toAbsolute(
stringMatchedFiles,
cwd
) as string[];
// Add to the list of all files to check
allFiles = allFiles.concat(absoluteMatchedFiles);
}
} catch (err) {
console.error(`❌ Error processing glob pattern '${pattern}': ${err}`);
}
} else {
// Handle as direct file path
const filePath = plugins.path.isAbsolute(pattern)
? pattern
: plugins.path.join(cwd, pattern);
try {
await plugins.smartfile.fs.fileExists(filePath);
allFiles.push(filePath);
} catch (err) {
console.error(`❌ Error: File not found: ${filePath}`);
process.exit(1);
}
}
}
// Filter to only TypeScript files
allFiles = allFiles.filter(file => file.endsWith('.ts') || file.endsWith('.tsx'));
if (allFiles.length === 0) {
console.error('\n❌ Error: No TypeScript files found to check');
console.error(' Please verify your file paths or glob patterns.\n');
process.exit(1);
}
console.log(`\n🔎 Found ${allFiles.length} TypeScript file${allFiles.length !== 1 ? 's' : ''} to check`);
// Process compiler options
const compilerOptions = tsbuild.mergeCompilerOptions({}, argvArg);
// Run type check without emitting
const success = await tsbuild.checkTypes(allFiles, compilerOptions, argvArg);
// Exit with appropriate code
process.exit(success ? 0 : 1);
});
tsbuildCli.startParse();
};

View File

@ -1,6 +1,6 @@
import * as plugins from './plugins.js';
import type { CompilerOptions, ScriptTarget, ModuleKind } from 'typescript';
import { compiler, mergeCompilerOptions, emitCheck } from './tsbuild.classes.tsbuild.js';
import { compiler, mergeCompilerOptions, emitCheck, checkTypes } from './tsbuild.classes.tsbuild.js';
export type { CompilerOptions, ScriptTarget, ModuleKind };
@ -32,12 +32,16 @@ export let compileGlobStringObject = async (
) => {
let compiledFiles: any[] = [];
// Log the compilation tasks in a nice format
console.log('\n👷 TypeScript Compilation Tasks:');
Object.entries(globStringObjectArg).forEach(([source, dest]) => {
console.log(` 📂 ${source}${dest}`);
});
console.log('');
for (const keyArg in globStringObjectArg) {
// Type safety check for key
if (keyArg && typeof keyArg === 'string' && globStringObjectArg[keyArg]) {
console.log(
`TypeScript assignment: transpile from ${keyArg} to ${globStringObjectArg[keyArg]}`
);
// Get files matching the glob pattern
const fileTreeArray = await plugins.smartfile.fs.listFileTree(cwdArg, keyArg);