Skip to content

Transpile pipe

tsc-rs --pipe is a persistent, transpile-only process. It reads one JSON request per line on stdin and writes one JSON response per line on stdout. A bundler or runtime keeps it alive and sends it modules, instead of paying process start-up for every file.

It does not type-check. For that, see Type-check daemon.

Start#

tsc-rs --pipe

The process prints a banner once it is ready to accept requests:

{"ready":true,"pid":1263400}

Request#

{
  "id": "1",
  "file": "page.tsx",
  "source": "export const A = (p: { n: number }) => <b>{p.n}</b>;",
  "options": {
    "target": "es2022",
    "module": "esnext",
    "jsx": "react-jsx",
    "sourceMap": false
  }
}

Each request must be a single line. It is pretty-printed here for reading.

Field Meaning
id Echoed back on the response
file File name. The extension selects TypeScript or TSX parsing
source Source text
options Compile options, see below
pkg_json_type The type of the nearest package.json. "module" makes a .js file ESM

When options is absent, flat jsx and module fields on the request are accepted instead.

Options#

Option Meaning
target ECMAScript target, for example es2022
module Module system, for example commonjs or esnext
jsx JSX mode: react, react-jsx, react-jsxdev, preserve. See JSX
jsxImportSource Import source for the automatic JSX runtime
jsxFactory Factory function for the classic JSX runtime
sourceMap Include a source map in the response
importHelpers Import emit helpers from tslib instead of inlining them
removeComments Strip comments
useDefineForClassFields Emit class fields with define semantics
fastEmit Skip cosmetic formatting normalization. Same as --fast-emit

Response#

{
  "id": "1",
  "ok": true,
  "output": "import { jsx as _jsx } from \"react/jsx-runtime\";\nexport const A = (p) => _jsx(\"b\", { children: p.n });\n",
  "elapsed_ms": 0,
  "exports": ["A"]
}
Field Meaning
ok true when the module compiled
output Emitted JavaScript
elapsed_ms Time spent on this request
imports Static imports, each with a specifier and a kind
dynamic_imports Specifiers of import() calls
exports Exported names
sourceMap Source map, when requested
error Present when ok is false

Empty lists are omitted. The import and export lists let the caller build a module graph without parsing the output again. They describe the source: a runtime import that the compiler adds, such as react/jsx-runtime, is not listed.

Errors#

A source file that does not parse is an error, with the position and the TypeScript code:

{"id":"1","ok":false,"error":"a.ts:1:19: TS1109: Expression expected.","elapsed_ms":0}

Batches#

Send several modules in one request. They are compiled in parallel:

{"id":"batch1","batch":[{"file":"a.ts","source":"..."},{"file":"b.ts","source":"..."}]}
{"id":"batch1","ok":true,"results":[{"file":"a.ts","ok":true,"output":"..."},{"file":"b.ts","ok":true,"output":"..."}]}

Shutdown#

{"id":"9","cmd":"shutdown"}

The process answers {"id":"9","ok":true,"shutdown":true} and exits. Closing stdin also ends it.

Lifetime caps#

A long-lived process should be recyclable. The pipe exits cleanly, after answering the current request, when either cap is reached, so a supervisor can start a fresh one:

Variable Default Effect
TSC_RS_PIPE_MAX_REQUESTS 5000 Exit after this many requests. 0 disables
TSC_RS_PIPE_MAX_MEMORY_MB 2048 Exit when resident memory passes this value. 0 disables

Your supervisor must treat an exit as normal and respawn.

Example: Node#

import { spawn } from "node:child_process";
import { createInterface } from "node:readline";

const child = spawn("tsc-rs", ["--pipe"]);
const lines = createInterface({ input: child.stdout });
const pending = new Map();
let nextId = 0;

lines.on("line", (line) => {
  const msg = JSON.parse(line);
  if (msg.ready) return;
  const resolve = pending.get(msg.id);
  pending.delete(msg.id);
  if (resolve) resolve(msg);
});

export function transpile(file, source) {
  const id = String(++nextId);
  const options = { target: "es2022", module: "esnext", jsx: "react-jsx" };
  child.stdin.write(JSON.stringify({ id, file, source, options }) + "\n");
  return new Promise((resolve) => pending.set(id, resolve));
}

Who uses it#

The pipe is how the bext engine compiles PRISM routes: a pool of tsc-rs --pipe workers transforms each module on first use, and the results are cached.

Found a mistake on this page? Open an issue on GitHub.