@git.zone/tsrepo

The git.zone TypeScript repository toolchain in one package: build, type check, test, bundle, run, publish and watch TypeScript projects, generate their documentation and build Node.js single executable applications, with the tsrepo command.

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/tsrepo

tsrepo needs Node.js 22.14 or later.

Commands

Command Behaves exactly like
tsrepo build [args], tsrepo build custom|emitcheck|tsfolders … tsbuild [args]
tsrepo check [patterns…] [flags] tsbuild check [patterns…] [flags]
tsrepo test [path] [flags], tsrepo test clean [--dry-run], tsrepo test ledger check … tstest [path] [flags], tstest clean …, tstest ledger check …
tsrepo bundle [command] [flags] tsbundle [command] [flags]
tsrepo run <file> [args] tsrun <file> [args]
tsrepo publish plan, tsrepo publish prepare <new-output-directory> tspublish plan, tspublish prepare …
tsrepo docs [typedoc|website|aidoc|readme|description|commit|auth|test] [flags] tsdoc [command] [flags]
tsrepo watch [--port <n>], tsrepo watch init tswatch [--port <n>], tswatch init
tsrepo sea build|fetch [--target <t>…] [flags], tsrepo sea help tssea build|fetch …, tssea help
tsrepo --help, tsrepo --version help and version of tsrepo itself

Each command takes the arguments and flags of the tool it replaces and gives the same output and exit codes; tsrepo build --help prints tsbuild's help with every flag, tsrepo test --help tstest's, tsrepo bundle --help tsbundle's, tsrepo docs --help tsdoc's, tsrepo watch --help tswatch's and tsrepo sea help tssea's. The one difference is --version: every command reports the tsrepo version (tsrepo test --version prints tstest version <tsrepo version>, and the first line of tsrepo sea help reads @git.zone/tssea v<tsrepo version>).

tsrepo check is a TypeScript type check without emitting files, with tsbuild's semantics: without patterns it checks ts/**/*.ts and then test/**/*.ts. It is not a quality gate: rsrepo check (@git.zone/rsrepo) is the Rust quality gate (rustfmt, clippy, tests and docs), a different command with the same name.

Programmatic use

Import Content
@git.zone/tsrepo runCli(argv) (the tsrepo command line) and version. Importing it loads no module; every command loads its module on demand, so tsrepo test never loads typedoc or the AI code of the docs module.
@git.zone/tsrepo/build the former @git.zone/tsbuild API: TsCompiler, TsConfig, TsFolderConfig, TsPublishConfig, TsUnpacker, TsPathRewriter, TsBuildLogger, FsHelpers, TsBuildCli, runCli(), and runBuildCli(argv)
@git.zone/tsrepo/run the former @git.zone/tsrun API: runPath(), runCli(), spawnPath(), and runRunCli(argv)
@git.zone/tsrepo/publish the former @git.zone/tspublish API: TsPublish, its interfaces, runCli(args), and runPublishCli(argv)
@git.zone/tsrepo/test the former @git.zone/tstest API: checkLedger(), the configuration, artifact and affected-selection helpers and types, TestExecutionMode, runCli(), and runTestCli(argv)
@git.zone/tsrepo/tapbundle the former @git.zone/tstest/tapbundle: tap, expect, TapWrap, TapTools, webhelpers, setProtocolEmitter
@git.zone/tsrepo/tapbundle_serverside the former @git.zone/tstest/tapbundle_serverside: TapNodeTools and its types
@git.zone/tsrepo/tapbundle_protocol the former @git.zone/tstest/tapbundle_protocol: ProtocolEmitter, ProtocolParser, PROTOCOL_MARKERS, PROTOCOL_VERSION and the protocol types
@git.zone/tsrepo/bundle the former @git.zone/tsbundle API: TsBundle, AssetsHandler, HtmlHandler, CustomBundleHandler, runCustomBundles, Base64TsOutput, the presets, the notices API, the interfaces, runCli(), and runBundleCli(argv)
@git.zone/tsrepo/bundle/notices the former @git.zone/tsbundle/notices: the third-party notices API; it loads only Node.js builtins, @push.rocks/smartconfig and tsrepo's internal configuration resolver (Node.js builtins only). The same module is published on its own as @git.zone/notices (see below)
@git.zone/tsrepo/docs the former @git.zone/tsdoc API: AiDoc, its configuration types, NoChangesError, runCli(), and runDocsCli(argv)
@git.zone/tsrepo/docs/website the former @git.zone/tsdoc/website: DocsWebsiteBuilder, DocsWebsiteServer and the website interfaces
@git.zone/tsrepo/watch the former @git.zone/tswatch API: TsWatch, Watcher, ConfigHandler, TswatchInit, runInit(), the environment marker, registerCommands(), the interfaces, runCli(), and runWatchCli(argv)
@git.zone/tsrepo/sea the former @git.zone/tssea API: TsSea, runCli(), runCommandLine(), parseCommandLine(), usage, TsseaUsageError, TsseaError, the configuration types, tsseaTargets, tsseaConfigKey, nodeReleasePins, resolveCacheRoot, cacheDirEnvVar, and runSeaCli(argv)
@git.zone/tsrepo/sea/config the former @git.zone/tssea/config: tsseaConfigKey, resolveOutputs, ITsseaOutput(s), TTsseaTarget, TsseaError; it loads only node:fs, node:path and @push.rocks/smartsea/layout. It also exports the validator and target table the sea module shares with it (resolveConfig, tsseaTargets, targetInfos and their helpers); only the former @git.zone/tssea/config names are meant for other tools.
@git.zone/tsrepo/sea/cache the former @git.zone/tssea/cache: the layout of the Node.js download cache and classifyCacheEntry; it imports nothing

Each subpath keeps the API of the package it replaces, so the API documentation of tstest 8.0.0 (including tapbundle, tapbundle_serverside and tapbundle_protocol), tsbundle 3.7.0, tsbuild 6.0.2, tsrun 3.0.3, tspublish 2.1.0, tsdoc 3.0.2, tswatch 7.1.1 and tssea 1.2.1 (with the fixes of its main branch after 1.2.1: the bounded --build-sea run and its buildTimeoutMs key) applies with the import path from this table.

run<Module>Cli(argv) is a module's command line; argv has the layout of process.argv, [execPath, <entry>, ...args].

import { spawnPath } from '@git.zone/tsrepo/run';

const child = spawnPath('./script.ts', import.meta.url, { args: ['--flag'], timeout: 60_000 });
console.log(await child.exitCode);

spawnPath() and runPath(path, fromUrl, { cwd }) start <tsrepo>/cli.js run <script> … in a child process; the script sees [execPath, <absolute script path>, ...args], as under the tsrun binary.

The run module loads nothing but Node.js builtins and tsx, so tsrepo run starts as fast as tsrun did.

import { tap, expect } from '@git.zone/tsrepo/tapbundle';

tap.test('adds', async () => {
  expect(1 + 1).toEqual(2);
});

export default tap.start();

tsrepo test runs Node.js test files through tsrepo run and bundles Chromium test files with the bundle module.

tsrepo watch bundles with the bundle module and serves with @api.global/typedserver; tsrepo docs typedoc runs the typedoc that tsrepo itself depends on, resolved from tsrepo's install (typedoc/package.json and its bin), wherever the package manager put it. tsrepo docs delegates its AI work (readme, description, commit) to @git.zone/tsagent.

tsrepo sea takes a bundle that tsrepo bundle wrote with "sea": true and builds the executables; its notices are read and written with the bundle notices module, so ThirdPartyNoticesError from @git.zone/tsrepo/bundle/notices is the class @git.zone/tsrepo/sea reports. The Node.js download cache stays where tssea kept it (~/.cache/tssea on Linux, $XDG_CACHE_HOME/tssea, ~/Library/Caches/tssea, %LOCALAPPDATA%\tssea; TSSEA_CACHE_DIR overrides it), with the same layout, so tssea's downloads are reused.

Module map

Folder Subpath Carries
ts/ . the tsrepo router and root API
ts_shared/ (internal) .smartconfig.json resolution of the @git.zone/tsrepo key and the tsrepo version
ts_run/ ./run tsrun 3.0.3
ts_publish/ ./publish tspublish 2.1.0
ts_build/ ./build tsbuild 6.0.2
ts_bundle_notices/ ./bundle/notices; published as @git.zone/notices tsbundle 3.7.0 ts/mod_notices; documented in ts_bundle_notices/readme.md
ts_bundle/ ./bundle the rest of tsbundle 3.7.0
ts_tapbundle_protocol/, ts_tapbundle/, ts_tapbundle_serverside/ ./tapbundle_protocol, ./tapbundle, ./tapbundle_serverside tstest 8.0.0's tapbundle family; documented in ts_tapbundle/readme.md, ts_tapbundle_serverside/readme.md, ts_tapbundle_protocol/readme.md and the protocol's design notes ts_tapbundle_protocol/protocol.md
ts_test/ ./test the tstest 8.0.0 runner; documented in ts_test/readme.md
ts_sea_config/, ts_sea_cache/, ts_sea/ ./sea/config, ./sea/cache, ./sea tssea ts_config/, ts_cache/ and ts/
ts_watch/ ./watch tswatch 7.1.1
ts_docs_web/ (bundled) tsdoc 3.0.2's documentation website UI (ts_web/, html/), bundled into ts_docs/bundled_website.ts
ts_docs/ ./docs, ./docs/website tsdoc 3.0.2

A module imports another module only through that module's index.ts; modules load one another on demand, so a command pays only for what it uses.

@git.zone/notices

The notices module is also published as its own package, @git.zone/notices: the third-party notices format and resolver that tsrepo's bundle and sea modules and @git.zone/rsrepo share. It depends only on Node.js builtins and @push.rocks/smartconfig, not on @git.zone/tsrepo or @git.zone/rsrepo, so a tool that writes or reads notices installs neither toolchain. It carries ts_bundle_notices/ and tsrepo's configuration resolver ts_shared/, has the API of @git.zone/tsrepo/bundle/notices and the version of the tsrepo release it ships with; gitzone release publishes it before @git.zone/tsrepo. tsrepo itself uses the module in-tree.

WebAssembly

@git.zone/rsrepo compiles Rust crates to wasm32-unknown-unknown and writes <name>.wasm, a dependency-free ESM loader <name>.js, <name>.d.ts and the crates' third-party notices into a package's dist_wasm/. The loader finds its binary with the standard asset pattern and reads it with node:fs from a file: URL, or fetches it otherwise:

const wasmUrl = new URL('./<name>.wasm', import.meta.url);

const compileModule = async () => {
  if (wasmUrl.protocol === 'file:') {
    const { readFile } = process.getBuiltinModule('node:fs/promises');
    return WebAssembly.compile(await readFile(wasmUrl));
  }
  return WebAssembly.compileStreaming(fetch(wasmUrl));
};

export const instantiate = async () => {
  const instance = await WebAssembly.instantiate(await compileModule());
  return instance.exports;
};

The Node.js branch takes node:fs/promises from process.getBuiltinModule() (Node.js 22.3 and later) rather than an import, so no bundler resolves a Node.js built-in for a browser build.

  • tsrepo bundle (and every bundle tsrepo builds, Chromium test bundles included) emits the binary of each new URL('./<file>.wasm', import.meta.url), a string literal starting with ./ or ../, and rewrites the URL. A browser bundle carries the binary as an asset below chunks/<namespace>/ beside it, with esbuild, rolldown and rspack, split or not; the URL then names that asset relative to the file that contains it. A platform: "node" bundle (esbuild or rolldown) stays one file: the URL becomes a data:application/wasm;base64,… URL, which the loader fetches. A URL that names a missing binary fails the build. Browser module workers that import a loader carry the binary the same way, emitted by the worker build into the same chunks/<namespace>/ and located relative to the worker chunk (esbuild and rspack; rolldown bundles no workers).
  • Third-party notices: a bundle whose notices are enabled (by default its production builds) merges the notices of every binary it emits or inlines, <file>.wasm.third-party-notices.json beside the binary as rsrepo writes it when the project configures notices, into <bundle>.third-party-notices.json and .txt; the crates appear as cargo:<crate>@<version> entries next to the npm packages, each package and file once. A binary without that file fails such a build, naming the binary and the file it expects, as a bundled npm package without license text does; a malformed one fails it with the parser's message. Development builds write no notices and read none.
  • tsrepo test runs a test file that imports a loader in Node.js, unbundled, and in Chromium, bundled; the Chromium test server sends .wasm files as application/wasm, which WebAssembly.compileStreaming requires.
  • tsrepo build compiles only ts* folders into their dist_ts* folders and never touches dist_wasm/. A package ships it when its files cover it, as dist_*/**/* does; a tspublish.json sub-package ships a dist_wasm/ its modules import when it lists it in include ("include": ["dist_wasm"]).

The TypeScript compilers

  • tsrepo build and tsrepo check compile with TypeScript 7, the native compiler, pinned to 7.1.0-dev.20260928.1 as in tsbuild 6. It is installed under the npm alias typescript-native ("typescript-native": "npm:typescript@7.1.0-dev.20260928.1"), and the build module is its only user.
  • typescript itself is 6.0.3, the classic compiler API, which the publish and bundle modules and typedoc 0.28 (tsrepo docs typedoc) use.

Configuration

tsrepo reads .smartconfig.json. The key @git.zone/tsrepo takes one subsection per configurable module:

Subsection Same shape as the legacy key
build none; tsbuild read no configuration, and the build module reads none yet
test @git.zone/tstest
bundle @git.zone/tsbundle
docs @git.zone/tsdoc
watch @git.zone/tswatch
sea @git.zone/tssea

A module reads its subsection when it exists, and otherwise its legacy key. A module configured under both fails with an error that names both keys; tsrepo never merges them.

{
  "@git.zone/tsrepo": {
    "bundle": {
      "bundles": [{ "from": "./ts/cli.ts", "to": "./dist_node/main.mjs", "bundler": "esbuild", "platform": "node", "sea": true, "production": true }]
    },
    "sea": {
      "nodeVersion": "26.11.1",
      "bundle": "./dist_node/main.mjs",
      "outDir": "./dist/binaries",
      "targets": [{ "target": "linux-x64", "name": "myapp-linux-x64" }]
    },
    "watch": { "preset": "npm" }
  }
}
  • test and bundle: for the bundle module this includes the thirdPartyNotices settings and the notices supplements a bundled package ships in its own .smartconfig.json, and tsrepo bundle init, which adds bundles to the bundle subsection when it exists and otherwise to @git.zone/tsbundle, and refuses a project configured under both.
  • watch: tsrepo watch starts from the watch section; tsrepo watch init (and the wizard tsrepo watch runs without one) writes the watch subsection when it exists and otherwise @git.zone/tswatch.
  • sea: tsrepo sea build and fetch read the sea section; a project configured under both keys fails with CONFIG_INVALID. Its validation messages keep naming the block "@git.zone/tssea", as tssea's did. @git.zone/tsrepo/sea/config's resolveOutputs(block, projectDir) takes the block itself, as @git.zone/tssea/config did: a tool that reads a project's configuration picks the block by the same rule.
  • docs: the readme command reads legal, and tsrepo docs website (and DocsWebsiteBuilder) reads its website block, from the docs section; as tsdoc did, both also fall back to an old tsdoc key (for website, its keys fill in what the docs section leaves unset). The AI settings (model, provider, authMode, the ai block) are read by @git.zone/tsagent from the top-level keys @git.zone/tsagent, tsdoc and @git.zone/tsdoc only; it does not read "@git.zone/tsrepo".docs yet, so keep AI settings under @git.zone/tsagent or tsdoc. A project that sets "@git.zone/tsrepo".docs and also keeps a @git.zone/tsdoc block is configured under both keys and fails (the readme command and the website both refuse it), so move the AI settings out of @git.zone/tsdoc first.

The build module reads tsconfig.json, folder tsconfig.json files and tspublish.json exactly as tsbuild 6 does.

Migrating from the single tools

Commands:

Before Now
tsbuild … tsrepo build …
tsbuild check … tsrepo check …
tsrun file.ts … tsrepo run file.ts …
tspublish plan / prepare <dir> tsrepo publish plan / prepare <dir>
tstest [path] …, tstest clean tsrepo test [path] …, tsrepo test clean
tsbundle [command] … tsrepo bundle [command] …
tsdoc, tsdoc typedoc|website|aidoc|readme|description|commit|auth|test … tsrepo docs, tsrepo docs typedoc|website|aidoc|readme|description|commit|auth|test …
tswatch, tswatch init tsrepo watch, tsrepo watch init
tssea build|fetch|help … tsrepo sea build|fetch|help …
a cli.ts.js that calls tsrun.runPath('./cli.child.js', import.meta.url) a cli.ts.js that registers tsx (import { register } from 'tsx/esm/api') and imports the TypeScript entry

Imports:

Before Now
@git.zone/tsbuild @git.zone/tsrepo/build
@git.zone/tsrun @git.zone/tsrepo/run
@git.zone/tspublish @git.zone/tsrepo/publish
@git.zone/tstest @git.zone/tsrepo/test
@git.zone/tstest/tapbundle @git.zone/tsrepo/tapbundle
@git.zone/tstest/tapbundle_serverside @git.zone/tsrepo/tapbundle_serverside
@git.zone/tstest/tapbundle_protocol @git.zone/tsrepo/tapbundle_protocol
@git.zone/tsbundle @git.zone/tsrepo/bundle
@git.zone/tsbundle/notices @git.zone/tsrepo/bundle/notices
@git.zone/tsdoc @git.zone/tsrepo/docs
@git.zone/tsdoc/website @git.zone/tsrepo/docs/website
@git.zone/tswatch @git.zone/tsrepo/watch
@git.zone/tssea @git.zone/tsrepo/sea
@git.zone/tssea/config @git.zone/tsrepo/sea/config
@git.zone/tssea/cache @git.zone/tsrepo/sea/cache

Configuration keys (the legacy keys keep working; see "Configuration"):

Legacy key New subsection
@git.zone/tstest "@git.zone/tsrepo".test
@git.zone/tsbundle "@git.zone/tsrepo".bundle
@git.zone/tsdoc "@git.zone/tsrepo".docs (AI settings stay under @git.zone/tsagent or @git.zone/tsdoc; see "Configuration")
@git.zone/tswatch "@git.zone/tsrepo".watch
@git.zone/tssea "@git.zone/tsrepo".sea

Move a section by renaming it, never by copying it: a module configured under both keys fails. Tools older than tsrepo read only the legacy keys, so move the sections only once the project uses tsrepo.

Dependency build scripts: pnpm 12 refuses to install packages whose build scripts the project has not decided on, and two of tsrepo's dependencies have one. A project whose pnpm-workspace.yaml does not decide on them fails pnpm add -D @git.zone/tsrepo with ERR_PNPM_IGNORED_BUILDS (Ignored build scripts: esbuild, puppeteer). Decide them as tsrepo's own workspace does:

# pnpm-workspace.yaml
allowBuilds:
  esbuild: true
  puppeteer: false

puppeteer: false skips puppeteer's browser download; Chromium test files and TapNodeTools.createBrowser() then use a google-chrome, chromium or chromium-browser binary from the PATH. A project whose lockfile still resolves sharp to 0.34.x (reached through the docs module's AI dependencies; 0.34 has an install script, a fresh resolution picks one without) fails the same way on sharp until it adds sharp: false, which tsrepo's own workspace also declares: sharp's platform binaries are optional dependencies and need no source build.

  • Keep one tsrepo version in a project's install. Two versions (for example a project's tsrepo and an older one pulled in through another dependency) are two copies of every module, so tap from one is not the tap of the other and instanceof checks across them fail (ThirdPartyNoticesError, TsseaError). Depend on tsrepo with a caret range (^1.0.0) and run pnpm dedupe after upgrading; pnpm why @git.zone/tsrepo lists the versions installed.
  • Test files import tap and expect from @git.zone/tsrepo/tapbundle. A test file that still imports @git.zone/tstest/tapbundle resolves it from an installed @git.zone/tstest.
  • The tstest run ledger stays in <git-common-dir>/tstest/. Its run key now carries the tsrepo version, so the first tsrepo test after a switch from tstest does not reuse a run recorded by tstest.
  • Bundle output keeps the owner marker and notices generator @git.zone/tsbundle, so directories tsbundle wrote stay owned and are updated in place.
  • tsbundle's packaged default tsconfig is assets/tsconfig.json at the tsrepo package root.
  • The tssea download cache (~/.cache/tssea and the other locations above) and its layout are unchanged; @git.zone/tsrepo/sea/cache describes it as @git.zone/tssea/cache did.
  • tsrepo docs typedoc finds typedoc in tsrepo's own install; tsdoc looked only in <tsdoc>/node_modules/.bin.
  • tsrepo check is a type check (tsbuild semantics); rsrepo check is the Rust quality gate.

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 hello@task.vc.

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.

S
Description
The git.zone TypeScript repository toolchain: build, check, test, bundle, run, publish, watch, document and build single executable applications for TypeScript projects from one package.
Readme
1.6 MiB
Languages
TypeScript 99%
JavaScript 0.7%
HTML 0.1%
Shell 0.1%