Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c94351c
node:stream: define the Readable operators with the class and load th…
robobun Nov 6, 2026
7989ac0
process.stdout: load the Writable chain, not the node:stream barrel
robobun Nov 6, 2026
51ec702
process.stdin takes the class from internal/fs/streams; tests
robobun Nov 6, 2026
c7d9240
aot: process.stdout and process.stderr load no node:fs behind the host
robobun Nov 6, 2026
e522dd1
review: stdin keeps node:fs's class, lint matches top-level requires …
robobun Nov 6, 2026
aa899ed
aot: keep the mock test as on main
robobun Nov 6, 2026
3e5904f
node:fs: void the prototype read that puts ReadStream below Readable
robobun Nov 6, 2026
742b65b
streams: one-line comments
robobun Nov 6, 2026
5208745
aot: _http_server's second text loads Duplex where the first text does
robobun Nov 6, 2026
d874690
node:fs: a stream over a FileHandle calls the functions node:fs write…
robobun Nov 7, 2026
839ca6e
Merge origin/main into robobun/1f3c2915/stdout-without-stream-barrel
robobun Nov 8, 2026
ce4a474
streams: a built-in module stands below what a program stored on node…
robobun Nov 8, 2026
bb8c9d2
node:http: read Stream and Duplex off the Stream function, not the no…
robobun Nov 8, 2026
af38b05
node:http: load all of node:stream again
robobun Nov 8, 2026
df4d1a7
node:fs: a stub under stream.Readable fails the load of the fs stream…
robobun Nov 8, 2026
97a4492
fs streams: kFs is set where it was, undefined on a stream that write…
robobun Nov 8, 2026
cf71189
fs streams: the write assertion is for node:fs alone; the stub test c…
robobun Nov 8, 2026
7671b94
fs streams: the first use of fs.ReadStream takes the Readable stored …
robobun Nov 8, 2026
33a2688
fs streams: one line on the prototype accessor
robobun Nov 8, 2026
e9d84ab
ci: retrigger
robobun Nov 8, 2026
e6509bf
node:fs: a null under stream.Readable takes the library's own Readable
robobun Nov 8, 2026
c12e24d
fs streams: fs.ReadStream takes the library's own Readable at its fir…
robobun Nov 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/codegen/builtins/host/runtime-dependencies.builtins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ const standardStreamModulesThatTestsCanWatch = standardStreamModules.map(load =>
...load,
when: b.whenTestsReplaceBuiltins,
}));
// A standard output is a WriteStream, which writes through a FileSink and loads no node:fs. The input is a ReadStream, which reads through node:fs.
const standardOutputModulesThatTestsCanWatch = standardStreamModulesThatTestsCanWatch.filter(
load => load.module !== "node:fs",
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

const loadedModulesByPath: Readonly<Record<b.BuiltinPath, readonly b.ModuleLoadSpec[]>> = {
"node:fs/promises": evaluates("internal/fifo"),
Expand All @@ -81,14 +85,14 @@ const loadedModulesByPath: Readonly<Record<b.BuiltinPath, readonly b.ModuleLoadS
},
],
"process.openStdin": standardStreamModules,
"console._stderr": standardStreamModulesThatTestsCanWatch,
"console._stdout": standardStreamModulesThatTestsCanWatch,
"process.stderr": standardStreamModulesThatTestsCanWatch,
"console._stderr": standardOutputModulesThatTestsCanWatch,
"console._stdout": standardOutputModulesThatTestsCanWatch,
"process.stderr": standardOutputModulesThatTestsCanWatch,
"process.stdin": [
...standardStreamModulesThatTestsCanWatch,
...standardStreamModules.map(load => ({ ...load, when: b.whenProgramStores("process.nextTick") })),
],
"process.stdout": standardStreamModulesThatTestsCanWatch,
"process.stdout": standardOutputModulesThatTestsCanWatch,
};

const pluginFunctionsOfBuild = functionsIn(
Expand Down
9 changes: 4 additions & 5 deletions src/js/builtins/ProcessObjectInternals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,9 @@ export function getStdioWriteStream(
});
stream._type = "tty";
} else {
const fs = require("node:fs");
stream = new fs.WriteStream(null, { autoClose: false, fd, $fastPath: true });
// The class itself, not fs.WriteStream: the stream does not load node:fs.
const { WriteStream } = require("internal/fs/streams");
stream = new WriteStream(null, { autoClose: false, fd, $fastPath: true });
stream.readable = false;
stream._type = "fs";

Expand Down Expand Up @@ -701,10 +702,8 @@ export function createOnWarning(
// process.stderr.write, not console.error: under an inspector Session console.* emits
// Runtime.consoleAPICalled, and a throwing listener there is surfaced via emitWarning,
// so console.error would re-enter and loop. writeWarning reads process.stderr per call, as Node does.
// A compiled program may write without making the stream, whose modules put the operators of node:stream on Readable.prototype.
if (IS_COMPILED_AHEAD_OF_TIME && !redirectPath) require("internal/streams/load_all").rest?.();
const wroteInPlaceOfStream = writeWarning(message + "\n", redirectPath);
// Those modules also load node:events, which puts its EventEmitter.prototype above the prototype of `process`. Behind writeWarning(), which reads the members
// The modules of the stream load node:events, which puts its EventEmitter.prototype above the prototype of `process`. Behind writeWarning(), which reads the members
// of built-in objects that they read, in their order. A program that cannot tell does without the module.
if (IS_COMPILED_AHEAD_OF_TIME && WARNINGS_LOAD_NODE_EVENTS && wroteInPlaceOfStream) require("node:events");
}
Expand Down
9 changes: 2 additions & 7 deletions src/js/builtins/stdio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@ export function getLightStdioWriteStreams(
// queued by the first read of it comes earlier than any other. And node:events puts its EventEmitter.prototype above the prototype of `process`.
const nextTick = process.nextTick;
require("node:events");
// And all of node:stream puts its operators on Readable.prototype, which a program that has loaded a part of it reaches.
const streams = require("internal/streams/load_all");
streams.rest?.();
const served: ServedByLightStream = streams.servedByLightOutputs;
const served: ServedByLightStream = require("internal/streams/load_all").servedByLightOutputs;
// What writeFast() of internal/fs/streams does with a receiver that is not a stream: it asks for the state of a stream and then calls _write().
function writeToForeignReceiver(receiver: any): never {
if (receiver === undefined)
Expand Down Expand Up @@ -163,9 +160,7 @@ export function getLightStdinStream(isTTY: boolean, needsWholeStream: NeedsWhole
// As in getLightStdioWriteStreams().
void process.nextTick;
require("node:events");
const streams = require("internal/streams/load_all");
streams.rest?.();
const served: ServedByLightStream = streams.servedByLightInput;
const served: ServedByLightStream = require("internal/streams/load_all").servedByLightInput;
return new Proxy(
{
get "%the whole stream%"() {
Expand Down
160 changes: 95 additions & 65 deletions src/js/internal/fs/streams.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
// fs.ReadStream and fs.WriteStream are lazily loaded to avoid importing 'node:stream' until required
const { Readable, Writable, finished } = require("node:stream");
const fs = require("node:fs");
const { read, write, fsync, writev } = fs;
// process.stdout is a WriteStream: the Writable class and finished() load here, not node:stream, Readable or node:fs.
const { Stream } = require("internal/streams/legacy");
// node:stream is `Stream` with its members: what a program has stored there, where node:stream is loaded. Else each loads alone.
const ReadableOfProgram: $Node.ReadableConstructor | undefined = $getByIdDirect(Stream, "Readable") ?? undefined;
const Writable: $Node.WritableConstructor = $getByIdDirect(Stream, "Writable") ?? require("internal/streams/writable");
Comment thread
robobun marked this conversation as resolved.
Resolved
const finished: typeof Stream.finished =
$getByIdDirect(Stream, "finished") ?? require("internal/streams/end-of-stream");
const fileHandles = require("internal/fs/file_handles");
type FileHandleInternals = NonNullable<typeof fileHandles.internals>;
type FileHandle = $Node.FileHandle;
Expand Down Expand Up @@ -73,7 +76,8 @@ declare global {
flush?: boolean;
open(): void;
close(callback?: ErrorCallback): void;
[kFs]: StreamFs;
/** node:fs, or the `fs` option. Undefined on a WriteStream that writes through a FileSink. */
[kFs]: StreamFs | undefined;
[kIsPerformingIO]: boolean;
/** true: the path must be opened; a sink: a FileSink. */
[kWriteStreamFastPath]?: undefined | true | FileSink;
Expand Down Expand Up @@ -132,51 +136,56 @@ const { internals } = fileHandles;
if (internals) takeFileHandleInternals(internals);
else fileHandles.whenLoaded = takeFileHandleInternals;

const fileHandleStreamFs = (fh: FileHandle) => ({
// try to use the basic fs.read/write/fsync if available, since they are less
// abstractions. however, node.js allows patching the file handle, so this has
// to be checked for.
read:
fh.read === fileHandlePrototypeRead
? read
: function (
fd: number | null,
buf: Buffer,
offset: number,
length: number,
pos: number | undefined,
cb: (error: Error | null, bytesRead?: number, buffer?: Buffer) => void,
) {
return fh.read(buf, offset, length, pos).then(
({ bytesRead, buffer }) => cb(null, bytesRead, buffer),
err => cb(err, 0, buf),
);
},
write:
fh.write === fileHandlePrototypeWrite
? write
: function (
fd: number | null,
buffer: Buffer,
offset: number,
length: number,
position: number | undefined,
cb: (error: Error | null, bytesWritten?: number, buffer?: Buffer) => void,
) {
return fh.write(buffer, offset, length, position).then(
({ bytesWritten, buffer }) => cb(null, bytesWritten, buffer),
err => cb(err, 0, buffer),
);
},
writev: fh.writev === fileHandlePrototypeWritev ? writev : undefined,
fsync:
fh.sync === fileHandlePrototypeFsync
? fsync
: function (fd: number, cb: ErrorCallback) {
return fh.sync().then(() => cb(), cb);
},
close: streamFileHandleClose.bind(fh),
});
// node:fs loads with the first stream over a FileHandle. The stream calls these as node:fs writes them, not what its exports hold now.
function fileHandleStreamFs(fh: FileHandle) {
require("node:fs");
const { read, write, fsync, writev } = require("internal/shared").originals.fs!;
return {
// try to use the basic fs.read/write/fsync if available, since they are less
// abstractions. however, node.js allows patching the file handle, so this has
// to be checked for.
Comment thread
robobun marked this conversation as resolved.
Resolved
read:
fh.read === fileHandlePrototypeRead
? read
: function (
fd: number | null,
buf: Buffer,
offset: number,
length: number,
pos: number | undefined,
cb: (error: Error | null, bytesRead?: number, buffer?: Buffer) => void,
) {
return fh.read(buf, offset, length, pos).then(
({ bytesRead, buffer }) => cb(null, bytesRead, buffer),
err => cb(err, 0, buf),
);
},
write:
fh.write === fileHandlePrototypeWrite
? write
: function (
fd: number | null,
buffer: Buffer,
offset: number,
length: number,
position: number | undefined,
cb: (error: Error | null, bytesWritten?: number, buffer?: Buffer) => void,
) {
return fh.write(buffer, offset, length, position).then(
({ bytesWritten, buffer }) => cb(null, bytesWritten, buffer),
err => cb(err, 0, buffer),
);
},
writev: fh.writev === fileHandlePrototypeWritev ? writev : undefined,
fsync:
fh.sync === fileHandlePrototypeFsync
? fsync
: function (fd: number, cb: ErrorCallback) {
return fh.sync().then(() => cb(), cb);
},
close: streamFileHandleClose.bind(fh),
};
}

function streamFileHandleClose(this: FileHandle, fd: FD, cb: ErrorCallback) {
$assert(this[kFd] == fd, "fd mismatch");
Expand Down Expand Up @@ -270,9 +279,9 @@ function ReadStream(

if (windowsHandle != null) {
validateWindowsHandle(windowsHandle, fd, customFs);
this[kFs] = fs;
this[kFs] = require("node:fs");
} else if (fd == null) {
this[kFs] = customFs || fs;
this[kFs] = customFs || require("node:fs");
this.fd = null;
this.path = getValidatedPath(path);
const { flags, mode } = options;
Expand All @@ -292,7 +301,7 @@ function ReadStream(
validateInt32(fd, "fd", 0, 2147483647);
}
this.fd = fd as number;
this[kFs] = customFs || fs;
this[kFs] = customFs || require("node:fs");
} else if (typeof fd === "object" && FileHandle !== undefined && fd instanceof FileHandle) {
if (options.fs) {
throw $ERR_METHOD_NOT_IMPLEMENTED("FileHandle with fs");
Expand Down Expand Up @@ -340,12 +349,30 @@ function ReadStream(
// such as highWaterMark and signal, and a throw there leaks the fd, as in Node.
if (windowsHandle != null) this.fd = windowsHandleToFd(windowsHandle);

Readable.$call(this, options);
(ReadableOfProgram ?? require("internal/streams/readable")).$call(this, options);
return this as unknown as void;
}
$toClass(ReadStream, "ReadStream", Readable);
// Where node:stream is loaded, so is Readable: the class goes below what lies there now, and a stub there fails this load, which then runs again.
$toClass(ReadStream, "ReadStream", ReadableOfProgram);
const readStreamPrototype = ReadStream.prototype;

// Else the class goes below the library's own Readable at its first use (`new`, `instanceof`, a read of `prototype`), so a WriteStream loads no Readable.
if (ReadableOfProgram === undefined)
Object.defineProperty(ReadStream, "prototype", {
get() {
const Readable = require("internal/streams/readable");
Object.setPrototypeOf(readStreamPrototype, Readable.prototype);
Object.setPrototypeOf(ReadStream, Readable);
Object.defineProperty(ReadStream, "prototype", { value: readStreamPrototype, writable: true });
return readStreamPrototype;
},
set(value) {
Object.defineProperty(ReadStream, "prototype", { value, writable: true });
},
Comment thread
robobun marked this conversation as resolved.
Resolved
enumerable: false,
configurable: true,
});

Object.defineProperty(readStreamPrototype, "autoClose", {
get(this: $Node.ReadStream) {
return this._readableState.autoDestroy;
Expand Down Expand Up @@ -389,7 +416,7 @@ function streamConstruct(this: FileStream, callback: ErrorCallback) {
return;
}

this[kFs].open!(this.path, this.flags, this.mode, (err, fd) => {
this[kFs]!.open!(this.path, this.flags, this.mode, (err, fd) => {
if (err) {
callback(err);
} else {
Expand Down Expand Up @@ -417,7 +444,7 @@ readStreamPrototype._read = function (this: $Node.ReadStream, n: number) {
const buf = Buffer.allocUnsafeSlow(n);

this[kIsPerformingIO] = true;
this[kFs].read!(this.fd, buf, 0, n, this.pos, (er, bytesRead, buf) => {
this[kFs]!.read!(this.fd, buf, 0, n, this.pos, (er, bytesRead, buf) => {
this[kIsPerformingIO] = false;

// Tell ._destroy() that it's safe to close the fd now.
Expand Down Expand Up @@ -491,7 +518,7 @@ function close(stream: FileStream, err: Error | null | undefined, cb: ErrorCallb
if (stream.fd == null) {
cb(err);
} else if (stream.flush) {
stream[kFs].fsync!(stream.fd, flushErr => {
stream[kFs]!.fsync!(stream.fd, flushErr => {
closeAfterSync(stream, err || flushErr, cb);
});
} else {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Expand All @@ -500,7 +527,7 @@ function close(stream: FileStream, err: Error | null | undefined, cb: ErrorCallb
}

function closeAfterSync(stream: FileStream, err: Error | null | undefined, cb: ErrorCallback) {
stream[kFs].close!(stream.fd!, er => {
stream[kFs]!.close!(stream.fd!, er => {
cb(er || err);
});
stream.fd = null;
Expand All @@ -523,11 +550,13 @@ function WriteStream(
options.decodeStrings = true;

let { fd, autoClose, fs: customFs, start, flush, windowsHandle } = options;
// A stream that writes through a FileSink never reads node:fs, and process.stdout does not load it.
const fsOf = () => customFs || (fastPath ? undefined : require("node:fs"));
if (windowsHandle != null) {
validateWindowsHandle(windowsHandle, fd, customFs);
this[kFs] = fs;
this[kFs] = fsOf();
} else if (fd == null) {
this[kFs] = customFs || fs;
this[kFs] = fsOf();
this.fd = null;
// Internal $fastPath callers (writableFromFileSink) discard .path; do not
// resolve it - path.resolve("") needs process.cwd(), which throws when
Expand All @@ -550,7 +579,7 @@ function WriteStream(
validateInt32(fd, "fd", 0, 2147483647);
}
this.fd = fd as number;
this[kFs] = customFs || fs;
this[kFs] = fsOf();
} else if (typeof fd === "object" && FileHandle !== undefined && fd instanceof FileHandle) {
if (options.fs) {
throw $ERR_METHOD_NOT_IMPLEMENTED("FileHandle with fs");
Expand Down Expand Up @@ -579,7 +608,6 @@ function WriteStream(
if (!writev) this._writev = null!;
} else {
this._writev = undefined;
$assert(this[kFs].write, "assuming user does not delete fs.write!");
}

if (flush == null) {
Expand Down Expand Up @@ -609,8 +637,10 @@ function WriteStream(
fastWriter = Bun.file(fd as number).writer() as FileSink;
} catch {
fastPath = false;
this[kFs] = fsOf();
}
}
$assert(fastPath || customFs || this[kFs]!.write, "assuming user does not delete fs.write!");

// Enable fast path
if (fastPath) {
Expand Down Expand Up @@ -661,7 +691,7 @@ function writeAll(
cb: ErrorCallback,
retries = 0,
) {
(IS_COMPILED_AHEAD_OF_TIME ? fileSystemOrThrow(this[kFs]) : this[kFs]).write!(
(IS_COMPILED_AHEAD_OF_TIME ? fileSystemOrThrow(this[kFs]) : this[kFs]!).write!(
this.fd,
data,
0,
Expand Down Expand Up @@ -705,7 +735,7 @@ function writevAll(
cb: ErrorCallback,
retries = 0,
) {
this[kFs].writev!(this.fd, chunks, this.pos, (er, bytesWritten, buffers) => {
this[kFs]!.writev!(this.fd, chunks, this.pos, (er, bytesWritten, buffers) => {
// No data currently available and operation should be retried later.
if (er?.code === "EAGAIN") {
er = null;
Expand Down
4 changes: 3 additions & 1 deletion src/js/internal/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,9 @@ const originals: {
"emit" | "addListener" | "once" | "removeListener" | "removeAllListeners" | "listenerCount"
> & { storeEmitWithRejectionCapture(emitter: $Node.EventEmitter): void })
| undefined;
} = { eventEmitter: undefined };
// What internal/fs/streams calls for a stream over a FileHandle.
fs: Pick<$BunModules["node:fs"], "read" | "write" | "fsync" | "writev"> | undefined;
} = { eventEmitter: undefined, fs: undefined };

export default {
isStoppedModuleGraphRunning,
Expand Down
Loading
Loading