@git.zone/tsbuild
v6.0.2
Published
A tool for compiling TypeScript files using the latest nightly features, offering flexible APIs and a CLI for streamlined development.
Maintainers
Readme
@git.zone/tsbuild
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/. 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/ account to submit Pull Requests directly.
Install
pnpm add --save-dev @git.zone/tsbuildWhy 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 |
| Folder tsconfig.json | A source folder can compile in its own environment, such as a service worker with the webworker library, and editors read the same settings |
| 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
CLI Usage
Compile your TypeScript project:
pnpm exec tsbuildCompiles ./ts/**/*.ts to ./dist_ts/
Custom directories:
pnpm exec tsbuild custom src utilsCompiles:
./src/**/*.ts->./dist_src/./utils/**/*.ts->./dist_utils/
Auto-discover and compile in dependency order:
pnpm exec tsbuild tsfoldersFinds all ts_* folders and compiles them respecting dependencies.
Programmatic Usage
Basic compilation:
import { TsCompiler } from '@git.zone/tsbuild';
const compiler = new TsCompiler();
await compiler.compileFilesOrThrow([
'./src/index.ts',
'./src/utils.ts'
], { outDir: './dist' });Production-ready with error tracking (recommended):
import { TsCompiler } from '@git.zone/tsbuild';
const compiler = new TsCompiler();
const result = await compiler.compileFiles([
'./src/index.ts',
'./src/utils.ts'
], { outDir: './dist' });
if (result.errorSummary.totalErrors > 0) {
console.error(`Compilation failed with ${result.errorSummary.totalErrors} errors`);
process.exit(1);
}
console.log(`Compiled ${result.emittedFiles.length} files successfully!`);Glob pattern compilation:
import { TsCompiler } from '@git.zone/tsbuild';
const compiler = new TsCompiler();
await compiler.compileGlob({
'./ts/**/*.ts': './dist_ts',
'./ts_web/**/*.ts': './dist_web'
});The TypeScript compiler
tsbuild compiles with TypeScript 7, the native compiler, pinned to one exact version ([email protected]). TypeScript 7's JavaScript API (typescript/unstable/sync) starts a tsc --api process and talks to it over an unversioned protocol that may change with any release, so tsbuild does not accept a version range: every TypeScript upgrade is a deliberate tsbuild release, and tsbuild's golden tests compare the exact output and diagnostics of each fixture to catch any drift.
The pin is a development build because TypeScript 7.0, the stable release, ships an earlier version of that API, without the programs, the emit and the in-memory files tsbuild compiles with; the API is complete from 7.1.0-dev.20260919.1. Builds from 7.1.0-dev.20260929.1 on report TS5115 ("Instantiations of the following types appear infinitely circular") in the declarations of zod 4.5.0 to 4.6.5 (microsoft/TypeScript#64605), so tsbuild pins the last build before that regression.
tsbuild 5, on TypeScript 6, continues as a maintenance line for projects that cannot migrate yet:
pnpm add --save-dev @git.zone/tsbuild@^5- One
tsc --apiprocess serves each tsbuild operation (a compile, a type check, an emit check); it starts with the operation and stops when the operation ends, also when it fails. It runs in the project directory, the oneTsCompilerwas given, because TypeScript finds the packagestypesnames, and the default type roots, from there; operations on different projects in one Node process each run their own. - tsbuild's API speaks in its own types: compiler options as a tsconfig.json spells them (
ICompilerOptions, for example{ module: 'nodenext', lib: ['dom', 'esnext'] }) and diagnostics asITsDiagnostic(file, one-based line and column, code, category, message). No type of the TypeScript compiler appears in tsbuild's API. - tsbuild no longer re-exports the compiler. Tooling that works on TypeScript sources - codemods, AST assertions in tests - depends on
typescriptitself. Note that TypeScript 7 no longer ships the classic JavaScript compiler API (createSourceFile,forEachChild,SyntaxKind, ...) undertypescript; its parser and AST are reachable throughtypescript/unstable/sync(API.createSourceFile) andtypescript/unstable/ast, and a tool that needs the classic API stays ontypescript@6. - TypeScript 7 removed options that TypeScript 6 only deprecated:
target: "es5",moduleResolution: "node10"(also spelled"node") and"classic",esModuleInterop: false,allowSyntheticDefaultImports: false,alwaysStrict: false,baseUrl,downlevelIteration,outFileandmoduleamd,umdandsystem. A project that still sets one fails to compile with TypeScript's own error (TS5102 or TS5108) naming the option.
Migrating from tsbuild 5
Remove the options TypeScript 7 removed from every
tsconfig.jsontsbuild reads, the project's and each source folder's:| Option | Replacement | |--------|-------------| |
baseUrl| Remove it and write eachpathstarget relative to thetsconfig.json("./ts_shared/*") | |moduleResolution: "node","node10","nodejs","classic"|"nodenext", tsbuild's default, or"bundler"for code that only a bundler reads | |target: "es5","es3"| Remove it (tsbuild's default isesnext) or namees2015or later | |downlevelIteration| Remove it; it only applied to ES5 output | |esModuleInterop: false,allowSyntheticDefaultImports: false,alwaysStrict: false| Remove them; TypeScript 7 always applies these | |outFile,module: "amd","umd","system","none"| Remove them and bundle the output, for example with@git.zone/tsbundle|Import TypeScript yourself. Code that imported
@git.zone/tsbuild/typescript, ortypescript,CompilerOptions,ScriptTargetorModuleKindfrom@git.zone/tsbuild, depends ontypescriptdirectly:typescript@^6for the classic compiler API (createProgram,createSourceFile,SyntaxKind, ...).Expect a few new type errors. TypeScript 7 infers some types for which TypeScript 6 fell back to
anyunder tsbuild'snoImplicitAny: false, such as a variable that its own initializer references (const host = new Host({ ready: () => host.started })). Calls through such a variable are type-checked now, and the errors they report are genuine.Drop memory workarounds for type checks. The checker runs in the native
tscprocess, outside Node's heap: a--max-old-space-sizeset for tsbuild no longer applies to it, and tsbuild 6 has nocheckerTypeLimit, the bounded checker of tsbuild 5.2.Expect declarations that differ in form only. Against tsbuild 5, the emitted JavaScript is the same apart from its inline source maps. Declarations print some types differently: as
typeofthe declaration a value comes from (with an added import, or adeclare constfor a module-local function), with union and object members in another order, with double-quoted strings,unknowninstead ofunknown | undefined, and JSDoc kept on tuple elements.Keep relying on installed
@typespackages, or name them. TypeScript 5 included every installed@typespackage; TypeScript 6 and 7 include only the onestypesnames, and none without it, soprocess,BufferorNodeJSbecame unknown (TS2591, TS2694) in projects that relied on the old default. From 6.0.1 on, tsbuild setstypes: ["*"]when notsconfig.jsonsetstypes: TypeScript's spelling for every package in the type roots (thosetypeRootsnames, or elsenode_modules/@typesof the project and of its parent directories), so such a project builds as it did. Atypeslist in the project's or a folder'stsconfig.json, or in programmatic options, replaces it unchanged. Editors read thetsconfig.jsonwith TypeScript 6 or 7 rules and see no@typespackage it does not name; name them there ("types": ["node"]) for the editor to see what tsbuild compiles with.
CLI Commands
1. Default Build
pnpm exec tsbuild [options]Compiles all TypeScript files from ./ts/ to ./dist_ts/
Options:
| Flag | Description |
|------|-------------|
| --skiplibcheck | Skip type checking of declaration files |
| --confirmskiplibcheck | Skip lib check with extended warning (5s pause) |
| --disallowimplicitany | Disallow implicit any types |
| --commonjs | Emit CommonJS instead of ECMAScript modules: module: "commonjs", moduleResolution: "bundler" and verbatimModuleSyntax: false |
| --json | Output results as JSON (for CI/CD) |
| --quiet | Suppress console output |
Examples:
# Standard build
pnpm exec tsbuild
# Build with JSON output for CI
pnpm exec tsbuild --json --quiet
# CommonJS build
pnpm exec tsbuild --commonjs
# Strict mode
pnpm exec tsbuild --disallowimplicitany2. Custom Directories
pnpm exec tsbuild custom <dir1> <dir2> ... [options]Compile specific directories to their corresponding dist_ folders.
# Compile src and utils
pnpm exec tsbuild custom src utils
# Creates: ./dist_src/ and ./dist_utils/
# Multiple directories with options
pnpm exec tsbuild custom api models services --commonjs3. TSFolders (Dependency-Aware)
pnpm exec tsbuild tsfolders [options]Automatically discovers and compiles all ts_* folders in dependency order:
- Prioritizes
ts_interfacesfirst (if no tspublish.json) - Prioritizes
ts_sharedsecond (if no tspublish.json) - Reads
tspublish.jsonin each folder fororderproperty - Compiles in correct sequence
Example output:
TypeScript Folder Compilation Plan (5 folders)
1. ts_interfaces
2. ts_shared
3. ts_core
4. ts_utils
5. ts_modules4. Emit Check
pnpm exec tsbuild emitcheck <file_or_pattern> [more...] [options]Validates TypeScript files can be compiled without actually emitting them.
# Check specific files
pnpm exec tsbuild emitcheck src/main.ts src/utils.ts
# Check with glob patterns
pnpm exec tsbuild emitcheck "src/**/*.ts" "test/**/*.ts"Exit codes:
0- All files can be emitted1- One or more files have errors
5. Type Check
pnpm exec tsbuild check [pattern] [more...] [options]Performs type checking without emitting files.
With arguments: Check specified files/patterns
pnpm exec tsbuild check "ts/**/*.ts"
pnpm exec tsbuild check "src/**/*.ts" "test/**/*.ts"Without arguments: Two-phase default check
- Phase 1: Type check
ts/**/*(strict, includes .d.ts) - Phase 2: Type check
test/**/*(relaxed, skipLibCheck: true)
pnpm exec tsbuild check
# Running default type checking sequence...
# Checking ts/**/* files...
# Checking test/**/* files with --skiplibcheck...
# All default type checks passed!Both forms check every file under the compiler options of the folder that owns it, including files that a checked file imports from another folder; see Folder tsconfig.json.
API Reference
TsCompiler Class
The main class for TypeScript compilation.
import { TsCompiler } from '@git.zone/tsbuild';
const compiler = new TsCompiler(cwd?: string, argvArg?: any);Constructor Parameters:
cwd- Working directory (defaults toprocess.cwd())argvArg- CLI arguments object for flags like--skiplibcheck,--quiet, etc.
compileFiles(fileNames, customOptions?, taskInfo?)
Compile files with error tracking. Returns result instead of throwing.
const result = await compiler.compileFiles(
['./src/index.ts', './src/utils.ts'],
{ outDir: './dist' }
);
console.log(`Emitted: ${result.emittedFiles.length} files`);
console.log(`Errors: ${result.errorSummary.totalErrors}`);Returns: Promise<ICompileResult>
interface ICompileResult {
emittedFiles: string[];
errorSummary: IErrorSummary;
}
interface IErrorSummary {
errorsByFile: Record<string, ITsDiagnostic[]>;
generalErrors: ITsDiagnostic[];
totalErrors: number;
totalFiles: number;
}
// tsbuild's own diagnostic type
interface ITsDiagnostic {
fileName: string | null; // absolute; null for a problem of the compilation as a whole
line: number | null; // one-based
column: number | null; // one-based
code: number; // 2322 for TS2322; 0 for tsbuild's own diagnostics
category: 'error' | 'warning' | 'suggestion' | 'message';
message: string; // a chain of messages, one per line, indented by depth
sourceLine: string | null;
}customOptions of every method are compiler options as a tsconfig.json spells them (ICompilerOptions); relative paths resolve from cwd.
compileFilesOrThrow(fileNames, customOptions?)
Compile files and throw on error. For simple scripts.
try {
const emittedFiles = await compiler.compileFilesOrThrow(
['./src/index.ts'],
{ outDir: './dist' }
);
console.log('Compiled:', emittedFiles);
} catch (error) {
console.error('Compilation failed!');
process.exit(1);
}Returns: Promise<string[]> - Array of emitted file paths
compileGlob(globPatterns, customOptions?)
Compile multiple glob patterns to different destinations. Automatically clears output directories before compilation, unpacks nested output, and rewrites cross-module import paths.
const result = await compiler.compileGlob({
'./ts/**/*.ts': './dist_ts',
'./ts_web/**/*.ts': './dist_web',
'./ts_node/**/*.ts': './dist_node'
});Returns: Promise<ICompileResult>
checkTypes(fileNames, customOptions?)
Type check files without emitting. Fast validation.
const success = await compiler.checkTypes(['./src/**/*.ts']);
if (!success) {
console.error('Type errors found!');
process.exit(1);
}Returns: Promise<boolean> - true if no errors
checkEmit(fileNames, customOptions?)
Validate files can be emitted without actually emitting.
const canEmit = await compiler.checkEmit(['./src/index.ts']);
if (!canEmit) {
console.error('Cannot emit these files!');
}Returns: Promise<boolean> - true if can emit
createOptions(customOptions?, folderName?)
Get merged compiler options (useful for debugging). Pass a folder name to include the compiler options that folder adds in its own tsconfig.json.
const options = compiler.createOptions({ strict: true });
console.log(options); // Shows merged options
const workerOptions = compiler.createOptions({}, 'ts_web_serviceworker');
console.log(workerOptions.lib); // ['webworker', 'esnext']Supporting Classes
TsConfig
TypeScript configuration management.
import { TsConfig } from '@git.zone/tsbuild';
const config = new TsConfig(process.cwd());
const options = config.merge({ target: 'ES2022' }, argvArg);
// Folder compiler options: what ts_web_serviceworker/tsconfig.json adds to the project's, or null without one
const folderOptions = config.loadFolder('ts_web_serviceworker');
// Every source folder with its own tsconfig.json; throws for the first invalid one
const configuredFolders = config.getConfiguredFolders(); // ['ts_web_serviceworker']
// The folder whose options govern a file, or null for files on the project options
const owner = config.getOwningFolder('./ts_web_serviceworker/index.ts'); // 'ts_web_serviceworker'
const workerOptions = config.merge({}, argvArg, 'ts_web_serviceworker');TsPublishConfig
Reads tspublish.json for module configuration. A missing file means no configuration; a file that cannot be read, is not a JSON object, or still carries the compilerOptions key of tsbuild 4.6.0 throws with its path.
import { TsPublishConfig } from '@git.zone/tsbuild';
const pubConfig = new TsPublishConfig('./ts_core');
console.log(pubConfig.shouldUnpack); // true/false
console.log(pubConfig.order); // number, Infinity if unsetTsUnpacker
Flattens nested TypeScript output directories.
import { TsUnpacker } from '@git.zone/tsbuild';
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).
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.
import { FsHelpers } from '@git.zone/tsbuild';
const files = await FsHelpers.listFilesWithGlob('./', 'ts/**/*.ts');
const exists = await FsHelpers.fileExists('./tsconfig.json');
const dirExists = await FsHelpers.directoryExists('./ts');TsBuildCli
CLI command handler. Used internally by the CLI.
import { TsBuildCli, runCli } from '@git.zone/tsbuild';
// Run the CLI
runCli();
// Or with custom working directory
const cli = new TsBuildCli('/path/to/project');
cli.run();Configuration
tsconfig.json Support
tsbuild fully supports all compiler options from tsconfig.json. Your project configuration is respected and intelligently merged.
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"verbatimModuleSyntax": true
}
}Configuration Priority (6 Levels)
Compiler option values use TypeScript's standard JSON syntax. For example,
"lib": ["ES2022"] selects the ES2022 library and replaces the default DOM/ESNext
selection; Node projects can pair it with "types": ["node"]. TSBuild validates
these values with TypeScript's tsconfig.json parser, reports invalid values with
their TypeScript diagnostic codes and hands them to TypeScript as written; relative
paths resolve from the project directory. Each compilation task retains its own
inferred source root and output layout.
A type check (check, emitcheck, checkTypes(), checkEmit()) emits
nothing and therefore runs without outDir and declarationDir: a test can
import the package by its own name through the exports map of its
package.json (@scope/package/module), and the import resolves to the
declarations of the last build, as with tsc -p, so build before you check.
The package's own sources import each other by relative path; an import of the
package by its own name from the compiled ts/ sources is not supported.
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 | Folder tsconfig.json | What the tsconfig.json of the source folder that owns the files adds (if present), see Folder tsconfig.json |
| 4 | Protected Defaults | Critical options for build integrity |
| 5 | Programmatic Options | Options passed to API functions |
| 6 | CLI Flags | Command-line arguments (highest priority) |
Protected Options
These options cannot be overridden by tsconfig.json, and a folder tsconfig.json cannot set them at all (but they can be overridden programmatically or via CLI):
| Option | Value | Reason |
|--------|-------|--------|
| outDir | 'dist_ts/' | Required for automatic path transformations |
| noEmitOnError | true | Prevents broken builds from being emitted |
| declaration | true | Ensures .d.ts files for library consumers |
| inlineSourceMap | true | Consistent debugging experience |
Default Compiler Options
When no tsconfig.json exists:
{
declaration: true, // Generate .d.ts files
inlineSourceMap: true, // Debug-friendly
noEmitOnError: true, // Fail-fast on errors
outDir: 'dist_ts/', // Output directory
module: 'nodenext', // Modern Node.js modules
target: 'esnext', // Latest JavaScript
moduleResolution: 'nodenext',
lib: ['dom', 'esnext'], // Browser globals and the latest ECMAScript library
noImplicitAny: false, // Flexible for quick development
verbatimModuleSyntax: true, // Explicit imports/exports
types: ['*'] // Every installed @types package, as TypeScript 5 included them
}TypeScript 7 always applies the CommonJS interop that esModuleInterop switched on, so tsbuild no longer sets it.
Folder tsconfig.json
A source folder - ts or ts_* - can compile in its own environment. The typical case is a service worker or web worker: its code needs the webworker library, which cannot be combined with the DOM library of the defaults because both declare the same globals differently (TS2403). Give the folder a tsconfig.json that extends the project's tsconfig.json, for example ts_web_serviceworker/tsconfig.json:
{
"extends": "../tsconfig.json",
"compilerOptions": {
"lib": ["webworker", "esnext"]
}
}The folder then compiles against the worker globals, and without the DOM:
declare const self: ServiceWorkerGlobalScope;
self.addEventListener('install', (eventArg: ExtendableEvent) => {
eventArg.waitUntil(self.skipWaiting());
});
self.addEventListener('fetch', (eventArg: FetchEvent) => {
eventArg.respondWith(fetch(eventArg.request));
});What tsbuild reads. For every file below the folder, tsbuild merges what the folder's tsconfig.json adds over the project's options, in tsbuild, custom, tsfolders, check, emitcheck and the TsCompiler API. Protected options still apply, programmatic options and CLI flags still win, and a list such as lib or types replaces the inherited one. The options use the tsconfig.json syntax, comments included, and TypeScript validates them; path-valued options such as typeRoots resolve from the file that declares them. tsbuild never changes the project's tsconfig.json.
What the editor sees. An editor applies the nearest tsconfig.json to a file, so it checks the folder's files with the same settings tsbuild uses: the project's plus the folder's. tsbuild keeps it that way:
extendsmust lead to the project'stsconfig.json, directly or through other tsconfig files of the project, such as a../tsconfig.worker.jsonthat extends./tsconfig.json. A folder cannot detach from the project's settings.- A folder
tsconfig.jsonholds onlyextendsandcompilerOptions. tsbuild compiles every file below the folder with it, which is exactly the set of files an editor applies it to when it leaves the file selection to TypeScript's default;files,includeandexcludewould make the two differ and are rejected. For the same reason the project'stsconfig.jsonmust not declarefilesorinclude, which the folder'stsconfig.jsonwould inherit.referencesis rejected as well: tsbuild reads other folders through their declarations itself. - The defaults and protected options of tsbuild are in no
tsconfig.json, for the folder as for the project. - At a folder boundary tsbuild is stricter than an editor: an editor that shows a file importing the folder checks the folder's sources under that file's settings, while tsbuild reads the folder's declarations (see below).
Allowed options. A folder tsconfig.json may add environment settings and switch on stricter checks, never weaken checking:
| Kind | Options |
|------|---------|
| Environment | lib, types, typeRoots, target, jsx, jsxFactory, jsxFragmentFactory, jsxImportSource |
| Stricter checks, true only | strict, noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, strictBuiltinIteratorReturn, noImplicitThis, useUnknownInCatchVariables, alwaysStrict, noUnusedLocals, noUnusedParameters, exactOptionalPropertyTypes, noImplicitReturns, noFallthroughCasesInSwitch, noUncheckedIndexedAccess, noImplicitOverride, noPropertyAccessFromIndexSignature, noUncheckedSideEffectImports, isolatedModules, isolatedDeclarations, erasableSyntaxOnly, verbatimModuleSyntax, forceConsistentCasingInFileNames |
| Stricter checks, false only | allowUnreachableCode, allowUnusedLabels |
Every other option - rootDir, outDir, paths, module, moduleResolution, declaration, noEmitOnError, inlineSourceMap and the rest - belongs in the project's tsconfig.json; options that weaken checking, such as skipLibCheck and noCheck, belong nowhere. tsbuild rejects it, and a strictness flag set to its weaker value, with an error that names the option and the file. Only ts and ts_* folders carry their own tsconfig.json; tsbuild does not read one in another folder such as test/, whose files compile with the project's options.
Imports across folders. A file that imports a folder with its own tsconfig.json reads it through the declarations that folder emits under its own options - the .d.ts files its dist_ folder publishes, generated in memory, so the build order does not matter. tsbuild emits these declarations folder by folder in dependency order, each folder over the declarations of the folders it imports, and hands them to the importing program in place of the folder's sources:
- Types keep their meaning across the boundary, including types the folder infers from its own environment. A type the importing side cannot express, such as
FetchEventin a DOM folder or a test, is an error in those declarations, reported with the source file and the folder'stsconfig.json, instead of silently turning intoany. Keep what both sides use expressible in both environments, or in a folder without its owntsconfig.jsonsuch asts_interfaces. - The imported files are type-checked under their own folder's options: by the folder's own task when the same
tsbuild,custom,tsfoldersorcompileGlob()run compiles that folder, otherwise by the compilation or check that imports them. Their errors fail it. - A compilation never emits the imported folder's files; its output imports the folder's
dist_folder. - Folders with their own
tsconfig.jsoncannot import each other in a cycle, just as TypeScript project references cannot. skipLibCheckskips declaration files, and with them the declarations read at a folder boundary, as it does for project references intsc --build.compileFiles()refuses root files of folders with different options, because one program has one set of options. Compile such folders as separate tasks, ascompileGlob()andtsfoldersdo.
Errors before cleaning. Before compileGlob() - and with it every build command - clears an output directory, it reads the project's tsconfig.json, every folder tsconfig.json, the owning folder of every task and the tspublish.json that decides how a task's output is unpacked. An invalid configuration fails the build and leaves the previous output in place; the CLI prints the error, or {"success": false, "error": "..."} under --json, and exits with code 1.
Path Transformation
tsbuild automatically transforms path mappings:
tsconfig.json:
{
"compilerOptions": {
"paths": {
"@models/*": ["./ts_models/*"]
}
}
}Automatic transformation:
./ts_models/* -> ./dist_ts_models/*Features
Clean Builds
Output directories are automatically cleared before compilation:
Clearing output directory: ./dist_ts
Compiling 14 files from ./ts/**/*.tsThis ensures no stale files remain from previous builds.
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 for monorepo workflows. This enables building and publishing multiple packages from a single repository.
Directory Structure
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)Each ts_* folder can contain its own tspublish.json to configure compilation and publishing behavior.
tspublish.json Configuration
Create a tspublish.json in each publishable ts_* folder. @git.zone/tspublish expects publishing fields, while tsbuild additionally reads order and unpack for build ordering and output flattening. A folder's compiler options live in its own tsconfig.json, not here; a compilerOptions key in tspublish.json fails the build with a message naming that file:
{
"name": "@myorg/core",
"order": 3,
"unpack": true,
"dependencies": ["@myorg/interfaces", "@myorg/shared"],
"registries": ["useBase"],
"bin": []
}| Option | Type | Default | Description |
|--------|------|---------|-------------|
| name | string | -- | Package name for publishing |
| order | number | Infinity | Build sequence (lower builds first) |
| unpack | boolean | true | Flatten nested output directories |
| dependencies | string[] | [] | Published package dependencies resolved from the monorepo package.json |
| registries | string[] | -- | Publish destination; gitzone release requires ["useBase"] and publishes to the registries in release.targets.npm of .smartconfig.json |
| bin | string[] | [] | CLI binary names generated by @git.zone/tspublish |
Build Order
The tsfolders command respects the order property:
pnpm exec tsbuild tsfoldersDefault ordering (without tspublish.json):
ts_interfaces- always first (shared types)ts_shared- always second (shared utilities)- Other folders sorted by
orderproperty - Folders without
orderare built last
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 dependencytsbuild automatically flattens this to:
dist_ts_core/
index.js <-- flat, clean structureThis is especially important for monorepos where packages import from each other.
Control via tspublish.json:
{
"unpack": true
}"unpack": true(default) - Flatten nested directories after compilation"unpack": false- Keep original nested structure
What gets unpacked:
- The source folder's contents (
ts_core/) are moved to the root ofdist_ts_core/ - Sibling folders (
ts_shared/) that were pulled in are removed (they have their own dist)
Decorator Support
tsbuild supports both TC39 standard decorators (recommended) and legacy experimental decorators.
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:
// TC39 standard decorator
function log(target: any, context: ClassMethodDecoratorContext) {
return function (...args: any[]) {
console.log(`Calling ${String(context.name)}`);
return target.apply(this, args);
};
}
class UserService {
@log
getUser(id: string) {
return { id, name: 'John' };
}
}Legacy Decorators (Backwards Compatibility)
For existing projects using frameworks that still require experimental decorators:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Structured Logging
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
name: Build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: pnpm install
- run: pnpm exec tsbuildJSON Output
pnpm exec tsbuild --json --quiet{
"success": true,
"totals": {
"errors": 0,
"filesWithErrors": 0,
"tasks": 1
},
"errorsByFile": {}
}When the build cannot start, for example because a folder tsconfig.json is invalid, the output is the error instead, and the exit code is 1:
{
"success": false,
"error": "/path/to/project/ts_web_serviceworker/tsconfig.json: compilerOptions.skipLibCheck is not allowed in a folder tsconfig.json. ..."
}Package.json Scripts
{
"scripts": {
"build": "tsbuild",
"build:prod": "tsbuild --disallowimplicitany",
"typecheck": "tsbuild check",
"pretest": "tsbuild emitcheck 'test/**/*.ts'"
}
}Troubleshooting
Common Issues
"Cannot find module" errors in compiled output
Make sure path mappings are configured in tsconfig.json. tsbuild automatically transforms them.
Decorator errors
Ensure your tsconfig.json has:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}Slow compilation
Use --skiplibcheck to skip declaration file checking:
pnpm exec tsbuild --skiplibcheckOnly 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 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.
Trademarks
This 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 or third parties, 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 or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
Company Information
Task Venture Capital GmbH
Registered at District Court Bremen HRB 35230 HB, Germany
For any legal inquiries or further information, please contact us via email at [email protected].
By 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.
