Comparing version 2.0.5 to 2.0.6
{ | ||
"type": "module", | ||
"name": "bun-repl", | ||
"version": "2.0.5", | ||
"version": "2.0.6", | ||
"description": "Experimental REPL for Bun", | ||
@@ -6,0 +6,0 @@ "main": "src/module/repl.ts", |
@@ -23,3 +23,3 @@ #!/usr/bin/env bun | ||
const validFlags = [ | ||
'-h', '--help', '-e', '--eval', '-p', '--print', '--debug', '--sloppy' | ||
'-h', '--help', '-e', '--eval', '-p', '--print', '--debug', '--sloppy', '--no-history', | ||
] as const; | ||
@@ -26,0 +26,0 @@ if (process.argv.length > 2) { |
140
src/repl.ts
import { type JSC } from 'bun-devtools'; | ||
import { join, dirname, basename, resolve as pathresolve } from 'node:path'; | ||
import { readFileSync, existsSync, readSync } from 'node:fs'; | ||
import os from 'node:os'; | ||
@@ -10,3 +11,3 @@ import readline from 'node:readline'; | ||
import utl, { $Proxy } from './utl'; | ||
import bun, { serve, inspect as bunInspect } from 'bun'; | ||
import bun, { serve, write, inspect as bunInspect } from 'bun'; | ||
const { exit, cwd } = process; | ||
@@ -53,7 +54,14 @@ | ||
const { isBuffer } = Buffer; | ||
const SymbolToStringTag = Symbol.toStringTag; | ||
const JSONParse = JSON.parse; | ||
const JSONStringify = JSON.stringify; | ||
const ObjectAssign = Object.assign; | ||
const ObjectDefineProperty = Object.defineProperty; | ||
const ReflectGet = Reflect.get; | ||
const ReflectSet = Reflect.set; | ||
const ReflectDeleteProperty = Reflect.deleteProperty; | ||
const FunctionApply = Function.prototype.call.bind(Function.prototype.apply) as Primordial<Function, 'apply'>; | ||
const BufferToString = Function.prototype.call.bind(Reflect.get(Buffer.prototype, 'toString') as Buffer['toString']) as Primordial<Buffer, 'toString'>; | ||
const StringTrim = Function.prototype.call.bind(String.prototype.trim) as Primordial<String, 'trim'>; | ||
const StringPrototypeSlice = Function.prototype.call.bind(String.prototype.slice) as Primordial<String, 'slice'>; | ||
const StringPrototypeSplit = Function.prototype.call.bind(String.prototype.split) as Primordial<String, 'split'>; | ||
@@ -65,2 +73,4 @@ const StringPrototypeIncludes = Function.prototype.call.bind(String.prototype.includes) as Primordial<String, 'includes'>; | ||
const ArrayPrototypeJoin = Function.prototype.call.bind(Array.prototype.join) as Primordial<Array<any>, 'join'>; | ||
const ArrayPrototypeFind = Function.prototype.call.bind(Array.prototype.find) as Primordial<Array<any>, 'find'>; | ||
const ArrayPrototypeFilter = Function.prototype.call.bind(Array.prototype.filter) as Primordial<Array<any>, 'filter'>; | ||
const MapGet = Function.prototype.call.bind(Map.prototype.get) as Primordial<Map<any, any>, 'get'>; | ||
@@ -80,5 +90,8 @@ const MapSet = Function.prototype.call.bind(Map.prototype.set) as Primordial<Map<any, any>, 'set'>; | ||
}; | ||
// Uncomment below for extreme debugging | ||
//for (const k in console) console[k as keyof typeof console] = GLOBAL.console.trace; | ||
const IS_DEBUG = process.argv.includes('--debug'); | ||
const debuglog = IS_DEBUG ? (...args: string[]) => (console.debug($.dim+'DEBUG:', ...args, $.reset)) : () => void 0; | ||
//const SLOPPY_MODE = process.argv.includes('--sloppy'); | ||
const NO_HISTORY = process.env.BUN_REPL_NO_HISTORY || process.argv.includes('--no-history'); | ||
@@ -256,2 +269,51 @@ type Primordial<T, M extends keyof T> = <S extends T>( | ||
} | ||
// Yes, if someone types a 0xFFFF characters line, they will fill up the buffer. | ||
// But that's very unlikely to happen, and if it does, it will just cutoff the line. | ||
// Anyway this just a temporary workaround for https://github.com/oven-sh/bun/issues/5267 | ||
const promptReuseBuffer = Buffer.allocUnsafe(0xFFFF); | ||
const stdoutWrite = process.stdout.write.bind(process.stdout); | ||
globalThis.prompt = function prompt(question: string = 'Prompt', defaultValue: unknown = null): string | null { | ||
stdoutWrite(question + ' '); | ||
let i = -1; | ||
// eslint-disable-next-line no-constant-condition | ||
while (true) { | ||
// This currently relies on https://github.com/oven-sh/bun/issues/5305 | ||
// If that bug is fixed, this will break one way or another. | ||
const read = readSync(0, promptReuseBuffer, { length: 1, offset: ++i }); | ||
if (read !== 1) { | ||
if (read === 0) { // This means the buffer got filled up (unlikely, but possible) | ||
stdoutWrite('\n'); | ||
return BufferToString(promptReuseBuffer, 'utf8', 0, i) || defaultValue as string | null; | ||
} | ||
else throw new Error('Unexpected read length'); // This will tell us if the bug is fixed. | ||
} | ||
const char = promptReuseBuffer[i]; | ||
if (char === 3) { | ||
stdoutWrite('^C\n'); | ||
exit(0); | ||
} | ||
if (char === 4) { | ||
stdoutWrite('\n'); | ||
return defaultValue as string | null; | ||
} | ||
if (char === 27) { | ||
stdoutWrite('^['); | ||
continue; | ||
} | ||
if (char === 127) { | ||
i--; | ||
if (i < 0) continue; | ||
stdoutWrite('\b \b'); | ||
if (promptReuseBuffer[i] === 27) stdoutWrite('\b \b'); | ||
i--; | ||
continue; | ||
} | ||
stdoutWrite(BufferToString(promptReuseBuffer, 'utf8', i, i + 1)); | ||
if (char === 13 || char === 10) { | ||
stdoutWrite('\n'); | ||
return BufferToString(promptReuseBuffer, 'utf8', 0, i) || defaultValue as string | null; | ||
} | ||
} | ||
}; | ||
Object.defineProperty(GLOBAL, '@@bunReplRuntimeHelpers', { | ||
@@ -266,2 +328,4 @@ value: Object.freeze({ | ||
Object.defineProperty(GLOBAL, 'eval', { value: eval, configurable: false, writable: false }); // used by inlined import.meta trick | ||
Object.defineProperty(GLOBAL, 'Object', { value: Object, configurable: false, writable: false }); // used by swc | ||
Object.freeze(Object); // bun's node:events polyfill relies on this | ||
Object.freeze(Promise); // must preserve .name property | ||
@@ -277,4 +341,4 @@ // eslint-disable-next-line @typescript-eslint/no-floating-promises | ||
const thiz = this; | ||
function* wrappedIter() { yield* original.apply(thiz, argz); } | ||
return Object.defineProperty(wrappedIter(), Symbol.toStringTag, { value: name, configurable: true }); | ||
function* wrappedIter() { yield* FunctionApply(original, thiz, argz) as Generator; } | ||
return ObjectDefineProperty(wrappedIter(), SymbolToStringTag, { value: name, configurable: true }); | ||
}); | ||
@@ -332,3 +396,3 @@ }; | ||
async eval(code: string, topLevelAwaited = false, extraOut?: { errored?: boolean, noPrintError?: boolean }): Promise<string> { | ||
debuglog(`transpiled code: ${code.trimEnd()}`); | ||
debuglog(`transpiled code: ${StringTrim(code)}`); | ||
const { result, wasThrown: thrown } = await this.rawEval(code); | ||
@@ -354,3 +418,3 @@ let remoteObj: EvalRemoteObject = result; | ||
} | ||
if (result.preview.properties?.find(p => p.name === 'status')?.value === 'rejected') { | ||
if (result.preview.properties && ArrayPrototypeFind(result.preview.properties, p => p.name === 'status')?.value === 'rejected') { | ||
remoteObj.wasRejectedPromise = true; | ||
@@ -365,7 +429,7 @@ } | ||
if (!remoteObj.description) throw new EvalError(`Received BigInt value without description: ${JSONStringify(remoteObj)}`); | ||
const value = BigInt(remoteObj.description.slice(0, -1)); | ||
Reflect.set(GLOBAL, remoteObj.wasThrown ? '_error' : '_', value); | ||
const value = BigInt(StringPrototypeSlice(remoteObj.description, 0, -1)); | ||
ReflectSet(GLOBAL, remoteObj.wasThrown ? '_error' : '_', value); | ||
return (remoteObj.wasThrown ? $.red + 'Uncaught ' + $.reset : '') + SafeInspect(value, | ||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access | ||
Reflect.get(GLOBAL, 'repl')?.writer?.options as utl.InspectOptions | ||
ReflectGet(GLOBAL, 'repl')?.writer?.options as utl.InspectOptions | ||
?? { colors: Bun.enableANSIColors, showProxy: true } | ||
@@ -377,4 +441,4 @@ ); | ||
const REPL_INTERNALS = '@@bunReplInternals'; | ||
Object.defineProperty(GLOBAL, REPL_INTERNALS, { | ||
value: { SafeInspect, SafeGet, StringReplace }, | ||
ObjectDefineProperty(GLOBAL, REPL_INTERNALS, { | ||
value: { SafeInspect, SafeGet, StringReplace, bunInspect }, | ||
configurable: true, | ||
@@ -385,3 +449,3 @@ }); | ||
functionDeclaration: `(v) => { | ||
const { SafeInspect, SafeGet, StringReplace } = this['${REPL_INTERNALS}']; | ||
const { SafeInspect, SafeGet, StringReplace, bunInspect } = this['${REPL_INTERNALS}']; | ||
if (!${wasThrown!}) this._ = v; | ||
@@ -394,3 +458,3 @@ else this._error = v; | ||
)}${$.reset}\`; | ||
if (${remoteObj.subtype === 'error'}) return Bun.inspect(v, { colors: ${Bun.enableANSIColors} }); | ||
if (${remoteObj.subtype === 'error'}) return bunInspect(v, { colors: ${Bun.enableANSIColors} }); | ||
return SafeInspect(v, this.repl?.writer?.options ?? { colors: ${Bun.enableANSIColors}, showProxy: true }); | ||
@@ -400,3 +464,3 @@ }`, | ||
}); | ||
Reflect.deleteProperty(GLOBAL, REPL_INTERNALS); | ||
ReflectDeleteProperty(GLOBAL, REPL_INTERNALS); | ||
if (inspected.wasThrown) throw new EvalError(`Failed to inspect object: ${JSONStringify(inspected)}`); | ||
@@ -414,2 +478,3 @@ if (!this.typeof(inspected.result, 'string')) throw new EvalError(`Received non-string inspect result: ${JSONStringify(inspected)}`); | ||
async function loadHistoryData(): Promise<{ path: string, lines: string[] }> { | ||
if (NO_HISTORY) return { path: '', lines: [] }; | ||
let out: { path: string; lines: string[]; } | null; | ||
@@ -428,10 +493,9 @@ if (process.env.XDG_DATA_HOME && (out = await tryLoadHistory(process.env.XDG_DATA_HOME, 'bun'))) return out; | ||
debuglog(`Trying to load REPL history from ${path}`); | ||
let file = Bun.file(path); | ||
if (!await file.exists()) { | ||
if (!existsSync(path)) { | ||
debuglog(`History file not found, creating new one at ${path}`); | ||
await Bun.write(path, '\n'); | ||
file = Bun.file(path); // BUG: Bun.file doesn't update the file's status after writing to it | ||
await write(path, '\n'); | ||
// BUG: Bun.file doesn't update the file's status after writing to it | ||
} | ||
debuglog(`Loading history file from ${path}`); | ||
return { path, lines: (await file.text()).split('\n') }; | ||
return { path, lines: readFileSync(path, 'utf8').split('\n') }; | ||
} catch (err) { | ||
@@ -475,2 +539,8 @@ debuglog(`Failed to load history file from ${path}\nError will be printed below:`); | ||
debuglog('Debug mode enabled.'); | ||
if (IS_DEBUG) { // while debuglog by itself is handy for simple strings its important to note JS will still evaluate the arguments | ||
debuglog( | ||
`REPL version ${await tryGetPackageVersion()} running on Bun v${Bun.version}+${Bun.revision} (${process.platform} ${process.arch})` | ||
); | ||
debuglog(`OS Info: ${os.type()} -- ${os.release()} -- ${os.version()}`); | ||
} | ||
const repl = new REPLServer(); | ||
@@ -524,3 +594,4 @@ await repl.ready; | ||
} | ||
debuglog(`REPL history data loaded: (${history.lines.length} lines) ${history.path}`); | ||
if (NO_HISTORY) debuglog('Skipped history file loading due to --no-history flag.'); | ||
else debuglog(`REPL history data loaded: (${history.lines.length} lines) ${history.path}`); | ||
const rl = readline.createInterface({ | ||
@@ -542,2 +613,5 @@ input: process.stdin, | ||
debuglog('readline interface created.'); | ||
process.on('exit', () => { | ||
rl.close(); | ||
}); | ||
console.log(`Welcome to Bun v${Bun.version}\nType ".help" for more information.`); | ||
@@ -548,5 +622,15 @@ console.warn( | ||
//* Only primordials should be used beyond this point! | ||
rl.on('SIGINT', () => { | ||
if (rl.line.length === 0) rl.close(); | ||
else { | ||
console.log(''); | ||
ReflectSet(rl, 'line', ''); | ||
rl.prompt(); | ||
} | ||
}); | ||
rl.on('close', () => { | ||
Bun.write(history.path, history.lines.filter(l => l !== '.exit').join('\n')) | ||
.catch(() => console.warn(`[!] Failed to save REPL history to ${history.path}!`)); | ||
if (!NO_HISTORY) write( | ||
history.path, | ||
ArrayPrototypeJoin(ArrayPrototypeFilter(history.lines, l => l !== '.exit'), '\n') | ||
).catch(() => console.warn(`[!] Failed to save REPL history to ${history.path}!`)); | ||
console.log(''); // ensure newline | ||
@@ -601,3 +685,15 @@ exit(0); | ||
const err = e as Error; | ||
console.error((err?.message ?? 'Unknown transpiler error.').split('\nCaused by:\n')[0].trim()); | ||
if (err.stack?.includes('@swc/core')) { | ||
console.error( | ||
'Internal failure due to global builtins tampering, this is not a bug but a temporary limitation of the REPL.\n' + | ||
'Please do not report this and avoid tampering with the global builtins in the REPL.' | ||
); | ||
exit(0); | ||
} | ||
console.error( | ||
StringTrim(StringPrototypeSplit( | ||
err?.message ?? 'Unknown transpiler error.', '\nCaused by:\n' as unknown as RegExp | ||
)[0]) | ||
); | ||
if (IS_DEBUG) console.error(e); | ||
rl.prompt(); | ||
@@ -604,0 +700,0 @@ return; |
Sorry, the diff of this file is not supported yet
275218
4132