• Alpha
  • On npm: @ts-r.dev/build

ts-r-build: bundler and minifier

An esbuild-compatible bundler and minifier. Same flags as esbuild 0.28.2, ES2022 and later targets, no plugins or CSS bundling yet.

ts-r-build bundles and minifies TypeScript and JavaScript for production. Its command line follows esbuild 0.28.2, so most esbuild commands work after swapping the binary name. Like esbuild, it strips types without checking them. Run ts-r-check or tsc for that.

What works today

  • Bundling ES modules and CommonJS, with tree shaking that honours sideEffects and /*#__PURE__*/ annotations.
  • Output formats esm, cjs and iife, and code splitting with --format=esm --splitting.
  • Platforms browser, node and neutral.
  • tsconfig paths and baseUrl, from the nearest tsconfig.json or from --tsconfig.
  • Minification of whitespace, identifiers and syntax, separately or all at once with --minify.
  • Source maps: linked, external, inline or both.
  • JSX with the classic or the automatic runtime.
  • --watch, which rebuilds when an input changes.
  • A persistent build cache in ~/.cache/ts-r.

Not yet

  • No plugins and no JavaScript config file. Everything is set on the command line.
  • No CSS bundling. CSS imports are concatenated into a sibling .css file and @import is not followed.
  • No serve mode with --watch, and no legal-comment extraction.
  • No --mangle-props, --drop, --pure or --keep-names.
  • No syntax lowering: targets below ES2022 are rejected.
  • IIFE output isn't re-indented, and runtime helpers are repeated per chunk.
  • Not implemented: import defer and import source, glob require and import() of template strings, imports of data: JavaScript, with { type } loader overrides, Yarn PnP.
  • Minified output is about 3% larger than esbuild's.

The full list is in the design record, 0008 bundler.

Command line

Usage: ts-r-build [options] [entry points]

Simple options:
  --bundle                  Bundle all dependencies into the output files
  --define:K=V              Substitute K with V while parsing
  --external:M              Exclude module M from the bundle (can use * wildcards)
  --format=...              Output format (iife | cjs | esm)
  --loader:X=L              Use loader L for .X files (js, jsx, ts, tsx, json, text,
                            file, css, empty, base64, dataurl, binary)
  --minify                  Minify the output (sets all --minify-* flags)
  --outdir=...              The output directory (for multiple entry points)
  --outfile=...             The output file (for one entry point)
  --packages=external       Exclude every package import from the bundle
  --platform=...            Platform target (browser | node | neutral)
  --sourcemap               Emit a source map
  --splitting               Enable code splitting (requires --format=esm)
  --target=...              Environment target (es2022, esnext, node18, chrome110, ...)
  --watch                   Rebuild when a source file changes

Advanced options:
  --banner:js=...           Text at the start of every JS output
  --chunk-names=...         Path template for shared chunks (default "[name]-[hash]")
  --conditions=...          Custom package.json exports conditions
  --entry-names=...         Path template for entry points (default "[dir]/[name]")
  --footer:js=...           Text at the end of every JS output
  --global-name=...         Name of the global for --format=iife exports
  --ignore-annotations      Ignore /*#__PURE__*/ and sideEffects
  --jsx=...                 transform | automatic | preserve
  --jsx-dev                 Use React's automatic runtime in development mode
  --jsx-factory=...         What to use for JSX instead of React.createElement
  --jsx-fragment=...        What to use for JSX instead of React.Fragment
  --jsx-import-source=...   Override the package name for the automatic runtime
  --log-level=...           verbose | debug | info | warning | error | silent
  --main-fields=...         Override the main file order in package.json
  --metafile=...            Write metadata about the build to a JSON file
  --minify-whitespace       Remove whitespace
  --minify-identifiers      Shorten identifiers
  --minify-syntax           Use equivalent but shorter syntax
  --no-cache                Do not use the persistent build cache
  --out-extension:.js=.mjs  Use a custom output extension
  --public-path=...         Prefix for file-loader URLs
  --resolve-extensions=...  Comma-separated list of implicit extensions
  --sourcemap=...           linked | external | inline | both
  --sources-content=false   Omit sourcesContent in source maps
  --tree-shaking=...        Force tree shaking on or off (true | false)
  --tsconfig=...            Use this tsconfig.json instead of the nearest one
  --threads=N               Worker threads (default: TS_R_THREADS or all cores, max 8)
  --version                 Print the version

The flags mean what they mean in esbuild. Where ts-r differs:

  • Unknown flags are errors. A few esbuild flags that only change cosmetics, such as --charset, are accepted and ignored.
  • Without --outfile or --outdir, the bundle goes to stdout.
  • --target must allow at least ES2022. Older targets are an error, not a down-level.
  • --threads defaults to the TS_R_THREADS environment variable, else the number of cores, capped at 8.
  • --no-cache skips the persistent cache (the build namespace in ~/.cache/ts-r).

Exit codes: 1 on any error, 0 otherwise. You can also run it through the launcher as ts-r build.

Next to esbuild and Vite

ts-r-build works on its own. Use it where you call the esbuild command line today, or to build a library while the app keeps Vite. It has no JavaScript API, so scripts that call esbuild.build({...}) from Node need to switch to the command line first. It doesn't read vite.config.

ts-r-dev uses the same bundler to pre-bundle an app's dependencies.

Measured

Apple M-series Mac (a shared laptop), 4 threads, against esbuild 0.28.2, on public projects pinned in bench/repos.toml (cargo xtask bench --repos excalidraw,three-x10).

Inputts-r-buildesbuild
excalidraw app, minified ESM, no cache (median of 10)255 ms201 ms
Same, with the persistent cache132 ms201 ms
excalidraw: first build plus 20 edit-and-rebuild cycles, one process1,358 ms3,457 ms
three.js x10, minified IIFE with source maps, no cache (median of 10)378 ms225 ms
Same, with the persistent cache241 ms225 ms

Without its cache, ts-r-build is slower than esbuild on both. It also doesn't bundle template-literal import()s yet, so on excalidraw it leaves out the 61 locale chunks esbuild emits (7.2 MB of output against 9.3 MB), which flatters its times. The three.js x10 bundles agree: both load in Node with the same exports; ts-r's is 2.2% larger.

Correctness:

  • esbuild's end-to-end bundler tests, run against ts-r-build: 651 of 746 compared cases pass (87.3%).
  • Ecosystem bundle stage: 844 packages pass and 71 fail (92.2%). Minified output is 3.1% larger than esbuild's in total, with a median per-package ratio of 1.048.

Method and commits: build, ecosystem and correctness.