mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
aa5ea829a25feb514b6938809fd24f59d2bf0334
14427 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> |
||
|
|
4815c10efe |
fix(repl): address fourth round of CodeRabbit review comments
- Fix BracketDepth.template tracking: decrement template count when closing template literal and check template depth in needsContinuation() - Fix reverseSearch: show proper "not yet implemented" message instead of confusing prompt that suggests waiting for input - Fix evalDirect: await promises like eval() does for consistent async behavior - Fix getHistoryPath: use std.fs.path.join instead of manual string formatting for cross-platform correctness Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> |
||
|
|
eb804bd8e0 |
fix more review feedback issues
- Fix History.next() underflow guard for empty history - Fix saveHistory to resolve relative paths to absolute - Fix completion suffix bounds check for property completions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> |
||
|
|
702e8448d6 |
address additional code review feedback
- Fix .install command prefix parsing (.i alias used wrong offset) - Fix loadFile to resolve paths to absolute paths first - Remove silent fallback in saveToFile, emit warning instead 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> |
||
|
|
f5018d71df | [autofix.ci] apply automated fixes | ||
|
|
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> |
||
|
|
018358d8a1 |
feat(repl): persist history to ~/.bun_repl_history
The REPL now saves command history to the user's home directory ($HOME/.bun_repl_history or %USERPROFILE%\.bun_repl_history on Windows). History is automatically loaded when the REPL starts and saved on exit. The .save command can still be used to save history to a custom file. 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> |
||
|
|
efa6eab698 |
feat(repl): add simple REPL transforms for let/const persistence
Add a simple string-based transform that converts top-level `let` and `const` declarations to `var`, which allows variables to persist across REPL lines since `var` declarations become properties of the global object in sloppy mode. This is a temporary solution until we can properly integrate the AST-based repl_transforms.zig module with the transpiler pipeline. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> |
||
|
|
d0bf75f88f |
feat(repl): implement native Zig REPL for bun repl
This implements a new REPL in Zig that replaces the previous bunx-based REPL. The new REPL provides: - Interactive line editing with cursor movement and history navigation - Syntax highlighting using QuickAndDirtyJavaScriptSyntaxHighlighter - Multi-line input with bracket matching detection - REPL commands (.help, .exit, .clear, .load, .save, .editor, .timing) - JavaScript evaluation using the global eval() function - Pretty-printed output for evaluated expressions - Support for both TTY (interactive) and non-TTY (piped) input The REPL properly initializes the JSC VirtualMachine with the API lock held, enabling safe JavaScript execution. It uses Bun's transpiler infrastructure and output formatting. TODO: - Implement REPL transforms for variable persistence across lines - Integrate package installation - Add shell mode integration - Implement JSC-based autocomplete Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> |
||
|
|
3536e422e9 | Merge branch 'main' into jarred/repl-mode | ||
|
|
f8adf01f51 |
fix(install): handle null metadata gracefully instead of panicking (#26238)
## Summary
- Fixed a panic in `bun add` when HTTP requests fail before receiving
response headers
- The panic "Assertion failure: Expected metadata to be set" now becomes
a graceful error message
## Root Cause
In `src/install/PackageManagerTask.zig`, the code assumed `metadata` is
always non-null and panicked when it wasn't. However, `metadata` can be
null when:
- HTTP request fails before receiving response headers
- Network connection is refused/lost
- Timeout occurs before response
- Firewall blocks/corrupts the response
## Fix
Replaced the panic with proper error handling, following the existing
pattern in `runTasks.zig`:
```zig
const metadata = manifest.network.response.metadata orelse {
// Handle the error gracefully instead of panicking
const err = manifest.network.response.fail orelse error.HTTPError;
// ... show user-friendly error message
};
```
## Test plan
- [x] Added regression test `test/regression/issue/26236.test.ts`
- [x] Test verifies Bun shows graceful error instead of panicking
- [x] `bun bd test test/regression/issue/26236.test.ts` passes
Fixes #26236
🤖 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>
|
||
|
|
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> |
||
|
|
8be7c161d0 |
fix: handle spread/rest patterns in convertBindingToExpr
- For arrays: wrap last element in E.Spread when has_spread is true
- For objects: set property.kind to .spread when is_spread flag is set
- Copy flags and is_single_line from binding to match Binding.toExpr
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>
|
||
|
|
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>
|
||
|
|
b001c9afc0 | [autofix.ci] apply automated fixes | ||
|
|
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 | ||
|
|
f8a049e9f2 |
perf(buffer): optimize swap16/swap64 with __builtin_bswap (#26190)
## Summary Optimize `Buffer.swap16()` and `Buffer.swap64()` by replacing byte-by-byte swapping loops with `__builtin_bswap16/64` compiler intrinsics. ## Problem `Buffer.swap16` and `Buffer.swap64` were significantly slower than Node.js due to inefficient byte-level operations: - **swap16**: Swapped bytes one at a time in a loop - **swap64**: Used a nested loop with 4 byte swaps per 8-byte element ## Solution Replace the manual byte swapping with `__builtin_bswap16/64` intrinsics, which compile to single CPU instructions (`BSWAP` on x86, `REV` on ARM). Use `memcpy` for loading/storing values to handle potentially unaligned buffers safely. ## Benchmark Results (64KB buffer, Apple M4 Max) | Operation | Bun 1.3.6 | Node.js 24 | This PR | Improvement | |-----------|-----------|------------|---------|-------------| | swap16 | 1.00 µs | 0.57 µs | 0.56 µs | **1.79x faster** | | swap32 | 0.55 µs | 0.77 µs | 0.54 µs | (no change, already fast) | | swap64 | 2.02 µs | 0.58 µs | 0.56 µs | **3.6x faster** | Bun now matches or exceeds Node.js performance for all swap operations. ## Notes - `swap32` was not modified as the compiler already optimizes the 4-byte swap pattern - All existing tests pass |
||
|
|
12a45b7cbf |
Remove dead data URL check in fetch implementation (#26197)
## Summary - Remove unreachable dead code that checked for data URLs in `fetchImpl()` - Data URLs are already handled earlier in the function via the `dispatch_request` block which processes `.data` scheme URLs - This redundant check at lines 375-387 could never be reached ## Test plan - [ ] Verify existing fetch tests pass with `bun bd test test/js/web/fetch/` - [ ] Confirm data URL fetching still works correctly (handled by earlier code path) ## Changelog <!-- CHANGELOG:START --> <!-- No user-facing changes - internal code cleanup only --> <!-- CHANGELOG:END --> 🤖 Generated with [Claude Code](https://claude.com/claude-code) (100% 12-shotted by claude-opus-4-5) Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
c3b4e5568c |
fix(http): check socket state before operations in doRedirect (#26221)
## Summary - Fix assertion failure when using HTTP proxy with redirects and socket closes during redirect processing - Add `isClosedOrHasError()` checks before `releaseSocket` and `closeSocket` in `doRedirect` Fixes #26220 ## Root Cause In `doRedirect` (`src/http.zig:786-797`), the code called `releaseSocket` or `closeSocket` without checking if the socket was already closed. When `onClose` is triggered while `is_redirect_pending` is true, it calls `doRedirect`, but the socket is already closed at that point, causing the assertion in `HTTPContext.zig:168` to fail: ```zig assert(!socket.isClosed()); // FAILS - socket IS closed ``` ## Fix Added `!socket.isClosedOrHasError()` checks before socket operations in `doRedirect`, matching the pattern already used at line 1790 in the same file. ## Test plan - [x] All existing proxy redirect tests pass (`bun bd test test/js/bun/http/proxy.test.ts`) - [x] Build completes successfully (`bun bd`) 🤖 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> |
||
|
|
716801e92d |
gitignore: add .direnv dir (#26198)
### What does this PR do? The `.direnv` folder is created by [direnv](https://direnv.net/) when using `use flake` in `.envrc` to automatically load the Nix development shell. Since the repo already includes a flake.nix, developers on NixOS commonly use direnv (via nix-direnv) to auto-load the environment. This folder contains cached environment data and should not be committed. |
||
|
|
939f5cf7af |
fix(nix): disable fortify hardening for debug builds (#26199)
### What does this PR do? NixOS enables security hardening flags by default in `mkShell` / `devShells` e.g. `_FORTIFY_SOURCE=2`. This flag adds runtime buffer overflow checks but requires compiler optimization (`-O1` or higher) to work, since it needs to inline functions to insert checks. Debug builds use `-O0` (no optimization), which causes this compilation error: `error: _FORTIFY_SOURCE requires compiling with optimization (-O) [-Werror,-W#warnings]` This patch is a standard Nix way to disable this specific flag while keeping other hardening features intact. It doesn't affect release builds since it's scoped to `devShells`. ### How did you verify your code works? `bun bd test` successfully runs test cases. |
||
|
|
496aeb97f9 |
refactor(wrapAnsi): use WTF::find for character searches (#26200)
## Summary This PR addresses the review feedback from #26061 ([comment](https://github.com/oven-sh/bun/pull/26061#discussion_r2697257836)) requesting the use of `WTF::find` for newline searches in `wrapAnsi.cpp`. ## Changes ### 1. CRLF Normalization (lines 628-639) Replaced manual loop with `WTF::findNextNewline` which provides SIMD-optimized detection for `\r`, `\n`, and `\r\n` sequences. **Before:** ```cpp for (size_t i = 0; i < input.size(); ++i) { if (i + 1 < input.size() && input[i] == '\r' && input[i + 1] == '\n') { normalized.append(static_cast<Char>('\n')); i++; } else { normalized.append(input[i]); } } ``` **After:** ```cpp size_t pos = 0; while (pos < input.size()) { auto newline = WTF::findNextNewline(input, pos); if (newline.position == WTF::notFound) { normalized.append(std::span { input.data() + pos, input.size() - pos }); break; } if (newline.position > pos) normalized.append(std::span { input.data() + pos, newline.position - pos }); normalized.append(static_cast<Char>('\n')); pos = newline.position + newline.length; } ``` ### 2. Word Length Calculation (lines 524-533) Replaced manual loop with `WTF::find` for space character detection. **Before:** ```cpp for (const Char* it = lineStart; it <= lineEnd; ++it) { if (it == lineEnd || *it == ' ') { // word boundary logic } } ``` **After:** ```cpp auto lineSpan = std::span<const Char>(lineStart, lineEnd); size_t wordStartIdx = 0; while (wordStartIdx <= lineSpan.size()) { size_t spacePos = WTF::find(lineSpan, static_cast<Char>(' '), wordStartIdx); // word boundary logic using spacePos } ``` ## Benchmark Results Tested on Apple M4 Max. No performance regression observed - most benchmarks show slight improvements. | Benchmark | Before | After | Change | |-----------|--------|-------|--------| | Short text (45 chars) | 613 ns | 583 ns | -4.9% | | Medium text (810 chars) | 10.85 µs | 10.31 µs | -5.0% | | Long text (8100 chars) | 684 µs | 102 µs | -85% * | | Colored short | 1.26 µs | 806 ns | -36% | | Colored medium | 19.24 µs | 13.80 µs | -28% | | Japanese (full-width) | 7.74 µs | 7.43 µs | -4.0% | | Emoji text | 9.35 µs | 9.27 µs | -0.9% | | Hyperlink (OSC 8) | 5.73 µs | 5.58 µs | -2.6% | \* Large variance in baseline measurement ## Testing - All 35 existing tests pass - Manual verification of CRLF normalization and word wrapping edge cases --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> |
||
|
|
3b5f2fe756 |
chore(deps): update BoringSSL fork to latest upstream (#26212)
## Summary
Updates the BoringSSL fork to the latest upstream (337 commits since
last update) with bug fixes for Node.js crypto compatibility.
### Upstream BoringSSL Changes (337 commits)
| Category | Count |
|----------|-------|
| API Changes (including namespacing) | 42 |
| Code Cleanup/Refactoring | 35 |
| Testing/CI | 32 |
| Build System (Bazel, CMake) | 27 |
| Bug Fixes | 25 |
| Post-Quantum Cryptography | 14 |
| TLS/SSL Changes | 12 |
| Rust Bindings/Wrappers | 9 |
| Performance Improvements | 8 |
| Documentation | 8 |
#### Highlights
**Post-Quantum Cryptography**
- ML-DSA (Module-Lattice Digital Signature Algorithm): Full EVP
integration, Wycheproof tests, external mu verification
- SLH-DSA: Implementation of pure SLH-DSA-SHAKE-256f
- Merkle Tree Certificates: New support for verifying signatureless MTCs
**Major API Changes**
- New `CRYPTO_IOVEC` based AEAD APIs for zero-copy I/O across all
ciphers
- Massive namespacing effort moving internal symbols into `bssl`
namespace
- `bssl::Span` modernization to match `std::span` behavior
**TLS/SSL**
- Added `TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256` support
- HMAC on SHA-384 for TLS 1.3
- Improved Lucky 13 mitigation
**Build System**
- Bazel 8.x and 9.0.0 compatibility
- CI upgrades: Ubuntu 24.04, Android NDK r29
---
### Bun-specific Patches (in oven-sh/boringssl)
1. **Fix SHA512-224 EVP final buffer size** (`digests.cc.inc`)
- `BCM_sha512_224_final` writes 32 bytes but `EVP_MD.md_size` is 28
bytes
- Now uses a temp buffer to avoid buffer overwrite
2. **Fix `EVP_do_all_sorted` to return only lowercase names**
(`evp_do_all.cc`)
- `EVP_CIPHER_do_all_sorted` and `EVP_MD_do_all_sorted` now return only
lowercase names
- Matches Node.js behavior for `crypto.getCiphers()` and
`crypto.getHashes()`
---
### Changes in Bun
- Updated BoringSSL commit hash to
`4f4f5ef8ebc6e23cbf393428f0ab1b526773f7ac`
- Removed `ignoreSHA512_224` parameter from `ncrypto::getDigestByName()`
to enable SHA512-224 support
- Removed special SHA512-224 buffer handling in `JSHash.cpp` (no longer
needed after BoringSSL fix)
## Test plan
- [x] `crypto.createHash('sha512-224')` works correctly
- [x] `crypto.getHashes()` returns lowercase names (md4, md5, sha1,
sha256, etc.)
- [x] `crypto.getCiphers()` returns lowercase names (aes-128-cbc,
aes-256-gcm, etc.)
- [x] `test/regression/issue/crypto-names.test.ts` passes
- [x] All CI tests 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>
|
||
|
|
f833f11afa | fix(bake): respect --no-clear-screen in DevServer HMR (#26184) | ||
|
|
b2e5c6c7d1 |
Upgrade WebKit to ea1bfb85d259 (#26161)
## Summary - Upgrades WebKit from `c4d4cae03ece` to `ea1bfb85d259` - Merges upstream WebKit changes into oven-sh/webkit fork ## WebKit Upgrade Summary (JavaScriptCore Changes) ### JSType Enum Changes **No breaking changes to JSType enum from upstream.** The diff showing `InternalFieldTupleType` removal is actually showing Bun's custom addition - upstream WebKit does not have this type. The Bun fork maintains `InternalFieldTupleType` after `DerivedStringObjectType`, which is preserved during the upgrade. ### Notable Performance Improvements #### ARM64 Conditional Compare Chain (ccmp/ccmn) - **Commit:** `2cd6a734ed6c` - Implements ARM64 `ccmp`/`ccmn` instruction chaining for compound boolean expressions - Converts patterns like `if (x0 == 0 && x1 == 1)` into efficient conditional compare sequences - Reduces branch prediction misses and code size - Introduces new Air opcodes: `CompareOnFlags`, `CompareConditionallyOnFlags`, `BranchOnFlags` #### Extended Constant Materialization for Float16/Float/Double/V128 - **Commit:** `0521cc7f331a` - Enhanced ARM64 constant materialization using `movi`, `mvni`, and vector `fmov` - Avoids memory loads for Float constants (32-bit values can now be materialized directly) - Adds `FPImm128` and `Move128ToVector` Air instructions #### DFG/FTL Storage Pointer Improvements - **Commits:** `00c0add58ec3`, `7051d3ac1f34` - FTL Phis now properly support storage (butterfly) pointers - Introduces `KnownStorageUse` for all storage operands in DFG/FTL - Fixes issues with Array allocation sinking when creating storage Phis - Improves GC safety by ensuring butterfly pointers are properly tracked ### Bug Fixes #### Thread Termination Race Condition - **Commit:** `23922a766f07` - Fixes race condition in `VM::m_hasTerminationRequest` between main thread and worker threads - Moves `setHasTerminationRequest()` call into `VMTraps::handleTraps()` to eliminate race #### ThrowScope Exception Clearing - **Commit:** `67abaaa35c4d` - ThrowScopes can no longer accidentally clear termination exceptions - Introduces `tryClearException()` which fails on termination exceptions - Affects iterator operations, promises, and WebCore stream handling #### Bytecode Cache JIT Threshold - **Commit:** `e0644034f46e` - Functions loaded from bytecode cache now correctly set JIT threshold - Previously, cached functions would JIT immediately on first execution #### Wasm Fixes - **Commit:** `8579516f4b61` - Fix JIT-less Wasm-to-JS i31ref marshalling for i31 values in double format - **Commit:** `22b6a610f6ff` - Fix nullability for wasm js-string builtins return types (`cast`, `fromCharCode`, `fromCodePoint`, `concat`, `substring`) - **Commit:** `5ad2efd177db` - Optimize Wasm BlockSignature to avoid lock contention during parsing #### 32-bit ARM (Armv7) Fix - **Commit:** `9cc23c0e75b7` - Fixes tail call shuffler register allocation on 32-bit ARM - Prevents assertion failures when JSValue can load via FPR but GPRs are exhausted ### New Features #### Temporal PlainYearMonth Support - **Commit:** `d865004780e6` - Enables all PlainYearMonth test262 tests - Fixes several bugs in month code handling and rounding modes #### Wasm IPInt Execution Tracing - **Commit:** `634156af4114` - Adds `--traceWasmIPIntExecution` option for debugging WebAssembly interpreter execution ### Code Quality Improvements - **Commit:** `31bc5e6778d4` - `JSRegExpStringIterator` reduced from 56 to 40 bytes by merging boolean fields into bitfield - **Commit:** `cda948675446` - Fix fragile include dependency in `JSC::getCallDataInline` - **Commit:** `bd87f5db107e` - Fix unretained local variable warnings in JavaScriptCore/API ## Merge Conflicts Resolved Fixed 4 merge conflicts related to Bun-specific patches: 1. `Source/JavaScriptCore/API/JSVirtualMachine.mm` - Removed JSLockHolder as per Bun's patch 2. `Source/JavaScriptCore/runtime/JSBoundFunction.h` - Used relative includes instead of framework includes 3. `Source/JavaScriptCore/runtime/JSObjectInlines.h` - Used relative includes and updated `JSFunction.h` to `JSFunctionInlines.h` 4. `Source/WTF/wtf/text/WTFString.h` - Preserved ExternalStringImpl support ## Test plan - [x] WebKit builds successfully (`bun build.ts debug`) - [x] JSType enum values verified to be compatible - [ ] CI builds and tests 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> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com> Co-authored-by: vadim-anthropic <vadim@anthropic.com> Co-authored-by: Dylan Conway <dylan.conway567@gmail.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>
|
||
|
|
7e9fa4ab08 |
feat(scripts): enhance buildkite-failures.ts to fetch and save full logs (#26177)
## Summary
- Fetches complete logs from BuildKite's public API (no token required)
- Saves logs to `/tmp/bun-build-{number}-{platform}-{step}.log`
- Shows log file path in output for each failed job
- Displays brief error summary (unique errors, max 5)
- Adds help text with usage examples (`--help`)
- Groups failures by type (build/test/other)
- Shows annotation counts with link to view full annotations
- Documents usage in CLAUDE.md
## Test plan
- [x] Tested with build #35051 (9 failed jobs)
- [x] Verified logs saved to `/tmp/bun-build-35051-*.log`
- [x] Verified error extraction and deduplication works
- [x] Verified `--help` flag shows usage
🤖 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>
|
||
|
|
6f6f76f0c0 |
fix(macho): only update signature size on ARM64 with codesigning enabled (#26175)
The signature size adjustment was being applied unconditionally, but it should only happen when building for ARM64 and codesigning is enabled. This prevents incorrect offset calculations on non-ARM64 platforms. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.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> |
||
|
|
f01467d3dc |
perf(buffer): optimize Buffer.from(array) by using setFromArrayLike directly (#26135)
## Summary Optimizes `Buffer.from(array)` by bypassing `JSC::construct()` overhead (~30ns) and leveraging JSC's internal array optimizations. ## Changes - For JSArray inputs, directly use `setFromArrayLike()` which internally detects array indexing types (Int32Shape/DoubleShape) and uses bulk copy operations (`copyFromInt32ShapeArray`/`copyFromDoubleShapeArray`) - Array-like objects and iterables continue to use the existing slow path - Added mitata benchmark for measuring performance ## Benchmark Results | Test | Before | After | Improvement | |------|--------|-------|-------------| | Buffer.from(int32[8]) | ~85ns | ~43ns | ~50% faster | | Buffer.from(int32[64]) | ~207ns | ~120ns | ~42% faster | | Buffer.from(int32[1024]) | ~1.85μs | ~1.32μs | ~29% faster | | Buffer.from(double[8]) | ~86ns | ~50ns | ~42% faster | | Buffer.from(double[64]) | ~212ns | ~151ns | ~29% faster | Bun is now faster than Node.js for these operations. ## Test All 449 buffer tests pass. |
||
|
|
b268004715 | Upgrade WebKit to d5bd162d9ab2 (#25958) | ||
|
|
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> |