oven-sh/bun
Built-in JS modules
src/js/ is the home of Bun's first-party TypeScript/JavaScript code that ends up baked into the binary. It contains:
- Node.js compatibility modules (
node:fs,node:path,node:events, ...). - Bun-specific modules (
bun:sqlite,bun:ffi,bun:test,bun:wrap). - Replacements for popular npm packages (
ws,node-fetch). - JSC builtins (
ReadableStream,WritableStream,Buffer, parts ofprocess). - Internal helpers shared across the above (
internal/validators.ts,internal/util.ts).
Directory layout
src/js/
├── node/ # node:fs, node:path, node:events, node:stream, node:net, ...
├── bun/ # bun:sqlite, bun:ffi, bun:test, bun:jsc, bun:test/expect, ...
├── thirdparty/ # bundled replacements for popular npm packages (ws, node-fetch, ...)
├── builtins/ # JSC builtin functions (ReadableStream initializer, etc.)
├── internal/ # shared helpers; never user-visible (validators, util, ...)
├── eval/ # bun -e and bun -p preludes
├── builtins.d.ts # types for the $-globals and JSC intrinsics
├── private.d.ts # internal type augmentations
├── internal-for-testing.ts
└── wasi-runner.jsSpecial syntax (the $ prefix)
Files in src/js/ are not plain ES modules. They run through src/codegen/builtin-parser.ts and src/codegen/bundle-modules.ts at build time. The preprocessor recognises:
| Token | Meaning |
|---|---|
$foo |
Replaced with @foo and resolved to a JSC intrinsic, private global, or builtin. |
.$call(...), .$apply(...) |
Tamper-proof versions of Function.prototype.call/apply. Never use .call/.apply. |
$isObject(x), $isCallable(x) |
Fast type predicates that avoid prototype lookups. |
$putByIdDirectPrivate, $getByIdDirectPrivate |
Private slot access on JSC objects. |
$ERR_INVALID_ARG_TYPE(...) |
Pre-defined Node-compatible error factories. |
$debug(...) |
Stripped in release builds. |
$assert(...) |
Stripped in release builds. |
process.platform, process.arch |
Dead-code-eliminated at build time so platform-specific branches don't ship. |
The $ is rewritten to __intrinsic__ during preprocessing and back to @ before being passed to the JSC builtin compiler. See src/js/CLAUDE.md for the full reference.
How a module is loaded
graph LR
Spec["import 'node:fs'"] --> Loader[ModuleLoader.zig]
Loader --> Hardcoded[HardcodedModule.zig]
Hardcoded --> ID[numeric module ID]
ID --> Registry[InternalModuleRegistry.cpp]
Registry --> Eval[evaluate stored bundle]
Eval --> Out[exported default object]Each module in src/js/{node,bun,thirdparty}/ is bundled into a closure at build time. The closure returns the module's default export. InternalModuleRegistry.cpp (generated from internal-module-registry-scanner.ts) maps a numeric ID → the closure factory, and ModuleLoader.zig resolves node:fs etc. to the right ID via HardcodedModule.zig.
In development, bun run build re-bundles src/js/ modules and writes them to disk; the running Bun reads them from disk so JS changes don't require a Zig rebuild.
Authoring rules
- Use
require()with a string literal.const fs = require("node:fs")is rewritten by the preprocessor; dynamic specifiers don't work. - Export via
export default { ... }. The bundler converts the default export toreturn. - Use JSC intrinsics where possible.
$Array.from(...)is faster thanArray.from(...)because it bypasses prototype lookups. - Avoid public-mutable globals.
Array.prototype.mapmay be replaced by user code; use$ArrayPrototype.map.$call(arr, ...)for safety inside the runtime. - Use
internal/validatorsfor argument validation. Matches Node's runtime errors verbatim so error-message snapshots match.
Examples
src/js/node/path.ts is pure TS, no native bindings. It imports internal/validators and exports posix, win32, and the joined namespace.
src/js/node/fs.ts is mixed: most operations call into Zig via $cpp/$lazy markers that resolve to src/bun.js/node/fs.zig host functions.
src/js/bun/sqlite.ts exports the Database class. The class is implemented in C++ (src/bun.js/bindings/sqlite/) but the high-level wrapper (statement caching, default options, error wrapping) is in TS.
src/js/builtins/ReadableStreamInternals.ts and friends are direct ports of WebKit's stream builtins. They run as JSC builtins, not as modules.
Adding a new built-in module
- Drop a file at
src/js/node/foo.ts(orbun/,thirdparty/). - Run
bun run buildto regenerate the registry —internal-module-registry-scanner.tspicks up new files automatically. - The numeric ID is assigned alphabetically. If JSC's CommonJS shim path matters to you, register the module in
HardcodedModule.zigtoo. - Add tests in
test/js/node/ortest/js/bun/.
Integration points
- Module loader —
src/bun.js/ModuleLoader.zigcalls into the registry. - JSC builtins —
src/js/builtins/files are compiled by JSC's builtin compiler at build time. - Code generation —
src/codegen/bundle-modules.ts,src/codegen/bundle-functions.ts,src/codegen/internal-module-registry-scanner.ts. See Code generation. - Tests —
bun test test/js/node/...runs againstbun bd.
Entry points for modification
- To add or fix a Node API, prefer touching
src/js/node/<name>.tsfirst; only drop into Zig when the operation is performance-sensitive or needs OS access. - To add a new
bun:module, createsrc/js/bun/<name>.ts. The naming is the user-facing import string. - To replace another npm package as a built-in, drop into
src/js/thirdparty/<name>.tsand ensure thepackage.jsonresolution for that name routes through the registry.
Key source files
| File | Purpose |
|---|---|
src/js/CLAUDE.md |
Authoritative reference for the special syntax. |
src/codegen/bundle-modules.ts |
Build-time bundler for these files. |
src/codegen/bundle-functions.ts |
Build-time bundler for JSC builtins. |
src/codegen/internal-module-registry-scanner.ts |
Discovers and IDs all modules. |
src/bun.js/HardcodedModule.zig |
Native-side dispatch table. |
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.