Files
bun.sh/src/js/eval/repl.ts
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

391 lines
9.6 KiB
TypeScript

// 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();