Compare commits

...

26 Commits

Author SHA1 Message Date
Claude
31c8881f12 Fix GC protection for last_result/last_error and remove sleep in test
- Protect last_result and last_error JSValues with protect()/unprotect()
  to prevent garbage collection of values held only in Zig-side memory.
  Added setLastResult/setLastError helpers that handle the GC protection
  dance, and unprotect both values in deinit.
- Replace Bun.sleep(100) with waitFor("some partial input") in the
  Ctrl+C test to avoid time-based flakiness.

https://claude.ai/code/session_01WqWLCrDDdXCf24YdjbWpsM
2026-02-24 09:50:05 +00:00
Jarred Sumner
6984355cfd fix(repl): don't skip piped tests on Windows, fix ban-words
The piped stdin tests don't use Bun.Terminal and should work on
Windows. Only the interactive terminal tests need todoIf(isWindows).
Fix banned word: use .{0} ** 256 instead of = undefined for buffer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-24 01:43:13 -08:00
autofix-ci[bot]
3334539705 [autofix.ci] apply automated fixes 2026-02-24 09:38:06 +00:00
Jarred Sumner
9ac02b2954 refactor(repl): use bun.tty.setMode, bun.sys for I/O, fix Windows
- Add src/tty.zig with typed setMode() wrapping Bun__ttySetMode
- Replace manual termios manipulation with bun.tty.setMode(.raw/.normal)
- Add Windows terminal support via bun.windows.updateStdioModeFlags
- Use bun.sys.openA + writeAll for file writes (cross-platform)
- Remove dead src/js/eval/repl.ts (replaced by native Zig REPL)
- Move Bun__ttySetMode extern to shared tty.zig, update all callers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-24 01:36:20 -08:00
Claude
46ecd5c56b Fix memory leak in transformForRepl: defer deinit of BufferWriter's MutableString
The BufferWriter's internal MutableString buffer was never freed after
its contents were duplicated, leaking memory on every REPL evaluation.
Add defer buffer_printer.ctx.buffer.deinit() to match the cleanup
pattern used in server.zig.

https://claude.ai/code/session_01WqWLCrDDdXCf24YdjbWpsM
2026-02-24 09:13:25 +00:00
Jarred Sumner
79573ace56 fix(repl): use bun.sys.File for I/O, buffer stdin reads
Replace std.fs and std.posix.read with bun.sys.File for cross-platform
file I/O (history load/save, .load, .save commands). Buffer stdin reads
in a 256-byte buffer instead of syscall-per-byte. Use bun.sys.Error
formatter for error messages. Use bun.path.joinZBuf for path joining.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-24 01:12:27 -08:00
autofix-ci[bot]
be5578d6c5 [autofix.ci] apply automated fixes 2026-02-24 09:01:03 +00:00
Jarred Sumner
f6d0cba24b fix(repl): use SIMD ANSI iterator for clipboard, add double Ctrl+C exit
Replace pure-Zig ANSISkipper with extern struct backed by C++ SIMD
ANSI::findEscapeCharacter + consumeANSI. The iterator yields slices
of clean text between escape sequences with no allocations in the
common case (no ANSI present).

Also add double Ctrl+C to exit on empty line.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-24 00:58:36 -08:00
autofix-ci[bot]
dfabbcece2 [autofix.ci] apply automated fixes 2026-02-24 08:39:34 +00:00
Jarred Sumner
33fb0dea71 feat(repl): add .copy command with OSC 52 clipboard support
.copy with no args copies _ (last result) to clipboard.
.copy <expr> evaluates the expression and copies the result.
Strings are copied raw (not quoted), other values use Bun.inspect.

Adds strings.ANSISkipper to strip ANSI escape sequences before
encoding to clipboard. Uses bun.base64 for the OSC 52 payload.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-24 00:37:54 -08:00
Jarred Sumner
6ba9f68199 fix(repl): skip source code preview for [repl] frames
The error formatter was showing the transformed IIFE wrapper source
code (e.g. `(() => { return { __proto__: null, ...`) for errors in
the REPL. Set enable_source_code_preview=false when the top frame's
source_url is [repl].

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-23 19:11:12 -08:00
Jarred Sumner
6dc6dfb258 fix(repl): fix output ordering, add import/require support, improve error display
- Fix output buffering mismatch causing prompts to appear before results
  by using unbuffered writes consistently and flushing before prompt draw
- Convert static imports to awaited dynamic imports in REPL transform
  so `import x from 'mod'` works interactively
- Set up bound require() with proper CJS module context rooted at cwd
- Use Bun.inspect (ConsoleObject.format2) instead of util.inspect for
  rich value formatting (Response preview, colored output, etc.)
- Add SIGINT handling for hanging promises (await new Promise(() => {}))
  by temporarily re-enabling signals and using executionForbidden
- Enable TSX parsing in REPL for TypeScript/JSX support
- Fix DECLARE_CATCH_SCOPE build error, init macro context for imports
- Change prompt from "bun>" to dim "❯" (or ">" on basic terminals)
- Add 56 tests covering imports, require, .load/.save, errors,
  multiline, TypeScript, async, history navigation, and terminal PTY

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-23 18:53:16 -08:00
autofix-ci[bot]
2a8030ce60 [autofix.ci] apply automated fixes 2026-02-24 00:19:04 +00:00
Claude Bot
5f0648939d 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-02-23 16:17:09 -08:00
Claude Bot
793011b76a 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-02-23 16:16:19 -08:00
Claude Bot
e6e53013ec 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-02-23 16:16:18 -08:00
Claude Bot
e8130f57a7 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-02-23 16:16:18 -08:00
Claude Bot
23affefcb7 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-02-23 16:16:18 -08:00
Claude Bot
2e9b876824 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-02-23 16:16:18 -08:00
Claude Bot
a169f1ffa1 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-02-23 16:16:18 -08:00
Claude Bot
1c632299d0 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-02-23 16:16:17 -08:00
Claude Bot
ad593f454a 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-02-23 16:16:17 -08:00
autofix-ci[bot]
ae1cf65a79 [autofix.ci] apply automated fixes 2026-02-23 16:16:17 -08:00
Claude Bot
e10370e0ac 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-02-23 16:16:17 -08:00
autofix-ci[bot]
4d2d892285 [autofix.ci] apply automated fixes 2026-02-23 16:16:17 -08:00
Claude Bot
5176677e60 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-02-23 16:16:16 -08:00
18 changed files with 3200 additions and 39 deletions

View File

@@ -28,9 +28,6 @@ pub fn ReplTransforms(comptime P: type) type {
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;
@@ -41,6 +38,17 @@ pub fn ReplTransforms(comptime P: type) type {
}
}
// Check if there's top-level await or imports (imports become dynamic awaited imports)
var has_top_level_await = p.top_level_await_keyword.len > 0;
if (!has_top_level_await) {
for (all_stmts) |stmt| {
if (stmt.data == .s_import) {
has_top_level_await = true;
break;
}
}
}
// Apply transform with is_async based on presence of top-level await
try transformWithHoisting(p, parts, all_stmts, allocator, has_top_level_await);
}
@@ -154,6 +162,71 @@ pub fn ReplTransforms(comptime P: type) type {
try inner_stmts.append(stmt);
}
},
.s_import => |import_data| {
// Convert static imports to dynamic imports for REPL evaluation:
// import X from 'mod' -> var X = (await import('mod')).default
// import { a, b } from 'mod' -> var {a, b} = await import('mod')
// import * as X from 'mod' -> var X = await import('mod')
// import 'mod' -> await import('mod')
const path_str = p.import_records.items[import_data.import_record_index].path.text;
const import_expr = p.newExpr(E.Import{
.expr = p.newExpr(E.String{ .data = path_str }, stmt.loc),
.import_record_index = std.math.maxInt(u32),
}, stmt.loc);
const await_expr = p.newExpr(E.Await{ .value = import_expr }, stmt.loc);
if (import_data.star_name_loc) |_| {
// import * as X from 'mod' -> var X = await import('mod')
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 = import_data.namespace_ref }, stmt.loc),
.value = null,
},
}))),
}, stmt.loc));
const assign = p.newExpr(E.Binary{
.op = .bin_assign,
.left = p.newExpr(E.Identifier{ .ref = import_data.namespace_ref }, stmt.loc),
.right = await_expr,
}, stmt.loc);
try inner_stmts.append(p.s(S.SExpr{ .value = assign }, stmt.loc));
} else if (import_data.default_name) |default_name| {
// import X from 'mod' -> var X = (await import('mod')).default
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 = default_name.ref.? }, default_name.loc),
.value = null,
},
}))),
}, stmt.loc));
const dot_default = p.newExpr(E.Dot{
.target = await_expr,
.name = "default",
.name_loc = stmt.loc,
}, stmt.loc);
const assign = p.newExpr(E.Binary{
.op = .bin_assign,
.left = p.newExpr(E.Identifier{ .ref = default_name.ref.? }, default_name.loc),
.right = dot_default,
}, stmt.loc);
try inner_stmts.append(p.s(S.SExpr{ .value = assign }, stmt.loc));
// Also handle named imports alongside default: import X, { a } from 'mod'
if (import_data.items.len > 0) {
try convertNamedImports(p, import_data, &hoisted_stmts, &inner_stmts, allocator, stmt.loc);
}
} else if (import_data.items.len > 0) {
// import { a, b } from 'mod' -> destructure from await import('mod')
try convertNamedImports(p, import_data, &hoisted_stmts, &inner_stmts, allocator, stmt.loc);
} else {
// import 'mod' (side-effect only) -> await import('mod')
try inner_stmts.append(p.s(S.SExpr{ .value = await_expr }, stmt.loc));
}
},
.s_directive => |directive| {
// In REPL mode, treat directives (string literals) as expressions
const str_expr = p.newExpr(E.String{ .data = directive.value }, stmt.loc);
@@ -195,6 +268,68 @@ pub fn ReplTransforms(comptime P: type) type {
}
}
/// Convert named imports to individual var assignments from the dynamic import
/// import { a, b as c } from 'mod' ->
/// var a; var c; (hoisted)
/// var __mod = await import('mod'); a = __mod.a; c = __mod.b; (inner)
fn convertNamedImports(
p: *P,
import_data: *const S.Import,
hoisted_stmts: *ListManaged(Stmt),
inner_stmts: *ListManaged(Stmt),
allocator: Allocator,
loc: logger.Loc,
) !void {
const path_str = p.import_records.items[import_data.import_record_index].path.text;
const import_expr = p.newExpr(E.Import{
.expr = p.newExpr(E.String{ .data = path_str }, loc),
.import_record_index = std.math.maxInt(u32),
}, loc);
const await_expr = p.newExpr(E.Await{ .value = import_expr }, loc);
// Store the module in the namespace ref: var __ns = await import('mod')
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 = import_data.namespace_ref }, loc),
.value = null,
},
}))),
}, loc));
const ns_assign = p.newExpr(E.Binary{
.op = .bin_assign,
.left = p.newExpr(E.Identifier{ .ref = import_data.namespace_ref }, loc),
.right = await_expr,
}, loc);
try inner_stmts.append(p.s(S.SExpr{ .value = ns_assign }, loc));
// For each named import: var name; name = __ns.originalName;
for (import_data.items) |item| {
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 = item.name.ref.? }, item.name.loc),
.value = null,
},
}))),
}, loc));
const ns_ref_expr = p.newExpr(E.Identifier{ .ref = import_data.namespace_ref }, loc);
const prop_access = p.newExpr(E.Dot{
.target = ns_ref_expr,
.name = item.original_name,
.name_loc = item.name.loc,
}, loc);
const item_assign = p.newExpr(E.Binary{
.op = .bin_assign,
.left = p.newExpr(E.Identifier{ .ref = item.name.ref.? }, item.name.loc),
.right = prop_access,
}, loc);
try inner_stmts.append(p.s(S.SExpr{ .value = item_assign }, loc));
}
}
/// 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) {

View File

@@ -2669,7 +2669,7 @@ pub fn remapZigException(
allow_source_code_preview: bool,
) void {
error_instance.toZigException(this.global, exception);
const enable_source_code_preview = allow_source_code_preview and
var enable_source_code_preview = allow_source_code_preview and
!(bun.feature_flag.BUN_DISABLE_SOURCE_CODE_PREVIEW.get() or
bun.feature_flag.BUN_DISABLE_TRANSPILED_SOURCE_CODE_PREVIEW.get());
@@ -2764,6 +2764,12 @@ pub fn remapZigException(
}
}
// Don't show source code preview for REPL frames - it would show the
// transformed IIFE wrapper code, not what the user typed.
if (top.source_url.eqlComptime("[repl]")) {
enable_source_code_preview = false;
}
var top_source_url = top.source_url.toUTF8(bun.default_allocator);
defer top_source_url.deinit();
@@ -2815,7 +2821,6 @@ pub fn remapZigException(
// Avoid printing "export default 'native'"
break :code ZigString.Slice.empty;
}
var log = logger.Log.init(bun.default_allocator);
defer log.deinit();

View File

@@ -698,8 +698,7 @@ pub fn setRawMode(
if (comptime Environment.isPosix) {
// Use the existing TTY mode function
const mode: c_int = if (enabled) 1 else 0;
const tty_result = Bun__ttySetMode(this.master_fd.cast(), mode);
const tty_result = bun.tty.setMode(this.master_fd.cast(), if (enabled) .raw else .normal);
if (tty_result != 0) {
return globalObject.throw("Failed to set raw mode", .{});
}
@@ -708,9 +707,6 @@ pub fn setRawMode(
this.flags.raw_mode = enabled;
return .js_undefined;
}
extern fn Bun__ttySetMode(fd: c_int, mode: c_int) c_int;
/// POSIX termios struct for terminal flags manipulation
const Termios = if (Environment.isPosix) std.posix.termios else void;

View File

@@ -1,10 +1,14 @@
// clang-format off
#include "ModuleLoader.h"
#include "root.h"
#include "ModuleLoader.h"
#include "headers-handwritten.h"
#include "JSCommonJSModule.h"
#include <JavaScriptCore/JSBoundFunction.h>
#include <JavaScriptCore/PropertySlot.h>
#include <JavaScriptCore/JSMap.h>
#include <JavaScriptCore/JSString.h>
#include <JavaScriptCore/SourceCode.h>
#include "ZigGlobalObject.h"
#include "InternalModuleRegistry.h"
@@ -85,3 +89,44 @@ extern "C" [[ZIG_EXPORT(nothrow)]] void Bun__ExposeNodeModuleGlobals(Zig::Global
FOREACH_EXPOSED_BUILTIN_IMR(PUT_CUSTOM_GETTER_SETTER)
#undef PUT_CUSTOM_GETTER_SETTER
}
// Set up require(), module, __filename, __dirname on globalThis for the REPL.
// Creates a CommonJS module object rooted at the given directory so require() resolves correctly.
extern "C" [[ZIG_EXPORT(nothrow)]] void Bun__REPL__setupGlobalRequire(
Zig::GlobalObject* globalObject,
const unsigned char* cwdPtr,
size_t cwdLen)
{
using namespace JSC;
auto& vm = getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
auto cwdStr = WTF::String::fromUTF8(std::span { cwdPtr, cwdLen });
auto* filename = jsString(vm, makeString(cwdStr, "[repl]"_s));
auto* dirname = jsString(vm, WTF::String(cwdStr));
auto* moduleObject = Bun::JSCommonJSModule::create(vm,
globalObject->CommonJSModuleObjectStructure(),
filename, filename, dirname, SourceCode());
moduleObject->hasEvaluated = true;
auto* resolveFunction = JSBoundFunction::create(vm, globalObject,
globalObject->requireResolveFunctionUnbound(), filename,
ArgList(), 1, globalObject->commonStrings().resolveString(globalObject),
makeSource("resolve"_s, SourceOrigin(), SourceTaintedOrigin::Untainted));
RETURN_IF_EXCEPTION(scope, );
auto* requireFunction = JSBoundFunction::create(vm, globalObject,
globalObject->requireFunctionUnbound(), moduleObject,
ArgList(), 1, globalObject->commonStrings().requireString(globalObject),
makeSource("require"_s, SourceOrigin(), SourceTaintedOrigin::Untainted));
RETURN_IF_EXCEPTION(scope, );
requireFunction->putDirect(vm, vm.propertyNames->resolve, resolveFunction, 0);
moduleObject->putDirect(vm, WebCore::clientData(vm)->builtinNames().requirePublicName(), requireFunction, 0);
globalObject->putDirect(vm, WebCore::builtinNames(vm).requirePublicName(), requireFunction, 0);
globalObject->putDirect(vm, Identifier::fromString(vm, "module"_s), moduleObject, 0);
globalObject->putDirect(vm, Identifier::fromString(vm, "__filename"_s), filename, 0);
globalObject->putDirect(vm, Identifier::fromString(vm, "__dirname"_s), dirname, 0);
}

View File

@@ -6151,6 +6151,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_TOP_EXCEPTION_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

@@ -57,6 +57,46 @@ static std::optional<WTF::String> stripANSI(const std::span<const Char> input)
return result.toString();
}
struct BunANSIIterator {
const unsigned char* input;
size_t input_len;
size_t cursor;
const unsigned char* slice_ptr;
size_t slice_len;
};
extern "C" bool Bun__ANSI__next(BunANSIIterator* it)
{
auto start = it->input + it->cursor;
const auto end = it->input + it->input_len;
// Skip past any ANSI sequences at current position
while (start < end) {
const auto escPos = ANSI::findEscapeCharacter(start, end);
if (escPos != start) break;
const auto after = ANSI::consumeANSI(start, end);
if (after == start) {
start++;
break;
}
start = after;
}
if (start >= end) {
it->cursor = it->input_len;
it->slice_ptr = nullptr;
it->slice_len = 0;
return false;
}
const auto escPos = ANSI::findEscapeCharacter(start, end);
const auto slice_end = escPos ? escPos : end;
it->slice_ptr = start;
it->slice_len = slice_end - start;
it->cursor = slice_end - it->input;
return true;
}
JSC_DEFINE_HOST_FUNCTION(jsFunctionBunStripANSI, (JSC::JSGlobalObject * globalObject, JSC::CallFrame* callFrame))
{
auto& vm = globalObject->vm();

View File

@@ -191,6 +191,7 @@ pub const linux = @import("./linux.zig");
/// Translated from `c-headers-for-zig.h` for the current platform.
pub const c = @import("translated-c-headers");
pub const tty = @import("./tty.zig");
pub const sha = @import("./sha.zig");
pub const FeatureFlags = @import("./feature_flags.zig");

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");
@@ -842,12 +843,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 => {

View File

@@ -38,9 +38,6 @@ pub const InitCommand = struct {
return input.items[0 .. input.items.len - 1 :0];
}
}
extern fn Bun__ttySetMode(fd: i32, mode: i32) i32;
fn processRadioButton(label: string, comptime Choices: type) !Choices {
const colors = Output.enable_ansi_colors_stdout;
const choices = switch (colors) {
@@ -190,7 +187,7 @@ pub const InitCommand = struct {
}) catch null;
if (Environment.isPosix)
_ = Bun__ttySetMode(0, 1);
_ = bun.tty.setMode(0, .raw);
defer {
if (comptime Environment.isWindows) {
@@ -202,7 +199,7 @@ pub const InitCommand = struct {
}
}
if (Environment.isPosix) {
_ = Bun__ttySetMode(0, 0);
_ = bun.tty.setMode(0, .normal);
}
}

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

@@ -0,0 +1,163 @@
//! 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 up require(), module, __filename, __dirname relative to cwd
const cwd = vm.transpiler.fs.top_level_dir;
bun.cpp.Bun__REPL__setupGlobalRequire(vm.global, cwd.ptr, cwd.len);
// 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;

View File

@@ -1068,7 +1068,7 @@ pub const UpdateInteractiveCommand = struct {
}) catch null;
if (Environment.isPosix)
_ = Bun__ttySetMode(0, 1);
_ = bun.tty.setMode(0, .raw);
defer {
if (comptime Environment.isWindows) {
@@ -1080,7 +1080,7 @@ pub const UpdateInteractiveCommand = struct {
}
}
if (Environment.isPosix) {
_ = Bun__ttySetMode(0, 0);
_ = bun.tty.setMode(0, .normal);
}
}
@@ -1810,9 +1810,6 @@ pub const UpdateInteractiveCommand = struct {
}
}
};
extern fn Bun__ttySetMode(fd: c_int, mode: c_int) c_int;
const string = []const u8;
pub const CatalogUpdateRequest = struct {

1896
src/repl.zig Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2387,6 +2387,36 @@ pub const CodePoint = i32;
const string = []const u8;
/// SIMD-accelerated iterator that yields slices of text between ANSI escape sequences.
/// The C++ side uses ANSI::findEscapeCharacter (SIMD) and ANSI::consumeANSI.
pub const ANSIIterator = extern struct {
input: [*]const u8,
input_len: usize,
cursor: usize,
slice_ptr: ?[*]const u8,
slice_len: usize,
pub fn init(input: []const u8) ANSIIterator {
return .{
.input = input.ptr,
.input_len = input.len,
.cursor = 0,
.slice_ptr = null,
.slice_len = 0,
};
}
/// Returns the next slice of non-ANSI text, or null when done.
pub fn next(self: *ANSIIterator) ?[]const u8 {
if (Bun__ANSI__next(self)) {
return (self.slice_ptr orelse return null)[0..self.slice_len];
}
return null;
}
extern fn Bun__ANSI__next(it: *ANSIIterator) bool;
};
const escapeHTML_ = @import("./immutable/escapeHTML.zig");
const escapeRegExp_ = @import("./escapeRegExp.zig");
const paths_ = @import("./immutable/paths.zig");

11
src/tty.zig Normal file
View File

@@ -0,0 +1,11 @@
pub const Mode = enum(c_int) {
normal = 0,
raw = 1,
io = 2,
};
pub fn setMode(fd: c_int, mode: Mode) c_int {
return Bun__ttySetMode(fd, @intFromEnum(mode));
}
extern fn Bun__ttySetMode(fd: c_int, mode: c_int) c_int;

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,660 @@
// Tests for Bun REPL
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isWindows, tempDir } from "harness";
import path from "path";
// Helper to run REPL with piped stdin (non-TTY mode) 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",
NO_COLOR: "1",
...env,
},
});
proc.stdin.write(inputStr);
proc.stdin.flush();
proc.stdin.end();
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 };
}
function stripAnsi(str: string): string {
return str
.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "")
.replace(/\x1b\][^\x07]*\x07/g, "")
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "");
}
// Helper to run REPL in a PTY and interact with it
async function withTerminalRepl(
fn: (helpers: {
terminal: Bun.Terminal;
proc: Bun.ChildProcess;
send: (text: string) => void;
waitFor: (pattern: string | RegExp, timeoutMs?: number) => Promise<string>;
allOutput: () => string;
}) => Promise<void>,
) {
const received: string[] = [];
let cursor = 0;
let resolveWaiter: ((value: string) => void) | null = null;
let waiterPattern: string | RegExp | null = null;
await using terminal = new Bun.Terminal({
cols: 120,
rows: 40,
data(_term, data) {
const str = Buffer.from(data).toString();
received.push(str);
if (resolveWaiter && waiterPattern) {
const all = received.join("");
const recent = all.slice(cursor);
const match = typeof waiterPattern === "string" ? recent.includes(waiterPattern) : waiterPattern.test(recent);
if (match) {
cursor = all.length;
resolveWaiter(recent);
resolveWaiter = null;
waiterPattern = null;
}
}
},
});
await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
terminal,
env: {
...bunEnv,
TERM: "xterm-256color",
},
});
const send = (text: string) => terminal.write(text);
const waitFor = (pattern: string | RegExp, timeoutMs = 5000): Promise<string> => {
const all = received.join("");
const recent = all.slice(cursor);
const alreadyMatch = typeof pattern === "string" ? recent.includes(pattern) : pattern.test(recent);
if (alreadyMatch) {
cursor = all.length;
return Promise.resolve(recent);
}
return new Promise<string>((resolve, reject) => {
resolveWaiter = resolve;
waiterPattern = pattern;
setTimeout(() => {
resolveWaiter = null;
waiterPattern = null;
reject(
new Error(
`Timed out waiting for pattern: ${pattern}\nReceived so far:\n${stripAnsi(received.join("").slice(cursor))}`,
),
);
}, timeoutMs);
});
};
const allOutput = () => stripAnsi(received.join(""));
await waitFor(/\u276f|> /); // Wait for prompt
await fn({ terminal, proc, send, waitFor, allOutput });
// Clean exit
send(".exit\n");
await Promise.race([proc.exited, Bun.sleep(2000)]);
if (!proc.killed) proc.kill();
}
describe("Bun REPL", () => {
describe("basic evaluation", () => {
test("evaluates simple expression", async () => {
const { stdout, exitCode } = await runRepl(["1 + 1", ".exit"]);
expect(stripAnsi(stdout)).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 output = stripAnsi(stdout);
expect(output).toContain("2");
expect(output).toContain("6");
expect(output).toContain("4");
expect(exitCode).toBe(0);
});
test("evaluates string expressions", async () => {
const { stdout, exitCode } = await runRepl(["'hello'.toUpperCase()", ".exit"]);
expect(stripAnsi(stdout)).toContain("HELLO");
expect(exitCode).toBe(0);
});
test("evaluates object literals", async () => {
const { stdout, exitCode } = await runRepl(["({ a: 1, b: 2 })", ".exit"]);
const output = stripAnsi(stdout);
expect(output).toContain("a");
expect(output).toContain("b");
expect(exitCode).toBe(0);
});
test("evaluates array expressions", async () => {
const { stdout, exitCode } = await runRepl(["[1, 2, 3].map(x => x * 2)", ".exit"]);
const output = stripAnsi(stdout);
expect(output).toContain("2");
expect(output).toContain("4");
expect(output).toContain("6");
expect(exitCode).toBe(0);
});
});
describe("special variables", () => {
test("_ contains last result", async () => {
const { stdout, exitCode } = await runRepl(["42", "_", ".exit"]);
const output = stripAnsi(stdout);
// 42 should appear at least twice: once for the eval, once for _
expect(output.split("42").length - 1).toBeGreaterThanOrEqual(2);
expect(exitCode).toBe(0);
});
test("_ updates with each result", async () => {
const { stdout, exitCode } = await runRepl(["10", "_ * 2", "_ + 5", ".exit"]);
const output = stripAnsi(stdout);
expect(output).toContain("10");
expect(output).toContain("20");
expect(output).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"]);
expect(stripAnsi(stdout)).toContain("test error");
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(output).toContain(".load");
expect(output).toContain(".save");
expect(exitCode).toBe(0);
});
test(".load loads and evaluates a file", async () => {
using dir = tempDir("repl-load-test", {
"test.js": "var loadedVar = 42;\n",
});
const filePath = path.join(String(dir), "test.js");
const { stdout, exitCode } = await runRepl([`.load ${filePath}`, "loadedVar", ".exit"]);
const output = stripAnsi(stdout);
expect(output).toContain("42");
expect(exitCode).toBe(0);
});
test(".load with nonexistent file shows error", async () => {
const { stdout, stderr, exitCode } = await runRepl([".load /nonexistent/path/file.js", "1 + 1", ".exit"]);
const allOutput = stripAnsi(stdout + stderr);
expect(allOutput.toLowerCase()).toMatch(/error|not found|no such file/i);
// REPL should continue after failed load
expect(allOutput).toContain("2");
expect(exitCode).toBe(0);
});
test(".load without filename shows usage", async () => {
const { stdout, exitCode } = await runRepl([".load", ".exit"]);
const output = stripAnsi(stdout);
expect(output.toLowerCase()).toMatch(/usage|filename/i);
expect(exitCode).toBe(0);
});
test(".save saves history to file", async () => {
using dir = tempDir("repl-save-test", {});
const filePath = path.join(String(dir), "saved.js");
const { exitCode } = await runRepl(["const x = 1", "const y = 2", `.save ${filePath}`, ".exit"]);
expect(exitCode).toBe(0);
const content = await Bun.file(filePath).text();
expect(content).toContain("const x = 1");
expect(content).toContain("const y = 2");
});
test(".save without filename shows usage", async () => {
const { stdout, exitCode } = await runRepl([".save", ".exit"]);
const output = stripAnsi(stdout);
expect(output.toLowerCase()).toMatch(/usage|filename/i);
expect(exitCode).toBe(0);
});
test("unknown command shows error", async () => {
const { stdout, exitCode } = await runRepl([".nonexistent", "1 + 1", ".exit"]);
const output = stripAnsi(stdout);
expect(output.toLowerCase()).toContain("unknown");
// REPL should continue
expect(output).toContain("2");
expect(exitCode).toBe(0);
});
});
describe(".copy command", () => {
test(".copy with no args copies last result", async () => {
const { stdout, exitCode } = await runRepl(["42", ".copy", ".exit"]);
const output = stripAnsi(stdout);
expect(output).toContain("Copied");
expect(output).toContain("clipboard");
expect(exitCode).toBe(0);
});
test(".copy with expression evaluates and copies", async () => {
const { stdout, exitCode } = await runRepl([".copy 1 + 1", ".exit"]);
const output = stripAnsi(stdout);
expect(output).toContain("Copied");
expect(output).toContain("clipboard");
expect(exitCode).toBe(0);
});
test(".copy still sets _ variable", async () => {
const { stdout, exitCode } = await runRepl([".copy 'hello'", "_", ".exit"]);
const output = stripAnsi(stdout);
expect(output).toContain("hello");
expect(exitCode).toBe(0);
});
});
describe("error handling", () => {
test("handles syntax errors gracefully", async () => {
const { stdout, stderr, exitCode } = await runRepl(["(1 + ))", "1 + 1", ".exit"]);
const allOutput = stripAnsi(stdout + stderr);
expect(allOutput.toLowerCase()).toContain("error");
// REPL should continue working after syntax error
expect(allOutput).toContain("2");
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).toMatch(/not defined|ReferenceError/);
expect(exitCode).toBe(0);
});
test("handles thrown string errors", async () => {
const { stdout, stderr, exitCode } = await runRepl(["throw 'custom error'", "1 + 1", ".exit"]);
const allOutput = stripAnsi(stdout + stderr);
expect(allOutput).toContain("custom error");
// REPL should continue after thrown error
expect(allOutput).toContain("2");
expect(exitCode).toBe(0);
});
test("handles thrown Error objects", async () => {
const { stdout, stderr, exitCode } = await runRepl(["throw new Error('boom')", ".exit"]);
const allOutput = stripAnsi(stdout + stderr);
expect(allOutput).toContain("boom");
expect(exitCode).toBe(0);
});
test("shows system error properties", async () => {
const { stdout, stderr, exitCode } = await runRepl(["fs.readFileSync('/nonexistent/path/file.txt')", ".exit"]);
const allOutput = stripAnsi(stdout + stderr);
expect(allOutput).toMatch(/ENOENT|no such file/);
expect(exitCode).toBe(0);
});
});
describe("import statements", () => {
test("import default from builtin module", async () => {
const { stdout, exitCode } = await runRepl(["import path from 'path'", "typeof path.join", ".exit"]);
const output = stripAnsi(stdout);
expect(output).toContain("function");
expect(exitCode).toBe(0);
});
test("import named exports from builtin module", async () => {
const { stdout, exitCode } = await runRepl(["import { join, resolve } from 'path'", "typeof join", ".exit"]);
const output = stripAnsi(stdout);
expect(output).toContain("function");
expect(exitCode).toBe(0);
});
test("import namespace from builtin module", async () => {
const { stdout, exitCode } = await runRepl(["import * as os from 'os'", "typeof os.cpus", ".exit"]);
const output = stripAnsi(stdout);
expect(output).toContain("function");
expect(exitCode).toBe(0);
});
test("import used across lines", async () => {
const { stdout, exitCode } = await runRepl(["import path from 'path'", "path.join('/tmp', 'test')", ".exit"]);
const output = stripAnsi(stdout);
expect(output).toContain("/tmp/test");
expect(exitCode).toBe(0);
});
test("import nonexistent module shows error", async () => {
const { stdout, stderr, exitCode } = await runRepl(["import _ from 'nonexistent-module-xyz'", "1 + 1", ".exit"]);
const allOutput = stripAnsi(stdout + stderr);
// Should show an error about the module not being found
expect(allOutput.toLowerCase()).toMatch(/error|not found|cannot find|resolve/);
// REPL should continue after failed import
expect(allOutput).toContain("2");
expect(exitCode).toBe(0);
});
});
describe("require", () => {
test("require is defined", async () => {
const { stdout, exitCode } = await runRepl(["typeof require", ".exit"]);
expect(stripAnsi(stdout)).toContain("function");
expect(exitCode).toBe(0);
});
test("require builtin module", async () => {
const { stdout, exitCode } = await runRepl(["const path = require('path')", "typeof path.join", ".exit"]);
expect(stripAnsi(stdout)).toContain("function");
expect(exitCode).toBe(0);
});
test("require.resolve works", async () => {
const { stdout, exitCode } = await runRepl(["typeof require.resolve", ".exit"]);
expect(stripAnsi(stdout)).toContain("function");
expect(exitCode).toBe(0);
});
});
describe("global objects", () => {
test("has access to Bun globals", async () => {
const { stdout, exitCode } = await runRepl(["typeof Bun.version", ".exit"]);
expect(stripAnsi(stdout)).toContain("string");
expect(exitCode).toBe(0);
});
test("has access to console", async () => {
const { stdout, exitCode } = await runRepl(["console.log('hello from repl')", ".exit"]);
expect(stripAnsi(stdout)).toContain("hello from repl");
expect(exitCode).toBe(0);
});
test("has access to Buffer", async () => {
const { stdout, exitCode } = await runRepl(["Buffer.from('hello').length", ".exit"]);
expect(stripAnsi(stdout)).toContain("5");
expect(exitCode).toBe(0);
});
test("has access to process", async () => {
const { stdout, exitCode } = await runRepl(["typeof process.version", ".exit"]);
expect(stripAnsi(stdout)).toContain("string");
expect(exitCode).toBe(0);
});
test("has __dirname and __filename", async () => {
const { stdout, exitCode } = await runRepl(["typeof __dirname", "typeof __filename", ".exit"]);
const output = stripAnsi(stdout);
expect(output).toContain("string");
expect(exitCode).toBe(0);
});
test("has module object", async () => {
const { stdout, exitCode } = await runRepl(["typeof module", ".exit"]);
expect(stripAnsi(stdout)).toContain("object");
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"]);
expect(stripAnsi(stdout)).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"]);
expect(stripAnsi(stdout)).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"]);
expect(stripAnsi(stdout)).toContain("8");
expect(exitCode).toBe(0);
});
});
describe("multiline input", () => {
test("handles multiline function definition", async () => {
const { stdout, exitCode } = await runRepl([
"function greet(name) {",
" return 'hi ' + name",
"}",
"greet('world')",
".exit",
]);
expect(stripAnsi(stdout)).toContain("hi world");
expect(exitCode).toBe(0);
});
test("handles multiline object", async () => {
const { stdout, exitCode } = await runRepl(["({", " x: 1,", " y: 2", "})", ".exit"]);
const output = stripAnsi(stdout);
expect(output).toContain("x");
expect(output).toContain("y");
expect(exitCode).toBe(0);
});
});
describe("async evaluation", () => {
test("await expressions", async () => {
const { stdout, exitCode } = await runRepl(["await Promise.resolve(42)", ".exit"]);
expect(stripAnsi(stdout)).toContain("42");
expect(exitCode).toBe(0);
});
test("await rejected promise shows error", async () => {
const { stdout, stderr, exitCode } = await runRepl([
"await Promise.reject(new Error('async fail'))",
"1 + 1",
".exit",
]);
const allOutput = stripAnsi(stdout + stderr);
expect(allOutput).toContain("async fail");
// REPL should continue after rejected promise
expect(allOutput).toContain("2");
expect(exitCode).toBe(0);
});
test("async functions", async () => {
const { stdout, exitCode } = await runRepl([
"async function getValue() { return 123; }",
"await getValue()",
".exit",
]);
expect(stripAnsi(stdout)).toContain("123");
expect(exitCode).toBe(0);
});
});
describe("TypeScript support", () => {
test("type annotations are stripped", async () => {
const { stdout, exitCode } = await runRepl(["const x: number = 42", "x", ".exit"]);
expect(stripAnsi(stdout)).toContain("42");
expect(exitCode).toBe(0);
});
test("interface declarations work", async () => {
const { stdout, exitCode } = await runRepl([
"interface User { name: string }",
"const u: User = { name: 'test' }",
"u.name",
".exit",
]);
expect(stripAnsi(stdout)).toContain("test");
expect(exitCode).toBe(0);
});
});
describe("welcome message", () => {
test("shows welcome message with version", async () => {
const { stdout, exitCode } = await runRepl([".exit"]);
const output = stripAnsi(stdout);
expect(output).toContain("Welcome to Bun");
expect(output).toMatch(/Bun v\d+\.\d+\.\d+/);
expect(exitCode).toBe(0);
});
});
});
// Interactive terminal-based REPL tests
describe.todoIf(isWindows)("Bun REPL (Terminal)", () => {
test("shows welcome message and prompt", async () => {
await withTerminalRepl(async ({ allOutput }) => {
const output = allOutput();
expect(output).toContain("Welcome to Bun");
expect(output).toMatch(/\u276f|> /);
});
});
test("evaluates expression and shows result", async () => {
await withTerminalRepl(async ({ send, waitFor }) => {
send("40 + 2\n");
const output = await waitFor("42");
expect(stripAnsi(output)).toContain("42");
});
});
test("error shows in terminal", async () => {
await withTerminalRepl(async ({ send, waitFor }) => {
send("throw new Error('order test')\n");
const output = await waitFor("order test");
expect(stripAnsi(output)).toContain("order test");
});
});
test("console.log shows in terminal", async () => {
await withTerminalRepl(async ({ send, waitFor }) => {
send("console.log('side effect')\n");
const output = await waitFor("side effect");
expect(stripAnsi(output)).toContain("side effect");
});
});
test("Ctrl+C cancels current input", async () => {
await withTerminalRepl(async ({ send, waitFor, allOutput }) => {
send("some partial input");
await waitFor("some partial input");
send("\x03"); // Ctrl+C
await waitFor(/\u276f|> /);
// Should be back at a clean prompt
send("1 + 1\n");
await waitFor("2");
});
});
test("Ctrl+D exits on empty line", async () => {
const received: string[] = [];
await using terminal = new Bun.Terminal({
cols: 120,
rows: 40,
data(_term, data) {
received.push(Buffer.from(data).toString());
},
});
await using proc = Bun.spawn({
cmd: [bunExe(), "repl"],
terminal,
env: { ...bunEnv, TERM: "xterm-256color" },
});
// Wait for ready
await new Promise<void>(resolve => {
const check = () => {
if (received.join("").includes("\u276f") || received.join("").includes("> ")) return resolve();
setTimeout(check, 50);
};
check();
});
terminal.write("\x04"); // Ctrl+D
const exitCode = await Promise.race([proc.exited, Bun.sleep(3000).then(() => -1)]);
expect(exitCode).toBe(0);
});
test("require works in terminal", async () => {
await withTerminalRepl(async ({ send, waitFor }) => {
send("typeof require\n");
const output = await waitFor("function");
expect(stripAnsi(output)).toContain("function");
});
});
test("import statement works in terminal", async () => {
await withTerminalRepl(async ({ send, waitFor }) => {
send("import path from 'path'\n");
// Wait for the import to complete
await waitFor(/\u276f|> /);
send("path.sep\n");
await waitFor("/");
});
});
test("up arrow recalls previous command", async () => {
await withTerminalRepl(async ({ send, waitFor }) => {
send("111 + 222\n");
await waitFor("333");
// Press up arrow to recall previous command
send("\x1b[A"); // Up arrow escape sequence
await Bun.sleep(100);
send("\n");
// Should evaluate the same expression again
await waitFor("333");
});
});
test("multiline input with open brace", async () => {
await withTerminalRepl(async ({ send, waitFor }) => {
send("function test() {\n");
await waitFor("..."); // multiline prompt
send(" return 99\n");
send("}\n");
// Wait for function to be defined
await waitFor(/\u276f|> /);
send("test()\n");
await waitFor("99");
});
});
});

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");
});
});