mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 14:22:01 +00:00
Compare commits
16 Commits
claude/mov
...
claude/rep
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da3980843a | ||
|
|
bef209431a | ||
|
|
baf1bbda8b | ||
|
|
4dd8b288cd | ||
|
|
e154f1750a | ||
|
|
163623e775 | ||
|
|
32e2540e56 | ||
|
|
974fb9a272 | ||
|
|
43f5df2596 | ||
|
|
c189cdc60f | ||
|
|
d630b68d15 | ||
|
|
768b60ebf1 | ||
|
|
ea511ed08c | ||
|
|
804c716f8f | ||
|
|
c30b36f433 | ||
|
|
78f1d497f4 |
11
packages/bun-types/bun.d.ts
vendored
11
packages/bun-types/bun.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
365
src/ast/repl_transforms.zig
Normal 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;
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
6
src/bun.js/bindings/headers.h
generated
6
src/bun.js/bindings/headers.h
generated
@@ -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);
|
||||
|
||||
@@ -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
159
src/cli/repl_command.zig
Normal 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
390
src/js/eval/repl.ts
Normal 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();
|
||||
@@ -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
1690
src/repl.zig
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"`.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
343
test/js/bun/repl/repl.test.ts
Normal file
343
test/js/bun/repl/repl.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
291
test/js/bun/transpiler/repl-transform.test.ts
Normal file
291
test/js/bun/transpiler/repl-transform.test.ts
Normal 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:");
|
||||
});
|
||||
});
|
||||
});
|
||||
38
test/regression/issue/26058.test.ts
Normal file
38
test/regression/issue/26058.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user