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>
keyof unknown is used (#25460)
Bun
Read the docs →
What is Bun?
Bun is an all-in-one toolkit for JavaScript and TypeScript apps. It ships as a single executable called bun.
At its core is the Bun runtime, a fast JavaScript runtime designed as a drop-in replacement for Node.js. It's written in Zig and powered by JavaScriptCore under the hood, dramatically reducing startup times and memory usage.
bun run index.tsx # TS and JSX supported out-of-the-box
The bun command-line tool also implements a test runner, script runner, and Node.js-compatible package manager. Instead of 1,000 node_modules for development, you only need bun. Bun's built-in tools are significantly faster than existing options and usable in existing Node.js projects with little to no changes.
bun test # run tests
bun run start # run the `start` script in `package.json`
bun install <pkg> # install a package
bunx cowsay 'Hello, world!' # execute a package
Install
Bun supports Linux (x64 & arm64), macOS (x64 & Apple Silicon) and Windows (x64).
Linux users — Kernel version 5.6 or higher is strongly recommended, but the minimum is 5.1.
x64 users — if you see "illegal instruction" or similar errors, check our CPU requirements
# with install script (recommended)
curl -fsSL https://bun.com/install | bash
# on windows
powershell -c "irm bun.sh/install.ps1 | iex"
# with npm
npm install -g bun
# with Homebrew
brew tap oven-sh/bun
brew install bun
# with Docker
docker pull oven/bun
docker run --rm --init --ulimit memlock=-1:-1 oven/bun
Upgrade
To upgrade to the latest version of Bun, run:
bun upgrade
Bun automatically releases a canary build on every commit to main. To upgrade to the latest canary build, run:
bun upgrade --canary
Quick links
-
Intro
-
Templating
-
CLI
-
Runtime
-
Package manager
-
Bundler
-
Test runner
-
Package runner
-
API
- HTTP server (
Bun.serve) - WebSockets
- Workers
- Binary data
- Streams
- File I/O (
Bun.file) - import.meta
- SQLite (
bun:sqlite) - PostgreSQL (
Bun.sql) - Redis (
Bun.redis) - S3 Client (
Bun.s3) - FileSystemRouter
- TCP sockets
- UDP sockets
- Globals
- $ Shell
- Child processes (spawn)
- Transpiler (
Bun.Transpiler) - Hashing
- Colors (
Bun.color) - Console
- FFI (
bun:ffi) - C Compiler (
bun:fficc) - HTMLRewriter
- Testing (
bun:test) - Cookies (
Bun.Cookie) - Utils
- Node-API
- Glob (
Bun.Glob) - Semver (
Bun.semver) - DNS
- fetch API extensions
- HTTP server (
Guides
-
Binary
- Convert a Blob to a string
- Convert a Buffer to a blob
- Convert a Blob to a DataView
- Convert a Buffer to a string
- Convert a Blob to a ReadableStream
- Convert a Blob to a Uint8Array
- Convert a DataView to a string
- Convert a Uint8Array to a Blob
- Convert a Blob to an ArrayBuffer
- Convert an ArrayBuffer to a Blob
- Convert a Buffer to a Uint8Array
- Convert a Uint8Array to a Buffer
- Convert a Uint8Array to a string
- Convert a Buffer to an ArrayBuffer
- Convert an ArrayBuffer to a Buffer
- Convert an ArrayBuffer to a string
- Convert a Uint8Array to a DataView
- Convert a Buffer to a ReadableStream
- Convert a Uint8Array to an ArrayBuffer
- Convert an ArrayBuffer to a Uint8Array
- Convert an ArrayBuffer to an array of numbers
- Convert a Uint8Array to a ReadableStream
-
Ecosystem
- Use React and JSX
- Use Gel with Bun
- Use Prisma with Bun
- Add Sentry to a Bun app
- Create a Discord bot
- Run Bun as a daemon with PM2
- Use Drizzle ORM with Bun
- Build an app with Nuxt and Bun
- Build an app with Qwik and Bun
- Build an app with Astro and Bun
- Build an app with Remix and Bun
- Build a frontend using Vite and Bun
- Build an app with Next.js and Bun
- Run Bun as a daemon with systemd
- Deploy a Bun application on Render
- Build an HTTP server using Hono and Bun
- Build an app with SvelteKit and Bun
- Build an app with SolidStart and Bun
- Build an HTTP server using Elysia and Bun
- Build an HTTP server using StricJS and Bun
- Containerize a Bun application with Docker
- Build an HTTP server using Express and Bun
- Use Neon Postgres through Drizzle ORM
- Server-side render (SSR) a React component
- Read and write data to MongoDB using Mongoose and Bun
- Use Neon's Serverless Postgres with Bun
-
HTMLRewriter
-
HTTP
- Hot reload an HTTP server
- Common HTTP server usage
- Write a simple HTTP server
- Configure TLS on an HTTP server
- Send an HTTP request using fetch
- Proxy HTTP requests using fetch()
- Start a cluster of HTTP servers
- Stream a file as an HTTP Response
- fetch with unix domain sockets in Bun
- Upload files via HTTP using FormData
- Streaming HTTP Server with Async Iterators
- Streaming HTTP Server with Node.js Streams
-
Install
- Add a dependency
- Add a Git dependency
- Add a peer dependency
- Add a trusted dependency
- Add a development dependency
- Add a tarball dependency
- Add an optional dependency
- Generate a yarn-compatible lockfile
- Configuring a monorepo using workspaces
- Install a package under a different name
- Install dependencies with Bun in GitHub Actions
- Using bun install with Artifactory
- Configure git to diff Bun's lockb lockfile
- Override the default npm registry for bun install
- Using bun install with an Azure Artifacts npm registry
- Migrate from npm install to bun install
- Configure a private registry for an organization scope with bun install
-
Process
-
Read file
-
Runtime
- Delete files
- Run a Shell Command
- Import a JSON file
- Import a TOML file
- Set a time zone in Bun
- Set environment variables
- Re-map import paths
- Delete directories
- Read environment variables
- Import a HTML file as text
- Install and run Bun in GitHub Actions
- Debugging Bun with the web debugger
- Install TypeScript declarations for Bun
- Debugging Bun with the VS Code extension
- Inspect memory usage using V8 heap snapshots
- Define and replace static globals & constants
- Codesign a single-file JavaScript executable on macOS
-
Streams
- Convert a ReadableStream to JSON
- Convert a ReadableStream to a Blob
- Convert a ReadableStream to a Buffer
- Convert a ReadableStream to a string
- Convert a ReadableStream to a Uint8Array
- Convert a ReadableStream to an array of chunks
- Convert a Node.js Readable to JSON
- Convert a ReadableStream to an ArrayBuffer
- Convert a Node.js Readable to a Blob
- Convert a Node.js Readable to a string
- Convert a Node.js Readable to an Uint8Array
- Convert a Node.js Readable to an ArrayBuffer
-
Test
- Spy on methods in
bun test - Bail early with the Bun test runner
- Mock functions in
bun test - Run tests in watch mode with Bun
- Use snapshot testing in
bun test - Skip tests with the Bun test runner
- Using Testing Library with Bun
- Update snapshots in
bun test - Run your tests with the Bun test runner
- Set the system time in Bun's test runner
- Set a per-test timeout with the Bun test runner
- Migrate from Jest to Bun's test runner
- Write browser DOM tests with Bun and happy-dom
- Mark a test as a "todo" with the Bun test runner
- Re-run tests multiple times with the Bun test runner
- Generate code coverage reports with the Bun test runner
- import, require, and test Svelte components with bun test
- Set a code coverage threshold with the Bun test runner
- Spy on methods in
-
Util
- Generate a UUID
- Hash a password
- Escape an HTML string
- Get the current Bun version
- Encode and decode base64 strings
- Compress and decompress data with gzip
- Sleep for a fixed number of milliseconds
- Detect when code is executed with Bun
- Check if two objects are deeply equal
- Compress and decompress data with DEFLATE
- Get the absolute path to the current entrypoint
- Get the directory of the current file
- Check if the current file is the entrypoint
- Get the file name of the current file
- Convert a file URL to an absolute path
- Convert an absolute path to a file URL
- Get the absolute path of the current file
- Get the path to an executable bin file
-
WebSocket
-
Write file
Contributing
Refer to the Project > Contributing guide to start contributing to Bun.
License
Refer to the Project > License page for information about Bun's licensing.