Open-Source Wikis

/

Bun

/

Systems

/

Built-in JS modules

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 of process).
  • 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.js

Special 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 to return.
  • Use JSC intrinsics where possible. $Array.from(...) is faster than Array.from(...) because it bypasses prototype lookups.
  • Avoid public-mutable globals. Array.prototype.map may be replaced by user code; use $ArrayPrototype.map.$call(arr, ...) for safety inside the runtime.
  • Use internal/validators for 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

  1. Drop a file at src/js/node/foo.ts (or bun/, thirdparty/).
  2. Run bun run build to regenerate the registry — internal-module-registry-scanner.ts picks up new files automatically.
  3. The numeric ID is assigned alphabetically. If JSC's CommonJS shim path matters to you, register the module in HardcodedModule.zig too.
  4. Add tests in test/js/node/ or test/js/bun/.

Integration points

  • Module loadersrc/bun.js/ModuleLoader.zig calls into the registry.
  • JSC builtinssrc/js/builtins/ files are compiled by JSC's builtin compiler at build time.
  • Code generationsrc/codegen/bundle-modules.ts, src/codegen/bundle-functions.ts, src/codegen/internal-module-registry-scanner.ts. See Code generation.
  • Testsbun test test/js/node/... runs against bun bd.

Entry points for modification

  • To add or fix a Node API, prefer touching src/js/node/<name>.ts first; only drop into Zig when the operation is performance-sensitive or needs OS access.
  • To add a new bun: module, create src/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>.ts and ensure the package.json resolution 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.

Built-in JS modules – Bun wiki | Factory