Toolchain
Transpiler
Section titled “Transpiler”Bun’s own parser and printer, compiled to WebAssembly. Output matches bun build --no-bundle --target=browser.
-
.ts,.tsx,.jsx,.mts,.cts,.js,.mjs - JSX with Bun’s defaults (React automatic runtime)
-
/** @jsxImportSource preact */pragma comments - Legacy decorators with
experimentalDecoratorsandemitDecoratorMetadataintsconfig.json - Top-level
await -
tsconfig.jsonjsxandjsxImportSourceare not read by the transpiler (pathsandbaseUrlare, by the resolver); use the@jsxImportSourcepragma for another JSX runtime
enum Level { Low = 1, High = 10 }
interface Task { title: string; level: Level }
const tasks = [{ title: "docs", level: Level.High }] satisfies Task[];
class Queue<T> {
#items: T[] = [];
constructor(private readonly name: string) {}
push(item: T) { this.#items.push(item); return this; }
get size() { return this.#items.length; }
}
const queue = new Queue<Task>("main").push(tasks[0]);
await Bun.sleep(10); // top-level await
console.log(Level[tasks[0].level], queue.size);Type checking
Section titled “Type checking”The language service of tsrs (TypeScript 7’s compiler ported to Rust), compiled
to WebAssembly. The editor shows type errors in .ts, .tsx, .mts and .cts files a moment after you stop typing,
once the file parses. It loads with the first TypeScript file you open (~2.2 MB compressed, Bun’s type definitions
included) and keeps the project in memory: the first check takes a fraction of a second (about 0.6 s with the Astro
example’s node_modules), later ones a few milliseconds.
- Errors for the open file only, with the editor’s unsaved text; other files are checked as saved
- Uses the nearest
tsconfig.json, includingextends(relative, from packages, or a list); without one, the optionsbun initwrites plustypes: ["bun"] - Types for
Bun.*,bun:*,node:*and HTML imports from@types/bun, bundled; a project’s own@types/bunor@types/nodetakes their place - Package types from installed
node_modules(.d.tsfiles andpackage.jsontypes/exports) - Hover info: rest the pointer on a name for its type and documentation (JSDoc, with examples)
- Completions: members after
., names in scope, keywords, and import paths, each with its type and documentation. Auto-imports offer what the project’s other files, its installed packages and Bun’s and Node’s built-in modules (bun:sqlite,node:fs) export, and add the import - Parameter hints in calls: the signature, its overloads, and the current parameter’s documentation
- No go-to-definition yet
- No
tsccommand in the console
Formatter (bun fmt)
Section titled “Formatter (bun fmt)”Bun’s own formatter, compiled to WebAssembly. It formats like Prettier, and its output matches bun fmt byte for byte:
each file goes through bun fmt --stdin-filepath, Bun’s own code, with the project’s files. It loads the first time you
format (~2.3 MB compressed). Use Format in the tab bar or Shift+Alt+F for the open
file, or bun fmt in the console.
- JavaScript, TypeScript, JSX, JSON, CSS, SCSS, Less, HTML, Markdown, MDX, YAML, TOML, GraphQL, Handlebars and Svelte
- The project’s config:
bunfig.toml[fmt],.prettierrc(JSON, YAML or TOML),package.json"prettier",.editorconfig; the nearest one applies, as in Bun -
.prettierignore -
bun fmt [path…]formats a folder or files;--check(-c) lists what isn’t formatted and changes nothing -
bun fmt’s formatting flags (--no-semi,--single-quote,--print-width=<n>, …), which win over the config - A syntax error leaves the file as is and prints
bun fmt’s message with the line in question; Format also marks it in the editor - Skips
node_modules, lock files and minified files, likebun fmt - The editor’s Format is one undoable edit, and the cursor stays where the text didn’t change
- In a folder, files that
.prettierignorelists count as unchanged instead of being left out of the count - Configs written in JavaScript (
prettier.config.js) and Prettier plugins - No import sorting or Tailwind class sorting
- No
--stagedor--changed(no git)
Module resolution
Section titled “Module resolution”Bun’s own resolver, compiled to WebAssembly (~900 KB, loaded with the first module), over the project’s files.
- Relative and absolute imports, with Bun’s extension order (
.tsxand.tsbefore.js) andindex.* - Packages from
node_modules, walking up folders:exports(nested conditions, arrays,./*patterns), then the main fields (module,mainfor server code;browser,module,mainfor the page) - Conditions:
bun,node,importorrequire,development,defaultfor server code;browser,module,import,development,defaultfor code in the preview page -
#importsfrom the nearestpackage.json -
tsconfig.jsonpathsandbaseUrl - JSON imports
- CSS imported from code that goes through an HTML import (bundled), or through Vite
- CSS imported from server code (
import "./x.css", also.module.css) is{}, as in Bun’s runtime. In a page served without bundling, it is linked as a stylesheet and CSS module class names map to themselves. -
http:/https:imports are left to the browser rolldownis replaced by the playground’s own@rolldown/browser, since the native binding can’t load
An import that can’t be resolved shows cannot resolve "x" in the console.
tsconfig.json paths, index.* files and JSON imports, in one project:
{ "compilerOptions": { "paths": { "@lib/*": ["./src/lib/*"] } } }export const greet = (name: string) => `Hello, ${name}`;{ "name": "resolver" }import { greet } from "@lib/greet";
import config from "./src/config.json";
console.log(greet(config.name));CommonJS and require
Section titled “CommonJS and require”-
.cjsfiles, and.jsfiles without ESM syntax in packages that aren’t"type": "module" -
require("x")with a string literal. Modules run on firstrequire, as in Node; cycles see partial exports. - Named imports from CommonJS, found by a static scan (
exports.x =,module.exports = { … }, re-exports). Exports computed at runtime aren’t visible as named imports. -
import.meta.requirewith string literals -
createRequire(import.meta.url)serves built-ins only; itsrequire("./x.cjs")throws -
require(variable)throws. So does arequirethat can’t be resolved, when it runs. -
require.cache,require.main, native.nodeaddons
let count = 0;
exports.next = () => ++count;
module.exports.name = "counter";import { next, name } from "./counter.cjs";
const again = import.meta.require("./counter.cjs");
next();
console.log(name, again.next()); // one module instance: counter 2bun install
Section titled “bun install”Runs in the console (bun install, bun add, bun remove) and automatically when a project opens with missing dependencies.
- Packages from the npm registry, with Bun’s semver
-
dependencies,devDependencies,optionalDependenciesand required peer dependencies -
bun.lock(text format): exact versions, nestednode_modules, integrity checks. A stale lockfile logs a warning. - Optional packages limited to other platforms (native bindings) are skipped
- Without a lockfile, the install is flat: the first version seen wins
- No
bun.lockis written.bun addrefuses to run when abun.lockexists. - No lifecycle scripts (
postinstall, …) - No git, tarball,
file:,workspace:ornpm:dependencies - Only
registry.npmjs.org;.npmrcisn’t read - Binary
bun.lockb
Missing packages install before the first run:
{ "dependencies": { "picocolors": "^1.1.0", "zod": "^3.23.0" } }import pc from "picocolors";
import { z } from "zod";
const User = z.object({ name: z.string(), age: z.number().int() });
console.log(pc.green("valid:"), User.safeParse({ name: "Ada", age: 36 }).success);
console.log(pc.red("invalid:"), User.safeParse({ name: "Ada", age: "old" }).success);Bundler
Section titled “Bundler”Bun.build and HTML imports use Bun’s real bundler, compiled to WebAssembly (~7.5 MB, loaded on first use). See Bun.build for the supported options.
- Single-threaded; each build starts fresh
- HTML imports in
Bun.serveare built for the browser, without minification or source maps - If a build fails, errors go to the console and the page is served unbundled
The first run downloads the bundler:
export const square = (n: number) => n * n;
export const unused = () => "tree-shaken away";await Bun.write("src/entry.ts", 'import { square } from "./math";\nconsole.log(square(7));\n');
const result = await Bun.build({ entrypoints: ["./src/entry.ts"], outdir: "./dist", minify: true });
for (const output of result.outputs) console.log(output.path, output.size, "bytes");
console.log(await Bun.file("./dist/entry.js").text());