Compare commits

...

16 Commits

Author SHA1 Message Date
autofix-ci[bot]
da3980843a [autofix.ci] apply automated fixes 2026-01-20 22:10:02 +00:00
Claude Bot
bef209431a feat(repl): integrate replMode transforms for proper REPL evaluation
This integrates the replMode transpiler transforms into the native Zig REPL,
providing proper handling of:

- Expression result capture via { value: expr } wrapper
- Variable hoisting for persistence across REPL lines (const/let → var)
- Function and class declaration hoisting
- Top-level await support with async IIFE wrapper
- Object literal detection (no parentheses needed for { a: 1 })

Key changes:
- Add transformForRepl() function in repl.zig that uses the parser with
  repl_mode=true to apply REPL-specific AST transforms
- Handle async IIFE results by awaiting promises before extracting values
- Extract the actual value from the { value: expr } wrapper object
- Set _ only after extracting the inner value (moved from C++ to Zig)
- Set _error on global for rejected promises
- Disable dead_code_elimination in parser options (REPL needs all code)
- Enable top_level_await in parser options

The C++ Bun__REPL__evaluate no longer sets _ since the Zig code now
handles this after extracting the value from the REPL transform wrapper.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 22:08:18 +00:00
Claude Bot
baf1bbda8b feat(transpiler): add replMode option for REPL transforms
Cherry-pick replMode feature from jarred/repl-mode branch:
- Add `replMode` option to Bun.Transpiler for REPL transforms
- Wraps expressions in { value: expr } for result capture
- Hoists var/let/const declarations for persistence across REPL lines
- Hoists function/class declarations with var for vm context persistence
- Auto-detects object literals (starting with { without trailing ;)
- Uses sync/async IIFE wrappers based on top-level await presence

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 20:35:13 +00:00
Claude Bot
4dd8b288cd fix(repl): add Windows platform compatibility
- Use conditional compilation for POSIX-specific termios APIs
- Use bun.getenvZ for environment variables on Windows
- Use bun.FileDescriptor.fromUV(0) for stdin on Windows
- Use USERPROFILE as fallback for HOME on Windows

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 20:19:54 +00:00
autofix-ci[bot]
e154f1750a [autofix.ci] apply automated fixes 2026-01-20 19:19:24 +00:00
Claude Bot
163623e775 test(repl): add comprehensive tests for native Zig REPL
Tests cover:
- Basic JavaScript evaluation (expressions, strings, objects, arrays)
- Special variables (_ and _error)
- REPL commands (.help, .exit, .clear)
- Error handling (syntax errors, runtime errors, thrown errors)
- Global objects (Bun, console, Buffer, process)
- Variable persistence across evaluations
- Async evaluation (promises, async functions)
- Welcome message and version display
- Terminal (PTY) integration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 19:16:59 +00:00
Claude Bot
32e2540e56 fix(repl): use globalObject instead of globalThis for _ and _error variables
The globalThis() method returns a JSGlobalProxy which doesn't expose
properties set via putDirect to the global scope. By putting directly
on globalObject instead, the _ and _error special REPL variables are
now visible when referenced in subsequent expressions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 19:08:00 +00:00
Claude Bot
974fb9a272 feat(repl): implement native Zig REPL with full TUI support
This commit introduces a native Zig implementation of `bun repl` that
provides a modern, feature-rich interactive JavaScript environment.

Key features:
- Syntax highlighting using QuickAndDirtySyntaxHighlighter
- Full line editing with Emacs-style keybindings (Ctrl+A/E/K/U/W, etc.)
- Persistent history saved to ~/.bun_repl_history
- Tab completion for global properties
- Multi-line input detection (unclosed brackets/strings)
- REPL commands: .help, .exit, .clear, .load, .save, .editor, .break
- Raw terminal mode for smooth character-by-character input
- Result formatting via util.inspect integration
- Special REPL variables: _ (last result), _error (last error)

The implementation consists of:
- src/repl.zig: ~1500 line REPL implementation
  - LineEditor: cursor movement, editing, clipboard
  - History: load/save from file, navigation
  - Syntax highlighting with ANSI colors
  - JavaScript evaluation via C++ bindings
- src/cli/repl_command.zig: CLI integration and VM setup
- C++ bindings in bindings.cpp for JSC evaluation and completion

The native REPL provides faster startup and better integration with
Bun's toolkit compared to the previous TypeScript implementation.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 18:58:00 +00:00
Claude Bot
43f5df2596 fix: use platform-aware path joining for temp file creation
- Use bun.path.joinAbsStringBufZ for cross-platform path construction
- Use std.posix.toPosixPath for null-terminating temp_dir
- Simplify code by using filename directly instead of re-extracting basename
- Use bufPrintZ for null-terminated filename buffer

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 08:24:00 +00:00
Claude Bot
c189cdc60f fix: prevent infinite loop when write returns 0 bytes
Treat write returning 0 bytes as a fatal error to prevent hanging
if the write syscall unexpectedly returns zero.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 08:11:26 +00:00
Claude Bot
d630b68d15 fix: handle partial writes in REPL temp file creation
Loop until all bytes are written to handle potential partial writes
from bun.sys.write().

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 08:02:13 +00:00
Claude Bot
768b60ebf1 fix: address additional review feedback for built-in REPL
- Use cross-platform PID: std.c.getpid() on POSIX, GetCurrentProcessId() on Windows
- Resolve .load command paths relative to process.cwd()
- Remove empty placeholder test file

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 07:50:12 +00:00
autofix-ci[bot]
ea511ed08c [autofix.ci] apply automated fixes 2026-01-14 07:36:17 +00:00
Claude Bot
804c716f8f address code review feedback
- Use platformTempDir() instead of hardcoded /tmp for cross-platform support
- Add unique PID-based suffix to temp file to prevent collisions
- Clean up temp file after REPL exits using defer unlinkat
- Handle os.homedir() edge case when $HOME is unset
- Debounce history writes to reduce disk I/O
- Flush pending history on REPL close
- Consolidate tests to reduce duplication
- Add assertion that REPL prints "Welcome to Bun"

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 07:34:36 +00:00
autofix-ci[bot]
c30b36f433 [autofix.ci] apply automated fixes 2026-01-14 07:19:06 +00:00
Claude Bot
78f1d497f4 feat(cli): add built-in REPL to improve startup time
Replace the external `bun-repl` npm package with a built-in REPL
implementation. This significantly improves `bun repl` startup time
by eliminating the need to download and run an external package.

Changes:
- Add `src/cli/repl_command.zig` - embeds and runs the REPL script
- Add `src/js/eval/repl.ts` - JavaScript REPL implementation using
  node:readline and node:vm
- Update `src/cli.zig` to use the new ReplCommand

Features of the built-in REPL:
- Interactive JavaScript/TypeScript evaluation
- Command history with persistence (~/.bun_repl_history)
- REPL commands: .help, .exit, .clear, .load
- Multi-line input support for incomplete expressions
- Tab completion for global properties and REPL commands
- Color output in TTY mode
- Proper error formatting

Fixes #26058

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 07:17:20 +00:00
18 changed files with 3526 additions and 20 deletions

View File

@@ -1654,6 +1654,17 @@ declare module "bun" {
* @default "warn"
*/
logLevel?: "verbose" | "debug" | "info" | "warn" | "error";
/**
* Enable REPL mode transforms:
* - Wraps top-level inputs that appear to be object literals (inputs starting with '{' without trailing ';') in parentheses
* - Hoists all declarations as var for REPL persistence across vm.runInContext calls
* - Wraps last expression in { __proto__: null, value: expr } for result capture
* - Wraps code in sync/async IIFE to avoid parentheses around object literals
*
* @default false
*/
replMode?: boolean;
}
/**

View File

@@ -6467,6 +6467,11 @@ pub fn NewParser_(
parts.items[0].stmts = top_level_stmts;
}
// REPL mode transforms
if (p.options.repl_mode) {
try repl_transforms.ReplTransforms(P).apply(p, parts, allocator);
}
var top_level_symbols_to_parts = js_ast.Ast.TopLevelSymbolToParts{};
var top_level = &top_level_symbols_to_parts;
@@ -6760,6 +6765,8 @@ var falseValueExpr = Expr.Data{ .e_boolean = E.Boolean{ .value = false } };
const string = []const u8;
const repl_transforms = @import("./repl_transforms.zig");
const Define = @import("../defines.zig").Define;
const DefineData = @import("../defines.zig").DefineData;

View File

@@ -38,6 +38,13 @@ pub const Parser = struct {
/// able to customize what import sources are used.
framework: ?*bun.bake.Framework = null,
/// 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
/// - Wraps code with await in async IIFE
repl_mode: bool = false,
pub fn hashForRuntimeTranspiler(this: *const Options, hasher: *std.hash.Wyhash, did_use_jsx: bool) void {
bun.assert(!this.bundle);

365
src/ast/repl_transforms.zig Normal file
View File

@@ -0,0 +1,365 @@
/// REPL Transform module - transforms code for interactive REPL evaluation
///
/// This module provides transformations for REPL mode:
/// - Wraps the last expression in { value: expr } for result capture
/// - Wraps code with await in async IIFE with variable hoisting
/// - Hoists declarations for variable persistence across REPL lines
pub fn ReplTransforms(comptime P: type) type {
return struct {
const Self = @This();
/// Apply REPL-mode transforms to the AST.
/// This transforms code for interactive evaluation:
/// - Wraps the last expression in { value: expr } for result capture
/// - Wraps code with await in async IIFE with variable hoisting
pub fn apply(p: *P, parts: *ListManaged(js_ast.Part), allocator: Allocator) !void {
// Skip transform if there's a top-level return (indicates module pattern)
if (p.has_top_level_return) {
return;
}
// Collect all statements
var total_stmts_count: usize = 0;
for (parts.items) |part| {
total_stmts_count += part.stmts.len;
}
if (total_stmts_count == 0) {
return;
}
// Check if there's top-level await
const has_top_level_await = p.top_level_await_keyword.len > 0;
// Collect all statements into a single array
var all_stmts = bun.handleOom(allocator.alloc(Stmt, total_stmts_count));
var stmt_idx: usize = 0;
for (parts.items) |part| {
for (part.stmts) |stmt| {
all_stmts[stmt_idx] = stmt;
stmt_idx += 1;
}
}
// Apply transform with is_async based on presence of top-level await
try transformWithHoisting(p, parts, all_stmts, allocator, has_top_level_await);
}
/// Transform code with hoisting and IIFE wrapper
/// @param is_async: true for async IIFE (when top-level await present), false for sync IIFE
fn transformWithHoisting(
p: *P,
parts: *ListManaged(js_ast.Part),
all_stmts: []Stmt,
allocator: Allocator,
is_async: bool,
) !void {
if (all_stmts.len == 0) return;
// Lists for hoisted declarations and inner statements
var hoisted_stmts = ListManaged(Stmt).init(allocator);
var inner_stmts = ListManaged(Stmt).init(allocator);
try hoisted_stmts.ensureTotalCapacity(all_stmts.len);
try inner_stmts.ensureTotalCapacity(all_stmts.len);
// Process each statement - hoist all declarations for REPL persistence
for (all_stmts) |stmt| {
switch (stmt.data) {
.s_local => |local| {
// Hoist all declarations as var so they become context properties
// In sloppy mode, var at top level becomes a property of the global/context object
// This is essential for REPL variable persistence across vm.runInContext calls
const kind: S.Local.Kind = .k_var;
// Extract individual identifiers from binding patterns for hoisting
var hoisted_decl_list = ListManaged(G.Decl).init(allocator);
for (local.decls.slice()) |decl| {
try extractIdentifiersFromBinding(p, decl.binding, &hoisted_decl_list);
}
if (hoisted_decl_list.items.len > 0) {
try hoisted_stmts.append(p.s(S.Local{
.kind = kind,
.decls = Decl.List.fromOwnedSlice(hoisted_decl_list.items),
}, stmt.loc));
}
// Create assignment expressions for the inner statements
for (local.decls.slice()) |decl| {
if (decl.value) |value| {
// Create assignment expression: binding = value
const assign_expr = createBindingAssignment(p, decl.binding, value, allocator);
try inner_stmts.append(p.s(S.SExpr{ .value = assign_expr }, stmt.loc));
}
}
},
.s_function => |func| {
// For function declarations:
// Hoist as: var funcName;
// Inner: this.funcName = funcName; function funcName() {}
if (func.func.name) |name_loc| {
try hoisted_stmts.append(p.s(S.Local{
.kind = .k_var,
.decls = Decl.List.fromOwnedSlice(bun.handleOom(allocator.dupe(G.Decl, &.{
G.Decl{
.binding = p.b(B.Identifier{ .ref = name_loc.ref.? }, name_loc.loc),
.value = null,
},
}))),
}, stmt.loc));
// Add this.funcName = funcName assignment
const this_expr = p.newExpr(E.This{}, stmt.loc);
const this_dot = p.newExpr(E.Dot{
.target = this_expr,
.name = p.symbols.items[name_loc.ref.?.innerIndex()].original_name,
.name_loc = name_loc.loc,
}, stmt.loc);
const func_id = p.newExpr(E.Identifier{ .ref = name_loc.ref.? }, name_loc.loc);
const assign = p.newExpr(E.Binary{
.op = .bin_assign,
.left = this_dot,
.right = func_id,
}, stmt.loc);
try inner_stmts.append(p.s(S.SExpr{ .value = assign }, stmt.loc));
}
// Add the function declaration itself
try inner_stmts.append(stmt);
},
.s_class => |class| {
// For class declarations:
// Hoist as: var ClassName; (use var so it persists to vm context)
// Inner: ClassName = class ClassName {}
if (class.class.class_name) |name_loc| {
try hoisted_stmts.append(p.s(S.Local{
.kind = .k_var,
.decls = Decl.List.fromOwnedSlice(bun.handleOom(allocator.dupe(G.Decl, &.{
G.Decl{
.binding = p.b(B.Identifier{ .ref = name_loc.ref.? }, name_loc.loc),
.value = null,
},
}))),
}, stmt.loc));
// Convert class declaration to assignment: ClassName = class ClassName {}
const class_expr = p.newExpr(class.class, stmt.loc);
const class_id = p.newExpr(E.Identifier{ .ref = name_loc.ref.? }, name_loc.loc);
const assign = p.newExpr(E.Binary{
.op = .bin_assign,
.left = class_id,
.right = class_expr,
}, stmt.loc);
try inner_stmts.append(p.s(S.SExpr{ .value = assign }, stmt.loc));
} else {
try inner_stmts.append(stmt);
}
},
.s_directive => |directive| {
// In REPL mode, treat directives (string literals) as expressions
const str_expr = p.newExpr(E.String{ .data = directive.value }, stmt.loc);
try inner_stmts.append(p.s(S.SExpr{ .value = str_expr }, stmt.loc));
},
else => {
try inner_stmts.append(stmt);
},
}
}
// Wrap the last expression in return { value: expr }
wrapLastExpressionWithReturn(p, &inner_stmts, allocator);
// Create the IIFE: (() => { ...inner_stmts... })() or (async () => { ... })()
const arrow = p.newExpr(E.Arrow{
.args = &.{},
.body = .{ .loc = logger.Loc.Empty, .stmts = inner_stmts.items },
.is_async = is_async,
}, logger.Loc.Empty);
const iife = p.newExpr(E.Call{
.target = arrow,
.args = ExprNodeList{},
}, logger.Loc.Empty);
// Final output: hoisted declarations + IIFE call
const final_stmts_count = hoisted_stmts.items.len + 1;
var final_stmts = bun.handleOom(allocator.alloc(Stmt, final_stmts_count));
for (hoisted_stmts.items, 0..) |stmt, j| {
final_stmts[j] = stmt;
}
final_stmts[hoisted_stmts.items.len] = p.s(S.SExpr{ .value = iife }, logger.Loc.Empty);
// Update parts
if (parts.items.len > 0) {
parts.items[0].stmts = final_stmts;
parts.items.len = 1;
}
}
/// Wrap the last expression in return { value: expr }
fn wrapLastExpressionWithReturn(p: *P, inner_stmts: *ListManaged(Stmt), allocator: Allocator) void {
if (inner_stmts.items.len > 0) {
var last_idx: usize = inner_stmts.items.len;
while (last_idx > 0) {
last_idx -= 1;
const last_stmt = inner_stmts.items[last_idx];
switch (last_stmt.data) {
.s_empty, .s_comment => continue,
.s_expr => |expr_data| {
// Wrap in return { value: expr }
const wrapped = wrapExprInValueObject(p, expr_data.value, allocator);
inner_stmts.items[last_idx] = p.s(S.Return{ .value = wrapped }, last_stmt.loc);
break;
},
else => break,
}
}
}
}
/// Extract individual identifiers from a binding pattern for hoisting
fn extractIdentifiersFromBinding(p: *P, binding: Binding, decls: *ListManaged(G.Decl)) !void {
switch (binding.data) {
.b_identifier => |ident| {
try decls.append(G.Decl{
.binding = p.b(B.Identifier{ .ref = ident.ref }, binding.loc),
.value = null,
});
},
.b_array => |arr| {
for (arr.items) |item| {
try extractIdentifiersFromBinding(p, item.binding, decls);
}
},
.b_object => |obj| {
for (obj.properties) |prop| {
try extractIdentifiersFromBinding(p, prop.value, decls);
}
},
.b_missing => {},
}
}
/// Create { __proto__: null, value: expr } wrapper object
/// Uses null prototype to create a clean data object
fn wrapExprInValueObject(p: *P, expr: Expr, allocator: Allocator) Expr {
var properties = bun.handleOom(allocator.alloc(G.Property, 2));
// __proto__: null - creates null-prototype object
properties[0] = G.Property{
.key = p.newExpr(E.String{ .data = "__proto__" }, expr.loc),
.value = p.newExpr(E.Null{}, expr.loc),
};
// value: expr - the actual result value
properties[1] = G.Property{
.key = p.newExpr(E.String{ .data = "value" }, expr.loc),
.value = expr,
};
return p.newExpr(E.Object{
.properties = G.Property.List.fromOwnedSlice(properties),
}, expr.loc);
}
/// Create assignment expression from binding pattern
fn createBindingAssignment(p: *P, binding: Binding, value: Expr, allocator: Allocator) Expr {
switch (binding.data) {
.b_identifier => |ident| {
return p.newExpr(E.Binary{
.op = .bin_assign,
.left = p.newExpr(E.Identifier{ .ref = ident.ref }, binding.loc),
.right = value,
}, binding.loc);
},
.b_array => {
// For array destructuring, create: [a, b] = value
return p.newExpr(E.Binary{
.op = .bin_assign,
.left = convertBindingToExpr(p, binding, allocator),
.right = value,
}, binding.loc);
},
.b_object => {
// For object destructuring, create: {a, b} = value
return p.newExpr(E.Binary{
.op = .bin_assign,
.left = convertBindingToExpr(p, binding, allocator),
.right = value,
}, binding.loc);
},
.b_missing => {
// Return Missing expression to match convertBindingToExpr
return p.newExpr(E.Missing{}, binding.loc);
},
}
}
/// Convert a binding pattern to an expression (for assignment targets)
/// Handles spread/rest patterns in arrays and objects to match Binding.toExpr behavior
fn convertBindingToExpr(p: *P, binding: Binding, allocator: Allocator) Expr {
switch (binding.data) {
.b_identifier => |ident| {
return p.newExpr(E.Identifier{ .ref = ident.ref }, binding.loc);
},
.b_array => |arr| {
var items = bun.handleOom(allocator.alloc(Expr, arr.items.len));
for (arr.items, 0..) |item, i| {
const expr = convertBindingToExpr(p, item.binding, allocator);
// Check for spread pattern: if has_spread and this is the last element
if (arr.has_spread and i == arr.items.len - 1) {
items[i] = p.newExpr(E.Spread{ .value = expr }, expr.loc);
} else if (item.default_value) |default_val| {
items[i] = p.newExpr(E.Binary{
.op = .bin_assign,
.left = expr,
.right = default_val,
}, item.binding.loc);
} else {
items[i] = expr;
}
}
return p.newExpr(E.Array{
.items = ExprNodeList.fromOwnedSlice(items),
.is_single_line = arr.is_single_line,
}, binding.loc);
},
.b_object => |obj| {
var properties = bun.handleOom(allocator.alloc(G.Property, obj.properties.len));
for (obj.properties, 0..) |prop, i| {
properties[i] = G.Property{
.flags = prop.flags,
.key = prop.key,
// Set kind to .spread if the property has spread flag
.kind = if (prop.flags.contains(.is_spread)) .spread else .normal,
.value = convertBindingToExpr(p, prop.value, allocator),
.initializer = prop.default_value,
};
}
return p.newExpr(E.Object{
.properties = G.Property.List.fromOwnedSlice(properties),
.is_single_line = obj.is_single_line,
}, binding.loc);
},
.b_missing => {
return p.newExpr(E.Missing{}, binding.loc);
},
}
}
};
}
const std = @import("std");
const Allocator = std.mem.Allocator;
const ListManaged = std.array_list.Managed;
const bun = @import("bun");
const logger = bun.logger;
const js_ast = bun.ast;
const B = js_ast.B;
const Binding = js_ast.Binding;
const E = js_ast.E;
const Expr = js_ast.Expr;
const ExprNodeList = js_ast.ExprNodeList;
const S = js_ast.S;
const Stmt = js_ast.Stmt;
const G = js_ast.G;
const Decl = G.Decl;

View File

@@ -42,6 +42,7 @@ pub const Config = struct {
minify_identifiers: bool = false,
minify_syntax: bool = false,
no_macros: bool = false,
repl_mode: bool = false,
pub fn fromJS(this: *Config, globalThis: *jsc.JSGlobalObject, object: jsc.JSValue, allocator: std.mem.Allocator) bun.JSError!void {
if (object.isUndefinedOrNull()) {
@@ -245,6 +246,10 @@ pub const Config = struct {
this.dead_code_elimination = flag;
}
if (try object.getBooleanLoose(globalThis, "replMode")) |flag| {
this.repl_mode = flag;
}
if (try object.getTruthy(globalThis, "minify")) |minify| {
if (minify.isBoolean()) {
this.minify_whitespace = minify.toBoolean();
@@ -698,7 +703,8 @@ pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) b
transpiler.options.macro_remap = config.macro_map;
}
transpiler.options.dead_code_elimination = config.dead_code_elimination;
// REPL mode disables DCE to preserve expressions like `42`
transpiler.options.dead_code_elimination = config.dead_code_elimination and !config.repl_mode;
transpiler.options.minify_whitespace = config.minify_whitespace;
// Keep defaults for these
@@ -717,6 +723,7 @@ pub fn constructor(globalThis: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) b
transpiler.options.inlining = config.runtime.inlining;
transpiler.options.hot_module_reloading = config.runtime.hot_module_reloading;
transpiler.options.react_fast_refresh = false;
transpiler.options.repl_mode = config.repl_mode;
return this;
}
@@ -738,9 +745,47 @@ pub fn deinit(this: *JSTranspiler) void {
bun.destroy(this);
}
/// Check if code looks like an object literal that would be misinterpreted as a block
/// Returns true if code starts with { (after whitespace) and doesn't end with ;
/// This matches Node.js REPL behavior for object literal disambiguation
fn isLikelyObjectLiteral(code: []const u8) bool {
// Skip leading whitespace
var start: usize = 0;
while (start < code.len and (code[start] == ' ' or code[start] == '\t' or code[start] == '\n' or code[start] == '\r')) {
start += 1;
}
// Check if starts with {
if (start >= code.len or code[start] != '{') {
return false;
}
// Skip trailing whitespace
var end: usize = code.len;
while (end > 0 and (code[end - 1] == ' ' or code[end - 1] == '\t' or code[end - 1] == '\n' or code[end - 1] == '\r')) {
end -= 1;
}
// Check if ends with semicolon - if so, it's likely a block statement
if (end > 0 and code[end - 1] == ';') {
return false;
}
return true;
}
fn getParseResult(this: *JSTranspiler, allocator: std.mem.Allocator, code: []const u8, loader: ?Loader, macro_js_ctx: Transpiler.MacroJSValueType) ?Transpiler.ParseResult {
const name = this.config.default_loader.stdinName();
const source = &logger.Source.initPathString(name, code);
// In REPL mode, wrap potential object literals in parentheses
// If code starts with { and doesn't end with ; it might be an object literal
// that would otherwise be parsed as a block statement
const processed_code: []const u8 = if (this.config.repl_mode and isLikelyObjectLiteral(code))
std.fmt.allocPrint(allocator, "({s})", .{code}) catch code
else
code;
const source = &logger.Source.initPathString(name, processed_code);
const jsx = if (this.config.tsconfig != null)
this.config.tsconfig.?.mergeJSX(this.transpiler.options.jsx)

View File

@@ -6109,6 +6109,162 @@ CPP_DECL [[ZIG_EXPORT(nothrow)]] unsigned int Bun__CallFrame__getLineNumber(JSC:
return lineColumn.line;
}
// REPL evaluation function - evaluates JavaScript code in the global scope
// Returns the result value, or undefined if an exception was thrown
// If an exception is thrown, the exception value is stored in *exception
extern "C" JSC::EncodedJSValue Bun__REPL__evaluate(
JSC::JSGlobalObject* globalObject,
const unsigned char* sourcePtr,
size_t sourceLen,
const unsigned char* filenamePtr,
size_t filenameLen,
JSC::EncodedJSValue* exception)
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_CATCH_SCOPE(vm);
WTF::String source = WTF::String::fromUTF8(std::span { sourcePtr, sourceLen });
WTF::String filename = filenameLen > 0
? WTF::String::fromUTF8(std::span { filenamePtr, filenameLen })
: "[repl]"_s;
JSC::SourceCode sourceCode = JSC::makeSource(
source,
JSC::SourceOrigin {},
JSC::SourceTaintedOrigin::Untainted,
filename,
WTF::TextPosition(),
JSC::SourceProviderSourceType::Program);
WTF::NakedPtr<JSC::Exception> evalException;
JSC::JSValue result = JSC::evaluate(globalObject, sourceCode, globalObject->globalThis(), evalException);
if (evalException) {
*exception = JSC::JSValue::encode(evalException->value());
// Set _error on the globalObject directly (not globalThis proxy)
globalObject->putDirect(vm, JSC::Identifier::fromString(vm, "_error"_s), evalException->value());
scope.clearException();
return JSC::JSValue::encode(JSC::jsUndefined());
}
if (scope.exception()) {
*exception = JSC::JSValue::encode(scope.exception()->value());
// Set _error on the globalObject directly (not globalThis proxy)
globalObject->putDirect(vm, JSC::Identifier::fromString(vm, "_error"_s), scope.exception()->value());
scope.clearException();
return JSC::JSValue::encode(JSC::jsUndefined());
}
// Note: _ is now set in Zig code (repl.zig) after extracting the value from
// the REPL transform wrapper. We don't set it here anymore.
return JSC::JSValue::encode(result);
}
// REPL completion function - gets completions for a partial property access
// Returns an array of completion strings, or undefined if no completions
extern "C" JSC::EncodedJSValue Bun__REPL__getCompletions(
JSC::JSGlobalObject* globalObject,
JSC::EncodedJSValue targetValue,
const unsigned char* prefixPtr,
size_t prefixLen)
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSC::JSValue target = JSC::JSValue::decode(targetValue);
if (!target || target.isUndefined() || target.isNull()) {
target = globalObject->globalThis();
}
if (!target.isObject()) {
return JSC::JSValue::encode(JSC::jsUndefined());
}
WTF::String prefix = prefixLen > 0
? WTF::String::fromUTF8(std::span { prefixPtr, prefixLen })
: WTF::String();
JSC::JSObject* object = target.getObject();
JSC::PropertyNameArrayBuilder propertyNames(vm, JSC::PropertyNameMode::Strings, JSC::PrivateSymbolMode::Exclude);
object->getPropertyNames(globalObject, propertyNames, DontEnumPropertiesMode::Include);
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined()));
JSC::JSArray* completions = JSC::constructEmptyArray(globalObject, nullptr, 0);
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined()));
unsigned completionIndex = 0;
for (const auto& propertyName : propertyNames) {
WTF::String name = propertyName.string();
if (prefix.isEmpty() || name.startsWith(prefix)) {
completions->putDirectIndex(globalObject, completionIndex++, JSC::jsString(vm, name));
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined()));
}
}
// Also check the prototype chain
JSC::JSValue proto = object->getPrototype(globalObject);
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(completions));
while (proto && proto.isObject()) {
JSC::JSObject* protoObj = proto.getObject();
JSC::PropertyNameArrayBuilder protoNames(vm, JSC::PropertyNameMode::Strings, JSC::PrivateSymbolMode::Exclude);
protoObj->getPropertyNames(globalObject, protoNames, DontEnumPropertiesMode::Include);
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(completions));
for (const auto& propertyName : protoNames) {
WTF::String name = propertyName.string();
if (prefix.isEmpty() || name.startsWith(prefix)) {
completions->putDirectIndex(globalObject, completionIndex++, JSC::jsString(vm, name));
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(completions));
}
}
proto = protoObj->getPrototype(globalObject);
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(completions));
}
return JSC::JSValue::encode(completions);
}
// Format a value for REPL output using util.inspect style
extern "C" JSC::EncodedJSValue Bun__REPL__formatValue(
JSC::JSGlobalObject* globalObject,
JSC::EncodedJSValue valueEncoded,
int32_t depth,
bool colors)
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
// Get the util.inspect function from the global object
auto* bunGlobal = jsCast<Zig::GlobalObject*>(globalObject);
JSC::JSValue inspectFn = bunGlobal->utilInspectFunction();
if (!inspectFn || !inspectFn.isCallable()) {
// Fallback to toString if util.inspect is not available
JSC::JSValue value = JSC::JSValue::decode(valueEncoded);
return JSC::JSValue::encode(value.toString(globalObject));
}
// Create options object
JSC::JSObject* options = JSC::constructEmptyObject(globalObject);
options->putDirect(vm, JSC::Identifier::fromString(vm, "depth"_s), JSC::jsNumber(depth));
options->putDirect(vm, JSC::Identifier::fromString(vm, "colors"_s), JSC::jsBoolean(colors));
options->putDirect(vm, JSC::Identifier::fromString(vm, "maxArrayLength"_s), JSC::jsNumber(100));
options->putDirect(vm, JSC::Identifier::fromString(vm, "maxStringLength"_s), JSC::jsNumber(10000));
options->putDirect(vm, JSC::Identifier::fromString(vm, "breakLength"_s), JSC::jsNumber(80));
JSC::MarkedArgumentBuffer args;
args.append(JSC::JSValue::decode(valueEncoded));
args.append(options);
JSC::JSValue result = JSC::call(globalObject, inspectFn, JSC::ArgList(args), "util.inspect"_s);
RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined()));
return JSC::JSValue::encode(result);
}
extern "C" void JSC__ArrayBuffer__ref(JSC::ArrayBuffer* self) { self->ref(); }
extern "C" void JSC__ArrayBuffer__deref(JSC::ArrayBuffer* self) { self->deref(); }
extern "C" void JSC__ArrayBuffer__asBunArrayBuffer(JSC::ArrayBuffer* self, Bun__ArrayBuffer* out)

View File

@@ -168,6 +168,12 @@ CPP_DECL uint32_t JSC__JSInternalPromise__status(const JSC::JSInternalPromise* a
CPP_DECL void JSC__JSFunction__optimizeSoon(JSC::EncodedJSValue JSValue0);
#pragma mark - REPL Functions
CPP_DECL JSC::EncodedJSValue Bun__REPL__evaluate(JSC::JSGlobalObject* globalObject, const unsigned char* sourcePtr, size_t sourceLen, const unsigned char* filenamePtr, size_t filenameLen, JSC::EncodedJSValue* exception);
CPP_DECL JSC::EncodedJSValue Bun__REPL__getCompletions(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue targetValue, const unsigned char* prefixPtr, size_t prefixLen);
CPP_DECL JSC::EncodedJSValue Bun__REPL__formatValue(JSC::JSGlobalObject* globalObject, JSC::EncodedJSValue valueEncoded, int32_t depth, bool colors);
#pragma mark - JSC::JSGlobalObject
CPP_DECL VirtualMachine* JSC__JSGlobalObject__bunVM(JSC::JSGlobalObject* arg0);

View File

@@ -92,6 +92,7 @@ pub const AuditCommand = @import("./cli/audit_command.zig").AuditCommand;
pub const InitCommand = @import("./cli/init_command.zig").InitCommand;
pub const WhyCommand = @import("./cli/why_command.zig").WhyCommand;
pub const FuzzilliCommand = @import("./cli/fuzzilli_command.zig").FuzzilliCommand;
pub const ReplCommand = @import("./cli/repl_command.zig").ReplCommand;
pub const Arguments = @import("./cli/Arguments.zig");
@@ -813,12 +814,8 @@ pub const Command = struct {
return;
},
.ReplCommand => {
// TODO: Put this in native code.
var ctx = try Command.init(allocator, log, .BunxCommand);
ctx.debug.run_in_bun = true; // force the same version of bun used. fixes bun-debug for example
var args = bun.argv[0..];
args[1] = "bun-repl";
try BunxCommand.exec(ctx, args);
const ctx = try Command.init(allocator, log, .RunCommand);
try ReplCommand.exec(ctx);
return;
},
.RemoveCommand => {

159
src/cli/repl_command.zig Normal file
View File

@@ -0,0 +1,159 @@
//! Bun REPL Command - Native Zig REPL with full TUI support
//!
//! This is the entry point for `bun repl` which provides an interactive
//! JavaScript REPL with:
//! - Syntax highlighting using QuickAndDirtySyntaxHighlighter
//! - Full line editing with Emacs-style keybindings
//! - Persistent history
//! - Tab completion
//! - Multi-line input support
//! - REPL commands (.help, .exit, .clear, .load, .save, .editor)
pub const ReplCommand = struct {
pub fn exec(ctx: Command.Context) !void {
@branchHint(.cold);
// Initialize the Zig REPL
var repl = Repl.init(ctx.allocator);
defer repl.deinit();
// Boot the JavaScript VM for the REPL
try bootReplVM(ctx, &repl);
}
fn bootReplVM(ctx: Command.Context, repl: *Repl) !void {
// Load bunfig if not already loaded
if (!ctx.debug.loaded_bunfig) {
try bun.cli.Arguments.loadConfigPath(ctx.allocator, true, "bunfig.toml", ctx, .RunCommand);
}
// Initialize JSC
bun.jsc.initialize(true); // true for eval mode
js_ast.Expr.Data.Store.create();
js_ast.Stmt.Data.Store.create();
const arena = Arena.init();
// Create a virtual path for REPL evaluation
const repl_path = "[repl]";
// Initialize the VM
const vm = try jsc.VirtualMachine.init(.{
.allocator = arena.allocator(),
.log = ctx.log,
.args = ctx.args,
.store_fd = false,
.smol = ctx.runtime_options.smol,
.eval = true,
.debugger = ctx.runtime_options.debugger,
.dns_result_order = DNSResolver.Order.fromStringOrDie(ctx.runtime_options.dns_result_order),
.is_main_thread = true,
});
var b = &vm.transpiler;
vm.preload = ctx.preloads;
vm.argv = ctx.passthrough;
vm.arena = @constCast(&arena);
vm.allocator = vm.arena.allocator();
// Configure bundler options
b.options.install = ctx.install;
b.resolver.opts.install = ctx.install;
b.resolver.opts.global_cache = ctx.debug.global_cache;
b.resolver.opts.prefer_offline_install = (ctx.debug.offline_mode_setting orelse .online) == .offline;
b.resolver.opts.prefer_latest_install = (ctx.debug.offline_mode_setting orelse .online) == .latest;
b.options.global_cache = b.resolver.opts.global_cache;
b.options.prefer_offline_install = b.resolver.opts.prefer_offline_install;
b.options.prefer_latest_install = b.resolver.opts.prefer_latest_install;
b.resolver.env_loader = b.env;
b.options.env.behavior = .load_all_without_inlining;
b.options.dead_code_elimination = false; // REPL needs all code
b.configureDefines() catch {
dumpBuildError(vm);
Global.exit(1);
};
bun.http.AsyncHTTP.loadEnv(vm.allocator, vm.log, b.env);
vm.loadExtraEnvAndSourceCodePrinter();
vm.is_main_thread = true;
jsc.VirtualMachine.is_main_thread_vm = true;
// Store VM reference in REPL (safe - no JS allocation)
repl.vm = vm;
repl.global = vm.global;
// Create the ReplRunner and execute within the API lock
// NOTE: JS-allocating operations like ExposeNodeModuleGlobals must
// be done inside the API lock callback, not before
var runner = ReplRunner{
.repl = repl,
.vm = vm,
.arena = arena,
.entry_path = repl_path,
};
const callback = jsc.OpaqueWrap(ReplRunner, ReplRunner.start);
vm.global.vm().holdAPILock(&runner, callback);
}
fn dumpBuildError(vm: *jsc.VirtualMachine) void {
Output.flush();
const writer = Output.errorWriterBuffered();
defer Output.flush();
vm.log.print(writer) catch {};
}
};
/// Runs the REPL within the VM's API lock
const ReplRunner = struct {
repl: *Repl,
vm: *jsc.VirtualMachine,
arena: bun.allocators.MimallocArena,
entry_path: []const u8,
pub fn start(this: *ReplRunner) void {
const vm = this.vm;
// Set up the REPL environment (now inside API lock)
this.setupReplEnvironment();
// Run the REPL loop
this.repl.runWithVM(vm) catch |err| {
Output.prettyErrorln("<r><red>REPL error: {s}<r>", .{@errorName(err)});
};
// Clean up
vm.onExit();
Global.exit(vm.exit_handler.exit_code);
}
fn setupReplEnvironment(this: *ReplRunner) void {
const vm = this.vm;
// Expose Node.js module globals (__dirname, __filename, require, etc.)
// This must be done inside the API lock as it allocates JS objects
bun.cpp.Bun__ExposeNodeModuleGlobals(vm.global);
// Set timezone if specified
if (vm.transpiler.env.get("TZ")) |tz| {
if (tz.len > 0) {
_ = vm.global.setTimeZone(&jsc.ZigString.init(tz));
}
}
vm.transpiler.env.loadTracy();
}
};
const Repl = @import("../repl.zig");
const bun = @import("bun");
const Global = bun.Global;
const Output = bun.Output;
const js_ast = bun.ast;
const jsc = bun.jsc;
const Arena = bun.allocators.MimallocArena;
const Command = bun.cli.Command;
const DNSResolver = bun.api.dns.Resolver;

390
src/js/eval/repl.ts Normal file
View File

@@ -0,0 +1,390 @@
// Built-in REPL implementation for `bun repl`
// This replaces the external bun-repl package for faster startup
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import readline from "node:readline";
import util from "node:util";
import { runInThisContext } from "node:vm";
// REPL state
let lastResult: any = undefined;
let lastError: any = undefined;
let lineBuffer = "";
let inMultilineInput = false;
// ANSI color codes
const useColors = Boolean(process.stdout.isTTY && !("NO_COLOR" in process.env));
const colors = {
reset: useColors ? "\x1b[0m" : "",
cyan: useColors ? "\x1b[36m" : "",
yellow: useColors ? "\x1b[33m" : "",
red: useColors ? "\x1b[31m" : "",
green: useColors ? "\x1b[32m" : "",
dim: useColors ? "\x1b[2m" : "",
};
function colorize(text: string, color: string): string {
return color ? `${color}${text}${colors.reset}` : text;
}
// History file path - handle edge case where homedir() returns empty string
const homeDir = os.homedir();
const historyPath = homeDir ? path.join(homeDir, ".bun_repl_history") : "";
const maxHistorySize = 1000;
// Debounce timer for history saves
let historySaveTimer: ReturnType<typeof setTimeout> | null = null;
let pendingHistory: string[] | null = null;
function loadHistory(): string[] {
if (!historyPath) return [];
try {
if (fs.existsSync(historyPath)) {
const content = fs.readFileSync(historyPath, "utf-8");
return content.split("\n").filter((line: string) => line.trim());
}
} catch {
// Ignore errors loading history
}
return [];
}
function saveHistoryImmediate(history: string[]): void {
if (!historyPath) return;
try {
const toSave = history.slice(-maxHistorySize);
fs.writeFileSync(historyPath, toSave.join("\n") + "\n");
} catch {
// Ignore errors saving history
}
}
function saveHistory(history: string[]): void {
// Debounce history writes - save after 1 second of inactivity
pendingHistory = history;
if (historySaveTimer) {
clearTimeout(historySaveTimer);
}
historySaveTimer = setTimeout(() => {
if (pendingHistory) {
saveHistoryImmediate(pendingHistory);
pendingHistory = null;
}
historySaveTimer = null;
}, 1000);
}
function flushHistory(): void {
// Flush any pending history writes immediately
if (historySaveTimer) {
clearTimeout(historySaveTimer);
historySaveTimer = null;
}
if (pendingHistory) {
saveHistoryImmediate(pendingHistory);
pendingHistory = null;
}
}
// Check if code is incomplete (e.g., unclosed brackets)
function isIncompleteCode(code: string): boolean {
// Simple bracket counting approach
let braceCount = 0;
let bracketCount = 0;
let parenCount = 0;
let inString: string | null = null;
let inTemplate = false;
let escaped = false;
for (let i = 0; i < code.length; i++) {
const char = code[i];
if (escaped) {
escaped = false;
continue;
}
if (char === "\\") {
escaped = true;
continue;
}
// Handle strings
if (!inString && !inTemplate) {
if (char === '"' || char === "'") {
inString = char;
continue;
}
if (char === "`") {
inTemplate = true;
continue;
}
} else if (inString && char === inString) {
inString = null;
continue;
} else if (inTemplate && char === "`") {
inTemplate = false;
continue;
}
// Skip content inside strings
if (inString || inTemplate) continue;
// Count brackets
switch (char) {
case "{":
braceCount++;
break;
case "}":
braceCount--;
break;
case "[":
bracketCount++;
break;
case "]":
bracketCount--;
break;
case "(":
parenCount++;
break;
case ")":
parenCount--;
break;
}
}
// Incomplete if any unclosed delimiters or unclosed strings
return inString !== null || inTemplate || braceCount > 0 || bracketCount > 0 || parenCount > 0;
}
// REPL commands
const replCommands: Record<string, { help: string; action: (args: string) => void }> = {
".help": {
help: "Print this help message",
action: () => {
console.log("REPL Commands:");
for (const [cmd, { help }] of Object.entries(replCommands)) {
console.log(` ${cmd.padEnd(12)} ${help}`);
}
},
},
".exit": {
help: "Exit the REPL",
action: () => {
process.exit(0);
},
},
".clear": {
help: "Clear the REPL context",
action: () => {
lastResult = undefined;
lastError = undefined;
console.log("REPL context cleared");
},
},
".load": {
help: "Load a file into the REPL session",
action: (filename: string) => {
if (!filename.trim()) {
console.log(colorize("Usage: .load <filename>", colors.red));
return;
}
try {
// Resolve relative paths against the user's current working directory
const resolvedPath = path.resolve(process.cwd(), filename.trim());
const code = fs.readFileSync(resolvedPath, "utf-8");
const result = evaluateCode(code);
if (result !== undefined) {
console.log(formatResult(result));
}
} catch (err: any) {
console.log(colorize(`Error loading file: ${err.message}`, colors.red));
}
},
},
};
// Evaluate code in the global context
function evaluateCode(code: string): any {
// Handle special _ and _error variables
(globalThis as any)._ = lastResult;
(globalThis as any)._error = lastError;
try {
// Use runInThisContext for proper JavaScript evaluation
const result = runInThisContext(code, {
filename: "repl",
displayErrors: true,
});
lastResult = result;
return result;
} catch (err: any) {
lastError = err;
throw err;
}
}
// Format the result for display
function formatResult(result: any): string {
if (result === undefined) {
return colorize("undefined", colors.dim);
}
return util.inspect(result, {
colors: useColors,
depth: 4,
maxArrayLength: 100,
maxStringLength: 10000,
breakLength: process.stdout.columns || 80,
});
}
// Get the prompt string
function getPrompt(): string {
if (inMultilineInput) {
return colorize("... ", colors.dim);
}
return colorize("bun", colors.green) + colorize("> ", colors.reset);
}
// Simple tab completer
function completer(line: string): [string[], string] {
const completions: string[] = [];
const trimmed = line.trim();
// Complete REPL commands
if (trimmed.startsWith(".")) {
const matches = Object.keys(replCommands).filter(cmd => cmd.startsWith(trimmed));
return [matches, trimmed];
}
// Try to complete global properties
try {
// Find the last word being typed
const match = line.match(/[\w$]+$/);
if (match) {
const prefix = match[0];
const props = Object.getOwnPropertyNames(globalThis).filter(p => p.startsWith(prefix));
return [props, prefix];
}
} catch {
// Ignore completion errors
}
return [completions, line];
}
// Handle a line of input
function handleLine(line: string, rl: any, history: string[]): void {
const trimmedLine = line.trim();
// Handle empty line
if (!trimmedLine && !inMultilineInput) {
rl.prompt();
return;
}
// Handle REPL commands
if (trimmedLine.startsWith(".") && !inMultilineInput) {
const spaceIndex = trimmedLine.indexOf(" ");
const cmd = spaceIndex > 0 ? trimmedLine.slice(0, spaceIndex) : trimmedLine;
const args = spaceIndex > 0 ? trimmedLine.slice(spaceIndex + 1) : "";
if (replCommands[cmd]) {
replCommands[cmd].action(args);
rl.prompt();
return;
}
}
// Accumulate input
lineBuffer += (lineBuffer ? "\n" : "") + line;
// Check if code is complete
if (isIncompleteCode(lineBuffer)) {
inMultilineInput = true;
rl.setPrompt(getPrompt());
rl.prompt();
return;
}
const code = lineBuffer;
lineBuffer = "";
inMultilineInput = false;
// Add to history
if (code.trim()) {
history.push(code);
saveHistory(history);
}
// Evaluate the code
try {
const result = evaluateCode(code);
if (result !== undefined) {
console.log(formatResult(result));
}
} catch (err: any) {
// Format error message
if (err.name === "SyntaxError") {
console.log(colorize(`SyntaxError: ${err.message}`, colors.red));
} else {
console.log(colorize(`${err.name || "Error"}: ${err.message}`, colors.red));
if (err.stack && process.env.BUN_DEBUG) {
console.log(colorize(err.stack, colors.dim));
}
}
}
rl.setPrompt(getPrompt());
rl.prompt();
}
// Main REPL function
function startRepl(): void {
// Print welcome message
console.log(`Welcome to Bun v${Bun.version}`);
console.log('Type ".help" for more information.');
const history = loadHistory();
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prompt: getPrompt(),
terminal: process.stdin.isTTY,
historySize: maxHistorySize,
completer: process.stdin.isTTY ? completer : undefined,
history: history.slice(-maxHistorySize),
});
rl.on("line", (line: string) => {
handleLine(line, rl, history);
});
rl.on("close", () => {
flushHistory();
console.log();
process.exit(0);
});
rl.on("SIGINT", () => {
if (inMultilineInput) {
// Cancel multiline input
lineBuffer = "";
inMultilineInput = false;
console.log();
rl.setPrompt(getPrompt());
rl.prompt();
} else {
console.log("\n(To exit, press Ctrl+D or type .exit)");
rl.prompt();
}
});
rl.prompt();
}
// Start the REPL
startRepl();

View File

@@ -1802,6 +1802,10 @@ pub const BundleOptions = struct {
minify_identifiers: bool = false,
keep_names: bool = false,
dead_code_elimination: bool = true,
/// REPL mode: transforms code for interactive evaluation with vm.runInContext.
/// Hoists declarations as var for persistence, wraps code in IIFE, and
/// captures the last expression in { value: expr } for result extraction.
repl_mode: bool = false,
css_chunking: bool,
ignore_dce_annotations: bool = false,

1690
src/repl.zig Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -215,6 +215,13 @@ pub const Runtime = struct {
/// When `feature("FLAG_NAME")` is called, it returns true if FLAG_NAME is in this set.
bundler_feature_flags: *const bun.StringSet = &empty_bundler_feature_flags,
/// 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,
pub const empty_bundler_feature_flags: bun.StringSet = bun.StringSet.initComptime();
/// Initialize bundler feature flags for dead-code elimination via `import { feature } from "bun:bundle"`.

View File

@@ -1115,6 +1115,8 @@ pub const Transpiler = struct {
opts.features.dead_code_elimination = transpiler.options.dead_code_elimination;
opts.features.remove_cjs_module_wrapper = this_parse.remove_cjs_module_wrapper;
opts.features.bundler_feature_flags = transpiler.options.bundler_feature_flags;
opts.features.repl_mode = transpiler.options.repl_mode;
opts.repl_mode = transpiler.options.repl_mode;
if (transpiler.macro_context == null) {
transpiler.macro_context = js_ast.Macro.MacroContext.init(transpiler);

View File

@@ -1,12 +0,0 @@
import { expect, test } from "bun:test";
import "harness";
import { isArm64, isMusl } from "harness";
// https://github.com/oven-sh/bun/issues/12070
test.skipIf(
// swc, which bun-repl uses, published a glibc build for arm64 musl
// and so it crashes on process.exit.
isMusl && isArm64,
)("bun repl", () => {
expect(["repl", "-e", "process.exit(0)"]).toRun();
});

View File

@@ -0,0 +1,343 @@
// Tests for Bun REPL
// These tests verify the interactive REPL functionality including:
// - Basic JavaScript evaluation
// - Special variables (_ and _error)
// - REPL commands (.help, .exit, .clear)
// - Multi-line input
// - History navigation
// - Tab completion
// - Error handling
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isWindows } from "harness";
// Helper function to run REPL with input and capture output
async function runRepl(
input: string | string[],
options: {
timeout?: number;
env?: Record<string, string>;
} = {},
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const inputStr = Array.isArray(input) ? input.join("\n") + "\n" : input;
const { timeout = 5000, env = {} } = options;
const proc = Bun.spawn({
cmd: [bunExe(), "repl"],
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
env: {
...bunEnv,
TERM: "dumb", // Disable color codes for easier parsing
NO_COLOR: "1",
...env,
},
});
// Write input to stdin
proc.stdin.write(inputStr);
proc.stdin.flush();
proc.stdin.end();
// Wait for process with timeout
const exitCode = await Promise.race([
proc.exited,
Bun.sleep(timeout).then(() => {
proc.kill();
return -1;
}),
]);
const stdout = await new Response(proc.stdout).text();
const stderr = await new Response(proc.stderr).text();
return { stdout, stderr, exitCode };
}
// Strip ANSI escape sequences and control characters for easier assertion
function stripAnsi(str: string): string {
// Remove ANSI escape codes
return str
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "")
.replace(/\x1b\][^\x07]*\x07/g, "") // OSC sequences
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, ""); // Control chars except \n, \r, \t
}
// Extract result values from REPL output
function extractResults(output: string): string[] {
const lines = stripAnsi(output).split("\n");
const results: string[] = [];
for (const line of lines) {
const trimmed = line.trim();
// Skip empty lines, prompts, and welcome message
if (
trimmed &&
!trimmed.startsWith("bun>") &&
!trimmed.startsWith("...>") &&
!trimmed.startsWith("Welcome to Bun") &&
!trimmed.startsWith("Type .help")
) {
results.push(trimmed);
}
}
return results;
}
describe.todoIf(isWindows)("Bun REPL", () => {
describe("basic evaluation", () => {
test("evaluates simple expression", async () => {
const { stdout, exitCode } = await runRepl(["1 + 1", ".exit"]);
const results = extractResults(stdout);
expect(results).toContain("2");
expect(exitCode).toBe(0);
});
test("evaluates multiple expressions", async () => {
const { stdout, exitCode } = await runRepl(["1 + 1", "2 * 3", "Math.sqrt(16)", ".exit"]);
const results = extractResults(stdout);
expect(results).toContain("2");
expect(results).toContain("6");
expect(results).toContain("4");
expect(exitCode).toBe(0);
});
test("evaluates string expressions", async () => {
const { stdout, exitCode } = await runRepl(["'hello'.toUpperCase()", ".exit"]);
const results = extractResults(stdout);
expect(results.some(r => r.includes("HELLO"))).toBe(true);
expect(exitCode).toBe(0);
});
test("evaluates object literals", async () => {
const { stdout, exitCode } = await runRepl(["({ a: 1, b: 2 })", ".exit"]);
const results = extractResults(stdout);
expect(results.some(r => r.includes("a") && r.includes("b"))).toBe(true);
expect(exitCode).toBe(0);
});
test("evaluates array expressions", async () => {
const { stdout, exitCode } = await runRepl(["[1, 2, 3].map(x => x * 2)", ".exit"]);
const results = extractResults(stdout);
expect(results.some(r => r.includes("2") && r.includes("4") && r.includes("6"))).toBe(true);
expect(exitCode).toBe(0);
});
});
describe("special variables", () => {
test("_ contains last result", async () => {
const { stdout, exitCode } = await runRepl(["42", "_", ".exit"]);
const results = extractResults(stdout);
// Should have 42 twice - once from evaluation, once from _
const fortyTwos = results.filter(r => r === "42");
expect(fortyTwos.length).toBe(2);
expect(exitCode).toBe(0);
});
test("_ updates with each result", async () => {
const { stdout, exitCode } = await runRepl(["10", "_ * 2", "_ + 5", ".exit"]);
const results = extractResults(stdout);
expect(results).toContain("10");
expect(results).toContain("20");
expect(results).toContain("25");
expect(exitCode).toBe(0);
});
test("_error contains last error", async () => {
const { stdout, exitCode } = await runRepl(["throw new Error('test error')", "_error.message", ".exit"]);
const results = extractResults(stdout);
expect(results.some(r => r.includes("test error"))).toBe(true);
expect(exitCode).toBe(0);
});
test("_ is not updated for undefined results", async () => {
const { stdout, exitCode } = await runRepl(["42", "undefined", "_", ".exit"]);
const results = extractResults(stdout);
// _ should still be 42, not undefined
const fortyTwos = results.filter(r => r === "42");
expect(fortyTwos.length).toBeGreaterThanOrEqual(2);
expect(exitCode).toBe(0);
});
});
describe("REPL commands", () => {
test(".exit exits the REPL", async () => {
const { exitCode } = await runRepl([".exit"]);
expect(exitCode).toBe(0);
});
test(".help shows help message", async () => {
const { stdout, exitCode } = await runRepl([".help", ".exit"]);
const output = stripAnsi(stdout);
expect(output).toContain(".help");
expect(output).toContain(".exit");
expect(exitCode).toBe(0);
});
test(".clear clears context", async () => {
const { stdout, exitCode } = await runRepl(["const x = 42", ".clear", ".exit"]);
expect(exitCode).toBe(0);
});
});
describe("error handling", () => {
test("handles syntax errors gracefully", async () => {
// Use a complete but invalid syntax - extra closing paren
const { stdout, stderr, exitCode } = await runRepl(["(1 + ))", "1 + 1", ".exit"]);
const allOutput = stripAnsi(stdout + stderr);
// Should contain error indication but still continue
expect(allOutput.toLowerCase().includes("error") || allOutput.toLowerCase().includes("unexpected")).toBe(true);
expect(exitCode).toBe(0);
});
test("handles runtime errors gracefully", async () => {
const { stdout, stderr, exitCode } = await runRepl(["undefinedVariable", "1 + 1", ".exit"]);
const allOutput = stripAnsi(stdout + stderr);
expect(allOutput.includes("not defined") || allOutput.includes("ReferenceError")).toBe(true);
expect(exitCode).toBe(0);
});
test("handles thrown errors", async () => {
const { stdout, stderr, exitCode } = await runRepl(["throw 'custom error'", "1 + 1", ".exit"]);
const allOutput = stripAnsi(stdout + stderr);
expect(allOutput).toContain("custom error");
expect(exitCode).toBe(0);
});
});
describe("global objects", () => {
test("has access to Bun globals", async () => {
const { stdout, exitCode } = await runRepl(["typeof Bun.version", ".exit"]);
const results = extractResults(stdout);
expect(results.some(r => r.includes("string"))).toBe(true);
expect(exitCode).toBe(0);
});
test("has access to console", async () => {
const { stdout, exitCode } = await runRepl(["console.log('hello from repl')", ".exit"]);
const output = stripAnsi(stdout);
expect(output).toContain("hello from repl");
expect(exitCode).toBe(0);
});
test("has access to Buffer", async () => {
const { stdout, exitCode } = await runRepl(["Buffer.from('hello').length", ".exit"]);
const results = extractResults(stdout);
expect(results).toContain("5");
expect(exitCode).toBe(0);
});
test("has access to process", async () => {
const { stdout, exitCode } = await runRepl(["typeof process.version", ".exit"]);
const results = extractResults(stdout);
expect(results.some(r => r.includes("string"))).toBe(true);
expect(exitCode).toBe(0);
});
});
describe("variable persistence", () => {
test("variables persist across evaluations", async () => {
const { stdout, exitCode } = await runRepl(["const x = 10", "const y = 20", "x + y", ".exit"]);
const results = extractResults(stdout);
expect(results).toContain("30");
expect(exitCode).toBe(0);
});
test("let variables can be reassigned", async () => {
const { stdout, exitCode } = await runRepl(["let counter = 0", "counter++", "counter++", "counter", ".exit"]);
const results = extractResults(stdout);
expect(results).toContain("2");
expect(exitCode).toBe(0);
});
test("functions persist", async () => {
const { stdout, exitCode } = await runRepl(["function add(a, b) { return a + b; }", "add(5, 3)", ".exit"]);
const results = extractResults(stdout);
expect(results).toContain("8");
expect(exitCode).toBe(0);
});
});
describe("async evaluation", () => {
test("evaluates promises", async () => {
const { stdout, exitCode } = await runRepl(["Promise.resolve(42)", ".exit"]);
const results = extractResults(stdout);
expect(results.some(r => r.includes("42") || r.includes("Promise"))).toBe(true);
expect(exitCode).toBe(0);
});
test("evaluates async functions", async () => {
const { stdout, exitCode } = await runRepl(["async function getValue() { return 123; }", "getValue()", ".exit"]);
const results = extractResults(stdout);
expect(results.some(r => r.includes("123") || r.includes("Promise"))).toBe(true);
expect(exitCode).toBe(0);
});
});
describe("welcome message", () => {
test("shows welcome message on startup", async () => {
const { stdout, exitCode } = await runRepl([".exit"]);
const output = stripAnsi(stdout);
expect(output).toContain("Welcome to Bun");
expect(exitCode).toBe(0);
});
test("shows version in welcome message", async () => {
const { stdout, exitCode } = await runRepl([".exit"]);
const output = stripAnsi(stdout);
// Should contain "Bun v" followed by version numbers
expect(output).toMatch(/Bun v\d+\.\d+\.\d+/);
expect(exitCode).toBe(0);
});
});
});
// Terminal-based REPL tests (for interactive features)
describe.todoIf(isWindows)("Bun REPL with Terminal", () => {
test("spawns REPL in PTY and receives welcome message", async () => {
const received: Uint8Array[] = [];
const { promise: welcomeReceived, resolve: gotWelcome } = Promise.withResolvers<void>();
const terminal = new Bun.Terminal({
cols: 80,
rows: 24,
data(_term, data) {
received.push(new Uint8Array(data));
const str = Buffer.from(data).toString();
if (str.includes("Welcome")) {
gotWelcome();
}
},
});
const proc = Bun.spawn({
cmd: [bunExe(), "repl"],
terminal,
env: {
...bunEnv,
TERM: "xterm-256color",
},
});
// Wait for welcome message (with timeout)
await Promise.race([welcomeReceived, Bun.sleep(3000)]);
// Exit the REPL
terminal.write(".exit\n");
// Wait for process exit with timeout
await Promise.race([proc.exited, Bun.sleep(1000)]);
// Kill if still running
if (!proc.killed) {
proc.kill();
}
const allData = Buffer.concat(received).toString();
expect(allData).toContain("Welcome to Bun");
terminal.close();
});
});

View File

@@ -0,0 +1,291 @@
import { describe, expect, test } from "bun:test";
import vm from "node:vm";
describe("Bun.Transpiler replMode", () => {
describe("basic transform output", () => {
const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });
test("simple expression wrapped in value object", () => {
const result = transpiler.transformSync("42");
// Should contain value wrapper
expect(result).toContain("value:");
});
test("variable declaration with await", () => {
const result = transpiler.transformSync("var x = await 1");
// Should hoist var declaration
expect(result).toContain("var x");
// Should have async wrapper
expect(result).toContain("async");
});
test("const becomes var with await", () => {
const result = transpiler.transformSync("const x = await 1");
// const should become var for REPL persistence (becomes context property)
expect(result).toContain("var x");
expect(result).not.toContain("const x");
});
test("let becomes var with await", () => {
const result = transpiler.transformSync("let x = await 1");
// let should become var for REPL persistence (becomes context property)
expect(result).toContain("var x");
expect(result).not.toContain("let x");
expect(result).toContain("async");
});
test("no async wrapper when no await", () => {
const result = transpiler.transformSync("var x = 1; x + 5");
// Should still have value wrapper for the last expression
expect(result).toContain("value:");
// Should not wrap in async when no await
expect(result).not.toMatch(/\(\s*async\s*\(\s*\)\s*=>/);
});
test("function declaration with await", () => {
const result = transpiler.transformSync("await 1; function foo() { return 42; }");
// Should hoist function declaration
expect(result).toContain("var foo");
expect(result).toContain("async");
});
test("class declaration with await", () => {
const result = transpiler.transformSync("await 1; class Bar { }");
// Should hoist class declaration with var (not let) for vm context persistence
expect(result).toContain("var Bar");
expect(result).toContain("async");
});
});
describe("REPL session with node:vm", () => {
const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });
async function runRepl(code: string, context?: object) {
const ctx = vm.createContext(context ?? { console, Promise });
const transformed = transpiler.transformSync(code);
return await vm.runInContext(transformed, ctx);
}
test("simple expression returns value object", async () => {
const result = await runRepl("42");
expect(result).toEqual({ value: 42 });
});
test("arithmetic expression", async () => {
const result = await runRepl("2 + 3 * 4");
expect(result).toEqual({ value: 14 });
});
test("string expression", async () => {
const result = await runRepl('"hello world"');
expect(result).toEqual({ value: "hello world" });
});
test("object literal (auto-detected)", async () => {
// Object literals don't need parentheses - the transpiler auto-detects them
const result = await runRepl("{a: 1, b: 2}");
expect(result).toEqual({ value: { a: 1, b: 2 } });
});
test("array literal", async () => {
const result = await runRepl("[1, 2, 3]");
expect(result).toEqual({ value: [1, 2, 3] });
});
test("await expression", async () => {
const result = await runRepl("await Promise.resolve(100)");
expect(result).toEqual({ value: 100 });
});
test("await with variable", async () => {
const ctx = vm.createContext({ Promise });
const code1 = transpiler.transformSync("var x = await Promise.resolve(10)");
await vm.runInContext(code1, ctx);
expect(ctx.x).toBe(10);
const code2 = transpiler.transformSync("x * 2");
const result = await vm.runInContext(code2, ctx);
expect(result).toEqual({ value: 20 });
});
});
describe("variable persistence across lines", () => {
const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });
async function runReplSession(lines: string[]) {
const ctx = vm.createContext({ console, Promise });
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);
expect(results[3]).toBe(20);
expect(context.x).toBe(20);
});
test("let persists with await", async () => {
const { results } = await runReplSession(["let y = await Promise.resolve(100)", "y * 2"]);
expect(results[1]).toBe(200);
});
test("function declarations persist", async () => {
const { results, context } = await runReplSession(["await 1; function add(a, b) { return a + b; }", "add(2, 3)"]);
expect(results[1]).toBe(5);
expect(typeof context.add).toBe("function");
});
test("class declarations persist to vm context", async () => {
// Class declarations use 'var' hoisting so they persist to vm context
const { results, context } = await runReplSession([
"await 1; class Counter { constructor() { this.count = 0; } inc() { this.count++; } }",
"new Counter()",
]);
// The class is returned in the result's value
expect(typeof results[0]).toBe("function");
expect(results[0].name).toBe("Counter");
// The class should be accessible in subsequent REPL lines
expect(results[1]).toBeInstanceOf(context.Counter);
expect(typeof context.Counter).toBe("function");
});
});
describe("object literal detection", () => {
const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });
async function runRepl(code: string, context?: object) {
const ctx = vm.createContext(context ?? { console, Promise });
const transformed = transpiler.transformSync(code);
return await vm.runInContext(transformed, ctx);
}
test("{a: 1} parsed as object literal, not block", async () => {
const result = await runRepl("{a: 1}");
expect(result.value).toEqual({ a: 1 });
});
test("{a: 1, b: 2} parsed as object literal", async () => {
const result = await runRepl("{a: 1, b: 2}");
expect(result.value).toEqual({ a: 1, b: 2 });
});
test("{foo: await bar()} parsed as object literal", async () => {
const ctx = vm.createContext({
bar: async () => 42,
});
const code = transpiler.transformSync("{foo: await bar()}");
const result = await vm.runInContext(code, ctx);
expect(result.value).toEqual({ foo: 42 });
});
test("{x: 1}; is NOT wrapped (has trailing semicolon)", async () => {
// With semicolon, it's explicitly a block statement
const code = transpiler.transformSync("{x: 1};");
// The output should NOT treat this as an object literal
// It should be a block with a labeled statement, no value wrapper
expect(code).not.toContain("value:");
expect(code).toContain("x:");
});
test("whitespace around object literal is handled", async () => {
const result = await runRepl(" { a: 1 } ");
expect(result.value).toEqual({ a: 1 });
});
});
describe("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 produces empty output", () => {
// Comments are stripped by the transpiler
const result = transpiler.transformSync("// just a comment");
expect(result.trim()).toBe("");
});
test("TypeScript types stripped", () => {
const result = transpiler.transformSync("const x: number = await Promise.resolve(42)");
expect(result).not.toContain(": number");
});
test("multiple await expressions", async () => {
const ctx = vm.createContext({ Promise });
const code = transpiler.transformSync("await 1; await 2; await 3");
const result = await vm.runInContext(code, ctx);
// Last expression should be wrapped
expect(result).toEqual({ value: 3 });
});
test("destructuring assignment persists", async () => {
const ctx = vm.createContext({ Promise });
const code = transpiler.transformSync("var { a, b } = await Promise.resolve({ a: 1, b: 2 })");
await vm.runInContext(code, ctx);
expect(ctx.a).toBe(1);
expect(ctx.b).toBe(2);
});
test("array destructuring persists", async () => {
const ctx = vm.createContext({ Promise });
const code = transpiler.transformSync("var [x, y, z] = await Promise.resolve([10, 20, 30])");
await vm.runInContext(code, ctx);
expect(ctx.x).toBe(10);
expect(ctx.y).toBe(20);
expect(ctx.z).toBe(30);
});
});
describe("no transform cases", () => {
const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });
test("async function expression - no async wrapper", () => {
const result = transpiler.transformSync("async function foo() { await 1; }");
// await inside async function doesn't trigger TLA transform
// The top level has no await
expect(result).not.toMatch(/^\(async/);
});
test("arrow async function - no async wrapper", () => {
const result = transpiler.transformSync("const fn = async () => await 1");
// await inside arrow function doesn't trigger TLA transform
expect(result).not.toMatch(/^\(async\s*\(\)/);
});
});
describe("replMode option", () => {
test("replMode false by default", () => {
const transpiler = new Bun.Transpiler({ loader: "tsx" });
const result = transpiler.transformSync("42");
// Without replMode, no value wrapper
expect(result).not.toContain("value:");
});
test("replMode true adds transforms", () => {
const transpiler = new Bun.Transpiler({ loader: "tsx", replMode: true });
const result = transpiler.transformSync("42");
// With replMode, value wrapper should be present
expect(result).toContain("value:");
});
});
});

View File

@@ -0,0 +1,38 @@
// Test for GitHub issue #26058: bun repl is slow
// This test verifies that `bun repl` now uses a built-in REPL instead of bunx bun-repl
import { spawnSync } from "bun";
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
describe("issue #26058 - bun repl startup time", () => {
test("bun repl starts without downloading packages", () => {
// The key indicator that bunx is being used is the "Resolving dependencies" message
// Our built-in REPL should not print this
// Use timeout to prevent hanging since REPL requires TTY for interactive input
const result = spawnSync({
cmd: [bunExe(), "repl"],
env: {
...bunEnv,
TERM: "dumb",
},
stderr: "pipe",
stdout: "pipe",
stdin: "ignore",
timeout: 3000,
});
const stderr = result.stderr?.toString() || "";
const stdout = result.stdout?.toString() || "";
// Should NOT see package manager output from bunx
expect(stderr).not.toContain("Resolving dependencies");
expect(stderr).not.toContain("bun add");
expect(stdout).not.toContain("Resolving dependencies");
// The built-in REPL should print "Welcome to Bun" when starting
// Even without a TTY, the welcome message should appear in stdout
expect(stdout).toContain("Welcome to Bun");
});
});