Compare commits

...

4 Commits

8 changed files with 2542 additions and 1551 deletions

View File

@@ -1,5 +1,26 @@
# Changelog
## 2025-11-02 - 2.7.0 - feat(tsbuild)
Add tsconfig.json support and safer compiler option merging; protect critical options; apply path and enum transforms; bump dependencies.
- Add robust tsconfig.json reading with graceful fallback when no tsconfig is present or it is invalid
- Merge compiler options in a clear priority order (defaults -> tsconfig -> protected defaults -> programmatic -> CLI flags)
- Introduce protected (critical) compiler options that cannot be overridden by tsconfig.json: outDir, noEmitOnError, declaration, emitDecoratorMetadata, inlineSourceMap
- Convert string values from tsconfig (target, module, moduleResolution) to TypeScript enum values where applicable; special-case NodeNext
- Transform tsconfig path mappings by replacing './ts_' with './dist_ts_' to keep runtime path resolution consistent with compiled output
- Expose getCriticalDefaults helper and adjust mergeCompilerOptions to apply protected defaults before programmatic and CLI overrides
- Update README with documentation for tsconfig support, merge order, protected compiler options, and example tsconfig
- Bump dependencies/devDependencies: @push.rocks/smartcli ^4.0.19, @push.rocks/smartlog ^3.1.10, typescript 5.9.3, @git.zone/tsrun ^1.6.2, @git.zone/tstest ^2.7.0
## 2025-08-29 - 2.6.8 - fix(tsbuild)
Avoid process.exit in library, add confirmskiplibcheck flag, improve CLI exit handling and JSON/quiet modes, update test script
- Changed package.json test script from "tsrun test/test.ts --verbose" to "tstest test/test.ts --verbose".
- Library no longer calls process.exit from compile and compileWithErrorTracking; errors are returned or thrown so callers can decide process termination.
- skipLibCheck behavior updated: delay/warning only happens when --confirmskiplibcheck is present; otherwise a short informational note is printed (suppressed in --quiet/--json).
- CLI now awaits compileGlobStringObject calls and inspects a final error summary attached to argv to decide process.exit(1) when errors occurred.
- compileGlobStringObject/exports now respect --quiet and --json modes, emit a JSON summary when --json is used, and attach the final error summary to argv so the CLI can determine exit behavior.
## 2025-08-18 - 2.6.7 - fix(tspublish)
Bump @git.zone/tspublish dependency to ^1.10.3

View File

@@ -1,6 +1,6 @@
{
"name": "@git.zone/tsbuild",
"version": "2.6.7",
"version": "2.7.0",
"private": false,
"description": "A tool for compiling TypeScript files using the latest nightly features, offering flexible APIs and a CLI for streamlined development.",
"main": "dist_ts/index.js",
@@ -10,7 +10,7 @@
"tsbuild": "./cli.js"
},
"scripts": {
"test": "tsrun test/test.ts --verbose",
"test": "tstest test/test.ts --verbose",
"build": "node cli.ts.js --web",
"buildDocs": "tsdoc"
},
@@ -38,17 +38,17 @@
"dependencies": {
"@git.zone/tspublish": "^1.10.3",
"@push.rocks/early": "^4.0.4",
"@push.rocks/smartcli": "^4.0.11",
"@push.rocks/smartcli": "^4.0.19",
"@push.rocks/smartdelay": "^3.0.5",
"@push.rocks/smartfile": "^11.2.7",
"@push.rocks/smartlog": "^3.1.8",
"@push.rocks/smartlog": "^3.1.10",
"@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3",
"typescript": "5.9.2"
"typescript": "5.9.3"
},
"devDependencies": {
"@git.zone/tsrun": "^1.2.47",
"@git.zone/tstest": "^2.3.4",
"@git.zone/tsrun": "^1.6.2",
"@git.zone/tstest": "^2.7.0",
"@types/node": "^22.15.21"
},
"files": [

3767
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -184,6 +184,49 @@ Example:
npx tsbuild --skiplibcheck --disallowimplicitany
```
## Configuration with tsconfig.json
`@git.zone/tsbuild` fully supports `tsconfig.json` for compiler configuration. All compiler options from your `tsconfig.json` will be respected and merged with tsbuild's defaults.
### Option Priority (Merge Order)
When multiple configuration sources are present, they are merged in the following order (later sources override earlier ones):
1. **Default compiler options** - Base defaults for all options
2. **tsconfig.json** - All compiler options from your project's `tsconfig.json` (if present)
3. **Protected defaults** - Critical options for build integrity (see below)
4. **Programmatic options** - Options passed to API functions
5. **CLI flags** - Command-line arguments (highest priority)
### Protected Compiler Options
To ensure build integrity and correct functionality, the following options are protected and cannot be overridden by `tsconfig.json` alone (but can be overridden programmatically or via CLI):
- `outDir: 'dist_ts/'` - Required for path transformation logic
- `noEmitOnError: true` - Prevents broken builds from being emitted
- `declaration: true` - Ensures .d.ts files for library consumers
- `emitDecoratorMetadata: true` - Required for dependency injection frameworks
- `inlineSourceMap: true` - Provides consistent debugging experience
### Working Without tsconfig.json
`@git.zone/tsbuild` works perfectly fine without a `tsconfig.json` file. If no `tsconfig.json` is found, it will gracefully fall back to sensible defaults without errors.
Example `tsconfig.json`:
```json
{
"compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false,
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"verbatimModuleSyntax": true
}
}
```
## Default Compiler Options
By default, `@git.zone/tsbuild` uses the following compiler options:
@@ -200,7 +243,7 @@ By default, `@git.zone/tsbuild` uses the following compiler options:
target: ScriptTarget.ESNext,
moduleResolution: ModuleResolutionKind.NodeNext,
lib: ['lib.dom.d.ts', 'lib.es2022.d.ts'],
noImplicitAny: false, // Now allowing implicit any by default
noImplicitAny: false,
esModuleInterop: true,
useDefineForClassFields: false,
verbatimModuleSyntax: true,
@@ -208,7 +251,7 @@ By default, `@git.zone/tsbuild` uses the following compiler options:
}
```
These options can be overridden by providing a custom `CompilerOptions` object.
These defaults are merged with your `tsconfig.json` options (if present), programmatic options, and CLI flags according to the priority order described above.
## Path Resolution

View File

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

View File

@@ -62,19 +62,56 @@ export class TsBuild {
* 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 = {};
let tsconfig: any;
// Try to read tsconfig.json, but don't fail if it doesn't exist
try {
tsconfig = plugins.smartfile.fs.toObjectSync(plugins.path.join(paths.cwd, 'tsconfig.json'));
} catch (error) {
// tsconfig.json doesn't exist or is invalid - use defaults
return {};
}
if (!tsconfig || !tsconfig.compilerOptions) {
return returnObject;
return {};
}
// Process baseUrl
if (tsconfig.compilerOptions.baseUrl) {
returnObject.baseUrl = tsconfig.compilerOptions.baseUrl;
// Start by copying ALL compiler options from tsconfig.json
const returnObject: CompilerOptions = { ...tsconfig.compilerOptions };
// Apply special transformations for string-to-enum conversions
// Process target (convert string to enum)
if (tsconfig.compilerOptions.target && 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 paths
// Process module (convert string to enum)
if (tsconfig.compilerOptions.module && 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 (convert string to enum)
if (tsconfig.compilerOptions.moduleResolution && 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;
}
}
// Apply path transformations (ts_ → dist_ts_)
if (tsconfig.compilerOptions.paths) {
returnObject.paths = { ...tsconfig.compilerOptions.paths };
for (const path of Object.keys(returnObject.paths)) {
@@ -84,57 +121,23 @@ export class TsBuild {
}
}
// 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;
}
/**
* Returns critical default options that should not be overridden by tsconfig.json
* These options are essential for tsbuild's functionality and build integrity
*/
private getCriticalDefaults(): CompilerOptions {
return {
outDir: 'dist_ts/', // Required for path transformation logic
noEmitOnError: true, // Build integrity - prevent broken builds
declaration: true, // Library consumers depend on .d.ts files
emitDecoratorMetadata: true, // Required for dependency injection frameworks
inlineSourceMap: true, // Consistent debugging experience
};
}
/**
* Process command line arguments and return applicable compiler options
*/
@@ -162,6 +165,13 @@ export class TsBuild {
/**
* Merges compilerOptions with the default compiler options
*
* Merge order (later overwrites earlier):
* 1. compilerOptionsDefault - Base defaults for all options
* 2. getTsConfigOptions() - User's tsconfig.json (all options)
* 3. getCriticalDefaults() - Protected options that shouldn't be overridden by tsconfig.json
* 4. customTsOptions - Programmatic options (can override critical defaults)
* 5. getCommandLineOptions() - CLI flags (highest priority)
*/
public mergeCompilerOptions(
customTsOptions: CompilerOptions = {},
@@ -169,10 +179,11 @@ export class TsBuild {
): CompilerOptions {
// create merged options
const mergedOptions: CompilerOptions = {
...compilerOptionsDefault,
...customTsOptions,
...this.getCommandLineOptions(argvArg),
...this.getTsConfigOptions(),
...compilerOptionsDefault, // 1. All defaults
...this.getTsConfigOptions(), // 2. User's tsconfig.json (all options)
...this.getCriticalDefaults(), // 3. Protected overrides
...customTsOptions, // 4. Programmatic options
...this.getCommandLineOptions(argvArg), // 5. CLI flags (highest priority)
};
return mergedOptions;
@@ -309,10 +320,17 @@ export class TsBuild {
*/
public async compileWithErrorTracking(): Promise<{ emittedFiles: any[], errorSummary: IErrorSummary }> {
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);
if (this.argvArg?.confirmskiplibcheck) {
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);
} else {
// No delay by default; keep a short note unless in quiet/json modes
if (!this.argvArg?.quiet && !this.argvArg?.json) {
console.log('⚠️ skipLibCheck enabled; use --confirmskiplibcheck to pause with warning.');
}
}
}
// Enhanced logging with task info
@@ -382,7 +400,9 @@ export class TsBuild {
this.displayErrorSummary(combinedErrorSummary);
console.error('\n❌ TypeScript emit failed. Please investigate the errors listed above!');
console.error(' No output files have been generated.\n');
process.exit(exitCode);
// Do not exit here; return error summary so caller can decide
done.resolve({ emittedFiles: [], errorSummary: combinedErrorSummary });
return done.promise;
}
return done.promise;
@@ -393,10 +413,16 @@ export class TsBuild {
*/
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);
if (this.argvArg?.confirmskiplibcheck) {
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);
} else {
if (!this.argvArg?.quiet && !this.argvArg?.json) {
console.log('⚠️ skipLibCheck enabled; use --confirmskiplibcheck to pause with warning.');
}
}
}
console.log(`🔨 Compiling ${this.fileNames.length} files...`);
@@ -412,7 +438,8 @@ export class TsBuild {
this.displayErrorSummary(preEmitErrorSummary);
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);
// Throw instead of exiting to keep library pure
throw new Error('TypeScript pre-emit checks failed.');
}
// If no pre-emit errors, proceed with emit
@@ -438,7 +465,8 @@ export class TsBuild {
this.displayErrorSummary(emitErrorSummary);
console.error('\n❌ TypeScript emit failed. Please investigate the errors listed above!');
console.error(' No output files have been generated.\n');
process.exit(exitCode);
// Throw instead of exiting to keep library pure
throw new Error('TypeScript emit failed.');
}
return done.promise;

View File

@@ -9,7 +9,7 @@ export const runCli = async () => {
* the standard task compiles anything in ts/ directory to dist directory
*/
tsbuildCli.standardCommand().subscribe(async (argvArg) => {
tsbuild.compileGlobStringObject(
await tsbuild.compileGlobStringObject(
{
'./ts/**/*.ts': './dist_ts',
},
@@ -17,6 +17,10 @@ export const runCli = async () => {
process.cwd(),
argvArg
);
const summary = (argvArg as any)?.__tsbuildFinalErrorSummary;
if (summary && summary.totalErrors > 0) {
process.exit(1);
}
});
/**
@@ -30,6 +34,10 @@ export const runCli = async () => {
compilationCommandObject[`./${directory}/**/*.ts`] = `./dist_${directory}`;
}
await tsbuild.compileGlobStringObject(compilationCommandObject, {}, process.cwd(), argvArg);
const summary = (argvArg as any)?.__tsbuildFinalErrorSummary;
if (summary && summary.totalErrors > 0) {
process.exit(1);
}
});
/**
@@ -182,6 +190,10 @@ export const runCli = async () => {
compilationCommandObject[`./${tsFolder}/**/*.ts`] = `./dist_${tsFolder}`;
}
await tsbuild.compileGlobStringObject(compilationCommandObject, {}, process.cwd(), argvArg);
const summary = (argvArg as any)?.__tsbuildFinalErrorSummary;
if (summary && summary.totalErrors > 0) {
process.exit(1);
}
});
/**

View File

@@ -130,12 +130,16 @@ export let compileGlobStringObject = async (
const totalTasks = Object.keys(globStringObjectArg).length;
let currentTask = 0;
// Log the compilation tasks in a nice format
console.log(`\n👷 TypeScript Compilation Tasks (${totalTasks} task${totalTasks !== 1 ? 's' : ''}):`);
Object.entries(globStringObjectArg).forEach(([source, dest]) => {
console.log(` 📂 ${source}${dest}`);
});
console.log('');
// Log the compilation tasks in a nice format (skip for --quiet or --json)
const isQuiet = argvArg?.quiet === true;
const isJson = argvArg?.json === true;
if (!isQuiet && !isJson) {
console.log(`\n👷 TypeScript Compilation Tasks (${totalTasks} task${totalTasks !== 1 ? 's' : ''}):`);
Object.entries(globStringObjectArg).forEach(([source, dest]) => {
console.log(` 📂 ${source}${dest}`);
});
console.log('');
}
for (const keyArg in globStringObjectArg) {
// Type safety check for key
@@ -185,7 +189,35 @@ export let compileGlobStringObject = async (
// Display final error summary after all compilation tasks
const finalErrorSummary = mergeErrorSummaries(errorSummaries);
displayFinalErrorSummary(finalErrorSummary);
// Output summary based on mode
if (isJson) {
const result = {
success: finalErrorSummary.totalErrors === 0,
totals: {
errors: finalErrorSummary.totalErrors,
filesWithErrors: finalErrorSummary.totalFiles,
tasks: totalTasks,
},
errorsByFile: Object.fromEntries(
Object.entries(finalErrorSummary.errorsByFile).map(([file, diags]) => [
file,
diags.map(d => ({
code: d.code,
message: plugins.typescript.flattenDiagnosticMessageText(d.messageText as any, '\n'),
}))
])
),
};
console.log(JSON.stringify(result));
} else if (!isQuiet) {
displayFinalErrorSummary(finalErrorSummary);
}
// Attach summary to argvArg so CLI can decide exit behavior
if (argvArg && typeof argvArg === 'object') {
(argvArg as any).__tsbuildFinalErrorSummary = finalErrorSummary;
}
return compiledFiles;
};