Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c1805229b | |||
| 724e5bf7ec | |||
| cfe2eafe89 | |||
| b3d1982170 | |||
| 83e0e9b5dd | |||
| 094f9df55f | |||
| a6a006aaec | |||
| 9477875c1d | |||
| 2b73f3d582 | |||
| 0ffdcf852f | |||
| f8f20be4f4 | |||
| fb5421a8c4 | |||
| 79d48b0875 | |||
| 8977ff4525 | |||
| 9dc74fd392 | |||
| 85a33021e4 | |||
| e8e64a4ef3 | |||
| a5dd5252db | |||
| dc9a9640df |
@@ -30,8 +30,5 @@
|
||||
},
|
||||
"@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": []
|
||||
}
|
||||
}
|
||||
68
changelog.md
68
changelog.md
@@ -1,5 +1,73 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-24 - 4.4.0 - feat(config)
|
||||
add smartconfig metadata and update TypeScript build configuration docs
|
||||
|
||||
- replace npmextra.json with .smartconfig.json and include it in published files
|
||||
- add a repository license file
|
||||
- upgrade TypeScript and related build dependencies
|
||||
- refine tsconfig path handling to avoid mutating source path mappings
|
||||
- fix tspublish config caching to distinguish unloaded and missing states
|
||||
- expand documentation for import path rewriting, structured logging, and the compilation pipeline
|
||||
|
||||
## 2026-03-06 - 4.3.0 - feat(mod_logger)
|
||||
add centralized TsBuildLogger and replace ad-hoc console output with structured, colored logging
|
||||
|
||||
- Add ts/mod_logger/classes.logger.ts providing header/step/detail/success/error/warn/indent utilities with ANSI color support
|
||||
- Export logger from ts/mod_logger/index.ts and re-export from ts/index.ts
|
||||
- Replace console.log/console.error/console.warn calls in mod_cli, mod_compiler, and mod_unpack with TsBuildLogger methods for consistent, hierarchical output
|
||||
- Refactor error-summary, emit and type-check output to use logger separators, colors, and structured messages
|
||||
|
||||
## 2026-03-05 - 4.2.6 - fix(meta)
|
||||
no changes
|
||||
|
||||
- Current package version: 4.2.5
|
||||
- No code or file changes detected in this commit; no release required
|
||||
|
||||
## 2026-03-05 - 4.2.5 - fix(compiler)
|
||||
yield to the event loop after TypeScript emit to allow pending microtasks and I/O to settle before reading or modifying the output directory
|
||||
|
||||
- Added await new Promise(resolve => process.nextTick(resolve)) immediately after program.emit()
|
||||
- Prevents race conditions by allowing libuv write completions and other deferred callbacks to complete before accessing the output directory
|
||||
- File changed: ts/mod_compiler/classes.tscompiler.ts
|
||||
|
||||
## 2026-03-05 - 4.2.4 - fix(fshelpers)
|
||||
remove outdated comment about using synchronous rm to avoid XFS metadata corruption
|
||||
|
||||
- Comment-only change in ts/mod_fs/classes.fshelpers.ts; no runtime or API behavior changes
|
||||
- Bump patch version from 4.2.3 to 4.2.4
|
||||
|
||||
## 2026-03-05 - 4.2.3 - fix(compiler)
|
||||
defer unpacking until after all compilations and remove diagnostic filesystem syncs to avoid XFS metadata visibility issues
|
||||
|
||||
- Queue pending unpack operations during compilation and run them after all compile tasks complete to avoid modifying output directories while other compilations are writing.
|
||||
- Remove TypeScript sys interception, execSync('sync') calls, and per-unpack fs.fsyncSync usage that attempted to work around XFS delayed metadata commits; rely on performing all unpacks after compilation instead.
|
||||
- Clean up noisy diagnostic code (external 'ls' comparisons, readdir snapshots) and simplify logging of unpack results.
|
||||
- Remove unused imports (fs and child_process.execSync) from the compiler module.
|
||||
|
||||
## 2026-03-05 - 4.2.2 - fix(compiler)
|
||||
force global filesystem sync to flush XFS delayed logging and add diagnostics comparing Node's readdirSync with system ls to detect directory entry inconsistencies
|
||||
|
||||
- Replace per-directory fs.fsyncSync loop with execSync('sync') to ensure parent B+tree metadata is flushed on XFS
|
||||
- Import execSync from child_process
|
||||
- Add diagnostic comparison: run ls -1 and compare its entries to fs.readdirSync; log mismatches and full entry lists for debugging Node.js caching/readdir inconsistencies
|
||||
|
||||
## 2026-03-05 - 4.2.1 - fix(compiler)
|
||||
use TypeScript sys hooks instead of fs monkeypatching to detect writes/deletes in previous output directories
|
||||
|
||||
- Replace direct fs.* monkeypatching with interception of typescript.sys.writeFile, typescript.sys.deleteFile and typescript.sys.createDirectory
|
||||
- Add guards for optional sys.deleteFile before overriding it and preserve original sys methods to restore after compilation
|
||||
- Update diagnostic messages to reference TypeScript sys ops and add an informational message when no ops are observed
|
||||
- Reduce surface area of changes by avoiding global fs changes and focusing on TypeScript's sys API for safer interception
|
||||
|
||||
## 2026-03-05 - 4.2.0 - feat(mod_compiler)
|
||||
add diagnostic interception of fs operations to detect and report unexpected file system changes in previously compiled output directories during compilation
|
||||
|
||||
- Wraps fs.unlinkSync, fs.rmSync, fs.rmdirSync, fs.renameSync and fs.writeFileSync to record operations targeting other successful output directories during a compile.
|
||||
- Enabled only when there are previously compiled output dirs and when not running in quiet or JSON mode; original fs methods are restored after compilation.
|
||||
- Logs up to 30 intercepted operations and prints a summary count if any ops were observed; intercepted calls still perform the original fs action (diagnostic-only).
|
||||
- No functional change to compilation behavior beyond additional diagnostic reporting.
|
||||
|
||||
## 2026-03-05 - 4.1.26 - fix(compiler)
|
||||
fsync output directories after unpack to avoid XFS delayed logging causing corrupt or invisible directory entries during subsequent TypeScript emits
|
||||
|
||||
|
||||
21
license
Normal file
21
license
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Task Venture Capital GmbH
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
14
package.json
14
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@git.zone/tsbuild",
|
||||
"version": "4.1.26",
|
||||
"version": "4.4.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",
|
||||
@@ -36,21 +36,21 @@
|
||||
},
|
||||
"homepage": "https://code.foss.global/git.zone/tsbuild#README",
|
||||
"dependencies": {
|
||||
"@git.zone/tspublish": "^1.11.2",
|
||||
"@git.zone/tspublish": "^1.11.3",
|
||||
"@push.rocks/early": "^4.0.4",
|
||||
"@push.rocks/smartcli": "^4.0.20",
|
||||
"@push.rocks/smartdelay": "^3.0.5",
|
||||
"@push.rocks/smartfile": "^13.1.2",
|
||||
"@push.rocks/smartfs": "^1.3.2",
|
||||
"@push.rocks/smartfs": "^1.5.0",
|
||||
"@push.rocks/smartlog": "^3.2.1",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.3",
|
||||
"typescript": "5.9.3"
|
||||
"typescript": "^6.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@git.zone/tsrun": "^2.0.1",
|
||||
"@git.zone/tstest": "^3.2.0",
|
||||
"@types/node": "^25.3.3"
|
||||
"@git.zone/tstest": "^3.5.1",
|
||||
"@types/node": "^25.5.0"
|
||||
},
|
||||
"files": [
|
||||
"ts/**/*",
|
||||
@@ -61,7 +61,7 @@
|
||||
"dist_ts_web/**/*",
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
"npmextra.json",
|
||||
".smartconfig.json",
|
||||
"readme.md"
|
||||
],
|
||||
"browserslist": [
|
||||
|
||||
2900
pnpm-lock.yaml
generated
2900
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -7,11 +7,13 @@
|
||||
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
|
||||
5. **TsPathRewriter** - Cross-module import path rewriting in compiled output
|
||||
6. **FsHelpers** - Filesystem utilities (static methods)
|
||||
7. **TsBuildLogger** - Centralized structured console output (4-level hierarchy)
|
||||
8. **TsBuildCli** - CLI command handler
|
||||
|
||||
### CLI Commands (5)
|
||||
1. **tsbuild** (default) - Compiles ./ts/**/*.ts → ./dist_ts/
|
||||
1. **tsbuild** (default) - Compiles ./ts/**/*.ts -> ./dist_ts/
|
||||
2. **tsbuild custom <dir1> <dir2>** - Custom directory compilation
|
||||
3. **tsbuild tsfolders** - Auto-discover ts_* folders, compile in order
|
||||
4. **tsbuild emitcheck <pattern>** - Validate emit without output
|
||||
@@ -32,6 +34,7 @@
|
||||
ts/
|
||||
index.ts # Main entry, re-exports all modules
|
||||
plugins.ts # Dependency imports only
|
||||
00_commitinfo_data.ts # Commit info data
|
||||
|
||||
mod_fs/
|
||||
index.ts # exports
|
||||
@@ -46,9 +49,17 @@ ts/
|
||||
index.ts # exports
|
||||
classes.tsunpacker.ts # TsUnpacker - output flattening
|
||||
|
||||
mod_pathrewrite/
|
||||
index.ts # exports
|
||||
classes.tspathrewriter.ts # TsPathRewriter - import path rewriting
|
||||
|
||||
mod_logger/
|
||||
index.ts # exports
|
||||
classes.logger.ts # TsBuildLogger - structured console output
|
||||
|
||||
mod_compiler/
|
||||
index.ts # exports
|
||||
classes.tscompiler.ts # TsCompiler + legacy compatibility functions
|
||||
classes.tscompiler.ts # TsCompiler - main compilation engine
|
||||
|
||||
mod_cli/
|
||||
index.ts # exports
|
||||
@@ -62,6 +73,7 @@ ts/
|
||||
- Type checking with `checkTypes()`, `checkEmit()`
|
||||
- Configuration via TsConfig
|
||||
- Automatic unpacking via TsUnpacker
|
||||
- Import path rewriting via TsPathRewriter (in compileGlob)
|
||||
|
||||
**TsConfig** (`mod_config/classes.tsconfig.ts`)
|
||||
- Load and parse tsconfig.json
|
||||
@@ -78,8 +90,18 @@ ts/
|
||||
- Flatten output directories
|
||||
- Configurable via TsPublishConfig
|
||||
|
||||
**TsPathRewriter** (`mod_pathrewrite/classes.tspathrewriter.ts`)
|
||||
- Auto-detect ts_* folders in project
|
||||
- Rewrite ES module imports, dynamic imports, and require() calls
|
||||
- Transforms `../ts_*` references to `../dist_ts_*` in compiled output
|
||||
|
||||
**TsBuildLogger** (`mod_logger/classes.logger.ts`)
|
||||
- 4-level visual hierarchy: HEADER, STEP, DETAIL, SUCCESS/ERROR/WARN
|
||||
- ANSI color output
|
||||
- Static methods (no instantiation needed)
|
||||
|
||||
**FsHelpers** (`mod_fs/classes.fshelpers.ts`)
|
||||
- `listFilesWithGlob()` - Glob pattern file listing
|
||||
- `listFilesWithGlob()` - Glob pattern file listing via smartfs
|
||||
- `extractSourceFolder()` - Pattern parsing
|
||||
- File/directory existence checks
|
||||
- Directory operations
|
||||
@@ -103,8 +125,8 @@ Cannot be overridden by tsconfig.json alone:
|
||||
- `inlineSourceMap: true` - Debugging
|
||||
|
||||
### Path Transformation
|
||||
- Automatic: `./ts_interfaces` → `./dist_ts_interfaces`
|
||||
- In tsconfig paths: `./ts_*` → `./dist_ts_*` (first array element only)
|
||||
- Automatic: `./ts_interfaces` -> `./dist_ts_interfaces`
|
||||
- In tsconfig paths: `./ts_*` -> `./dist_ts_*` (first array element only)
|
||||
|
||||
## Default Compiler Options
|
||||
- Module: NodeNext (ESM with CommonJS fallback)
|
||||
@@ -137,14 +159,16 @@ interface ICompileResult {
|
||||
```
|
||||
|
||||
## Dependencies Used
|
||||
- @git.zone/tspublish@^1.10.3 - Module ordering
|
||||
- @push.rocks/smartcli@^4.0.19 - CLI framework
|
||||
- @git.zone/tspublish@^1.11.3 - Module ordering
|
||||
- @push.rocks/early@^4.0.4 - Early initialization
|
||||
- @push.rocks/smartcli@^4.0.20 - CLI framework
|
||||
- @push.rocks/smartdelay@^3.0.5 - Delay utilities
|
||||
- @push.rocks/smartfile@^13.1.2 - File content handling
|
||||
- @push.rocks/smartfs@^1.2.0 - Filesystem operations
|
||||
- @push.rocks/smartfs@^1.5.0 - Filesystem operations
|
||||
- @push.rocks/smartlog@^3.2.1 - Logging
|
||||
- @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
|
||||
- typescript@^6.0.2 - TypeScript compiler
|
||||
|
||||
### smartfs Usage
|
||||
- File listing: `smartfs.directory(path).recursive().filter(pattern).list()`
|
||||
@@ -152,29 +176,32 @@ interface ICompileResult {
|
||||
- Directory existence: `smartfs.directory(path).exists()`
|
||||
- FsHelpers wraps smartfs for glob pattern support
|
||||
|
||||
## Compilation Pipeline (compileGlob)
|
||||
|
||||
1. **Phase 1: Resolve & Clean** - Resolve glob patterns, list files, clear all output dirs
|
||||
2. **Phase 2: Compile** - Compile each task sequentially with error tracking
|
||||
3. **Phase 3: Unpack** - Flatten nested output directories for successful compilations
|
||||
4. **Phase 4: Path Rewrite** - Rewrite cross-module import paths in output dirs
|
||||
|
||||
## 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
|
||||
ts_core/ <- nested output
|
||||
ts_shared/ <- pulled-in dependency
|
||||
```
|
||||
|
||||
The unpack feature automatically flattens this to:
|
||||
```
|
||||
dist_ts_core/
|
||||
index.js ← flat
|
||||
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()`
|
||||
- `unpack: true` (default if not present) -> flatten output
|
||||
- `unpack: false` -> skip unpacking
|
||||
|
||||
## Special Behaviors
|
||||
|
||||
@@ -206,10 +233,4 @@ Two-phase check:
|
||||
- Protected options ensure integrity
|
||||
- Pre-emit checks before emit phase
|
||||
- CLI exit code handling (0=success, 1=error)
|
||||
|
||||
## Recent Changes
|
||||
- 3.1.4+ - Major restructuring with mod_* modules
|
||||
- Full OO architecture with TsCompiler, TsConfig, TsPublishConfig, TsUnpacker, TsBuildCli, FsHelpers
|
||||
- Backward compatibility maintained for legacy functions
|
||||
- Automatic "unpack" feature for nested output directories
|
||||
- Migrated filesystem operations from smartfile to smartfs
|
||||
- Event loop yield after emit for XFS filesystem reliability
|
||||
|
||||
189
readme.md
189
readme.md
@@ -1,12 +1,12 @@
|
||||
# @git.zone/tsbuild
|
||||
|
||||
A powerful, modern TypeScript build tool with smart defaults, full tsconfig.json support, and automatic output directory management. 🚀
|
||||
A powerful, modern TypeScript build tool with smart defaults, full tsconfig.json support, automatic output directory management, and cross-module import path rewriting.
|
||||
|
||||
## Issue Reporting and Security
|
||||
|
||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
||||
|
||||
## 📦 Install
|
||||
## Install
|
||||
|
||||
```bash
|
||||
# Using pnpm (recommended)
|
||||
@@ -16,22 +16,23 @@ pnpm install @git.zone/tsbuild --save-dev
|
||||
npm install @git.zone/tsbuild --save-dev
|
||||
```
|
||||
|
||||
## ⚡ Why tsbuild?
|
||||
## Why tsbuild?
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| 🔧 **Smart tsconfig.json Integration** | Respects all your compiler options with intelligent merging |
|
||||
| 🛡️ **Protected Defaults** | Critical build settings are safeguarded while staying flexible |
|
||||
| 🎯 **Zero Config** | Works perfectly without tsconfig.json |
|
||||
| 🌐 **Glob Pattern Support** | Compile multiple directories with a single command |
|
||||
| 📊 **Dependency-Aware** | Automatically orders compilation based on module dependencies |
|
||||
| ✅ **Type Checking** | Validate code without emitting files |
|
||||
| 🧹 **Clean Builds** | Automatically clears output directories before compilation |
|
||||
| 📁 **Auto-Unpack** | Flattens nested output directories automatically |
|
||||
| 🔄 **CI/CD Ready** | JSON output mode and proper exit codes |
|
||||
| ✨ **Modern Defaults** | ESNext, NodeNext modules, decorators out of the box |
|
||||
| **Smart tsconfig.json Integration** | Respects all your compiler options with intelligent merging |
|
||||
| **Protected Defaults** | Critical build settings are safeguarded while staying flexible |
|
||||
| **Zero Config** | Works perfectly without tsconfig.json |
|
||||
| **Glob Pattern Support** | Compile multiple directories with a single command |
|
||||
| **Dependency-Aware** | Automatically orders compilation based on module dependencies |
|
||||
| **Type Checking** | Validate code without emitting files |
|
||||
| **Clean Builds** | Automatically clears output directories before compilation |
|
||||
| **Auto-Unpack** | Flattens nested output directories automatically |
|
||||
| **Import Path Rewriting** | Rewrites cross-module imports to point at compiled output |
|
||||
| **CI/CD Ready** | JSON output mode and proper exit codes |
|
||||
| **Modern Defaults** | ESNext, NodeNext modules, decorators out of the box |
|
||||
|
||||
## 🚀 Quick Start
|
||||
## Quick Start
|
||||
|
||||
### CLI Usage
|
||||
|
||||
@@ -46,8 +47,8 @@ Compiles `./ts/**/*.ts` to `./dist_ts/`
|
||||
npx tsbuild custom src utils
|
||||
```
|
||||
Compiles:
|
||||
- `./src/**/*.ts` → `./dist_src/`
|
||||
- `./utils/**/*.ts` → `./dist_utils/`
|
||||
- `./src/**/*.ts` -> `./dist_src/`
|
||||
- `./utils/**/*.ts` -> `./dist_utils/`
|
||||
|
||||
**Auto-discover and compile in dependency order:**
|
||||
```bash
|
||||
@@ -97,7 +98,7 @@ await compiler.compileGlob({
|
||||
});
|
||||
```
|
||||
|
||||
## 🛠️ CLI Commands
|
||||
## CLI Commands
|
||||
|
||||
### 1. Default Build
|
||||
|
||||
@@ -149,7 +150,7 @@ npx tsbuild custom src utils
|
||||
npx tsbuild custom api models services --commonjs
|
||||
```
|
||||
|
||||
### 3. TSFolders (Dependency-Aware) 📊
|
||||
### 3. TSFolders (Dependency-Aware)
|
||||
|
||||
```bash
|
||||
npx tsbuild tsfolders [options]
|
||||
@@ -165,14 +166,14 @@ Automatically discovers and compiles all `ts_*` folders in dependency order:
|
||||
**Example output:**
|
||||
```
|
||||
TypeScript Folder Compilation Plan (5 folders)
|
||||
1/5 ts_interfaces
|
||||
2/5 ts_shared
|
||||
3/5 ts_core
|
||||
4/5 ts_utils
|
||||
5/5 ts_modules
|
||||
1. ts_interfaces
|
||||
2. ts_shared
|
||||
3. ts_core
|
||||
4. ts_utils
|
||||
5. ts_modules
|
||||
```
|
||||
|
||||
### 4. Emit Check ✓
|
||||
### 4. Emit Check
|
||||
|
||||
```bash
|
||||
npx tsbuild emitcheck <file_or_pattern> [more...] [options]
|
||||
@@ -192,7 +193,7 @@ npx tsbuild emitcheck "src/**/*.ts" "test/**/*.ts"
|
||||
- `0` - All files can be emitted
|
||||
- `1` - One or more files have errors
|
||||
|
||||
### 5. Type Check 🔍
|
||||
### 5. Type Check
|
||||
|
||||
```bash
|
||||
npx tsbuild check [pattern] [more...] [options]
|
||||
@@ -218,7 +219,7 @@ npx tsbuild check
|
||||
# All default type checks passed!
|
||||
```
|
||||
|
||||
## 📖 API Reference
|
||||
## API Reference
|
||||
|
||||
### TsCompiler Class
|
||||
|
||||
@@ -285,7 +286,7 @@ try {
|
||||
|
||||
#### compileGlob(globPatterns, customOptions?)
|
||||
|
||||
Compile multiple glob patterns to different destinations. Automatically clears output directories before compilation and unpacks nested output.
|
||||
Compile multiple glob patterns to different destinations. Automatically clears output directories before compilation, unpacks nested output, and rewrites cross-module import paths.
|
||||
|
||||
```typescript
|
||||
const result = await compiler.compileGlob({
|
||||
@@ -367,10 +368,28 @@ Flattens nested TypeScript output directories.
|
||||
```typescript
|
||||
import { TsUnpacker } from '@git.zone/tsbuild';
|
||||
|
||||
const unpacker = new TsUnpacker('./dist_ts', './ts');
|
||||
const unpacker = new TsUnpacker('ts_core', './dist_ts_core');
|
||||
await unpacker.unpack();
|
||||
```
|
||||
|
||||
#### TsPathRewriter
|
||||
|
||||
Rewrites cross-module import paths in compiled output files. When TypeScript compiles files that import from sibling directories (e.g., `../ts_shared/helper.js`), TsPathRewriter rewrites those paths to reference the compiled output directories (`../dist_ts_shared/helper.js`).
|
||||
|
||||
```typescript
|
||||
import { TsPathRewriter } from '@git.zone/tsbuild';
|
||||
|
||||
// Auto-detect all ts_* folders in the project
|
||||
const rewriter = await TsPathRewriter.fromProjectDirectory(process.cwd());
|
||||
const filesModified = await rewriter.rewriteDirectory('./dist_ts_core');
|
||||
|
||||
// Or from explicit glob patterns
|
||||
const rewriter2 = TsPathRewriter.fromGlobPatterns({
|
||||
'./ts_core/**/*.ts': './dist_ts_core',
|
||||
'./ts_shared/**/*.ts': './dist_ts_shared',
|
||||
});
|
||||
```
|
||||
|
||||
#### FsHelpers
|
||||
|
||||
Static filesystem utilities.
|
||||
@@ -398,7 +417,7 @@ const cli = new TsBuildCli('/path/to/project');
|
||||
cli.run();
|
||||
```
|
||||
|
||||
## ⚙️ Configuration
|
||||
## Configuration
|
||||
|
||||
### tsconfig.json Support
|
||||
|
||||
@@ -419,19 +438,19 @@ tsbuild fully supports all compiler options from `tsconfig.json`. Your project c
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Priority (5 Levels) 📋
|
||||
### Configuration Priority (5 Levels)
|
||||
|
||||
When multiple configuration sources exist, they merge in this order (later overrides earlier):
|
||||
|
||||
| Priority | Source | Description |
|
||||
|----------|--------|-------------|
|
||||
| 1️⃣ | **Default Options** | tsbuild's sensible defaults |
|
||||
| 2️⃣ | **tsconfig.json** | All options from your tsconfig.json (if present) |
|
||||
| 3️⃣ | **Protected Defaults** | Critical options for build integrity |
|
||||
| 4️⃣ | **Programmatic Options** | Options passed to API functions |
|
||||
| 5️⃣ | **CLI Flags** | Command-line arguments (highest priority) |
|
||||
| 1 | **Default Options** | tsbuild's sensible defaults |
|
||||
| 2 | **tsconfig.json** | All options from your tsconfig.json (if present) |
|
||||
| 3 | **Protected Defaults** | Critical options for build integrity |
|
||||
| 4 | **Programmatic Options** | Options passed to API functions |
|
||||
| 5 | **CLI Flags** | Command-line arguments (highest priority) |
|
||||
|
||||
### Protected Options 🛡️
|
||||
### Protected Options
|
||||
|
||||
These options cannot be overridden by tsconfig.json alone (but can be overridden programmatically or via CLI):
|
||||
|
||||
@@ -449,8 +468,6 @@ When no tsconfig.json exists:
|
||||
```typescript
|
||||
{
|
||||
declaration: true, // Generate .d.ts files
|
||||
emitDecoratorMetadata: true, // Support DI frameworks
|
||||
experimentalDecorators: true, // Enable decorators
|
||||
inlineSourceMap: true, // Debug-friendly
|
||||
noEmitOnError: true, // Fail-fast on errors
|
||||
outDir: 'dist_ts/', // Output directory
|
||||
@@ -463,7 +480,7 @@ When no tsconfig.json exists:
|
||||
}
|
||||
```
|
||||
|
||||
### Path Transformation 🔄
|
||||
### Path Transformation
|
||||
|
||||
tsbuild automatically transforms path mappings:
|
||||
|
||||
@@ -480,12 +497,12 @@ tsbuild automatically transforms path mappings:
|
||||
|
||||
**Automatic transformation:**
|
||||
```
|
||||
./ts_models/* → ./dist_ts_models/*
|
||||
./ts_models/* -> ./dist_ts_models/*
|
||||
```
|
||||
|
||||
## ✨ Features
|
||||
## Features
|
||||
|
||||
### Clean Builds 🧹
|
||||
### Clean Builds
|
||||
|
||||
Output directories are automatically cleared before compilation:
|
||||
|
||||
@@ -496,7 +513,23 @@ Compiling 14 files from ./ts/**/*.ts
|
||||
|
||||
This ensures no stale files remain from previous builds.
|
||||
|
||||
### Monorepo Support with tspublish 📦
|
||||
### Import Path Rewriting
|
||||
|
||||
When working with multi-module projects (multiple `ts_*` folders), TypeScript compiles import paths relative to source directories. After compilation and unpacking, these paths would be wrong because the directory structure changes.
|
||||
|
||||
tsbuild automatically detects all `ts_*` folders in the project and rewrites import paths in the compiled output:
|
||||
|
||||
```
|
||||
# Source code
|
||||
import { helper } from '../ts_shared/helper.js';
|
||||
|
||||
# Compiled output (after rewriting)
|
||||
import { helper } from '../dist_ts_shared/helper.js';
|
||||
```
|
||||
|
||||
This works for ES module imports, dynamic `import()` calls, and CommonJS `require()` statements. The rewriting happens automatically as part of `compileGlob()`.
|
||||
|
||||
### Monorepo Support with tspublish
|
||||
|
||||
tsbuild is designed to work seamlessly with [@git.zone/tspublish](https://code.foss.global/git.zone/tspublish) for monorepo workflows. This enables building and publishing multiple packages from a single repository.
|
||||
|
||||
@@ -504,12 +537,12 @@ tsbuild is designed to work seamlessly with [@git.zone/tspublish](https://code.f
|
||||
|
||||
```
|
||||
my-project/
|
||||
├── ts/ # Main package source
|
||||
├── ts_interfaces/ # Shared interfaces (order: 1)
|
||||
├── ts_shared/ # Shared utilities (order: 2)
|
||||
├── ts_core/ # Core logic (order: 3)
|
||||
├── ts_web/ # Web-specific code (order: 4)
|
||||
└── ts_node/ # Node-specific code (order: 5)
|
||||
ts/ # Main package source
|
||||
ts_interfaces/ # Shared interfaces (order: 1)
|
||||
ts_shared/ # Shared utilities (order: 2)
|
||||
ts_core/ # Core logic (order: 3)
|
||||
ts_web/ # Web-specific code (order: 4)
|
||||
ts_node/ # Node-specific code (order: 5)
|
||||
```
|
||||
|
||||
Each `ts_*` folder can contain its own `tspublish.json` to configure compilation and publishing behavior.
|
||||
@@ -529,10 +562,10 @@ Create a `tspublish.json` in each `ts_*` folder:
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
|--------|------|---------|-------------|
|
||||
| `name` | string | — | Package name for publishing |
|
||||
| `name` | string | -- | Package name for publishing |
|
||||
| `order` | number | `Infinity` | Build sequence (lower builds first) |
|
||||
| `unpack` | boolean | `true` | Flatten nested output directories |
|
||||
| `dependencies` | string[] | — | Monorepo dependencies to bundle |
|
||||
| `dependencies` | string[] | -- | Monorepo dependencies to bundle |
|
||||
|
||||
#### Build Order
|
||||
|
||||
@@ -548,31 +581,21 @@ npx tsbuild tsfolders
|
||||
3. Other folders sorted by `order` property
|
||||
4. Folders without `order` are built last
|
||||
|
||||
**Example output:**
|
||||
```
|
||||
TypeScript Folder Compilation Plan (5 folders)
|
||||
1/5 ts_interfaces (order: 1)
|
||||
2/5 ts_shared (order: 2)
|
||||
3/5 ts_core (order: 3)
|
||||
4/5 ts_web (order: 4)
|
||||
5/5 ts_node (order: 5)
|
||||
```
|
||||
|
||||
### Auto-Unpack 📁
|
||||
### Auto-Unpack
|
||||
|
||||
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
|
||||
ts_core/ <-- nested output
|
||||
ts_shared/ <-- pulled-in dependency
|
||||
```
|
||||
|
||||
tsbuild automatically flattens this to:
|
||||
|
||||
```
|
||||
dist_ts_core/
|
||||
index.js ← flat, clean structure
|
||||
index.js <-- flat, clean structure
|
||||
```
|
||||
|
||||
This is especially important for monorepos where packages import from each other.
|
||||
@@ -591,11 +614,11 @@ This is especially important for monorepos where packages import from each other
|
||||
- The source folder's contents (`ts_core/`) are moved to the root of `dist_ts_core/`
|
||||
- Sibling folders (`ts_shared/`) that were pulled in are removed (they have their own dist)
|
||||
|
||||
### Decorator Support 🎨
|
||||
### Decorator Support
|
||||
|
||||
tsbuild supports both **TC39 standard decorators** (recommended) and legacy experimental decorators.
|
||||
|
||||
**TC39 Standard Decorators (Preferred)** 🌟
|
||||
**TC39 Standard Decorators (Preferred)**
|
||||
|
||||
We strongly recommend using TC39 standard decorators for all new code. They are the official JavaScript standard and provide better semantics:
|
||||
|
||||
@@ -618,19 +641,8 @@ class UserService {
|
||||
|
||||
**Legacy Decorators (Backwards Compatibility)**
|
||||
|
||||
For existing projects using frameworks like NestJS, TypeORM, or Angular that still require experimental decorators:
|
||||
For existing projects using frameworks that still require experimental decorators:
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
class UserService {
|
||||
constructor(
|
||||
@Inject('CONFIG') private config: Config,
|
||||
private logger: Logger
|
||||
) {}
|
||||
}
|
||||
```
|
||||
|
||||
Enable legacy decorators in your `tsconfig.json`:
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
@@ -640,9 +652,18 @@ Enable legacy decorators in your `tsconfig.json`:
|
||||
}
|
||||
```
|
||||
|
||||
> 💡 **Tip:** When starting new projects or migrating existing ones, prefer TC39 decorators. They're the future of JavaScript and don't require experimental flags.
|
||||
### Structured Logging
|
||||
|
||||
## 🔄 CI/CD Integration
|
||||
tsbuild uses a 4-level visual hierarchy for console output, making it easy to follow compilation progress:
|
||||
|
||||
- **HEADER** - Top-level section start (emoji + bold text + separator)
|
||||
- **STEP** - Major action within a section (emoji + text)
|
||||
- **DETAIL** - Supplementary info under a step (indented)
|
||||
- **SUCCESS/ERROR/WARN** - Outcome indicators with color coding
|
||||
|
||||
All output uses ANSI color codes for terminal readability. Use `--quiet` to suppress output or `--json` for machine-readable format.
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
### GitHub Actions
|
||||
|
||||
@@ -657,7 +678,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
node-version: '22'
|
||||
|
||||
- run: npm install
|
||||
- run: npx tsbuild
|
||||
@@ -694,7 +715,7 @@ npx tsbuild --json --quiet
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
@@ -725,7 +746,7 @@ Only use this if you trust your dependencies' type definitions.
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./license) file.
|
||||
|
||||
**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.
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tsbuild',
|
||||
version: '4.1.26',
|
||||
version: '4.4.0',
|
||||
description: 'A tool for compiling TypeScript files using the latest nightly features, offering flexible APIs and a CLI for streamlined development.'
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ export * from './mod_fs/index.js';
|
||||
export * from './mod_config/index.js';
|
||||
export * from './mod_unpack/index.js';
|
||||
export * from './mod_pathrewrite/index.js';
|
||||
export * from './mod_logger/index.js';
|
||||
export * from './mod_compiler/index.js';
|
||||
export * from './mod_cli/index.js';
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as tspublish from '@git.zone/tspublish';
|
||||
|
||||
import { TsCompiler } from '../mod_compiler/index.js';
|
||||
import { FsHelpers } from '../mod_fs/index.js';
|
||||
import { TsBuildLogger as log } from '../mod_logger/index.js';
|
||||
|
||||
/**
|
||||
* TsBuildCli handles all CLI commands for tsbuild.
|
||||
@@ -78,21 +79,20 @@ export class TsBuildCli {
|
||||
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');
|
||||
log.error('Please provide at least one TypeScript file path or glob pattern');
|
||||
log.indent('Usage: tsbuild emitcheck <pattern> [...]');
|
||||
log.indent('Example: tsbuild emitcheck "src/**/*.ts" "test/**/*.ts"');
|
||||
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');
|
||||
log.error('No TypeScript files found to check');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\n🔎 Found ${allFiles.length} TypeScript file${allFiles.length !== 1 ? 's' : ''} to check`);
|
||||
log.step('🔎', `Found ${allFiles.length} TypeScript file${allFiles.length !== 1 ? 's' : ''} to check`);
|
||||
|
||||
const compiler = new TsCompiler(this.cwd, argvArg);
|
||||
const success = await compiler.checkEmit(allFiles);
|
||||
@@ -153,19 +153,12 @@ export class TsBuildCli {
|
||||
|
||||
// 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) + '┤');
|
||||
log.header('📂', `TypeScript Folder Compilation Plan (${folderCount} folder${folderCount !== 1 ? 's' : ''})`);
|
||||
|
||||
sortedTsFolders.forEach((folder, index) => {
|
||||
const prefix = index === folderCount - 1 ? '└─' : '├─';
|
||||
const position = `${index + 1}/${folderCount}`;
|
||||
console.log(`│ ${prefix} ${position.padStart(5)} ${folder.padEnd(46)} │`);
|
||||
log.indent(`${index + 1}. ${folder}`);
|
||||
});
|
||||
|
||||
console.log('└' + '─'.repeat(60) + '┘\n');
|
||||
|
||||
// Build compilation object
|
||||
const compilationCommandObject: Record<string, string> = {};
|
||||
for (const tsFolder of sortedTsFolders) {
|
||||
@@ -198,12 +191,11 @@ export class TsBuildCli {
|
||||
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');
|
||||
log.error('No TypeScript files found to check');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\n🔎 Found ${allFiles.length} TypeScript file${allFiles.length !== 1 ? 's' : ''} to check`);
|
||||
log.step('🔎', `Found ${allFiles.length} TypeScript file${allFiles.length !== 1 ? 's' : ''} to check`);
|
||||
|
||||
const compiler = new TsCompiler(this.cwd, argvArg);
|
||||
const success = await compiler.checkTypes(allFiles);
|
||||
@@ -216,34 +208,34 @@ export class TsBuildCli {
|
||||
* 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');
|
||||
log.header('🔬', 'Default type checking sequence');
|
||||
|
||||
// First check ts/**/* without skiplibcheck
|
||||
console.log('📂 Checking ts/**/* files...');
|
||||
log.step('📂', '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/`);
|
||||
log.detail('📄', `${tsTsFiles.length} TypeScript files`);
|
||||
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/**/*');
|
||||
log.error('Type checking failed for ts/**/*');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('✅ Type checking passed for ts/**/*\n');
|
||||
log.success('Type checking passed for ts/**/*');
|
||||
} else {
|
||||
console.log(' No TypeScript files found in ts/\n');
|
||||
log.detail('📄', 'No TypeScript files found in ts/');
|
||||
}
|
||||
|
||||
// Then check test/**/* with skiplibcheck
|
||||
console.log('📂 Checking test/**/* files with --skiplibcheck...');
|
||||
log.step('📂', '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/`);
|
||||
log.detail('📄', `${testTsFiles.length} TypeScript files`);
|
||||
const testAbsoluteFiles = smartpath.transform.toAbsolute(testTsFiles, this.cwd) as string[];
|
||||
|
||||
const testArgvArg = { ...argvArg, skiplibcheck: true };
|
||||
@@ -251,15 +243,15 @@ export class TsBuildCli {
|
||||
const testSuccess = await testCompiler.checkTypes(testAbsoluteFiles);
|
||||
|
||||
if (!testSuccess) {
|
||||
console.error('❌ Type checking failed for test/**/*');
|
||||
log.error('Type checking failed for test/**/*');
|
||||
process.exit(1);
|
||||
}
|
||||
console.log('✅ Type checking passed for test/**/*\n');
|
||||
log.success('Type checking passed for test/**/*');
|
||||
} else {
|
||||
console.log(' No TypeScript files found in test/\n');
|
||||
log.detail('📄', 'No TypeScript files found in test/');
|
||||
}
|
||||
|
||||
console.log('✅ All default type checks passed!\n');
|
||||
log.success('All default type checks passed!');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -272,19 +264,19 @@ export class TsBuildCli {
|
||||
for (const pattern of patterns) {
|
||||
if (pattern.includes('*') || pattern.includes('{') || pattern.includes('?')) {
|
||||
// Handle as glob pattern
|
||||
console.log(`Processing glob pattern: ${pattern}`);
|
||||
log.step('🔎', `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}'`);
|
||||
log.warn(`No files matched pattern '${pattern}'`);
|
||||
} else {
|
||||
console.log(`📂 Found ${stringMatchedFiles.length} files matching pattern '${pattern}'`);
|
||||
log.detail('📂', `${stringMatchedFiles.length} files matching '${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}`);
|
||||
log.error(`Error processing glob pattern '${pattern}': ${err}`);
|
||||
}
|
||||
} else {
|
||||
// Handle as direct file path
|
||||
@@ -293,7 +285,7 @@ export class TsBuildCli {
|
||||
if (fileExists) {
|
||||
allFiles.push(filePath);
|
||||
} else {
|
||||
console.error(`❌ Error: File not found: ${filePath}`);
|
||||
log.error(`File not found: ${filePath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { CompilerOptions, Diagnostic, Program } from 'typescript';
|
||||
import typescript from 'typescript';
|
||||
import * as fs from 'fs';
|
||||
import * as smartdelay from '@push.rocks/smartdelay';
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
import * as smartpath from '@push.rocks/smartpath';
|
||||
@@ -9,6 +8,7 @@ import { TsConfig } from '../mod_config/index.js';
|
||||
import { FsHelpers } from '../mod_fs/index.js';
|
||||
import { performUnpack } from '../mod_unpack/index.js';
|
||||
import { TsPathRewriter } from '../mod_pathrewrite/index.js';
|
||||
import { TsBuildLogger as log } from '../mod_logger/index.js';
|
||||
|
||||
/**
|
||||
* Interface for error summary data
|
||||
@@ -118,33 +118,23 @@ export class TsCompiler {
|
||||
}
|
||||
|
||||
const { errorsByFile, generalErrors, totalErrors, totalFiles } = errorSummary;
|
||||
const c = log.c;
|
||||
|
||||
// Print error summary header
|
||||
console.log('\n' + '='.repeat(80));
|
||||
console.log('');
|
||||
console.log(c.dim + log.separator() + c.reset);
|
||||
console.log(
|
||||
`❌ Found ${totalErrors} error${totalErrors !== 1 ? 's' : ''} in ${totalFiles} file${totalFiles !== 1 ? 's' : ''}:`
|
||||
`${c.red}❌ Found ${totalErrors} error${totalErrors !== 1 ? 's' : ''} in ${totalFiles} file${totalFiles !== 1 ? 's' : ''}${c.reset}`
|
||||
);
|
||||
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',
|
||||
};
|
||||
console.log(c.dim + log.separator() + c.reset);
|
||||
|
||||
// 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}`
|
||||
`\n ${c.cyan}File: ${displayPath}${c.reset} ${c.yellow}(${fileErrors.length} error${fileErrors.length !== 1 ? 's' : ''})${c.reset}`
|
||||
);
|
||||
console.log('-'.repeat(80));
|
||||
console.log(` ${c.dim}${'─'.repeat(40)}${c.reset}`);
|
||||
|
||||
fileErrors.forEach((diagnostic) => {
|
||||
if (diagnostic.file && diagnostic.start !== undefined) {
|
||||
@@ -153,19 +143,18 @@ export class TsCompiler {
|
||||
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}`
|
||||
` ${c.white}Line ${line + 1}, Col ${character + 1}${c.reset}: ${c.brightRed}${errorCode}${c.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}`);
|
||||
console.log(` ${lineContent.trimEnd()}`);
|
||||
const indicator = ' '.repeat(character) + `${c.red}^${c.reset}`;
|
||||
console.log(` ${indicator}`);
|
||||
}
|
||||
} catch {
|
||||
// Failed to get source text, skip showing the code snippet
|
||||
// Failed to get source text
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -173,17 +162,18 @@ export class TsCompiler {
|
||||
|
||||
// Print general errors
|
||||
if (generalErrors.length > 0) {
|
||||
console.log(`\n${colors.yellow}General Errors:${colors.reset}`);
|
||||
console.log('-'.repeat(80));
|
||||
console.log(`\n ${c.yellow}General Errors:${c.reset}`);
|
||||
console.log(` ${c.dim}${'─'.repeat(40)}${c.reset}`);
|
||||
|
||||
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(` ${c.brightRed}${errorCode}${c.reset}: ${message}`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(80) + '\n');
|
||||
console.log('');
|
||||
console.log(c.dim + log.separator() + c.reset);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,12 +181,11 @@ export class TsCompiler {
|
||||
*/
|
||||
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');
|
||||
log.warn('WARNING: You are skipping libcheck... Is that really wanted?');
|
||||
log.indent('Continuing in 5 seconds...');
|
||||
await smartdelay.delayFor(5000);
|
||||
} else if (!this.argvArg?.quiet && !this.argvArg?.json) {
|
||||
console.log('⚠️ skipLibCheck enabled; use --confirmskiplibcheck to pause with warning.');
|
||||
log.warn('skipLibCheck enabled; use --confirmskiplibcheck to pause with warning.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,17 +203,14 @@ export class TsCompiler {
|
||||
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}`);
|
||||
log.step('🔨', `[${taskNumber}/${totalTasks}] Compiling ${fileCount} file${fileCount !== 1 ? 's' : ''} from ${sourcePattern}`);
|
||||
log.detail('📁', `Output: ${relativeDestDir}`);
|
||||
} else {
|
||||
console.log(`🔨 Compiling ${fileNames.length} files...`);
|
||||
log.step('🔨', `Compiling ${fileNames.length} files...`);
|
||||
}
|
||||
|
||||
const done = smartpromise.defer<ICompileResult>();
|
||||
@@ -234,17 +220,21 @@ export class TsCompiler {
|
||||
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');
|
||||
log.error('Pre-emit checks failed');
|
||||
done.resolve({ emittedFiles: [], errorSummary: preEmitErrorSummary });
|
||||
return done.promise;
|
||||
}
|
||||
|
||||
// If no pre-emit errors, proceed with emit
|
||||
const emitResult = program.emit();
|
||||
|
||||
// Yield to the event loop so any pending microtasks, nextTick callbacks,
|
||||
// or deferred I/O from TypeScript's emit (e.g. libuv write completions)
|
||||
// can settle before we read or modify the output directory.
|
||||
await new Promise<void>((resolve) => process.nextTick(resolve));
|
||||
|
||||
const emitErrorSummary = this.processDiagnostics(emitResult.diagnostics);
|
||||
|
||||
// Combine error summaries
|
||||
@@ -262,9 +252,9 @@ export class TsCompiler {
|
||||
|
||||
if (taskInfo) {
|
||||
const { taskNumber, totalTasks } = taskInfo;
|
||||
console.log(`✅ [${taskNumber}/${totalTasks}] Task completed in ${duration}ms`);
|
||||
log.success(`[${taskNumber}/${totalTasks}] Completed in ${duration}ms`);
|
||||
} else {
|
||||
console.log(`✅ TypeScript emit succeeded! (${duration}ms)`);
|
||||
log.success(`TypeScript emit succeeded (${duration}ms)`);
|
||||
}
|
||||
|
||||
// Get count of emitted files by type
|
||||
@@ -273,16 +263,13 @@ export class TsCompiler {
|
||||
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`
|
||||
);
|
||||
log.detail('📄', `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');
|
||||
log.error('TypeScript emit failed');
|
||||
done.resolve({ emittedFiles: [], errorSummary: combinedErrorSummary });
|
||||
}
|
||||
|
||||
@@ -318,56 +305,46 @@ export class TsCompiler {
|
||||
const isJson = this.argvArg?.json === true;
|
||||
|
||||
if (!isQuiet && !isJson) {
|
||||
console.log(`\n👷 TypeScript Compilation Tasks (${totalTasks} task${totalTasks !== 1 ? 's' : ''}):`);
|
||||
log.header('👷', `TypeScript Compilation Tasks (${totalTasks} task${totalTasks !== 1 ? 's' : ''})`);
|
||||
Object.entries(globPatterns).forEach(([source, dest]) => {
|
||||
console.log(` 📂 ${source} → ${dest}`);
|
||||
log.detail('📂', `${source} → ${dest}`);
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Phase 1: Resolve glob patterns and clean ALL output directories upfront.
|
||||
interface IResolvedTask {
|
||||
pattern: string;
|
||||
destPath: string;
|
||||
destDir: string;
|
||||
absoluteFiles: string[];
|
||||
}
|
||||
const resolvedTasks: IResolvedTask[] = [];
|
||||
|
||||
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;
|
||||
|
||||
// Diagnostic helper
|
||||
const diagSnap = (label: string) => {
|
||||
if (!isQuiet && !isJson) {
|
||||
for (const prevDir of successfulOutputDirs) {
|
||||
try {
|
||||
const entries = fs.readdirSync(prevDir);
|
||||
const dirs = entries.filter(e => { try { return fs.statSync(prevDir + '/' + e).isDirectory(); } catch { return false; } });
|
||||
const shortDir = prevDir.replace(this.cwd + '/', '');
|
||||
console.log(` 📋 [${label}] ${shortDir}: ${entries.length} entries, ${dirs.length} dirs [${entries.sort().join(', ')}]`);
|
||||
} catch {
|
||||
console.log(` 📋 [${label}] ${prevDir.replace(this.cwd + '/', '')}: MISSING!`);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Clear the destination directory before compilation if it exists
|
||||
diagSnap('pre-clear');
|
||||
if (await FsHelpers.directoryExists(destDir)) {
|
||||
if (!isQuiet && !isJson) {
|
||||
console.log(`🧹 Clearing output directory: ${destPath}`);
|
||||
log.step('🧹', `Clearing output directory: ${destPath}`);
|
||||
}
|
||||
await FsHelpers.removeDirectory(destDir);
|
||||
}
|
||||
diagSnap('post-clear');
|
||||
|
||||
// Update compiler options with the output directory
|
||||
resolvedTasks.push({ pattern, destPath, destDir, absoluteFiles });
|
||||
}
|
||||
|
||||
// Phase 2: Compile all tasks.
|
||||
const pendingUnpacks: Array<{ pattern: string; destDir: string }> = [];
|
||||
|
||||
for (const task of resolvedTasks) {
|
||||
const options: CompilerOptions = {
|
||||
...customOptions,
|
||||
outDir: destDir,
|
||||
outDir: task.destDir,
|
||||
listEmittedFiles: true,
|
||||
};
|
||||
|
||||
@@ -375,70 +352,27 @@ export class TsCompiler {
|
||||
const taskInfo: ITaskInfo = {
|
||||
taskNumber: currentTask,
|
||||
totalTasks,
|
||||
sourcePattern: pattern,
|
||||
destDir: destPath,
|
||||
fileCount: absoluteFiles.length,
|
||||
sourcePattern: task.pattern,
|
||||
destDir: task.destPath,
|
||||
fileCount: task.absoluteFiles.length,
|
||||
};
|
||||
|
||||
const result = await this.compileFiles(absoluteFiles, options, taskInfo);
|
||||
const result = await this.compileFiles(task.absoluteFiles, options, taskInfo);
|
||||
emittedFiles.push(...result.emittedFiles);
|
||||
errorSummaries.push(result.errorSummary);
|
||||
diagSnap('post-compile');
|
||||
|
||||
// Diagnostic: log emitted files that went to unexpected directories
|
||||
if (!isQuiet && !isJson && result.emittedFiles.length > 0) {
|
||||
const unexpectedFiles = result.emittedFiles.filter(f => !f.startsWith(destDir + '/') && !f.startsWith(destDir + '\\'));
|
||||
if (unexpectedFiles.length > 0) {
|
||||
console.log(` ⚠️ [diag] ${unexpectedFiles.length} files emitted OUTSIDE ${destPath}:`);
|
||||
for (const f of unexpectedFiles.slice(0, 20)) {
|
||||
console.log(` ${f.replace(this.cwd + '/', '')}`);
|
||||
}
|
||||
if (unexpectedFiles.length > 20) {
|
||||
console.log(` ... and ${unexpectedFiles.length - 20} more`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Perform unpack if compilation succeeded
|
||||
if (result.errorSummary.totalErrors === 0) {
|
||||
await performUnpack(pattern, destDir, this.cwd);
|
||||
successfulOutputDirs.push(destDir);
|
||||
pendingUnpacks.push({ pattern: task.pattern, destDir: task.destDir });
|
||||
successfulOutputDirs.push(task.destDir);
|
||||
}
|
||||
|
||||
// Fsync all output directories to force XFS metadata commit
|
||||
// before the next compilation step. Without this, XFS delayed logging
|
||||
// can cause directory entries from previous compilations to become
|
||||
// invisible or corrupted during subsequent TypeScript emit operations.
|
||||
for (const dir of successfulOutputDirs) {
|
||||
try {
|
||||
const fd = fs.openSync(dir, 'r');
|
||||
fs.fsyncSync(fd);
|
||||
fs.closeSync(fd);
|
||||
} catch {
|
||||
// Directory might not exist yet
|
||||
}
|
||||
}
|
||||
|
||||
// Diagnostic: log all output directory states after each compilation
|
||||
if (!isQuiet && !isJson) {
|
||||
for (const prevDir of successfulOutputDirs) {
|
||||
try {
|
||||
const entries = fs.readdirSync(prevDir);
|
||||
const dirs = entries.filter(e => {
|
||||
try { return fs.statSync(prevDir + '/' + e).isDirectory(); } catch { return false; }
|
||||
});
|
||||
console.log(` 📋 [diag] ${prevDir.replace(this.cwd + '/', '')}: ${entries.length} entries, ${dirs.length} dirs`);
|
||||
} catch {
|
||||
console.log(` 📋 [diag] ${prevDir.replace(this.cwd + '/', '')}: MISSING!`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Rewrite import paths in all output directories to handle cross-module references
|
||||
// This must happen after ALL compilations so all destination folders exist
|
||||
// Use fromProjectDirectory to detect ALL ts_* folders, not just the ones being compiled
|
||||
// Phase 3: Perform all unpacks after all compilations are done.
|
||||
for (const { pattern, destDir } of pendingUnpacks) {
|
||||
await performUnpack(pattern, destDir, this.cwd);
|
||||
}
|
||||
|
||||
// Rewrite import paths in all output directories
|
||||
if (successfulOutputDirs.length > 0) {
|
||||
const rewriter = await TsPathRewriter.fromProjectDirectory(this.cwd);
|
||||
let totalRewritten = 0;
|
||||
@@ -446,22 +380,7 @@ export class TsCompiler {
|
||||
totalRewritten += await rewriter.rewriteDirectory(outputDir);
|
||||
}
|
||||
if (totalRewritten > 0 && !isQuiet && !isJson) {
|
||||
console.log(` 🔄 Rewrote import paths in ${totalRewritten} file${totalRewritten !== 1 ? 's' : ''}`);
|
||||
}
|
||||
|
||||
// Diagnostic: log output directory states after path rewriting
|
||||
if (!isQuiet && !isJson) {
|
||||
for (const dir of successfulOutputDirs) {
|
||||
try {
|
||||
const entries = fs.readdirSync(dir);
|
||||
const dirs = entries.filter(e => {
|
||||
try { return fs.statSync(dir + '/' + e).isDirectory(); } catch { return false; }
|
||||
});
|
||||
console.log(` 📋 [diag-post-rewrite] ${dir.replace(this.cwd + '/', '')}: ${entries.length} entries, ${dirs.length} dirs`);
|
||||
} catch {
|
||||
console.log(` 📋 [diag-post-rewrite] ${dir.replace(this.cwd + '/', '')}: MISSING!`);
|
||||
}
|
||||
}
|
||||
log.detail('🔄', `Rewrote import paths in ${totalRewritten} file${totalRewritten !== 1 ? 's' : ''}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -510,7 +429,7 @@ export class TsCompiler {
|
||||
const options = { ...this.createOptions(customOptions), noEmit: true };
|
||||
const fileCount = fileNames.length;
|
||||
|
||||
console.log(`\n🔍 Checking if ${fileCount} file${fileCount !== 1 ? 's' : ''} can be emitted...`);
|
||||
log.step('🔍', `Checking if ${fileCount} file${fileCount !== 1 ? 's' : ''} can be emitted...`);
|
||||
|
||||
const program = this.createProgram(fileNames, options);
|
||||
|
||||
@@ -530,12 +449,10 @@ export class TsCompiler {
|
||||
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`);
|
||||
log.success(`Emit check passed (${fileCount} file${fileCount !== 1 ? 's' : ''})`);
|
||||
} 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');
|
||||
log.error('Emit check failed');
|
||||
}
|
||||
|
||||
return success;
|
||||
@@ -548,7 +465,7 @@ export class TsCompiler {
|
||||
const options = { ...this.createOptions(customOptions), noEmit: true };
|
||||
const fileCount = fileNames.length;
|
||||
|
||||
console.log(`\n🔍 Type checking ${fileCount} TypeScript file${fileCount !== 1 ? 's' : ''}...`);
|
||||
log.step('🔍', `Type checking ${fileCount} file${fileCount !== 1 ? 's' : ''}...`);
|
||||
|
||||
const program = this.createProgram(fileNames, options);
|
||||
const diagnostics = typescript.getPreEmitDiagnostics(program);
|
||||
@@ -557,12 +474,10 @@ export class TsCompiler {
|
||||
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`);
|
||||
log.success(`Type check passed (${fileCount} file${fileCount !== 1 ? 's' : ''})`);
|
||||
} 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');
|
||||
log.error('Type check failed');
|
||||
}
|
||||
|
||||
return success;
|
||||
@@ -600,42 +515,28 @@ export class TsCompiler {
|
||||
* Display final compilation summary
|
||||
*/
|
||||
private displayFinalSummary(errorSummary: IErrorSummary): void {
|
||||
const c = log.c;
|
||||
|
||||
if (errorSummary.totalErrors === 0) {
|
||||
console.log('\n📊 \x1b[32mCompilation Summary: All tasks completed successfully! ✅\x1b[0m\n');
|
||||
log.header('📊', `${c.green}Compilation Summary: All tasks completed successfully! ✅${c.reset}`);
|
||||
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}`);
|
||||
}
|
||||
log.header('📊', 'Compilation Summary');
|
||||
|
||||
console.log(
|
||||
`\n${colors.brightRed}Total: ${errorSummary.totalErrors} error${errorSummary.totalErrors !== 1 ? 's' : ''} across ${errorSummary.totalFiles} file${errorSummary.totalFiles !== 1 ? 's' : ''}${colors.reset}`
|
||||
`${c.brightRed}❌ ${errorSummary.totalErrors} error${errorSummary.totalErrors !== 1 ? 's' : ''} across ${errorSummary.totalFiles} file${errorSummary.totalFiles !== 1 ? 's' : ''}${c.reset}`
|
||||
);
|
||||
console.log('='.repeat(80) + '\n');
|
||||
|
||||
Object.entries(errorSummary.errorsByFile).forEach(([fileName, errors]) => {
|
||||
const displayPath = fileName.replace(process.cwd(), '').replace(/^\//, '');
|
||||
log.indent(
|
||||
`${c.red}•${c.reset} ${c.cyan}${displayPath}${c.reset} ${c.yellow}(${errors.length} error${errors.length !== 1 ? 's' : ''})${c.reset}`
|
||||
);
|
||||
});
|
||||
|
||||
if (errorSummary.generalErrors.length > 0) {
|
||||
console.log(`${c.brightRed}❌ General errors: ${errorSummary.generalErrors.length}${c.reset}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ export const compilerOptionsDefault: CompilerOptions = {
|
||||
noImplicitAny: false,
|
||||
esModuleInterop: true,
|
||||
verbatimModuleSyntax: true,
|
||||
baseUrl: './',
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -98,12 +97,13 @@ export class TsConfig {
|
||||
|
||||
// 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_');
|
||||
const paths: Record<string, string[]> = { ...tsconfig.compilerOptions.paths };
|
||||
for (const pathKey of Object.keys(paths)) {
|
||||
if (Array.isArray(paths[pathKey]) && paths[pathKey].length > 0) {
|
||||
paths[pathKey][0] = paths[pathKey][0].replace('./ts_', './dist_ts_');
|
||||
}
|
||||
}
|
||||
returnObject.paths = paths;
|
||||
}
|
||||
|
||||
this.cachedTsConfig = returnObject;
|
||||
|
||||
@@ -17,7 +17,8 @@ export interface ITsPublishJson {
|
||||
*/
|
||||
export class TsPublishConfig {
|
||||
private folderPath: string;
|
||||
private cachedConfig: ITsPublishJson | null | undefined = undefined;
|
||||
private cachedConfig: ITsPublishJson | null = null;
|
||||
private cacheLoaded = false;
|
||||
|
||||
constructor(folderPath: string) {
|
||||
this.folderPath = folderPath;
|
||||
@@ -35,7 +36,7 @@ export class TsPublishConfig {
|
||||
* Returns null if file doesn't exist or is invalid
|
||||
*/
|
||||
public async load(): Promise<ITsPublishJson | null> {
|
||||
if (this.cachedConfig !== undefined) {
|
||||
if (this.cacheLoaded) {
|
||||
return this.cachedConfig;
|
||||
}
|
||||
|
||||
@@ -43,9 +44,11 @@ export class TsPublishConfig {
|
||||
const configPath = path.join(this.folderPath, 'tspublish.json');
|
||||
const content = await fs.promises.readFile(configPath, 'utf8');
|
||||
this.cachedConfig = JSON.parse(content);
|
||||
this.cacheLoaded = true;
|
||||
return this.cachedConfig;
|
||||
} catch {
|
||||
this.cachedConfig = null;
|
||||
this.cacheLoaded = true;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -55,7 +58,7 @@ export class TsPublishConfig {
|
||||
* Returns null if file doesn't exist or is invalid
|
||||
*/
|
||||
public loadSync(): ITsPublishJson | null {
|
||||
if (this.cachedConfig !== undefined) {
|
||||
if (this.cacheLoaded) {
|
||||
return this.cachedConfig;
|
||||
}
|
||||
|
||||
@@ -63,9 +66,11 @@ export class TsPublishConfig {
|
||||
const configPath = path.join(this.folderPath, 'tspublish.json');
|
||||
const content = fs.readFileSync(configPath, 'utf8');
|
||||
this.cachedConfig = JSON.parse(content);
|
||||
this.cacheLoaded = true;
|
||||
return this.cachedConfig;
|
||||
} catch {
|
||||
this.cachedConfig = null;
|
||||
this.cacheLoaded = true;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -111,6 +116,7 @@ export class TsPublishConfig {
|
||||
* Clear the cached config (useful for reloading)
|
||||
*/
|
||||
public clearCache(): void {
|
||||
this.cachedConfig = undefined;
|
||||
this.cachedConfig = null;
|
||||
this.cacheLoaded = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,9 +123,6 @@ export class FsHelpers {
|
||||
|
||||
/**
|
||||
* Remove a directory recursively.
|
||||
* Uses synchronous rm to avoid XFS metadata corruption observed with
|
||||
* async fs.promises.rm affecting sibling directory entries on the
|
||||
* libuv thread pool under signal pressure.
|
||||
*/
|
||||
public static async removeDirectory(dirPath: string): Promise<void> {
|
||||
fs.rmSync(dirPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
||||
|
||||
72
ts/mod_logger/classes.logger.ts
Normal file
72
ts/mod_logger/classes.logger.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Centralized console output for tsbuild.
|
||||
*
|
||||
* Visual hierarchy (4 levels):
|
||||
* HEADER — top-level section start (emoji + bold text + separator line)
|
||||
* STEP — major action within a section (emoji + text, no indent)
|
||||
* DETAIL — supplementary info under a step (3-space indent + emoji + text)
|
||||
* SUCCESS/ERROR/WARN — outcome indicators (emoji + text, no indent)
|
||||
*/
|
||||
export class TsBuildLogger {
|
||||
static readonly c = {
|
||||
reset: '\x1b[0m',
|
||||
bold: '\x1b[1m',
|
||||
dim: '\x1b[2m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
cyan: '\x1b[36m',
|
||||
white: '\x1b[37m',
|
||||
brightRed: '\x1b[91m',
|
||||
brightGreen: '\x1b[92m',
|
||||
brightYellow: '\x1b[93m',
|
||||
};
|
||||
|
||||
static readonly SEPARATOR_WIDTH = 70;
|
||||
|
||||
static separator(char = '─'): string {
|
||||
return char.repeat(this.SEPARATOR_WIDTH);
|
||||
}
|
||||
|
||||
/** Level 1: Section header. Blank line before, separator after. */
|
||||
static header(emoji: string, text: string): void {
|
||||
console.log('');
|
||||
console.log(`${emoji} ${this.c.bold}${text}${this.c.reset}`);
|
||||
console.log(this.c.dim + this.separator() + this.c.reset);
|
||||
}
|
||||
|
||||
/** Level 2: Step within a section. No indent. */
|
||||
static step(emoji: string, text: string): void {
|
||||
console.log(`${emoji} ${text}`);
|
||||
}
|
||||
|
||||
/** Level 3: Detail under a step. 3-space indent. */
|
||||
static detail(emoji: string, text: string): void {
|
||||
console.log(` ${emoji} ${text}`);
|
||||
}
|
||||
|
||||
/** Outcome: success */
|
||||
static success(text: string): void {
|
||||
console.log(`${this.c.green}✅ ${text}${this.c.reset}`);
|
||||
}
|
||||
|
||||
/** Outcome: error (goes to stderr) */
|
||||
static error(text: string): void {
|
||||
console.error(`${this.c.red}❌ ${text}${this.c.reset}`);
|
||||
}
|
||||
|
||||
/** Outcome: warning */
|
||||
static warn(text: string): void {
|
||||
console.log(`${this.c.yellow}⚠️ ${text}${this.c.reset}`);
|
||||
}
|
||||
|
||||
/** Plain indented line (for code snippets, list items, etc.) */
|
||||
static indent(text: string, level = 1): void {
|
||||
console.log(' '.repeat(level) + text);
|
||||
}
|
||||
|
||||
/** Blank line */
|
||||
static blank(): void {
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
1
ts/mod_logger/index.ts
Normal file
1
ts/mod_logger/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './classes.logger.js';
|
||||
@@ -2,6 +2,7 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { TsPublishConfig } from '../mod_config/index.js';
|
||||
import { FsHelpers } from '../mod_fs/index.js';
|
||||
import { TsBuildLogger as log } from '../mod_logger/index.js';
|
||||
|
||||
/**
|
||||
* TsUnpacker handles flattening of nested TypeScript output directories.
|
||||
@@ -90,9 +91,9 @@ export class TsUnpacker {
|
||||
* 2. Moving contents of the nested source folder up to the dest dir
|
||||
* 3. Removing the now-empty nested source folder
|
||||
*
|
||||
* Uses synchronous fs operations to avoid race conditions with
|
||||
* async readdir returning partial/stale results under signal pressure
|
||||
* or XFS metadata lag (observed in process-group environments like gitzone).
|
||||
* Uses synchronous fs operations for reliability.
|
||||
* Called after all compilations are complete (not between compilations)
|
||||
* to avoid filesystem metadata issues on XFS.
|
||||
*
|
||||
* Returns true if unpacking was performed, false if skipped.
|
||||
*/
|
||||
@@ -107,17 +108,6 @@ export class TsUnpacker {
|
||||
|
||||
const nestedPath = this.getNestedPath();
|
||||
|
||||
// Force XFS to flush pending directory metadata before reading.
|
||||
// XFS delayed logging (CIL) can defer metadata commits, causing
|
||||
// readdirSync/opendirSync to return incomplete results immediately
|
||||
// after TypeScript's emit() creates files via writeFileSync.
|
||||
const destFd = fs.openSync(this.destDir, 'r');
|
||||
fs.fsyncSync(destFd);
|
||||
fs.closeSync(destFd);
|
||||
const nestedFd = fs.openSync(nestedPath, 'r');
|
||||
fs.fsyncSync(nestedFd);
|
||||
fs.closeSync(nestedFd);
|
||||
|
||||
// Step 1: Remove sibling entries (everything in dest except the source folder)
|
||||
const destEntries = fs.readdirSync(this.destDir);
|
||||
for (const entry of destEntries) {
|
||||
@@ -138,9 +128,7 @@ export class TsUnpacker {
|
||||
// Step 3: Remove the now-empty nested directory
|
||||
fs.rmdirSync(nestedPath);
|
||||
|
||||
// Diagnostic: verify final state
|
||||
const finalEntries = fs.readdirSync(this.destDir);
|
||||
console.log(` 📦 Unpacked ${this.sourceFolderName}: moved ${nestedEntries.length} entries, final: ${finalEntries.length} entries`);
|
||||
log.detail('📦', `Unpacked ${this.sourceFolderName}: ${nestedEntries.length} entries`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user