diff options
Diffstat (limited to 'src/js/builtins')
-rw-r--r-- | src/js/builtins/BunBuiltinNames.h | 19 | ||||
-rw-r--r-- | src/js/builtins/BundlerPlugin.ts | 3 | ||||
-rw-r--r-- | src/js/builtins/ConsoleObject.ts | 840 | ||||
-rw-r--r-- | src/js/builtins/ImportMetaObject.ts | 20 | ||||
-rw-r--r-- | src/js/builtins/JSBufferConstructor.ts | 3 | ||||
-rw-r--r-- | src/js/builtins/Module.ts | 19 | ||||
-rw-r--r-- | src/js/builtins/ProcessObjectInternals.ts | 43 | ||||
-rw-r--r-- | src/js/builtins/ReadableStream.ts | 9 | ||||
-rw-r--r-- | src/js/builtins/ReadableStreamInternals.ts | 13 | ||||
-rw-r--r-- | src/js/builtins/tsconfig.json | 4 |
10 files changed, 884 insertions, 89 deletions
diff --git a/src/js/builtins/BunBuiltinNames.h b/src/js/builtins/BunBuiltinNames.h index 1c34f2726..f3266d5c2 100644 --- a/src/js/builtins/BunBuiltinNames.h +++ b/src/js/builtins/BunBuiltinNames.h @@ -69,8 +69,10 @@ using namespace JSC; macro(createCommonJSModule) \ macro(createEmptyReadableStream) \ macro(createFIFO) \ + macro(createInternalModuleById) \ macro(createNativeReadableStream) \ macro(createReadableStream) \ + macro(createUsedReadableStream) \ macro(createUninitializedArrayBuffer) \ macro(createWritableStreamFromInternal) \ macro(cwd) \ @@ -119,6 +121,7 @@ using namespace JSC; macro(inFlightCloseRequest) \ macro(inFlightWriteRequest) \ macro(initializeWith) \ + macro(internalModuleRegistry) \ macro(internalRequire) \ macro(internalStream) \ macro(internalWritable) \ @@ -130,7 +133,6 @@ using namespace JSC; macro(join) \ macro(kind) \ macro(lazy) \ - macro(lazyLoad) \ macro(lazyStreamPrototypeMap) \ macro(loadCJS2ESM) \ macro(localStreams) \ @@ -138,7 +140,6 @@ using namespace JSC; macro(makeDOMException) \ macro(makeGetterTypeError) \ macro(makeThisTypeError) \ - macro(map) \ macro(method) \ macro(nextTick) \ macro(normalize) \ @@ -146,6 +147,7 @@ using namespace JSC; macro(once) \ macro(options) \ macro(origin) \ + macro(overridableRequire) \ macro(ownerReadableStream) \ macro(parse) \ macro(password) \ @@ -186,11 +188,10 @@ using namespace JSC; macro(require) \ macro(requireESM) \ macro(requireMap) \ + macro(requireNativeModule) \ macro(resolve) \ macro(resolveSync) \ macro(resume) \ - macro(search) \ - macro(searchParams) \ macro(self) \ macro(sep) \ macro(setBody) \ @@ -206,17 +207,12 @@ using namespace JSC; macro(startedPromise) \ macro(state) \ macro(status) \ + macro(statusText) \ macro(storedError) \ macro(strategy) \ macro(strategyHWM) \ macro(strategySizeAlgorithm) \ macro(stream) \ - macro(streamClosed) \ - macro(streamClosing) \ - macro(streamErrored) \ - macro(streamReadable) \ - macro(streamWaiting) \ - macro(streamWritable) \ macro(structuredCloneForStream) \ macro(syscall) \ macro(textDecoderStreamDecoder) \ @@ -245,9 +241,6 @@ using namespace JSC; macro(writer) \ macro(writing) \ macro(written) \ - macro(createInternalModuleById) \ - macro(internalModuleRegistry) \ - macro(requireNativeModule) \ class BunBuiltinNames { public: diff --git a/src/js/builtins/BundlerPlugin.ts b/src/js/builtins/BundlerPlugin.ts index 7be030ee8..d2c88b667 100644 --- a/src/js/builtins/BundlerPlugin.ts +++ b/src/js/builtins/BundlerPlugin.ts @@ -162,6 +162,9 @@ export function runSetupFunction(this: BundlerPlugin, setup: Setup, config: Buil onResolve, onStart: notImplementedIssueFn(2771, "On-start callbacks"), resolve: notImplementedIssueFn(2771, "build.resolve()"), + module: () => { + throw new TypeError("module() is not supported in Bun.build() yet. Only via Bun.plugin() at runtime"); + }, // esbuild's options argument is different, we provide some interop initialOptions: { ...config, diff --git a/src/js/builtins/ConsoleObject.ts b/src/js/builtins/ConsoleObject.ts index 45746459a..b48593154 100644 --- a/src/js/builtins/ConsoleObject.ts +++ b/src/js/builtins/ConsoleObject.ts @@ -1,18 +1,55 @@ $overriddenName = "[Symbol.asyncIterator]"; export function asyncIterator(this: Console) { - const Iterator = async function* ConsoleAsyncIterator() { - const stream = Bun.stdin.stream(); - var reader = stream.getReader(); + var stream = Bun.stdin.stream(); - // TODO: use builtin - var decoder = new (globalThis as any).TextDecoder("utf-8", { fatal: false }) as TextDecoder; - var deferredError; - var indexOf = Bun.indexOfLine; + var decoder = new TextDecoder("utf-8", { fatal: false }); + var indexOf = Bun.indexOfLine; + var actualChunk: Uint8Array; + var i: number = -1; + var idx: number; + var last: number; + var done: boolean; + var value: Uint8Array[]; + var value_len: number; + var pendingChunk: Uint8Array | undefined; + async function* ConsoleAsyncIterator() { + var reader = stream.getReader(); + var deferredError; try { + if (i !== -1) { + last = i + 1; + i = indexOf(actualChunk, last); + + while (i !== -1) { + yield decoder.decode(actualChunk.subarray(last, i)); + last = i + 1; + i = indexOf(actualChunk, last); + } + + for (idx++; idx < value_len; idx++) { + actualChunk = value[idx]; + if (pendingChunk) { + actualChunk = Buffer.concat([pendingChunk, actualChunk]); + pendingChunk = undefined; + } + + last = 0; + // TODO: "\r", 0x4048, 0x4049, 0x404A, 0x404B, 0x404C, 0x404D, 0x404E, 0x404F + i = indexOf(actualChunk, last); + while (i !== -1) { + yield decoder.decode(actualChunk.subarray(last, i)); + last = i + 1; + i = indexOf(actualChunk, last); + } + i = -1; + + pendingChunk = actualChunk.subarray(last); + } + actualChunk = undefined!; + } + while (true) { - var done, value; - var pendingChunk; const firstResult = reader.readMany(); if ($isPromise(firstResult)) { ({ done, value } = await firstResult); @@ -27,26 +64,29 @@ export function asyncIterator(this: Console) { return; } - var actualChunk; // we assume it was given line-by-line - for (const chunk of value) { - actualChunk = chunk; + for (idx = 0, value_len = value.length; idx < value_len; idx++) { + actualChunk = value[idx]; if (pendingChunk) { - actualChunk = Buffer.concat([pendingChunk, chunk]); - pendingChunk = null; + actualChunk = Buffer.concat([pendingChunk, actualChunk]); + pendingChunk = undefined; } - var last = 0; + last = 0; // TODO: "\r", 0x4048, 0x4049, 0x404A, 0x404B, 0x404C, 0x404D, 0x404E, 0x404F - var i = indexOf(actualChunk, last); + i = indexOf(actualChunk, last); while (i !== -1) { + // This yield may end the function, in that case we need to be able to recover state + // if the iterator was fired up again. yield decoder.decode(actualChunk.subarray(last, i)); last = i + 1; i = indexOf(actualChunk, last); } + i = -1; pendingChunk = actualChunk.subarray(last); } + actualChunk = undefined!; } } catch (e) { deferredError = e; @@ -57,11 +97,11 @@ export function asyncIterator(this: Console) { throw deferredError; } } - }; + } const symbol = globalThis.Symbol.asyncIterator; - this[symbol] = Iterator; - return Iterator(); + this[symbol] = ConsoleAsyncIterator; + return ConsoleAsyncIterator(); } export function write(this: Console, input) { @@ -76,9 +116,769 @@ export function write(this: Console, input) { const count = $argumentCount(); for (var i = 1; i < count; i++) { - wrote += writer.write($argument(i)); + wrote += writer.write(arguments[i]); } writer.flush(true); return wrote; } + +// This is the `console.Console` constructor. It is mostly copied from Node. +// https://github.com/nodejs/node/blob/d2c7c367741bdcb6f7f77f55ce95a745f0b29fef/lib/internal/console/constructor.js +// Some parts are copied from imported files and inlined here. Not too much of a performance issue +// to do extra work at startup, since most people do not need `console.Console`. +// TODO: probably could extract `getStringWidth`; probably make that a native function. note how it is copied from `readline.js` +export function createConsoleConstructor(console: typeof globalThis.console) { + const { inspect, formatWithOptions } = require("node:util"); + const { isBuffer } = require("node:buffer"); + + const StringPrototypeIncludes = String.prototype.includes; + const RegExpPrototypeSymbolReplace = RegExp.prototype[Symbol.replace]; + const ArrayPrototypeUnshift = Array.prototype.unshift; + const StringPrototypeRepeat = String.prototype.repeat; + const StringPrototypeSlice = String.prototype.slice; + const ObjectPrototypeHasOwnProperty = Object.prototype.hasOwnProperty; + const StringPrototypePadStart = String.prototype.padStart; + const StringPrototypeSplit = String.prototype.split; + const NumberPrototypeToFixed = Number.prototype.toFixed; + const StringPrototypeNormalize = String.prototype.normalize; + const StringPrototypeCodePointAt = String.prototype.codePointAt; + const ArrayPrototypeMap = Array.prototype.map; + const ArrayPrototypeJoin = Array.prototype.join; + const ArrayPrototypePush = Array.prototype.push; + + const kCounts = Symbol("counts"); + + const kSecond = 1000; + const kMinute = 60 * kSecond; + const kHour = 60 * kMinute; + + // Regex used for ansi escape code splitting + // Adopted from https://github.com/chalk/ansi-regex/blob/HEAD/index.js + // License: MIT, authors: @sindresorhus, Qix-, arjunmehta and LitoMore + // Matches all ansi escape code sequences in a string + var ansiPattern = + "[\\u001B\\u009B][[\\]()#;?]*" + + "(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*" + + "|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)" + + "|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"; + var ansi = new RegExp(ansiPattern, "g"); + + /** + * Returns true if the character represented by a given + * Unicode code point is full-width. Otherwise returns false. + */ + var isFullWidthCodePoint = code => { + // Code points are partially derived from: + // https://www.unicode.org/Public/UNIDATA/EastAsianWidth.txt + return ( + code >= 0x1100 && + (code <= 0x115f || // Hangul Jamo + code === 0x2329 || // LEFT-POINTING ANGLE BRACKET + code === 0x232a || // RIGHT-POINTING ANGLE BRACKET + // CJK Radicals Supplement .. Enclosed CJK Letters and Months + (code >= 0x2e80 && code <= 0x3247 && code !== 0x303f) || + // Enclosed CJK Letters and Months .. CJK Unified Ideographs Extension A + (code >= 0x3250 && code <= 0x4dbf) || + // CJK Unified Ideographs .. Yi Radicals + (code >= 0x4e00 && code <= 0xa4c6) || + // Hangul Jamo Extended-A + (code >= 0xa960 && code <= 0xa97c) || + // Hangul Syllables + (code >= 0xac00 && code <= 0xd7a3) || + // CJK Compatibility Ideographs + (code >= 0xf900 && code <= 0xfaff) || + // Vertical Forms + (code >= 0xfe10 && code <= 0xfe19) || + // CJK Compatibility Forms .. Small Form Variants + (code >= 0xfe30 && code <= 0xfe6b) || + // Halfwidth and Fullwidth Forms + (code >= 0xff01 && code <= 0xff60) || + (code >= 0xffe0 && code <= 0xffe6) || + // Kana Supplement + (code >= 0x1b000 && code <= 0x1b001) || + // Enclosed Ideographic Supplement + (code >= 0x1f200 && code <= 0x1f251) || + // Miscellaneous Symbols and Pictographs 0x1f300 - 0x1f5ff + // Emoticons 0x1f600 - 0x1f64f + (code >= 0x1f300 && code <= 0x1f64f) || + // CJK Unified Ideographs Extension B .. Tertiary Ideographic Plane + (code >= 0x20000 && code <= 0x3fffd)) + ); + }; + + var isZeroWidthCodePoint = code => { + return ( + code <= 0x1f || // C0 control codes + (code >= 0x7f && code <= 0x9f) || // C1 control codes + (code >= 0x300 && code <= 0x36f) || // Combining Diacritical Marks + (code >= 0x200b && code <= 0x200f) || // Modifying Invisible Characters + // Combining Diacritical Marks for Symbols + (code >= 0x20d0 && code <= 0x20ff) || + (code >= 0xfe00 && code <= 0xfe0f) || // Variation Selectors + (code >= 0xfe20 && code <= 0xfe2f) || // Combining Half Marks + (code >= 0xe0100 && code <= 0xe01ef) + ); // Variation Selectors + }; + + function stripVTControlCharacters(str) { + return (RegExpPrototypeSymbolReplace as any).$call(ansi, str, ""); + } + + /** + * Returns the number of columns required to display the given string. + */ + var getStringWidth = function getStringWidth(str, removeControlChars = true) { + var width = 0; + + if (removeControlChars) str = stripVTControlCharacters(str); + str = StringPrototypeNormalize.$call(str, "NFC"); + for (var char of str) { + var code = StringPrototypeCodePointAt.$call(char, 0); + if (isFullWidthCodePoint(code)) { + width += 2; + } else if (!isZeroWidthCodePoint(code)) { + width++; + } + } + + return width; + }; + + const tableChars = { + middleMiddle: "─", + rowMiddle: "┼", + topRight: "┐", + topLeft: "┌", + leftMiddle: "├", + topMiddle: "┬", + bottomRight: "┘", + bottomLeft: "└", + bottomMiddle: "┴", + rightMiddle: "┤", + left: "│ ", + right: " │", + middle: " │ ", + }; + + const renderRow = (row, columnWidths) => { + let out = tableChars.left; + for (let i = 0; i < row.length; i++) { + const cell = row[i]; + const len = getStringWidth(cell); + const needed = (columnWidths[i] - len) / 2; + // round(needed) + ceil(needed) will always add up to the amount + // of spaces we need while also left justifying the output. + out += + (StringPrototypeRepeat as any).$call(" ", needed) + cell + StringPrototypeRepeat.$call(" ", Math.ceil(needed)); + if (i !== row.length - 1) out += tableChars.middle; + } + out += tableChars.right; + return out; + }; + + const table = (head, columns) => { + const columnWidths = ArrayPrototypeMap.$call(head, h => getStringWidth(h)) as number[]; + const longestColumn = Math.max(...(ArrayPrototypeMap as any).$call(columns, a => a.length)); + const rows: any = $newArrayWithSize(longestColumn); + + for (let i = 0; i < head.length; i++) { + const column = columns[i]; + for (let j = 0; j < longestColumn; j++) { + if (rows[j] === undefined) rows[j] = []; + const value = (rows[j][i] = ObjectPrototypeHasOwnProperty.$call(column, j) ? column[j] : ""); + const width = columnWidths[i] || 0; + const counted = getStringWidth(value); + columnWidths[i] = Math.max(width, counted); + } + } + + const divider = ArrayPrototypeMap.$call(columnWidths, i => + StringPrototypeRepeat.$call(tableChars.middleMiddle, i + 2), + ); + + let result = + tableChars.topLeft + + ArrayPrototypeJoin.$call(divider, tableChars.topMiddle) + + tableChars.topRight + + "\n" + + renderRow(head, columnWidths) + + "\n" + + tableChars.leftMiddle + + ArrayPrototypeJoin.$call(divider, tableChars.rowMiddle) + + tableChars.rightMiddle + + "\n"; + + for (const row of rows) result += `${renderRow(row, columnWidths)}\n`; + + result += + tableChars.bottomLeft + ArrayPrototypeJoin.$call(divider, tableChars.bottomMiddle) + tableChars.bottomRight; + + return result; + }; + + // Track amount of indentation required via `console.group()`. + const kGroupIndent = Symbol("kGroupIndent"); + const kGroupIndentationWidth = Symbol("kGroupIndentWidth"); + const kFormatForStderr = Symbol("kFormatForStderr"); + const kFormatForStdout = Symbol("kFormatForStdout"); + const kGetInspectOptions = Symbol("kGetInspectOptions"); + const kColorMode = Symbol("kColorMode"); + const kIsConsole = Symbol("kIsConsole"); + const kWriteToConsole = Symbol("kWriteToConsole"); + const kBindProperties = Symbol("kBindProperties"); + const kBindStreamsEager = Symbol("kBindStreamsEager"); + const kBindStreamsLazy = Symbol("kBindStreamsLazy"); + const kUseStdout = Symbol("kUseStdout"); + const kUseStderr = Symbol("kUseStderr"); + + const optionsMap = new WeakMap<any, any>(); + function Console(this: any, options /* or: stdout, stderr, ignoreErrors = true */) { + // We have to test new.target here to see if this function is called + // with new, because we need to define a custom instanceof to accommodate + // the global console. + if (new.target === undefined) { + return Reflect.construct(Console, arguments); + } + + if (!options || typeof options.write === "function") { + options = { + stdout: options, + stderr: arguments[1], + ignoreErrors: arguments[2], + }; + } + + const { + stdout, + stderr = stdout, + ignoreErrors = true, + colorMode = "auto", + inspectOptions, + groupIndentation, + } = options; + + if (!stdout || typeof stdout.write !== "function") { + // throw new ERR_CONSOLE_WRITABLE_STREAM("stdout"); + throw new TypeError("stdout is not a writable stream"); + } + if (!stderr || typeof stderr.write !== "function") { + // throw new ERR_CONSOLE_WRITABLE_STREAM("stderr"); + throw new TypeError("stderr is not a writable stream"); + } + + if (typeof colorMode !== "boolean" && colorMode !== "auto") { + // throw new ERR_INVALID_ARG_VALUE("colorMode", colorMode); + throw new TypeError("colorMode must be a boolean or 'auto'"); + } + + if (groupIndentation !== undefined) { + // validateInteger(groupIndentation, "groupIndentation", 0, kMaxGroupIndentation); + } + + if (inspectOptions !== undefined) { + // validateObject(inspectOptions, "options.inspectOptions"); + + if (inspectOptions.colors !== undefined && options.colorMode !== undefined) { + // throw new ERR_INCOMPATIBLE_OPTION_PAIR("options.inspectOptions.color", "colorMode"); + } + optionsMap.set(this, inspectOptions); + } + + // Bind the prototype functions to this Console instance + Object.keys(Console.prototype).forEach(key => { + // We have to bind the methods grabbed from the instance instead of from + // the prototype so that users extending the Console can override them + // from the prototype chain of the subclass. + this[key] = this[key].bind(this); + Object.defineProperty(this[key], "name", { + value: key, + }); + }); + + this[kBindStreamsEager](stdout, stderr); + this[kBindProperties](ignoreErrors, colorMode, groupIndentation); + } + + const consolePropAttributes = { + writable: true, + enumerable: false, + configurable: true, + }; + + // Fixup global.console instanceof global.console.Console + Object.defineProperty(Console, Symbol.hasInstance, { + value(instance) { + return instance[kIsConsole] || instance === console; + }, + }); + + const kColorInspectOptions = { colors: true }; + const kNoColorInspectOptions = {}; + + Object.defineProperties((Console.prototype = {}), { + [kBindStreamsEager]: { + ...consolePropAttributes, + // Eager version for the Console constructor + value: function (stdout, stderr) { + Object.defineProperties(this, { + "_stdout": { ...consolePropAttributes, value: stdout }, + "_stderr": { ...consolePropAttributes, value: stderr }, + }); + }, + }, + [kBindStreamsLazy]: { + ...consolePropAttributes, + // Lazily load the stdout and stderr from an object so we don't + // create the stdio streams when they are not even accessed + value: function (object) { + let stdout; + let stderr; + Object.defineProperties(this, { + "_stdout": { + enumerable: false, + configurable: true, + get() { + if (!stdout) stdout = object.stdout; + return stdout; + }, + set(value) { + stdout = value; + }, + }, + "_stderr": { + enumerable: false, + configurable: true, + get() { + if (!stderr) { + stderr = object.stderr; + } + return stderr; + }, + set(value) { + stderr = value; + }, + }, + }); + }, + }, + [kBindProperties]: { + ...consolePropAttributes, + value: function (ignoreErrors, colorMode, groupIndentation = 2) { + Object.defineProperties(this, { + "_stdoutErrorHandler": { + ...consolePropAttributes, + value: createWriteErrorHandler(this, kUseStdout), + }, + "_stderrErrorHandler": { + ...consolePropAttributes, + value: createWriteErrorHandler(this, kUseStderr), + }, + "_ignoreErrors": { + ...consolePropAttributes, + value: Boolean(ignoreErrors), + }, + "_times": { ...consolePropAttributes, value: new Map() }, + // Corresponds to https://console.spec.whatwg.org/#count-map + [kCounts]: { ...consolePropAttributes, value: new Map() }, + [kColorMode]: { ...consolePropAttributes, value: colorMode }, + [kIsConsole]: { ...consolePropAttributes, value: true }, + [kGroupIndent]: { ...consolePropAttributes, value: "" }, + [kGroupIndentationWidth]: { + ...consolePropAttributes, + value: groupIndentation, + }, + [Symbol.toStringTag]: { + writable: false, + enumerable: false, + configurable: true, + value: "console", + }, + }); + }, + }, + [kWriteToConsole]: { + ...consolePropAttributes, + value: function (streamSymbol, string) { + const ignoreErrors = this._ignoreErrors; + const groupIndent = this[kGroupIndent]; + + const useStdout = streamSymbol === kUseStdout; + const stream = useStdout ? this._stdout : this._stderr; + const errorHandler = useStdout ? this._stdoutErrorHandler : this._stderrErrorHandler; + + if (groupIndent.length !== 0) { + if (StringPrototypeIncludes.$call(string, "\n")) { + // ?! + string = (RegExpPrototypeSymbolReplace.$call as any)(/\n/g, string, `\n${groupIndent}`); + } + string = groupIndent + string; + } + string += "\n"; + + if (ignoreErrors === false) return stream.write(string); + + // There may be an error occurring synchronously (e.g. for files or TTYs + // on POSIX systems) or asynchronously (e.g. pipes on POSIX systems), so + // handle both situations. + try { + // Add and later remove a noop error handler to catch synchronous + // errors. + if (stream.listenerCount("error") === 0) stream.once("error", noop); + + stream.write(string, errorHandler); + } catch (e) { + // Console is a debugging utility, so it swallowing errors is not + // desirable even in edge cases such as low stack space. + // if (isStackOverflowError(e)) throw e; + // Sorry, there's no proper way to pass along the error here. + } finally { + stream.removeListener("error", noop); + } + }, + }, + [kGetInspectOptions]: { + ...consolePropAttributes, + value: function (stream) { + let color = this[kColorMode]; + if (color === "auto") { + if (process.env.FORCE_COLOR !== undefined) { + color = Bun.enableANSIColors; + } else { + color = stream.isTTY && (typeof stream.getColorDepth === "function" ? stream.getColorDepth() > 2 : true); + } + } + + const options = optionsMap.get(this); + if (options) { + if (options.colors === undefined) { + options.colors = color; + } + return options; + } + + return color ? kColorInspectOptions : kNoColorInspectOptions; + }, + }, + [kFormatForStdout]: { + ...consolePropAttributes, + value: function (args) { + const opts = this[kGetInspectOptions](this._stdout); + return formatWithOptions(opts, ...args); + }, + }, + [kFormatForStderr]: { + ...consolePropAttributes, + value: function (args) { + const opts = this[kGetInspectOptions](this._stderr); + return formatWithOptions(opts, ...args); + }, + }, + }); + + // Make a function that can serve as the callback passed to `stream.write()`. + function createWriteErrorHandler(instance, streamSymbol) { + return err => { + // This conditional evaluates to true if and only if there was an error + // that was not already emitted (which happens when the _write callback + // is invoked asynchronously). + const stream = streamSymbol === kUseStdout ? instance._stdout : instance._stderr; + if (err !== null && !stream._writableState.errorEmitted) { + // If there was an error, it will be emitted on `stream` as + // an `error` event. Adding a `once` listener will keep that error + // from becoming an uncaught exception, but since the handler is + // removed after the event, non-console.* writes won't be affected. + // we are only adding noop if there is no one else listening for 'error' + if (stream.listenerCount("error") === 0) { + stream.once("error", noop); + } + } + }; + } + + const consoleMethods: any = { + log(...args) { + this[kWriteToConsole](kUseStdout, this[kFormatForStdout](args)); + }, + + warn(...args) { + this[kWriteToConsole](kUseStderr, this[kFormatForStderr](args)); + }, + + dir(object, options) { + this[kWriteToConsole]( + kUseStdout, + inspect(object, { + customInspect: false, + ...this[kGetInspectOptions](this._stdout), + ...options, + }), + ); + }, + + time(label = "default") { + // Coerces everything other than Symbol to a string + label = `${label}`; + if (this._times.has(label)) { + process.emitWarning(`Label '${label}' already exists for console.time()`); + return; + } + // trace(kTraceBegin, kTraceConsoleCategory, `time::${label}`, 0); + this._times.set(label, process.hrtime()); + }, + + timeEnd(label = "default") { + // Coerces everything other than Symbol to a string + label = `${label}`; + const found = timeLogImpl(this, "timeEnd", label); + // trace(kTraceEnd, kTraceConsoleCategory, `time::${label}`, 0); + if (found) { + this._times.delete(label); + } + }, + + timeLog(label = "default", ...data) { + // Coerces everything other than Symbol to a string + label = `${label}`; + timeLogImpl(this, "timeLog", label, data); + // trace(kTraceInstant, kTraceConsoleCategory, `time::${label}`, 0); + }, + + trace: function trace(...args) { + const err: Error = { + name: "Trace", + message: this[kFormatForStderr](args), + }; + Error.captureStackTrace(err, trace); + this.error(err.stack); + }, + + assert(expression, ...args) { + if (!expression) { + args[0] = `Assertion failed${args.length === 0 ? "" : `: ${args[0]}`}`; + // The arguments will be formatted in warn() again + this.warn.$apply(this, args); + } + }, + + // Defined by: https://console.spec.whatwg.org/#clear + clear() { + // It only makes sense to clear if _stdout is a TTY. + // Otherwise, do nothing. + if (this._stdout.isTTY && process.env.TERM !== "dumb") { + this._stdout.write("\x1b[2J"); + this._stdout.write("\x1b[0f"); + } + }, + + // Defined by: https://console.spec.whatwg.org/#count + count(label = "default") { + // Ensures that label is a string, and only things that can be + // coerced to strings. e.g. Symbol is not allowed + label = `${label}`; + const counts = this[kCounts]; + let count = counts.get(label); + if (count === undefined) count = 1; + else count++; + counts.set(label, count); + // trace(kTraceCount, kTraceConsoleCategory, `count::${label}`, 0, count); + this.log(`${label}: ${count}`); + }, + + // Defined by: https://console.spec.whatwg.org/#countreset + countReset(label = "default") { + const counts = this[kCounts]; + if (!counts.has(label)) { + process.emitWarning(`Count for '${label}' does not exist`); + return; + } + // trace(kTraceCount, kTraceConsoleCategory, `count::${label}`, 0, 0); + counts.delete(`${label}`); + }, + + group(...data) { + if (data.length > 0) { + this.log.$apply(this, data); + } + this[kGroupIndent] += StringPrototypeRepeat.$call(" ", this[kGroupIndentationWidth]); + }, + + groupEnd() { + this[kGroupIndent] = StringPrototypeSlice.$call( + this[kGroupIndent], + 0, + this[kGroupIndent].length - this[kGroupIndentationWidth], + ); + }, + + // https://console.spec.whatwg.org/#table + table(tabularData, properties) { + if (properties !== undefined) { + // validateArray(properties, "properties"); + } + + if (tabularData === null || typeof tabularData !== "object") return this.log(tabularData); + const final = (k, v) => this.log(table(k, v)); + + const _inspect = v => { + const depth = v !== null && typeof v === "object" && !isArray(v) && Object.keys(v).length > 2 ? -1 : 0; + const opt = { + depth, + maxArrayLength: 3, + breakLength: Infinity, + ...this[kGetInspectOptions](this._stdout), + }; + return inspect(v, opt); + }; + const getIndexArray = length => Array.from({ length }, (_, i) => _inspect(i)); + + const mapIter = $isMapIterator(tabularData); + let isKeyValue = false; + let i = 0; + // if (mapIter) { + // const res = previewEntries(tabularData, true); + // tabularData = res[0]; + // isKeyValue = res[1]; + // } + + if (isKeyValue || $isMap(tabularData)) { + const keys = []; + const values = []; + let length = 0; + if (mapIter) { + for (; i < tabularData.length / 2; ++i) { + ArrayPrototypePush.$call(keys, _inspect(tabularData[i * 2])); + ArrayPrototypePush.$call(values, _inspect(tabularData[i * 2 + 1])); + length++; + } + } else { + for (const { 0: k, 1: v } of tabularData) { + ArrayPrototypePush.$call(keys, _inspect(k)); + ArrayPrototypePush.$call(values, _inspect(v)); + length++; + } + } + return final([iterKey, keyKey, valuesKey], [getIndexArray(length), keys, values]); + } + + const setIter = $isSetIterator(tabularData); + // if (setIter) tabularData = previewEntries(tabularData); + + const setlike = setIter || mapIter || $isSet(tabularData); + if (setlike) { + const values = []; + let length = 0; + for (const v of tabularData as Set<any>) { + ArrayPrototypePush.$call(values, _inspect(v)); + length++; + } + return final([iterKey, valuesKey], [getIndexArray(length), values]); + } + + const map = { __proto__: null }; + let hasPrimitives = false; + const valuesKeyArray: any = []; + const indexKeyArray = Object.keys(tabularData); + + for (; i < indexKeyArray.length; i++) { + const item = tabularData[indexKeyArray[i]]; + const primitive = item === null || (typeof item !== "function" && typeof item !== "object"); + if (properties === undefined && primitive) { + hasPrimitives = true; + valuesKeyArray[i] = _inspect(item); + } else { + const keys = properties || Object.keys(item); + for (const key of keys) { + map[key] ??= []; + if ((primitive && properties) || !ObjectPrototypeHasOwnProperty.$call(item, key)) map[key][i] = ""; + else map[key][i] = _inspect(item[key]); + } + } + } + + const keys = Object.keys(map); + const values = Object.values(map); + if (hasPrimitives) { + ArrayPrototypePush.$call(keys, valuesKey); + ArrayPrototypePush.$call(values, valuesKeyArray); + } + ArrayPrototypeUnshift.$call(keys, indexKey); + ArrayPrototypeUnshift.$call(values, indexKeyArray); + + return final(keys, values); + }, + }; + + // Returns true if label was found + function timeLogImpl(self, name, label, data?) { + const time = self._times.get(label); + if (time === undefined) { + process.emitWarning(`No such label '${label}' for console.${name}()`); + return false; + } + const duration = process.hrtime(time); + const ms = duration[0] * 1000 + duration[1] / 1e6; + + const formatted = formatTime(ms); + + if (data === undefined) { + self.log("%s: %s", label, formatted); + } else { + self.log("%s: %s", label, formatted, ...data); + } + return true; + } + + function pad(value) { + return StringPrototypePadStart.$call(`${value}`, 2, "0"); + } + + function formatTime(ms) { + let hours = 0; + let minutes = 0; + let seconds: string | number = 0; + + if (ms >= kSecond) { + if (ms >= kMinute) { + if (ms >= kHour) { + hours = Math.floor(ms / kHour); + ms = ms % kHour; + } + minutes = Math.floor(ms / kMinute); + ms = ms % kMinute; + } + seconds = ms / kSecond; + } + + if (hours !== 0 || minutes !== 0) { + ({ 0: seconds, 1: ms } = (StringPrototypeSplit.$call as any)(NumberPrototypeToFixed.$call(seconds, 3), ".")); + const res = hours !== 0 ? `${hours}:${pad(minutes)}` : minutes; + return `${res}:${pad(seconds)}.${ms} (${hours !== 0 ? "h:m" : ""}m:ss.mmm)`; + } + + if (seconds !== 0) { + return `${NumberPrototypeToFixed.$call(seconds, 3)}s`; + } + + return `${Number(NumberPrototypeToFixed.$call(ms, 3))}ms`; + } + + const keyKey = "Key"; + const valuesKey = "Values"; + const indexKey = "(index)"; + const iterKey = "(iteration index)"; + + const isArray = v => $isJSArray(v) || $isTypedArrayView(v) || isBuffer(v); + + function noop() {} + + for (const method of Reflect.ownKeys(consoleMethods)) Console.prototype[method] = consoleMethods[method]; + + Console.prototype.debug = Console.prototype.log; + Console.prototype.info = Console.prototype.log; + Console.prototype.dirxml = Console.prototype.log; + Console.prototype.error = Console.prototype.warn; + Console.prototype.groupCollapsed = Console.prototype.group; + + return Console; +} diff --git a/src/js/builtins/ImportMetaObject.ts b/src/js/builtins/ImportMetaObject.ts index 9409bb0f1..bf498e9f9 100644 --- a/src/js/builtins/ImportMetaObject.ts +++ b/src/js/builtins/ImportMetaObject.ts @@ -118,17 +118,17 @@ export function internalRequire(this: ImportMetaObject, id) { if (last5 === ".json") { var fs = (globalThis[Symbol.for("_fs")] ||= Bun.fs()); var exports = JSON.parse(fs.readFileSync(id, "utf8")); - $requireMap.$set(id, $createCommonJSModule(id, exports, true)); + $requireMap.$set(id, $createCommonJSModule(id, exports, true, undefined)); return exports; } else if (last5 === ".node") { - const module = $createCommonJSModule(id, {}, true); + const module = $createCommonJSModule(id, {}, true, undefined); process.dlopen(module, id); $requireMap.$set(id, module); return module.exports; } else if (last5 === ".toml") { var fs = (globalThis[Symbol.for("_fs")] ||= Bun.fs()); var exports = Bun.TOML.parse(fs.readFileSync(id, "utf8")); - $requireMap.$set(id, $createCommonJSModule(id, exports, true)); + $requireMap.$set(id, $createCommonJSModule(id, exports, true, undefined)); return exports; } else { var exports = $requireESM(id); @@ -136,7 +136,7 @@ export function internalRequire(this: ImportMetaObject, id) { if (cachedModule) { return cachedModule.exports; } - $requireMap.$set(id, $createCommonJSModule(id, exports, true)); + $requireMap.$set(id, $createCommonJSModule(id, exports, true, undefined)); return exports; } } @@ -152,7 +152,7 @@ export function createRequireCache() { const esm = Loader.registry.$get(key); if (esm?.evaluated) { const namespace = Loader.getModuleNamespaceObject(esm.module); - const mod = $createCommonJSModule(key, namespace, true); + const mod = $createCommonJSModule(key, namespace, true, undefined); $requireMap.$set(key, mod); return mod; } @@ -165,7 +165,7 @@ export function createRequireCache() { }, has(target, key: string) { - return $requireMap.$has(key) || Loader.registry.$has(key); + return $requireMap.$has(key) || Boolean(Loader.registry.$get(key)?.evaluated); }, deleteProperty(target, key: string) { @@ -177,13 +177,11 @@ export function createRequireCache() { ownKeys(target) { var array = [...$requireMap.$keys()]; - const registryKeys = [...Loader.registry.$keys()]; - for (const key of registryKeys) { - if (!array.includes(key)) { + for (const key of Loader.registry.$keys()) { + if (!array.includes(key) && Loader.registry.$get(key)?.evaluated) { $arrayPush(array, key); } } - return array; }, @@ -193,7 +191,7 @@ export function createRequireCache() { }, getOwnPropertyDescriptor(target, key: string) { - if ($requireMap.$has(key) || Loader.registry.$has(key)) { + if ($requireMap.$has(key) || Loader.registry.$get(key)?.evaluated) { return { configurable: true, enumerable: true, diff --git a/src/js/builtins/JSBufferConstructor.ts b/src/js/builtins/JSBufferConstructor.ts index a1072ea10..69615d8dc 100644 --- a/src/js/builtins/JSBufferConstructor.ts +++ b/src/js/builtins/JSBufferConstructor.ts @@ -1,3 +1,6 @@ +// This is marked as a constructor because Node.js allows `new Buffer.from`, +// Some legacy dependencies depend on this, see #3638 +$constructor; export function from(items) { if ($isUndefinedOrNull(items)) { throw new TypeError( diff --git a/src/js/builtins/Module.ts b/src/js/builtins/Module.ts index aa08bc728..b074d3488 100644 --- a/src/js/builtins/Module.ts +++ b/src/js/builtins/Module.ts @@ -1,21 +1,14 @@ -interface CommonJSModuleRecord { - $require(id: string, mod: any): any; - children: CommonJSModuleRecord[]; - exports: any; - id: string; - loaded: boolean; - parent: undefined; - path: string; - paths: string[]; - require: typeof require; -} - $getter; export function main() { return $requireMap.$get(Bun.main); } export function require(this: CommonJSModuleRecord, id: string) { + return $overridableRequire.$call(this, id); +} + +// overridableRequire can be overridden by setting `Module.prototype.require` +export function overridableRequire(this: CommonJSModuleRecord, id: string) { const existing = $requireMap.$get(id) || $requireMap.$get((id = $resolveSync(id, this.path, false))); if (existing) { // Scenario where this is necessary: @@ -45,7 +38,7 @@ export function require(this: CommonJSModuleRecord, id: string) { // To handle import/export cycles, we need to create a module object and put // it into the map before we import it. - const mod = $createCommonJSModule(id, {}, false); + const mod = $createCommonJSModule(id, {}, false, this); $requireMap.$set(id, mod); // This is where we load the module. We will see if Module._load and diff --git a/src/js/builtins/ProcessObjectInternals.ts b/src/js/builtins/ProcessObjectInternals.ts index 2bb8648df..a72a96c2a 100644 --- a/src/js/builtins/ProcessObjectInternals.ts +++ b/src/js/builtins/ProcessObjectInternals.ts @@ -24,27 +24,6 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -// TODO: move this to native code? -export function binding(bindingName) { - if (bindingName === "constants") { - return $processBindingConstants; - } - const issue = { - fs: 3546, - buffer: 2020, - natives: 2254, - uv: 2891, - }[bindingName]; - if (issue) { - throw new Error( - `process.binding("${bindingName}") is not implemented in Bun. Track the status & thumbs up the issue: https://github.com/oven-sh/bun/issues/${issue}`, - ); - } - throw new TypeError( - `process.binding("${bindingName}") is not implemented in Bun. If that breaks something, please file an issue and include a reproducible code sample.`, - ); -} - export function getStdioWriteStream(fd) { const tty = require("node:tty"); @@ -110,7 +89,8 @@ export function getStdinStream(fd) { const tty = require("node:tty"); - const stream = new tty.ReadStream(fd); + const ReadStream = tty.isatty(fd) ? tty.ReadStream : require("node:fs").ReadStream; + const stream = new ReadStream(fd); const originalOn = stream.on; stream.on = function (event, listener) { @@ -126,7 +106,7 @@ export function getStdinStream(fd) { if (event === "readable") { ref(); } - return originalOn.call(this, event, listener); + return originalOn.$call(this, event, listener); }; stream.fd = fd; @@ -134,13 +114,13 @@ export function getStdinStream(fd) { const originalPause = stream.pause; stream.pause = function () { unref(); - return originalPause.call(this); + return originalPause.$call(this); }; const originalResume = stream.resume; stream.resume = function () { ref(); - return originalResume.call(this); + return originalResume.$call(this); }; async function internalRead(stream) { @@ -272,6 +252,11 @@ export function initializeNextTickQueue(process, nextTickQueue, drainMicrotasksF // but allows much quicker checks. class FixedCircularBuffer { + top: number; + bottom: number; + list: Array<FixedCircularBuffer | undefined>; + next: FixedCircularBuffer | null; + constructor() { this.bottom = 0; this.top = 0; @@ -303,6 +288,9 @@ export function initializeNextTickQueue(process, nextTickQueue, drainMicrotasksF } class FixedQueue { + head: FixedCircularBuffer; + tail: FixedCircularBuffer; + constructor() { this.head = this.tail = new FixedCircularBuffer(); } @@ -401,3 +389,8 @@ export function initializeNextTickQueue(process, nextTickQueue, drainMicrotasksF return nextTick; } + +$getter; +export function mainModule() { + return $requireMap.$get(Bun.main); +} diff --git a/src/js/builtins/ReadableStream.ts b/src/js/builtins/ReadableStream.ts index 419a8e696..c1d43a430 100644 --- a/src/js/builtins/ReadableStream.ts +++ b/src/js/builtins/ReadableStream.ts @@ -291,6 +291,15 @@ export function createEmptyReadableStream() { } $linkTimeConstant; +export function createUsedReadableStream() { + var stream = new ReadableStream({ + pull() {}, + } as any); + stream.getReader(); + return stream; +} + +$linkTimeConstant; export function createNativeReadableStream(nativePtr, nativeType, autoAllocateChunkSize) { return new ReadableStream({ $lazy: true, diff --git a/src/js/builtins/ReadableStreamInternals.ts b/src/js/builtins/ReadableStreamInternals.ts index e249aea0a..36f049f3e 100644 --- a/src/js/builtins/ReadableStreamInternals.ts +++ b/src/js/builtins/ReadableStreamInternals.ts @@ -1034,7 +1034,8 @@ export function createTextStream(highWaterMark) { if (byteLength > 0) { hasBuffer = true; if (rope.length > 0) { - $arrayPush(array, rope, chunk); + $arrayPush(array, rope); + $arrayPush(array, chunk); rope = ""; } else { $arrayPush(array, chunk); @@ -1073,12 +1074,12 @@ export function createTextStream(highWaterMark) { } if (hasBuffer && !hasString) { - return new globalThis.TextDecoder().decode($Bun.concatArrayBuffers(array)); + return new globalThis.TextDecoder().decode(Bun.concatArrayBuffers(array)); } // worst case: mixed content - var arrayBufferSink = new $Bun.ArrayBufferSink(); + var arrayBufferSink = new Bun.ArrayBufferSink(); arrayBufferSink.start({ highWaterMark: estimatedLength, asUint8Array: true, @@ -1205,7 +1206,7 @@ export function initializeArrayBufferStream(underlyingSource, highWaterMark) { highWaterMark && typeof highWaterMark === "number" ? { highWaterMark, stream: true, asUint8Array: true } : { stream: true, asUint8Array: true }; - var sink = new $Bun.ArrayBufferSink(); + var sink = new Bun.ArrayBufferSink(); sink.start(opts); var controller = { @@ -1492,7 +1493,7 @@ export function lazyLoadStream(stream, autoAllocateChunkSize) { var nativePtr = $getByIdDirectPrivate(stream, "bunNativePtr"); var Prototype = $lazyStreamPrototypeMap.$get(nativeType); if (Prototype === undefined) { - var [pull, start, cancel, setClose, deinit, setRefOrUnref, drain] = $lazyLoad(nativeType); + var [pull, start, cancel, setClose, deinit, setRefOrUnref, drain] = $lazy(nativeType); var closer = [false]; var handleResult; function handleNativeReadableStreamPromiseResult(val) { @@ -1679,7 +1680,7 @@ export function readableStreamIntoText(stream) { } export function readableStreamToArrayBufferDirect(stream, underlyingSource) { - var sink = new $Bun.ArrayBufferSink(); + var sink = new Bun.ArrayBufferSink(); $putByIdDirectPrivate(stream, "underlyingSource", undefined); var highWaterMark = $getByIdDirectPrivate(stream, "highWaterMark"); sink.start(highWaterMark ? { highWaterMark } : {}); diff --git a/src/js/builtins/tsconfig.json b/src/js/builtins/tsconfig.json index edaedd2a9..71940a981 100644 --- a/src/js/builtins/tsconfig.json +++ b/src/js/builtins/tsconfig.json @@ -1,7 +1,9 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "noEmit": true + "noEmit": true, + "module": "esnext", + "moduleResolution": "bundler" }, "include": [ ".", |