mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 06:12:08 +00:00
Compare commits
8 Commits
claude/fix
...
claude/bui
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43f5df2596 | ||
|
|
c189cdc60f | ||
|
|
d630b68d15 | ||
|
|
768b60ebf1 | ||
|
|
ea511ed08c | ||
|
|
804c716f8f | ||
|
|
c30b36f433 | ||
|
|
78f1d497f4 |
@@ -92,6 +92,7 @@ pub const AuditCommand = @import("./cli/audit_command.zig").AuditCommand;
|
||||
pub const InitCommand = @import("./cli/init_command.zig").InitCommand;
|
||||
pub const WhyCommand = @import("./cli/why_command.zig").WhyCommand;
|
||||
pub const FuzzilliCommand = @import("./cli/fuzzilli_command.zig").FuzzilliCommand;
|
||||
pub const ReplCommand = @import("./cli/repl_command.zig").ReplCommand;
|
||||
|
||||
pub const Arguments = @import("./cli/Arguments.zig");
|
||||
|
||||
@@ -813,12 +814,8 @@ pub const Command = struct {
|
||||
return;
|
||||
},
|
||||
.ReplCommand => {
|
||||
// TODO: Put this in native code.
|
||||
var ctx = try Command.init(allocator, log, .BunxCommand);
|
||||
ctx.debug.run_in_bun = true; // force the same version of bun used. fixes bun-debug for example
|
||||
var args = bun.argv[0..];
|
||||
args[1] = "bun-repl";
|
||||
try BunxCommand.exec(ctx, args);
|
||||
const ctx = try Command.init(allocator, log, .RunCommand);
|
||||
try ReplCommand.exec(ctx);
|
||||
return;
|
||||
},
|
||||
.RemoveCommand => {
|
||||
|
||||
87
src/cli/repl_command.zig
Normal file
87
src/cli/repl_command.zig
Normal file
@@ -0,0 +1,87 @@
|
||||
pub const ReplCommand = struct {
|
||||
pub fn exec(ctx: Command.Context) !void {
|
||||
@branchHint(.cold);
|
||||
|
||||
// Embed the REPL script
|
||||
const repl_script = @embedFile("../js/eval/repl.ts");
|
||||
|
||||
// Get platform-specific temp directory
|
||||
const temp_dir = bun.fs.FileSystem.RealFS.platformTempDir();
|
||||
|
||||
// Create unique temp file name with PID to avoid collisions
|
||||
const pid = if (bun.Environment.isWindows)
|
||||
std.os.windows.GetCurrentProcessId()
|
||||
else
|
||||
std.c.getpid();
|
||||
|
||||
// Format the filename with PID (null-terminated for syscalls)
|
||||
var filename_buf: [64:0]u8 = undefined;
|
||||
const filename = std.fmt.bufPrintZ(&filename_buf, "bun-repl-{d}.ts", .{pid}) catch {
|
||||
Output.prettyErrorln("<r><red>error<r>: Could not create temp file name", .{});
|
||||
Global.exit(1);
|
||||
};
|
||||
|
||||
// Join temp_dir and filename using platform-aware path joining
|
||||
var temp_path_buf: [bun.MAX_PATH_BYTES]u8 = undefined;
|
||||
const temp_path = bun.path.joinAbsStringBufZ(temp_dir, &temp_path_buf, &.{filename}, .auto);
|
||||
|
||||
// Open temp directory for openat/unlinkat operations
|
||||
const temp_dir_z = std.posix.toPosixPath(temp_dir) catch {
|
||||
Output.prettyErrorln("<r><red>error<r>: Temp directory path too long", .{});
|
||||
Global.exit(1);
|
||||
};
|
||||
const temp_dir_fd = switch (bun.sys.open(&temp_dir_z, bun.O.DIRECTORY | bun.O.RDONLY, 0)) {
|
||||
.result => |fd| fd,
|
||||
.err => {
|
||||
Output.prettyErrorln("<r><red>error<r>: Could not access temp directory", .{});
|
||||
Global.exit(1);
|
||||
},
|
||||
};
|
||||
defer temp_dir_fd.close();
|
||||
|
||||
const temp_file_fd = switch (bun.sys.openat(temp_dir_fd, filename, bun.O.CREAT | bun.O.WRONLY | bun.O.TRUNC, 0o644)) {
|
||||
.result => |fd| fd,
|
||||
.err => {
|
||||
Output.prettyErrorln("<r><red>error<r>: Could not create temp file", .{});
|
||||
Global.exit(1);
|
||||
},
|
||||
};
|
||||
|
||||
// Write the script to the temp file, handling partial writes
|
||||
var offset: usize = 0;
|
||||
while (offset < repl_script.len) {
|
||||
switch (bun.sys.write(temp_file_fd, repl_script[offset..])) {
|
||||
.err => {
|
||||
Output.prettyErrorln("<r><red>error<r>: Could not write temp file", .{});
|
||||
temp_file_fd.close();
|
||||
Global.exit(1);
|
||||
},
|
||||
.result => |written| {
|
||||
if (written == 0) {
|
||||
Output.prettyErrorln("<r><red>error<r>: Could not write temp file: write returned 0 bytes", .{});
|
||||
temp_file_fd.close();
|
||||
Global.exit(1);
|
||||
}
|
||||
offset += written;
|
||||
},
|
||||
}
|
||||
}
|
||||
temp_file_fd.close();
|
||||
|
||||
// Ensure cleanup on exit - unlink temp file after Run.boot returns
|
||||
defer {
|
||||
_ = bun.sys.unlinkat(temp_dir_fd, filename);
|
||||
}
|
||||
|
||||
// Run the temp file
|
||||
try Run.boot(ctx, temp_path, null);
|
||||
}
|
||||
};
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const bun = @import("bun");
|
||||
const Global = bun.Global;
|
||||
const Output = bun.Output;
|
||||
const Command = bun.cli.Command;
|
||||
const Run = bun.bun_js.Run;
|
||||
390
src/js/eval/repl.ts
Normal file
390
src/js/eval/repl.ts
Normal file
@@ -0,0 +1,390 @@
|
||||
// Built-in REPL implementation for `bun repl`
|
||||
// This replaces the external bun-repl package for faster startup
|
||||
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import readline from "node:readline";
|
||||
import util from "node:util";
|
||||
import { runInThisContext } from "node:vm";
|
||||
|
||||
// REPL state
|
||||
let lastResult: any = undefined;
|
||||
let lastError: any = undefined;
|
||||
let lineBuffer = "";
|
||||
let inMultilineInput = false;
|
||||
|
||||
// ANSI color codes
|
||||
const useColors = Boolean(process.stdout.isTTY && !("NO_COLOR" in process.env));
|
||||
const colors = {
|
||||
reset: useColors ? "\x1b[0m" : "",
|
||||
cyan: useColors ? "\x1b[36m" : "",
|
||||
yellow: useColors ? "\x1b[33m" : "",
|
||||
red: useColors ? "\x1b[31m" : "",
|
||||
green: useColors ? "\x1b[32m" : "",
|
||||
dim: useColors ? "\x1b[2m" : "",
|
||||
};
|
||||
|
||||
function colorize(text: string, color: string): string {
|
||||
return color ? `${color}${text}${colors.reset}` : text;
|
||||
}
|
||||
|
||||
// History file path - handle edge case where homedir() returns empty string
|
||||
const homeDir = os.homedir();
|
||||
const historyPath = homeDir ? path.join(homeDir, ".bun_repl_history") : "";
|
||||
const maxHistorySize = 1000;
|
||||
|
||||
// Debounce timer for history saves
|
||||
let historySaveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let pendingHistory: string[] | null = null;
|
||||
|
||||
function loadHistory(): string[] {
|
||||
if (!historyPath) return [];
|
||||
try {
|
||||
if (fs.existsSync(historyPath)) {
|
||||
const content = fs.readFileSync(historyPath, "utf-8");
|
||||
return content.split("\n").filter((line: string) => line.trim());
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors loading history
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function saveHistoryImmediate(history: string[]): void {
|
||||
if (!historyPath) return;
|
||||
try {
|
||||
const toSave = history.slice(-maxHistorySize);
|
||||
fs.writeFileSync(historyPath, toSave.join("\n") + "\n");
|
||||
} catch {
|
||||
// Ignore errors saving history
|
||||
}
|
||||
}
|
||||
|
||||
function saveHistory(history: string[]): void {
|
||||
// Debounce history writes - save after 1 second of inactivity
|
||||
pendingHistory = history;
|
||||
if (historySaveTimer) {
|
||||
clearTimeout(historySaveTimer);
|
||||
}
|
||||
historySaveTimer = setTimeout(() => {
|
||||
if (pendingHistory) {
|
||||
saveHistoryImmediate(pendingHistory);
|
||||
pendingHistory = null;
|
||||
}
|
||||
historySaveTimer = null;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function flushHistory(): void {
|
||||
// Flush any pending history writes immediately
|
||||
if (historySaveTimer) {
|
||||
clearTimeout(historySaveTimer);
|
||||
historySaveTimer = null;
|
||||
}
|
||||
if (pendingHistory) {
|
||||
saveHistoryImmediate(pendingHistory);
|
||||
pendingHistory = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if code is incomplete (e.g., unclosed brackets)
|
||||
function isIncompleteCode(code: string): boolean {
|
||||
// Simple bracket counting approach
|
||||
let braceCount = 0;
|
||||
let bracketCount = 0;
|
||||
let parenCount = 0;
|
||||
let inString: string | null = null;
|
||||
let inTemplate = false;
|
||||
let escaped = false;
|
||||
|
||||
for (let i = 0; i < code.length; i++) {
|
||||
const char = code[i];
|
||||
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char === "\\") {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle strings
|
||||
if (!inString && !inTemplate) {
|
||||
if (char === '"' || char === "'") {
|
||||
inString = char;
|
||||
continue;
|
||||
}
|
||||
if (char === "`") {
|
||||
inTemplate = true;
|
||||
continue;
|
||||
}
|
||||
} else if (inString && char === inString) {
|
||||
inString = null;
|
||||
continue;
|
||||
} else if (inTemplate && char === "`") {
|
||||
inTemplate = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip content inside strings
|
||||
if (inString || inTemplate) continue;
|
||||
|
||||
// Count brackets
|
||||
switch (char) {
|
||||
case "{":
|
||||
braceCount++;
|
||||
break;
|
||||
case "}":
|
||||
braceCount--;
|
||||
break;
|
||||
case "[":
|
||||
bracketCount++;
|
||||
break;
|
||||
case "]":
|
||||
bracketCount--;
|
||||
break;
|
||||
case "(":
|
||||
parenCount++;
|
||||
break;
|
||||
case ")":
|
||||
parenCount--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Incomplete if any unclosed delimiters or unclosed strings
|
||||
return inString !== null || inTemplate || braceCount > 0 || bracketCount > 0 || parenCount > 0;
|
||||
}
|
||||
|
||||
// REPL commands
|
||||
const replCommands: Record<string, { help: string; action: (args: string) => void }> = {
|
||||
".help": {
|
||||
help: "Print this help message",
|
||||
action: () => {
|
||||
console.log("REPL Commands:");
|
||||
for (const [cmd, { help }] of Object.entries(replCommands)) {
|
||||
console.log(` ${cmd.padEnd(12)} ${help}`);
|
||||
}
|
||||
},
|
||||
},
|
||||
".exit": {
|
||||
help: "Exit the REPL",
|
||||
action: () => {
|
||||
process.exit(0);
|
||||
},
|
||||
},
|
||||
".clear": {
|
||||
help: "Clear the REPL context",
|
||||
action: () => {
|
||||
lastResult = undefined;
|
||||
lastError = undefined;
|
||||
console.log("REPL context cleared");
|
||||
},
|
||||
},
|
||||
".load": {
|
||||
help: "Load a file into the REPL session",
|
||||
action: (filename: string) => {
|
||||
if (!filename.trim()) {
|
||||
console.log(colorize("Usage: .load <filename>", colors.red));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Resolve relative paths against the user's current working directory
|
||||
const resolvedPath = path.resolve(process.cwd(), filename.trim());
|
||||
const code = fs.readFileSync(resolvedPath, "utf-8");
|
||||
const result = evaluateCode(code);
|
||||
if (result !== undefined) {
|
||||
console.log(formatResult(result));
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.log(colorize(`Error loading file: ${err.message}`, colors.red));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Evaluate code in the global context
|
||||
function evaluateCode(code: string): any {
|
||||
// Handle special _ and _error variables
|
||||
(globalThis as any)._ = lastResult;
|
||||
(globalThis as any)._error = lastError;
|
||||
|
||||
try {
|
||||
// Use runInThisContext for proper JavaScript evaluation
|
||||
const result = runInThisContext(code, {
|
||||
filename: "repl",
|
||||
displayErrors: true,
|
||||
});
|
||||
lastResult = result;
|
||||
return result;
|
||||
} catch (err: any) {
|
||||
lastError = err;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Format the result for display
|
||||
function formatResult(result: any): string {
|
||||
if (result === undefined) {
|
||||
return colorize("undefined", colors.dim);
|
||||
}
|
||||
return util.inspect(result, {
|
||||
colors: useColors,
|
||||
depth: 4,
|
||||
maxArrayLength: 100,
|
||||
maxStringLength: 10000,
|
||||
breakLength: process.stdout.columns || 80,
|
||||
});
|
||||
}
|
||||
|
||||
// Get the prompt string
|
||||
function getPrompt(): string {
|
||||
if (inMultilineInput) {
|
||||
return colorize("... ", colors.dim);
|
||||
}
|
||||
return colorize("bun", colors.green) + colorize("> ", colors.reset);
|
||||
}
|
||||
|
||||
// Simple tab completer
|
||||
function completer(line: string): [string[], string] {
|
||||
const completions: string[] = [];
|
||||
const trimmed = line.trim();
|
||||
|
||||
// Complete REPL commands
|
||||
if (trimmed.startsWith(".")) {
|
||||
const matches = Object.keys(replCommands).filter(cmd => cmd.startsWith(trimmed));
|
||||
return [matches, trimmed];
|
||||
}
|
||||
|
||||
// Try to complete global properties
|
||||
try {
|
||||
// Find the last word being typed
|
||||
const match = line.match(/[\w$]+$/);
|
||||
if (match) {
|
||||
const prefix = match[0];
|
||||
const props = Object.getOwnPropertyNames(globalThis).filter(p => p.startsWith(prefix));
|
||||
return [props, prefix];
|
||||
}
|
||||
} catch {
|
||||
// Ignore completion errors
|
||||
}
|
||||
|
||||
return [completions, line];
|
||||
}
|
||||
|
||||
// Handle a line of input
|
||||
function handleLine(line: string, rl: any, history: string[]): void {
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
// Handle empty line
|
||||
if (!trimmedLine && !inMultilineInput) {
|
||||
rl.prompt();
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle REPL commands
|
||||
if (trimmedLine.startsWith(".") && !inMultilineInput) {
|
||||
const spaceIndex = trimmedLine.indexOf(" ");
|
||||
const cmd = spaceIndex > 0 ? trimmedLine.slice(0, spaceIndex) : trimmedLine;
|
||||
const args = spaceIndex > 0 ? trimmedLine.slice(spaceIndex + 1) : "";
|
||||
|
||||
if (replCommands[cmd]) {
|
||||
replCommands[cmd].action(args);
|
||||
rl.prompt();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Accumulate input
|
||||
lineBuffer += (lineBuffer ? "\n" : "") + line;
|
||||
|
||||
// Check if code is complete
|
||||
if (isIncompleteCode(lineBuffer)) {
|
||||
inMultilineInput = true;
|
||||
rl.setPrompt(getPrompt());
|
||||
rl.prompt();
|
||||
return;
|
||||
}
|
||||
|
||||
const code = lineBuffer;
|
||||
lineBuffer = "";
|
||||
inMultilineInput = false;
|
||||
|
||||
// Add to history
|
||||
if (code.trim()) {
|
||||
history.push(code);
|
||||
saveHistory(history);
|
||||
}
|
||||
|
||||
// Evaluate the code
|
||||
try {
|
||||
const result = evaluateCode(code);
|
||||
if (result !== undefined) {
|
||||
console.log(formatResult(result));
|
||||
}
|
||||
} catch (err: any) {
|
||||
// Format error message
|
||||
if (err.name === "SyntaxError") {
|
||||
console.log(colorize(`SyntaxError: ${err.message}`, colors.red));
|
||||
} else {
|
||||
console.log(colorize(`${err.name || "Error"}: ${err.message}`, colors.red));
|
||||
if (err.stack && process.env.BUN_DEBUG) {
|
||||
console.log(colorize(err.stack, colors.dim));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rl.setPrompt(getPrompt());
|
||||
rl.prompt();
|
||||
}
|
||||
|
||||
// Main REPL function
|
||||
function startRepl(): void {
|
||||
// Print welcome message
|
||||
console.log(`Welcome to Bun v${Bun.version}`);
|
||||
console.log('Type ".help" for more information.');
|
||||
|
||||
const history = loadHistory();
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
prompt: getPrompt(),
|
||||
terminal: process.stdin.isTTY,
|
||||
historySize: maxHistorySize,
|
||||
completer: process.stdin.isTTY ? completer : undefined,
|
||||
history: history.slice(-maxHistorySize),
|
||||
});
|
||||
|
||||
rl.on("line", (line: string) => {
|
||||
handleLine(line, rl, history);
|
||||
});
|
||||
|
||||
rl.on("close", () => {
|
||||
flushHistory();
|
||||
console.log();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
rl.on("SIGINT", () => {
|
||||
if (inMultilineInput) {
|
||||
// Cancel multiline input
|
||||
lineBuffer = "";
|
||||
inMultilineInput = false;
|
||||
console.log();
|
||||
rl.setPrompt(getPrompt());
|
||||
rl.prompt();
|
||||
} else {
|
||||
console.log("\n(To exit, press Ctrl+D or type .exit)");
|
||||
rl.prompt();
|
||||
}
|
||||
});
|
||||
|
||||
rl.prompt();
|
||||
}
|
||||
|
||||
// Start the REPL
|
||||
startRepl();
|
||||
@@ -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();
|
||||
});
|
||||
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