Bun Runtime Update: PTY Support, Feature Flags, and Enhanced Compatibility

javascript runtime

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 ESC state 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_ONESHOT flags.
  • 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 a url property is passed, restoring compatibility with libraries like taze.
  • 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 a zlib version mismatch.
  • Fixed: A panic in error handling on Windows when .bunx metadata files were corrupted. The system now gracefully falls back to a slower path instead of crashing.
  • Fixed: bunx panicking 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() and url.domainToUnicode() now return an empty string for invalid domains instead of throwing a TypeError, matching Node.js behavior.
  • Fixed: Native modules no longer fail with symbol 'napi_register_module_v1' not found when loaded multiple times (e.g., during hot module reloading or when required in both main thread and a worker).
  • Fixed: The node:http server's request.socket._secureEstablished property 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 as never, but now properly falls back to PropertyKey.
  • Fixed: Compatibility issues with @types/node@25 within @types/bun.
  • Fixed: TypeScript type compatibility with @types/node@25.0.2 where the process.noDeprecation property type definition had changed.

Web APIs

  • Fixed: Response.clone() and Request.clone() no longer incorrectly lock the original body when response.body or request.body was accessed before calling clone().

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.stringify now correctly quotes strings ending with colons (e.g., "tin:"), preventing YAML.parse from 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, Y were 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:, or github: dependencies. These sources now require explicit trustedDependencies configuration to execute lifecycle scripts.
  • Fixed: An internal JSC Loader property was inadvertently leaking into node:vm contexts, making it visible in sandboxed environments where it should not be.

Linux Specific Fixes

  • Fixed: Bun.write and fs.copyFile no longer fail on eCryptfs and other encrypted filesystems on Linux.