Commit Graph

4517 Commits

Author SHA1 Message Date
Claude Bot
aa5ea829a2 feat(repl): add shell command support and fix async event loop handling
- Add $`command` syntax for running shell commands in REPL
- Transform $`...` to await Bun.$`...` for shell execution
- Fix event loop handling for async operations by calling autoTick()
- Now properly awaits Bun.sleep(), setTimeout(), and Bun.$ operations
- Add tests for shell commands and Bun.sleep

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 22:11:14 +00:00
Claude Bot
be79d67802 address code review feedback for REPL
- Delete .bun_repl_history and add to .gitignore
- Fix memory leak in getCompletions for property names
- Use vm.waitForPromise instead of manual event loop polling
- Add proper cleanup for stores and arena on init failure
- Remove manual timeout from test helper

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 21:11:55 +00:00
Claude Bot
cf159ae2fb test(repl): add comprehensive tests for bun repl
Add 41 tests covering:
- REPL commands (.exit, .q, .help)
- Expression evaluation (numbers, strings, objects, arrays)
- Variable persistence across lines
- Function and class definitions
- Async/await support
- Built-in APIs (Bun, process, fetch, URL, etc.)
- Error handling (syntax and runtime errors)
- Modern JS features (destructuring, spread, arrow functions, etc.)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 20:51:57 +00:00
Claude Bot
261f429fd1 feat(repl): add JSC-based autocomplete for object properties
The REPL now provides intelligent autocomplete for object properties
by dynamically querying the JSC runtime. When typing `obj.` and pressing
Tab, the REPL will show available properties from the actual object.

Features:
- Property completion for any object (e.g., `Bun.`, `console.`)
- Navigates nested paths (e.g., `Bun.file.`)
- Includes both own properties and prototype chain

Also adds tests for class persistence and destructuring to verify
the full AST transforms work correctly.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 20:44:24 +00:00
Claude Bot
73cd4e92bb feat(repl): integrate full transpiler with replMode for AST transforms
This replaces the simple string-based let/const->var transform with
Bun's full transpiler in replMode, enabling:

- Proper hoisting of let/const/var/function/class declarations
- Top-level await support via async IIFE wrapping
- Result capture in { value: expr } wrapper
- Object literal disambiguation (wrapping { } in parens)
- Class and function declarations that persist across lines

The REPL now properly awaits promises by running the event loop
until they resolve, allowing `await Promise.resolve(42)` to work.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 20:28:02 +00:00
Claude Bot
e6f08624b1 test(repl): add comprehensive tests for bun repl
Add tests covering:
- Basic expression evaluation
- Bun globals availability
- Variable persistence (let, const, function)
- REPL commands (.help, .timing)
- Error handling
- Object and array display
- Multi-statement lines

Note: Top-level await test is skipped until the full REPL transforms
are integrated, which will wrap code containing await in an async IIFE.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 18:40:35 +00:00
Jarred Sumner
3536e422e9 Merge branch 'main' into jarred/repl-mode 2026-01-19 14:09:41 -08:00
robobun
d85d40ea29 fix(ffi): respect C_INCLUDE_PATH and LIBRARY_PATH env vars (#26250)
## Summary
- Make `bun:ffi`'s TinyCC compiler check standard C compiler environment
variables
- Add support for `C_INCLUDE_PATH` (include paths) and `LIBRARY_PATH`
(library paths)
- Fixes compilation on NixOS and other systems that don't use standard
FHS paths

## Test plan
- [x] Added regression test `test/regression/issue/26249.test.ts` that
verifies:
  - Single path in `C_INCLUDE_PATH` works
  - Multiple colon-separated paths in `C_INCLUDE_PATH` work
- [x] Verified test fails with system bun (without fix)
- [x] Verified test passes with debug build (with fix)
- [x] Verified existing `cc.test.ts` tests still pass

Closes #26249

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 11:24:02 -08:00
Jarred Sumner
a47f1555d6 refactor: address code review feedback
- Clarify JSDoc for replMode in bun.d.ts
- Consolidate applySyncTransform and applyAsyncTransform into transformWithHoisting
- Fix b_missing to return E.Missing in createBindingAssignment
- Add documentation comment for repl_mode in options.zig
- Remove unnecessary async modifiers from sync test callbacks

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 0
Claude-Permission-Prompts: 0
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# REPL Transform Fixes and Node.js Parity

## Current Status

The basic `replMode` option is implemented. This plan covers fixes and parity with Node.js REPL.

## Issues to Fix

### 1. Value Wrapper Has Extra Parentheses (CRITICAL)
**Current output:**
```js
({
  __proto__: null,
  value: 42
});
```

**Expected behavior (per Node.js):**
- For **non-async expressions**: Node.js returns `null` (no transform) - the REPL evaluates the expression directly
- For **async expressions**: `(async () => { return { __proto__: null, value: (expr) } })()`

**Solution:**
1. For non-async expressions: Don't wrap in `{ value: expr }` - just return the expression as-is
2. For async expressions: The `{ __proto__: null, value: expr }` is already inside the function after `return`, so no outer parens needed
3. Add inner parens around the expression value for clarity: `{ __proto__: null, value: (expr) }`

### 2. Object Literal Disambiguation (CRITICAL)
**Input:** `{a: 1}` or `{foo: await fetch()}`

**Current:** Parsed as block with labeled statement, NOT object literal

**Solution:** Pre-check input at transpiler layer:
- If code starts with `{` and doesn't end with `;`, try parsing as `(_=CODE)`
- If valid, wrap input as `(CODE)` before processing
- This matches Node.js approach in `repl.js` line 411-414

### 3. Class Declarations Don't Persist to VM Context
**Current:** Uses `let ClassName;` hoisting - doesn't become vm context property

**Node.js behavior:** Also uses `let` - this is a known limitation in Node.js too!

Looking at Node.js `await.js` line 31-37:
```js
ClassDeclaration(node, state, c) {
  state.prepend(node, `${node.id.name}=`);
  ArrayPrototypePush(state.hoistedDeclarationStatements, `let ${node.id.name}; `);
}
```

**Decision:** Use `var` instead of `let` for class hoisting. This makes classes persist to vm context, matching user expectations for REPL behavior. (Different from Node.js which uses `let`)

---

## Implementation Plan

## Usage Example

```typescript
// REPL tool implementation
const transpiler = new Bun.Transpiler({
  loader: "tsx",
  replMode: true,  // NEW OPTION
});

// For each REPL input line:
const transformed = transpiler.transformSync(userInput);

// Execute in persistent VM context
const result = vm.runInContext(transformed, replContext);

// result.value contains the expression result (wrapped to prevent auto-await)
console.log(result.value);
```

## Design Decisions

- **Value wrapper**: Use `{ value: expr }` wrapper like Node.js to prevent auto-awaiting Promise results
- **Static imports**: Keep static imports as-is (Bun handles them natively)
- **Scope**: Full Node.js REPL transform parity

---

## Node.js REPL Transform Behavior (Reference)

From `vendor/node/lib/internal/repl/await.js`:

### 1. Object Literal Detection
```javascript
// {a:1} → ({a:1}) when starts with { and no trailing ;
if (/^\s*{/.test(code) && !/;\s*$/.test(code)) {
  code = `(${code})`;
}
```

### 2. Top-Level Await Transform
```javascript
// Input:  await x
// Output: (async () => { return { value: (await x) } })()

// Input:  var x = await 1
// Output: var x; (async () => { void (x = await 1) })()

// Input:  const x = await 1
// Output: let x; (async () => { void (x = await 1) })()  // const→let

// Input:  function foo() {}  (with await somewhere)
// Output: var foo; (async () => { this.foo = foo; function foo() {} })()

// Input:  class Foo {}  (with await somewhere)
// Output: let Foo; (async () => { Foo=class Foo {} })()
```

### 3. Transform Skipping
Returns `null` (no transform) when:
- No `await` expression present at top level
- Top-level `return` statement exists
- Code is inside async functions/arrow functions/class methods

---

## Implementation Plan

### Fix 1: Remove Extra Parentheses from Value Wrapper

**Problem:** The printer adds `()` around objects at statement start to disambiguate from blocks.

**Solution:** Always use an IIFE wrapper (sync or async) so the object is after `return`:

```js
// Non-async expression (current - BAD)
({ __proto__: null, value: 42 });

// Non-async expression (fixed - GOOD)
(() => { return { __proto__: null, value: 42 } })()

// Non-async with hoisting (fixed - GOOD)
var x;
(() => { void (x = 1); return { __proto__: null, value: x } })()

// Async (already correct)
var x;
(async () => { void (x = await 1); return { __proto__: null, value: x } })()
```

**Files to modify:**
1. `src/ast/P.zig` - `applyReplValueWrapper()` function

**Changes:**
- Remove the simple `{ value: expr }` wrapper approach
- Always use `applyReplAsyncTransform()` style IIFE wrapping, but with `is_async = false` for non-async code
- This ensures the object is always after `return`, avoiding the parentheses issue
- Hoisting still works for both cases

### Fix 2: Object Literal Disambiguation

**File:** `src/bun.js/api/JSTranspiler.zig` - Before parsing

Add pre-processing check:
```zig
// In transformSync, before parsing:
if (config.repl_mode) {
    // Check if input looks like object literal: starts with { and doesn't end with ;
    if (startsWithBrace(source) and !endsWithSemicolon(source)) {
        // Try parsing as expression by wrapping: _=(CODE)
        // If valid, wrap input as (CODE)
        source = wrapAsExpression(source);
    }
}
```

This matches Node.js `isObjectLiteral()` check in `repl/utils.js:786-789`:
```js
function isObjectLiteral(code) {
  return /^\s*{/.test(code) && !/;\s*$/.test(code);
}
```

### Fix 3: Class Declaration Persistence

**File:** `src/ast/P.zig` - `applyReplAsyncTransform()` in the class handling section

Change from:
```zig
// let Foo; (hoisted)
try hoisted_stmts.append(p.s(S.Local{ .kind = .k_let, ... }));
```

To:
```zig
// var Foo; (hoisted) - use var so it becomes context property
try hoisted_stmts.append(p.s(S.Local{ .kind = .k_var, ... }));
// Also add: this.Foo = Foo; assignment after class declaration
```

---

## Node.js REPL Test Cases to Match

From `vendor/node/test/parallel/test-repl-preprocess-top-level-await.js`:

| Input | Expected Output |
|-------|-----------------|
| `await 0` | `(async () => { return { value: (await 0) } })()` |
| `var a = await 1` | `var a; (async () => { void (a = await 1) })()` |
| `let a = await 1` | `let a; (async () => { void (a = await 1) })()` |
| `const a = await 1` | `let a; (async () => { void (a = await 1) })()` |
| `await 0; function foo() {}` | `var foo; (async () => { await 0; this.foo = foo; function foo() {} })()` |
| `await 0; class Foo {}` | `let Foo; (async () => { await 0; Foo=class Foo {} })()` |
| `var {a} = {a:1}, [b] = [1]` | `var a, b; (async () => { void ( ({a} = {a:1}), ([b] = [1])) })()` |

---

## Files to Modify

| File | Changes |
|------|---------|
| `src/ast/P.zig` | Fix value wrapper format, fix class hoisting to use var |
| `src/bun.js/api/JSTranspiler.zig` | Add object literal pre-check |
| `src/ast/js_printer.zig` | May need to check object literal printing |
| `test/js/bun/transpiler/repl-transform.test.ts` | Update tests for exact Node.js parity |

---

## Verification

1. Run Node.js preprocess test cases through Bun's transpiler
2. Verify output matches Node.js exactly (or functionally equivalent)
3. Test with vm.runInContext for variable persistence
4. Test object literal inputs: `{a: 1}`, `{foo: await bar()}`

---

## DEPRECATED - Previous Implementation (Already Done)

### 1. Add `replMode` to Bun.Transpiler API

**File**: `src/bun.js/api/JSTranspiler.zig`

Add to `Config` struct (around line 27-44):
```zig
pub const Config = struct {
    // ... existing fields ...
    repl_mode: bool = false,
```

Parse the option in `Config.fromJS()` (around line 420-430):
```zig
if (try object.getBooleanLoose(globalThis, "replMode")) |flag| {
    this.repl_mode = flag;
}
```

Apply the option in `constructor()` (around line 714-721):
```zig
transpiler.options.repl_mode = config.repl_mode;
```

### 2. Add Feature Flag to Runtime

**File**: `src/runtime.zig` (in `Runtime.Features`)

```zig
/// REPL mode: transforms code for interactive evaluation
/// - Wraps lone object literals `{...}` in parentheses
/// - Hoists variable declarations for REPL persistence
/// - Wraps last expression in { value: expr } for result capture
/// - Assigns functions to context for persistence
repl_mode: bool = false,
```

### 3. Add to BundleOptions

**File**: `src/options.zig`

Add to `BundleOptions` struct:
```zig
repl_mode: bool = false,
```

### 4. Implement REPL Transforms in Parser

**File**: `src/ast/P.zig`

#### 4a. Object Literal Detection (Parser-Level)

In REPL mode, the parser should prefer interpreting ambiguous `{...}` as object literals instead of blocks.

**Location**: `src/ast/parseStmt.zig` in statement parsing

When `repl_mode` is true and the parser sees `{` at the start of a statement:
1. Try parsing as expression statement (object literal) first
2. If that fails, fall back to block statement

This is similar to how JavaScript engines handle REPL input. The parser already has the infrastructure to do this - we just need to change the precedence in REPL mode.

```zig
// In parseStmt when repl_mode is true and we see '{'
if (p.options.features.repl_mode and p.token.tag == .t_open_brace) {
    // Try parsing as expression first
    const saved_state = p.saveState();
    if (p.tryParseExpressionStatement()) |expr_stmt| {
        return expr_stmt;
    }
    p.restoreState(saved_state);
    // Fall back to block statement
    return p.parseBlockStatement();
}
```

This handles:
- `{a: 1}` → parsed as object literal expression
- `{a: 1, b: 2}` → parsed as object literal expression
- `{ let x = 1; }` → fails as expression, parsed as block
- `{ label: break label; }` → fails as expression (break not valid in object), parsed as block

#### 4b. REPL Transform Pass (in toAST after visiting)

Add a new function `applyReplTransforms()` that:

1. **Detect if transform is needed**: Walk AST to check for top-level `await`
2. **Skip transform when**:
   - No `await` at top level
   - Top-level `return` statement exists
3. **When transform IS needed**:
   - Wrap entire code in `(async () => { ... })()`
   - Hoist variable declarations outside the async wrapper
   - Convert `const` to `let` for persistence
   - Wrap last expression in `return { value: (expr) }`
   - Handle function declarations (assign to `this`)
   - Handle class declarations (hoist as `let`)

**Key Logic:**
```zig
fn applyReplTransforms(p: *Parser, stmts: []Stmt) ![]Stmt {
    // 1. Check for top-level await
    const has_await = p.hasTopLevelAwait(stmts);
    const has_return = p.hasTopLevelReturn(stmts);

    if (!has_await or has_return) {
        // Just wrap last expression, no async wrapper needed
        return p.wrapLastExpression(stmts);
    }

    // 2. Collect declarations to hoist
    var hoisted = std.ArrayList(Stmt).init(p.allocator);
    var inner_stmts = std.ArrayList(Stmt).init(p.allocator);

    for (stmts) |stmt| {
        switch (stmt.data) {
            .s_local => |local| {
                // Hoist declaration, convert const→let
                try hoisted.append(p.createHoistedDecl(local));
                // Add assignment expression to inner
                try inner_stmts.append(p.createAssignmentExpr(local));
            },
            .s_function => |func| {
                // var foo; (hoisted)
                try hoisted.append(p.createVarDecl(func.name));
                // this.foo = foo; function foo() {} (inner)
                try inner_stmts.append(p.createThisAssignment(func.name));
                try inner_stmts.append(stmt);
            },
            .s_class => |class| {
                // let Foo; (hoisted)
                try hoisted.append(p.createLetDecl(class.name));
                // Foo = class Foo {} (inner)
                try inner_stmts.append(p.createClassAssignment(class));
            },
            else => try inner_stmts.append(stmt),
        }
    }

    // 3. Wrap last expression in return { value: expr }
    p.wrapLastExpressionWithReturn(&inner_stmts);

    // 4. Create async IIFE: (async () => { ...inner... })()
    const async_iife = p.createAsyncIIFE(inner_stmts.items);

    // 5. Combine: hoisted declarations + async IIFE
    try hoisted.append(async_iife);
    return hoisted.toOwnedSlice();
}
```

### 5. TypeScript Type Definitions

**File**: `packages/bun-types/bun.d.ts`

Add to `TranspilerOptions` interface (around line 1748):
```typescript
interface TranspilerOptions {
  // ... existing options ...

  /**
   * Enable REPL mode transforms:
   * - Wraps object literals in parentheses
   * - Hoists declarations for REPL persistence
   * - Wraps last expression in { value: expr } for result capture
   * - Wraps code with await in async IIFE
   */
  replMode?: boolean;
}
```

---

## Files to Modify

| File | Changes |
|------|---------|
| `src/bun.js/api/JSTranspiler.zig` | Add `repl_mode` to Config, parse from JS, apply to transpiler |
| `src/runtime.zig` | Add `repl_mode: bool` to `Runtime.Features` |
| `src/options.zig` | Add `repl_mode: bool` to `BundleOptions` |
| `src/ast/P.zig` | REPL transform pass in `toAST()` |
| `src/ast/parseStmt.zig` | Object literal vs block disambiguation in REPL mode |
| `packages/bun-types/bun.d.ts` | Add `replMode?: boolean` to `TranspilerOptions` |

---

## Test Cases

Create test file: `test/js/bun/transpiler/repl-transform.test.ts`

### Part 1: Transform Output Tests (Unit Tests)

Test exact transformation output matches expected patterns:

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("Bun.Transpiler replMode - Transform Output", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  // Based on Node.js test-repl-preprocess-top-level-await.js
  const testCases: [string, string | null][] = [
    // No await = null (no async transform, but still expression capture)
    ['0', null],

    // Basic await
    ['await 0', '(async () => { return { value: (await 0) } })()'],
    ['await 0;', '(async () => { return { value: (await 0) }; })()'],
    ['(await 0)', '(async () => { return ({ value: (await 0) }) })()'],

    // No transform for await inside async functions
    ['async function foo() { await 0; }', null],
    ['async () => await 0', null],
    ['class A { async method() { await 0 } }', null],

    // Top-level return = no transform
    ['await 0; return 0;', null],

    // Multiple await - last one gets return wrapper
    ['await 1; await 2;', '(async () => { await 1; return { value: (await 2) }; })()'],

    // Variable hoisting - var
    ['var a = await 1', 'var a; (async () => { void (a = await 1) })()'],

    // Variable hoisting - let
    ['let a = await 1', 'let a; (async () => { void (a = await 1) })()'],

    // Variable hoisting - const becomes let
    ['const a = await 1', 'let a; (async () => { void (a = await 1) })()'],

    // For loop with var - hoist var
    ['for (var i = 0; i < 1; ++i) { await i }',
     'var i; (async () => { for (void (i = 0); i < 1; ++i) { await i } })()'],

    // For loop with let - no hoist
    ['for (let i = 0; i < 1; ++i) { await i }',
     '(async () => { for (let i = 0; i < 1; ++i) { await i } })()'],

    // Destructuring with var
    ['var {a} = {a:1}, [b] = [1], {c:{d}} = {c:{d: await 1}}',
     'var a, b, d; (async () => { void ( ({a} = {a:1}), ([b] = [1]), ({c:{d}} = {c:{d: await 1}})) })()'],

    // Destructuring with let
    ['let [a, b, c] = await ([1, 2, 3])',
     'let a, b, c; (async () => { void ([a, b, c] = await ([1, 2, 3])) })()'],

    // Function declarations - assign to this
    ['await 0; function foo() {}',
     'var foo; (async () => { await 0; this.foo = foo; function foo() {} })()'],

    // Class declarations - hoist as let
    ['await 0; class Foo {}',
     'let Foo; (async () => { await 0; Foo=class Foo {} })()'],

    // Nested scopes
    ['if (await true) { var a = 1; }',
     'var a; (async () => { if (await true) { void (a = 1); } })()'],
    ['if (await true) { let a = 1; }',
     '(async () => { if (await true) { let a = 1; } })()'],

    // Mixed declarations
    ['var a = await 1; let b = 2; const c = 3;',
     'var a; let b; let c; (async () => { void (a = await 1); void (b = 2); void (c = 3); })()'],

    // for await
    ['for await (var i of asyncIterable) { i; }',
     'var i; (async () => { for await (i of asyncIterable) { i; } })()'],

    // for-of with var
    ['for (var i of [1,2,3]) { await 1; }',
     'var i; (async () => { for (i of [1,2,3]) { await 1; } })()'],

    // for-in with var
    ['for (var i in {x:1}) { await 1 }',
     'var i; (async () => { for (i in {x:1}) { await 1 } })()'],

    // Spread in destructuring
    ['var { ...rest } = await {}',
     'var rest; (async () => { void ({ ...rest } = await {}) })()'],
  ];

  for (const [input, expected] of testCases) {
    test(`transform: ${input.slice(0, 40)}...`, () => {
      const result = transpiler.transformSync(input);
      if (expected === null) {
        // No async transform expected, but expression capture may still happen
        expect(result).not.toMatch(/^\(async/);
      } else {
        expect(result.trim()).toBe(expected);
      }
    });
  }

  // Object literal detection - parser handles this automatically in REPL mode
  describe("object literal vs block disambiguation", () => {
    test("{a: 1} parsed as object literal", async () => {
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{a: 1}");
      const result = await vm.runInContext(code, ctx);
      // Should evaluate to object, not undefined (block with label)
      expect(result.value).toEqual({ a: 1 });
    });

    test("{a: 1, b: 2} parsed as object literal", async () => {
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{a: 1, b: 2}");
      const result = await vm.runInContext(code, ctx);
      expect(result.value).toEqual({ a: 1, b: 2 });
    });

    test("{ let x = 1; x } parsed as block", async () => {
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{ let x = 1; x }");
      const result = await vm.runInContext(code, ctx);
      // Block returns last expression value
      expect(result.value).toBe(1);
    });

    test("{ x: 1; y: 2 } parsed as block with labels", async () => {
      // Semicolons make this a block with labeled statements, not object
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{ x: 1; y: 2 }");
      const result = await vm.runInContext(code, ctx);
      // Block with labels returns last value
      expect(result.value).toBe(2);
    });
  });
});
```

### Part 2: Variable Persistence Tests (Integration with node:vm)

Test that variables persist across multiple REPL evaluations:

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("REPL Variable Persistence", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  // Helper to run multiple REPL lines in sequence
  async function runReplSession(lines: string[], context?: object) {
    const ctx = vm.createContext(context ?? { console });
    const results: any[] = [];

    for (const line of lines) {
      const transformed = transpiler.transformSync(line);
      const result = await vm.runInContext(transformed, ctx);
      results.push(result?.value ?? result);
    }

    return { results, context: ctx };
  }

  test("var persists across lines", async () => {
    const { results, context } = await runReplSession([
      "var x = 10",
      "x + 5",
      "x = 20",
      "x",
    ]);

    expect(results[1]).toBe(15);  // x + 5
    expect(results[3]).toBe(20);  // x after reassignment
    expect(context.x).toBe(20);   // x visible in context
  });

  test("let persists across lines (hoisted)", async () => {
    const { results } = await runReplSession([
      "let y = await Promise.resolve(100)",
      "y * 2",
    ]);

    expect(results[1]).toBe(200);
  });

  test("const becomes let, can be reassigned in later lines", async () => {
    const { results } = await runReplSession([
      "const z = await Promise.resolve(5)",
      "z",
      // In REPL, const becomes let, so next line can redeclare
      "z = 10",  // This works because const→let
      "z",
    ]);

    expect(results[1]).toBe(5);
    expect(results[3]).toBe(10);
  });

  test("function declarations persist", async () => {
    const { results, context } = await runReplSession([
      "await 1; function add(a, b) { return a + b; }",
      "add(2, 3)",
      "function multiply(a, b) { return a * b; }",  // no await
      "multiply(4, 5)",
    ]);

    expect(results[1]).toBe(5);
    expect(results[3]).toBe(20);
    expect(typeof context.add).toBe("function");
    expect(typeof context.multiply).toBe("function");
  });

  test("class declarations persist", async () => {
    const { results, context } = await runReplSession([
      "await 1; class Counter { constructor() { this.count = 0; } inc() { this.count++; } }",
      "const c = new Counter()",
      "c.inc(); c.inc(); c.count",
    ]);

    expect(results[2]).toBe(2);
    expect(typeof context.Counter).toBe("function");
  });

  test("complex session with mixed declarations", async () => {
    const { results } = await runReplSession([
      "var total = 0",
      "async function addAsync(n) { return total += await Promise.resolve(n); }",
      "await addAsync(10)",
      "await addAsync(20)",
      "total",
    ]);

    expect(results[2]).toBe(10);
    expect(results[3]).toBe(30);
    expect(results[4]).toBe(30);
  });

  test("destructuring assignment persists", async () => {
    const { results, context } = await runReplSession([
      "var { a, b } = await Promise.resolve({ a: 1, b: 2 })",
      "a + b",
      "var [x, y, z] = [10, 20, 30]",
      "x + y + z",
    ]);

    expect(results[1]).toBe(3);
    expect(results[3]).toBe(60);
    expect(context.a).toBe(1);
    expect(context.x).toBe(10);
  });
});
```

### Part 3: eval() Scoping Semantics Tests

Test that REPL behaves like eval() with proper scoping:

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("REPL eval() Scoping Semantics", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  test("var hoists to global context", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("var globalVar = 42");
    await vm.runInContext(code, ctx);

    expect(ctx.globalVar).toBe(42);
  });

  test("let/const hoisted for REPL but scoped correctly", async () => {
    const ctx = vm.createContext({});

    // With await, let is hoisted outside async wrapper
    const code1 = transpiler.transformSync("let x = await 1");
    await vm.runInContext(code1, ctx);
    expect(ctx.x).toBe(1);

    // Without await, let behavior depends on implementation
    const code2 = transpiler.transformSync("let y = 2");
    await vm.runInContext(code2, ctx);
    // y should still be accessible in REPL context
  });

  test("block-scoped let does NOT leak", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("if (await true) { let blockScoped = 1; }");
    await vm.runInContext(code, ctx);

    // blockScoped should NOT be visible in context
    expect(ctx.blockScoped).toBeUndefined();
  });

  test("function in block hoists with var (sloppy mode)", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("if (await true) { function blockFn() { return 42; } }");
    await vm.runInContext(code, ctx);

    // In sloppy mode, function in block hoists to function scope
    expect(typeof ctx.blockFn).toBe("function");
    expect(ctx.blockFn()).toBe(42);
  });

  test("this binding in function declarations", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("await 1; function greet() { return 'hello'; }");
    await vm.runInContext(code, ctx);

    // Function should be assigned to this (context) for REPL persistence
    expect(ctx.greet()).toBe("hello");
  });

  test("async function expression captures result", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("await (async () => { return 42; })()");
    const result = await vm.runInContext(code, ctx);

    expect(result.value).toBe(42);
  });

  test("Promise result NOT auto-awaited due to { value: } wrapper", async () => {
    const ctx = vm.createContext({});

    // Without wrapper, result would be auto-awaited
    // With { value: } wrapper, we get the Promise object
    const code = transpiler.transformSync("await Promise.resolve(Promise.resolve(42))");
    const result = await vm.runInContext(code, ctx);

    // The inner Promise should be in value, not auto-resolved
    expect(result.value).toBeInstanceOf(Promise);
    expect(await result.value).toBe(42);
  });
});
```

### Part 4: Edge Cases and Error Handling

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("REPL Edge Cases", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  test("empty input", () => {
    const result = transpiler.transformSync("");
    expect(result).toBe("");
  });

  test("whitespace only", () => {
    const result = transpiler.transformSync("   \n\t  ");
    expect(result.trim()).toBe("");
  });

  test("comment only", () => {
    const result = transpiler.transformSync("// just a comment");
    expect(result).toContain("// just a comment");
  });

  test("multiline input", () => {
    const input = `
      var x = await 1;
      var y = await 2;
      x + y
    `;
    const result = transpiler.transformSync(input);
    expect(result).toContain("var x");
    expect(result).toContain("var y");
    expect(result).toContain("async");
  });

  test("TypeScript syntax", () => {
    const input = "const x: number = await Promise.resolve(42)";
    const result = transpiler.transformSync(input);
    expect(result).not.toContain(": number"); // Types stripped
    expect(result).toContain("let x");
  });

  test("JSX in REPL", () => {
    const input = "await Promise.resolve(<div>Hello</div>)";
    const result = transpiler.transformSync(input);
    expect(result).toContain("async");
  });

  test("import expression (dynamic)", () => {
    // Dynamic imports should work fine
    const input = "await import('fs')";
    const result = transpiler.transformSync(input);
    expect(result).toContain("import");
  });

  test("nested await expressions", () => {
    const input = "await (await Promise.resolve(Promise.resolve(1)))";
    const result = transpiler.transformSync(input);
    expect(result).toContain("{ value:");
  });

  test("for-await-of", () => {
    const input = `
      async function* gen() { yield 1; yield 2; }
      for await (const x of gen()) { console.log(x); }
    `;
    const result = transpiler.transformSync(input);
    expect(result).toContain("var gen");
    expect(result).toContain("for await");
  });
});
```

---

## Verification Plan

### 1. Build and Basic Tests
```bash
# Build Bun with changes
bun bd

# Run the REPL transform tests
bun bd test test/js/bun/transpiler/repl-transform.test.ts
```

### 2. Manual Transform Output Verification
```typescript
// test-repl-manual.ts
const t = new Bun.Transpiler({ loader: "tsx", replMode: true });

// Object literal
console.log("Object literal:");
console.log(t.transformSync("{a: 1}"));
// Expected: contains "({a: 1})"

// Basic await
console.log("\nBasic await:");
console.log(t.transformSync("await 0"));
// Expected: (async () => { return { value: (await 0) } })()

// Variable hoisting
console.log("\nVar hoisting:");
console.log(t.transformSync("var x = await 1"));
// Expected: var x; (async () => { void (x = await 1) })()

// const → let
console.log("\nConst to let:");
console.log(t.transformSync("const x = await 1"));
// Expected: let x; (async () => { void (x = await 1) })()

// Function hoisting
console.log("\nFunction:");
console.log(t.transformSync("await 0; function foo() {}"));
// Expected: var foo; (async () => { await 0; this.foo = foo; function foo() {} })()
```

### 3. Full REPL Session Simulation
```typescript
// test-repl-session.ts
import vm from "node:vm";

const t = new Bun.Transpiler({ loader: "tsx", replMode: true });
const ctx = vm.createContext({ console, Promise });

async function repl(code: string) {
  const transformed = t.transformSync(code);
  console.log(`> ${code}`);
  console.log(`[transformed]: ${transformed}`);
  const result = await vm.runInContext(transformed, ctx);
  console.log(`= ${JSON.stringify(result?.value ?? result)}\n`);
  return result?.value ?? result;
}

// Test session
await repl("var counter = 0");
await repl("function increment() { return ++counter; }");
await repl("increment()");  // Should be 1
await repl("increment()");  // Should be 2
await repl("counter");      // Should be 2

await repl("const data = await Promise.resolve({ x: 10, y: 20 })");
await repl("data.x + data.y");  // Should be 30

await repl("class Point { constructor(x, y) { this.x = x; this.y = y; } }");
await repl("const p = new Point(3, 4)");
await repl("Math.sqrt(p.x**2 + p.y**2)");  // Should be 5
```

### 4. Verify No Regressions
```bash
# Run existing transpiler tests
bun bd test test/js/bun/transpiler/

# Run existing vm tests
bun bd test test/js/node/vm/
```

### 5. Cross-check with Node.js (Optional)
Compare transform outputs with Node.js's `processTopLevelAwait`:
```typescript
// Compare a few key transforms with Node.js output
const cases = [
  "await 0",
  "var x = await 1",
  "await 0; function foo() {}",
];
// Verify Bun output matches Node.js patterns
```
</claude-plan>
2026-01-19 00:35:49 -08:00
Jarred Sumner
84ef7598fd fix: hoist all declarations as var for context property persistence
In REPL mode, var declarations at the top level become properties of
the vm context object, while let/const do not. Change all hoisting to
use var so that all variables are accessible via context.varName.

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 0
Claude-Permission-Prompts: 0
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# REPL Transform Fixes and Node.js Parity

## Current Status

The basic `replMode` option is implemented. This plan covers fixes and parity with Node.js REPL.

## Issues to Fix

### 1. Value Wrapper Has Extra Parentheses (CRITICAL)
**Current output:**
```js
({
  __proto__: null,
  value: 42
});
```

**Expected behavior (per Node.js):**
- For **non-async expressions**: Node.js returns `null` (no transform) - the REPL evaluates the expression directly
- For **async expressions**: `(async () => { return { __proto__: null, value: (expr) } })()`

**Solution:**
1. For non-async expressions: Don't wrap in `{ value: expr }` - just return the expression as-is
2. For async expressions: The `{ __proto__: null, value: expr }` is already inside the function after `return`, so no outer parens needed
3. Add inner parens around the expression value for clarity: `{ __proto__: null, value: (expr) }`

### 2. Object Literal Disambiguation (CRITICAL)
**Input:** `{a: 1}` or `{foo: await fetch()}`

**Current:** Parsed as block with labeled statement, NOT object literal

**Solution:** Pre-check input at transpiler layer:
- If code starts with `{` and doesn't end with `;`, try parsing as `(_=CODE)`
- If valid, wrap input as `(CODE)` before processing
- This matches Node.js approach in `repl.js` line 411-414

### 3. Class Declarations Don't Persist to VM Context
**Current:** Uses `let ClassName;` hoisting - doesn't become vm context property

**Node.js behavior:** Also uses `let` - this is a known limitation in Node.js too!

Looking at Node.js `await.js` line 31-37:
```js
ClassDeclaration(node, state, c) {
  state.prepend(node, `${node.id.name}=`);
  ArrayPrototypePush(state.hoistedDeclarationStatements, `let ${node.id.name}; `);
}
```

**Decision:** Use `var` instead of `let` for class hoisting. This makes classes persist to vm context, matching user expectations for REPL behavior. (Different from Node.js which uses `let`)

---

## Implementation Plan

## Usage Example

```typescript
// REPL tool implementation
const transpiler = new Bun.Transpiler({
  loader: "tsx",
  replMode: true,  // NEW OPTION
});

// For each REPL input line:
const transformed = transpiler.transformSync(userInput);

// Execute in persistent VM context
const result = vm.runInContext(transformed, replContext);

// result.value contains the expression result (wrapped to prevent auto-await)
console.log(result.value);
```

## Design Decisions

- **Value wrapper**: Use `{ value: expr }` wrapper like Node.js to prevent auto-awaiting Promise results
- **Static imports**: Keep static imports as-is (Bun handles them natively)
- **Scope**: Full Node.js REPL transform parity

---

## Node.js REPL Transform Behavior (Reference)

From `vendor/node/lib/internal/repl/await.js`:

### 1. Object Literal Detection
```javascript
// {a:1} → ({a:1}) when starts with { and no trailing ;
if (/^\s*{/.test(code) && !/;\s*$/.test(code)) {
  code = `(${code})`;
}
```

### 2. Top-Level Await Transform
```javascript
// Input:  await x
// Output: (async () => { return { value: (await x) } })()

// Input:  var x = await 1
// Output: var x; (async () => { void (x = await 1) })()

// Input:  const x = await 1
// Output: let x; (async () => { void (x = await 1) })()  // const→let

// Input:  function foo() {}  (with await somewhere)
// Output: var foo; (async () => { this.foo = foo; function foo() {} })()

// Input:  class Foo {}  (with await somewhere)
// Output: let Foo; (async () => { Foo=class Foo {} })()
```

### 3. Transform Skipping
Returns `null` (no transform) when:
- No `await` expression present at top level
- Top-level `return` statement exists
- Code is inside async functions/arrow functions/class methods

---

## Implementation Plan

### Fix 1: Remove Extra Parentheses from Value Wrapper

**Problem:** The printer adds `()` around objects at statement start to disambiguate from blocks.

**Solution:** Always use an IIFE wrapper (sync or async) so the object is after `return`:

```js
// Non-async expression (current - BAD)
({ __proto__: null, value: 42 });

// Non-async expression (fixed - GOOD)
(() => { return { __proto__: null, value: 42 } })()

// Non-async with hoisting (fixed - GOOD)
var x;
(() => { void (x = 1); return { __proto__: null, value: x } })()

// Async (already correct)
var x;
(async () => { void (x = await 1); return { __proto__: null, value: x } })()
```

**Files to modify:**
1. `src/ast/P.zig` - `applyReplValueWrapper()` function

**Changes:**
- Remove the simple `{ value: expr }` wrapper approach
- Always use `applyReplAsyncTransform()` style IIFE wrapping, but with `is_async = false` for non-async code
- This ensures the object is always after `return`, avoiding the parentheses issue
- Hoisting still works for both cases

### Fix 2: Object Literal Disambiguation

**File:** `src/bun.js/api/JSTranspiler.zig` - Before parsing

Add pre-processing check:
```zig
// In transformSync, before parsing:
if (config.repl_mode) {
    // Check if input looks like object literal: starts with { and doesn't end with ;
    if (startsWithBrace(source) and !endsWithSemicolon(source)) {
        // Try parsing as expression by wrapping: _=(CODE)
        // If valid, wrap input as (CODE)
        source = wrapAsExpression(source);
    }
}
```

This matches Node.js `isObjectLiteral()` check in `repl/utils.js:786-789`:
```js
function isObjectLiteral(code) {
  return /^\s*{/.test(code) && !/;\s*$/.test(code);
}
```

### Fix 3: Class Declaration Persistence

**File:** `src/ast/P.zig` - `applyReplAsyncTransform()` in the class handling section

Change from:
```zig
// let Foo; (hoisted)
try hoisted_stmts.append(p.s(S.Local{ .kind = .k_let, ... }));
```

To:
```zig
// var Foo; (hoisted) - use var so it becomes context property
try hoisted_stmts.append(p.s(S.Local{ .kind = .k_var, ... }));
// Also add: this.Foo = Foo; assignment after class declaration
```

---

## Node.js REPL Test Cases to Match

From `vendor/node/test/parallel/test-repl-preprocess-top-level-await.js`:

| Input | Expected Output |
|-------|-----------------|
| `await 0` | `(async () => { return { value: (await 0) } })()` |
| `var a = await 1` | `var a; (async () => { void (a = await 1) })()` |
| `let a = await 1` | `let a; (async () => { void (a = await 1) })()` |
| `const a = await 1` | `let a; (async () => { void (a = await 1) })()` |
| `await 0; function foo() {}` | `var foo; (async () => { await 0; this.foo = foo; function foo() {} })()` |
| `await 0; class Foo {}` | `let Foo; (async () => { await 0; Foo=class Foo {} })()` |
| `var {a} = {a:1}, [b] = [1]` | `var a, b; (async () => { void ( ({a} = {a:1}), ([b] = [1])) })()` |

---

## Files to Modify

| File | Changes |
|------|---------|
| `src/ast/P.zig` | Fix value wrapper format, fix class hoisting to use var |
| `src/bun.js/api/JSTranspiler.zig` | Add object literal pre-check |
| `src/ast/js_printer.zig` | May need to check object literal printing |
| `test/js/bun/transpiler/repl-transform.test.ts` | Update tests for exact Node.js parity |

---

## Verification

1. Run Node.js preprocess test cases through Bun's transpiler
2. Verify output matches Node.js exactly (or functionally equivalent)
3. Test with vm.runInContext for variable persistence
4. Test object literal inputs: `{a: 1}`, `{foo: await bar()}`

---

## DEPRECATED - Previous Implementation (Already Done)

### 1. Add `replMode` to Bun.Transpiler API

**File**: `src/bun.js/api/JSTranspiler.zig`

Add to `Config` struct (around line 27-44):
```zig
pub const Config = struct {
    // ... existing fields ...
    repl_mode: bool = false,
```

Parse the option in `Config.fromJS()` (around line 420-430):
```zig
if (try object.getBooleanLoose(globalThis, "replMode")) |flag| {
    this.repl_mode = flag;
}
```

Apply the option in `constructor()` (around line 714-721):
```zig
transpiler.options.repl_mode = config.repl_mode;
```

### 2. Add Feature Flag to Runtime

**File**: `src/runtime.zig` (in `Runtime.Features`)

```zig
/// REPL mode: transforms code for interactive evaluation
/// - Wraps lone object literals `{...}` in parentheses
/// - Hoists variable declarations for REPL persistence
/// - Wraps last expression in { value: expr } for result capture
/// - Assigns functions to context for persistence
repl_mode: bool = false,
```

### 3. Add to BundleOptions

**File**: `src/options.zig`

Add to `BundleOptions` struct:
```zig
repl_mode: bool = false,
```

### 4. Implement REPL Transforms in Parser

**File**: `src/ast/P.zig`

#### 4a. Object Literal Detection (Parser-Level)

In REPL mode, the parser should prefer interpreting ambiguous `{...}` as object literals instead of blocks.

**Location**: `src/ast/parseStmt.zig` in statement parsing

When `repl_mode` is true and the parser sees `{` at the start of a statement:
1. Try parsing as expression statement (object literal) first
2. If that fails, fall back to block statement

This is similar to how JavaScript engines handle REPL input. The parser already has the infrastructure to do this - we just need to change the precedence in REPL mode.

```zig
// In parseStmt when repl_mode is true and we see '{'
if (p.options.features.repl_mode and p.token.tag == .t_open_brace) {
    // Try parsing as expression first
    const saved_state = p.saveState();
    if (p.tryParseExpressionStatement()) |expr_stmt| {
        return expr_stmt;
    }
    p.restoreState(saved_state);
    // Fall back to block statement
    return p.parseBlockStatement();
}
```

This handles:
- `{a: 1}` → parsed as object literal expression
- `{a: 1, b: 2}` → parsed as object literal expression
- `{ let x = 1; }` → fails as expression, parsed as block
- `{ label: break label; }` → fails as expression (break not valid in object), parsed as block

#### 4b. REPL Transform Pass (in toAST after visiting)

Add a new function `applyReplTransforms()` that:

1. **Detect if transform is needed**: Walk AST to check for top-level `await`
2. **Skip transform when**:
   - No `await` at top level
   - Top-level `return` statement exists
3. **When transform IS needed**:
   - Wrap entire code in `(async () => { ... })()`
   - Hoist variable declarations outside the async wrapper
   - Convert `const` to `let` for persistence
   - Wrap last expression in `return { value: (expr) }`
   - Handle function declarations (assign to `this`)
   - Handle class declarations (hoist as `let`)

**Key Logic:**
```zig
fn applyReplTransforms(p: *Parser, stmts: []Stmt) ![]Stmt {
    // 1. Check for top-level await
    const has_await = p.hasTopLevelAwait(stmts);
    const has_return = p.hasTopLevelReturn(stmts);

    if (!has_await or has_return) {
        // Just wrap last expression, no async wrapper needed
        return p.wrapLastExpression(stmts);
    }

    // 2. Collect declarations to hoist
    var hoisted = std.ArrayList(Stmt).init(p.allocator);
    var inner_stmts = std.ArrayList(Stmt).init(p.allocator);

    for (stmts) |stmt| {
        switch (stmt.data) {
            .s_local => |local| {
                // Hoist declaration, convert const→let
                try hoisted.append(p.createHoistedDecl(local));
                // Add assignment expression to inner
                try inner_stmts.append(p.createAssignmentExpr(local));
            },
            .s_function => |func| {
                // var foo; (hoisted)
                try hoisted.append(p.createVarDecl(func.name));
                // this.foo = foo; function foo() {} (inner)
                try inner_stmts.append(p.createThisAssignment(func.name));
                try inner_stmts.append(stmt);
            },
            .s_class => |class| {
                // let Foo; (hoisted)
                try hoisted.append(p.createLetDecl(class.name));
                // Foo = class Foo {} (inner)
                try inner_stmts.append(p.createClassAssignment(class));
            },
            else => try inner_stmts.append(stmt),
        }
    }

    // 3. Wrap last expression in return { value: expr }
    p.wrapLastExpressionWithReturn(&inner_stmts);

    // 4. Create async IIFE: (async () => { ...inner... })()
    const async_iife = p.createAsyncIIFE(inner_stmts.items);

    // 5. Combine: hoisted declarations + async IIFE
    try hoisted.append(async_iife);
    return hoisted.toOwnedSlice();
}
```

### 5. TypeScript Type Definitions

**File**: `packages/bun-types/bun.d.ts`

Add to `TranspilerOptions` interface (around line 1748):
```typescript
interface TranspilerOptions {
  // ... existing options ...

  /**
   * Enable REPL mode transforms:
   * - Wraps object literals in parentheses
   * - Hoists declarations for REPL persistence
   * - Wraps last expression in { value: expr } for result capture
   * - Wraps code with await in async IIFE
   */
  replMode?: boolean;
}
```

---

## Files to Modify

| File | Changes |
|------|---------|
| `src/bun.js/api/JSTranspiler.zig` | Add `repl_mode` to Config, parse from JS, apply to transpiler |
| `src/runtime.zig` | Add `repl_mode: bool` to `Runtime.Features` |
| `src/options.zig` | Add `repl_mode: bool` to `BundleOptions` |
| `src/ast/P.zig` | REPL transform pass in `toAST()` |
| `src/ast/parseStmt.zig` | Object literal vs block disambiguation in REPL mode |
| `packages/bun-types/bun.d.ts` | Add `replMode?: boolean` to `TranspilerOptions` |

---

## Test Cases

Create test file: `test/js/bun/transpiler/repl-transform.test.ts`

### Part 1: Transform Output Tests (Unit Tests)

Test exact transformation output matches expected patterns:

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("Bun.Transpiler replMode - Transform Output", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  // Based on Node.js test-repl-preprocess-top-level-await.js
  const testCases: [string, string | null][] = [
    // No await = null (no async transform, but still expression capture)
    ['0', null],

    // Basic await
    ['await 0', '(async () => { return { value: (await 0) } })()'],
    ['await 0;', '(async () => { return { value: (await 0) }; })()'],
    ['(await 0)', '(async () => { return ({ value: (await 0) }) })()'],

    // No transform for await inside async functions
    ['async function foo() { await 0; }', null],
    ['async () => await 0', null],
    ['class A { async method() { await 0 } }', null],

    // Top-level return = no transform
    ['await 0; return 0;', null],

    // Multiple await - last one gets return wrapper
    ['await 1; await 2;', '(async () => { await 1; return { value: (await 2) }; })()'],

    // Variable hoisting - var
    ['var a = await 1', 'var a; (async () => { void (a = await 1) })()'],

    // Variable hoisting - let
    ['let a = await 1', 'let a; (async () => { void (a = await 1) })()'],

    // Variable hoisting - const becomes let
    ['const a = await 1', 'let a; (async () => { void (a = await 1) })()'],

    // For loop with var - hoist var
    ['for (var i = 0; i < 1; ++i) { await i }',
     'var i; (async () => { for (void (i = 0); i < 1; ++i) { await i } })()'],

    // For loop with let - no hoist
    ['for (let i = 0; i < 1; ++i) { await i }',
     '(async () => { for (let i = 0; i < 1; ++i) { await i } })()'],

    // Destructuring with var
    ['var {a} = {a:1}, [b] = [1], {c:{d}} = {c:{d: await 1}}',
     'var a, b, d; (async () => { void ( ({a} = {a:1}), ([b] = [1]), ({c:{d}} = {c:{d: await 1}})) })()'],

    // Destructuring with let
    ['let [a, b, c] = await ([1, 2, 3])',
     'let a, b, c; (async () => { void ([a, b, c] = await ([1, 2, 3])) })()'],

    // Function declarations - assign to this
    ['await 0; function foo() {}',
     'var foo; (async () => { await 0; this.foo = foo; function foo() {} })()'],

    // Class declarations - hoist as let
    ['await 0; class Foo {}',
     'let Foo; (async () => { await 0; Foo=class Foo {} })()'],

    // Nested scopes
    ['if (await true) { var a = 1; }',
     'var a; (async () => { if (await true) { void (a = 1); } })()'],
    ['if (await true) { let a = 1; }',
     '(async () => { if (await true) { let a = 1; } })()'],

    // Mixed declarations
    ['var a = await 1; let b = 2; const c = 3;',
     'var a; let b; let c; (async () => { void (a = await 1); void (b = 2); void (c = 3); })()'],

    // for await
    ['for await (var i of asyncIterable) { i; }',
     'var i; (async () => { for await (i of asyncIterable) { i; } })()'],

    // for-of with var
    ['for (var i of [1,2,3]) { await 1; }',
     'var i; (async () => { for (i of [1,2,3]) { await 1; } })()'],

    // for-in with var
    ['for (var i in {x:1}) { await 1 }',
     'var i; (async () => { for (i in {x:1}) { await 1 } })()'],

    // Spread in destructuring
    ['var { ...rest } = await {}',
     'var rest; (async () => { void ({ ...rest } = await {}) })()'],
  ];

  for (const [input, expected] of testCases) {
    test(`transform: ${input.slice(0, 40)}...`, () => {
      const result = transpiler.transformSync(input);
      if (expected === null) {
        // No async transform expected, but expression capture may still happen
        expect(result).not.toMatch(/^\(async/);
      } else {
        expect(result.trim()).toBe(expected);
      }
    });
  }

  // Object literal detection - parser handles this automatically in REPL mode
  describe("object literal vs block disambiguation", () => {
    test("{a: 1} parsed as object literal", async () => {
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{a: 1}");
      const result = await vm.runInContext(code, ctx);
      // Should evaluate to object, not undefined (block with label)
      expect(result.value).toEqual({ a: 1 });
    });

    test("{a: 1, b: 2} parsed as object literal", async () => {
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{a: 1, b: 2}");
      const result = await vm.runInContext(code, ctx);
      expect(result.value).toEqual({ a: 1, b: 2 });
    });

    test("{ let x = 1; x } parsed as block", async () => {
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{ let x = 1; x }");
      const result = await vm.runInContext(code, ctx);
      // Block returns last expression value
      expect(result.value).toBe(1);
    });

    test("{ x: 1; y: 2 } parsed as block with labels", async () => {
      // Semicolons make this a block with labeled statements, not object
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{ x: 1; y: 2 }");
      const result = await vm.runInContext(code, ctx);
      // Block with labels returns last value
      expect(result.value).toBe(2);
    });
  });
});
```

### Part 2: Variable Persistence Tests (Integration with node:vm)

Test that variables persist across multiple REPL evaluations:

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("REPL Variable Persistence", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  // Helper to run multiple REPL lines in sequence
  async function runReplSession(lines: string[], context?: object) {
    const ctx = vm.createContext(context ?? { console });
    const results: any[] = [];

    for (const line of lines) {
      const transformed = transpiler.transformSync(line);
      const result = await vm.runInContext(transformed, ctx);
      results.push(result?.value ?? result);
    }

    return { results, context: ctx };
  }

  test("var persists across lines", async () => {
    const { results, context } = await runReplSession([
      "var x = 10",
      "x + 5",
      "x = 20",
      "x",
    ]);

    expect(results[1]).toBe(15);  // x + 5
    expect(results[3]).toBe(20);  // x after reassignment
    expect(context.x).toBe(20);   // x visible in context
  });

  test("let persists across lines (hoisted)", async () => {
    const { results } = await runReplSession([
      "let y = await Promise.resolve(100)",
      "y * 2",
    ]);

    expect(results[1]).toBe(200);
  });

  test("const becomes let, can be reassigned in later lines", async () => {
    const { results } = await runReplSession([
      "const z = await Promise.resolve(5)",
      "z",
      // In REPL, const becomes let, so next line can redeclare
      "z = 10",  // This works because const→let
      "z",
    ]);

    expect(results[1]).toBe(5);
    expect(results[3]).toBe(10);
  });

  test("function declarations persist", async () => {
    const { results, context } = await runReplSession([
      "await 1; function add(a, b) { return a + b; }",
      "add(2, 3)",
      "function multiply(a, b) { return a * b; }",  // no await
      "multiply(4, 5)",
    ]);

    expect(results[1]).toBe(5);
    expect(results[3]).toBe(20);
    expect(typeof context.add).toBe("function");
    expect(typeof context.multiply).toBe("function");
  });

  test("class declarations persist", async () => {
    const { results, context } = await runReplSession([
      "await 1; class Counter { constructor() { this.count = 0; } inc() { this.count++; } }",
      "const c = new Counter()",
      "c.inc(); c.inc(); c.count",
    ]);

    expect(results[2]).toBe(2);
    expect(typeof context.Counter).toBe("function");
  });

  test("complex session with mixed declarations", async () => {
    const { results } = await runReplSession([
      "var total = 0",
      "async function addAsync(n) { return total += await Promise.resolve(n); }",
      "await addAsync(10)",
      "await addAsync(20)",
      "total",
    ]);

    expect(results[2]).toBe(10);
    expect(results[3]).toBe(30);
    expect(results[4]).toBe(30);
  });

  test("destructuring assignment persists", async () => {
    const { results, context } = await runReplSession([
      "var { a, b } = await Promise.resolve({ a: 1, b: 2 })",
      "a + b",
      "var [x, y, z] = [10, 20, 30]",
      "x + y + z",
    ]);

    expect(results[1]).toBe(3);
    expect(results[3]).toBe(60);
    expect(context.a).toBe(1);
    expect(context.x).toBe(10);
  });
});
```

### Part 3: eval() Scoping Semantics Tests

Test that REPL behaves like eval() with proper scoping:

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("REPL eval() Scoping Semantics", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  test("var hoists to global context", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("var globalVar = 42");
    await vm.runInContext(code, ctx);

    expect(ctx.globalVar).toBe(42);
  });

  test("let/const hoisted for REPL but scoped correctly", async () => {
    const ctx = vm.createContext({});

    // With await, let is hoisted outside async wrapper
    const code1 = transpiler.transformSync("let x = await 1");
    await vm.runInContext(code1, ctx);
    expect(ctx.x).toBe(1);

    // Without await, let behavior depends on implementation
    const code2 = transpiler.transformSync("let y = 2");
    await vm.runInContext(code2, ctx);
    // y should still be accessible in REPL context
  });

  test("block-scoped let does NOT leak", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("if (await true) { let blockScoped = 1; }");
    await vm.runInContext(code, ctx);

    // blockScoped should NOT be visible in context
    expect(ctx.blockScoped).toBeUndefined();
  });

  test("function in block hoists with var (sloppy mode)", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("if (await true) { function blockFn() { return 42; } }");
    await vm.runInContext(code, ctx);

    // In sloppy mode, function in block hoists to function scope
    expect(typeof ctx.blockFn).toBe("function");
    expect(ctx.blockFn()).toBe(42);
  });

  test("this binding in function declarations", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("await 1; function greet() { return 'hello'; }");
    await vm.runInContext(code, ctx);

    // Function should be assigned to this (context) for REPL persistence
    expect(ctx.greet()).toBe("hello");
  });

  test("async function expression captures result", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("await (async () => { return 42; })()");
    const result = await vm.runInContext(code, ctx);

    expect(result.value).toBe(42);
  });

  test("Promise result NOT auto-awaited due to { value: } wrapper", async () => {
    const ctx = vm.createContext({});

    // Without wrapper, result would be auto-awaited
    // With { value: } wrapper, we get the Promise object
    const code = transpiler.transformSync("await Promise.resolve(Promise.resolve(42))");
    const result = await vm.runInContext(code, ctx);

    // The inner Promise should be in value, not auto-resolved
    expect(result.value).toBeInstanceOf(Promise);
    expect(await result.value).toBe(42);
  });
});
```

### Part 4: Edge Cases and Error Handling

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("REPL Edge Cases", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  test("empty input", () => {
    const result = transpiler.transformSync("");
    expect(result).toBe("");
  });

  test("whitespace only", () => {
    const result = transpiler.transformSync("   \n\t  ");
    expect(result.trim()).toBe("");
  });

  test("comment only", () => {
    const result = transpiler.transformSync("// just a comment");
    expect(result).toContain("// just a comment");
  });

  test("multiline input", () => {
    const input = `
      var x = await 1;
      var y = await 2;
      x + y
    `;
    const result = transpiler.transformSync(input);
    expect(result).toContain("var x");
    expect(result).toContain("var y");
    expect(result).toContain("async");
  });

  test("TypeScript syntax", () => {
    const input = "const x: number = await Promise.resolve(42)";
    const result = transpiler.transformSync(input);
    expect(result).not.toContain(": number"); // Types stripped
    expect(result).toContain("let x");
  });

  test("JSX in REPL", () => {
    const input = "await Promise.resolve(<div>Hello</div>)";
    const result = transpiler.transformSync(input);
    expect(result).toContain("async");
  });

  test("import expression (dynamic)", () => {
    // Dynamic imports should work fine
    const input = "await import('fs')";
    const result = transpiler.transformSync(input);
    expect(result).toContain("import");
  });

  test("nested await expressions", () => {
    const input = "await (await Promise.resolve(Promise.resolve(1)))";
    const result = transpiler.transformSync(input);
    expect(result).toContain("{ value:");
  });

  test("for-await-of", () => {
    const input = `
      async function* gen() { yield 1; yield 2; }
      for await (const x of gen()) { console.log(x); }
    `;
    const result = transpiler.transformSync(input);
    expect(result).toContain("var gen");
    expect(result).toContain("for await");
  });
});
```

---

## Verification Plan

### 1. Build and Basic Tests
```bash
# Build Bun with changes
bun bd

# Run the REPL transform tests
bun bd test test/js/bun/transpiler/repl-transform.test.ts
```

### 2. Manual Transform Output Verification
```typescript
// test-repl-manual.ts
const t = new Bun.Transpiler({ loader: "tsx", replMode: true });

// Object literal
console.log("Object literal:");
console.log(t.transformSync("{a: 1}"));
// Expected: contains "({a: 1})"

// Basic await
console.log("\nBasic await:");
console.log(t.transformSync("await 0"));
// Expected: (async () => { return { value: (await 0) } })()

// Variable hoisting
console.log("\nVar hoisting:");
console.log(t.transformSync("var x = await 1"));
// Expected: var x; (async () => { void (x = await 1) })()

// const → let
console.log("\nConst to let:");
console.log(t.transformSync("const x = await 1"));
// Expected: let x; (async () => { void (x = await 1) })()

// Function hoisting
console.log("\nFunction:");
console.log(t.transformSync("await 0; function foo() {}"));
// Expected: var foo; (async () => { await 0; this.foo = foo; function foo() {} })()
```

### 3. Full REPL Session Simulation
```typescript
// test-repl-session.ts
import vm from "node:vm";

const t = new Bun.Transpiler({ loader: "tsx", replMode: true });
const ctx = vm.createContext({ console, Promise });

async function repl(code: string) {
  const transformed = t.transformSync(code);
  console.log(`> ${code}`);
  console.log(`[transformed]: ${transformed}`);
  const result = await vm.runInContext(transformed, ctx);
  console.log(`= ${JSON.stringify(result?.value ?? result)}\n`);
  return result?.value ?? result;
}

// Test session
await repl("var counter = 0");
await repl("function increment() { return ++counter; }");
await repl("increment()");  // Should be 1
await repl("increment()");  // Should be 2
await repl("counter");      // Should be 2

await repl("const data = await Promise.resolve({ x: 10, y: 20 })");
await repl("data.x + data.y");  // Should be 30

await repl("class Point { constructor(x, y) { this.x = x; this.y = y; } }");
await repl("const p = new Point(3, 4)");
await repl("Math.sqrt(p.x**2 + p.y**2)");  // Should be 5
```

### 4. Verify No Regressions
```bash
# Run existing transpiler tests
bun bd test test/js/bun/transpiler/

# Run existing vm tests
bun bd test test/js/node/vm/
```

### 5. Cross-check with Node.js (Optional)
Compare transform outputs with Node.js's `processTopLevelAwait`:
```typescript
// Compare a few key transforms with Node.js output
const cases = [
  "await 0",
  "var x = await 1",
  "await 0; function foo() {}",
];
// Verify Bun output matches Node.js patterns
```
</claude-plan>
2026-01-19 00:32:55 -08:00
Jarred Sumner
5a0705348b feat(transpiler): add replMode option for REPL transforms
Add a new `replMode` option to Bun.Transpiler that transforms code for
interactive REPL evaluation:

- Wraps expressions in `{ value: expr }` for result capture
- Uses sync/async IIFE wrappers to avoid parentheses around objects
- Hoists var/let/const declarations for persistence across REPL lines
- Converts const to let for REPL mutability
- Hoists function declarations with this.funcName assignment
- Hoists class declarations with var for vm context persistence
- Auto-detects object literals (starting with { without trailing ;)

This enables building a Node.js-compatible REPL using Bun.Transpiler
with vm.runInContext for persistent variable scope.

Usage:
```typescript
const transpiler = new Bun.Transpiler({
  loader: "tsx",
  replMode: true,
});
const transformed = transpiler.transformSync(userInput);
const result = await vm.runInContext(transformed, context);
console.log(result.value);
```

REPL transforms are extracted into separate repl_transforms.zig module.

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 8
Claude-Permission-Prompts: 0
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# REPL Transform Fixes and Node.js Parity

## Current Status

The basic `replMode` option is implemented. This plan covers fixes and parity with Node.js REPL.

## Issues to Fix

### 1. Value Wrapper Has Extra Parentheses (CRITICAL)
**Current output:**
```js
({
  __proto__: null,
  value: 42
});
```

**Expected behavior (per Node.js):**
- For **non-async expressions**: Node.js returns `null` (no transform) - the REPL evaluates the expression directly
- For **async expressions**: `(async () => { return { __proto__: null, value: (expr) } })()`

**Solution:**
1. For non-async expressions: Don't wrap in `{ value: expr }` - just return the expression as-is
2. For async expressions: The `{ __proto__: null, value: expr }` is already inside the function after `return`, so no outer parens needed
3. Add inner parens around the expression value for clarity: `{ __proto__: null, value: (expr) }`

### 2. Object Literal Disambiguation (CRITICAL)
**Input:** `{a: 1}` or `{foo: await fetch()}`

**Current:** Parsed as block with labeled statement, NOT object literal

**Solution:** Pre-check input at transpiler layer:
- If code starts with `{` and doesn't end with `;`, try parsing as `(_=CODE)`
- If valid, wrap input as `(CODE)` before processing
- This matches Node.js approach in `repl.js` line 411-414

### 3. Class Declarations Don't Persist to VM Context
**Current:** Uses `let ClassName;` hoisting - doesn't become vm context property

**Node.js behavior:** Also uses `let` - this is a known limitation in Node.js too!

Looking at Node.js `await.js` line 31-37:
```js
ClassDeclaration(node, state, c) {
  state.prepend(node, `${node.id.name}=`);
  ArrayPrototypePush(state.hoistedDeclarationStatements, `let ${node.id.name}; `);
}
```

**Decision:** Use `var` instead of `let` for class hoisting. This makes classes persist to vm context, matching user expectations for REPL behavior. (Different from Node.js which uses `let`)

---

## Implementation Plan

## Usage Example

```typescript
// REPL tool implementation
const transpiler = new Bun.Transpiler({
  loader: "tsx",
  replMode: true,  // NEW OPTION
});

// For each REPL input line:
const transformed = transpiler.transformSync(userInput);

// Execute in persistent VM context
const result = vm.runInContext(transformed, replContext);

// result.value contains the expression result (wrapped to prevent auto-await)
console.log(result.value);
```

## Design Decisions

- **Value wrapper**: Use `{ value: expr }` wrapper like Node.js to prevent auto-awaiting Promise results
- **Static imports**: Keep static imports as-is (Bun handles them natively)
- **Scope**: Full Node.js REPL transform parity

---

## Node.js REPL Transform Behavior (Reference)

From `vendor/node/lib/internal/repl/await.js`:

### 1. Object Literal Detection
```javascript
// {a:1} → ({a:1}) when starts with { and no trailing ;
if (/^\s*{/.test(code) && !/;\s*$/.test(code)) {
  code = `(${code})`;
}
```

### 2. Top-Level Await Transform
```javascript
// Input:  await x
// Output: (async () => { return { value: (await x) } })()

// Input:  var x = await 1
// Output: var x; (async () => { void (x = await 1) })()

// Input:  const x = await 1
// Output: let x; (async () => { void (x = await 1) })()  // const→let

// Input:  function foo() {}  (with await somewhere)
// Output: var foo; (async () => { this.foo = foo; function foo() {} })()

// Input:  class Foo {}  (with await somewhere)
// Output: let Foo; (async () => { Foo=class Foo {} })()
```

### 3. Transform Skipping
Returns `null` (no transform) when:
- No `await` expression present at top level
- Top-level `return` statement exists
- Code is inside async functions/arrow functions/class methods

---

## Implementation Plan

### Fix 1: Remove Extra Parentheses from Value Wrapper

**Problem:** The printer adds `()` around objects at statement start to disambiguate from blocks.

**Solution:** Always use an IIFE wrapper (sync or async) so the object is after `return`:

```js
// Non-async expression (current - BAD)
({ __proto__: null, value: 42 });

// Non-async expression (fixed - GOOD)
(() => { return { __proto__: null, value: 42 } })()

// Non-async with hoisting (fixed - GOOD)
var x;
(() => { void (x = 1); return { __proto__: null, value: x } })()

// Async (already correct)
var x;
(async () => { void (x = await 1); return { __proto__: null, value: x } })()
```

**Files to modify:**
1. `src/ast/P.zig` - `applyReplValueWrapper()` function

**Changes:**
- Remove the simple `{ value: expr }` wrapper approach
- Always use `applyReplAsyncTransform()` style IIFE wrapping, but with `is_async = false` for non-async code
- This ensures the object is always after `return`, avoiding the parentheses issue
- Hoisting still works for both cases

### Fix 2: Object Literal Disambiguation

**File:** `src/bun.js/api/JSTranspiler.zig` - Before parsing

Add pre-processing check:
```zig
// In transformSync, before parsing:
if (config.repl_mode) {
    // Check if input looks like object literal: starts with { and doesn't end with ;
    if (startsWithBrace(source) and !endsWithSemicolon(source)) {
        // Try parsing as expression by wrapping: _=(CODE)
        // If valid, wrap input as (CODE)
        source = wrapAsExpression(source);
    }
}
```

This matches Node.js `isObjectLiteral()` check in `repl/utils.js:786-789`:
```js
function isObjectLiteral(code) {
  return /^\s*{/.test(code) && !/;\s*$/.test(code);
}
```

### Fix 3: Class Declaration Persistence

**File:** `src/ast/P.zig` - `applyReplAsyncTransform()` in the class handling section

Change from:
```zig
// let Foo; (hoisted)
try hoisted_stmts.append(p.s(S.Local{ .kind = .k_let, ... }));
```

To:
```zig
// var Foo; (hoisted) - use var so it becomes context property
try hoisted_stmts.append(p.s(S.Local{ .kind = .k_var, ... }));
// Also add: this.Foo = Foo; assignment after class declaration
```

---

## Node.js REPL Test Cases to Match

From `vendor/node/test/parallel/test-repl-preprocess-top-level-await.js`:

| Input | Expected Output |
|-------|-----------------|
| `await 0` | `(async () => { return { value: (await 0) } })()` |
| `var a = await 1` | `var a; (async () => { void (a = await 1) })()` |
| `let a = await 1` | `let a; (async () => { void (a = await 1) })()` |
| `const a = await 1` | `let a; (async () => { void (a = await 1) })()` |
| `await 0; function foo() {}` | `var foo; (async () => { await 0; this.foo = foo; function foo() {} })()` |
| `await 0; class Foo {}` | `let Foo; (async () => { await 0; Foo=class Foo {} })()` |
| `var {a} = {a:1}, [b] = [1]` | `var a, b; (async () => { void ( ({a} = {a:1}), ([b] = [1])) })()` |

---

## Files to Modify

| File | Changes |
|------|---------|
| `src/ast/P.zig` | Fix value wrapper format, fix class hoisting to use var |
| `src/bun.js/api/JSTranspiler.zig` | Add object literal pre-check |
| `src/ast/js_printer.zig` | May need to check object literal printing |
| `test/js/bun/transpiler/repl-transform.test.ts` | Update tests for exact Node.js parity |

---

## Verification

1. Run Node.js preprocess test cases through Bun's transpiler
2. Verify output matches Node.js exactly (or functionally equivalent)
3. Test with vm.runInContext for variable persistence
4. Test object literal inputs: `{a: 1}`, `{foo: await bar()}`

---

## DEPRECATED - Previous Implementation (Already Done)

### 1. Add `replMode` to Bun.Transpiler API

**File**: `src/bun.js/api/JSTranspiler.zig`

Add to `Config` struct (around line 27-44):
```zig
pub const Config = struct {
    // ... existing fields ...
    repl_mode: bool = false,
```

Parse the option in `Config.fromJS()` (around line 420-430):
```zig
if (try object.getBooleanLoose(globalThis, "replMode")) |flag| {
    this.repl_mode = flag;
}
```

Apply the option in `constructor()` (around line 714-721):
```zig
transpiler.options.repl_mode = config.repl_mode;
```

### 2. Add Feature Flag to Runtime

**File**: `src/runtime.zig` (in `Runtime.Features`)

```zig
/// REPL mode: transforms code for interactive evaluation
/// - Wraps lone object literals `{...}` in parentheses
/// - Hoists variable declarations for REPL persistence
/// - Wraps last expression in { value: expr } for result capture
/// - Assigns functions to context for persistence
repl_mode: bool = false,
```

### 3. Add to BundleOptions

**File**: `src/options.zig`

Add to `BundleOptions` struct:
```zig
repl_mode: bool = false,
```

### 4. Implement REPL Transforms in Parser

**File**: `src/ast/P.zig`

#### 4a. Object Literal Detection (Parser-Level)

In REPL mode, the parser should prefer interpreting ambiguous `{...}` as object literals instead of blocks.

**Location**: `src/ast/parseStmt.zig` in statement parsing

When `repl_mode` is true and the parser sees `{` at the start of a statement:
1. Try parsing as expression statement (object literal) first
2. If that fails, fall back to block statement

This is similar to how JavaScript engines handle REPL input. The parser already has the infrastructure to do this - we just need to change the precedence in REPL mode.

```zig
// In parseStmt when repl_mode is true and we see '{'
if (p.options.features.repl_mode and p.token.tag == .t_open_brace) {
    // Try parsing as expression first
    const saved_state = p.saveState();
    if (p.tryParseExpressionStatement()) |expr_stmt| {
        return expr_stmt;
    }
    p.restoreState(saved_state);
    // Fall back to block statement
    return p.parseBlockStatement();
}
```

This handles:
- `{a: 1}` → parsed as object literal expression
- `{a: 1, b: 2}` → parsed as object literal expression
- `{ let x = 1; }` → fails as expression, parsed as block
- `{ label: break label; }` → fails as expression (break not valid in object), parsed as block

#### 4b. REPL Transform Pass (in toAST after visiting)

Add a new function `applyReplTransforms()` that:

1. **Detect if transform is needed**: Walk AST to check for top-level `await`
2. **Skip transform when**:
   - No `await` at top level
   - Top-level `return` statement exists
3. **When transform IS needed**:
   - Wrap entire code in `(async () => { ... })()`
   - Hoist variable declarations outside the async wrapper
   - Convert `const` to `let` for persistence
   - Wrap last expression in `return { value: (expr) }`
   - Handle function declarations (assign to `this`)
   - Handle class declarations (hoist as `let`)

**Key Logic:**
```zig
fn applyReplTransforms(p: *Parser, stmts: []Stmt) ![]Stmt {
    // 1. Check for top-level await
    const has_await = p.hasTopLevelAwait(stmts);
    const has_return = p.hasTopLevelReturn(stmts);

    if (!has_await or has_return) {
        // Just wrap last expression, no async wrapper needed
        return p.wrapLastExpression(stmts);
    }

    // 2. Collect declarations to hoist
    var hoisted = std.ArrayList(Stmt).init(p.allocator);
    var inner_stmts = std.ArrayList(Stmt).init(p.allocator);

    for (stmts) |stmt| {
        switch (stmt.data) {
            .s_local => |local| {
                // Hoist declaration, convert const→let
                try hoisted.append(p.createHoistedDecl(local));
                // Add assignment expression to inner
                try inner_stmts.append(p.createAssignmentExpr(local));
            },
            .s_function => |func| {
                // var foo; (hoisted)
                try hoisted.append(p.createVarDecl(func.name));
                // this.foo = foo; function foo() {} (inner)
                try inner_stmts.append(p.createThisAssignment(func.name));
                try inner_stmts.append(stmt);
            },
            .s_class => |class| {
                // let Foo; (hoisted)
                try hoisted.append(p.createLetDecl(class.name));
                // Foo = class Foo {} (inner)
                try inner_stmts.append(p.createClassAssignment(class));
            },
            else => try inner_stmts.append(stmt),
        }
    }

    // 3. Wrap last expression in return { value: expr }
    p.wrapLastExpressionWithReturn(&inner_stmts);

    // 4. Create async IIFE: (async () => { ...inner... })()
    const async_iife = p.createAsyncIIFE(inner_stmts.items);

    // 5. Combine: hoisted declarations + async IIFE
    try hoisted.append(async_iife);
    return hoisted.toOwnedSlice();
}
```

### 5. TypeScript Type Definitions

**File**: `packages/bun-types/bun.d.ts`

Add to `TranspilerOptions` interface (around line 1748):
```typescript
interface TranspilerOptions {
  // ... existing options ...

  /**
   * Enable REPL mode transforms:
   * - Wraps object literals in parentheses
   * - Hoists declarations for REPL persistence
   * - Wraps last expression in { value: expr } for result capture
   * - Wraps code with await in async IIFE
   */
  replMode?: boolean;
}
```

---

## Files to Modify

| File | Changes |
|------|---------|
| `src/bun.js/api/JSTranspiler.zig` | Add `repl_mode` to Config, parse from JS, apply to transpiler |
| `src/runtime.zig` | Add `repl_mode: bool` to `Runtime.Features` |
| `src/options.zig` | Add `repl_mode: bool` to `BundleOptions` |
| `src/ast/P.zig` | REPL transform pass in `toAST()` |
| `src/ast/parseStmt.zig` | Object literal vs block disambiguation in REPL mode |
| `packages/bun-types/bun.d.ts` | Add `replMode?: boolean` to `TranspilerOptions` |

---

## Test Cases

Create test file: `test/js/bun/transpiler/repl-transform.test.ts`

### Part 1: Transform Output Tests (Unit Tests)

Test exact transformation output matches expected patterns:

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("Bun.Transpiler replMode - Transform Output", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  // Based on Node.js test-repl-preprocess-top-level-await.js
  const testCases: [string, string | null][] = [
    // No await = null (no async transform, but still expression capture)
    ['0', null],

    // Basic await
    ['await 0', '(async () => { return { value: (await 0) } })()'],
    ['await 0;', '(async () => { return { value: (await 0) }; })()'],
    ['(await 0)', '(async () => { return ({ value: (await 0) }) })()'],

    // No transform for await inside async functions
    ['async function foo() { await 0; }', null],
    ['async () => await 0', null],
    ['class A { async method() { await 0 } }', null],

    // Top-level return = no transform
    ['await 0; return 0;', null],

    // Multiple await - last one gets return wrapper
    ['await 1; await 2;', '(async () => { await 1; return { value: (await 2) }; })()'],

    // Variable hoisting - var
    ['var a = await 1', 'var a; (async () => { void (a = await 1) })()'],

    // Variable hoisting - let
    ['let a = await 1', 'let a; (async () => { void (a = await 1) })()'],

    // Variable hoisting - const becomes let
    ['const a = await 1', 'let a; (async () => { void (a = await 1) })()'],

    // For loop with var - hoist var
    ['for (var i = 0; i < 1; ++i) { await i }',
     'var i; (async () => { for (void (i = 0); i < 1; ++i) { await i } })()'],

    // For loop with let - no hoist
    ['for (let i = 0; i < 1; ++i) { await i }',
     '(async () => { for (let i = 0; i < 1; ++i) { await i } })()'],

    // Destructuring with var
    ['var {a} = {a:1}, [b] = [1], {c:{d}} = {c:{d: await 1}}',
     'var a, b, d; (async () => { void ( ({a} = {a:1}), ([b] = [1]), ({c:{d}} = {c:{d: await 1}})) })()'],

    // Destructuring with let
    ['let [a, b, c] = await ([1, 2, 3])',
     'let a, b, c; (async () => { void ([a, b, c] = await ([1, 2, 3])) })()'],

    // Function declarations - assign to this
    ['await 0; function foo() {}',
     'var foo; (async () => { await 0; this.foo = foo; function foo() {} })()'],

    // Class declarations - hoist as let
    ['await 0; class Foo {}',
     'let Foo; (async () => { await 0; Foo=class Foo {} })()'],

    // Nested scopes
    ['if (await true) { var a = 1; }',
     'var a; (async () => { if (await true) { void (a = 1); } })()'],
    ['if (await true) { let a = 1; }',
     '(async () => { if (await true) { let a = 1; } })()'],

    // Mixed declarations
    ['var a = await 1; let b = 2; const c = 3;',
     'var a; let b; let c; (async () => { void (a = await 1); void (b = 2); void (c = 3); })()'],

    // for await
    ['for await (var i of asyncIterable) { i; }',
     'var i; (async () => { for await (i of asyncIterable) { i; } })()'],

    // for-of with var
    ['for (var i of [1,2,3]) { await 1; }',
     'var i; (async () => { for (i of [1,2,3]) { await 1; } })()'],

    // for-in with var
    ['for (var i in {x:1}) { await 1 }',
     'var i; (async () => { for (i in {x:1}) { await 1 } })()'],

    // Spread in destructuring
    ['var { ...rest } = await {}',
     'var rest; (async () => { void ({ ...rest } = await {}) })()'],
  ];

  for (const [input, expected] of testCases) {
    test(`transform: ${input.slice(0, 40)}...`, () => {
      const result = transpiler.transformSync(input);
      if (expected === null) {
        // No async transform expected, but expression capture may still happen
        expect(result).not.toMatch(/^\(async/);
      } else {
        expect(result.trim()).toBe(expected);
      }
    });
  }

  // Object literal detection - parser handles this automatically in REPL mode
  describe("object literal vs block disambiguation", () => {
    test("{a: 1} parsed as object literal", async () => {
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{a: 1}");
      const result = await vm.runInContext(code, ctx);
      // Should evaluate to object, not undefined (block with label)
      expect(result.value).toEqual({ a: 1 });
    });

    test("{a: 1, b: 2} parsed as object literal", async () => {
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{a: 1, b: 2}");
      const result = await vm.runInContext(code, ctx);
      expect(result.value).toEqual({ a: 1, b: 2 });
    });

    test("{ let x = 1; x } parsed as block", async () => {
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{ let x = 1; x }");
      const result = await vm.runInContext(code, ctx);
      // Block returns last expression value
      expect(result.value).toBe(1);
    });

    test("{ x: 1; y: 2 } parsed as block with labels", async () => {
      // Semicolons make this a block with labeled statements, not object
      const ctx = vm.createContext({});
      const code = transpiler.transformSync("{ x: 1; y: 2 }");
      const result = await vm.runInContext(code, ctx);
      // Block with labels returns last value
      expect(result.value).toBe(2);
    });
  });
});
```

### Part 2: Variable Persistence Tests (Integration with node:vm)

Test that variables persist across multiple REPL evaluations:

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("REPL Variable Persistence", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  // Helper to run multiple REPL lines in sequence
  async function runReplSession(lines: string[], context?: object) {
    const ctx = vm.createContext(context ?? { console });
    const results: any[] = [];

    for (const line of lines) {
      const transformed = transpiler.transformSync(line);
      const result = await vm.runInContext(transformed, ctx);
      results.push(result?.value ?? result);
    }

    return { results, context: ctx };
  }

  test("var persists across lines", async () => {
    const { results, context } = await runReplSession([
      "var x = 10",
      "x + 5",
      "x = 20",
      "x",
    ]);

    expect(results[1]).toBe(15);  // x + 5
    expect(results[3]).toBe(20);  // x after reassignment
    expect(context.x).toBe(20);   // x visible in context
  });

  test("let persists across lines (hoisted)", async () => {
    const { results } = await runReplSession([
      "let y = await Promise.resolve(100)",
      "y * 2",
    ]);

    expect(results[1]).toBe(200);
  });

  test("const becomes let, can be reassigned in later lines", async () => {
    const { results } = await runReplSession([
      "const z = await Promise.resolve(5)",
      "z",
      // In REPL, const becomes let, so next line can redeclare
      "z = 10",  // This works because const→let
      "z",
    ]);

    expect(results[1]).toBe(5);
    expect(results[3]).toBe(10);
  });

  test("function declarations persist", async () => {
    const { results, context } = await runReplSession([
      "await 1; function add(a, b) { return a + b; }",
      "add(2, 3)",
      "function multiply(a, b) { return a * b; }",  // no await
      "multiply(4, 5)",
    ]);

    expect(results[1]).toBe(5);
    expect(results[3]).toBe(20);
    expect(typeof context.add).toBe("function");
    expect(typeof context.multiply).toBe("function");
  });

  test("class declarations persist", async () => {
    const { results, context } = await runReplSession([
      "await 1; class Counter { constructor() { this.count = 0; } inc() { this.count++; } }",
      "const c = new Counter()",
      "c.inc(); c.inc(); c.count",
    ]);

    expect(results[2]).toBe(2);
    expect(typeof context.Counter).toBe("function");
  });

  test("complex session with mixed declarations", async () => {
    const { results } = await runReplSession([
      "var total = 0",
      "async function addAsync(n) { return total += await Promise.resolve(n); }",
      "await addAsync(10)",
      "await addAsync(20)",
      "total",
    ]);

    expect(results[2]).toBe(10);
    expect(results[3]).toBe(30);
    expect(results[4]).toBe(30);
  });

  test("destructuring assignment persists", async () => {
    const { results, context } = await runReplSession([
      "var { a, b } = await Promise.resolve({ a: 1, b: 2 })",
      "a + b",
      "var [x, y, z] = [10, 20, 30]",
      "x + y + z",
    ]);

    expect(results[1]).toBe(3);
    expect(results[3]).toBe(60);
    expect(context.a).toBe(1);
    expect(context.x).toBe(10);
  });
});
```

### Part 3: eval() Scoping Semantics Tests

Test that REPL behaves like eval() with proper scoping:

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("REPL eval() Scoping Semantics", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  test("var hoists to global context", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("var globalVar = 42");
    await vm.runInContext(code, ctx);

    expect(ctx.globalVar).toBe(42);
  });

  test("let/const hoisted for REPL but scoped correctly", async () => {
    const ctx = vm.createContext({});

    // With await, let is hoisted outside async wrapper
    const code1 = transpiler.transformSync("let x = await 1");
    await vm.runInContext(code1, ctx);
    expect(ctx.x).toBe(1);

    // Without await, let behavior depends on implementation
    const code2 = transpiler.transformSync("let y = 2");
    await vm.runInContext(code2, ctx);
    // y should still be accessible in REPL context
  });

  test("block-scoped let does NOT leak", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("if (await true) { let blockScoped = 1; }");
    await vm.runInContext(code, ctx);

    // blockScoped should NOT be visible in context
    expect(ctx.blockScoped).toBeUndefined();
  });

  test("function in block hoists with var (sloppy mode)", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("if (await true) { function blockFn() { return 42; } }");
    await vm.runInContext(code, ctx);

    // In sloppy mode, function in block hoists to function scope
    expect(typeof ctx.blockFn).toBe("function");
    expect(ctx.blockFn()).toBe(42);
  });

  test("this binding in function declarations", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("await 1; function greet() { return 'hello'; }");
    await vm.runInContext(code, ctx);

    // Function should be assigned to this (context) for REPL persistence
    expect(ctx.greet()).toBe("hello");
  });

  test("async function expression captures result", async () => {
    const ctx = vm.createContext({});

    const code = transpiler.transformSync("await (async () => { return 42; })()");
    const result = await vm.runInContext(code, ctx);

    expect(result.value).toBe(42);
  });

  test("Promise result NOT auto-awaited due to { value: } wrapper", async () => {
    const ctx = vm.createContext({});

    // Without wrapper, result would be auto-awaited
    // With { value: } wrapper, we get the Promise object
    const code = transpiler.transformSync("await Promise.resolve(Promise.resolve(42))");
    const result = await vm.runInContext(code, ctx);

    // The inner Promise should be in value, not auto-resolved
    expect(result.value).toBeInstanceOf(Promise);
    expect(await result.value).toBe(42);
  });
});
```

### Part 4: Edge Cases and Error Handling

```typescript
import { expect, test, describe } from "bun:test";
import vm from "node:vm";

describe("REPL Edge Cases", () => {
  const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });

  test("empty input", () => {
    const result = transpiler.transformSync("");
    expect(result).toBe("");
  });

  test("whitespace only", () => {
    const result = transpiler.transformSync("   \n\t  ");
    expect(result.trim()).toBe("");
  });

  test("comment only", () => {
    const result = transpiler.transformSync("// just a comment");
    expect(result).toContain("// just a comment");
  });

  test("multiline input", () => {
    const input = `
      var x = await 1;
      var y = await 2;
      x + y
    `;
    const result = transpiler.transformSync(input);
    expect(result).toContain("var x");
    expect(result).toContain("var y");
    expect(result).toContain("async");
  });

  test("TypeScript syntax", () => {
    const input = "const x: number = await Promise.resolve(42)";
    const result = transpiler.transformSync(input);
    expect(result).not.toContain(": number"); // Types stripped
    expect(result).toContain("let x");
  });

  test("JSX in REPL", () => {
    const input = "await Promise.resolve(<div>Hello</div>)";
    const result = transpiler.transformSync(input);
    expect(result).toContain("async");
  });

  test("import expression (dynamic)", () => {
    // Dynamic imports should work fine
    const input = "await import('fs')";
    const result = transpiler.transformSync(input);
    expect(result).toContain("import");
  });

  test("nested await expressions", () => {
    const input = "await (await Promise.resolve(Promise.resolve(1)))";
    const result = transpiler.transformSync(input);
    expect(result).toContain("{ value:");
  });

  test("for-await-of", () => {
    const input = `
      async function* gen() { yield 1; yield 2; }
      for await (const x of gen()) { console.log(x); }
    `;
    const result = transpiler.transformSync(input);
    expect(result).toContain("var gen");
    expect(result).toContain("for await");
  });
});
```

---

## Verification Plan

### 1. Build and Basic Tests
```bash
# Build Bun with changes
bun bd

# Run the REPL transform tests
bun bd test test/js/bun/transpiler/repl-transform.test.ts
```

### 2. Manual Transform Output Verification
```typescript
// test-repl-manual.ts
const t = new Bun.Transpiler({ loader: "tsx", replMode: true });

// Object literal
console.log("Object literal:");
console.log(t.transformSync("{a: 1}"));
// Expected: contains "({a: 1})"

// Basic await
console.log("\nBasic await:");
console.log(t.transformSync("await 0"));
// Expected: (async () => { return { value: (await 0) } })()

// Variable hoisting
console.log("\nVar hoisting:");
console.log(t.transformSync("var x = await 1"));
// Expected: var x; (async () => { void (x = await 1) })()

// const → let
console.log("\nConst to let:");
console.log(t.transformSync("const x = await 1"));
// Expected: let x; (async () => { void (x = await 1) })()

// Function hoisting
console.log("\nFunction:");
console.log(t.transformSync("await 0; function foo() {}"));
// Expected: var foo; (async () => { await 0; this.foo = foo; function foo() {} })()
```

### 3. Full REPL Session Simulation
```typescript
// test-repl-session.ts
import vm from "node:vm";

const t = new Bun.Transpiler({ loader: "tsx", replMode: true });
const ctx = vm.createContext({ console, Promise });

async function repl(code: string) {
  const transformed = t.transformSync(code);
  console.log(`> ${code}`);
  console.log(`[transformed]: ${transformed}`);
  const result = await vm.runInContext(transformed, ctx);
  console.log(`= ${JSON.stringify(result?.value ?? result)}\n`);
  return result?.value ?? result;
}

// Test session
await repl("var counter = 0");
await repl("function increment() { return ++counter; }");
await repl("increment()");  // Should be 1
await repl("increment()");  // Should be 2
await repl("counter");      // Should be 2

await repl("const data = await Promise.resolve({ x: 10, y: 20 })");
await repl("data.x + data.y");  // Should be 30

await repl("class Point { constructor(x, y) { this.x = x; this.y = y; } }");
await repl("const p = new Point(3, 4)");
await repl("Math.sqrt(p.x**2 + p.y**2)");  // Should be 5
```

### 4. Verify No Regressions
```bash
# Run existing transpiler tests
bun bd test test/js/bun/transpiler/

# Run existing vm tests
bun bd test test/js/node/vm/
```

### 5. Cross-check with Node.js (Optional)
Compare transform outputs with Node.js's `processTopLevelAwait`:
```typescript
// Compare a few key transforms with Node.js output
const cases = [
  "await 0",
  "var x = await 1",
  "await 0; function foo() {}",
];
// Verify Bun output matches Node.js patterns
```
</claude-plan>
2026-01-19 00:22:36 -08:00
Jarred Sumner
c47f84348a Update CLAUDE.md 2026-01-18 14:07:30 -08:00
robobun
039c89442f chore: bump TinyCC to latest upstream (Jan 2026) (#26210)
## What does this PR do?

Updates the oven-sh/tinycc fork to the latest upstream TinyCC,
incorporating 30+ upstream commits while preserving all Bun-specific
patches.

### Upstream changes incorporated
- Build system improvements (c2str.exe handling, cross-compilation)
- macOS 15 compatibility fixes
- libtcc debugging support
- pic/pie support for i386
- arm64 alignment and symbol offset fixes
- RISC-V 64 improvements (pointer difference, assembly, Zicsr extension)
- Relocation updates
- Preprocessor improvements (integer literal overflow handling)
- x86-64 cvts*2si fix
- Various bug fixes

### Bun-specific patches preserved
- Fix crash on macOS x64 (libxcselect.dylib memory handling)
- Implement `-framework FrameworkName` on macOS (for framework header
parsing)
- Add missing #ifdef guards for TCC_IS_NATIVE
- Make `__attribute__(deprecated)` a no-op
- Fix `__has_include` with framework paths
- Support attributes after identifiers in enums
- Fix dlsym behavior on macOS (RTLD_SELF first, then RTLD_DEFAULT)
- Various tccmacho.c improvements

### Related PRs
- TinyCC fork CI is passing:
https://github.com/oven-sh/tinycc/actions/runs/21105489093

## How did you verify your code works?

- [x] TinyCC fork CI passes on all platforms (Linux
x86_64/arm64/armv7/riscv64, macOS x86_64/arm64, Windows i386/x86_64)
- [ ] Bun CI passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 13:31:21 -08:00
robobun
3d46ae2fa4 fix(node-fetch): convert old-style Node.js streams to Web streams (#26226)
## Summary
- Fix multipart uploads using form-data + node-fetch@2 +
fs.createReadStream() being truncated
- Convert old-style Node.js streams (that don't implement
`Symbol.asyncIterator`) to Web ReadableStreams before passing to native
fetch

## Test plan
- [x] New tests in `test/regression/issue/26225.test.ts` verify:
  - Multipart uploads with form-data and createReadStream work correctly
  - Async iterable bodies still work (regression test)
  - Large file streams work correctly
- [x] Tests fail with `USE_SYSTEM_BUN=1` and pass with debug build

Fixes #26225

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-18 13:19:02 -08:00
robobun
1344151576 fix(json): prevent stack overflow in JSONC parser on deeply nested input (#26174)
## Summary
- Add stack overflow protection to JSON/JSONC parser to prevent
segmentation faults
- Parser now throws `RangeError: Maximum call stack size exceeded`
instead of crashing
- Fixes DoS vulnerability when parsing deeply nested JSON structures
(~150k+ depth)

## Test plan
- [x] Added regression tests for deeply nested arrays and objects (25k
depth)
- [x] Verified system Bun v1.3.6 crashes with segfault at 150k depth
- [x] Verified fix throws proper error instead of crashing
- [x] All existing JSONC tests pass

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 16:23:01 -08:00
SUZUKI Sosuke
44df912d37 Add Bun.wrapAnsi() for text wrapping with ANSI escape code preservation (#26061)
## Summary

Adds `Bun.wrapAnsi()`, a native implementation of the popular
[wrap-ansi](https://www.npmjs.com/package/wrap-ansi) npm package for
wrapping text with ANSI escape codes.

## API

```typescript
Bun.wrapAnsi(string: string, columns: number, options?: WrapAnsiOptions): string

interface WrapAnsiOptions {
  hard?: boolean;              // default: false - Break words longer than columns
  wordWrap?: boolean;          // default: true - Wrap at word boundaries
  trim?: boolean;              // default: true - Trim leading/trailing whitespace
  ambiguousIsNarrow?: boolean; // default: true - Treat ambiguous-width chars as narrow
}
```

## Features

- Wraps text to fit within specified column width
- Preserves ANSI escape codes (SGR colors/styles)
- Supports OSC 8 hyperlinks
- Respects Unicode display widths (full-width characters, emoji)
- Normalizes `\r\n` to `\n`

## Implementation Details

The implementation closes and reopens ANSI codes around line breaks for
robust terminal compatibility. This differs slightly from the npm
package in edge cases but produces visually equivalent output.

### Behavioral Differences from npm wrap-ansi

1. **ANSI code preservation**: Bun always maintains complete ANSI escape
sequences. The npm version can output malformed codes (missing ESC
character) in certain edge cases with `wordWrap: false, trim: false`.

2. **Newline ANSI handling**: Bun closes and reopens ANSI codes around
newlines for robustness. The npm version sometimes keeps them spanning
across newlines. The visual output is equivalent.

## Tests

- 27 custom tests covering basic functionality, ANSI codes, Unicode, and
options
- 23 tests ported from the npm package (MIT licensed, credited in file
header)
- All 50 tests pass

## Benchmark

<!-- Benchmark results will be added -->
```
$ cd /Users/sosuke/code/bun/bench && ../build/release/bun snippets/wrap-ansi.js
clk: ~3.82 GHz
cpu: Apple M4 Max
runtime: bun 1.3.7 (arm64-darwin)

benchmark                    avg (min … max) p75   p99    (min … top 1%)
-------------------------------------------- -------------------------------
Short text (45 chars) - npm    25.81 µs/iter  21.71 µs  █
                      (16.79 µs … 447.38 µs) 110.96 µs ▆█▃▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
Short text (45 chars) - Bun   685.55 ns/iter 667.00 ns    █
                       (459.00 ns … 2.16 ms)   1.42 µs ▁▁▁█▃▂▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁

summary
  Short text (45 chars) - Bun
   37.65x faster than Short text (45 chars) - npm

-------------------------------------------- -------------------------------
Medium text (810 chars) - npm 568.12 µs/iter 578.00 µs  ▄▅█▆▆▃
                     (525.25 µs … 944.71 µs) 700.75 µs ▄██████▆▅▄▃▃▂▂▂▁▁▁▁▁▁
Medium text (810 chars) - Bun  11.22 µs/iter  11.28 µs                     █
                       (11.04 µs … 11.46 µs)  11.33 µs █▁▁▁██▁█▁▁▁▁█▁█▁▁█▁▁█

summary
  Medium text (810 chars) - Bun
   50.62x faster than Medium text (810 chars) - npm

-------------------------------------------- -------------------------------
Long text (8100 chars) - npm    7.66 ms/iter   7.76 ms     ▂▂▅█   ▅
                         (7.31 ms … 8.10 ms)   8.06 ms ▃▃▄▃█████▇▇███▃▆▆▆▄▁▃
Long text (8100 chars) - Bun  112.14 µs/iter 113.50 µs        █
                     (102.50 µs … 146.04 µs) 124.92 µs ▁▁▁▁▁▁██▇▅█▃▂▂▂▂▁▁▁▁▁

summary
  Long text (8100 chars) - Bun
   68.27x faster than Long text (8100 chars) - npm

-------------------------------------------- -------------------------------
Colored short - npm            28.46 µs/iter  28.56 µs              █
                       (27.90 µs … 29.34 µs)  28.93 µs ▆▁▆▁▁▆▁▁▆▆▆▁▆█▁▁▁▁▁▁▆
Colored short - Bun           861.64 ns/iter 867.54 ns         ▂  ▇█▄▂
                     (839.68 ns … 891.12 ns) 882.04 ns ▃▅▄▅▆▆▇▆██▇████▆▃▅▅▅▂

summary
  Colored short - Bun
   33.03x faster than Colored short - npm

-------------------------------------------- -------------------------------
Colored medium - npm          557.84 µs/iter 562.63 µs      ▂▃█▄
                     (508.08 µs … 911.92 µs) 637.96 µs ▁▁▁▂▄█████▅▂▂▁▁▁▁▁▁▁▁
Colored medium - Bun           14.91 µs/iter  14.94 µs ██  ████ ██ █      ██
                       (14.77 µs … 15.17 µs)  15.06 µs ██▁▁████▁██▁█▁▁▁▁▁▁██

summary
  Colored medium - Bun
   37.41x faster than Colored medium - npm

-------------------------------------------- -------------------------------
Colored long - npm              7.84 ms/iter   7.90 ms       █  ▅
                         (7.53 ms … 8.38 ms)   8.19 ms ▂▂▂▄▃▆██▇██▇▃▂▃▃▃▄▆▂▂
Colored long - Bun            176.73 µs/iter 175.42 µs       █
                       (162.50 µs … 1.37 ms) 204.46 µs ▁▁▂▄▇██▅▂▂▂▁▁▁▁▁▁▁▁▁▁

summary
  Colored long - Bun
   44.37x faster than Colored long - npm

-------------------------------------------- -------------------------------
Hard wrap long - npm            8.05 ms/iter   8.12 ms       ▃ ▇█
                         (7.67 ms … 8.53 ms)   8.50 ms ▄▁▁▁▃▄█████▄▃▂▆▄▃▂▂▂▂
Hard wrap long - Bun          111.85 µs/iter 112.33 µs         ▇█
                     (101.42 µs … 145.42 µs) 123.88 µs ▁▁▁▁▁▁▁████▄▃▂▂▂▁▁▁▁▁

summary
  Hard wrap long - Bun
   72.01x faster than Hard wrap long - npm

-------------------------------------------- -------------------------------
Hard wrap colored - npm         8.82 ms/iter   8.92 ms   ▆ ██
                         (8.55 ms … 9.47 ms)   9.32 ms ▆▆████▆▆▄▆█▄▆▄▄▁▃▁▃▄▃
Hard wrap colored - Bun       174.38 µs/iter 175.54 µs   █ ▂
                     (165.75 µs … 210.25 µs) 199.50 µs ▁▃█▆███▃▂▃▂▂▂▂▂▁▁▁▁▁▁

summary
  Hard wrap colored - Bun
   50.56x faster than Hard wrap colored - npm

-------------------------------------------- -------------------------------
Japanese (full-width) - npm    51.00 µs/iter  52.67 µs    █▂   █▄
                      (40.71 µs … 344.88 µs)  66.13 µs ▁▁▃██▄▃▅██▇▄▃▄▃▂▂▁▁▁▁
Japanese (full-width) - Bun     7.46 µs/iter   7.46 µs       █
                        (6.50 µs … 34.92 µs)   9.38 µs ▁▁▁▁▁██▆▂▁▂▁▁▁▁▁▁▁▁▁▁

summary
  Japanese (full-width) - Bun
   6.84x faster than Japanese (full-width) - npm

-------------------------------------------- -------------------------------
Emoji text - npm              173.63 µs/iter 222.17 µs   █
                     (129.42 µs … 527.25 µs) 249.58 µs ▁▃█▆▃▃▃▁▁▁▁▁▁▁▂▄▆▄▂▂▁
Emoji text - Bun                9.42 µs/iter   9.47 µs           ██
                         (9.32 µs … 9.52 µs)   9.50 µs █▁▁███▁▁█▁██▁▁▁▁██▁▁█

summary
  Emoji text - Bun
   18.44x faster than Emoji text - npm

-------------------------------------------- -------------------------------
Hyperlink (OSC 8) - npm       208.00 µs/iter 254.25 µs   █
                     (169.58 µs … 542.17 µs) 281.00 µs ▁▇█▃▃▂▂▂▁▁▁▁▁▁▁▃▃▅▃▂▁
Hyperlink (OSC 8) - Bun         6.00 µs/iter   6.06 µs      █           ▄
                         (5.88 µs … 6.11 µs)   6.10 µs ▅▅▅▁▅█▅▁▅▁█▁▁▅▅▅▅█▅▁█

summary
  Hyperlink (OSC 8) - Bun
   34.69x faster than Hyperlink (OSC 8) - npm

-------------------------------------------- -------------------------------
No trim long - npm              8.32 ms/iter   8.38 ms  █▇
                        (7.61 ms … 13.67 ms)  11.74 ms ▃████▄▂▃▂▂▃▁▁▁▁▁▁▁▁▁▂
No trim long - Bun             93.92 µs/iter  94.42 µs           █▂
                      (82.75 µs … 162.38 µs) 103.83 µs ▁▁▁▁▁▁▁▁▄███▄▃▂▂▁▁▁▁▁

summary
  No trim long - Bun
   88.62x faster than No trim long - npm
```

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-01-16 16:12:23 -08:00
robobun
05434add3e fix(bundler): legal comments no longer break module.exports = require() redirect optimization (#26113)
## Summary

- Legal comments (`/*! ... */`) were preventing the `module.exports =
require()` redirect optimization from being applied to CommonJS wrapper
modules
- The fix scans all parts to find a single meaningful statement,
skipping comments, directives, and empty statements
- If exactly one such statement exists and matches the `module.exports =
require()` pattern, the redirect optimization is now applied

This fixes an issue where wrapper modules like Express's `index.js`:
```js
/*!
 * express
 * MIT Licensed
 */

'use strict';

module.exports = require('./lib/express');
```

Were generating unnecessary wrapper functions instead of being
redirected directly to the target module.

## Test plan

- [x] Added regression test in `test/regression/issue/3179.test.ts`
- [x] Verified test fails with system bun and passes with the fix
- [x] Tested manual reproduction scenario

Fixes #3179

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 15:57:27 -08:00
vadim-anthropic
8da29af1ae feat(node:inspector): implement Profiler API (#25939) 2026-01-16 10:12:28 -08:00
robobun
bcbb4fc35d fix(cli): show helpful error for unsupported file types instead of "File not found" (#26126)
## Summary

- When running `bun <file>` on a file with an unsupported type (e.g.,
`.css`, `.yaml`, `.toml`), Bun now shows a helpful error message instead
of the misleading "File not found"
- Tracks when a file is resolved but has a loader that can't be run
directly
- Shows the actual file path and file type in the error message

**Before:**
```
error: File not found "test.css"
```

**After:**
```
error: Cannot run "/path/to/test.css"
note: Bun cannot run css files directly
```

## Test plan

- [x] Added regression test in `test/regression/issue/1365.test.ts`
- [x] Test verifies unsupported files show "Cannot run" error
- [x] Test verifies nonexistent files still show "File not found"
- [x] Test fails with `USE_SYSTEM_BUN=1` and passes with debug build

Fixes #1365

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-01-15 23:40:45 -08:00
robobun
ad4aabf486 fix(Request): set cache and mode options correctly (#26099)
## Summary

- `new Request()` was ignoring `cache` and `mode` options, always
returning hardcoded default values ("default" for cache, "navigate" for
mode)
- Added proper storage and handling of these options in the Request
struct
- Both options are now correctly parsed from the constructor init object
and preserved when cloning

Fixes #2993

## Test plan

- [x] Added regression test in `test/regression/issue/2993.test.ts`
- [x] Tests verify all valid cache values: "default", "no-store",
"reload", "no-cache", "force-cache", "only-if-cached"
- [x] Tests verify all valid mode values: "same-origin", "no-cors",
"cors", "navigate"
- [x] Tests verify default values (cache: "default", mode: "cors")
- [x] Tests verify `Request.clone()` preserves options
- [x] Tests verify `new Request(request)` preserves options
- [x] Tests verify `new Request(request, init)` allows overriding
options


🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 23:02:16 -08:00
robobun
5b25a3abdb fix: don't call Bun.serve() on exported Server instances (#26144)
## Summary

- Fixes the entry point wrapper to distinguish between Server
configuration objects and already-running Server instances
- When a Server object from `Bun.serve()` is exported as the default
export, Bun no longer tries to call `Bun.serve()` on it again

## Root Cause

The entry point wrapper in `src/bundler/entry_points.zig` checks if the
default export has a `fetch` method to auto-start servers:

```javascript
if (typeof entryNamespace?.default?.fetch === 'function' || ...) {
   const server = Bun.serve(entryNamespace.default);
}
```

However, `Server` objects returned from `Bun.serve()` also have a
`fetch` method (for programmatic request handling), so the wrapper
mistakenly tried to call `Bun.serve(server)` on an already-running
server.

## Solution

Added an `isServerConfig()` helper that checks:
1. The object has a `fetch` function or `app` property (config object
indicators)
2. The object does NOT have a `stop` method (Server instance indicator)

Server instances have `stop`, `reload`, `upgrade`, etc. methods, while
config objects don't.

## Test plan

- [x] Added regression test that verifies exporting a Server as default
export works without errors
- [x] Added test that verifies config objects with `fetch` still trigger
auto-start
- [x] Verified test fails with `USE_SYSTEM_BUN=1` and passes with the
fix

Fixes #26142

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 20:28:32 -08:00
robobun
12243b9715 fix(ws): pass selected protocol from handleProtocols to upgrade response (#26118)
## Summary
- Fixes the `handleProtocols` option not setting the selected protocol
in WebSocket upgrade responses
- Removes duplicate protocol header values in responses

## Test plan
- Added regression tests in `test/regression/issue/3613.test.ts`
- Verified using fetch to check actual response headers contain the
correct protocol

Fixes #3613

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-01-15 18:22:01 -08:00
Ciro Spaciari
5d3f37d7ae feat(s3): add Content-Encoding header support for S3 uploads (#26149)
## Summary

Add support for setting the `Content-Encoding` header in S3 `.write()`
and `.writer()` calls, following the same pattern as
`Content-Disposition`.

This allows users to specify the encoding of uploaded content:

```typescript
// With .write()
await s3file.write("compressed data", { contentEncoding: "gzip" });

// With .writer()
const writer = s3file.writer({ contentEncoding: "gzip" });
writer.write("compressed data");
await writer.end();

// With bucket.write()
await bucket.write("key", data, { contentEncoding: "br" });
```

## Implementation

- Extended `SignedHeaders.Key` from 6 bits to 7 bits (64→128
combinations) to accommodate the new header
- Added `content_encoding` to `S3CredentialsWithOptions`, `SignOptions`,
and `SignResult` structs
- Updated `CanonicalRequest` format strings to include
`content-encoding` in AWS SigV4 signing
- Added `getContentEncoding()` method to `Headers` for fetch-based S3
uploads
- Expanded `_headers` array from 9 to 10 elements
- Pass `content_encoding` through all S3 upload paths (upload,
uploadStream, writableStream)

## Test plan

- Added tests for "should be able to set content-encoding" 
- Added tests for "should be able to set content-encoding in writer"
- Tests verify the Content-Encoding header is properly set on uploaded
objects via presigned URL fetch
- All 4 new tests pass with `bun bd test` and fail with
`USE_SYSTEM_BUN=1` (confirming the feature is new)

## Changelog

> Describe your changes in 1-2 sentences. These will be featured on
[bun.sh/blog](https://bun.sh/blog) and Bun's release notes.

Added `contentEncoding` option to S3 `.write()` and `.writer()` methods,
allowing users to set the `Content-Encoding` header when uploading
objects.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 18:09:33 -08:00
robobun
2a483631fb fix(http): allow body on GET/HEAD/OPTIONS requests for Node.js compatibility (#26145)
## Summary

Fixed `http.request()` and `https.request()` hanging indefinitely when a
GET request includes a body (via `req.write()`).

### Approach

Instead of adding a public `allowGetBody` option to `fetch()`, this PR
creates a dedicated internal function `nodeHttpClient` that:
- Uses a comptime parameter to avoid code duplication
- Allows body on GET/HEAD/OPTIONS requests (Node.js behavior)
- Is only accessible internally via `$newZigFunction`
- Keeps the public `Bun.fetch()` API unchanged (Web Standards compliant)

### Implementation

1. **fetch.zig**: Refactored to use `fetchImpl(comptime allow_get_body:
bool, ...)` shared implementation
- `Bun__fetch_()` calls `fetchImpl(false, ...)` - validates body on
GET/HEAD/OPTIONS
- `nodeHttpClient()` calls `fetchImpl(true, ...)` - allows body on
GET/HEAD/OPTIONS

2. **_http_client.ts**: Uses `$newZigFunction("fetch.zig",
"nodeHttpClient", 2)` for HTTP requests

## Test plan

- [x] Added regression test at `test/regression/issue/26143.test.ts`
- [x] Test verifies GET requests with body complete successfully
- [x] Test verifies HEAD requests with body complete successfully
- [x] Test verifies `Bun.fetch()` still throws on GET with body (Web
Standards)
- [x] Test fails on current release (v1.3.6) and passes with this fix

Fixes #26143

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Ciro Spaciari <ciro.spaciari@gmail.com>
Co-authored-by: Ciro Spaciari MacBook <ciro@anthropic.com>
2026-01-15 17:46:07 -08:00
robobun
cdcff11221 fix(cli): handle BrokenPipe gracefully in bun completions (#26097)
## Summary
- Fixes `bun completions` crashing with `BrokenPipe` error when piped to
commands that close stdout early (e.g., `bun completions | true`)
- The fix catches `error.BrokenPipe` and exits cleanly with status 0
instead of propagating the error

## Test plan
- [x] Added regression test that pipes `bun completions` to `true` and
verifies no BrokenPipe error occurs
- [x] Verified test fails with system Bun and passes with fixed build

Fixes #2977

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-01-15 17:44:59 -08:00
robobun
dfa704cc62 fix(s3): add contentDisposition and type support to presign() (#25999)
## Summary
- S3 `File.presign()` was ignoring the `contentDisposition` and `type`
options
- These options are now properly included as
`response-content-disposition` and `response-content-type` query
parameters in the presigned URL
- Added `content_type` field to `SignOptions` and
`S3CredentialsWithOptions` structs
- Added parsing for the `type` option in `getCredentialsWithOptions()`
- Query parameters are added in correct alphabetical order for AWS
Signature V4 compliance

## Test plan
- [x] Added regression test in `test/regression/issue/25750.test.ts`
- [x] Verified tests pass with debug build: `bun bd test
test/regression/issue/25750.test.ts`
- [x] Verified tests fail with system bun (without fix):
`USE_SYSTEM_BUN=1 bun test test/regression/issue/25750.test.ts`
- [x] Verified existing S3 presign tests still pass
- [x] Verified existing S3 signature order tests still pass

Fixes #25750

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 15:33:43 -08:00
Alistair Smith
97feb66189 Double the hardcoded max http header count (#26130)
### What does this PR do?

Doubles the hardcoded max http header count

### How did you verify your code works?

ci (?)

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-01-15 00:35:37 -08:00
robobun
ed75a0e2d1 fix(http): use correct client certificate for mTLS fetch() requests (#26129)
## Summary
- Fixes bug where `fetch()` with mTLS would use the first client
certificate for all subsequent requests to the same host, ignoring
per-request `tls` options
- Corrects `SSLConfig.isSame()` to properly compare all fields (was
incorrectly returning early when both optional fields were null)
- Sets `disable_keepalive=true` when reusing cached SSL contexts to
prevent socket pooling issues

Fixes #26125

## Test plan
- [x] Added regression test `test/regression/issue/26125.test.ts`
- [x] Verified test fails with system Bun 1.3.6 (demonstrates the bug)
- [x] Verified test passes with patched build

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 19:22:08 -08:00
Jarred Sumner
d4e5197208 Fix exception scope verification error 2026-01-14 19:09:18 -08:00
robobun
65a9a2a580 fix(process): emit EPIPE error on broken pipe for process.stdout.write() (#26124)
## Summary
- Fixes the broken pipe behavior for `process.stdout.write()` to match
Node.js
- When writing to a broken pipe (stdout destroyed), the process now
properly exits with code 1 instead of 0
- EPIPE errors are now properly propagated to JavaScript via the
stream's error event

## Test plan
- [x] Added regression test `test/regression/issue/1632.test.ts`
- [x] Verified test fails with system bun (exit code 0) and passes with
debug build (exit code 1)
- [x] Verified `console.log` still ignores errors (uses `catch {}`) and
doesn't crash
- [x] Verified callback-based `process.stdout.write()` receives EPIPE
error

## Changes
1. **`src/io/PipeWriter.zig`**: Return EPIPE as an error instead of
treating it as successful end-of-file (`.done`)
2. **`src/shell/IOWriter.zig`**: Track `broken_pipe` flag when EPIPE is
received via `onError` callback, and propagate error properly
3. **`src/js/internal/fs/streams.ts`**: When a write fails without a
callback, emit the error on the stream via `this.destroy(err)` to match
Node.js behavior

Fixes #1632

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 18:37:54 -08:00
robobun
393198d190 fix(install): quote workspace: versions in yarn lockfile (#26106)
## Summary
- Add colon (`:`) to the list of characters that require quoting in yarn
lockfile version strings
- This fixes yarn parse errors when using `workspace:*` dependencies in
monorepo setups

Fixes #3192

## Test plan
- [x] Added regression test that verifies `workspace:*` versions are
properly quoted
- [x] Test fails with system bun (before fix)
- [x] Test passes with debug build (after fix)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 18:22:32 -08:00
robobun
798b48c898 fix(runtime): exclude globalThis from auto-serve detection (#26107)
## Summary
- When a module exports `globalThis` (e.g., `module.exports =
globalThis`), Bun's auto-serve detection incorrectly triggered because
`globalThis.fetch` is the Fetch API function
- Scripts that export globalThis (like `core-js/es/global-this.js`)
would start a development server on port 3000 instead of exiting
normally
- Added explicit check to skip auto-serve when the default export is
`globalThis` itself

Fixes #440

## Test plan
- [x] Added test case `test/regression/issue/440.test.ts` that verifies:
  - `module.exports = globalThis` does not start a server
  - `export default globalThis` does not start a server
- [x] Verified test fails with system Bun (without fix)
- [x] Verified test passes with debug build (with fix)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 17:41:21 -08:00
robobun
6104705f5f fix(yaml): fix memory leak in YAML parser (#26090)
## Summary
- Fix memory leak in YAML parser that caused segfaults after high-volume
parsing
- Added `defer parser.deinit()` to free internal data structures
(context, block_indents, anchors, tag_handles, whitespace_buf)
- Fixes #26088

## Test plan
- [x] Added regression test at `test/regression/issue/26088.test.ts`
- [x] Verified YAML parsing still works correctly with debug build
- [x] Ran subset of YAML tests to confirm no regressions


🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-01-14 17:13:30 -08:00
robobun
11aedbe402 fix(fs.watch): emit 'change' events for files in watched directories on Linux (#26009)
## Summary
- Fixes #3657 - `fs.watch` on directory doesn't emit `change` events for
files created after watch starts

When watching a directory with `fs.watch`, files created after the watch
was established would only emit a 'rename' event on creation, but
subsequent modifications would not emit 'change' events.

## Root Cause

The issue was twofold:
1. `watch_dir_mask` in INotifyWatcher.zig was missing `IN.MODIFY`, so
the inotify system call was not subscribed to file modification events
for watched directories.
2. When directory events were processed in path_watcher.zig, all events
were hardcoded to emit 'rename' instead of properly distinguishing
between file creation/deletion ('rename') and file modification
('change').

## Changes

- Adds `IN.MODIFY` to `watch_dir_mask` to receive modification events
- Adds a `create` flag to `WatchEvent.Op` to track `IN.CREATE` events
- Updates directory event processing to emit 'change' for pure write
events and 'rename' for create/delete/move events

## Test plan
- [x] Added regression test `test/regression/issue/3657.test.ts`
- [x] Verified test fails with system Bun (before fix)
- [x] Verified test passes with debug build (after fix)
- [x] Verified manual reproduction from issue now works correctly

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 16:46:20 -08:00
robobun
b72af3d329 fix(compile): respect autoloadBunfig: false when execArgv is present (#26017)
## Summary

Fixes #25640

- Fixed bug where compiled binaries with `autoloadBunfig: false` would
still load `bunfig.toml` when `execArgv` was also provided
- The issue was that `Command.init(.AutoCommand)` was called to parse
execArgv, which loaded bunfig before checking the disable flag

## Test plan

- [x] Added tests for `autoloadBunfig: false` with `execArgv` in
`test/bundler/bundler_compile_autoload.test.ts`
- [x] Verified tests pass with debug build: `bun bd test
test/bundler/bundler_compile_autoload.test.ts`
- [x] Verified tests fail with system bun (demonstrates fix works):
`USE_SYSTEM_BUN=1 bun test test/bundler/bundler_compile_autoload.test.ts
-t "AutoloadBunfigDisabledWithExecArgv"`
- [x] All existing autoload tests still pass (22 tests total)

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 16:08:49 -08:00
robobun
f27c6768ce fix(bundler): include lazy chunks in frontend.files for compiled fullstack builds (#26024)
## Summary

- Fixed lazy-loaded chunks from dynamic imports not appearing in
`frontend.files` when using `--splitting` with `--compile` in fullstack
builds
- Updated `computeChunks.zig` to mark non-entry-point chunks as browser
chunks when they contain browser-targeted files
- Updated `HTMLImportManifest.zig` to include browser chunks from server
builds in the files manifest

Fixes #25628

## Test plan

- [ ] Added regression test `test/regression/issue/25628.test.ts` that
verifies lazy chunks appear in `frontend.files`
- [ ] Manually verified: system bun reports `CHUNK_COUNT:1` (bug), debug
bun reports `CHUNK_COUNT:2` (fix)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 16:08:06 -08:00
robobun
c57d0f73b4 fix(css): preserve logical border-radius properties (#26006)
## Summary
- CSS logical border-radius properties (`border-start-start-radius`,
`border-start-end-radius`, `border-end-end-radius`,
`border-end-start-radius`) were being silently dropped when processed by
the CSS bundler
- The bug was in `src/css/properties/border_radius.zig` where
`VendorPrefix{}` (all fields false) was used instead of `VendorPrefix{
.none = true }` when computing prefixes for logical properties
- This caused the properties to be dropped by a later `isEmpty()` check
since an empty prefix struct was returned

## Test plan
- [x] Added regression test `test/regression/issue/25785.test.ts`
- [x] Verified test fails with system Bun (`USE_SYSTEM_BUN=1 bun test`)
- [x] Verified test passes with fixed bun-debug (`bun bd test`)

Fixes #25785

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-01-14 13:34:31 -08:00
robobun
6a27a25e5b fix(debugger): retroactively report tests when TestReporter.enable is called (#25986)
## Summary
- Fixes #25972: TestReporter domain events not firing when debugger
connects after test discovery

When a debugger client connects and enables the TestReporter domain
after tests have been discovered (e.g., using `--inspect` instead of
`--inspect-wait`), the `TestReporter.found`, `TestReporter.start`, and
`TestReporter.end` events would not fire. This is because tests
discovered without an enabled debugger have `test_id_for_debugger = 0`,
and the event emission code checks for non-zero IDs.

The fix retroactively assigns test IDs and reports discovered tests when
`TestReporter.enable` is called:

1. Check if there's an active test file in collection or execution phase
2. Iterate through the test tree (DescribeScopes and test entries)
3. Assign unique `test_id_for_debugger` values to each test/describe
4. Send `TestReporter.found` events for each discovered test

## Test plan
- [ ] Verify IDE integrations can now receive test telemetry when
connecting after test discovery
- [ ] Ensure existing `--inspect-wait` behavior continues to work
(debugger enabled before discovery)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 13:32:51 -08:00
robobun
2b86ab0cd3 fix(shell): implement long listing format for ls -l builtin (#25991)
## Summary
- Implements the `-l` (long listing) flag functionality for the shell
`ls` builtin
- The flag was being parsed but never used - output was identical to
short format
- Now displays proper long listing format: file type, permissions, hard
link count, UID, GID, size, modification time, and filename

## Test plan
- [x] Added regression test in `test/regression/issue/25831.test.ts`
- [x] Test passes with debug build: `bun bd test
test/regression/issue/25831.test.ts`
- [x] Test fails with system bun (confirming the bug exists):
`USE_SYSTEM_BUN=1 bun test test/regression/issue/25831.test.ts`

Example output with fix:
```
$ bun -e 'import { $ } from "bun"; console.log(await $`ls -l`.text())'
drwxr-xr-x   2  1000  1000     4096 Jan 12 15:30 subdir
-rw-r--r--   1  1000  1000       11 Jan 12 15:30 file.txt
```

Fixes #25831

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 13:31:06 -08:00
robobun
6e6896510a fix(cli): prevent --version/--help interception in standalone executables with compile-exec-argv (#26083)
## Summary

Fixes https://github.com/oven-sh/bun/issues/26082

- Fixes a bug where standalone executables compiled with
`--compile-exec-argv` would intercept `--version`, `-v`, `--help`, and
`-h` flags before user code could handle them
- CLI applications using libraries like `commander` can now properly
implement their own version and help commands

## Root Cause

When `--compile-exec-argv` is used, `Command.init` was being called with
`.AutoCommand`, which parses ALL arguments (including user arguments).
The `Arguments.parse` function intercepts `--version`/`--help` flags for
`AutoCommand`, preventing them from reaching user code.

## Fix

Temporarily set `bun.argv` to only include the executable name +
embedded exec argv options when calling `Command.init`. This ensures:
1. Bun's embedded options (like `--smol`, `--use-system-ca`) are
properly parsed
2. User arguments (including `--version`/`--help`) are NOT intercepted
by Bun's parser
3. User arguments are properly passed through to user code

## Test plan

- [x] Added tests for `--version`, `-v`, `--help`, and `-h` flags in
`compile-argv.test.ts`
- [x] Verified tests fail with `USE_SYSTEM_BUN=1` (proving the bug
exists)
- [x] Verified tests pass with debug build
- [x] Verified existing compile-argv tests still pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 13:10:53 -08:00
robobun
5a71ead8a2 Add CLAUDE.md for Node.js compatibility tests (#26084)
## Summary
- Adds a CLAUDE.md file to `test/js/node/test/parallel/` documenting
that these are official Node.js tests
- Explains that these tests should not be modified since they come from
the Node.js repository
- Documents how to run these tests with debug builds (`bun bd
<file-path>` instead of `bun bd test`)

## Test plan
- [x] Verified file was created correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 13:07:38 -08:00
robobun
a9b5f5cbd1 fix(sql): prevent hang in sequential MySQL transactions with returned array queries (#26048)
## Summary

- Fix a hang in sequential MySQL transactions where an INSERT is awaited
followed by a SELECT returned in an array
- The issue occurred because `handleResultSetOK`'s defer block only
called `queue.advance()` without flushing, causing queries added during
the JS callback to not be properly sent
- Changed to call `flushQueue()` instead of just `advance()` to ensure
data is actually sent to the server

Fixes #26030

## Test plan

- Added regression test `test/regression/issue/26030.test.ts` with three
test cases:
- `Sequential transactions with INSERT and returned SELECT should not
hang` - reproduces the exact pattern from the bug report
- `Sequential transactions with returned array of multiple queries` -
tests returning multiple queries in array
- `Many sequential transactions with awaited INSERT and returned SELECT`
- stress tests with 5 sequential transactions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 12:53:04 -08:00
robobun
7333500df8 fix(bundler): rename named function expressions when shadowing outer symbol (#26027)
## Summary
- Fixed a bug where named function expressions were not renamed when
their name shadowed an outer symbol that's referenced inside the
function body
- This caused infinite recursion at runtime when namespace imports were
inlined
- Particularly affected Svelte 5 apps in dev mode

## Test plan
- [x] Added regression test that reproduces the issue
- [x] Verified test fails with system bun and passes with fix
- [x] Ran bundler tests (bundler_regressions, bundler_naming,
bundler_edgecase, bundler_minify) - all pass

## Root cause
The bundler was skipping `function_args` scopes when renaming symbols.
This meant named function expression names (which are declared in the
function_args scope) were never considered for renaming when they
collided with outer symbols.

For example, this code:
```javascript
import * as $ from './lib';
$.doSomething(function get() {
  return $.get(123);  // Should call outer get
});
```

Would be bundled as:
```javascript
function get(x) { return x * 2; } // from lib
doSomething(function get() {
  return get(123);  // Calls itself - infinite recursion!
});
```

Instead of:
```javascript
function get(x) { return x * 2; }
doSomething(function get2() {  // Renamed to avoid collision
  return get(123);  // Correctly calls outer get
});
```

Fixes #25648

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-01-14 12:52:41 -08:00
robobun
e6733333f0 fix(sql): MySQL VARCHAR with binary collations returns string instead of Buffer (#26064)
## Summary

- Fixed MySQL VARCHAR/CHAR/TEXT columns with binary collations (like
`utf8mb4_bin`) being incorrectly returned as `Buffer` instead of
`string`
- The fix checks for `character_set == 63` (binary collation) in
addition to the BINARY flag to properly distinguish true binary types

Fixes #26063

## Root Cause

PR #26011 introduced a fix for binary column handling that checked
`column.flags.BINARY` to determine if data should be returned as
`Buffer`. However, MySQL sets the BINARY flag on VARCHAR/CHAR/TEXT
columns with binary collations (like `utf8mb4_bin`) even though they
should return strings.

The proper way to detect true binary types (BINARY, VARBINARY, BLOB) is
to check if `character_set == 63` (the "binary" collation), not just the
BINARY flag.

## Changes

1. **Text Protocol** (`ResultSet.zig:143-148`): Updated binary check to
`column.flags.BINARY and column.character_set == 63`
2. **Binary Protocol** (`DecodeBinaryValue.zig:154-156`): Added
`character_set` parameter and updated binary check

## Test plan

- [ ] Added regression test `test/regression/issue/26063.test.ts` that
tests VARCHAR, CHAR, and TEXT columns with `utf8mb4_bin` collation
return strings
- [ ] Test verifies that true BINARY/VARBINARY/BLOB columns still return
Buffers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-01-14 12:50:36 -08:00
Ciro Spaciari
22bebfc467 respect agent options and connectOpts in https module (#25937) 2026-01-14 11:52:53 -08:00
robobun
1800093a64 fix(install): use scope-specific registry for scoped packages in frozen lockfile (#26047)
## Summary
- Fixed `bun install --frozen-lockfile` to use scope-specific registry
for scoped packages when the lockfile has an empty registry URL

When parsing a `bun.lock` file with an empty registry URL for a scoped
package (like `@example/test-package`), bun was unconditionally using
the default npm registry (`https://registry.npmjs.org/`) instead of
looking up the scope-specific registry from `bunfig.toml`.

For example, with this configuration in `bunfig.toml`:
```toml
[install.scopes]
example = { url = "https://npm.pkg.github.com" }
```

And this lockfile entry with an empty registry URL:
```json
"@example/test-package": ["@example/test-package@1.0.0", "", {}, "sha512-AAAA"]
```

bun would try to fetch from
`https://registry.npmjs.org/@example/test-package/-/...` instead of
`https://npm.pkg.github.com/@example/test-package/-/...`.

The fix uses `manager.scopeForPackageName()` (the same pattern used in
`pnpm.zig`) to look up the correct scope-specific registry URL.

## Test plan
- [x] Added regression test `test/regression/issue/026039.test.ts` that
verifies:
  - Scoped packages use the scope-specific registry from `bunfig.toml`
  - Non-scoped packages continue to use the default registry
- [x] Verified test fails with system bun (without fix) and passes with
debug build (with fix)

Fixes #26039

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-01-14 10:22:30 -08:00
Jarred Sumner
967a6a2021 Fix blocking realpathSync call (#26056)
### What does this PR do?

### How did you verify your code works?

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-01-13 23:05:46 -08:00
Jarred Sumner
49d0fbd2de Update 25716.test.ts 2026-01-13 22:38:31 -08:00
Tommy D. Rossi
af2317deb4 fix(bundler): allow reactFastRefresh Bun.build option with non-browser targets (#26035)
### What does this PR do?

Previously, reactFastRefresh was silently ignored when target was not
'browser', even when explicitly enabled. This was confusing as there was
no warning or error.

This change removes the `target == .browser` check, trusting explicit
user intent. If users enable reactFastRefresh with a non-browser target,
the transform will now be applied. If `$RefreshReg$` is not defined at
runtime, it will fail fast with a clear error rather than silently doing
nothing.

Use case: Terminal UIs (like [termcast](https://termcast.app)) need
React Fast Refresh with target: 'bun' for hot reloading in non-browser
environments.

### How did you verify your code works?

Updated existing test removing target browser
2026-01-13 22:13:17 -08:00
robobun
ab009fe00d fix(init): respect --minimal flag for agent rule files (#26051)
## Summary
- Fixes `bun init --minimal` creating Cursor rules files and CLAUDE.md
when it shouldn't
- Adds regression test to verify `--minimal` only creates package.json
and tsconfig.json

## Test plan
- [x] Verify test fails with system bun (unfixed): `USE_SYSTEM_BUN=1 bun
test test/cli/init/init.test.ts -t "bun init --minimal"`
- [x] Verify test passes with debug build: `bun bd test
test/cli/init/init.test.ts -t "bun init --minimal"`
- [x] All existing init tests pass: `bun bd test
test/cli/init/init.test.ts`

Fixes #26050

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 18:33:42 -08:00