
Bun Dev
- 5 installs
- 5 repo stars
- Updated August 5, 2026
- bjornmelin/dev-skills
bun-dev is a skill giving rule-first Bun development, build, and runtime guidance with a shared audit and remediation engine.
About
bun-dev is a rule-first Bun guidance skill with a shared audit and remediation engine. A developer uses it when adopting Bun, migrating a repo from Node.js, fixing package management, optimizing scripts and monorepos, configuring Bun with TypeScript, or deploying Bun workloads on Vercel. It routes decisions through a prioritized rule set and exposes audit, plan-fixes, apply-safe-fixes, and validate commands.
- Rule-first Bun development, build, and runtime guidance plus a shared audit/remediation engine
- Priority table routes package-manager, runtime, and Vercel Bun runtime decisions by rule prefix
- Covers monorepo orchestration, TypeScript config, bun test/build, and Node-to-Bun migration
Bun Dev by the numbers
- 5 all-time installs (skills.sh)
- Ranked #1,085 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
bun-dev capabilities & compatibility
- Capabilities
- bun audit · package management · monorepo orchestration · runtime config · vercel deploy
- Works with
- vercel
- Use cases
- devops · refactoring · ci cd
What bun-dev says it does
Rule-first Bun guidance plus a shared audit/remediation engine.
If changing package manager/runtime, open P1 rules first:
For “what changed recently?” (Bun v1.3.10), refresh references and open:
npx skills add https://github.com/bjornmelin/dev-skills --skill bun-devAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5 |
|---|---|
| repo stars | ★ 5 |
| Last updated | August 5, 2026 |
| Repository | bjornmelin/dev-skills ↗ |
What it does
Make Bun package-manager, runtime, monorepo, and Vercel-deployment decisions using a prioritized rule set.
Who is it for?
Adopting Bun, migrating from Node.js, and configuring Bun package management, monorepos, and Vercel runtime.
Skip if: Non-Bun runtimes or generic Node.js-only tooling questions.
When should I use this skill?
Adopting Bun, migrating a repo from Node.js, auditing or fixing Bun package management, or deploying Bun workloads on Vercel.
What you get
Prioritized Bun rules and a shared CLI that audits, plans fixes, applies safe fixes, and validates changes.
- Bun rule guidance
- audit report
- safe fix plan
By the numbers
- 5 priority tiers in the rule table
- 8 shared platform commands
Files
Bun Dev
Rule-first Bun guidance plus a shared audit/remediation engine.
Use bun-dev for operating-model decisions, rule lookup, release sync, and platform routing. Use bun-audit when the task is specifically about scanning a repo, planning safe fixes, applying deterministic remediations, or validating changes against Bun policy.
Start with rules/_index.md for discovery, then route by the Priority Table below.
Navigation (How To Use This Skill)
1. If changing package manager/runtime, open P1 rules first: pm-*, runtime-*, vercel-*. 2. If working in a monorepo, open: scripts-bun-run-parallel-sequential, scripts-bun-filter-and-workspaces. 3. If deploying to Vercel, open: vercel-bun-runtime-enable, vercel-bun-runtime-limitations. 4. For repo-wide enforcement, use the shared CLI: bun-platform audit, plan-fixes, apply-safe-fixes, validate. 5. For “what changed recently?” (Bun v1.3.10), refresh references and open: references/ref-bun-release-notes-bun-v1.3.10.md.
Quick Start (Audit -> Fix -> Verify)
Audit (report-only):
bun ~/.agents/skills/bun-dev/scripts/bun-platform.ts audit --root . --format textPlan safe fixes:
bun ~/.agents/skills/bun-dev/scripts/bun-platform.ts plan-fixes --root .Apply safe fixes:
bun ~/.agents/skills/bun-dev/scripts/bun-platform.ts apply-safe-fixes --root .Validate after remediation:
bun ~/.agents/skills/bun-dev/scripts/bun-platform.ts validate --root . --fail-on warnPriority Table (Route To The Right Rules)
| Priority | Category | Impact | Prefix |
|---|---|---|---|
| 1 | Package manager + lockfiles | CRITICAL | pm- |
| 1 | Runtime selection + “one runtime where possible” | CRITICAL | runtime- |
| 1 | Vercel Bun runtime | CRITICAL | vercel- |
| 2 | Scripts + monorepo orchestration | HIGH | scripts- |
| 2 | TypeScript + tooling | HIGH | tsconfig- |
| 3 | Testing | MEDIUM | test- |
| 3 | Bundling + build | MEDIUM | build- |
| 4 | Performance | MEDIUM | perf- |
| 5 | Migration + troubleshooting | LOW/MED | migrate-, troubleshooting- |
Quick Reference (Start Here)
1) Package Manager + Lockfiles (CRITICAL)
pm-bun-add-remove-updatepm-no-mixed-lockfilespm-commit-bun-lockbpm-bun-install-ci-frozen-lockfilepm-package-manager-fieldpm-bunx-vs-npx
2) Runtime Selection (CRITICAL)
runtime-bun-vs-node-chooseruntime-bun-run-bun-flagruntime-ts-direct-executionruntime-watch-and-hot-reloadruntime-env-files
3) Vercel Bun Runtime (CRITICAL)
vercel-bun-install-detectionvercel-bun-runtime-enablevercel-bun-runtime-limitationsvercel-nextjs-bun-runtime-scriptsvercel-bun-function-fetch-handler
4) Scripts + Monorepos (HIGH)
scripts-bun-run-parallel-sequentialscripts-bun-filter-and-workspacesscripts-no-npm-in-bun-repos
5) TypeScript (HIGH)
tsconfig-bun-recommendedtsconfig-bun-typestsconfig-module-resolution-bundler
6) Testing + Build (MEDIUM)
test-bun-test-runnertest-bun-retrytest-mocking-and-spyingbuild-bun-build-bundlerbuild-compile-executablesbuild-bun-compile-browser
7) Performance (MEDIUM)
perf-prefer-bun-native-apisperf-avoid-node-fs-promises-hot-paths
8) Migration + Troubleshooting (LOW/MED)
migrate-node-to-bun-checklisttroubleshooting-esm-cjs-and-exportstroubleshooting-types-bun
References (Vendor Docs Snapshots)
Start with references/index.md.
Shared Platform Commands
audit: report Bun findings intext,md, orjsonlist-rules: print all rule idsexplain <rule-id>: print the matching rule fileplan-fixes: print deterministic safe fix candidatesapply-safe-fixes: apply safe file rewrites onlyvalidate: rerun the audit and fail on a severity thresholdbenchmark: emit audit/fix-planning timingsrelease-sync: refresh Bun/Vercel references and rebuild rule indexes
Platform state:
.bun-platform/cache.sqlite: shared scan cache.bun-platform/rollbacks/: safe-fix rollback artifactsreferences/release-sync-report.json: local generated release intelligence summary (ignored; not committed)
Config file:
bun-platform.config.jsondisabledRulesseverityOverridesadaptersincludePathsexcludeDirsbaselinemaxFilesmaxBytesvalidationCommandsmanageGitignore- Example template:
assets/templates/bun-platform.config.example.json
Reference Map
| Topic | Reference | Start with rules |
|---|---|---|
| Bun v1.3.10 release notes | references/ref-bun-release-notes-bun-v1.3.10.md | scripts-bun-run-parallel-sequential, build-bun-compile-browser, test-bun-retry, test-bun-test-runner |
| Vercel Bun runtime | references/ref-vercel-bun-runtime.md | vercel-bun-runtime-enable, vercel-bun-runtime-limitations |
Refresh reference snapshots:
bun ~/.agents/skills/bun-dev/scripts/bun-platform.ts release-syncAutomation
Shared CLI:
bun ~/.agents/skills/bun-dev/scripts/bun-platform.ts audit --root .Compatibility wrapper:
bun ~/.agents/skills/bun-dev/scripts/bun-audit.ts audit --root .Skill integrity check (rule ids, indexes, flat references):
bun ~/.agents/skills/bun-dev/scripts/check-skill-integrity.tsinterface:
display_name: "Bun Dev"
short_description: "Bun operating model + shared audit/remediation platform"
default_prompt: "Use bun-dev for Bun-first operating-model guidance, release sync, and shared repo auditing. Prefer the shared CLI in scripts/bun-platform.ts for audit, plan-fixes, apply-safe-fixes, validate, benchmark, and release-sync. Route focused scan/remediation work to bun-audit when appropriate, but keep bun-dev as the source of truth for rules and references."
{
"disabledRules": [],
"severityOverrides": {
"pm-package-manager-field": "warn"
},
"adapters": [
"auto"
],
"includePaths": [],
"excludeDirs": [
"vendor",
".bun-platform"
],
"baseline": "./bun-platform-baseline.json",
"maxFiles": 5000,
"maxBytes": 50000000,
"validationCommands": [],
"manageGitignore": true
}
<rule-id>
Why
Explain the failure mode or footgun this rule prevents.
Do
- Prefer explicit, deterministic behavior.
- Include exact commands and minimal examples.
Don't
- Avoid ambiguous setup and mixed tooling.
Examples
Bad:
# ...Good:
# ...References Index
References are snapshots of vendor docs (Markdown-only, main page content). Prefer opening rules/<rule-id>.md first.
Refresh snapshots:
bun ~/.agents/skills/bun-dev/scripts/update-bun-release-notes.ts
bun ~/.agents/skills/bun-dev/scripts/update-vercel-bun-docs.tsBun
- Bun v1.3.10 release notes:
ref-bun-release-notes-bun-v1.3.10.md- Bun CLI + workflow cheatsheet (skill-authored):
ref-bun-cli-cheatsheet.md- Bun built-in APIs cheatsheet (skill-authored):
ref-bun-builtins-cheatsheet.md
Vercel
- Bun runtime docs:
ref-vercel-bun-runtime.md
Fast Lookup
rg -n \"--parallel|--sequential\" ~/.agents/skills/bun-dev/references/ref-bun-release-notes-bun-v1.3.10.md
rg -n \"bunVersion|Bun\\.serve|Beta\" ~/.agents/skills/bun-dev/references/ref-vercel-bun-runtime.md
rg -n \"bun (install|add|update|test|build)\" ~/.agents/skills/bun-dev/references/ref-bun-cli-cheatsheet.mdBun Built-in APIs Cheatsheet
Prefer Bun-native APIs for performance in Bun-first runtimes. If deploying to Vercel Functions, remember Bun.serve() is not supported there.
Fast Filesystem I/O
const file = Bun.file("./data.json");
const data = await file.json();
await Bun.write("./out.txt", "hello");HTTP Server (Bun Runtime Only)
const server = Bun.serve({
port: 3000,
fetch() {
return new Response("ok");
},
});
console.log(server.port);WebSocket Server (Bun Runtime Only)
Bun.serve({
port: 3000,
fetch(req, server) {
if (server.upgrade(req)) return;
return new Response("upgrade failed", { status: 500 });
},
websocket: {
open(ws) {
ws.send("welcome");
},
message(ws, message) {
ws.send(String(message));
},
},
});SQLite (bun:sqlite)
import { Database } from "bun:sqlite";
const db = new Database("my.db");
db.run("create table if not exists t (id integer primary key, name text)");
db.prepare("insert into t (name) values (?)").run("Alice");
const row = db.query("select * from t where name = ?").get("Alice");Password Hashing
const hash = await Bun.password.hash("pw");
const ok = await Bun.password.verify("pw", hash);Bun CLI Cheatsheet (Bun-First Repos)
This is a quick reference for day-to-day Bun development. Prefer rules/ for “what to do” and “what not to do”.
Install / Upgrade Bun
# Upgrade Bun in-place
bun upgrade
# Check version
bun --versionNew Project
bun init
# Templates
bun create react my-app
bun create next my-app
bun create vite my-appPackage Management
# Install deps from package.json
bun install
# Deterministic install (CI)
bun install --frozen-lockfile
# Add / remove
bun add <pkg>
bun add -d <pkg>
bun remove <pkg>
# Update / outdated
bun update
bun update <pkg>
bun outdated
# Tool runner (npx equivalent)
bunx <bin> [...args]Run Code
# Run a package.json script
bun run dev
# Run an entrypoint directly (TS/JS)
bun run src/index.ts
# Watch / hot reload
bun --watch run src/server.ts
bun --hot run src/server.ts
# Explicit env file
bun --env-file=.env.production run src/server.tsMonorepos (Workspaces)
# Run in all workspace packages
bun run --workspaces test
# Filter packages
bun run --filter \"packages/*\" build
# Parallel / sequential
bun run --parallel --workspaces lint typecheck
bun run --sequential --workspaces build
# Keep going if one fails
bun run --parallel --no-exit-on-error --workspaces testTesting
bun test
bun test --watch
bun test --coverage
bun test --grep \"pattern\"Bundling / Build
bun build ./src/index.ts --outdir ./dist --target=bun --minify --sourcemap
# Compile to an executable (CLI/service)
bun build ./src/cli.ts --compile --outfile mycliVercel Bun Runtime (Functions)
Enable Bun runtime:
{
\"$schema\": \"https://openapi.vercel.sh/vercel.json\",
\"bunVersion\": \"1.x\"
}Write a Bun Function:
export default {
async fetch(req: Request) {
return Response.json({ ok: true });
},
};Next.js with Bun runtime (notably ISR):
{
\"scripts\": {
\"dev\": \"bun run --bun next dev\",
\"build\": \"bun run --bun next build\"
}
}To install Bun
```sh#curl $ curl -fsSL https://bun.sh/install | bash
$ npm install -g bun
$ powershell -c "irm bun.sh/install.ps1|iex"
$ scoop install bun
$ brew tap oven-sh/bun $ brew install bun
$ docker pull oven/bun $ docker run --rm --init --ulimit memlock=-1:-1 oven/bun
#### To upgrade Bun
$ bun upgrade
## New REPL
<!-- https://github.com/oven-sh/bun/commit/fa3a30f075ee208331c497fde1ea5cdae682e17f -->
Bun's REPL has been completely rewritten in Zig, replacing the previous third-party npm package. The new REPL starts instantly without downloading any packages, and includes a full-featured terminal UI.
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">In the next version of Bun<br><br>Bun gets a native REPL <a href="https://t.co/RLtaUymgWu">pic.twitter.com/RLtaUymgWu</a></p>— Jarred Sumner (@jarredsumner) <a href="https://twitter.com/jarredsumner/status/2026587131997831604?ref_src=twsrc%5Etfw">February 25, 2026</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
Features:
- **Copy to clipboard** - `.copy` command copies the expression to clipboard
- **Top-level await** - you can use it.
- **ESM import & require** - all the ways to load modules just work.
- **Syntax highlighting** — JavaScript code is colorized as you type.
- **Line editing with Emacs keybindings** — `Ctrl+A/E` to jump to start/end of line, `Ctrl+K/U` to kill to end/start, `Ctrl+W` to delete word backward, `Ctrl+L` to clear screen, and arrow key navigation.
- **Persistent history** — Command history is saved to `~/.bun_repl_history` and navigable with Up/Down arrows or `Ctrl+P/N`.
- **Tab completion** — Complete object properties and REPL commands.
- **Multi-line input** — Automatic continuation detection for incomplete expressions.
- **REPL commands** — `.help`, `.exit`, `.clear`, `.load`, `.save`, `.editor`.
- **Special variables** — `_` holds the last expression result, `_error` holds the last error.
- **Proper REPL semantics** — `const` and `let` declarations are hoisted to `var` for persistence across lines, top-level `await` works out of the box, `import` statements are converted to dynamic imports, and object literals like `{ a: 1 }` are detected without needing parentheses:
const x = 42
x + 1
43
await fetch("https://example.com").then(r => r.status)
200
import { readFile } from "fs/promises"
{ name: "bun", version: Bun.version }
{ name: "bun", version: "1.3.1" }
## `--compile --target=browser` for self-contained HTML output
<!-- https://github.com/oven-sh/bun/commit/b817abe55ef8f9dd0d34ee4c8c0b3dc2ddfc5303 -->
You can now use `bun build --compile --target=browser` to produce self-contained HTML files with all JavaScript, CSS, and assets inlined directly into the output. This supports TypeScript, JSX, React, CSS, ESM, CJS, and everything else Bun's bundler already supports.
This is useful for distributing `.html` files that work via `file://` URLs without needing a web server or worrying about CORS restrictions.
- `<script src="...">` tags become inline `<script>` with bundled code
- `<link rel="stylesheet">` tags become inline `<style>` tags
- Asset references (including CSS `url()`) become `data:` URIs
**CLI:**
bun build --compile --target=browser ./index.html
**API:**
await Bun.build({ entrypoints: ["./index.html"], target: "browser", compile: true, });
All entrypoints must be `.html` files. Cannot be used with `--splitting`.
## TC39 Standard ES Decorators
<!-- https://github.com/oven-sh/bun/commit/ce715b5a0f023732723ebe1f1e6502acf025646e -->
Bun's transpiler now fully supports [TC39 stage-3 standard ES decorators](https://github.com/tc39/proposal-decorators) — the non-legacy variant used when `experimentalDecorators` is **not** enabled in your `tsconfig.json`.
This has been one of the most requested features since 2023. Previously, Bun only supported legacy/experimental TypeScript decorators, which meant code using the modern decorator spec — including the `accessor` keyword, decorator metadata via `Symbol.metadata`, and the `ClassMethodDecoratorContext`/`ClassFieldDecoratorContext` APIs — would either fail to parse or produce incorrect results.
Now, all of the following work correctly:
function logged(originalMethod: any, context: ClassMethodDecoratorContext) { const name = String(context.name); return function (this: any, ...args: any[]) { console.log(Entering ${name}); const result = originalMethod.call(this, ...args); console.log(Exiting ${name}); return result; }; }
class Example { @logged greet(name: string) { console.log(Hello, ${name}!); } }
new Example().greet("world"); // Entering greet // Hello, world! // Exiting greet
Auto-accessors with the `accessor` keyword are now supported, including on private fields:
import { Signal } from "signal-polyfill";
function signal(target: any) { const { get } = target; return { get() { return get.call(this).get(); }, set(value: any) { get.call(this).set(value); }, init(value: any) { return new Signal.State(value); }, }; }
class Counter { @signal accessor #value = 0;
get value() { return this.#value; } increment() { this.#value++; } }
const c = new Counter(); c.increment(); console.log(c.value); // 1
Field decorators with `addInitializer`, decorator metadata, and correct evaluation ordering all work as specified:
function wrap<This, T>(value: T, ctx: ClassFieldDecoratorContext<This, T>) { ctx.addInitializer(function () { console.log("Initialized", this); }); return (initialValue: T) => initialValue; }
class A { @wrap public a: number = 1; }
const a = new A(); // "Initialized" A {}
### What's supported
| Feature | Details |
| ---------------------------------- | ----------------------------------------------------------------------- |
| Method/getter/setter decorators | Static and instance, public and private |
| Field decorators | Initializer replacement + `addInitializer` |
| Auto-accessor (`accessor` keyword) | Public and private fields |
| Class decorators | Statement and expression positions |
| Decorator metadata | `Symbol.metadata` support |
| Evaluation order | Decorator expressions and computed keys evaluated in spec-defined order |
Legacy decorators (`experimentalDecorators: true` in `tsconfig.json`) continue to work as before.
## Faster event loop on macOS & Linux
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">Ensuring everyone really understands. <a href="https://t.co/REGFQ2se1G">https://t.co/REGFQ2se1G</a> <a href="https://t.co/jvoXgSVyYe">pic.twitter.com/jvoXgSVyYe</a></p>— Ben Dicken (@BenjDicken) <a href="https://twitter.com/BenjDicken/status/2021254589945872666?ref_src=twsrc%5Etfw">February 10, 2026</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
## Windows ARM64 Support
<!-- https://github.com/oven-sh/bun/commit/30e609e08073cf7114bfb278506962a5b19d0677 -->
Bun now natively supports Windows on ARM64 (Snapdragon, etc.). You can install and run Bun on ARM64 Windows devices, and cross-compile standalone executables targeting `bun-windows-arm64`.
await Bun.build({ entrypoints: ["./path/to/my/app.ts"], compile: { target: "bun-windows-arm64", outfile: "./myapp", // .exe added automatically }, });
Or from the CLI:
$ bun build --compile --target=bun-windows-arm64 ./path/to/my/app.ts --outfile myapp
## Barrel Import Optimization
When you `import { Button } from 'antd'`, the bundler normally has to parse every file that `antd/index.js` re-exports — potentially thousands of modules. Bun's bundler now detects pure barrel files (re-export index files) and **only parses the submodules you actually use**.
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">In the next version of Bun<br><br>Bun's bundler & frontend dev server gets automatic barrel-file optimization. <br><br>This makes libraries like `lucida-react` build up to 2x faster <a href="https://t.co/LxS0Y4VjcI">pic.twitter.com/LxS0Y4VjcI</a></p>— Jarred Sumner (@jarredsumner) <a href="https://twitter.com/jarredsumner/status/2021778115312464248?ref_src=twsrc%5Etfw">February 12, 2026</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
This works in two modes:
- **Automatic mode:** Packages with `"sideEffects": false` in their `package.json` get barrel optimization automatically — no configuration needed.
- **Explicit mode:** Use the new `optimizeImports` option in `Bun.build()` for packages that don't have `"sideEffects": false`.
await Bun.build({ entrypoints: ["./app.ts"], optimizeImports: ["antd", "@mui/material", "lodash-es"], });
A file qualifies as a barrel if every named export is a re-export (`export { X } from './x'`). If a barrel file has any local exports, or if any importer uses `import *`, all submodules are loaded as usual.
`export *` re-exports are always loaded to avoid circular resolution issues — only named re-exports that aren't used by any importer are deferred. Multi-level barrel chains (A re-exports from B re-exports from C) are handled automatically via BFS un-deferral.
## Fewer closures in bundled output
To make ESM & CJS work as people expect, all bundlers must generate additional wrapping code around modules. This wrapper code has some overhead. And in v1.3.10, Bun's bundled output for ESM & CJS projects now has significantly less overhead.
Measured on a ~23 MB single-bundle app with 600+ React imports:
| Metric | Before | After | Delta |
| -------------------- | ------- | ------- | ------------------ |
| **Total objects** | 745,985 | 664,001 | **−81,984 (−11%)** |
| **Heap size** | 115 MB | 111 MB | **−4 MB** |
| GetterSetter | 34,625 | 13,428 | −21,197 (−61%) |
| Function | 221,302 | 197,024 | −24,278 (−11%) |
| JSLexicalEnvironment | 70,101 | 44,633 | −25,468 (−36%) |
These improvements apply automatically to all `Bun.build()` and `bun build` output — no code changes required.
## `--retry` flag for `bun test`
<!-- https://github.com/oven-sh/bun/commit/099b5e430c68302baba46627139b5dfd49b6a0bb -->
You can now set a default retry count for all tests using the `--retry` flag. This is useful for handling flaky tests in CI environments without adding `{ retry: N }` to every individual test.
Retry all failing tests up to 3 times
bun test --retry 3
Per-test `{ retry: N }` options still take precedence over the global flag:
import { test, expect } from "bun:test";
// Uses the global --retry count test("flaky network call", async () => { const res = await fetch("https://example.com/api"); expect(res.ok).toBe(true); });
// Overrides the global --retry count test("very flaky test", { retry: 5 }, () => { // ... });
You can also configure this in `bunfig.toml`:
[test] retry = 3
When using the JUnit XML reporter, each retry attempt is now emitted as a separate `<testcase>` entry. Failed attempts include a `<failure>` element, followed by the final passing `<testcase>`. This gives CI systems and flaky test detection tools per-attempt timing and result data using standard JUnit XML.
Thanks to @alii for the contribution!
## `ArrayBuffer` output for `Bun.generateHeapSnapshot("v8")`
<!-- https://github.com/oven-sh/bun/commit/77b640641537258727f17804d4b3eba944fefec3 -->
`Bun.generateHeapSnapshot("v8")` now accepts an optional second argument `"arraybuffer"` to return the heap snapshot as an `ArrayBuffer` instead of a string. This avoids the overhead of creating a JavaScript string for large snapshots and prevents potential crashes when heap snapshots approach the max `uint32` string length.
The `ArrayBuffer` contains UTF-8 encoded JSON that can be written directly to a file or decoded with `TextDecoder`:
const snapshot = Bun.generateHeapSnapshot("v8", "arraybuffer");
// Write directly to a file — no string conversion needed await Bun.write("heap.heapsnapshot", snapshot);
// Or decode and parse if needed const parsed = JSON.parse(new TextDecoder().decode(snapshot));
## TLS keepalive for custom SSL configs (mTLS)
<!-- https://github.com/oven-sh/bun/commit/e735bffaa9543b3a119b3c25b26b05bfae127a26 -->
Previously, all HTTP connections using custom TLS configurations — such as client certificates (mTLS) or custom CA certificates — had keepalive disabled, forcing a new TCP+TLS handshake on every request.
Custom TLS connections now properly participate in keepalive pooling. Identical TLS configurations are deduplicated via a global registry with reference counting, and the SSL context cache uses bounded LRU eviction (max 60 entries, 30-minute TTL).
This is automatically enabled when using `fetch()` or `bun install`.
## Updated Root Certificates
<!-- https://github.com/oven-sh/bun/commit/883e43c37100c3dd4351e42cd4d477ee5f195a8d -->
Bun's bundled root certificates have been updated from NSS 3.117 to NSS 3.119 (Firefox 147.0.3). This removes 4 distrusted CommScope root certificates per Mozilla's NSS 3.118 changes:
- CommScope Public Trust ECC Root-01
- CommScope Public Trust ECC Root-02
- CommScope Public Trust RSA Root-01
- CommScope Public Trust RSA Root-02
This update resolves TLS connection failures that some users experienced after Cloudflare rotated `example.com`'s certificate to a chain terminating at the removed `AAA Certificate Services` (Comodo) root CA.
## Upgraded JavaScriptCore Engine
<!-- https://github.com/oven-sh/bun/commit/4d9752a1f09b0a4ce3a1910374b80dcff6006e23 -->
Bun's underlying JavaScript engine (JavaScriptCore) has been upgraded, bringing several performance improvements and bug fixes.
### Deep Rope String Slicing — 168x faster
Repeated string concatenation using `+=` previously created deeply nested rope strings that caused O(n²) behavior when slicing. The engine now limits rope traversal depth and falls back to flattening the string, dramatically improving performance.
let s = ""; for (let i = 0; i < 100_000; i++) { s += "A"; } // Slicing this string is now up to 168x faster s.slice(0, 100);
### `String.prototype.endsWith` — up to 10.5x faster
`String.prototype.endsWith` is now optimized in the DFG/FTL JIT tiers with a dedicated intrinsic. Constant-foldable cases are up to 10.5x faster, and the general case is 1.45x faster.
const str = "hello world"; str.endsWith("world"); // up to 10.5x faster when constant-folded
### RegExp Flag Getters — 1.6x faster
RegExp flag property getters (`.global`, `.ignoreCase`, `.multiline`, `.dotAll`, `.sticky`, `.unicode`, `.unicodeSets`, `.hasIndices`) now have inline cache and DFG/FTL support, making them ~1.6x faster.
### `Intl.formatToParts` — up to 1.15x faster
`Intl` `formatToParts` methods now use pre-built structures for returned part objects, reducing allocation overhead.
### Other Engine Improvements
- `BigInt` values now store digits inline, eliminating a separate allocation and pointer indirection
- String iterator creation is now optimized in DFG/FTL, enabling allocation sinking
- Integer modulo operations in DFG/FTL now avoid expensive `fmod` double operations when inputs are integer-like
- The JIT worklist thread count has been increased from 3 to 4
- Register allocator improvements for better spill slot coalescing
### Bug Fixes
- Fixed: `RegExp.prototype.test()` returning incorrect results due to stale captures in FixedCount groups (@pchasco)
- Fixed: Infinite loop in RegExp JIT when using non-greedy backreferences to zero-width captures (@pchasco)
- Fixed: Incorrect RegExp backtracking from nested alternative end branches (@pchasco)
- Fixed: WebAssembly `ref.cast`/`ref.test` producing wrong results due to inverted condition in B3 optimization (@nickaein)
## `structuredClone` is up to 25x faster for arrays
<!-- https://github.com/oven-sh/bun/commit/0f43ea9becb629711c06d48089b7ee8e9eab325e -->
`structuredClone` and `postMessage` now have a fast path when the root value is a dense array of primitives or strings. Instead of going through the full serialization/deserialization machinery, Bun keeps data in native structures and uses `memcpy` where possible.
This optimization applies automatically when cloning arrays of numbers, strings, booleans, `null`, or `undefined` — the most common case for `postMessage` payloads and deep copies.
const numbers = Array.from({ length: 1000 }, (_, i) => i); structuredClone(numbers); // 25.3x faster
const strings = Array.from({ length: 100 }, (_, i) => item-${i}); structuredClone(strings); // 2.2x faster
const mixed = [1, "hello", true, null, undefined, 3.14]; structuredClone(mixed); // 2.3x faster
| Benchmark | Before | After | Speedup |
| --------------------------------- | --------- | --------- | --------- |
| `structuredClone([10 numbers])` | 308.71 ns | 40.38 ns | **7.6x** |
| `structuredClone([100 numbers])` | 1.62 µs | 86.87 ns | **18.7x** |
| `structuredClone([1000 numbers])` | 13.79 µs | 544.56 ns | **25.3x** |
| `structuredClone([10 strings])` | 642.38 ns | 307.38 ns | **2.1x** |
| `structuredClone([100 strings])` | 5.67 µs | 2.57 µs | **2.2x** |
| `structuredClone([10 mixed])` | 446.32 ns | 198.35 ns | **2.3x** |
Non-eligible inputs (objects, nested arrays) are unchanged with no regression.
Thanks to @sosukesuzuki for the contribution!
## `structuredClone` is faster for arrays of objects
<!-- https://github.com/oven-sh/bun/commit/fa78d2b408a42b54bf7d49ea6a5392412d7a1a3d -->
`structuredClone` and `postMessage` now use a fast path when cloning dense arrays of simple objects, completely bypassing byte-buffer serialization. This is the most common real-world pattern — arrays of flat objects with primitive or string values.
// This is now 1.7x faster than before const data = [ { name: "Alice", age: 30 }, { name: "Bob", age: 25 }, ];
const cloned = structuredClone(data);
When the array contains objects that share the same shape (same property names in the same order), a structure cache skips repeated property transitions during deserialization — making same-shape object arrays especially fast.
This builds on the existing fast paths for dense arrays of primitives and strings (up to 25x faster for integer arrays), extending the optimization to the object case.
| Benchmark | Node.js v24.12 | Bun v1.3.8 | Bun v1.3.1 |
| --------------- | -------------- | ---------- | -------------------------- |
| `[10 objects]` | 2.83 µs | 2.72 µs | **1.56 µs** (1.7x faster) |
| `[100 objects]` | 24.51 µs | 25.98 µs | **14.11 µs** (1.8x faster) |
The fast path falls back to normal serialization for objects with getters/setters, nested objects/arrays, non-enumerable properties, or elements like `Date`, `RegExp`, `Map`, `Set`, and `ArrayBuffer`.
Thanks to @sosukesuzuki for the contribution!
## Faster `structuredClone` for numeric arrays
<!-- https://github.com/oven-sh/bun/commit/3debd0a2d2b47d846b08efa3138e4b0f4c40e393 -->
Eliminated a redundant zero-fill in the `structuredClone` fast path for `Int32` and `Double` arrays. Previously, an internal buffer was zero-initialized and then immediately overwritten with the actual data. Now the buffer is constructed directly from the source data in a single copy.
Thanks to @sosukesuzuki for the contribution!
## `Buffer.slice()` / `Buffer.subarray()` is ~1.8x faster
<!-- https://github.com/oven-sh/bun/commit/9484218ba455ba591ebf75ad4fca98b012f3dd13 -->
`Buffer.slice()` and `Buffer.subarray()` have been moved from a JS builtin to a native C++ implementation, eliminating closure allocations and JS→C++ constructor overhead on every call. An int32 fast path skips `toNumber()` coercion when arguments are already integers — the common case for calls like `buf.slice(0, 10)`.
| Benchmark | Before | After | Speedup |
| -------------------------------- | -------- | ------------ | --------- |
| `Buffer(64).slice()` | 27.19 ns | **14.56 ns** | **1.87×** |
| `Buffer(1024).slice()` | 27.84 ns | **14.62 ns** | **1.90×** |
| `Buffer(1M).slice()` | 29.20 ns | **14.89 ns** | **1.96×** |
| `Buffer(64).slice(10)` | 30.26 ns | **16.01 ns** | **1.89×** |
| `Buffer(1024).slice(10, 100)` | 30.92 ns | **18.32 ns** | **1.69×** |
| `Buffer(1024).slice(-100, -10)` | 28.82 ns | **17.37 ns** | **1.66×** |
| `Buffer(1024).subarray(10, 100)` | 28.67 ns | **16.32 ns** | **1.76×** |
Thanks to @sosukesuzuki for the contribution!
## `path.parse()` is 2.2–7x faster
<!-- https://github.com/oven-sh/bun/commit/e29e830a2559083536974e65bfd720b516e81db5 -->
`path.parse()` now uses a pre-built object structure for its return value, avoiding repeated property transitions on every call. This brings **~2.2–2.8x** speedups for typical paths and up to **~7x** for edge cases like empty strings.
import { posix } from "path";
// 2.2x faster (119ns vs 267ns) posix.parse("/home/user/dir/file.txt"); // => { root: "/", dir: "/home/user/dir", base: "file.txt", ext: ".txt", name: "file" }
// 7x faster (21ns vs 152ns) posix.parse(""); // => { root: "", dir: "", base: "", ext: "", name: "" }
| Path | Before | After | Speedup |
| --------------------------- | --------- | --------- | --------- |
| `"/home/user/dir/file.txt"` | 266.71 ns | 119.62 ns | **2.23x** |
| `"/home/user/dir/"` | 239.10 ns | 91.46 ns | **2.61x** |
| `"file.txt"` | 232.55 ns | 89.20 ns | **2.61x** |
| `"/root"` | 246.75 ns | 92.68 ns | **2.66x** |
| `""` | 152.19 ns | 20.72 ns | **7.34x** |
Thanks to @sosukesuzuki for the contribution!
## Fixed: `Bun.spawn()` stdio pipes breaking Python asyncio-based MCP servers
<!-- https://github.com/oven-sh/bun/commit/b2d8504a09a6cf9f3282418570fa7205870bfc94 -->
Bun's subprocess stdio pipes used `shutdown()` calls on their underlying socketpairs to make them unidirectional. On `SOCK_STREAM` sockets, `shutdown(SHUT_WR)` sends a FIN to the peer — which caused programs that poll their stdio file descriptors for readability (like Python's `asyncio.connect_write_pipe()`) to interpret it as "connection closed" and tear down their transport prematurely.
This broke **all Python MCP servers** using the `model_context_protocol` SDK whenever they took more than a few seconds to initialize. The `shutdown()` calls have been removed entirely — the socketpairs are already used unidirectionally by convention, and the calls provided no functional benefit.
// Python MCP servers spawned via Bun.spawn() now work correctly const proc = Bun.spawn({ cmd: ["python3", "mcp_server.py"], stdin: "pipe", stdout: "pipe", stderr: "pipe", });
// Previously, the Python server's asyncio write transport would // be torn down after a few seconds of initialization delay. // Now it stays open as expected. const response = await new Response(proc.stdout).text();
## Bugfixes
### Node.js compatibility improvements
- Fixed: `AsyncLocalStorage` context not being preserved in `stream.finished` callbacks, causing `getStore()` to return `undefined` instead of the expected value
- Fixed: `Error.captureStackTrace(e, fn)` with a function not in the call stack now correctly returns the error name and message (e.g. `"Error: test"`) instead of `undefined`, matching Node.js behavior
- Fixed: `fs.watch` and `fs.watchFile` not properly handling `file:` URL strings with percent-encoded characters (e.g. `%20` for spaces)
- Fixed: `node:http` sending duplicate `Transfer-Encoding: chunked` headers when explicitly set via `res.writeHead()`, which caused nginx 1.25+ to return 502 errors (@psmamps)
- Fixed: `http.ClientRequest.write()` called multiple times was stripping the explicitly-set `Content-Length` header and switching to `Transfer-Encoding: chunked`, breaking binary file uploads (e.g. Vercel CLI). Bun now preserves `Content-Length` when explicitly set, matching Node.js behavior.
- Fixed: `OutgoingMessage.setHeaders()` incorrectly throwing `ERR_HTTP_HEADERS_SENT`
- Fixed: HTTP response splitting vulnerability in `node:http`. Thanks to @VenkatKwest for reporting this!
- Fixed: Crash when accessing `X509Certificate.issuerCertificate`
- Fixed: Rare crash in `napi_close_callback_scope`
- Fixed: ref count leak in `setImmediate` when the timer's JS object was garbage collected before the immediate task ran
- Fixed: dynamic `import()` of unknown `node:` modules (like `node:sqlite`) inside CJS files no longer fails at transpile time, allowing try/catch to handle the error gracefully at runtime. This fixes Next.js builds with turbopack + `cacheComponents: true` + Better Auth, where Kysely's dialect detection uses `import("node:sqlite")` inside a try/catch.
- Fixed: three GC safety issues that could cause crashes during garbage collection marking, most notably affecting projects using `module._compile` overrides (`ts-node`, `pirates`, `@swc-node/register`, etc.) where an unvisited write barrier could lead to use-after-free crashes
- Fixed: potential GC-related crashes when constructing objects with string values (e.g., HTTP headers, SQLite column names) by avoiding GC allocations inside `ObjectInitializationScope` (thanks @sosukesuzuki!)
- Fixed: crash on older Linux kernels (< 3.17, e.g. Synology NAS) where the `getrandom()` syscall doesn't exist, causing a panic with `"getrandom() failed to provide entropy"`. Bun now falls back to `/dev/urandom` via BoringSSL on these systems.
- Fixed: Socket `recvfrom` failing with `EINVAL` on gVisor-based environments (e.g. Google Cloud Run) due to invalid `MSG_NOSIGNAL` flag being passed to receive operations
- Fixed: a crash on Windows (`OutOfMemory` panic) in `node:fs` path handling when the system is under memory pressure by removing an unnecessary 64KB buffer allocation for paths with drive letter
- Fixed: memory leak when upgrading TCP sockets to TLS in node:tls (thanks to @alanstott!)
### Bun APIs
- Fixed: Fuzzer-detected crash when using `Bun.spawn` with `stdin: new Response(data)` concurrently with `Bun.file().exists()` calls and other spawned process stdout reads
- Fixed: Fuzzer-detected crash in `Bun.spawn`/`Bun.spawnSync` caused by integer overflow when the command array has a spoofed `.length` near `u32` max
- Fixed: Fuzzer-detected crash caused by a double-free in `Bun.plugin.clearAll()` that could corrupt the heap allocator during Worker termination or VM destruction
- Fixed: Fuzzer-detected crash when calling `Listener.getsockname()` without an object argument or with a non-object argument (e.g. `undefined`, `123`, `"foo"`) due to a null pointer dereference
- Fixed: `Bun.stripANSI()` hanging indefinitely in certain cases
- Fixed: crash when resolving `bun:main` before the entry point is generated, such as in HTML entry points or the test runner
- Fixed: `db.close(true)` throwing "database is locked" after using `db.transaction()` due to transaction controller prepared statements not being finalized on close
- Fixed: `bun:sql` PostgreSQL client now uses constant-time comparison for SCRAM-SHA-256 server signature verification, preventing potential timing side-channel attacks
- Fixed: HTTP header injection vulnerability in S3 client where CRLF characters in `contentDisposition`, `contentEncoding`, or `type` options could be used to inject arbitrary HTTP headers
- Fixed: Memory leak (~260KB per request) when cancelling streaming HTTP response bodies via `reader.cancel()` or `body.cancel()`. A strong GC root on the `ReadableStream` was never released on cancellation, causing `ReadableStream` objects, associated `Promise`s, and `Uint8Array` buffers to be retained indefinitely. (thanks @sosukesuzuki!)
- Fixed: Memory leak when cancelling S3 download streams mid-download — `ReadableStream` objects were retained indefinitely because the strong GC reference wasn't released on cancel
- Fixed: `Bun.build()` failing with `NotOpenForReading` when called multiple times after using `FileSystemRouter` routes as entrypoints. The `FileSystemRouter` was caching file descriptors that `Bun.build()` would later close, causing subsequent builds to fail with stale file descriptors. (@ecd4e680)
- Fixed: Crash when constructing objects from entries (e.g. `FileSystemRouter.routes`) caused by GC triggering during partially-initialized object slots
- Fixed: "Unknown HMR script" error that occurred during rapid consecutive file edits when using Bun's dev server with HMR (@prekucki)
- Fixed: Bun.sql now rejects null bytes in connection parameters to prevent protocol injection
### Web APIs
- Fixed: WebSocket connections over `wss://` through an HTTP proxy crashing or receiving spurious 1006 close codes instead of clean 1000 closes when the server sent a ping frame
- Fixed: WebSocket client frame desync when pong payloads were split across TCP segments, which could cause subsequent messages to be misinterpreted as invalid frame headers
- Fixed: Missing RFC 6455 validation for WebSocket pong control frames — payloads exceeding 125 bytes are now correctly rejected, matching the existing behavior for ping and close frames
### bun install
- Fixed: `bun install` producing incomplete `node_modules` on NFS, FUSE, and bind mount filesystems where directory entries were silently skipped due to unknown file types
- Fixed: Path traversal vulnerability in tarball directory extraction.
- Fixed: Scanner-detected undefined behavior in the .npmrc parser when processing truncated or invalid UTF-8 sequences in `.npmrc` files
- Improved: Bun now generates & verifies integrity hashes for GitHub & HTTPS tarball dependencies. Thanks to @dsherret and @orenyomtov for reporting this issue!
### JavaScript bundler
- Fixed: `bun build --compile` on Linux could produce a corrupted binary when a partial write occurred during executable generation
- Fixed: `bun build --compile` producing an all-zeros binary when the output directory is on a different filesystem than the temp directory, common in Docker containers, Gitea runners, and other environments using overlayfs
- Fixed: `bun build --compile --sourcemap=external` not writing `.map` files to disk — they were embedded in the executable but never actually written. With `--splitting`, each chunk now correctly gets its own `.map` file instead of all overwriting a single file. (@AidanGoldworthy)
- Fixed: `bun build` producing syntactically invalid JavaScript (`Promise.resolve().then(() => )`) for unused dynamic imports like `void import("./dep.ts")` or bare `import("./dep.ts")` expression statements
- Fixed: `Bun.build` with HTML entrypoints returning 404s for non-JS/CSS URL assets like `<link rel="manifest" href="./manifest.json" />` — these files are now correctly copied to the output directory instead of being parsed by their extension-based loader
- Fixed: CSS `<link>` tags missing from second (and subsequent) HTML entrypoints when multiple HTML entrypoints shared the same CSS file with `--production` mode bundling
### CSS Parser
- Fixed: CSS bundler leaving duplicate `@layer` declarations and `@import` statements in output when using `@layer` declarations (e.g. `@layer one;`) followed by `@import` rules with `layer()`
- Fixed: CSS bundler incorrectly removing `:root` rules when they appeared before `@property` at-rules due to style rule deduplication merging across at-rule boundaries (thanks @dylan-conway!)
### bun test
- Fixed: `bun test --bail` not writing JUnit reporter output file (`--reporter-outfile`) when early exit was triggered by a test failure
### Bun Shell
- Fixed: `seq inf`, `seq nan`, and `seq -inf` hanging indefinitely in Bun's shell instead of returning an error (thanks @dylan-conway!)
- Fixed: `[[ -d "" ]]` and `[[ -f "" ]]` crashing with an out-of-bounds panic in Bun's shell instead of returning exit code 1 (thanks @dylan-conway!)
- Fixed: Scanner-detected crash when shell builtins (`ls`, `touch`, `mkdir`, `cp`) run inside command substitution `$(...)` and encounter errors (e.g., permission denied) (thanks @dylan-conway!)
- Fixed: Bun's built-in `echo` in the shell treated `-e` and `-E` flags as literal text instead of parsing them, causing commands like `echo -e $password | sudo -S ...` to fail. Now supports `-e` (enable backslash escapes), `-E` (disable backslash escapes), and combined flags like `-ne`/`-en`, matching bash behavior. Supported escape sequences include `\\`, `\a`, `\b`, `\c`, `\e`, `\f`, `\n`, `\r`, `\t`, `\v`, `\0nnn` (octal), and `\xHH` (hex)
- Fixed: `Bun.$` shell template literals leaking internal `__bunstr_N` references in output when an interpolated value contained a space and a subsequent value contained multi-byte UTF-8 characters (e.g., `Í`, `€`)
- Fixed: Scanner-detected crash in the `seq` shell builtin when called with only flags and no numeric arguments (e.g. `await Bun.$\`seq -w\``)
- Fixed: crash in the shell interpreter when `setupIOBeforeRun` fails (e.g., stdout handle unavailable on Windows), which caused a segfault during GC sweep
### TypeScript types
- Fixed: TypeScript types for `Bun.build()` now correctly allow `splitting` to be used together with `compile` (@alii)
### Windows
- Fixed: Crash that could occur when spawning processes or writing to pipes in long-lived applications
- Fixed: Crash on Windows when a standalone executable with `compile.autoloadDotenv = false` spawned a `Worker` in a directory containing a `.env` file. The dotenv loader was mutating environment state owned by another thread, causing a `ThreadLock` assertion panic. (Thanks to @Hona!)
- Fixed: `"switch on corrupt value"` panic on Windows impacting Claude Code & Opencode users
- Fixed: Hypothetical crash on Windows when `GetFinalPathNameByHandleW` returned paths exceeding buffer capacity
### Thanks to 11 contributors!
- [@alanstott](https://github.com/alanstott)
- [@alii](https://github.com/alii)
- [@cirospaciari](https://github.com/cirospaciari)
- [@dylan-conway](https://github.com/dylan-conway)
- [@hk-shao](https://github.com/hk-shao)
- [@hona](https://github.com/hona)
- [@jarred-sumner](https://github.com/jarred-sumner)
- [@martinamps](https://github.com/martinamps)
- [@prekucki](https://github.com/prekucki)
- [@robobun](https://github.com/robobun)
- Getting Started Expand menu Projects and Deployments
- Use a Template
- Import Existing Project
- Add a Domain
- Buy a Domain
- Transfer an Existing Domain
- Collaborate
- Next Steps
- Fundamental Concepts Expand menu Request Lifecycle
- Build System
- What is Compute?
- Supported Frameworks Expand menu Full-stack Expand menu Next.js
- SvelteKit
- Nuxt
- Remix
- TanStack Start
- Frontends Expand menu Astro
- Vite
- React Router
- Create React App
- Backends Expand menu Nitro
- Express
- Elysia
- FastAPI
- Fastify
- Flask
- Hono
- Koa
- NestJS
- xmcp
- All Frameworks
- Incremental Migration
- Production Checklist
- Knowledge Base
- APIs & SDKs Expand menu Rest API
- Vercel SDK
- Marketplace Partner API
- Marketplace Vercel API
- Access Expand menu Account Management
- Sign in with Vercel Expand menu Getting Started
- Agent Quickstart
- Scopes & Permissions
- Tokens
- Authorization Server API
- Manage from Dashboard
- Consent Page
- Troubleshooting
- Activity Log
- Deployment Protection Expand menu Bypass Deployment Protection Expand menu Exceptions
- OPTIONS Allowlist
- Protection Bypass for Automation
- Sharable Links
- Protect Deployments Expand menu Password Protection
- Trusted IPs
- Vercel Authentication
- Directory Sync
- SAML SSO
- Two-factor (2FA)
- AI Expand menu Vercel Agent Expand menu Code Review beta Expand menu Managing Reviews
- Investigation beta
- Installation beta
- Pricing
- AI SDK
- AI Gateway Expand menu Getting Started Expand menu Text
- Image
- Video beta
- Agent Quickstart
- Models & Providers Expand menu Provider Options
- Model Fallbacks
- Provider Timeouts beta
- Automatic Caching
- Provider Filtering & Ordering
- Model Variants
- Capabilities Expand menu Observability
- Usage & Billing
- Image Generation Expand menu Using AI SDK
- Using Chat Completions API
- Video Generation beta Expand menu Text-to-Video beta
- Image-to-Video beta
- Reference-to-Video beta
- Motion Control beta
- Video Editing beta
- Web Search
- Reasoning Expand menu OpenAI
- Anthropic
- Google / Vertex
- Amazon Bedrock
- Zero Data Retention
- SDKs & APIs Expand menu AI SDK
- OpenAI Responses API
- OpenAI Chat Completions API Expand menu Chat Completions
- Tool Calls
- Structured Outputs
- Advanced
- Embeddings
- Image Generation
- REST API
- Anthropic Messages API Expand menu Messages
- Tool Calls
- Advanced
- File Attachments
- Structured Outputs
- OpenResponses API Expand menu Text Generation
- Streaming
- Image Input
- Tool Calling
- Provider Options
- Python
- Authentication & BYOK Expand menu Authentication
- BYOK
- Ecosystem Expand menu Framework Integrations Expand menu LangChain
- LangFuse
- LiteLLM
- LlamaIndex
- Mastra
- Pydantic AI
- App Attribution
- Pricing
- Chat Platforms Expand menu LibreChat
- OpenClaw (Clawdbot)
- Chatbox
- Open WebUI
- MCP Expand menu Deploy MCP servers
- Agent Resources Expand menu Markdown Access
- Vercel MCP server beta Expand menu Tools
- Coding Agents Expand menu Claude Code
- Conductor
- Roo Code
- Cline
- Blackbox AI
- Crush
- OpenCode
- Superset
- Integrations for Agents
- Integrations for Models Expand menu Adding a Provider
- Adding a Model
- xAI
- Groq
- fal
- Deep Infra
- ElevenLabs
- LMNT
- OpenAI
- Perplexity
- Pinecone
- Replicate
- Together AI
- CLI Workflows
- Skills
- llms.txt
- sitemap.md
- Build & Deploy Expand menu Builds Expand menu Build Features
- Build Image
- Build Queues
- Configuring a Build
- Managing Builds
- Deploy Hooks
- Deployment Checks
- Deployment Retention
- Deployments Expand menu Environments
- Generated URLs
- Managing Deployments
- Promoting Deployments
- Troubleshoot Build Errors
- Accessing Build Logs
- Claim Deployments
- Inspect OG Metadata
- Preview Deployment Suffix
- Promote Preview to Production
- Rollback Production
- Sharing a Preview Deployment
- Troubleshoot project collaboration
- Environment Variables Expand menu Framework Environment Variables
- Manage Across Environments
- Managing Environment Variables
- Reserved Environment Variables
- Rotating Environment Variables
- Sensitive Environment Variables
- Shared Environment Variables
- System Environment Variables
- Git Integrations Expand menu GitHub
- Azure DevOps
- Bitbucket
- GitLab
- Instant Rollback
- Microfrontends Expand menu Getting Started
- Local Development
- Path Routing
- Configuration
- Managing Microfrontends Expand menu Security
- Using Vercel Toolbar
- Testing & Troubleshooting
- Monorepos Expand menu Turborepo
- Remote Caching
- Nx
- Monorepos FAQ
- Package Managers
- Restricting Git Connections to a single Vercel team
- Rolling Releases Expand menu Rolling Release Deployment
- Skew Protection
- Webhooks Expand menu Webhooks API Reference
- CDN Expand menu Overview
- How Vercel CDN works Expand menu Compression
- Global Network & Regions
- Routing Expand menu Redirects Expand menu Configuration Redirects
- Bulk Redirects Expand menu Getting Started
- Rewrites
- Project Routing Rules
- Security Expand menu Encryption & TLS
- Security Headers
- Incremental Static Regeneration Expand menu Getting Started
- Usage & Pricing
- Request Collapsing
- Caching Expand menu CDN Cache Expand menu Purge CDN Cache
- Runtime Cache Expand menu Data Cache
- Cache-Control Headers
- System Headers Expand menu Request Headers
- Response Headers
- Image Optimization Expand menu Getting Started
- Limits and Pricing
- Managing Usage & Costs
- Legacy Pricing
- Custom Error Pages
- Pricing & Usage
- CLI Expand menu Deploying from CLI
- Project Linking
- Telemetry
- Global Options
- vercel alias
- vercel bisect
- vercel blob
- vercel build
- vercel buy
- vercel cache
- vercel certs
- vercel contract
- vercel curl beta
- vercel deploy
- vercel dev
- vercel dns
- vercel domains
- vercel env
- vercel flags
- vercel git
- vercel guidance
- vercel help
- vercel httpstat beta
- vercel init
- vercel inspect
- vercel install
- vercel integration
- vercel integration-resource
- vercel link
- vercel list
- vercel login
- vercel logout
- vercel logs
- vercel mcp
- vercel microfrontends
- vercel open
- vercel project
- vercel promote
- vercel pull
- vercel redeploy
- vercel redirects
- vercel remove
- vercel rollback
- vercel rolling-release
- vercel routes
- vercel switch
- vercel target
- vercel teams
- vercel telemetry
- vercel usage
- vercel webhooks beta
- vercel whoami
- Collaboration Expand menu Comments Expand menu Enabling Comments
- Using Comments
- Managing Comments
- Integrations
- Draft Mode
- Edit Mode
- Toolbar Expand menu Add to Environments Expand menu Add to Localhost
- Add to Production
- Managing Toolbar
- Browser Extensions
- Accessibility Audit Tool
- Interaction Timing Tool
- Layout Shift Tool
- Compute Expand menu Fluid Compute
- Functions Expand menu Getting Started
- Streaming
- Runtimes Expand menu Node.js Expand menu Advanced Node.js Usage
- Supported Node.js versions
- Bun
- Python
- Rust
- Go Runtime Go
- Ruby
- Wasm
- Edge Runtime
- Configuring Functions Expand menu Duration
- Memory
- Runtime
- Region
- Advanced Configuration
- API Reference Expand menu Node.js
- Python
- Logs
- Limits
- Concurrency Scaling
- Debug Slow Functions
- Pricing Expand menu Legacy Usage & Pricing
- Routing Middleware Expand menu Getting Started
- API
- Agent Quickstart
- Cron Jobs Expand menu Getting Started
- Managing Cron Jobs
- Usage & Pricing
- OG Image Generation Expand menu @vercel/og
- Examples
- Sandbox Expand menu Quickstart
- Concepts Expand menu Authentication
- Snapshots
- Firewall
- Examples
- SDK Reference
- CLI Reference
- System Specifications
- Pricing and Limits
- Run Commands in Vercel Sandbox
- Queues beta Expand menu Quickstart
- Concepts
- SDK Reference
- API Reference
- Observability
- Poll Mode
- Pricing and Limits
- Workflow beta
- Flags Expand menu Vercel Flags beta Expand menu Getting Started
- Dashboard Expand menu Feature Flag
- Entities
- Segments
- SDK Keys
- Drafts
- Archive
- SDKs Expand menu Flags SDK
- OpenFeature
- Core
- Limits and Pricing
- CLI Expand menu Clean Up After Rollout
- Roll Out a Feature
- Run an A/B Test
- Set Up Flags Explorer
- Flags Explorer Expand menu Getting Started
- Reference
- Pricing
- Marketplace
- Flags SDK
- Observability Expand menu Runtime Logs
- Web Analytics
- Integrations Expand menu Overview
- Install an Integration Expand menu Add a Native Integration
- Add a Connectable Account
- Agent Tools
- Permissions and Access
- Create an Integration Expand menu Native integration concepts
- Create a Native Integration
- Deployment integration actions
- Native Integration Flows
- Integration Approval Checklist
- Using Integrations API
- Billing and Refunds
- Integration Image Guidelines
- Requirements for listing an Integration
- Upgrade an Integration
- Building Integrations with Vercel REST API
- Secrets Rotation
- CMS Integrations Expand menu Agility CMS
- ButterCMS
- Contentful
- DatoCMS
- Formspree
- Makeswift
- Sanity
- Sitecore
- Commerce and Payments Expand menu Stripe
- External Platforms Expand menu Kubernetes
- Multi-tenant Expand menu Domain Management
- Limits
- Observability Expand menu Overview Expand menu Insights
- Debug 500 Errors
- Observability Plus
- Alerts beta
- Logs Expand menu Runtime
- Tracing Expand menu Instrumentation
- Session Tracing
- Query Expand menu Query Reference
- Monitoring Expand menu Getting Started
- Monitoring Reference
- Limits and Pricing
- Notebooks
- Speed Insights Expand menu Getting Started
- Using Speed Insights
- Metrics
- Privacy
- @vercel/speed-insights
- Limits and Pricing
- Managing Usage & Costs
- Troubleshooting
- Migrating from Legacy
- Drains Expand menu Using Drains
- Reference Expand menu Logs
- Traces
- Speed Insights
- Web Analytics
- Security
- Web Analytics Expand menu Getting Started
- Using Web Analytics
- Filtering
- Custom Events
- Redacting Sensitive Data
- Privacy
- @vercel/analytics
- Pricing
- Troubleshooting
- Manage & Optimize
- Debug Cache Issues
- Platform Expand menu Project Configuration Expand menu vercel.json
- vercel.ts
- General Settings
- Project Settings
- Git Configuration
- Git Settings
- Global Configuration
- Security settings
- Projects Expand menu Deploy from CLI
- Managing projects
- Transferring a project
- Domains Expand menu Working with Domains Expand menu Adding a Domain
- Adding a Domain to an Environment
- Assigning a Domain to a Git Branch
- Claiming Ownership
- Deploying & Redirecting Domains
- Removing a Domain
- Renewing a Domain
- Transferring Domains
- Viewing & Searching Domains
- Working with DNS
- Managing DNS Records
- Working with Nameservers
- Managing Nameservers
- Working with SSL
- Custom SSL Certificates
- Pre-Generate SSL Certificates
- Supported Domains
- Troubleshooting Domains
- Set Up Custom Domain
- Using Domains API
- Notifications
- Build Output API Expand menu Build Output Configuration
- Features
- Vercel Primitives
- Glossary
- Limits Expand menu Fair Use Guidelines
- Checks Expand menu Checks API
- Checks Reference
- Manage Redirects at Scale
- Support Center
- Pricing Expand menu Plans Expand menu Hobby Plan
- Pro Plan Expand menu Pro Plan Trial
- Billing FAQ
- Enterprise Plan Expand menu Billing FAQ
- MIUs for AI
- Pricing Expand menu Regional Pricing Expand menu Cape Town, South Africa (cpt1)
- Cleveland, USA (cle1)
- Dubai, UAE (dxb1)
- Dublin, Ireland (dub1)
- Frankfurt, Germany (fra1)
- Hong Kong (hkg1)
- London, UK (lhr1)
- Montréal, Canada (yul1)
- Mumbai, India (bom1)
- Osaka, Japan (kix1)
- Paris, France (cdg1)
- Portland, USA (pdx1)
- San Francisco, USA (sfo1)
- São Paulo, Brazil (gru1)
- Seoul, South Korea (icn1)
- Singapore (sin1)
- Stockholm, Sweden (arn1)
- Sydney, Australia (syd1)
- Tokyo, Japan (hnd1)
- Washington D.C., USA (iad1)
- Manage and Optimize Usage
- Calculating Usage of Resources
- Billing & Invoices
- Legacy Metrics
- Taxes
- Spend Management
- Security Expand menu Overview Expand menu Security & Compliance Measures
- Shared Responsibility Model
- PCI DSS iframe Integration
- Reverse Proxy Servers and Vercel
- Access Control
- Audit Logs
- Firewall Expand menu Firewall Concepts
- DDoS Mitigation
- Attack Challenge Mode
- Web Application Firewall Expand menu Custom Rules
- Rate Limiting
- Rule Configuration
- System Bypass Rules
- Rate Limiting SDK
- IP Blocking
- WAF Managed Rulesets
- Examples
- Usage & Pricing
- Firewall API
- Firewall Observability
- Bot Management
- BotID Expand menu Get Started with BotID
- Handling Verified Bots
- Advanced BotID Configuration
- Form Submissions
- Local Development Behavior
- Connectivity Expand menu Secure Compute
- Static IPs Expand menu Getting Started
- OIDC Expand menu AWS
- Azure
- Connect your API
- Google Cloud Platform
- OIDC Reference
- RBAC Expand menu Access Roles Expand menu Extended Permissions
- Project Level Roles
- Team Level Roles
- Access Groups
- Managing Team Members
- Two-factor Enforcement
- Storage Expand menu Overview
- Blob Expand menu Private Storage beta
- Public Storage
- Server Uploads
- Client Uploads
- Using the SDK
- Pricing
- Security
- Examples
- Manage Vercel Blob Storage
- Edge Config Expand menu Getting Started
- Using Edge Config
- Edge Configs & REST API
- Edge Configs & Dashboard
- Edge Config SDK
- Limits & Pricing
- Integrations Expand menu DevCycle
- Hypertune
- LaunchDarkly
- Split
- Statsig
- Marketplace
Menu
Using the Bun Runtime with Vercel Functions
Ask AI about this page
Last updated November 10, 2025
The Bun runtime is available in Beta on all plans
Bun is a fast, all-in-one JavaScript runtime that serves as an alternative to Node.js.
Bun provides Node.js API compatibility and is generally faster than Node.js for CPU-bound tasks. It includes a bundler, test runner, and package manager.
Configuring the runtime
For all frameworks, including Next.js, you can configure the runtime in your vercel.json file using the `bunVersion` property.
Once you configure the runtime version, Vercel manages the Bun minor and patch versions automatically, meaning you only need to set the major version. Currently, "1.x" is the only valid value.
vercel.json
{ "$schema" : "https://openapi.vercel.sh/vercel.json" , "bunVersion" : "1.x" }
Vercel manages the Bun minor and patch versions automatically. 1.x is the only valid value currently.
Framework-specific considerations
Next.js
When using Next.js, and ISR, you must change your build and dev commands in your package.json file to use the Bun runtime:
Before:
package.json
{ "scripts" : { "dev" : "next dev" , "build" : "next build" } }
After:
package.json
{ "scripts" : { "dev" : "bun run --bun next dev" , "build" : "bun run --bun next build" } }
Routing Middleware
The Bun runtime works with Routing Middleware the same way as the Node.js runtime once you set the bunVersion in your vercel.json file. Note that you'll also have to set the runtime config to nodejs in your middleware . ts file.
Feature support
The Bun runtime on Vercel supports most Node.js features. The main differences relate to automatic source maps, bytecode caching, and request metrics on the node:http and node:https modules. Request metrics using fetch work with both runtimes.
See the table below for a detailed comparison:
Bun Runtime vs Node.js Runtime feature support comparison table
Feature
Bun Runtime
Node.js Runtime
Node.js APIs
Automatic source maps
Request metrics (node:http/https)
Request metrics (fetch)
Supported APIs
Vercel Functions using the Bun runtime support most Node.js APIs, including standard Web APIs such as the Request and Response Objects.
Using TypeScript with Bun
Bun has built-in TypeScript support with zero configuration required. The runtime supports files ending with .ts inside of the /api directory as TypeScript files to compile and serve when deploying.
api/hello.ts
` export default { async fetch (request : Request ) { const url = new URL ( request .url); const name = url . searchParams .get ( 'name' ) || 'World' ;
return Response .json ({ message : Hello ${ name } ! }); } , }; `
Performance considerations
Bun is generally faster than Node.js, especially for CPU-bound tasks. Performance varies by workload, and in some cases Node.js may be faster depending on the specific operations your function performs.
When to use Bun
Bun is best suited for new workloads where you want a fast, all-in-one toolkit with built-in support for TypeScript, JSX, and modern JavaScript features. Consider using Bun when:
- You want faster execution for CPU-bound tasks
- You prefer zero-config TypeScript and JSX support
- You're starting a new project and want to use modern tooling
Consider using Node.js instead if:
- Node.js is already installed on your project and is working for you
- You need automatic source maps for debugging
- You need request metrics on the node:http or node:https modules
Both runtimes run on Fluid compute and support the same core Vercel Functions features.
Was this helpful?
supported.
Send
Copy as Markdown Give feedback Ask AI about this page
Rules Index
Open SKILL.md first to route by priority. Prefer opening specific rules over references.
Package Manager + Lockfiles (P1)
pm-bun-add-remove-updatepm-bun-install-ci-frozen-lockfilepm-bunx-vs-npxpm-commit-bun-lockbpm-no-mixed-lockfilespm-package-manager-field
Runtime Selection (P1)
runtime-bun-run-bun-flagruntime-bun-vs-node-chooseruntime-env-filesruntime-ts-direct-executionruntime-watch-and-hot-reload
Vercel Bun Runtime (P1)
vercel-bun-function-fetch-handlervercel-bun-install-detectionvercel-bun-runtime-enablevercel-bun-runtime-limitationsvercel-nextjs-bun-runtime-scripts
Scripts + Monorepos (P2)
scripts-bun-filter-and-workspacesscripts-bun-run-parallel-sequentialscripts-no-npm-in-bun-repos
TypeScript + Tooling (P2)
tsconfig-bun-recommendedtsconfig-bun-typestsconfig-module-resolution-bundler
Testing (P3)
test-bun-retrytest-bun-test-runnertest-mocking-and-spying
Build + Bundling (P3)
build-bun-build-bundlerbuild-bun-compile-browserbuild-compile-executables
Performance (P4)
perf-avoid-node-fs-promises-hot-pathsperf-prefer-bun-native-apis
Migration (P5)
migrate-node-to-bun-checklist
Troubleshooting (P5)
troubleshooting-esm-cjs-and-exportstroubleshooting-types-bun
build-bun-build-bundler
Why
bun build provides a fast bundler for JS/TS, can target browser/node/bun, and supports minification/sourcemaps/splitting. Bundling reduces runtime overhead and simplifies deployments.
Do
- Use
bun buildfor production bundles where appropriate. - Choose a
--targetthat matches the deployment runtime (bun,node,browser). - Use
--minifyand--sourcemapfor production builds as needed. - Use
bun build --compile --target=browseronly for HTML entrypoints that must be fully inlined.
Don't
- Don’t bundle server frameworks that expect filesystem access or dynamic requires unless you verify runtime behavior.
Examples
bun build ./src/index.ts --outdir ./dist --target=bun --minify --sourcemapBuild API:
const result = await Bun.build({
entrypoints: ["./src/index.ts"],
outdir: "./dist",
target: "bun",
minify: true,
sourcemap: "external",
});
if (!result.success) console.error(result.logs);build-bun-compile-browser
Why
Bun v1.3.10 can compile HTML entrypoints into self-contained .html output.
Use this when you need offline/disconnected delivery or a single-file artifact for sharing UI demos.
Do
- Use
bun build --compile --target=browseronly with.htmlentrypoints. - Keep asset-heavy pages in mind because everything is inlined.
- Prefer
bun build+ regular--target=browserfor multi-file apps that should keep runtime chunking.
Don't
- Don’t use
--splittingwith this mode; it is not supported. - Don’t rely on non-local asset loading assumptions, because browser output is inlined.
Examples
CLI self-contained HTML:
bun build --compile --target=browser ./index.html --outfile ./dist/index.htmlProgrammatic equivalent:
await Bun.build({
entrypoints: ["./index.html"],
target: "browser",
compile: true,
outdir: "./dist",
});build-compile-executables
Why
bun build --compile can produce a standalone executable, useful for CLIs and some deployment targets.
Do
- Use
--compilefor CLI tools and small services where a single binary is valuable. - Cross-compile when targeting different platforms.
Don't
- Don’t assume a compiled binary is always smaller/faster than a bundle; verify per target.
Examples
bun build ./src/cli.ts --compile --outfile mycliCross-compile:
bun build ./src/cli.ts --compile --target=bun-linux-x64 --outfile mycli-linuxmigrate-node-to-bun-checklist
Why
Node -> Bun migrations are usually straightforward, but toolchain drift (lockfiles, scripts, TS config) is the most common failure mode.
Do
- Install/upgrade Bun.
- Remove Node package manager lockfiles and install artifacts.
- Install dependencies with Bun and commit
bun.lockb. - Update scripts to use
bun run/bunx. - Add Bun types (
@types/bun) and confirm TS config. - Run your full verification suite.
Don't
- Don’t keep multiple lockfiles “for compatibility”.
- Don’t assume every Node package behaves identically under Bun without running tests.
Checklist
rm -rf node_modules
rm -f package-lock.json pnpm-lock.yaml yarn.lock
bun install
bun add -d @types/bun
bun testperf-avoid-node-fs-promises-hot-paths
Why
Node-compat filesystem APIs (fs/promises) can be slower than Bun’s native file APIs, especially in hot paths.
Do
- Prefer
Bun.file()for repeated reads. - Keep Node-compat code for portability boundaries only.
Don't
- Don’t repeatedly
readFilefromfs/promisesinside request handlers without profiling.
Examples
Bad:
import { readFile } from "node:fs/promises";
export async function handler() {
return await readFile("./data.txt", "utf8");
}Good:
export async function handler() {
return await Bun.file("./data.txt").text();
}perf-prefer-bun-native-apis
Why
Bun provides native APIs that are often faster than Node-compat alternatives (especially filesystem I/O and HTTP). Using them intentionally can yield large wins.
Do
- Prefer
Bun.file()+file.text()/json()for reads. - Prefer
Bun.write()for writes. - Prefer Bun-native server patterns (when your deployment target supports them).
Don't
- Don’t use
Bun.serve()on platforms that don’t support it (e.g., Vercel Functions).
Examples
const file = Bun.file("./data.json");
const data = await file.json();await Bun.write("./out.txt", "hello");pm-bun-add-remove-update
Why
Most “Bun migrations” fail because day-to-day dependency operations still use npm/pnpm/yarn. Standardize the common lifecycle commands so installs and lockfiles stay consistent.
Do
- Install from
package.json: bun install- Add dependencies:
bun add <pkg>bun add -d <pkg>(dev dependency)bun add --optional <pkg>- Remove dependencies:
bun remove <pkg>- Update dependencies:
bun update(all)bun update <pkg>(one)bun outdated
Don't
- Don’t run
npm install/pnpm add/yarn addin Bun-first repos.
Examples
bun add react@latest
bun add -d typescript @types/bun
bun remove left-pad
bun update --latest
bun outdatedpm-bun-install-ci-frozen-lockfile
Why
CI should be deterministic: dependency versions must come from the lockfile, not from whatever the registry serves at build time.
Do
- Use
bun install --frozen-lockfilein CI. - Fail fast if the lockfile is out of sync.
Don't
- Don’t use a non-frozen install in CI unless you explicitly want dependency drift.
Examples
bun install --frozen-lockfilepm-bunx-vs-npx
Why
bunx is Bun’s tool runner (analogous to npx). Using bunx keeps repos Bun-first and avoids pulling in a Node/npm toolchain for ad-hoc CLIs.
Do
- Prefer
bunx <pkg>overnpx <pkg>. - Pin versions for critical tooling runs:
bunx <pkg>@<version> ....
Don't
- Don’t use
npxin Bun-first repos unless you intentionally require Node’s npx behavior.
Examples
bunx biome check .
bunx tsc -p tsconfig.json
bunx typescript@5.7.3 tsc --versionpm-commit-bun-lockb
Why
bun.lockb is the source of truth for deterministic installs. If it’s missing (or ignored), dependency resolution can change silently between machines and CI runs.
Do
- Commit
bun.lockb. - Use
bun install --frozen-lockfilein CI to guarantee installs match the lockfile. - Ensure
.gitignoredoes not ignorebun.lockb.
Don't
- Don’t ignore
bun.lockb. - Don’t regenerate lockfiles with other package managers.
Examples
CI install:
bun install --frozen-lockfilepm-no-mixed-lockfiles
Why
Multiple lockfiles (bun.lockb, package-lock.json, pnpm-lock.yaml, yarn.lock) create nondeterministic installs and CI/CD drift. Many platforms (including Vercel) select an install strategy by lockfile presence; multiple lockfiles can trigger the wrong toolchain.
Do
- Keep exactly one lockfile committed for the repo’s chosen package manager.
- For Bun-first repos, commit
bun.lockband delete other lockfiles. - Standardize all scripts and docs on
bun install/bun run/bunx.
Don't
- Don’t keep
bun.lockbalongsidepackage-lock.json/pnpm-lock.yaml/yarn.lock. - Don’t run
npm install/pnpm install/yarn installin a Bun-first repo.
Examples
Bad (mixed lockfiles):
bun.lockb
package-lock.jsonGood (Bun-only):
bun.lockbpm-package-manager-field
Why
The packageManager field in package.json makes the intended package manager explicit for humans, CI, and tooling.
Do
- Set
"packageManager": "bun@<version>"for Bun-first repos. - Keep it aligned with the Bun version used in CI/build images.
Don't
- Don’t leave
packageManagerunset in repos where consistency matters. - Don’t set it to a different package manager while expecting Bun workflows.
Examples
{
"packageManager": "bun@1.3.10"
}runtime-bun-run-bun-flag
Why
Some package binaries have a #!/usr/bin/env node shebang. Bun may run these under Node by default (to match expectations). The --bun flag forces Bun to execute the binary with the Bun runtime instead.
This is especially relevant when you want “Bun all the way down” (for consistency or platform requirements).
Do
- Use
bun run --bun <bin> ...when you intentionally want Bun to execute a Node-shebang binary. - Consider
bunfig.toml(run.bun = true) orBUN_RUN_BUN=1when a repo consistently needs this behavior.
Don't
- Don’t blanket-apply
--bunwithout verifying the binary is compatible with Bun’s runtime behavior.
Examples
Force Bun to execute a Node-shebang CLI:
bun run --bun next dev
bun run --bun next buildruntime-bun-vs-node-choose
Why
“Using Bun” can mean:
- Bun as a package manager (install/run tools), while runtime remains Node (common for many ecosystems).
- Bun as the runtime (server, scripts, tests, bundling).
Conflating the two leads to subtle breakages and duplicated runtime requirements.
Do
- Decide explicitly:
- Bun-first runtime: run servers/tests/scripts with Bun; avoid Node-only assumptions.
- Node runtime + Bun package manager: keep Node where required, but standardize on Bun for installs and scripts.
- Document the choice in
package.json(packageManager) and CI config.
Don't
- Don’t keep multiple runtimes “just in case” without a reason.
- Don’t mix package managers even if you keep Node as a runtime.
Examples
Bun package manager, Node runtime (typical):
bun install
bunx eslint .
node dist/server.jsBun runtime:
bun run src/server.ts
bun testruntime-env-files
Why
Environment configuration is a frequent source of “works locally, fails in CI”. Bun supports .env loading and an explicit --env-file flag.
Do
- Use
.envfor local development defaults. - Use
bun --env-file=<path> run ...for explicit environment selection. - Prefer
Bun.envfor Bun-first code (andprocess.envfor Node compatibility).
Don't
- Don’t rely on ambient shell state when you need a repeatable run.
Examples
bun --env-file=.env.production run src/server.tsconst port = Bun.env.PORT ?? "3000";runtime-ts-direct-execution
Why
Bun runs TypeScript directly. Lean on this in Bun-first repos to simplify dev and reduce toolchain surface area.
Do
- Run TS/JS entrypoints directly:
bun run src/index.ts- Pass args normally:
bun run src/server.ts --port 3000
Don't
- Don’t add
tsx/ts-nodejust to run TypeScript if Bun already owns the runtime.
Examples
bun run src/server.ts --port 3000runtime-watch-and-hot-reload
Why
Fast feedback loops are one of Bun’s biggest advantages. Use built-in watch/hot modes instead of external file watchers when possible.
Do
- Auto-restart on file changes:
bun --watch run src/server.ts- Use hot reloading when appropriate:
bun --hot run src/server.ts
Don't
- Don’t add
nodemonby default in Bun-first repos.
Examples
bun --watch run src/index.tsscripts-bun-filter-and-workspaces
Why
Monorepos need a reliable way to run scripts across packages without cd gymnastics or external orchestration tools. Bun supports:
--workspacesto run in all workspace packages--filterto run in matching packages
Do
- Use
--workspacesfor “run everywhere”. - Use
--filter(orbun --filter <pattern> <script>) for subsets. - Use
--elide-lines 0to show full output if you need complete logs.
Don't
- Don’t shell out to multiple terminals for routine multi-package runs.
Examples
Run dev in all workspace packages:
bun --filter \"*\" devRun build sequentially across all packages:
bun run --sequential --workspaces buildShow full logs:
bun run --parallel --elide-lines 0 --filter \"packages/*\" testscripts-bun-run-parallel-sequential
Why
Running multiple scripts (or workspace scripts) is a common need in monorepos and CI. Bun supports Foreman-style execution with:
--parallelfor concurrent runs--sequentialfor ordered runs
These flags are available in Bun v1.3.10+.
This reduces the need for extra dependencies like concurrently for simple cases.
Do
- Use
bun run --parallel ...for concurrent scripts. - Use
bun run --sequential ...when order matters. - Combine with
--workspacesor--filterfor monorepos. - Use
--no-exit-on-errorwhen you need a “best effort” run.
Don't
- Don’t introduce additional process supervisors until Bun’s native options are insufficient.
Examples
Run multiple scripts in one package:
bun run --parallel lint typecheckRun a script across all workspace packages concurrently:
bun run --parallel --workspaces testRun across a subset of packages:
bun run --parallel --filter \"packages/*\" buildContinue even if one fails:
bun run --parallel --no-exit-on-error --workspaces testscripts-no-npm-in-bun-repos
Why
Mixing npm/pnpm/yarn commands with Bun creates split-brain dependency graphs and lockfile drift. It also makes onboarding and CI unpredictable.
Do
- Use
bun installfor installs. - Use
bun run <script>for package scripts. - Use
bunx <bin>for one-off CLIs.
Don't
- Don’t call
npm,pnpm,yarn, ornpxfrompackage.jsonscripts in Bun-first repos.
Examples
Bad:
{
"scripts": {
"lint": "npm run biome",
"gen": "npx prisma generate"
}
}Good:
{
"scripts": {
"lint": "bunx biome check .",
"gen": "bunx prisma generate"
}
}test-bun-retry
Why
bun test in Bun v1.3.10 supports a global retry count for flaky environments.
Use retries for CI stability without per-test duplication.
Do
- Start with
bun test --retry <count>in CI jobs. - Keep suite-level retry low and prefer fixing flaky behavior before raising it.
- Use per-test
{ retry: N }only when behavior differs from the suite default.
Don't
- Don’t use retries to mask deterministic failures.
- Don’t set a very high default retry globally and skip root-cause analysis.
Examples
Global retry for CI:
bun test --retry 3 --coverageOverride globally with per-test options:
import { test } from "bun:test";
test(
"very flaky test",
{ retry: 5 },
() => {
// ...
},
);test-bun-test-runner
Why
bun test is Bun’s built-in test runner. Using it avoids extra runtime/tooling overhead and keeps the toolchain consistent.
Do
- Use
bun testfor running tests. - Use
--watchfor iterative TDD. - Use
--coveragewhen you need coverage reporting. - Use
--retrywhen you need a global retry budget in CI.
Don't
- Don’t add Jest/Vitest by default in Bun-first repos unless you need ecosystem-specific features.
Examples
Run all tests:
bun testWatch mode:
bun test --watchRetry in CI:
bun test --retry 3 --coverageMinimal test:
import { expect, test } from "bun:test";
test("adds", () => {
expect(1 + 1).toBe(2);
});test-mocking-and-spying
Why
Bun’s test runner includes first-class mocking/spying. Use it to avoid adding extra test frameworks unless you need ecosystem-specific features.
Newer Bun versions (including v1.3.10) also support Symbol.dispose for mocks/spies, which pairs well with using for automatic cleanup.
Do
- Use
mock()for function mocks. - Use
spyOn()for method interception.
Don't
- Don’t build custom mock helpers until you hit a real limitation.
Examples
import { expect, mock, spyOn, test } from "bun:test";
test("mock + spy", () => {
const fn = mock((x: number) => x + 1);
expect(fn(1)).toBe(2);
expect(fn).toHaveBeenCalledWith(1);
const obj = { method: () => "real" };
const spy = spyOn(obj, "method").mockReturnValue("fake");
expect(obj.method()).toBe("fake");
expect(spy).toHaveBeenCalled();
});troubleshooting-esm-cjs-and-exports
Why
ESM/CJS boundary issues are a top source of runtime failures when switching runtimes or bundlers: default exports, require() vs import, and package exports conditions.
Do
- Prefer ESM (
"type": "module") for new Bun-first code where feasible. - When bundling, set an explicit
--target(bunvsnode) to match production. - Use
bun buildexternalization when a dependency relies on runtime resolution.
Don't
- Don’t mix
require()and ESM imports without understanding how your runtime resolves them. - Don’t assume
node16TS resolution implies runtime behavior; verify.
Debugging Moves
- Inspect the failing package’s
package.jsonforexports. - Try a minimal reproduction with
bun runvsnodeto isolate runtime differences. - If bundling, temporarily mark the dependency as
externalto confirm it’s a bundling issue.
troubleshooting-types-bun
Why
Type errors like “Cannot find name 'Bun'” or missing bun:* module types are usually caused by missing type packages or missing compilerOptions.types.
Do
- Install
@types/bun. - Add
compilerOptions.types: ["bun-types"]when you need Bun globals. - Restart the TS server after changing types.
Don't
- Don’t paper over missing types with
any.
Example
bun add -d @types/bun{
"compilerOptions": {
"types": ["bun-types"]
}
}tsconfig-bun-recommended
Why
Bun can run TypeScript directly, but TypeScript tooling still needs correct compiler options. Bun’s recommended tsconfig.json reduces ESM/CJS surprises and makes resolution behavior predictable.
Do
- Start from
bun init(it generates a Bun-friendly tsconfig) and adjust minimally. - Prefer:
target: "ESNext"module: "Preserve"moduleResolution: "Bundler"verbatimModuleSyntax: truenoEmit: true
Don't
- Don’t use legacy
moduleResolutionmodes unless you have a specific requirement.
Example
{
"compilerOptions": {
"target": "ESNext",
"module": "Preserve",
"moduleResolution": "Bundler",
"verbatimModuleSyntax": true,
"allowImportingTsExtensions": true,
"noEmit": true,
"strict": true
}
}tsconfig-bun-types
Why
Without Bun’s type definitions, TypeScript won’t understand Bun globals (like Bun) and Bun-specific modules (bun:*), causing false errors and poor editor support.
Do
- Install
@types/bunas a dev dependency. - Add
"types": ["bun-types"]tocompilerOptionswhen you need Bun globals in TS.
Don't
- Don’t rely on implicit global types being present.
Examples
bun add -d @types/bun{
"compilerOptions": {
"types": ["bun-types"]
}
}tsconfig-module-resolution-bundler
Why
moduleResolution: "Bundler" matches modern ESM + bundler-style resolution and aligns better with Bun’s behavior than legacy Node resolution modes.
Do
- Prefer
compilerOptions.moduleResolution: "Bundler"in Bun-first TS projects. - Use
verbatimModuleSyntax: trueto keep imports/exports consistent.
Don't
- Don’t default to
node16/nodenextunless you specifically need Node’s conditional exports semantics in TS.
Example
{
"compilerOptions": {
"moduleResolution": "Bundler",
"verbatimModuleSyntax": true
}
}vercel-bun-function-fetch-handler
Why
Vercel’s Bun runtime uses a fetch-style handler for Functions. This keeps code aligned with Web-standard Request/Response APIs.
Do
- Export a default object with an async
fetch(request: Request)method. - Use
Response.json(...)for JSON responses.
Don't
- Don’t build a long-lived HTTP server inside a Function.
Examples
api/hello.ts:
export default {
async fetch(request: Request) {
const url = new URL(request.url);
const name = url.searchParams.get("name") || "World";
return Response.json({ message: `Hello ${name}!` });
},
};vercel-bun-install-detection
Why
Vercel selects the install strategy based on detected lockfiles. If you want Bun installs on Vercel, commit bun.lockb and avoid competing lockfiles.
Do
- Commit
bun.lockb. - Delete other lockfiles (
package-lock.json,pnpm-lock.yaml,yarn.lock). - Keep
packageManagerconsistent (bun@...).
Don't
- Don’t rely on “install command overrides” as the primary mechanism; prefer lockfile-based detection.
Example
bun.lockb
package.jsonvercel-bun-runtime-enable
Why
Vercel Functions and Routing Middleware can run on the Bun runtime (Vercel’s Bun runtime is currently Beta per the referenced docs). Enabling it is configuration-driven and should be explicit to avoid accidental runtime changes.
Do
- Enable Bun runtime by setting
bunVersioninvercel.json(orvercel.ts). - Prefer
bunVersion: "1.x"unless you have a hard requirement to pin. - Validate current support/constraints in
references/ref-vercel-bun-runtime.md.
Don't
- Don’t assume that using Bun locally automatically means Vercel is running Bun in production.
Examples
vercel.json:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"bunVersion": "1.x"
}vercel.ts:
import type { VercelConfig } from "@vercel/config/v1";
export const config: VercelConfig = {
bunVersion: "1.x",
};vercel-bun-runtime-limitations
Why
Vercel’s Bun runtime is optimized for serverless execution. Some Bun APIs (notably Bun.serve) are not supported in Vercel Functions.
Do
- Use the Vercel Function handler style (
fetch) or a framework adapter designed for Vercel. - Treat the runtime as request/response oriented; avoid assumptions of long-lived servers.
- If you need a long-lived server, deploy to a platform that supports
Bun.serve()as a persistent process.
Don't
- Don’t use
Bun.serve()inside Vercel Functions.
Examples
Bad (not supported in Vercel Functions):
Bun.serve({
fetch() {
return new Response("hi");
},
});Good (Vercel Function fetch handler):
export default {
async fetch(req: Request) {
return new Response("hi");
},
};vercel-nextjs-bun-runtime-scripts
Why
When using the Bun runtime on Vercel with Next.js (notably with ISR), Vercel recommends running next commands in a way that forces Bun to execute the CLI (since next is typically a Node-shebang binary).
Do
- Update
package.jsonscripts to run Next withbun run --bun ...when you enable Bun runtime and rely on Next.js features that need it (e.g., ISR).
Don't
- Don’t assume
next dev/next buildwill automatically execute under Bun when a binary has a Node shebang.
Examples
Before:
{
"scripts": {
"dev": "next dev",
"build": "next build"
}
}After:
{
"scripts": {
"dev": "bun run --bun next dev",
"build": "bun run --bun next build"
}
}#!/usr/bin/env bun
/**
* Deterministically rebuild `rules/_index.md`.
*
* Run:
* bun ~/.agents/skills/bun-dev/scripts/build-rules-index.ts
*/
import { readdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const skillRoot = join(__filename, "..", "..");
const rulesDir = join(skillRoot, "rules");
function isRuleFile(name: string): boolean {
return name.endsWith(".md") && name !== "_index.md";
}
function ruleIdFromFile(name: string): string {
return name.replace(/\.md$/, "");
}
async function main() {
const names = (await readdir(rulesDir)).filter(isRuleFile).sort((a, b) => a.localeCompare(b));
const lines: string[] = [];
lines.push("# Rules Index");
lines.push("");
lines.push("Open `SKILL.md` first to route by priority. Prefer opening specific rules over references.");
lines.push("");
const groups: Array<{ title: string; prefix: string }> = [
{ title: "Package Manager + Lockfiles (P1)", prefix: "pm-" },
{ title: "Runtime Selection (P1)", prefix: "runtime-" },
{ title: "Vercel Bun Runtime (P1)", prefix: "vercel-" },
{ title: "Scripts + Monorepos (P2)", prefix: "scripts-" },
{ title: "TypeScript + Tooling (P2)", prefix: "tsconfig-" },
{ title: "Testing (P3)", prefix: "test-" },
{ title: "Build + Bundling (P3)", prefix: "build-" },
{ title: "Performance (P4)", prefix: "perf-" },
{ title: "Migration (P5)", prefix: "migrate-" },
{ title: "Troubleshooting (P5)", prefix: "troubleshooting-" },
];
const remaining = new Set(names);
for (const g of groups) {
const groupNames = names.filter((n) => n.startsWith(g.prefix));
if (groupNames.length === 0) continue;
lines.push(`## ${g.title}`);
lines.push("");
for (const name of groupNames) {
remaining.delete(name);
lines.push(`- \`${ruleIdFromFile(name)}\``);
}
lines.push("");
}
const leftovers = [...remaining].sort((a, b) => a.localeCompare(b));
if (leftovers.length > 0) {
lines.push("## Other");
lines.push("");
for (const name of leftovers) lines.push(`- \`${ruleIdFromFile(name)}\``);
lines.push("");
}
await writeFile(join(rulesDir, "_index.md"), `${lines.join("\n")}\n`, "utf8");
}
await main();
#!/usr/bin/env bun
/**
* Compatibility wrapper for the Bun platform audit CLI.
*
* This keeps the long-lived bun-dev entrypoint stable while delegating to the
* shared platform engine.
*/
import { createSkillContext, runCli } from './lib/bun-platform-core';
await runCli(createSkillContext(import.meta.url), process.argv.slice(2));
#!/usr/bin/env bun
import { createSkillContext, runCli } from './lib/bun-platform-core';
await runCli(createSkillContext(import.meta.url), process.argv.slice(2));
#!/usr/bin/env bun
/**
* Skill integrity checks:
* - All rule IDs referenced in `SKILL.md` exist in `rules/`.
* - `references/index.md` exists.
* - `rules/_index.md` exists.
* - `references/` is flat (no subdirectories).
*
* Run:
* bun ~/.agents/skills/bun-dev/scripts/check-skill-integrity.ts
*/
import { readFile, readdir, stat } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const skillRoot = join(__filename, "..", "..");
function extractBacktickedIds(md: string): string[] {
const out: string[] = [];
const re = /`([a-z0-9-]+)`/g;
for (;;) {
const m = re.exec(md);
if (!m) break;
out.push(m[1]);
}
return out;
}
async function fileExists(path: string): Promise<boolean> {
try {
await stat(path);
return true;
} catch {
return false;
}
}
async function main() {
const skillMdPath = join(skillRoot, "SKILL.md");
const rulesDir = join(skillRoot, "rules");
const refsDir = join(skillRoot, "references");
const skillMd = await readFile(skillMdPath, "utf8");
const ids = new Set(extractBacktickedIds(skillMd));
// Only validate ids that look like rule IDs by prefix.
const rulePrefixes = [
"pm-",
"runtime-",
"vercel-",
"scripts-",
"tsconfig-",
"test-",
"build-",
"perf-",
"migrate-",
"troubleshooting-",
] as const;
const referencedRuleIds = [...ids].filter((id) => {
// Ignore backticked prefix strings like `pm-` used in tables.
if (id.endsWith("-")) return false;
// Must be a plausible rule id: at least one hyphen segment.
if (!/^[a-z0-9]+(?:-[a-z0-9]+)+$/.test(id)) return false;
return rulePrefixes.some((p) => id.startsWith(p));
});
const ruleFiles = (await readdir(rulesDir)).filter((f) => f.endsWith(".md"));
const existingRuleIds = new Set(ruleFiles.map((f) => f.replace(/\.md$/, "")));
const missing = referencedRuleIds.filter((id) => !existingRuleIds.has(id));
if (missing.length > 0) {
throw new Error(`Missing rule files for ids:\n${missing.map((m) => `- ${m}`).join("\n")}`);
}
if (!(await fileExists(join(rulesDir, "_index.md")))) {
throw new Error("Missing rules/_index.md (run build-rules-index.ts)");
}
if (!(await fileExists(join(refsDir, "index.md")))) {
throw new Error("Missing references/index.md");
}
// Ensure references are flat (no subdirectories).
for (const name of await readdir(refsDir)) {
const p = join(refsDir, name);
const s = await stat(p);
if (s.isDirectory()) {
throw new Error(`references/ must be flat; found directory: references/${name}`);
}
}
console.log("OK: bun-dev skill integrity checks passed.");
}
await main();
import {
createReleaseSyncReport,
createSkillContext,
applySafeFixes,
loadAuditConfig,
planSafeFixes,
runAudit,
} from '../lib/bun-platform-core';
import {
cpSync,
existsSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import { afterEach, describe, expect, test } from 'bun:test';
const fixturesRoot = path.join(import.meta.dir, 'fixtures');
const tempRoots: string[] = [];
function copyFixture(name: string): string {
const target = mkdtempSync(path.join(os.tmpdir(), `bun-platform-${name}-`));
cpSync(path.join(fixturesRoot, name), target, { recursive: true });
tempRoots.push(target);
return target;
}
afterEach(() => {
while (tempRoots.length > 0) {
rmSync(tempRoots.pop()!, { recursive: true, force: true });
}
});
describe('bun platform audit engine', () => {
test('reports mixed lockfiles as an error', () => {
const root = copyFixture('mixed-lockfiles');
const findings = runAudit(root, loadAuditConfig(root));
expect(findings.some((finding) => finding.ruleId === 'pm-no-mixed-lockfiles' && finding.severity === 'error')).toBe(
true,
);
});
test('plans and applies safe package.json fixes', () => {
const root = copyFixture('safe-fixes');
const planned = planSafeFixes(root, loadAuditConfig(root));
expect(planned.flatMap((fix) => [...fix.ruleIds]).sort()).toEqual(['pm-bunx-vs-npx', 'pm-package-manager-field']);
applySafeFixes(root, loadAuditConfig(root));
const packageJson = readFileSync(path.join(root, 'package.json'), 'utf8');
expect(packageJson).toContain(`"packageManager": "bun@${Bun.version}"`);
expect(packageJson).toContain('"gen": "bunx prisma generate"');
});
test('normalizes Next.js scripts when Vercel Bun runtime is enabled', () => {
const root = copyFixture('vercel-next');
const findings = runAudit(root, loadAuditConfig(root));
expect(findings.some((finding) => finding.ruleId === 'vercel-nextjs-bun-runtime-scripts')).toBe(true);
const planned = planSafeFixes(root, loadAuditConfig(root));
expect(planned.some((fix) => fix.ruleIds.includes('vercel-nextjs-bun-runtime-scripts'))).toBe(true);
applySafeFixes(root, loadAuditConfig(root));
const packageJson = readFileSync(path.join(root, 'package.json'), 'utf8');
expect(packageJson).toContain('"dev": "bun run --bun next dev"');
expect(packageJson).toContain('"build": "bun run --bun next build"');
});
test('respects disabled rules and baseline suppressions from config', () => {
const root = copyFixture('safe-fixes');
const baselinePath = path.join(root, 'baseline.json');
writeFileSync(baselinePath, JSON.stringify(['pm-package-manager-field:package.json'], null, 2));
writeFileSync(
path.join(root, 'bun-platform.config.json'),
JSON.stringify(
{
disabledRules: ['pm-bunx-vs-npx'],
baseline: './baseline.json',
},
null,
2,
),
);
const findings = runAudit(root, loadAuditConfig(root));
expect(findings.map((finding) => finding.ruleId)).not.toContain('pm-package-manager-field');
expect(findings.map((finding) => finding.ruleId)).not.toContain('pm-bunx-vs-npx');
});
test('creates a rollback artifact when safe fixes are applied', () => {
const root = copyFixture('safe-fixes');
applySafeFixes(root, loadAuditConfig(root));
const rollbackDir = path.join(root, '.bun-platform', 'rollbacks');
const entries = Bun.spawnSync({
cmd: ['zsh', '-lc', `find "${rollbackDir}" -type f | wc -l`],
stdout: 'pipe',
stderr: 'pipe',
});
expect(entries.stdout.toString().trim()).toBe('1');
});
test('caches audit results on disk', () => {
const root = copyFixture('mixed-lockfiles');
runAudit(root, loadAuditConfig(root));
const cachePath = path.join(root, '.bun-platform', 'cache.sqlite');
expect(Bun.file(cachePath).size).toBeGreaterThan(0);
});
test('auto-adds .bun-platform to .gitignore when platform state is created', () => {
const root = copyFixture('mixed-lockfiles');
runAudit(root, loadAuditConfig(root));
const gitignore = readFileSync(path.join(root, '.gitignore'), 'utf8');
expect(gitignore).toContain('# Bun platform state');
expect(gitignore).toContain('.bun-platform/');
});
test('does not duplicate the .bun-platform gitignore entry', () => {
const root = copyFixture('mixed-lockfiles');
runAudit(root, loadAuditConfig(root));
runAudit(root, loadAuditConfig(root));
const gitignore = readFileSync(path.join(root, '.gitignore'), 'utf8');
expect(gitignore.match(/\.bun-platform\//g)?.length ?? 0).toBe(1);
});
test('supports disabling gitignore management in config', () => {
const root = copyFixture('mixed-lockfiles');
writeFileSync(
path.join(root, 'bun-platform.config.json'),
JSON.stringify(
{
manageGitignore: false,
},
null,
2,
),
);
runAudit(root, loadAuditConfig(root));
expect(existsSync(path.join(root, '.gitignore'))).toBe(false);
});
test('reports GitHub Actions adapter findings', () => {
const root = copyFixture('github-actions');
const findings = runAudit(root, loadAuditConfig(root));
expect(findings.some((finding) => finding.ruleId === 'scripts-no-npm-in-bun-repos')).toBe(true);
expect(findings.some((finding) => finding.ruleId === 'pm-bun-install-ci-frozen-lockfile')).toBe(true);
});
test('reports Docker adapter findings', () => {
const root = copyFixture('docker');
const findings = runAudit(root, loadAuditConfig(root));
expect(findings.some((finding) => finding.ruleId === 'runtime-bun-vs-node-choose')).toBe(true);
expect(findings.some((finding) => finding.ruleId === 'scripts-no-npm-in-bun-repos')).toBe(true);
});
test('reports monorepo adapter findings', () => {
const root = copyFixture('monorepo');
const findings = runAudit(root, loadAuditConfig(root));
expect(findings.some((finding) => finding.ruleId === 'scripts-bun-filter-and-workspaces')).toBe(true);
});
test('creates a release sync report from current references', () => {
const context = createSkillContext(pathToFileURL(path.join(import.meta.dir, '..', 'bun-platform.ts')).href);
const report = createReleaseSyncReport(context);
expect(report.references.length).toBeGreaterThan(0);
expect(report.capabilityMap.some((entry) => entry.topic === 'bun test retry' && entry.classification === 'capability-present')).toBe(
true,
);
});
test('validate command executes Bun-native repo checks', () => {
const root = copyFixture('validate-success');
const proc = Bun.spawnSync({
cmd: ['bun', path.join(import.meta.dir, '..', 'bun-platform.ts'), 'validate', '--root', root, '--fail-on', 'warn'],
stdout: 'pipe',
stderr: 'pipe',
});
expect(proc.exitCode).toBe(0);
expect(proc.stdout.toString()).toContain('Validated 4 command(s).');
});
test('benchmark command runs against a temp fixture copy and not the committed fixture', () => {
const root = copyFixture('github-actions');
const proc = Bun.spawnSync({
cmd: ['bun', path.join(import.meta.dir, '..', 'bun-platform.ts'), 'benchmark', '--root', root],
stdout: 'pipe',
stderr: 'pipe',
});
expect(proc.exitCode).toBe(0);
expect(proc.stdout.toString()).toContain('"cacheEntries"');
expect(existsSync(path.join(fixturesRoot, 'github-actions', '.bun-platform'))).toBe(false);
});
test('committed fixtures stay free of platform state', () => {
for (const fixture of ['github-actions', 'validate-success', 'mixed-lockfiles', 'safe-fixes', 'vercel-next', 'docker', 'monorepo']) {
expect(existsSync(path.join(fixturesRoot, fixture, '.bun-platform'))).toBe(false);
}
});
});
FROM node:22-alpine
WORKDIR /app
COPY package.json bun.lockb ./
RUN npm install
RUN bun install
{
"name": "docker-fixture",
"private": true,
"packageManager": "bun@1.3.10"
}
name: ci
on: [push]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm install
- run: npx prisma generate
- run: bun install
{
"name": "github-actions-fixture",
"private": true,
"packageManager": "bun@1.3.10",
"scripts": {
"lint": "bun run lint:repo"
}
}
{}
{
"name": "mixed-lockfiles-fixture",
"private": true,
"scripts": {
"build": "bun build ./src/index.ts --outdir ./dist"
}
}
{
"name": "monorepo-fixture",
"private": true,
"packageManager": "bun@1.3.10",
"workspaces": [
"packages/*"
],
"scripts": {
"build": "bun run build",
"test": "bun run test"
}
}
{
"name": "@fixture/a",
"version": "0.0.0"
}
{
"name": "@fixture/b",
"version": "0.0.0"
}
{
"name": "safe-fixes-fixture",
"private": true,
"scripts": {
"gen": "npx prisma generate"
}
}
{
"name": "validate-success-fixture",
"private": true,
"packageManager": "bun@1.3.10",
"scripts": {
"lint": "bun -e \"console.log('lint ok')\"",
"typecheck": "bun -e \"console.log('typecheck ok')\"",
"test": "bun -e \"console.log('test ok')\"",
"build": "bun -e \"console.log('build ok')\""
}
}
{
"name": "vercel-next-fixture",
"private": true,
"packageManager": "bun@1.3.10",
"scripts": {
"dev": "next dev",
"build": "next build"
}
}
{
"bunVersion": "1.x"
}
#!/usr/bin/env bun
/**
* Fetch and snapshot Bun release notes into `references/`.
*
* Default:
* - URL: https://bun.com/blog/release-notes/bun-v1.3.10
* - Out: references/ref-bun-release-notes-bun-v1.3.10.md
*
* Run:
* bun ~/.agents/skills/bun-dev/scripts/update-bun-release-notes.ts
*
* Custom:
* bun ~/.agents/skills/bun-dev/scripts/update-bun-release-notes.ts --url https://bun.com/blog/release-notes/bun-vX.Y.Z --base ref-bun-release-notes-bun-vX.Y.Z
*/
import { mkdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
type Args = Readonly<{
url: string;
base: string;
}>;
const __filename = fileURLToPath(import.meta.url);
const skillRoot = join(__filename, "..", "..");
const referencesDir = join(skillRoot, "references");
function parseArgs(argv: readonly string[]): Args {
const args = [...argv];
const take = (flag: string): string | undefined => {
const idx = args.indexOf(flag);
if (idx === -1) return undefined;
const val = args[idx + 1];
if (!val || val.startsWith("--")) return undefined;
return val;
};
const url = take("--url") ?? "https://bun.com/blog/release-notes/bun-v1.3.10";
const base = take("--base") ?? "ref-bun-release-notes-bun-v1.3.10";
return { url, base };
}
function stripYamlFrontmatter(md: string): string {
if (!md.startsWith("---\n")) return md;
const end = md.indexOf("\n---\n", 4);
if (end === -1) return md;
return md.slice(end + "\n---\n".length);
}
function stripLiquidTags(md: string): string {
const out: string[] = [];
for (const line of md.split("\n")) {
const t = line.trim();
if (/^\{%\s*.*\s*%\}$/.test(t)) continue;
out.push(line);
}
return out.join("\n");
}
function tidyMarkdown(md: string): string {
let s = md.replace(/\r/g, "");
s = stripYamlFrontmatter(s);
s = stripLiquidTags(s);
s = s.replace(/\n{3,}/g, "\n\n");
s = s.trim();
return `${s}\n`;
}
function extractMarkdownAlternateHref(html: string): string | undefined {
const linkRe = /<link\b[^>]*\btype=["']text\/markdown["'][^>]*>/gi;
for (const m of html.matchAll(linkRe)) {
const tag = m[0];
const hrefMatch = tag.match(/\bhref=["']([^"']+)["']/i);
if (hrefMatch) return hrefMatch[1];
}
return undefined;
}
async function resolveMarkdownUrl(url: string): Promise<string> {
if (url.endsWith(".md")) return url;
const res = await fetch(url, {
headers: {
"user-agent": "bun-dev-skill/1.0 (+https://bun.com)",
accept: "text/html,application/xhtml+xml",
},
});
if (!res.ok) {
throw new Error(`Failed to fetch HTML: ${url} (${res.status} ${res.statusText})`);
}
const html = await res.text();
const href = extractMarkdownAlternateHref(html);
if (!href) {
throw new Error(`Could not find <link rel=\"alternate\" type=\"text/markdown\"> in ${url}`);
}
return new URL(href, url).toString();
}
async function main() {
const { url, base } = parseArgs(process.argv.slice(2));
await mkdir(referencesDir, { recursive: true });
const mdUrl = await resolveMarkdownUrl(url);
const res = await fetch(mdUrl, {
headers: {
"user-agent": "bun-dev-skill/1.0 (+https://bun.com)",
accept: "text/markdown,text/plain;q=0.9,*/*;q=0.8",
},
});
if (!res.ok) {
throw new Error(`Failed to fetch markdown: ${mdUrl} (${res.status} ${res.statusText})`);
}
const raw = await res.text();
const md = tidyMarkdown(raw);
const outPath = join(referencesDir, `${base}.md`);
// Clean up legacy formats (older versions of this script wrote html/txt).
await rm(join(referencesDir, `${base}.html`), { force: true });
await rm(join(referencesDir, `${base}.txt`), { force: true });
await writeFile(outPath, md, "utf8");
console.log(`Wrote:\n- ${outPath}`);
}
await main();
#!/usr/bin/env bun
/**
* Fetch and snapshot Vercel's Bun runtime docs into `references/`.
*
* Default:
* - URL: https://vercel.com/docs/functions/runtimes/bun
* - Out: references/ref-vercel-bun-runtime.md
*
* Run:
* bun ~/.agents/skills/bun-dev/scripts/update-vercel-bun-docs.ts
*
* Custom:
* bun ~/.agents/skills/bun-dev/scripts/update-vercel-bun-docs.ts --url <url> --base <base>
*/
import { mkdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
type Args = Readonly<{
url: string;
base: string;
}>;
const __filename = fileURLToPath(import.meta.url);
const skillRoot = join(__filename, "..", "..");
const referencesDir = join(skillRoot, "references");
function parseArgs(argv: readonly string[]): Args {
const args = [...argv];
const take = (flag: string): string | undefined => {
const idx = args.indexOf(flag);
if (idx === -1) return undefined;
const val = args[idx + 1];
if (!val || val.startsWith("--")) return undefined;
return val;
};
const url = take("--url") ?? "https://vercel.com/docs/functions/runtimes/bun";
const base = take("--base") ?? "ref-vercel-bun-runtime";
return { url, base };
}
function stripYamlFrontmatter(md: string): string {
if (!md.startsWith("---\n")) return md;
const end = md.indexOf("\n---\n", 4);
if (end === -1) return md;
return md.slice(end + "\n---\n".length);
}
function tidyMarkdown(md: string): string {
let s = md.replace(/\r/g, "");
s = stripYamlFrontmatter(s);
s = s.replace(/\n{3,}/g, "\n\n");
s = s.trim();
return `${s}\n`;
}
function decodeHtmlEntities(input: string): string {
return input
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/ /g, " ");
}
function htmlToMarkdown(html: string): string {
const mainMatch = html.match(/<main\b[^>]*>([\s\S]*?)<\/main>/i);
let content = mainMatch?.[1] ?? html;
content = content.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "");
content = content.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "");
content = content.replace(/<svg\b[^>]*>[\s\S]*?<\/svg>/gi, "");
content = content.replace(/<(h1|h2|h3|h4|h5|h6)\b[^>]*>([\s\S]*?)<\/\1>/gi, (_, tag, inner) => {
const level = Number(tag.slice(1));
return `\n${"#".repeat(level)} ${decodeHtmlEntities(inner.replace(/<[^>]+>/g, " ").trim())}\n`;
});
content = content.replace(/<li\b[^>]*>([\s\S]*?)<\/li>/gi, (_, inner) => `\n- ${decodeHtmlEntities(inner.replace(/<[^>]+>/g, " ").trim())}`);
content = content.replace(/<(p|div|section|article|pre|table|tr)\b[^>]*>/gi, "\n");
content = content.replace(/<\/(p|div|section|article|pre|table|tr)>/gi, "\n");
content = content.replace(/<code\b[^>]*>([\s\S]*?)<\/code>/gi, (_, inner) => `\`${decodeHtmlEntities(inner)}\``);
content = content.replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_, href, inner) => {
const text = decodeHtmlEntities(inner.replace(/<[^>]+>/g, " ").trim());
return text.length > 0 ? `[${text}](${href})` : href;
});
content = content.replace(/<[^>]+>/g, " ");
content = decodeHtmlEntities(content);
content = content.replace(/[ \t]+\n/g, "\n");
content = content.replace(/\n{3,}/g, "\n\n");
return tidyMarkdown(content);
}
function extractMarkdownAlternateHref(html: string): string | undefined {
const linkRe = /<link\b[^>]*\btype=["']text\/markdown["'][^>]*>/gi;
for (const m of html.matchAll(linkRe)) {
const tag = m[0];
const hrefMatch = tag.match(/\bhref=["']([^"']+)["']/i);
if (hrefMatch) return hrefMatch[1];
}
return undefined;
}
async function resolveMarkdownUrl(url: string): Promise<string> {
if (url.endsWith(".md")) return url;
const res = await fetch(url, {
headers: {
"user-agent": "bun-dev-skill/1.0 (+https://vercel.com)",
accept: "text/html,application/xhtml+xml",
},
});
if (!res.ok) {
throw new Error(`Failed to fetch HTML: ${url} (${res.status} ${res.statusText})`);
}
const html = await res.text();
const href = extractMarkdownAlternateHref(html);
if (!href) {
throw new Error(`Could not find <link rel=\"alternate\" type=\"text/markdown\"> in ${url}`);
}
return new URL(href, url).toString();
}
async function fetchHtml(url: string): Promise<string> {
const res = await fetch(url, {
headers: {
"user-agent": "bun-dev-skill/1.0 (+https://vercel.com)",
accept: "text/html,application/xhtml+xml",
},
});
if (!res.ok) {
throw new Error(`Failed to fetch HTML: ${url} (${res.status} ${res.statusText})`);
}
return res.text();
}
async function main() {
const { url, base } = parseArgs(process.argv.slice(2));
await mkdir(referencesDir, { recursive: true });
const html = await fetchHtml(url);
const mdUrl = await resolveMarkdownUrl(url).catch(() => undefined);
let md: string;
if (mdUrl) {
const res = await fetch(mdUrl, {
headers: {
"user-agent": "bun-dev-skill/1.0 (+https://vercel.com)",
accept: "text/markdown,text/plain;q=0.9,*/*;q=0.8",
},
});
if (res.ok) {
md = tidyMarkdown(await res.text());
} else {
md = htmlToMarkdown(html);
}
} else {
md = htmlToMarkdown(html);
}
const outPath = join(referencesDir, `${base}.md`);
// Clean up legacy formats (older versions of this script wrote html/txt).
await rm(join(referencesDir, `${base}.html`), { force: true });
await rm(join(referencesDir, `${base}.txt`), { force: true });
await writeFile(outPath, md, "utf8");
console.log(`Wrote:\n- ${outPath}`);
}
await main();
Related skills
FAQ
What does the priority table do?
It routes you to the right rules by prefix, with package manager, runtime, and Vercel Bun runtime as the critical priority-1 categories.
Does it cover Vercel deployment?
Yes. It covers enabling the Bun runtime, its limitations, Next.js ISR scripts, and the Bun function fetch handler.