10LOC
Bunadvanced

On-the-fly TS/JSX transforms with Bun.Transpiler

Published June 11, 2027

const transpiler = new Bun.Transpiler({ loader: "tsx" });

const source = `
  interface Props { name: string }
  export const Greeting = (props: Props) => <h1>Hello, {props.name}!</h1>;
`;

console.log(transpiler.transformSync(source));

const { imports, exports } = transpiler.scan(source);
console.log({ imports, exports });

export {};

What

new Bun.Transpiler({ loader }) gives direct access to the same TypeScript/JSX-to-JavaScript compiler Bun uses internally for every file it runs. .transformSync(code) strips types and compiles JSX to a string of plain JavaScript; .scan(code) extracts its imports and exports without executing anything.

Why it matters

Transforming a string of TS/JSX at runtime — for a plugin system, a live playground, or a tool that processes user-supplied snippets — usually means pulling in the TypeScript compiler API or @babel/core, both of which are considerably heavier than the job of "strip types, compile JSX" requires. Bun.Transpiler is the exact same native transpiler Bun already loaded to run your own code, exposed as a class, so this need not add a dependency at all — it's already sitting inside the runtime.

How it works

  • new Bun.Transpiler({ loader: "tsx" }) sets the default syntax to expect; .transformSync and .scan both accept a per-call loader override as a second argument if a single instance needs to handle mixed input.
  • .transformSync(source) does not resolve or execute imports — it's a pure text-to-text transform, and the module doesn't need to exist on disk for this to work.
  • The interface declaration and JSX in source both disappear in the output: types are erased entirely, and the JSX call compiles to the jsxDEV(...) runtime calls Bun's own JSX transform produces.
  • .scan(source) returns { exports, imports } — a list of what a module imports and exports — useful for building a dependency graph without a full bundler.

Gotchas

  • .transformSync runs on the calling thread and blocks it; .transform (async) offloads to Bun's worker threadpool instead, but the docs specifically recommend the sync version unless transpiling many large files, since the threadpool hop often costs more than the transform itself.
  • This only transforms syntax — it does not typecheck. Code with real type errors transpiles and runs exactly as if the types were correct, since types are erased, not verified.
  • .scan() ignores type-only imports/exports by design (they don't exist at runtime), so it isn't the right tool for anything that needs to see the full, unerased source structure.

Related

  • Bun.build() — the full bundler, appropriate when the output needs to resolve imports and produce a deployable bundle rather than transform one string in isolation.
  • bun build --compile's underlying transpilation step uses the same compiler as Bun.Transpiler, just wired into the larger bundling pipeline instead of called directly.