mirror of
https://github.com/oven-sh/bun
synced 2026-02-24 18:47:18 +01:00
Compare commits
26 Commits
claude/fix
...
claude/rep
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31c8881f12 | ||
|
|
6984355cfd | ||
|
|
3334539705 | ||
|
|
9ac02b2954 | ||
|
|
46ecd5c56b | ||
|
|
79573ace56 | ||
|
|
be5578d6c5 | ||
|
|
f6d0cba24b | ||
|
|
dfabbcece2 | ||
|
|
33fb0dea71 | ||
|
|
6ba9f68199 | ||
|
|
6dc6dfb258 | ||
|
|
2a8030ce60 | ||
|
|
5f0648939d | ||
|
|
793011b76a | ||
|
|
e6e53013ec | ||
|
|
e8130f57a7 | ||
|
|
23affefcb7 | ||
|
|
2e9b876824 | ||
|
|
a169f1ffa1 | ||
|
|
1c632299d0 | ||
|
|
ad593f454a | ||
|
|
ae1cf65a79 | ||
|
|
e10370e0ac | ||
|
|
4d2d892285 | ||
|
|
5176677e60 |
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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
163
src/cli/repl_command.zig
Normal 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;
|
||||
@@ -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
1896
src/repl.zig
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
11
src/tty.zig
Normal 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;
|
||||
@@ -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();
|
||||
});
|
||||
660
test/js/bun/repl/repl.test.ts
Normal file
660
test/js/bun/repl/repl.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
});
|
||||
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