Compare commits

...

8 Commits

Author SHA1 Message Date
Claude Bot
43f5df2596 fix: use platform-aware path joining for temp file creation
- Use bun.path.joinAbsStringBufZ for cross-platform path construction
- Use std.posix.toPosixPath for null-terminating temp_dir
- Simplify code by using filename directly instead of re-extracting basename
- Use bufPrintZ for null-terminated filename buffer

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

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

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

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

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

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

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

Fixes #26058

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

View File

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

87
src/cli/repl_command.zig Normal file
View 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
View File

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

View File

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