Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0040325914 | |||
| f2980dc00f | |||
| 681f2b4c54 | |||
| fb6bd614d3 | |||
| 0eaca1f3d1 | |||
| aeaa957efa | |||
| e5fcbb9a09 | |||
| 4ebc37fa5a | |||
| 5bfe60927e | |||
| 9e6b91b891 | |||
| e40e8f6a77 |
23
changelog.md
23
changelog.md
@@ -1,5 +1,28 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-01-04 - 4.1.0 - feat(docs)
|
||||
update README with improved docs and monorepo/tspublish guidance; namespace and extend npmextra.json with release registries; bump several dependencies
|
||||
|
||||
- Bumped dependencies: @git.zone/tspublish ^1.10.3 → ^1.11.0, @push.rocks/smartfs ^1.2.0 → ^1.3.1, @git.zone/tstest ^3.1.3 → ^3.1.4, @types/node ^25.0.1 → ^25.0.3
|
||||
- npmextra.json reorganized: replaced legacy keys with namespaced entries (@git.zone/cli, @git.zone/tsdoc) and added @ship.zone/szci; added release configuration with registries [https://verdaccio.lossless.digital, https://registry.npmjs.org] and public accessLevel
|
||||
- README rewritten: improved formatting, added emojis and tables, new 'Issue Reporting and Security' section, monorepo/tspublish usage docs, auto-unpack and decorator guidance, CI/CD and troubleshooting improvements
|
||||
- Non-breaking changes; recommended next semantic version bump is minor
|
||||
|
||||
## 2025-12-14 - 4.0.2 - fix(TsCompiler)
|
||||
Clear output directories before compilation to ensure clean builds and avoid stale files
|
||||
|
||||
- TsCompiler.compileGlob now clears the destination directory (if it exists) before compiling each glob pattern.
|
||||
- Clearing is logged unless --quiet or --json flags are set (e.g. "🧹 Clearing output directory: <dest>").
|
||||
- Uses FsHelpers.removeDirectory to remove previous output, preventing stale or duplicate emitted files.
|
||||
- Documentation (readme.md) updated to advertise automatic output directory management / clean builds.
|
||||
- Removed stale compiled test artifacts from test/assets/output to avoid interference with tests.
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
{
|
||||
"npmci": {
|
||||
"npmGlobalTools": [],
|
||||
"npmAccessLevel": "public"
|
||||
},
|
||||
"gitzone": {
|
||||
"@git.zone/cli": {
|
||||
"projectType": "npm",
|
||||
"module": {
|
||||
"githost": "gitlab.com",
|
||||
"gitscope": "gitzone",
|
||||
"gitrepo": "tsbuild",
|
||||
"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",
|
||||
"keywords": [
|
||||
"TypeScript",
|
||||
@@ -23,9 +19,19 @@
|
||||
"development",
|
||||
"API"
|
||||
]
|
||||
},
|
||||
"release": {
|
||||
"registries": [
|
||||
"https://verdaccio.lossless.digital",
|
||||
"https://registry.npmjs.org"
|
||||
],
|
||||
"accessLevel": "public"
|
||||
}
|
||||
},
|
||||
"tsdoc": {
|
||||
"@git.zone/tsdoc": {
|
||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||
},
|
||||
"@ship.zone/szci": {
|
||||
"npmGlobalTools": []
|
||||
}
|
||||
}
|
||||
15
package.json
15
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@git.zone/tsbuild",
|
||||
"version": "3.1.2",
|
||||
"version": "4.1.0",
|
||||
"private": false,
|
||||
"description": "A tool for compiling TypeScript files using the latest nightly features, offering flexible APIs and a CLI for streamlined development.",
|
||||
"main": "dist_ts/index.js",
|
||||
@@ -29,27 +29,28 @@
|
||||
"development",
|
||||
"API"
|
||||
],
|
||||
"author": "Lossless GmbH",
|
||||
"author": "Task Venture Capital GmbH",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://code.foss.global/git.zone/tsbuild/issues"
|
||||
},
|
||||
"homepage": "https://code.foss.global/git.zone/tsbuild#README",
|
||||
"dependencies": {
|
||||
"@git.zone/tspublish": "^1.10.3",
|
||||
"@git.zone/tspublish": "^1.11.0",
|
||||
"@push.rocks/early": "^4.0.4",
|
||||
"@push.rocks/smartcli": "^4.0.19",
|
||||
"@push.rocks/smartdelay": "^3.0.5",
|
||||
"@push.rocks/smartfile": "^11.2.7",
|
||||
"@push.rocks/smartfile": "^13.1.2",
|
||||
"@push.rocks/smartfs": "^1.3.1",
|
||||
"@push.rocks/smartlog": "^3.1.10",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"typescript": "5.9.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsrun": "^1.6.2",
|
||||
"@git.zone/tstest": "^2.7.0",
|
||||
"@types/node": "^22.15.21"
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@git.zone/tstest": "^3.1.4",
|
||||
"@types/node": "^25.0.3"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
|
||||
5482
pnpm-lock.yaml
generated
5482
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +0,0 @@
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
- mongodb-memory-server
|
||||
- puppeteer
|
||||
216
readme.hints.md
216
readme.hints.md
@@ -2,15 +2,13 @@
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Public API (8 functions + 1 class)
|
||||
1. **compileFileArray()** - Basic compilation, throws on error
|
||||
2. **compileFileArrayWithErrorTracking()** - RECOMMENDED, returns IErrorSummary
|
||||
3. **compileGlobStringObject()** - Most powerful, multiple patterns
|
||||
4. **TsBuild Class** - Object-oriented API with compile, checkTypes, checkEmit methods
|
||||
5. **mergeCompilerOptions()** - Utility for option merging
|
||||
6. **compiler()** - Legacy function
|
||||
7. **emitCheck()** - Validate emit capability
|
||||
8. **checkTypes()** - Type checking only
|
||||
### 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/
|
||||
@@ -27,10 +25,71 @@
|
||||
- `--quiet` - Suppress non-error output
|
||||
- `--json` - JSON output format
|
||||
|
||||
## Key Architecture Decisions
|
||||
## Architecture - Modular Structure
|
||||
|
||||
### Configuration Priority (5 levels)
|
||||
1. Default options (hardcoded)
|
||||
### 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)
|
||||
@@ -41,7 +100,6 @@ Cannot be overridden by tsconfig.json alone:
|
||||
- `outDir: 'dist_ts/'` - Path transformation logic
|
||||
- `noEmitOnError: true` - Build integrity
|
||||
- `declaration: true` - Library support
|
||||
- `emitDecoratorMetadata: true` - DI frameworks
|
||||
- `inlineSourceMap: true` - Debugging
|
||||
|
||||
### Path Transformation
|
||||
@@ -51,12 +109,12 @@ Cannot be overridden by tsconfig.json alone:
|
||||
## Default Compiler Options
|
||||
- Module: NodeNext (ESM with CommonJS fallback)
|
||||
- Target: ESNext (latest JavaScript)
|
||||
- Decorators: ENABLED (experimentalDecorators + emitDecoratorMetadata)
|
||||
- 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
|
||||
|
||||
@@ -70,35 +128,59 @@ interface IErrorSummary {
|
||||
}
|
||||
```
|
||||
|
||||
### Three Error Patterns
|
||||
1. **Throw Pattern** - compileFileArray: throws on error
|
||||
2. **Tracking Pattern** - compileFileArrayWithErrorTracking: returns IErrorSummary, NO throw
|
||||
3. **Boolean Pattern** - checkTypes/emitCheck: returns boolean
|
||||
|
||||
RECOMMENDATION: Use compileFileArrayWithErrorTracking for production code
|
||||
|
||||
## JSON Output Format
|
||||
```json
|
||||
{
|
||||
"success": boolean,
|
||||
"totals": {
|
||||
"errors": number,
|
||||
"filesWithErrors": number,
|
||||
"tasks": number
|
||||
},
|
||||
"errorsByFile": {
|
||||
"fileName": [
|
||||
{ "code": number, "message": string }
|
||||
]
|
||||
}
|
||||
### 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
|
||||
2. Always: ts_shared second
|
||||
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)
|
||||
|
||||
@@ -107,37 +189,12 @@ Two-phase check:
|
||||
1. Phase 1: Type check ts/**/* (strict, include .d.ts)
|
||||
2. Phase 2: Type check test/**/* (relaxed, skipLibCheck: true)
|
||||
|
||||
### Glob Pattern Support
|
||||
- `*` single level
|
||||
- `**` recursive
|
||||
- `?` single char
|
||||
- `{a,b}` alternation
|
||||
- Duplicates: Files matching multiple patterns compile multiple times
|
||||
|
||||
### Task Information Display
|
||||
When compiling multiple files with taskInfo param:
|
||||
Shows: `[1/3] Compiling 45 files from ./src/**/*.ts`
|
||||
Plus: File counts, duration, and file type breakdown
|
||||
|
||||
## File Structure
|
||||
- **index.ts** - Main entry, re-exports all
|
||||
- **tsbuild.exports.ts** - Core API functions
|
||||
- **tsbuild.classes.tsbuild.ts** - TsBuild class + utility functions
|
||||
- **tsbuild.cli.ts** - CLI command definitions
|
||||
- **plugins.ts** - Dependency imports (smartfile, smartpath, smartcli, etc.)
|
||||
- **paths.ts** - Path utilities (cwd, packageDir)
|
||||
|
||||
## Dependencies Used
|
||||
- @git.zone/tspublish@^1.10.3 - Module ordering
|
||||
- @push.rocks/* - smartcli, smartfile, smartpath, smartpromise, smartdelay
|
||||
- typescript@5.9.3 - TypeScript compiler
|
||||
|
||||
## Edge Cases
|
||||
|
||||
1. **Empty file list** - Returns [], no error
|
||||
2. **Glob duplicates** - Files compile multiple times, possible duplicate errors
|
||||
3. **Non-existent files** - Handled by TypeScript "file not found" errors
|
||||
4. **skipLibCheck warning** - 1-line default, 5-second pause with --confirmskiplibcheck
|
||||
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)
|
||||
@@ -150,26 +207,9 @@ Plus: File counts, duration, and file type breakdown
|
||||
- Pre-emit checks before emit phase
|
||||
- CLI exit code handling (0=success, 1=error)
|
||||
|
||||
## Recent Changes (from git log)
|
||||
- 2.6.8 - Current version
|
||||
- 2.6.7 - Previous version
|
||||
- Added confirmskiplibcheck flag
|
||||
- Improved CLI exit handling
|
||||
- JSON/quiet modes enhanced
|
||||
- Test script updates
|
||||
|
||||
## Configuration Example (tsconfig.json)
|
||||
Paths get automatically transformed:
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@utils/*": ["./ts_utils/*"] // → ["./dist_ts_utils/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## No ts_* folders found in project root
|
||||
The project itself doesn't have ts_interfaces, ts_shared, etc. directories.
|
||||
The tsfolders command is designed for OTHER projects using tsbuild.
|
||||
## 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
|
||||
|
||||
1
test/assets/output/tocompile.d.ts
vendored
1
test/assets/output/tocompile.d.ts
vendored
@@ -1 +0,0 @@
|
||||
export {};
|
||||
@@ -1,17 +0,0 @@
|
||||
console.log('test');
|
||||
console.log('test2');
|
||||
import * as early from '@push.rocks/early';
|
||||
early.start();
|
||||
early.stop();
|
||||
import { anExportedString } from './tocompile2.js';
|
||||
console.log(anExportedString);
|
||||
class test2 {
|
||||
constructor() {
|
||||
this.test = [];
|
||||
console.log('hi');
|
||||
}
|
||||
}
|
||||
const run = async () => {
|
||||
return 'hi';
|
||||
};
|
||||
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidG9jb21waWxlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdG9jb21waWxlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLENBQUM7QUFDcEIsT0FBTyxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUMsQ0FBQztBQUVyQixPQUFPLEtBQUssS0FBSyxNQUFNLG1CQUFtQixDQUFDO0FBRTNDLEtBQUssQ0FBQyxLQUFLLEVBQUUsQ0FBQztBQUNkLEtBQUssQ0FBQyxJQUFJLEVBQUUsQ0FBQztBQUViLE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQ25ELE9BQU8sQ0FBQyxHQUFHLENBQUMsZ0JBQWdCLENBQUMsQ0FBQztBQUU5QixNQUFNLEtBQUs7SUFFVDtRQURBLFNBQUksR0FBYSxFQUFFLENBQUM7UUFFbEIsT0FBTyxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQztJQUNwQixDQUFDO0NBQ0Y7QUFFRCxNQUFNLEdBQUcsR0FBRyxLQUFLLElBQXFCLEVBQUU7SUFDdEMsT0FBTyxJQUFJLENBQUM7QUFDZCxDQUFDLENBQUMifQ==
|
||||
1
test/assets/output/tocompile2.d.ts
vendored
1
test/assets/output/tocompile2.d.ts
vendored
@@ -1 +0,0 @@
|
||||
export declare const anExportedString = "exported string";
|
||||
@@ -1,3 +0,0 @@
|
||||
console.log('hello');
|
||||
export const anExportedString = 'exported string';
|
||||
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidG9jb21waWxlMi5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RvY29tcGlsZTIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUMsQ0FBQztBQUNyQixNQUFNLENBQUMsTUFBTSxnQkFBZ0IsR0FBRyxpQkFBaUIsQ0FBQyJ9
|
||||
16
test/test.ts
16
test/test.ts
@@ -1,19 +1,21 @@
|
||||
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',
|
||||
};
|
||||
|
||||
tap.test('should convert files from an array with single files to output', async (tools) => {
|
||||
tsbuild.compileFileArray(assetfiles, { outDir: './test/assets/output' });
|
||||
tap.test('should compile files from an array', async () => {
|
||||
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) => {
|
||||
tsbuild.compileGlobStringObject(assetfiles2);
|
||||
tap.test('should compile files from glob pattern object', async () => {
|
||||
const compiler = new TsCompiler();
|
||||
await compiler.compileGlob(assetfiles2);
|
||||
});
|
||||
|
||||
tap.start();
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tsbuild',
|
||||
version: '3.1.2',
|
||||
version: '4.1.0',
|
||||
description: 'A tool for compiling TypeScript files using the latest nightly features, offering flexible APIs and a CLI for streamlined development.'
|
||||
}
|
||||
|
||||
13
ts/index.ts
13
ts/index.ts
@@ -1,5 +1,14 @@
|
||||
import * as early from '@push.rocks/early';
|
||||
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();
|
||||
|
||||
320
ts/mod_cli/classes.tsbuildcli.ts
Normal file
320
ts/mod_cli/classes.tsbuildcli.ts
Normal 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
1
ts/mod_cli/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './classes.tsbuildcli.js';
|
||||
544
ts/mod_compiler/classes.tscompiler.ts
Normal file
544
ts/mod_compiler/classes.tscompiler.ts
Normal file
@@ -0,0 +1,544 @@
|
||||
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;
|
||||
|
||||
// Clear the destination directory before compilation if it exists
|
||||
if (await FsHelpers.directoryExists(destDir)) {
|
||||
if (!isQuiet && !isJson) {
|
||||
console.log(`🧹 Clearing output directory: ${destPath}`);
|
||||
}
|
||||
await FsHelpers.removeDirectory(destDir);
|
||||
}
|
||||
|
||||
// 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
1
ts/mod_compiler/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './classes.tscompiler.js';
|
||||
180
ts/mod_config/classes.tsconfig.ts
Normal file
180
ts/mod_config/classes.tsconfig.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
116
ts/mod_config/classes.tspublishconfig.ts
Normal file
116
ts/mod_config/classes.tspublishconfig.ts
Normal 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
2
ts/mod_config/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './classes.tsconfig.js';
|
||||
export * from './classes.tspublishconfig.js';
|
||||
144
ts/mod_fs/classes.fshelpers.ts
Normal file
144
ts/mod_fs/classes.fshelpers.ts
Normal 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
1
ts/mod_fs/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './classes.fshelpers.js';
|
||||
153
ts/mod_unpack/classes.tsunpacker.ts
Normal file
153
ts/mod_unpack/classes.tsunpacker.ts
Normal 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
1
ts/mod_unpack/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './classes.tsunpacker.js';
|
||||
@@ -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), '../');
|
||||
@@ -1,16 +1,13 @@
|
||||
// node native
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export {
|
||||
path
|
||||
}
|
||||
export { fs, path };
|
||||
|
||||
// @git.zone scope
|
||||
import * as tspublish from '@git.zone/tspublish';
|
||||
|
||||
export {
|
||||
tspublish
|
||||
}
|
||||
export { tspublish };
|
||||
|
||||
// @push.rocks scope
|
||||
import * as smartcli from '@push.rocks/smartcli';
|
||||
@@ -24,6 +21,4 @@ export { smartcli, smartdelay, smartfile, smartpath, smartpromise };
|
||||
// third party scope
|
||||
import typescript from 'typescript';
|
||||
|
||||
export {
|
||||
typescript
|
||||
}
|
||||
export { typescript };
|
||||
|
||||
@@ -1,592 +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,
|
||||
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.esnext.d.ts'],
|
||||
noImplicitAny: false, // Allow implicit any by default
|
||||
esModuleInterop: true,
|
||||
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 {
|
||||
let tsconfig: any;
|
||||
|
||||
// Try to read tsconfig.json, but don't fail if it doesn't exist
|
||||
try {
|
||||
tsconfig = plugins.smartfile.fs.toObjectSync(plugins.path.join(paths.cwd, 'tsconfig.json'));
|
||||
} catch (error) {
|
||||
// tsconfig.json doesn't exist or is invalid - use defaults
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!tsconfig || !tsconfig.compilerOptions) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Start by copying ALL compiler options from tsconfig.json
|
||||
const returnObject: CompilerOptions = { ...tsconfig.compilerOptions };
|
||||
|
||||
// Apply special transformations for string-to-enum conversions
|
||||
|
||||
// Process target (convert string to enum)
|
||||
if (tsconfig.compilerOptions.target && typeof tsconfig.compilerOptions.target === 'string') {
|
||||
const targetKey = tsconfig.compilerOptions.target.toUpperCase();
|
||||
if (targetKey in plugins.typescript.ScriptTarget) {
|
||||
returnObject.target = plugins.typescript.ScriptTarget[targetKey as keyof typeof plugins.typescript.ScriptTarget];
|
||||
}
|
||||
}
|
||||
|
||||
// Process module (convert string to enum)
|
||||
if (tsconfig.compilerOptions.module && typeof tsconfig.compilerOptions.module === 'string') {
|
||||
const moduleKey = tsconfig.compilerOptions.module.toUpperCase();
|
||||
if (moduleKey in plugins.typescript.ModuleKind) {
|
||||
returnObject.module = plugins.typescript.ModuleKind[moduleKey as keyof typeof plugins.typescript.ModuleKind];
|
||||
} else if (moduleKey === 'NODENEXT') {
|
||||
returnObject.module = plugins.typescript.ModuleKind.NodeNext;
|
||||
}
|
||||
}
|
||||
|
||||
// Process moduleResolution (convert string to enum)
|
||||
if (tsconfig.compilerOptions.moduleResolution && typeof tsconfig.compilerOptions.moduleResolution === 'string') {
|
||||
const moduleResolutionKey = tsconfig.compilerOptions.moduleResolution.toUpperCase();
|
||||
if (moduleResolutionKey in plugins.typescript.ModuleResolutionKind) {
|
||||
returnObject.moduleResolution = plugins.typescript.ModuleResolutionKind[
|
||||
moduleResolutionKey as keyof typeof plugins.typescript.ModuleResolutionKind
|
||||
];
|
||||
} else if (moduleResolutionKey === 'NODENEXT') {
|
||||
returnObject.moduleResolution = plugins.typescript.ModuleResolutionKind.NodeNext;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply path transformations (ts_ → dist_ts_)
|
||||
if (tsconfig.compilerOptions.paths) {
|
||||
returnObject.paths = { ...tsconfig.compilerOptions.paths };
|
||||
for (const path of Object.keys(returnObject.paths)) {
|
||||
if (Array.isArray(returnObject.paths[path]) && returnObject.paths[path].length > 0) {
|
||||
returnObject.paths[path][0] = returnObject.paths[path][0].replace('./ts_', './dist_ts_');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return returnObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns critical default options that should not be overridden by tsconfig.json
|
||||
* These options are essential for tsbuild's functionality and build integrity
|
||||
*/
|
||||
private getCriticalDefaults(): CompilerOptions {
|
||||
return {
|
||||
outDir: 'dist_ts/', // Required for path transformation logic
|
||||
noEmitOnError: true, // Build integrity - prevent broken builds
|
||||
declaration: true, // Library consumers depend on .d.ts files
|
||||
inlineSourceMap: true, // Consistent debugging experience
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* Merge order (later overwrites earlier):
|
||||
* 1. compilerOptionsDefault - Base defaults for all options
|
||||
* 2. getTsConfigOptions() - User's tsconfig.json (all options)
|
||||
* 3. getCriticalDefaults() - Protected options that shouldn't be overridden by tsconfig.json
|
||||
* 4. customTsOptions - Programmatic options (can override critical defaults)
|
||||
* 5. getCommandLineOptions() - CLI flags (highest priority)
|
||||
*/
|
||||
public mergeCompilerOptions(
|
||||
customTsOptions: CompilerOptions = {},
|
||||
argvArg?: any
|
||||
): CompilerOptions {
|
||||
// create merged options
|
||||
const mergedOptions: CompilerOptions = {
|
||||
...compilerOptionsDefault, // 1. All defaults
|
||||
...this.getTsConfigOptions(), // 2. User's tsconfig.json (all options)
|
||||
...this.getCriticalDefaults(), // 3. Protected overrides
|
||||
...customTsOptions, // 4. Programmatic options
|
||||
...this.getCommandLineOptions(argvArg), // 5. CLI flags (highest priority)
|
||||
};
|
||||
|
||||
return mergedOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (this.argvArg?.confirmskiplibcheck) {
|
||||
console.log('\n⚠️ WARNING ⚠️');
|
||||
console.log('You are skipping libcheck... Is that really wanted?');
|
||||
console.log('Continuing in 5 seconds...\n');
|
||||
await plugins.smartdelay.delayFor(5000);
|
||||
} else {
|
||||
// No delay by default; keep a short note unless in quiet/json modes
|
||||
if (!this.argvArg?.quiet && !this.argvArg?.json) {
|
||||
console.log('⚠️ skipLibCheck enabled; use --confirmskiplibcheck to pause with warning.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enhanced logging with task info
|
||||
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');
|
||||
// Do not exit here; return error summary so caller can decide
|
||||
done.resolve({ emittedFiles: [], errorSummary: combinedErrorSummary });
|
||||
return done.promise;
|
||||
}
|
||||
|
||||
return done.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* The main compiler function that compiles the files
|
||||
*/
|
||||
public async compile(): Promise<any[]> {
|
||||
if (this.options.skipLibCheck) {
|
||||
if (this.argvArg?.confirmskiplibcheck) {
|
||||
console.log('\n⚠️ WARNING ⚠️');
|
||||
console.log('You are skipping libcheck... Is that really wanted?');
|
||||
console.log('Continuing in 5 seconds...\n');
|
||||
await plugins.smartdelay.delayFor(5000);
|
||||
} else {
|
||||
if (!this.argvArg?.quiet && !this.argvArg?.json) {
|
||||
console.log('⚠️ skipLibCheck enabled; use --confirmskiplibcheck to pause with warning.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🔨 Compiling ${this.fileNames.length} files...`);
|
||||
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');
|
||||
// Throw instead of exiting to keep library pure
|
||||
throw new Error('TypeScript pre-emit checks failed.');
|
||||
}
|
||||
|
||||
// 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');
|
||||
// Throw instead of exiting to keep library pure
|
||||
throw new Error('TypeScript emit failed.');
|
||||
}
|
||||
|
||||
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();
|
||||
};
|
||||
@@ -1,339 +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) => {
|
||||
await tsbuild.compileGlobStringObject(
|
||||
{
|
||||
'./ts/**/*.ts': './dist_ts',
|
||||
},
|
||||
{},
|
||||
process.cwd(),
|
||||
argvArg
|
||||
);
|
||||
const summary = (argvArg as any)?.__tsbuildFinalErrorSummary;
|
||||
if (summary && summary.totalErrors > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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);
|
||||
const summary = (argvArg as any)?.__tsbuildFinalErrorSummary;
|
||||
if (summary && summary.totalErrors > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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);
|
||||
const summary = (argvArg as any)?.__tsbuildFinalErrorSummary;
|
||||
if (summary && summary.totalErrors > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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();
|
||||
};
|
||||
@@ -1,223 +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 (skip for --quiet or --json)
|
||||
const isQuiet = argvArg?.quiet === true;
|
||||
const isJson = argvArg?.json === true;
|
||||
if (!isQuiet && !isJson) {
|
||||
console.log(`\n👷 TypeScript Compilation Tasks (${totalTasks} task${totalTasks !== 1 ? 's' : ''}):`);
|
||||
Object.entries(globStringObjectArg).forEach(([source, dest]) => {
|
||||
console.log(` 📂 ${source} → ${dest}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
for (const keyArg in globStringObjectArg) {
|
||||
// Type safety check for key
|
||||
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);
|
||||
|
||||
// Output summary based on mode
|
||||
if (isJson) {
|
||||
const result = {
|
||||
success: finalErrorSummary.totalErrors === 0,
|
||||
totals: {
|
||||
errors: finalErrorSummary.totalErrors,
|
||||
filesWithErrors: finalErrorSummary.totalFiles,
|
||||
tasks: totalTasks,
|
||||
},
|
||||
errorsByFile: Object.fromEntries(
|
||||
Object.entries(finalErrorSummary.errorsByFile).map(([file, diags]) => [
|
||||
file,
|
||||
diags.map(d => ({
|
||||
code: d.code,
|
||||
message: plugins.typescript.flattenDiagnosticMessageText(d.messageText as any, '\n'),
|
||||
}))
|
||||
])
|
||||
),
|
||||
};
|
||||
console.log(JSON.stringify(result));
|
||||
} else if (!isQuiet) {
|
||||
displayFinalErrorSummary(finalErrorSummary);
|
||||
}
|
||||
|
||||
// Attach summary to argvArg so CLI can decide exit behavior
|
||||
if (argvArg && typeof argvArg === 'object') {
|
||||
(argvArg as any).__tsbuildFinalErrorSummary = finalErrorSummary;
|
||||
}
|
||||
|
||||
return compiledFiles;
|
||||
};
|
||||
@@ -1,7 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
|
||||
Reference in New Issue
Block a user