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;.transformSyncand.scanboth 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
sourceboth disappear in the output: types are erased entirely, and the JSX call compiles to thejsxDEV(...)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
.transformSyncruns 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 asBun.Transpiler, just wired into the larger bundling pipeline instead of called directly.