mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
aa5ea829a25feb514b6938809fd24f59d2bf0334
4517 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
3536e422e9 | Merge branch 'main' into jarred/repl-mode | ||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
c47f84348a | Update CLAUDE.md | ||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
8da29af1ae | feat(node:inspector): implement Profiler API (#25939) | ||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
d4e5197208 | Fix exception scope verification error | ||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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>
|
||
|
|
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> |
||
|
|
22bebfc467 | respect agent options and connectOpts in https module (#25937) | ||
|
|
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> |
||
|
|
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> |
||
|
|
49d0fbd2de | Update 25716.test.ts | ||
|
|
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 |
||
|
|
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> |