Bun Runtime Update: PTY Support, Feature Flags, and Enhanced Compatibility
This Bun runtime update introduces Pseudo-Terminal (PTY) API support, compile-time feature flags for dead-code elimination, and significantly improved Bun.stringWidth accuracy. It also brings crucial bug fixes for networking, Windows, Node.js compatibility, and security, enhancing overall stability and development experience.
Bun Runtime Update: PTY Support, Feature Flags, and Enhanced Compatibility
This update for the Bun runtime introduces significant new features and numerous bug fixes, enhancing its capabilities and stability across various platforms.
Installation and Upgrade
Install Bun:
- curl:
curl -fsSL https://bun.sh/install | bash - npm:
npm install -g bun - PowerShell:
powershell -c "irm bun.sh/install.ps1 | iex" - Scoop:
scoop install bun - Homebrew:
brew tap oven-sh/bun brew install bun - Docker:
docker pull oven/bun docker run --rm --init --ulimit memlock=-1:-1 oven/bun
Upgrade Bun:
bun upgrade
Bun.Terminal: API for Pseudo-Terminal (PTY) Support
Bun now features a built-in API for creating and managing pseudo-terminals (PTYs), which enables the execution of interactive terminal applications such as shells, vim, htop, and any program expecting a real TTY environment.
Utilize the new terminal option within Bun.spawn() to attach a PTY to your subprocess:
const commands = [
"echo Hello from PTY!",
"exit"
];
const proc = Bun.spawn(["bash"], {
terminal: {
cols: 80,
rows: 24,
data(terminal, data) {
process.stdout.write(data);
if (data.includes("$")) {
terminal.write(commands.shift() + "
");
}
},
},
});
await proc.exited;
proc.terminal.close();
With a PTY attached, the subprocess perceives process.stdout.isTTY as true. This enables features like colored output, cursor movement, and interactive prompts that typically require a genuine terminal.
Running Interactive Programs
Example for running vim interactively:
const proc = Bun.spawn(["vim", "file.txt"], {
terminal: {
cols: process.stdout.columns,
rows: process.stdout.rows,
data(term, data) {
process.stdout.write(data);
},
},
});
proc.exited.then((code) => process.exit(code));
// Handle terminal resize events
process.stdout.on("resize", () => {
proc.terminal.resize(process.stdout.columns, process.stdout.rows);
});
// Forward input from stdin to the terminal
process.stdin.setRawMode(true);
for await (const chunk of process.stdin) {
proc.terminal.write(chunk);
}
Reusable Terminals
Create a standalone terminal instance using new Bun.Terminal() to reuse it across multiple subprocesses:
await using terminal = new Bun.Terminal({
cols: 80,
rows: 24,
data(term, data) {
process.stdout.write(data);
},
});
const proc1 = Bun.spawn(["echo", "first"], { terminal });
await proc1.exited;
const proc2 = Bun.spawn(["echo", "second"], { terminal });
await proc2.exited;
// The terminal is automatically closed by `await using`
The Terminal object offers comprehensive PTY control via methods such as write(), resize(), setRawMode(), ref()/unref(), and close().
Note: Terminal support is currently exclusive to POSIX systems (Linux, macOS). For Windows support, users are encouraged to file an issue.
Compile-time Feature Flags for Dead-Code Elimination
Bun's bundler now supports compile-time feature flags through import { feature } from "bun:bundle". This enables statically-analyzable dead-code elimination, meaning code paths can be entirely removed from your bundle based on which flags are activated during the build process.
import { feature } from "bun:bundle";
if (feature("PREMIUM")) {
// This code is only included when the PREMIUM flag is enabled
initPremiumFeatures();
}
if (feature("DEBUG")) {
// This entire block is eliminated when the DEBUG flag is disabled
console.log("Debug mode");
}
The feature() function is replaced with true or false at bundle time. When combined with minification, unreachable branches are completely removed.
Example:
// Input
import { feature } from "bun:bundle";
const mode = feature("PREMIUM") ? "premium" : "free";
// Output (with --feature PREMIUM --minify)
var mode = "premium";
CLI Usage
Enable features during build, runtime, or tests:
# Enable a single feature during build
bun build --feature=PREMIUM ./app.ts --outdir ./out
# Enable a feature at runtime
bun run --feature=DEBUG ./app.ts
# Enable a feature in tests
bun test --feature=MOCK_API
# Enable multiple flags
bun build --feature=PREMIUM --feature=DEBUG ./app.ts
JavaScript API Usage
Features can also be configured programmatically:
await Bun.build({
entrypoints: ["./app.ts"],
outdir: "./out",
features: ["PREMIUM", "DEBUG"],
});
Type Safety
For improved autocomplete and compile-time validation, augment the Registry interface:
// env.d.ts
declare module "bun:bundle" {
interface Registry {
features: "DEBUG" | "PREMIUM" | "BETA_FEATURES";
}
}
After this, feature("TYPO") would correctly trigger a type error.
Common use cases for this functionality include platform-specific builds, environment-based features, A/B testing variants, and managing paid-tier features.
Improved Bun.stringWidth Accuracy
Bun.stringWidth now offers enhanced accuracy in calculating the terminal display width for a much broader range of Unicode characters, ANSI escape sequences, and emojis.
Zero-width Character Support
Previously unhandled invisible characters are now correctly measured as zero-width:
- Soft hyphen (U+00AD)
- Word joiner and invisible operators (U+2060-U+2064)
- Arabic formatting characters
- Indic script combining marks (Devanagari through Malayalam)
- Thai and Lao combining marks
- Tag characters and more
ANSI Escape Sequence Handling
- CSI sequences: All CSI final bytes (0x40-0x7E) are now properly handled, not just
m. Cursor movement, erase, scroll, and other control sequences are correctly excluded from width calculation. - OSC sequences: Support for OSC sequences, including OSC 8 hyperlinks, has been added, accommodating both BEL and ST terminators.
- Fixed: An
ESC ESCstate machine bug that incorrectly reset the state has been resolved.
Grapheme-aware Emoji Width
Emojis are now accurately measured as single graphemes:
Bun.stringWidth("πΊπΈ")// Now: 2 (flag emoji, previously: 1)Bun.stringWidth("ππ½")// Now: 2 (emoji + skin tone, previously: 4)Bun.stringWidth("π¨βπ©βπ§")// Now: 2 (ZWJ family sequence, previously: 8)Bun.stringWidth("\u2060")// Now: 0 (word joiner, previously: 1)
This ensures proper handling of flag emojis, skin tone modifiers, Zero-Width Joiner (ZWJ) sequences (for families and professions), keycap sequences, and variation selectors.
V8 Value Type Checking APIs
Bun now implements additional V8 C++ API methods for type checking, which are frequently used by native Node.js modules. These include:
v8::Value::IsMap()- Checks if a value is a JavaScript Map.v8::Value::IsArray()- Checks if a value is a JavaScript Array.v8::Value::IsInt32()- Checks if a value is a 32-bit integer.v8::Value::IsBigInt()- Checks if a value is a BigInt.
This enhancement significantly improves compatibility with native addons relying on these V8 type checking APIs.
Content-Disposition Support for S3 Uploads
Bun's integrated S3 client now supports the contentDisposition option. This allows developers to control how browsers handle downloaded files, such as setting specific filenames or dictating whether files should be displayed inline or downloaded as attachments.
Example: Force download with a specific filename
import { s3 } from "bun";
const file = s3.file("report.pdf", {
contentDisposition: 'attachment; filename="quarterly-report.pdf"',
});
Example: Set inline disposition when writing
await s3.write("image.png", imageData, {
contentDisposition: "inline",
});
This option is functional across all S3 upload methods, including simple, multipart, and streaming uploads.
Environment Variable Expansion in .npmrc Quoted Values
This update fixes environment variable expansion within quoted .npmrc values and introduces support for the ? optional modifier, aligning behavior with npm.
Previously, environment variables inside quoted strings were not expanded. Now, all three syntaxes work consistently:
# All expand to the value when NPM_TOKEN is set
token = ${NPM_TOKEN}
token = "${NPM_TOKEN}"
token = '${NPM_TOKEN}'
The ? modifier enables graceful handling of undefined environment variables:
# Without ? - undefined variables are left as-is
token = ${NPM_TOKEN} # β ${NPM_TOKEN}
# With ? - undefined variables expand to an empty string
token = ${NPM_TOKEN?} # β (empty)
auth = "Bearer ${TOKEN?}" # β Bearer
Bug Fixes
A comprehensive set of bug fixes has been implemented across various components:
Networking
- Fixed: A macOS kqueue event loop bug that could lead to 100% CPU usage with writable sockets when no actual I/O was pending. This was due to an incorrect bitwise comparison and missing
EV_ONESHOTflags. - Fixed: Incorrect behavior in certain scenarios when automatically re-subscribing to writable sockets after a write failure.
- Fixed:
fetch()no longer throws an error when a proxy object without aurlproperty is passed, restoring compatibility with libraries liketaze. - Fixed: HTTP proxy authentication silently failing with a 401 Unauthorized error when passwords exceeded 4096 characters (e.g., JWT tokens used as proxy credentials).
- Fixed: A potential crash that could occur when upgrading an existing TCP socket to TLS.
Windows Specific Fixes
- Fixed: A WebSocket crash on Windows when publishing large messages with
perMessageDeflate: true, caused by azlibversion mismatch. - Fixed: A panic in error handling on Windows when
.bunxmetadata files were corrupted. The system now gracefully falls back to a slower path instead of crashing. - Fixed:
bunxpanicking on Windows when empty string arguments were passed in certain cases, and incorrect splitting of quoted arguments containing spaces.
Node.js Compatibility
- Fixed:
url.domainToASCII()andurl.domainToUnicode()now return an empty string for invalid domains instead of throwing aTypeError, matching Node.js behavior. - Fixed: Native modules no longer fail with
symbol 'napi_register_module_v1' not foundwhen loaded multiple times (e.g., during hot module reloading or when required in both main thread and a worker). - Fixed: The
node:httpserver'srequest.socket._secureEstablishedproperty now returns correct values on HTTPS servers under concurrent connections in certain cases.
TypeScript Definitions
- Fixed: TypeScript type errors when using
expect().not.toContainKey()and related matchers. The argument was incorrectly inferred asnever, but now properly falls back toPropertyKey. - Fixed: Compatibility issues with
@types/node@25within@types/bun. - Fixed: TypeScript type compatibility with
@types/node@25.0.2where theprocess.noDeprecationproperty type definition had changed.
Web APIs
- Fixed:
Response.clone()andRequest.clone()no longer incorrectly lock the original body whenresponse.bodyorrequest.bodywas accessed before callingclone().
Bundler
- Fixed: The transpiler incorrectly simplifying object spread expressions with nullish coalescing to empty objects (e.g.,
{...k, a: k?.x ?? {}}). This produced invalid JavaScript output and caused "Expected CommonJS module to have a function wrapper" errors with Webpack-generated bundles.
YAML
- Fixed:
YAML.stringifynow correctly quotes strings ending with colons (e.g.,"tin:"), preventingYAML.parsefrom failing with "Unexpected token" errors. - Fixed: A YAML 1.2 spec compliance issue where
yes,Yes,YES,no,No,NO,on,On,ON,off,Off,OFF,y,Ywere incorrectly treated as boolean values instead of strings. These are booleans in YAML 1.1 but not in YAML 1.2.
Security
- Fixed: A security vulnerability where the default trusted dependencies list could be spoofed by non-npm packages using matching names via
file:,link:,git:, orgithub:dependencies. These sources now require explicittrustedDependenciesconfiguration to execute lifecycle scripts. - Fixed: An internal JSC
Loaderproperty was inadvertently leaking intonode:vmcontexts, making it visible in sandboxed environments where it should not be.
Linux Specific Fixes
- Fixed:
Bun.writeandfs.copyFileno longer fail on eCryptfs and other encrypted filesystems on Linux.