Compare commits

..

26 Commits

Author SHA1 Message Date
aeaa957efa 4.0.0 2025-12-13 23:00:52 +00:00
e5fcbb9a09 BREAKING(structure): modernize internal structure and support unpacking 2025-12-13 22:59:58 +00:00
4ebc37fa5a 3.1.4 2025-12-13 11:44:20 +00:00
5bfe60927e fix(npmextra.json): update to new format 2025-12-13 11:44:03 +00:00
9e6b91b891 v3.1.3 2025-12-13 09:44:15 +00:00
e40e8f6a77 fix(npmextra): Align npmextra.json package name with package.json (@git.zone/tsbuild) 2025-12-13 09:44:15 +00:00
ebe7afce82 v3.1.2 2025-11-28 08:44:42 +00:00
27aa318054 fix(TsBuild): Set default TypeScript target to ESNext 2025-11-28 08:44:42 +00:00
9a071ce82f v3.1.1 2025-11-27 23:04:13 +00:00
ad227ded73 fix(compiler): Update default TypeScript target to ES2024 2025-11-27 23:04:13 +00:00
c66a941aaf v3.1.0 2025-11-17 12:25:31 +00:00
a3b58dda39 feat(tsbuild.classes): Update default TypeScript lib to lib.esnext.d.ts 2025-11-17 12:25:31 +00:00
4b29107130 v3.0.0 2025-11-17 11:57:32 +00:00
09adbc1965 BREAKING CHANGE(TsBuild): Stop forcing emitDecoratorMetadata in protected compiler defaults 2025-11-17 11:57:32 +00:00
7fb571d4f6 v2.7.3 2025-11-17 11:51:56 +00:00
6b32dced5a fix(tsbuild.classes): Remove duplicate emitDecoratorMetadata from default compiler options and centralize it in protected defaults 2025-11-17 11:51:56 +00:00
df39aa48d0 v2.7.2 2025-11-17 08:08:21 +00:00
a4863bc761 fix(compilerOptions): Remove experimentalDecorators and useDefineForClassFields from default TypeScript compiler options 2025-11-17 08:08:21 +00:00
b38ef6cf82 2.7.1 2025-11-02 06:13:34 +00:00
8b50cd3090 fix(readme): Update documentation: expand README with usage, CLI and API examples; add readme.hints.md project memory 2025-11-02 06:13:34 +00:00
c6ab493efc 2.7.0 2025-11-02 05:31:55 +00:00
82ae8a0e4a feat(tsbuild): Add tsconfig.json support and safer compiler option merging; protect critical options; apply path and enum transforms; bump dependencies. 2025-11-02 05:31:55 +00:00
787becc4d3 2.6.8 2025-08-29 17:14:58 +00:00
746ca8767a fix(tsbuild): Avoid process.exit in library, add confirmskiplibcheck flag, improve CLI exit handling and JSON/quiet modes, update test script 2025-08-29 17:14:58 +00:00
55c1a2953a 2.6.7 2025-08-18 02:19:15 +00:00
8e9fcb8135 fix(tspublish): Bump @git.zone/tspublish dependency to ^1.10.3 2025-08-18 02:19:15 +00:00
28 changed files with 4889 additions and 4947 deletions

View File

@@ -1,5 +1,87 @@
# Changelog # Changelog
## 2025-12-13 - 3.1.3 - fix(npmextra)
Align npmextra.json package name with package.json (@git.zone/tsbuild)
- Corrected npmPackagename in npmextra.json from "@gitzone/tsbuild" to "@git.zone/tsbuild" to match package.json and README
- Metadata-only change: no code or API behavior affected
## 2025-11-28 - 3.1.2 - fix(TsBuild)
Set default TypeScript target to ESNext
- Default compiler target changed from ScriptTarget.ES2024 to ScriptTarget.ESNext in ts/tsbuild.classes.tsbuild.ts
- Aligns the default compiler options with documentation and ensures use of the latest language features
## 2025-11-27 - 3.1.1 - fix(compiler)
Update default TypeScript target to ES2024
- Default compiler option 'target' changed from ScriptTarget.ESNext to ScriptTarget.ES2024 in ts/tsbuild.classes.tsbuild.ts
- Aligns emitted code to ES2024 language features by default
## 2025-11-17 - 3.1.0 - feat(tsbuild.classes)
Update default TypeScript lib to lib.esnext.d.ts
- Changed default compilerOptions.lib from ['lib.dom.d.ts', 'lib.es2022.d.ts'] to ['lib.dom.d.ts', 'lib.esnext.d.ts'] in compilerOptionsDefault.
- Allows newer ECMAScript/DOM features by default when compiling with tsbuild (affects emitted types and available globals).
- Behavioral default change only — no public API changes; callers can still override lib via tsconfig, programmatic options, or CLI.
## 2025-11-17 - 3.0.0 - BREAKING CHANGE(TsBuild)
Stop forcing emitDecoratorMetadata in protected compiler defaults
- Removed emitDecoratorMetadata from the set of protected/critical default compiler options.
- Projects that relied on tsbuild automatically enabling emitDecoratorMetadata (for DI frameworks like NestJS, TypeORM, Inversify) must now enable it explicitly in their tsconfig.json or via programmatic/CLI options.
- No other protected defaults were changed; outDir, noEmitOnError, declaration and inlineSourceMap remain enforced.
- Behavior is now more permissive: decorator metadata generation is controlled by user configuration rather than being forced by tsbuild.
## 2025-11-17 - 2.7.3 - fix(tsbuild.classes)
Remove duplicate emitDecoratorMetadata from default compiler options and centralize it in protected defaults
- Removed emitDecoratorMetadata from compilerOptionsDefault in ts/tsbuild.classes.tsbuild.ts to avoid duplicate configuration.
- emitDecoratorMetadata remains enforced via getCriticalDefaults(), ensuring decorator metadata support is protected from tsconfig.json overrides.
- Prevents inconsistencies during compiler option merging by centralizing the decorator-related setting.
## 2025-11-17 - 2.7.2 - fix(compilerOptions)
Remove experimentalDecorators and useDefineForClassFields from default TypeScript compiler options
- Removed experimentalDecorators from compilerOptionsDefault in ts/tsbuild.classes.tsbuild.ts
- Removed useDefineForClassFields from compilerOptionsDefault in ts/tsbuild.classes.tsbuild.ts
- Default compiler options now rely on TypeScript's upstream defaults for decorator and class field behavior
- If your project relies on these settings, re-enable them in your tsconfig.json or pass them via the programmatic API / CLI
## 2025-11-02 - 2.7.1 - fix(readme)
Update documentation: expand README with usage, CLI and API examples; add readme.hints.md project memory
- Add readme.hints.md: new project memory / quick reference with public API and CLI summaries
- Expand and restructure readme.md: more comprehensive Quick Start, CLI Commands, API Reference, configuration, examples and troubleshooting
- Clarify protected compiler options, default compiler options, path transformation behavior and error-handling patterns
- Docs-only change — no source code or behavioral changes
## 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
- Updated dependency @git.zone/tspublish from ^1.10.2 to ^1.10.3 in package.json
## 2025-08-18 - 2.6.6 - fix(dependencies) ## 2025-08-18 - 2.6.6 - fix(dependencies)
Update dependency @git.zone/tspublish to ^1.10.2 Update dependency @git.zone/tspublish to ^1.10.2

View File

@@ -10,7 +10,7 @@
"gitscope": "gitzone", "gitscope": "gitzone",
"gitrepo": "tsbuild", "gitrepo": "tsbuild",
"description": "A tool for compiling TypeScript files using the latest nightly features, offering flexible APIs and a CLI for streamlined development.", "description": "A tool for compiling TypeScript files using the latest nightly features, offering flexible APIs and a CLI for streamlined development.",
"npmPackagename": "@gitzone/tsbuild", "npmPackagename": "@git.zone/tsbuild",
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [
"TypeScript", "TypeScript",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@git.zone/tsbuild", "name": "@git.zone/tsbuild",
"version": "2.6.6", "version": "4.0.0",
"private": false, "private": false,
"description": "A tool for compiling TypeScript files using the latest nightly features, offering flexible APIs and a CLI for streamlined development.", "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", "main": "dist_ts/index.js",
@@ -10,7 +10,7 @@
"tsbuild": "./cli.js" "tsbuild": "./cli.js"
}, },
"scripts": { "scripts": {
"test": "tsrun test/test.ts --verbose", "test": "tstest test/test.ts --verbose",
"build": "node cli.ts.js --web", "build": "node cli.ts.js --web",
"buildDocs": "tsdoc" "buildDocs": "tsdoc"
}, },
@@ -29,27 +29,28 @@
"development", "development",
"API" "API"
], ],
"author": "Lossless GmbH", "author": "Task Venture Capital GmbH",
"license": "MIT", "license": "MIT",
"bugs": { "bugs": {
"url": "https://code.foss.global/git.zone/tsbuild/issues" "url": "https://code.foss.global/git.zone/tsbuild/issues"
}, },
"homepage": "https://code.foss.global/git.zone/tsbuild#README", "homepage": "https://code.foss.global/git.zone/tsbuild#README",
"dependencies": { "dependencies": {
"@git.zone/tspublish": "^1.10.2", "@git.zone/tspublish": "^1.10.3",
"@push.rocks/early": "^4.0.4", "@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/smartdelay": "^3.0.5",
"@push.rocks/smartfile": "^11.2.7", "@push.rocks/smartfile": "^13.1.2",
"@push.rocks/smartlog": "^3.1.8", "@push.rocks/smartfs": "^1.2.0",
"@push.rocks/smartlog": "^3.1.10",
"@push.rocks/smartpath": "^6.0.0", "@push.rocks/smartpath": "^6.0.0",
"@push.rocks/smartpromise": "^4.2.3", "@push.rocks/smartpromise": "^4.2.3",
"typescript": "5.9.2" "typescript": "5.9.3"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsrun": "^1.2.47", "@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^2.3.4", "@git.zone/tstest": "^3.1.3",
"@types/node": "^22.15.21" "@types/node": "^25.0.1"
}, },
"files": [ "files": [
"ts/**/*", "ts/**/*",

5892
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +0,0 @@
onlyBuiltDependencies:
- esbuild
- mongodb-memory-server
- puppeteer

View File

@@ -0,0 +1,215 @@
# @git.zone/tsbuild - Project Memory
## Quick Reference
### Public API - Classes
1. **TsCompiler** - Main compilation class with compileFiles, compileGlob, checkTypes, checkEmit methods
2. **TsConfig** - TypeScript configuration management (tsconfig.json handling)
3. **TsPublishConfig** - TsPublish configuration (tspublish.json handling)
4. **TsUnpacker** - Output directory flattening
5. **FsHelpers** - Filesystem utilities (static methods)
6. **TsBuildCli** - CLI command handler
### CLI Commands (5)
1. **tsbuild** (default) - Compiles ./ts/**/*.ts → ./dist_ts/
2. **tsbuild custom <dir1> <dir2>** - Custom directory compilation
3. **tsbuild tsfolders** - Auto-discover ts_* folders, compile in order
4. **tsbuild emitcheck <pattern>** - Validate emit without output
5. **tsbuild check [pattern]** - Type check only
### CLI Flags
- `--skiplibcheck` - Skip .d.ts type checking (shows warning)
- `--confirmskiplibcheck` - Extended warning with 5s pause
- `--disallowimplicitany` - Stricter type checking
- `--commonjs` - Use CommonJS instead of ESNext
- `--quiet` - Suppress non-error output
- `--json` - JSON output format
## Architecture - Modular Structure
### Module Organization
```
ts/
index.ts # Main entry, re-exports all modules
plugins.ts # Dependency imports only
mod_fs/
index.ts # exports
classes.fshelpers.ts # FsHelpers - static filesystem utilities
mod_config/
index.ts # exports
classes.tsconfig.ts # TsConfig - tsconfig.json handling
classes.tspublishconfig.ts # TsPublishConfig - tspublish.json handling
mod_unpack/
index.ts # exports
classes.tsunpacker.ts # TsUnpacker - output flattening
mod_compiler/
index.ts # exports
classes.tscompiler.ts # TsCompiler + legacy compatibility functions
mod_cli/
index.ts # exports
classes.tsbuildcli.ts # TsBuildCli - CLI command handler
```
### Class Responsibilities
**TsCompiler** (`mod_compiler/classes.tscompiler.ts`)
- Core compilation with `compileFiles()`, `compileGlob()`
- Type checking with `checkTypes()`, `checkEmit()`
- Configuration via TsConfig
- Automatic unpacking via TsUnpacker
**TsConfig** (`mod_config/classes.tsconfig.ts`)
- Load and parse tsconfig.json
- Merge configuration with priority order
- Protected defaults handling
**TsPublishConfig** (`mod_config/classes.tspublishconfig.ts`)
- Load and parse tspublish.json
- `shouldUnpack` property (default true)
- `order` property for tsfolders ordering
**TsUnpacker** (`mod_unpack/classes.tsunpacker.ts`)
- Detect nested output structure
- Flatten output directories
- Configurable via TsPublishConfig
**FsHelpers** (`mod_fs/classes.fshelpers.ts`)
- `listFilesWithGlob()` - Glob pattern file listing
- `extractSourceFolder()` - Pattern parsing
- File/directory existence checks
- Directory operations
**TsBuildCli** (`mod_cli/classes.tsbuildcli.ts`)
- All CLI commands registered
- Uses TsCompiler for compilation
## Configuration Priority (5 levels)
1. Default options (hardcoded in TsConfig)
2. tsconfig.json (if exists)
3. Protected defaults (ensure integrity)
4. Programmatic options (function params)
5. CLI flags (highest priority)
### Protected Options
Cannot be overridden by tsconfig.json alone:
- `outDir: 'dist_ts/'` - Path transformation logic
- `noEmitOnError: true` - Build integrity
- `declaration: true` - Library support
- `inlineSourceMap: true` - Debugging
### Path Transformation
- Automatic: `./ts_interfaces``./dist_ts_interfaces`
- In tsconfig paths: `./ts_*``./dist_ts_*` (first array element only)
## Default Compiler Options
- Module: NodeNext (ESM with CommonJS fallback)
- Target: ESNext (latest JavaScript)
- Source Maps: Inline (no separate .map files)
- Declaration Files: ALWAYS generated (protected)
- Output: dist_ts/
- Implicit any: ALLOWED by default
- esModuleInterop: true
- verbatimModuleSyntax: true
## Error Handling
### Error Summary Structure
```typescript
interface IErrorSummary {
errorsByFile: Record<string, Diagnostic[]>
generalErrors: Diagnostic[]
totalErrors: number
totalFiles: number
}
```
### Compile Result Structure
```typescript
interface ICompileResult {
emittedFiles: string[]
errorSummary: IErrorSummary
}
```
## Dependencies Used
- @git.zone/tspublish@^1.10.3 - Module ordering
- @push.rocks/smartcli@^4.0.19 - CLI framework
- @push.rocks/smartfile@^13.1.2 - File content handling
- @push.rocks/smartfs@^1.2.0 - Filesystem operations
- @push.rocks/smartpath@^6.0.0 - Path transformation utilities
- @push.rocks/smartpromise@^4.2.3 - Promise utilities
- @push.rocks/smartdelay@^3.0.5 - Delay utilities
- typescript@5.9.3 - TypeScript compiler
### smartfs Usage
- File listing: `smartfs.directory(path).recursive().filter(pattern).list()`
- File existence: `smartfs.file(path).exists()`
- Directory existence: `smartfs.directory(path).exists()`
- FsHelpers wraps smartfs for glob pattern support
## Unpack Feature
When TypeScript compiles files that import from sibling directories, it creates nested output:
```
dist_ts_core/
ts_core/ ← nested output
ts_shared/ ← pulled-in dependency
```
The unpack feature automatically flattens this to:
```
dist_ts_core/
index.js ← flat
```
### Configuration
- Reads `tspublish.json` from source folder
- `unpack: true` (default if not present) → flatten output
- `unpack: false` → skip unpacking
### Implementation
- `TsUnpacker` class in `mod_unpack/classes.tsunpacker.ts`
- Called automatically after each successful compilation in `TsCompiler.compileGlob()`
## Special Behaviors
### tsfolders Command Ordering
1. Always: ts_interfaces first (if no tspublish.json)
2. Always: ts_shared second (if no tspublish.json)
3. Then: Other folders by `order` property in their tspublish.json
4. Finally: Folders without order property (Infinity)
### check Command Default (No Arguments)
Two-phase check:
1. Phase 1: Type check ts/**/* (strict, include .d.ts)
2. Phase 2: Type check test/**/* (relaxed, skipLibCheck: true)
## Edge Cases
1. **Empty file list** - Returns [], no error
2. **Glob duplicates** - Files compile multiple times
3. **Non-existent files** - TypeScript "file not found" errors
4. **skipLibCheck warning** - 1-line default, 5s pause with --confirmskiplibcheck
5. **Missing tsconfig.json** - Graceful fallback, no error
6. **Module resolution** - --commonjs switches to NodeJs (not NodeNext)
7. **Source maps** - Inline only (not separate .map files)
8. **File filtering** - Only .ts and .tsx; .d.ts and .js ignored
## Build Safety Features
- `noEmitOnError: true` - Prevents broken builds
- Error aggregation before final output
- Protected options ensure integrity
- Pre-emit checks before emit phase
- CLI exit code handling (0=success, 1=error)
## Recent Changes
- 3.1.4+ - Major restructuring with mod_* modules
- Full OO architecture with TsCompiler, TsConfig, TsPublishConfig, TsUnpacker, TsBuildCli, FsHelpers
- Backward compatibility maintained for legacy functions
- Automatic "unpack" feature for nested output directories
- Migrated filesystem operations from smartfile to smartfs

1017
readme.md

File diff suppressed because it is too large Load Diff

View File

@@ -6,12 +6,12 @@ early.stop();
import { anExportedString } from './tocompile2.js'; import { anExportedString } from './tocompile2.js';
console.log(anExportedString); console.log(anExportedString);
class test2 { class test2 {
test = [];
constructor() { constructor() {
this.test = [];
console.log('hi'); console.log('hi');
} }
} }
const run = async () => { const run = async () => {
return 'hi'; return 'hi';
}; };
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidG9jb21waWxlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdG9jb21waWxlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLENBQUM7QUFDcEIsT0FBTyxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUMsQ0FBQztBQUVyQixPQUFPLEtBQUssS0FBSyxNQUFNLG1CQUFtQixDQUFDO0FBRTNDLEtBQUssQ0FBQyxLQUFLLEVBQUUsQ0FBQztBQUNkLEtBQUssQ0FBQyxJQUFJLEVBQUUsQ0FBQztBQUViLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQ25ELE9BQU8sQ0FBQyxHQUFHLENBQUMsZ0JBQWdCLENBQUMsQ0FBQztBQUU5QixNQUFNLEtBQUs7SUFFVDtRQURBLFNBQUksR0FBYSxFQUFFLENBQUM7UUFFbEIsT0FBTyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUNwQixDQUFDO0NBQ0Y7QUFFRCxNQUFNLEdBQUcsR0FBRyxLQUFLLElBQXFCLEVBQUU7SUFDdEMsT0FBTyxJQUFJLENBQUM7QUFDZCxDQUFDLENBQUMifQ== //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidG9jb21waWxlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdG9jb21waWxlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLENBQUM7QUFDcEIsT0FBTyxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUMsQ0FBQztBQUVyQixPQUFPLEtBQUssS0FBSyxNQUFNLG1CQUFtQixDQUFDO0FBRTNDLEtBQUssQ0FBQyxLQUFLLEVBQUUsQ0FBQztBQUNkLEtBQUssQ0FBQyxJQUFJLEVBQUUsQ0FBQztBQUViLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQ25ELE9BQU8sQ0FBQyxHQUFHLENBQUMsZ0JBQWdCLENBQUMsQ0FBQztBQUU5QixNQUFNLEtBQUs7SUFDVCxJQUFJLEdBQWEsRUFBRSxDQUFDO0lBQ3BCO1FBQ0UsT0FBTyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUNwQixDQUFDO0NBQ0Y7QUFFRCxNQUFNLEdBQUcsR0FBRyxLQUFLLElBQXFCLEVBQUU7SUFDdEMsT0FBTyxJQUFJLENBQUM7QUFDZCxDQUFDLENBQUMifQ==

View File

@@ -1,19 +1,21 @@
import { tap, expect } from '@git.zone/tstest/tapbundle'; import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as tsbuild from '../ts/index.js'; import { TsCompiler } from '../ts/index.js';
let assetfiles: string[] = ['./test/assets/tocompile.ts', './test/assets/tocompile2.ts']; const assetfiles: string[] = ['./test/assets/tocompile.ts', './test/assets/tocompile2.ts'];
let assetfiles2 = { const assetfiles2 = {
'./test/assets/**/!(*.d.ts|*.js|output)': './test/assets/output', './test/assets/**/!(*.d.ts|*.js|output)': './test/assets/output',
}; };
tap.test('should convert files from an array with single files to output', async (tools) => { tap.test('should compile files from an array', async () => {
tsbuild.compileFileArray(assetfiles, { outDir: './test/assets/output' }); const compiler = new TsCompiler();
await compiler.compileFilesOrThrow(assetfiles, { outDir: './test/assets/output' });
}); });
tap.test('should convert files from an array with single files to output', async (tools) => { tap.test('should compile files from glob pattern object', async () => {
tsbuild.compileGlobStringObject(assetfiles2); const compiler = new TsCompiler();
await compiler.compileGlob(assetfiles2);
}); });
tap.start(); tap.start();

View File

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

View File

@@ -1,5 +1,14 @@
import * as early from '@push.rocks/early'; import * as early from '@push.rocks/early';
early.start('tsbuild'); early.start('tsbuild');
export * from './tsbuild.exports.js';
export * from './tsbuild.cli.js'; // Export from new modular structure
export * from './mod_fs/index.js';
export * from './mod_config/index.js';
export * from './mod_unpack/index.js';
export * from './mod_compiler/index.js';
export * from './mod_cli/index.js';
// Re-export TypeScript types for convenience
export type { CompilerOptions, ScriptTarget, ModuleKind } from 'typescript';
early.stop(); early.stop();

View File

@@ -0,0 +1,320 @@
import * as path from 'path';
import * as smartcli from '@push.rocks/smartcli';
import * as smartpath from '@push.rocks/smartpath';
import * as tspublish from '@git.zone/tspublish';
import { TsCompiler } from '../mod_compiler/index.js';
import { FsHelpers } from '../mod_fs/index.js';
/**
* TsBuildCli handles all CLI commands for tsbuild.
* Provides commands for compiling, type checking, and emit validation.
*/
export class TsBuildCli {
private cli: smartcli.Smartcli;
private cwd: string;
constructor(cwd: string = process.cwd()) {
this.cwd = cwd;
this.cli = new smartcli.Smartcli();
this.registerCommands();
}
/**
* Register all CLI commands
*/
private registerCommands(): void {
this.registerStandardCommand();
this.registerCustomCommand();
this.registerEmitCheckCommand();
this.registerTsFoldersCommand();
this.registerCheckCommand();
}
/**
* Standard command: compiles ts folder to dist_ts
*/
private registerStandardCommand(): void {
this.cli.standardCommand().subscribe(async (argvArg) => {
const compiler = new TsCompiler(this.cwd, argvArg);
await compiler.compileGlob({
'./ts/**/*.ts': './dist_ts',
});
const summary = (argvArg as any)?.__tsbuildFinalErrorSummary;
if (summary && summary.totalErrors > 0) {
process.exit(1);
}
});
}
/**
* Custom command: compiles specified directories to dist_ prefixed directories
*/
private registerCustomCommand(): void {
this.cli.addCommand('custom').subscribe(async (argvArg) => {
const listedDirectories = argvArg._;
listedDirectories.shift(); // removes the first element that is "custom"
const compilationCommandObject: Record<string, string> = {};
for (const directory of listedDirectories) {
compilationCommandObject[`./${directory}/**/*.ts`] = `./dist_${directory}`;
}
const compiler = new TsCompiler(this.cwd, argvArg);
await compiler.compileGlob(compilationCommandObject);
const summary = (argvArg as any)?.__tsbuildFinalErrorSummary;
if (summary && summary.totalErrors > 0) {
process.exit(1);
}
});
}
/**
* Emit check command: validates files can be emitted without producing output
*/
private registerEmitCheckCommand(): void {
this.cli.addCommand('emitcheck').subscribe(async (argvArg) => {
const patterns = argvArg._.slice(1);
if (patterns.length === 0) {
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);
}
const allFiles = await this.collectFilesFromPatterns(patterns);
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`);
const compiler = new TsCompiler(this.cwd, argvArg);
const success = await compiler.checkEmit(allFiles);
process.exit(success ? 0 : 1);
});
}
/**
* TsFolders command: compiles all ts_* directories in order
*/
private registerTsFoldersCommand(): void {
this.cli.addCommand('tsfolders').subscribe(async (argvArg) => {
// List folders matching /^ts/ regex
const allEntries = await FsHelpers.listDirectory(this.cwd);
const tsFolders = allEntries
.filter((e) => e.isDirectory && /^ts/.test(e.name))
.map((e) => e.name);
// Get tspublish.json based ranking
const tsPublishInstance = new tspublish.TsPublish();
const tsPublishModules = await tsPublishInstance.getModuleSubDirs(this.cwd);
// Create an array with folder names and their ranks
const foldersWithOrder: Array<{ folder: string; rank: number }> = [];
for (const folder of tsFolders) {
let rank = Infinity;
if (tsPublishModules[folder] && tsPublishModules[folder].order !== undefined) {
rank = tsPublishModules[folder].order;
}
foldersWithOrder.push({ folder, rank });
}
// Sort the folders based on rank
foldersWithOrder.sort((a, b) => a.rank - b.rank);
// Construct the sorted list of folders
const sortedTsFolders: string[] = [];
for (const item of foldersWithOrder) {
sortedTsFolders.push(item.folder);
}
// Ensure ts_interfaces is first and ts_shared is second if they exist
const ensurePosition = (folderName: string, position: number) => {
if (tsFolders.indexOf(folderName) > -1 && Object.keys(tsPublishModules).indexOf(folderName) === -1) {
const currentIndex = sortedTsFolders.indexOf(folderName);
if (currentIndex > -1) {
sortedTsFolders.splice(currentIndex, 1);
}
sortedTsFolders.splice(position, 0, folderName);
}
};
ensurePosition('ts_interfaces', 0);
ensurePosition('ts_shared', 1);
// Display compilation plan
const folderCount = sortedTsFolders.length;
console.log(`\n📂 TypeScript Folder Compilation Plan (${folderCount} folder${folderCount !== 1 ? 's' : ''})`);
console.log('┌' + '─'.repeat(60) + '┐');
console.log('│ 🔄 Compilation Order │');
console.log('├' + '─'.repeat(60) + '┤');
sortedTsFolders.forEach((folder, index) => {
const prefix = index === folderCount - 1 ? '└─' : '├─';
const position = `${index + 1}/${folderCount}`;
console.log(`${prefix} ${position.padStart(5)} ${folder.padEnd(46)}`);
});
console.log('└' + '─'.repeat(60) + '┘\n');
// Build compilation object
const compilationCommandObject: Record<string, string> = {};
for (const tsFolder of sortedTsFolders) {
compilationCommandObject[`./${tsFolder}/**/*.ts`] = `./dist_${tsFolder}`;
}
const compiler = new TsCompiler(this.cwd, argvArg);
await compiler.compileGlob(compilationCommandObject);
const summary = (argvArg as any)?.__tsbuildFinalErrorSummary;
if (summary && summary.totalErrors > 0) {
process.exit(1);
}
});
}
/**
* Check command: type checks files without emitting
*/
private registerCheckCommand(): void {
this.cli.addCommand('check').subscribe(async (argvArg) => {
const patterns = argvArg._.slice(1);
// If no patterns provided, default to checking ts/**/* and then test/**/*
if (patterns.length === 0) {
await this.runDefaultTypeChecks(argvArg);
return;
}
const allFiles = await this.collectFilesFromPatterns(patterns);
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`);
const compiler = new TsCompiler(this.cwd, argvArg);
const success = await compiler.checkTypes(allFiles);
process.exit(success ? 0 : 1);
});
}
/**
* Run default type checks for ts/ and test/ directories
*/
private async runDefaultTypeChecks(argvArg: any): Promise<void> {
console.log('\n🔬 Running default type checking sequence...\n');
// First check ts/**/* without skiplibcheck
console.log('📂 Checking ts/**/* files...');
const tsTsFiles = await FsHelpers.listFilesWithGlob(this.cwd, 'ts/**/*.ts');
if (tsTsFiles.length > 0) {
console.log(` Found ${tsTsFiles.length} TypeScript files in ts/`);
const tsAbsoluteFiles = smartpath.transform.toAbsolute(tsTsFiles, this.cwd) as string[];
const tsCompiler = new TsCompiler(this.cwd, argvArg);
const tsSuccess = await tsCompiler.checkTypes(tsAbsoluteFiles);
if (!tsSuccess) {
console.error('❌ Type checking failed for ts/**/*');
process.exit(1);
}
console.log('✅ Type checking passed for ts/**/*\n');
} else {
console.log(' No TypeScript files found in ts/\n');
}
// Then check test/**/* with skiplibcheck
console.log('📂 Checking test/**/* files with --skiplibcheck...');
const testTsFiles = await FsHelpers.listFilesWithGlob(this.cwd, 'test/**/*.ts');
if (testTsFiles.length > 0) {
console.log(` Found ${testTsFiles.length} TypeScript files in test/`);
const testAbsoluteFiles = smartpath.transform.toAbsolute(testTsFiles, this.cwd) as string[];
const testArgvArg = { ...argvArg, skiplibcheck: true };
const testCompiler = new TsCompiler(this.cwd, testArgvArg);
const testSuccess = await testCompiler.checkTypes(testAbsoluteFiles);
if (!testSuccess) {
console.error('❌ Type checking failed for test/**/*');
process.exit(1);
}
console.log('✅ Type checking passed for test/**/*\n');
} else {
console.log(' No TypeScript files found in test/\n');
}
console.log('✅ All default type checks passed!\n');
process.exit(0);
}
/**
* Collect files from patterns (glob or direct paths)
*/
private async collectFilesFromPatterns(patterns: string[]): Promise<string[]> {
let allFiles: string[] = [];
for (const pattern of patterns) {
if (pattern.includes('*') || pattern.includes('{') || pattern.includes('?')) {
// Handle as glob pattern
console.log(`Processing glob pattern: ${pattern}`);
try {
const stringMatchedFiles = await FsHelpers.listFilesWithGlob(this.cwd, pattern);
if (stringMatchedFiles.length === 0) {
console.warn(`⚠️ Warning: No files matched the pattern '${pattern}'`);
} else {
console.log(`📂 Found ${stringMatchedFiles.length} files matching pattern '${pattern}'`);
const absoluteMatchedFiles = smartpath.transform.toAbsolute(stringMatchedFiles, this.cwd) as string[];
allFiles = allFiles.concat(absoluteMatchedFiles);
}
} catch (err) {
console.error(`❌ Error processing glob pattern '${pattern}': ${err}`);
}
} else {
// Handle as direct file path
const filePath = path.isAbsolute(pattern) ? pattern : path.join(this.cwd, pattern);
const fileExists = await FsHelpers.fileExists(filePath);
if (fileExists) {
allFiles.push(filePath);
} else {
console.error(`❌ Error: File not found: ${filePath}`);
process.exit(1);
}
}
}
// Filter to only TypeScript files
return allFiles.filter((file) => file.endsWith('.ts') || file.endsWith('.tsx'));
}
/**
* Start parsing CLI arguments
*/
public run(): void {
this.cli.startParse();
}
}
/**
* Run the CLI
*/
export const runCli = async (): Promise<void> => {
const cli = new TsBuildCli();
cli.run();
};

1
ts/mod_cli/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './classes.tsbuildcli.js';

View File

@@ -0,0 +1,536 @@
import type { CompilerOptions, Diagnostic, Program } from 'typescript';
import typescript from 'typescript';
import * as smartdelay from '@push.rocks/smartdelay';
import * as smartpromise from '@push.rocks/smartpromise';
import * as smartpath from '@push.rocks/smartpath';
import { TsConfig } from '../mod_config/index.js';
import { FsHelpers } from '../mod_fs/index.js';
import { performUnpack } from '../mod_unpack/index.js';
/**
* Interface for error summary data
*/
export interface IErrorSummary {
errorsByFile: Record<string, Diagnostic[]>;
generalErrors: Diagnostic[];
totalErrors: number;
totalFiles: number;
}
/**
* Interface for task information
*/
export interface ITaskInfo {
taskNumber: number;
totalTasks: number;
sourcePattern: string;
destDir: string;
fileCount: number;
}
/**
* Interface for compilation result
*/
export interface ICompileResult {
emittedFiles: string[];
errorSummary: IErrorSummary;
}
/**
* TsCompiler handles TypeScript compilation with error tracking,
* configuration management, and output unpacking.
*/
export class TsCompiler {
private config: TsConfig;
private cwd: string;
private argvArg?: any;
constructor(cwd: string = process.cwd(), argvArg?: any) {
this.cwd = cwd;
this.config = new TsConfig(cwd);
this.argvArg = argvArg;
}
/**
* Get the current working directory
*/
public getCwd(): string {
return this.cwd;
}
/**
* Get the TsConfig instance
*/
public getConfig(): TsConfig {
return this.config;
}
/**
* Create compiler options by merging defaults, tsconfig.json, and custom options
*/
public createOptions(customOptions: CompilerOptions = {}): CompilerOptions {
return this.config.merge(customOptions, this.argvArg);
}
/**
* Create a TypeScript program from file names and options
*/
private createProgram(fileNames: string[], options: CompilerOptions): Program {
return typescript.createProgram(fileNames, options);
}
/**
* Process TypeScript diagnostics and return error summary
*/
private processDiagnostics(diagnostics: readonly Diagnostic[]): IErrorSummary {
const errorsByFile: Record<string, Diagnostic[]> = {};
const generalErrors: Diagnostic[] = [];
diagnostics.forEach((diagnostic) => {
if (diagnostic.file) {
const fileName = diagnostic.file.fileName;
if (!errorsByFile[fileName]) {
errorsByFile[fileName] = [];
}
errorsByFile[fileName].push(diagnostic);
} else {
generalErrors.push(diagnostic);
}
});
return {
errorsByFile,
generalErrors,
totalErrors: diagnostics.length,
totalFiles: Object.keys(errorsByFile).length,
};
}
/**
* Display error summary to console
*/
private displayErrorSummary(errorSummary: IErrorSummary): void {
if (errorSummary.totalErrors === 0) {
return;
}
const { errorsByFile, generalErrors, totalErrors, totalFiles } = errorSummary;
// Print error summary header
console.log('\n' + '='.repeat(80));
console.log(
`❌ Found ${totalErrors} error${totalErrors !== 1 ? 's' : ''} in ${totalFiles} file${totalFiles !== 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 = 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) {
console.log(` ${lineContent.trimEnd()}`);
const indicator = ' '.repeat(character) + `${colors.red}^${colors.reset}`;
console.log(` ${indicator}`);
}
} catch {
// 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 = 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');
}
/**
* Handle skipLibCheck warning display
*/
private async handleSkipLibCheckWarning(): Promise<void> {
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 smartdelay.delayFor(5000);
} else if (!this.argvArg?.quiet && !this.argvArg?.json) {
console.log('⚠️ skipLibCheck enabled; use --confirmskiplibcheck to pause with warning.');
}
}
/**
* Compile files with error tracking (returns result instead of throwing)
*/
public async compileFiles(
fileNames: string[],
customOptions: CompilerOptions = {},
taskInfo?: ITaskInfo
): Promise<ICompileResult> {
const options = this.createOptions(customOptions);
if (options.skipLibCheck) {
await this.handleSkipLibCheckWarning();
}
// Enhanced logging with task info
const startTime = Date.now();
if (taskInfo) {
const { taskNumber, totalTasks, sourcePattern, fileCount } = taskInfo;
const relativeDestDir = taskInfo.destDir.replace(process.cwd(), '').replace(/^\//, '');
console.log(
`\n🔨 [${taskNumber}/${totalTasks}] Compiling ${fileCount} file${fileCount !== 1 ? 's' : ''} from ${sourcePattern}`
);
console.log(` 📁 Output: ${relativeDestDir}`);
} else {
console.log(`🔨 Compiling ${fileNames.length} files...`);
}
const done = smartpromise.defer<ICompileResult>();
const program = this.createProgram(fileNames, options);
// Check for pre-emit diagnostics first
const preEmitDiagnostics = typescript.getPreEmitDiagnostics(program);
const preEmitErrorSummary = this.processDiagnostics(preEmitDiagnostics);
// Only continue to emit phase if no pre-emit errors
if (preEmitErrorSummary.totalErrors > 0) {
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');
done.resolve({ emittedFiles: [], errorSummary: preEmitErrorSummary });
return done.promise;
}
// If no pre-emit errors, proceed with emit
const emitResult = program.emit();
const emitErrorSummary = this.processDiagnostics(emitResult.diagnostics);
// Combine error summaries
const combinedErrorSummary: IErrorSummary = {
errorsByFile: { ...preEmitErrorSummary.errorsByFile, ...emitErrorSummary.errorsByFile },
generalErrors: [...preEmitErrorSummary.generalErrors, ...emitErrorSummary.generalErrors],
totalErrors: preEmitErrorSummary.totalErrors + emitErrorSummary.totalErrors,
totalFiles: Object.keys({ ...preEmitErrorSummary.errorsByFile, ...emitErrorSummary.errorsByFile }).length,
};
const exitCode = emitResult.emitSkipped ? 1 : 0;
if (exitCode === 0) {
const endTime = Date.now();
const duration = endTime - startTime;
if (taskInfo) {
const { taskNumber, totalTasks } = taskInfo;
console.log(`✅ [${taskNumber}/${totalTasks}] Task completed in ${duration}ms`);
} else {
console.log(`✅ TypeScript emit succeeded! (${duration}ms)`);
}
// 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 (emitResult.emittedFiles && emitResult.emittedFiles.length > 0) {
console.log(
` 📄 Generated ${emitResult.emittedFiles.length} files: ${jsFiles} .js, ${dtsFiles} .d.ts, ${mapFiles} source maps`
);
}
done.resolve({ emittedFiles: emitResult.emittedFiles || [], errorSummary: combinedErrorSummary });
} else {
this.displayErrorSummary(combinedErrorSummary);
console.error('\n❌ TypeScript emit failed. Please investigate the errors listed above!');
console.error(' No output files have been generated.\n');
done.resolve({ emittedFiles: [], errorSummary: combinedErrorSummary });
}
return done.promise;
}
/**
* Compile files (throws on error)
*/
public async compileFilesOrThrow(fileNames: string[], customOptions: CompilerOptions = {}): Promise<string[]> {
const result = await this.compileFiles(fileNames, customOptions);
if (result.errorSummary.totalErrors > 0) {
throw new Error('TypeScript compilation failed.');
}
return result.emittedFiles;
}
/**
* Compile glob patterns with automatic unpacking
*/
public async compileGlob(
globPatterns: Record<string, string>,
customOptions: CompilerOptions = {}
): Promise<ICompileResult> {
const emittedFiles: string[] = [];
const errorSummaries: IErrorSummary[] = [];
const totalTasks = Object.keys(globPatterns).length;
let currentTask = 0;
const isQuiet = this.argvArg?.quiet === true;
const isJson = this.argvArg?.json === true;
if (!isQuiet && !isJson) {
console.log(`\n👷 TypeScript Compilation Tasks (${totalTasks} task${totalTasks !== 1 ? 's' : ''}):`);
Object.entries(globPatterns).forEach(([source, dest]) => {
console.log(` 📂 ${source}${dest}`);
});
console.log('');
}
for (const pattern of Object.keys(globPatterns)) {
const destPath = globPatterns[pattern];
if (!pattern || !destPath) continue;
// Get files matching the glob pattern
const files = await FsHelpers.listFilesWithGlob(this.cwd, pattern);
// Transform to absolute paths
const absoluteFiles = smartpath.transform.toAbsolute(files, this.cwd) as string[];
// Get destination directory as absolute path
const destDir = smartpath.transform.toAbsolute(destPath, this.cwd) as string;
// Update compiler options with the output directory
const options: CompilerOptions = {
...customOptions,
outDir: destDir,
};
currentTask++;
const taskInfo: ITaskInfo = {
taskNumber: currentTask,
totalTasks,
sourcePattern: pattern,
destDir: destPath,
fileCount: absoluteFiles.length,
};
const result = await this.compileFiles(absoluteFiles, options, taskInfo);
emittedFiles.push(...result.emittedFiles);
errorSummaries.push(result.errorSummary);
// Perform unpack if compilation succeeded
if (result.errorSummary.totalErrors === 0) {
await performUnpack(pattern, destDir, this.cwd);
}
}
// Merge all error summaries
const finalErrorSummary = this.mergeErrorSummaries(errorSummaries);
// 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: typescript.flattenDiagnosticMessageText(d.messageText as any, '\n'),
})),
])
),
};
console.log(JSON.stringify(result));
} else if (!isQuiet) {
this.displayFinalSummary(finalErrorSummary);
}
// Attach summary to argvArg for CLI exit behavior
if (this.argvArg && typeof this.argvArg === 'object') {
(this.argvArg as any).__tsbuildFinalErrorSummary = finalErrorSummary;
}
return {
emittedFiles,
errorSummary: finalErrorSummary,
};
}
/**
* Check if files can be emitted without actually emitting
*/
public async checkEmit(fileNames: string[], customOptions: CompilerOptions = {}): Promise<boolean> {
const options = { ...this.createOptions(customOptions), noEmit: true };
const fileCount = fileNames.length;
console.log(`\n🔍 Checking if ${fileCount} file${fileCount !== 1 ? 's' : ''} can be emitted...`);
const program = this.createProgram(fileNames, options);
const preEmitDiagnostics = typescript.getPreEmitDiagnostics(program);
const preEmitErrorSummary = this.processDiagnostics(preEmitDiagnostics);
const emitResult = program.emit(undefined, undefined, undefined, true);
const emitErrorSummary = this.processDiagnostics(emitResult.diagnostics);
const combinedErrorSummary: IErrorSummary = {
errorsByFile: { ...preEmitErrorSummary.errorsByFile, ...emitErrorSummary.errorsByFile },
generalErrors: [...preEmitErrorSummary.generalErrors, ...emitErrorSummary.generalErrors],
totalErrors: preEmitErrorSummary.totalErrors + emitErrorSummary.totalErrors,
totalFiles: Object.keys({ ...preEmitErrorSummary.errorsByFile, ...emitErrorSummary.errorsByFile }).length,
};
const success = combinedErrorSummary.totalErrors === 0 && !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 {
this.displayErrorSummary(combinedErrorSummary);
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;
}
/**
* Check TypeScript files for type errors without emission
*/
public async checkTypes(fileNames: string[], customOptions: CompilerOptions = {}): Promise<boolean> {
const options = { ...this.createOptions(customOptions), noEmit: true };
const fileCount = fileNames.length;
console.log(`\n🔍 Type checking ${fileCount} TypeScript file${fileCount !== 1 ? 's' : ''}...`);
const program = this.createProgram(fileNames, options);
const diagnostics = typescript.getPreEmitDiagnostics(program);
const errorSummary = this.processDiagnostics(diagnostics);
const success = errorSummary.totalErrors === 0;
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 {
this.displayErrorSummary(errorSummary);
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;
}
/**
* Merge multiple error summaries into one
*/
private mergeErrorSummaries(summaries: IErrorSummary[]): IErrorSummary {
const mergedErrorsByFile: Record<string, Diagnostic[]> = {};
const mergedGeneralErrors: Diagnostic[] = [];
let totalErrors = 0;
summaries.forEach((summary) => {
Object.entries(summary.errorsByFile).forEach(([fileName, errors]) => {
if (!mergedErrorsByFile[fileName]) {
mergedErrorsByFile[fileName] = [];
}
mergedErrorsByFile[fileName] = mergedErrorsByFile[fileName].concat(errors);
});
mergedGeneralErrors.push(...summary.generalErrors);
totalErrors += summary.totalErrors;
});
return {
errorsByFile: mergedErrorsByFile,
generalErrors: mergedGeneralErrors,
totalErrors,
totalFiles: Object.keys(mergedErrorsByFile).length,
};
}
/**
* Display final compilation summary
*/
private displayFinalSummary(errorSummary: IErrorSummary): void {
if (errorSummary.totalErrors === 0) {
console.log('\n📊 \x1b[32mCompilation Summary: All tasks completed successfully! ✅\x1b[0m\n');
return;
}
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
yellow: '\x1b[33m',
cyan: '\x1b[36m',
brightRed: '\x1b[91m',
brightYellow: '\x1b[93m',
};
console.log('\n' + '='.repeat(80));
console.log(`📊 ${colors.brightYellow}Final Compilation Summary${colors.reset}`);
console.log('='.repeat(80));
if (errorSummary.totalFiles > 0) {
console.log(`${colors.brightRed}❌ Files with errors (${errorSummary.totalFiles}):${colors.reset}`);
Object.entries(errorSummary.errorsByFile).forEach(([fileName, errors]) => {
const displayPath = fileName.replace(process.cwd(), '').replace(/^\//, '');
console.log(
` ${colors.red}${colors.reset} ${colors.cyan}${displayPath}${colors.reset} ${colors.yellow}(${errors.length} error${errors.length !== 1 ? 's' : ''})${colors.reset}`
);
});
}
if (errorSummary.generalErrors.length > 0) {
console.log(`${colors.brightRed}❌ General errors: ${errorSummary.generalErrors.length}${colors.reset}`);
}
console.log(
`\n${colors.brightRed}Total: ${errorSummary.totalErrors} error${errorSummary.totalErrors !== 1 ? 's' : ''} across ${errorSummary.totalFiles} file${errorSummary.totalFiles !== 1 ? 's' : ''}${colors.reset}`
);
console.log('='.repeat(80) + '\n');
}
}

1
ts/mod_compiler/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './classes.tscompiler.js';

View File

@@ -0,0 +1,180 @@
import * as fs from 'fs';
import * as path from 'path';
import type { CompilerOptions } from 'typescript';
import typescript from 'typescript';
/**
* Default compiler options for TypeScript compilation
*/
export const compilerOptionsDefault: CompilerOptions = {
declaration: true,
inlineSourceMap: true,
noEmitOnError: true,
outDir: 'dist_ts/',
module: typescript.ModuleKind.NodeNext,
target: typescript.ScriptTarget.ESNext,
moduleResolution: typescript.ModuleResolutionKind.NodeNext,
lib: ['lib.dom.d.ts', 'lib.esnext.d.ts'],
noImplicitAny: false,
esModuleInterop: true,
verbatimModuleSyntax: true,
baseUrl: './',
};
/**
* TsConfig handles loading and merging TypeScript compiler configurations.
* It supports reading tsconfig.json and merging with defaults and custom options.
*/
export class TsConfig {
private cwd: string;
private cachedTsConfig: CompilerOptions | null = null;
constructor(cwd: string = process.cwd()) {
this.cwd = cwd;
}
/**
* Get the current working directory
*/
public getCwd(): string {
return this.cwd;
}
/**
* Load and parse tsconfig.json from the current directory
*/
public load(): CompilerOptions {
if (this.cachedTsConfig !== null) {
return this.cachedTsConfig;
}
let tsconfig: any;
try {
const tsconfigPath = path.join(this.cwd, 'tsconfig.json');
const tsconfigContent = fs.readFileSync(tsconfigPath, 'utf8');
tsconfig = JSON.parse(tsconfigContent);
} catch {
this.cachedTsConfig = {};
return {};
}
if (!tsconfig || !tsconfig.compilerOptions) {
this.cachedTsConfig = {};
return {};
}
const returnObject: CompilerOptions = { ...tsconfig.compilerOptions };
// Convert target string to enum
if (tsconfig.compilerOptions.target && typeof tsconfig.compilerOptions.target === 'string') {
const targetKey = tsconfig.compilerOptions.target.toUpperCase();
if (targetKey in typescript.ScriptTarget) {
returnObject.target = typescript.ScriptTarget[targetKey as keyof typeof typescript.ScriptTarget];
}
}
// Convert module string to enum
if (tsconfig.compilerOptions.module && typeof tsconfig.compilerOptions.module === 'string') {
const moduleKey = tsconfig.compilerOptions.module.toUpperCase();
if (moduleKey in typescript.ModuleKind) {
returnObject.module = typescript.ModuleKind[moduleKey as keyof typeof typescript.ModuleKind];
} else if (moduleKey === 'NODENEXT') {
returnObject.module = typescript.ModuleKind.NodeNext;
}
}
// Convert moduleResolution string to enum
if (tsconfig.compilerOptions.moduleResolution && typeof tsconfig.compilerOptions.moduleResolution === 'string') {
const moduleResolutionKey = tsconfig.compilerOptions.moduleResolution.toUpperCase();
if (moduleResolutionKey in typescript.ModuleResolutionKind) {
returnObject.moduleResolution = typescript.ModuleResolutionKind[
moduleResolutionKey as keyof typeof typescript.ModuleResolutionKind
];
} else if (moduleResolutionKey === 'NODENEXT') {
returnObject.moduleResolution = typescript.ModuleResolutionKind.NodeNext;
}
}
// Apply path transformations (ts_ → dist_ts_)
if (tsconfig.compilerOptions.paths) {
returnObject.paths = { ...tsconfig.compilerOptions.paths };
for (const pathKey of Object.keys(returnObject.paths)) {
if (Array.isArray(returnObject.paths[pathKey]) && returnObject.paths[pathKey].length > 0) {
returnObject.paths[pathKey][0] = returnObject.paths[pathKey][0].replace('./ts_', './dist_ts_');
}
}
}
this.cachedTsConfig = returnObject;
return returnObject;
}
/**
* Get default compiler options
*/
public getDefaultOptions(): CompilerOptions {
return { ...compilerOptionsDefault };
}
/**
* Get critical/protected default options that shouldn't be overridden by tsconfig.json
*/
public getProtectedDefaults(): CompilerOptions {
return {
outDir: 'dist_ts/',
noEmitOnError: true,
declaration: true,
inlineSourceMap: true,
};
}
/**
* Process command line arguments and return applicable compiler options
*/
public getCommandLineOptions(argvArg?: any): CompilerOptions {
if (!argvArg) return {};
const options: CompilerOptions = {};
if (argvArg.skiplibcheck) {
options.skipLibCheck = true;
}
if (argvArg.disallowimplicitany) {
options.noImplicitAny = true;
}
if (argvArg.commonjs) {
options.module = typescript.ModuleKind.CommonJS;
options.moduleResolution = typescript.ModuleResolutionKind.NodeJs;
}
return options;
}
/**
* Merge compiler options with proper priority order:
* 1. Default options
* 2. tsconfig.json options
* 3. Protected defaults (cannot be overridden by tsconfig)
* 4. Custom options (programmatic)
* 5. CLI options (highest priority)
*/
public merge(customOptions: CompilerOptions = {}, argvArg?: any): CompilerOptions {
return {
...this.getDefaultOptions(),
...this.load(),
...this.getProtectedDefaults(),
...customOptions,
...this.getCommandLineOptions(argvArg),
};
}
/**
* Clear the cached tsconfig (useful for reloading)
*/
public clearCache(): void {
this.cachedTsConfig = null;
}
}

View File

@@ -0,0 +1,116 @@
import * as fs from 'fs';
import * as path from 'path';
/**
* Interface for tspublish.json configuration
*/
export interface ITsPublishJson {
order?: number;
unpack?: boolean;
[key: string]: any;
}
/**
* TsPublishConfig handles loading and parsing tspublish.json files.
* These configuration files control module-specific settings like
* compilation order and output unpacking behavior.
*/
export class TsPublishConfig {
private folderPath: string;
private cachedConfig: ITsPublishJson | null | undefined = undefined;
constructor(folderPath: string) {
this.folderPath = folderPath;
}
/**
* Get the folder path this config is for
*/
public getFolderPath(): string {
return this.folderPath;
}
/**
* Load and parse tspublish.json from the folder
* Returns null if file doesn't exist or is invalid
*/
public async load(): Promise<ITsPublishJson | null> {
if (this.cachedConfig !== undefined) {
return this.cachedConfig;
}
try {
const configPath = path.join(this.folderPath, 'tspublish.json');
const content = await fs.promises.readFile(configPath, 'utf8');
this.cachedConfig = JSON.parse(content);
return this.cachedConfig;
} catch {
this.cachedConfig = null;
return null;
}
}
/**
* Synchronously load and parse tspublish.json from the folder
* Returns null if file doesn't exist or is invalid
*/
public loadSync(): ITsPublishJson | null {
if (this.cachedConfig !== undefined) {
return this.cachedConfig;
}
try {
const configPath = path.join(this.folderPath, 'tspublish.json');
const content = fs.readFileSync(configPath, 'utf8');
this.cachedConfig = JSON.parse(content);
return this.cachedConfig;
} catch {
this.cachedConfig = null;
return null;
}
}
/**
* Check if output should be unpacked (flattened)
* Default is true if not specified
*/
public get shouldUnpack(): boolean {
const config = this.loadSync();
if (!config || config.unpack === undefined) {
return true; // Default to true
}
return config.unpack === true;
}
/**
* Get the compilation order for tsfolders command
* Returns Infinity if not specified (sorted last)
*/
public get order(): number {
const config = this.loadSync();
if (!config || config.order === undefined) {
return Infinity;
}
return config.order;
}
/**
* Check if tspublish.json exists in the folder
*/
public async exists(): Promise<boolean> {
try {
const configPath = path.join(this.folderPath, 'tspublish.json');
await fs.promises.access(configPath, fs.constants.F_OK);
return true;
} catch {
return false;
}
}
/**
* Clear the cached config (useful for reloading)
*/
public clearCache(): void {
this.cachedConfig = undefined;
}
}

2
ts/mod_config/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './classes.tsconfig.js';
export * from './classes.tspublishconfig.js';

View File

@@ -0,0 +1,144 @@
import * as fs from 'fs';
import * as path from 'path';
import * as smartfsModule from '@push.rocks/smartfs';
// Create a smartfs instance with Node.js provider
const smartfs = new smartfsModule.SmartFs(new smartfsModule.SmartFsProviderNode());
/**
* FsHelpers provides filesystem utility methods for tsbuild.
* All methods are static for convenience.
*/
export class FsHelpers {
/**
* The smartfs instance for filesystem operations
*/
public static readonly smartfs = smartfs;
/**
* List files matching a glob pattern like './ts/**\/*.ts'
* Parses the pattern to extract base directory and filter pattern
*/
public static async listFilesWithGlob(basePath: string, globPattern: string): Promise<string[]> {
// Remove leading ./ if present
const pattern = globPattern.replace(/^\.\//, '');
// Find the first directory part before any glob characters
const globChars = ['*', '?', '{', '['];
let baseDir = basePath;
// Find where the glob pattern starts
const parts = pattern.split('/');
const staticParts: string[] = [];
const filterParts: string[] = [];
let foundGlob = false;
for (const part of parts) {
if (!foundGlob && !globChars.some(c => part.includes(c))) {
staticParts.push(part);
} else {
foundGlob = true;
filterParts.push(part);
}
}
// Build the base directory
if (staticParts.length > 0) {
baseDir = path.join(basePath, ...staticParts);
}
// Build the filter pattern (just the filename part, ignoring ** for directories)
// The recursive() handles the ** part
const fileFilter = filterParts[filterParts.length - 1] || '*';
// Check if we need recursive search
const needsRecursive = filterParts.some(p => p === '**' || p.includes('**'));
let dirBuilder = smartfs.directory(baseDir);
if (needsRecursive) {
dirBuilder = dirBuilder.recursive();
}
try {
const entries = await dirBuilder.filter(fileFilter).list();
return entries.filter(e => e.isFile).map(e => e.path);
} catch {
// Directory doesn't exist or other error
return [];
}
}
/**
* Extract source folder name from a glob pattern
* './ts_core/**\/*.ts' → 'ts_core'
* 'ts_foo/**\/*.ts' → 'ts_foo'
*/
public static extractSourceFolder(pattern: string): string | null {
const match = pattern.match(/^\.?\/?([^\/\*]+)/);
return match ? match[1] : null;
}
/**
* Get the current working directory
*/
public static getCwd(): string {
return process.cwd();
}
/**
* Get the package directory (where package.json is located)
*/
public static getPackageDir(): string {
return path.resolve(__dirname, '../../');
}
/**
* Check if a file exists
*/
public static async fileExists(filePath: string): Promise<boolean> {
return smartfs.file(filePath).exists();
}
/**
* Check if a directory exists
*/
public static async directoryExists(dirPath: string): Promise<boolean> {
return smartfs.directory(dirPath).exists();
}
/**
* Read a JSON file and parse it
*/
public static readJsonSync<T = any>(filePath: string): T {
const content = fs.readFileSync(filePath, 'utf8');
return JSON.parse(content);
}
/**
* List directory contents
*/
public static async listDirectory(dirPath: string) {
return smartfs.directory(dirPath).list();
}
/**
* Remove a directory recursively
*/
public static async removeDirectory(dirPath: string): Promise<void> {
await fs.promises.rm(dirPath, { recursive: true });
}
/**
* Move/rename a file or directory
*/
public static async move(src: string, dest: string): Promise<void> {
await fs.promises.rename(src, dest);
}
/**
* Remove an empty directory
*/
public static async removeEmptyDirectory(dirPath: string): Promise<void> {
await fs.promises.rmdir(dirPath);
}
}

1
ts/mod_fs/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './classes.fshelpers.js';

View File

@@ -0,0 +1,153 @@
import * as fs from 'fs';
import * as path from 'path';
import { TsPublishConfig } from '../mod_config/index.js';
import { FsHelpers } from '../mod_fs/index.js';
/**
* TsUnpacker handles flattening of nested TypeScript output directories.
*
* When TypeScript compiles files that import from sibling directories,
* it creates a nested structure like:
* dist_ts_core/ts_core/index.js
* dist_ts_core/ts_shared/helper.js
*
* This class flattens it to:
* dist_ts_core/index.js
*/
export class TsUnpacker {
private sourceFolderName: string;
private destDir: string;
private cwd: string;
private config: TsPublishConfig;
constructor(sourceFolderName: string, destDir: string, cwd: string = process.cwd()) {
this.sourceFolderName = sourceFolderName;
this.destDir = destDir;
this.cwd = cwd;
this.config = new TsPublishConfig(path.join(cwd, sourceFolderName));
}
/**
* Create an unpacker from a glob pattern
* './ts_core/**\/*.ts' → sourceFolderName = 'ts_core'
*/
public static fromGlobPattern(
sourcePattern: string,
destDir: string,
cwd: string = process.cwd()
): TsUnpacker | null {
const sourceFolderName = FsHelpers.extractSourceFolder(sourcePattern);
if (!sourceFolderName) {
return null;
}
return new TsUnpacker(sourceFolderName, destDir, cwd);
}
/**
* Get the source folder name
*/
public getSourceFolderName(): string {
return this.sourceFolderName;
}
/**
* Get the destination directory
*/
public getDestDir(): string {
return this.destDir;
}
/**
* Check if unpacking should be performed based on tspublish.json config
* Default is true if not specified
*/
public async shouldUnpack(): Promise<boolean> {
return this.config.shouldUnpack;
}
/**
* Check if nested structure exists in the destination directory
*/
public async detectNesting(): Promise<boolean> {
const nestedPath = path.join(this.destDir, this.sourceFolderName);
return FsHelpers.directoryExists(nestedPath);
}
/**
* Get the path to the nested directory
*/
public getNestedPath(): string {
return path.join(this.destDir, this.sourceFolderName);
}
/**
* Perform the unpack operation - flatten nested output directories
* Returns true if unpacking was performed, false if skipped
*/
public async unpack(): Promise<boolean> {
// Check if we should unpack based on config
if (!(await this.shouldUnpack())) {
return false;
}
// Check if nested structure exists
if (!(await this.detectNesting())) {
return false;
}
const nestedPath = this.getNestedPath();
// Delete sibling folders (not the source folder)
await this.removeSiblingDirectories();
// Move contents from nested folder up
await this.moveNestedContentsUp();
// Remove empty nested folder
await FsHelpers.removeEmptyDirectory(nestedPath);
return true;
}
/**
* Remove sibling directories in the destination folder
* (directories other than the source folder being unpacked)
*/
private async removeSiblingDirectories(): Promise<void> {
const entries = await FsHelpers.listDirectory(this.destDir);
for (const entry of entries) {
if (entry.isDirectory && entry.name !== this.sourceFolderName) {
await FsHelpers.removeDirectory(path.join(this.destDir, entry.name));
}
}
}
/**
* Move contents from the nested folder up to the destination directory
*/
private async moveNestedContentsUp(): Promise<void> {
const nestedPath = this.getNestedPath();
const entries = await FsHelpers.listDirectory(nestedPath);
for (const entry of entries) {
const src = path.join(nestedPath, entry.name);
const dest = path.join(this.destDir, entry.name);
await FsHelpers.move(src, dest);
}
}
}
/**
* Convenience function to perform unpack operation
* Can be used directly without instantiating the class
*/
export async function performUnpack(
sourcePattern: string,
destDir: string,
cwd: string = process.cwd()
): Promise<boolean> {
const unpacker = TsUnpacker.fromGlobPattern(sourcePattern, destDir, cwd);
if (!unpacker) {
return false;
}
return unpacker.unpack();
}

1
ts/mod_unpack/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './classes.tsunpacker.js';

View File

@@ -1,4 +0,0 @@
import * as plugins from './plugins.js';
export const cwd = process.cwd();
export const packageDir = plugins.path.join(plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url), '../');

View File

@@ -1,16 +1,13 @@
// node native // node native
import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
export { export { fs, path };
path
}
// @git.zone scope // @git.zone scope
import * as tspublish from '@git.zone/tspublish'; import * as tspublish from '@git.zone/tspublish';
export { export { tspublish };
tspublish
}
// @push.rocks scope // @push.rocks scope
import * as smartcli from '@push.rocks/smartcli'; import * as smartcli from '@push.rocks/smartcli';
@@ -24,6 +21,4 @@ export { smartcli, smartdelay, smartfile, smartpath, smartpromise };
// third party scope // third party scope
import typescript from 'typescript'; import typescript from 'typescript';
export { export { typescript };
typescript
}

View File

@@ -1,568 +0,0 @@
// 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';
/**
* Interface for error summary data
*/
export interface IErrorSummary {
errorsByFile: Record<string, plugins.typescript.Diagnostic[]>;
generalErrors: plugins.typescript.Diagnostic[];
totalErrors: number;
totalFiles: number;
}
/**
* 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;
private taskInfo?: any;
/**
* Create a new TsBuild instance
*/
constructor(
fileNames: string[] = [],
customOptions: CompilerOptions = {},
argvArg?: any,
taskInfo?: any
) {
this.fileNames = fileNames;
this.argvArg = argvArg;
this.taskInfo = taskInfo;
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 process TypeScript diagnostics and return error summary
*/
private processDiagnostics(diagnostics: readonly plugins.typescript.Diagnostic[]): IErrorSummary {
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);
}
});
return {
errorsByFile,
generalErrors,
totalErrors: diagnostics.length,
totalFiles: Object.keys(errorsByFile).length
};
}
/**
* Helper function to display error summary
*/
private displayErrorSummary(errorSummary: IErrorSummary): void {
if (errorSummary.totalErrors === 0) {
return;
}
const { errorsByFile, generalErrors, totalErrors, totalFiles } = errorSummary;
// Print error summary header
console.log('\n' + '='.repeat(80));
console.log(`❌ Found ${totalErrors} error${totalErrors !== 1 ? 's' : ''} in ${totalFiles} file${totalFiles !== 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');
}
/**
* 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 and returns error summary
*/
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);
}
// Enhanced logging with task info
const startTime = Date.now();
if (this.taskInfo) {
const { taskNumber, totalTasks, sourcePattern, destDir, fileCount } = this.taskInfo;
const relativeDestDir = destDir.replace(process.cwd(), '').replace(/^\//, '');
console.log(`\n🔨 [${taskNumber}/${totalTasks}] Compiling ${fileCount} file${fileCount !== 1 ? 's' : ''} from ${sourcePattern}`);
console.log(` 📁 Output: ${relativeDestDir}`);
} else {
console.log(`🔨 Compiling ${this.fileNames.length} files...`);
}
const done = plugins.smartpromise.defer<{ emittedFiles: any[], errorSummary: IErrorSummary }>();
const program = this.createProgram();
// Check for pre-emit diagnostics first
const preEmitDiagnostics = plugins.typescript.getPreEmitDiagnostics(program);
const preEmitErrorSummary = this.processDiagnostics(preEmitDiagnostics);
// Only continue to emit phase if no pre-emit errors
if (preEmitErrorSummary.totalErrors > 0) {
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');
// Return error summary instead of exiting to allow final summary display
done.resolve({ emittedFiles: [], errorSummary: preEmitErrorSummary });
return done.promise;
}
// If no pre-emit errors, proceed with emit
const emitResult = program.emit();
const emitErrorSummary = this.processDiagnostics(emitResult.diagnostics);
// Combine error summaries
const combinedErrorSummary: IErrorSummary = {
errorsByFile: { ...preEmitErrorSummary.errorsByFile, ...emitErrorSummary.errorsByFile },
generalErrors: [...preEmitErrorSummary.generalErrors, ...emitErrorSummary.generalErrors],
totalErrors: preEmitErrorSummary.totalErrors + emitErrorSummary.totalErrors,
totalFiles: Object.keys({ ...preEmitErrorSummary.errorsByFile, ...emitErrorSummary.errorsByFile }).length
};
const exitCode = emitResult.emitSkipped ? 1 : 0;
if (exitCode === 0) {
const endTime = Date.now();
const duration = endTime - startTime;
if (this.taskInfo) {
const { taskNumber, totalTasks } = this.taskInfo;
console.log(`✅ [${taskNumber}/${totalTasks}] Task completed in ${duration}ms`);
} else {
console.log(`✅ TypeScript emit succeeded! (${duration}ms)`);
}
// 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({ emittedFiles: emitResult.emittedFiles || [], errorSummary: combinedErrorSummary });
} else {
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);
}
return done.promise;
}
/**
* 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 preEmitErrorSummary = this.processDiagnostics(preEmitDiagnostics);
// Only continue to emit phase if no pre-emit errors
if (preEmitErrorSummary.totalErrors > 0) {
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);
}
// If no pre-emit errors, proceed with emit
const emitResult = program.emit();
const emitErrorSummary = this.processDiagnostics(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 {
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);
}
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 preEmitErrorSummary = this.processDiagnostics(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 emitErrorSummary = this.processDiagnostics(emitResult.diagnostics);
// Combine error summaries
const combinedErrorSummary: IErrorSummary = {
errorsByFile: { ...preEmitErrorSummary.errorsByFile, ...emitErrorSummary.errorsByFile },
generalErrors: [...preEmitErrorSummary.generalErrors, ...emitErrorSummary.generalErrors],
totalErrors: preEmitErrorSummary.totalErrors + emitErrorSummary.totalErrors,
totalFiles: Object.keys({ ...preEmitErrorSummary.errorsByFile, ...emitErrorSummary.errorsByFile }).length
};
const success = combinedErrorSummary.totalErrors === 0 && !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 {
this.displayErrorSummary(combinedErrorSummary);
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 errorSummary = this.processDiagnostics(diagnostics);
// Set success flag
const success = errorSummary.totalErrors === 0;
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 {
this.displayErrorSummary(errorSummary);
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();
};

View File

@@ -1,327 +0,0 @@
import * as plugins from './plugins.js';
import * as paths from './paths.js';
import * as tsbuild from './tsbuild.exports.js';
export const runCli = async () => {
const tsbuildCli = new plugins.smartcli.Smartcli();
/**
* the standard task compiles anything in ts/ directory to dist directory
*/
tsbuildCli.standardCommand().subscribe(async (argvArg) => {
tsbuild.compileGlobStringObject(
{
'./ts/**/*.ts': './dist_ts',
},
{},
process.cwd(),
argvArg
);
});
/**
* the custom command compiles any customDir to dist_customDir
*/
tsbuildCli.addCommand('custom').subscribe(async (argvArg) => {
const listedDirectories = argvArg._;
listedDirectories.shift(); // removes the first element that is "custom"
const compilationCommandObject: { [key: string]: string } = {};
for (const directory of listedDirectories) {
compilationCommandObject[`./${directory}/**/*.ts`] = `./dist_${directory}`;
}
await tsbuild.compileGlobStringObject(compilationCommandObject, {}, process.cwd(), argvArg);
});
/**
* the emitcheck command checks if a TypeScript file can be emitted without actually emitting it
*/
tsbuildCli.addCommand('emitcheck').subscribe(async (argvArg) => {
const patterns = argvArg._.slice(1); // Remove the first element which is 'emitcheck'
if (patterns.length === 0) {
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);
}
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 emit check
const success = await tsbuild.emitCheck(allFiles, compilerOptions, argvArg);
// Exit with appropriate code
process.exit(success ? 0 : 1);
});
/**
* the custom command compiles any customDir to dist_customDir
*/
tsbuildCli.addCommand('tsfolders').subscribe(async (argvArg) => {
const tsFolders = await plugins.smartfile.fs.listFolders(paths.cwd, /^ts/);
// Now tsFolders contains all other folders except 'ts_shared' and 'ts_interfaces'
// We've established a base order. Now let's look at tspublish.json based ranking.
const tsPublishInstance = new plugins.tspublish.TsPublish();
const tsPublishModules = await tsPublishInstance.getModuleSubDirs(paths.cwd);
// tsPublishModules is an object: { [folderName]: tspublishJsonData }
// Create an array with folder names and their ranks
const foldersWithOrder = [];
for (const folder of tsFolders) {
let rank = Infinity; // Default rank if not specified
if (tsPublishModules[folder] && tsPublishModules[folder].order !== undefined) {
rank = tsPublishModules[folder].order;
}
foldersWithOrder.push({ folder, rank });
}
// Sort the folders based on rank
foldersWithOrder.sort((a, b) => a.rank - b.rank);
// Construct the sorted list of folders
const sortedTsFolders = [];
// Add the rest of the folders in sorted order
for (const item of foldersWithOrder) {
sortedTsFolders.push(item.folder);
}
// Let's make sure 'ts_shared' is always transpiled first
const ensurePosition = (folderNameArg: string, ensuredPosition: number) => {
if (tsFolders.indexOf(folderNameArg) > -1 && Object.keys(tsPublishModules).indexOf(folderNameArg) === -1) {
sortedTsFolders.splice(tsFolders.indexOf(folderNameArg), 1);
sortedTsFolders.splice(ensuredPosition, 0, folderNameArg);
}
}
ensurePosition('ts_interfaces', 0);
ensurePosition('ts_shared', 1);
const compilationCommandObject: { [key: string]: string } = {};
const folderCount = sortedTsFolders.length;
console.log(`\n📂 TypeScript Folder Compilation Plan (${folderCount} folder${folderCount !== 1 ? 's' : ''})`);
console.log('┌' + '─'.repeat(60) + '┐');
console.log('│ 🔄 Compilation Order │');
console.log('├' + '─'.repeat(60) + '┤');
sortedTsFolders.forEach((folder, index) => {
const prefix = index === folderCount - 1 ? '└─' : '├─';
const position = `${index + 1}/${folderCount}`;
console.log(`${prefix} ${position.padStart(5)} ${folder.padEnd(46)}`);
});
console.log('└' + '─'.repeat(60) + '┘\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 no patterns provided, default to checking ts/**/* and then test/**/*
if (patterns.length === 0) {
console.log('\n🔬 Running default type checking sequence...\n');
// First check ts/**/* without skiplibcheck
console.log('📂 Checking ts/**/* files...');
const tsFiles = await plugins.smartfile.fs.listFileTree(process.cwd(), 'ts/**/*.ts');
const tsTsFiles = Array.isArray(tsFiles)
? tsFiles.filter((item): item is string => typeof item === 'string')
: [];
if (tsTsFiles.length > 0) {
console.log(` Found ${tsTsFiles.length} TypeScript files in ts/`);
const tsAbsoluteFiles = plugins.smartpath.transform.toAbsolute(
tsTsFiles,
process.cwd()
) as string[];
const tsCompilerOptions = tsbuild.mergeCompilerOptions({}, argvArg);
const tsSuccess = await tsbuild.checkTypes(tsAbsoluteFiles, tsCompilerOptions, argvArg);
if (!tsSuccess) {
console.error('❌ Type checking failed for ts/**/*');
process.exit(1);
}
console.log('✅ Type checking passed for ts/**/*\n');
} else {
console.log(' No TypeScript files found in ts/\n');
}
// Then check test/**/* with skiplibcheck
console.log('📂 Checking test/**/* files with --skiplibcheck...');
const testFiles = await plugins.smartfile.fs.listFileTree(process.cwd(), 'test/**/*.ts');
const testTsFiles = Array.isArray(testFiles)
? testFiles.filter((item): item is string => typeof item === 'string')
: [];
if (testTsFiles.length > 0) {
console.log(` Found ${testTsFiles.length} TypeScript files in test/`);
const testAbsoluteFiles = plugins.smartpath.transform.toAbsolute(
testTsFiles,
process.cwd()
) as string[];
// Create new argvArg with skiplibcheck for test files
const testArgvArg = { ...argvArg, skiplibcheck: true };
const testCompilerOptions = tsbuild.mergeCompilerOptions({}, testArgvArg);
const testSuccess = await tsbuild.checkTypes(testAbsoluteFiles, testCompilerOptions, testArgvArg);
if (!testSuccess) {
console.error('❌ Type checking failed for test/**/*');
process.exit(1);
}
console.log('✅ Type checking passed for test/**/*\n');
} else {
console.log(' No TypeScript files found in test/\n');
}
console.log('✅ All default type checks passed!\n');
process.exit(0);
}
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,191 +0,0 @@
import * as plugins from './plugins.js';
import type { CompilerOptions, ScriptTarget, ModuleKind } from 'typescript';
import { compiler, mergeCompilerOptions, emitCheck, checkTypes } from './tsbuild.classes.tsbuild.js';
export type { CompilerOptions, ScriptTarget, ModuleKind };
export * from './tsbuild.classes.tsbuild.js';
/**
* Interface for task information
*/
export interface ITaskInfo {
taskNumber: number;
totalTasks: number;
sourcePattern: string;
destDir: string;
fileCount: number;
}
/**
* compile an array of absolute file paths with error tracking
*/
export let compileFileArrayWithErrorTracking = async (
fileStringArrayArg: string[],
compilerOptionsArg: CompilerOptions = {},
argvArg?: any,
taskInfo?: ITaskInfo
): Promise<{ emittedFiles: any[], errorSummary: import('./tsbuild.classes.tsbuild.js').IErrorSummary }> => {
const { TsBuild } = await import('./tsbuild.classes.tsbuild.js');
const tsBuild = new TsBuild(fileStringArrayArg, compilerOptionsArg, argvArg, taskInfo);
return tsBuild.compileWithErrorTracking();
};
/**
* compile am array of absolute file paths
*/
export let compileFileArray = (
fileStringArrayArg: string[],
compilerOptionsArg: CompilerOptions = {},
argvArg?: any
): Promise<any[]> => {
return compiler(fileStringArrayArg, mergeCompilerOptions(compilerOptionsArg, argvArg), argvArg);
};
/**
* Helper function to merge error summaries
*/
function mergeErrorSummaries(summaries: import('./tsbuild.classes.tsbuild.js').IErrorSummary[]): import('./tsbuild.classes.tsbuild.js').IErrorSummary {
const mergedErrorsByFile: Record<string, plugins.typescript.Diagnostic[]> = {};
const mergedGeneralErrors: plugins.typescript.Diagnostic[] = [];
let totalErrors = 0;
summaries.forEach(summary => {
// Merge errors by file
Object.entries(summary.errorsByFile).forEach(([fileName, errors]) => {
if (!mergedErrorsByFile[fileName]) {
mergedErrorsByFile[fileName] = [];
}
mergedErrorsByFile[fileName] = mergedErrorsByFile[fileName].concat(errors);
});
// Merge general errors
mergedGeneralErrors.push(...summary.generalErrors);
totalErrors += summary.totalErrors;
});
return {
errorsByFile: mergedErrorsByFile,
generalErrors: mergedGeneralErrors,
totalErrors,
totalFiles: Object.keys(mergedErrorsByFile).length
};
}
/**
* Helper function to display final compilation summary
*/
function displayFinalErrorSummary(errorSummary: import('./tsbuild.classes.tsbuild.js').IErrorSummary): void {
if (errorSummary.totalErrors === 0) {
console.log('\n📊 \x1b[32mCompilation Summary: All tasks completed successfully! ✅\x1b[0m\n');
return;
}
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
yellow: '\x1b[33m',
cyan: '\x1b[36m',
brightRed: '\x1b[91m',
brightYellow: '\x1b[93m'
};
console.log('\n' + '='.repeat(80));
console.log(`📊 ${colors.brightYellow}Final Compilation Summary${colors.reset}`);
console.log('='.repeat(80));
if (errorSummary.totalFiles > 0) {
console.log(`${colors.brightRed}❌ Files with errors (${errorSummary.totalFiles}):${colors.reset}`);
Object.entries(errorSummary.errorsByFile).forEach(([fileName, errors]) => {
const displayPath = fileName.replace(process.cwd(), '').replace(/^\//, '');
console.log(` ${colors.red}${colors.reset} ${colors.cyan}${displayPath}${colors.reset} ${colors.yellow}(${errors.length} error${errors.length !== 1 ? 's' : ''})${colors.reset}`);
});
}
if (errorSummary.generalErrors.length > 0) {
console.log(`${colors.brightRed}❌ General errors: ${errorSummary.generalErrors.length}${colors.reset}`);
}
console.log(`\n${colors.brightRed}Total: ${errorSummary.totalErrors} error${errorSummary.totalErrors !== 1 ? 's' : ''} across ${errorSummary.totalFiles} file${errorSummary.totalFiles !== 1 ? 's' : ''}${colors.reset}`);
console.log('='.repeat(80) + '\n');
}
/**
* compile advanced glob configurations
* @param globStringArrayArg a array of glob strings
* {
* './some/origin/folder/**\/*.ts': './some/destination/folder'
* }
*/
export let compileGlobStringObject = async (
globStringObjectArg: Record<string, string>,
tsOptionsArg: CompilerOptions = {},
cwdArg: string = process.cwd(),
argvArg?: any
) => {
let compiledFiles: any[] = [];
const errorSummaries: import('./tsbuild.classes.tsbuild.js').IErrorSummary[] = [];
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('');
for (const keyArg in globStringObjectArg) {
// Type safety check for key
if (keyArg && typeof keyArg === 'string' && globStringObjectArg[keyArg]) {
// Get files matching the glob pattern
const fileTreeArray = await plugins.smartfile.fs.listFileTree(cwdArg, keyArg);
// Ensure fileTreeArray contains only strings before transforming
const stringFileTreeArray = Array.isArray(fileTreeArray)
? fileTreeArray.filter((item): item is string => typeof item === 'string')
: [];
// Transform to absolute paths
const absoluteFilePathArray = plugins.smartpath.transform.toAbsolute(
stringFileTreeArray,
cwdArg
) as string[];
// Get destination directory as absolute path
const destDir: string = plugins.smartpath.transform.toAbsolute(
globStringObjectArg[keyArg],
cwdArg
) as string;
// Update compiler options with the output directory
const updatedTsOptions: CompilerOptions = {
...tsOptionsArg,
outDir: destDir,
};
// Compile with error tracking
currentTask++;
const taskInfo = {
taskNumber: currentTask,
totalTasks,
sourcePattern: keyArg,
destDir: globStringObjectArg[keyArg],
fileCount: absoluteFilePathArray.length
};
const result = await compileFileArrayWithErrorTracking(absoluteFilePathArray, updatedTsOptions, argvArg, taskInfo);
compiledFiles = compiledFiles.concat(result.emittedFiles);
errorSummaries.push(result.errorSummary);
}
}
// Display final error summary after all compilation tasks
const finalErrorSummary = mergeErrorSummaries(errorSummaries);
displayFinalErrorSummary(finalErrorSummary);
return compiledFiles;
};

View File

@@ -1,7 +1,5 @@
{ {
"compilerOptions": { "compilerOptions": {
"experimentalDecorators": true,
"useDefineForClassFields": false,
"target": "ES2022", "target": "ES2022",
"module": "NodeNext", "module": "NodeNext",
"moduleResolution": "NodeNext", "moduleResolution": "NodeNext",