aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Derrick Farris <mr.dcfarris@gmail.com> 2023-02-14 17:54:50 -0600
committerGravatar GitHub <noreply@github.com> 2023-02-14 15:54:50 -0800
commita80981c9662bc439871ca2197a908632c82491d9 (patch)
tree064202a6a68ec233b2226c1e6ef95abadd50a7bd
parent6e1a52691a370058e97ddb84c8f7c49e5bbf1e7e (diff)
downloadbun-a80981c9662bc439871ca2197a908632c82491d9.tar.gz
bun-a80981c9662bc439871ca2197a908632c82491d9.tar.zst
bun-a80981c9662bc439871ca2197a908632c82491d9.zip
[WIP] fix(node:fs): export `fs.ReadStream` and `fs.WriteStream` (#1798)
* fix(node:fs): export fs.WriteStream and fs.ReadStream * test(node:fs): add tests for fs.ReadStream and fs.WriteStream * test(node:fs): prevent opening fd w/o closing * fix(node:fs): hack ESM export for fs streams to keep lazy loading * fix(node:fs): = -> ===, fix hasInstance comparison * test(node:fs): add test that actually checks that re-exported streams work * fix(fs): eagerly load our slow lazy fns (thanks esm) * fix(fs): employ @alexlamsl 's constructor w/o new trick on Read/WriteStream
-rw-r--r--src/bun.js/fs.exports.js212
-rw-r--r--test/bun.js/fs.test.js184
-rw-r--r--test/fixtures/export-lazy-fs-streams/export-*-from.ts1
-rw-r--r--test/fixtures/export-lazy-fs-streams/export-from.ts1
4 files changed, 326 insertions, 72 deletions
diff --git a/src/bun.js/fs.exports.js b/src/bun.js/fs.exports.js
index 00b0054e8..be82f0bd3 100644
--- a/src/bun.js/fs.exports.js
+++ b/src/bun.js/fs.exports.js
@@ -1,5 +1,9 @@
var { direct, isPromise, isCallable } = import.meta.primordials;
var promises = import.meta.require("node:fs/promises");
+
+var { Readable, NativeWritable, _getNativeReadableStreamPrototype, eos: eos_ } = import.meta.require("node:stream");
+var NativeReadable = _getNativeReadableStreamPrototype(2, Readable); // 2 means native type is a file here
+
var fs = Bun.fs();
var debug = process.env.DEBUG ? console.log : () => {};
export var access = function access(...args) {
@@ -172,52 +176,65 @@ function callbackify(fsFunction, args) {
// _events
// _eventsCount
// _maxListener
-var _lazyReadStream;
var readStreamPathFastPathSymbol = Symbol.for("Bun.Node.readStreamPathFastPath");
const readStreamSymbol = Symbol.for("Bun.NodeReadStream");
const readStreamPathOrFdSymbol = Symbol.for("Bun.NodeReadStreamPathOrFd");
+const writeStreamSymbol = Symbol.for("Bun.NodeWriteStream");
var writeStreamPathFastPathSymbol = Symbol.for("Bun.NodeWriteStreamFastPath");
var writeStreamPathFastPathCallSymbol = Symbol.for("Bun.NodeWriteStreamFastPathCall");
var kIoDone = Symbol.for("kIoDone");
-function getLazyReadStream() {
- if (_lazyReadStream) {
- return _lazyReadStream;
- }
+var defaultReadStreamOptions = {
+ file: undefined,
+ fd: undefined,
+ flags: "r",
+ encoding: undefined,
+ mode: 0o666,
+ autoClose: true,
+ emitClose: true,
+ start: 0,
+ end: Infinity,
+ highWaterMark: 64 * 1024,
+ fs: {
+ read,
+ open: (path, flags, mode, cb) => {
+ var fd;
+ try {
+ fd = openSync(path, flags, mode);
+ } catch (e) {
+ cb(e);
+ return;
+ }
- var { Readable, _getNativeReadableStreamPrototype, eos: eos_ } = import.meta.require("node:stream");
- var defaultReadStreamOptions = {
- file: undefined,
- fd: undefined,
- flags: "r",
- encoding: undefined,
- mode: 0o666,
- autoClose: true,
- emitClose: true,
- start: 0,
- end: Infinity,
- highWaterMark: 64 * 1024,
- fs: {
- read,
- open: (path, flags, mode, cb) => {
- var fd;
- try {
- fd = openSync(path, flags, mode);
- } catch (e) {
- cb(e);
- return;
- }
+ cb(null, fd);
+ },
+ openSync,
+ close,
+ },
+ autoDestroy: true,
+};
- cb(null, fd);
+var ReadStreamClass;
+export var ReadStream = (function (InternalReadStream) {
+ ReadStreamClass = InternalReadStream;
+ Object.defineProperty(ReadStreamClass.prototype, Symbol.toStringTag, {
+ value: "ReadStream",
+ enumerable: false,
+ });
+
+ return Object.defineProperty(
+ function ReadStream(options) {
+ return new InternalReadStream(options);
+ },
+ Symbol.hasInstance,
+ {
+ value(instance) {
+ return instance instanceof InternalReadStream;
},
- openSync,
- close,
},
- autoDestroy: true,
- };
-
- var NativeReadable = _getNativeReadableStreamPrototype(2, Readable); // 2 means native type is a file here
- var ReadStream = class ReadStream extends NativeReadable {
+ );
+})(
+ class ReadStream extends NativeReadable {
constructor(pathOrFd, options = defaultReadStreamOptions) {
if (typeof options !== "object" || !options) {
throw new TypeError("Expected options to be an object");
@@ -522,39 +539,49 @@ function getLazyReadStream() {
this[readStreamPathFastPathSymbol] = false;
return super.pipe(dest, pipeOpts);
}
- };
- return (_lazyReadStream = ReadStream);
-}
+ },
+);
-var internalCreateReadStream = function createReadStream(path, options) {
- const ReadStream = getLazyReadStream();
+export function createReadStream(path, options) {
return new ReadStream(path, options);
+}
+
+var defaultWriteStreamOptions = {
+ fd: null,
+ start: undefined,
+ pos: undefined,
+ encoding: undefined,
+ flags: "w",
+ mode: 0o666,
+ fs: {
+ write,
+ close,
+ open,
+ openSync,
+ },
};
-var _lazyWriteStream;
-export var createReadStream = internalCreateReadStream;
-
-function getLazyWriteStream() {
- if (_lazyWriteStream) return _lazyWriteStream;
-
- const { NativeWritable } = import.meta.require("node:stream");
-
- var defaultWriteStreamOptions = {
- fd: null,
- start: undefined,
- pos: undefined,
- encoding: undefined,
- flags: "w",
- mode: 0o666,
- fs: {
- write,
- close,
- open,
- openSync,
+var WriteStreamClass;
+export var WriteStream = (function (InternalWriteStream) {
+ WriteStreamClass = InternalWriteStream;
+ Object.defineProperty(WriteStreamClass.prototype, Symbol.toStringTag, {
+ value: "WritesStream",
+ enumerable: false,
+ });
+
+ return Object.defineProperty(
+ function WriteStream(options) {
+ return new InternalWriteStream(options);
},
- };
-
- var WriteStream = class WriteStream extends NativeWritable {
+ Symbol.hasInstance,
+ {
+ value(instance) {
+ return instance instanceof InternalWriteStream;
+ },
+ },
+ );
+})(
+ class WriteStream extends NativeWritable {
constructor(path, options = defaultWriteStreamOptions) {
if (!options) {
throw new TypeError("Expected options to be an object");
@@ -668,6 +695,7 @@ function getLazyWriteStream() {
bytesWritten = 0;
pos;
[writeStreamPathFastPathSymbol];
+ [writeStreamSymbol] = true;
start;
[writeStreamPathFastPathCallSymbol](readStream, pipeOpts) {
@@ -833,23 +861,52 @@ function getLazyWriteStream() {
this.emit("error", err);
}
}
- };
- return (_lazyWriteStream = WriteStream);
-}
+ },
+);
-var internalCreateWriteStream = function createWriteStream(path, options) {
- const WriteStream = getLazyWriteStream();
+export function createWriteStream(path, options) {
+ // const WriteStream = getLazyWriteStream();
return new WriteStream(path, options);
-};
+}
+
+// NOTE: This was too smart and doesn't actually work
+// export var WriteStream = Object.defineProperty(
+// function WriteStream(path, options) {
+// var _InternalWriteStream = getLazyWriteStream();
+// return new _InternalWriteStream(path, options);
+// },
+// Symbol.hasInstance,
+// { value: (instance) => instance[writeStreamSymbol] === true },
+// );
+
+// export var ReadStream = Object.defineProperty(
+// function ReadStream(path, options) {
+// var _InternalReadStream = getLazyReadStream();
+// return new _InternalReadStream(path, options);
+// },
+// Symbol.hasInstance,
+// { value: (instance) => instance[readStreamSymbol] === true },
+// );
-export var createWriteStream = internalCreateWriteStream;
Object.defineProperties(fs, {
createReadStream: {
- value: internalCreateReadStream,
+ value: createReadStream,
},
createWriteStream: {
value: createWriteStream,
},
+ ReadStream: {
+ value: ReadStream,
+ },
+ WriteStream: {
+ value: WriteStream,
+ },
+ // ReadStream: {
+ // get: () => getLazyReadStream(),
+ // },
+ // WriteStream: {
+ // get: () => getLazyWriteStream(),
+ // },
});
// lol
@@ -936,4 +993,17 @@ export default {
writeFile,
writeFileSync,
writeSync,
+ WriteStream,
+ ReadStream,
+
+ [Symbol.for("::bunternal::")]: {
+ ReadStreamClass,
+ WriteStreamClass,
+ },
+ // get WriteStream() {
+ // return getLazyWriteStream();
+ // },
+ // get ReadStream() {
+ // return getLazyReadStream();
+ // },
};
diff --git a/test/bun.js/fs.test.js b/test/bun.js/fs.test.js
index a203a47b4..086f19211 100644
--- a/test/bun.js/fs.test.js
+++ b/test/bun.js/fs.test.js
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it } from "bun:test";
import { gc, gcTick } from "./gc";
-import {
+import fs, {
closeSync,
existsSync,
mkdirSync,
@@ -26,8 +26,18 @@ import {
Dirent,
Stats,
} from "node:fs";
+import { tmpdir } from "node:os";
import { join } from "node:path";
+import {
+ ReadStream as ReadStream_,
+ WriteStream as WriteStream_,
+} from "../fixtures/export-lazy-fs-streams/export-from";
+import {
+ ReadStream as ReadStreamStar_,
+ WriteStream as WriteStreamStar_,
+} from "../fixtures/export-lazy-fs-streams/export-*-from";
+
const Buffer = globalThis.Buffer || Uint8Array;
if (!import.meta.dir) {
@@ -604,6 +614,178 @@ describe("createReadStream", () => {
});
});
+describe("fs.WriteStream", () => {
+ it("should be exported", () => {
+ expect(fs.WriteStream).toBeDefined();
+ });
+
+ it("should be constructable", () => {
+ const stream = new fs.WriteStream("test.txt");
+ expect(stream instanceof fs.WriteStream).toBe(true);
+ });
+
+ it("should be able to write to a file", (done) => {
+ const pathToDir = `${tmpdir()}/${Date.now()}`;
+ mkdirSync(pathToDir);
+ const path = `${pathToDir}/fs-writestream-test.txt`;
+
+ const stream = new fs.WriteStream(path, { flags: "w+" });
+ stream.write("Test file written successfully");
+ stream.end();
+
+ stream.on("error", (e) => {
+ done(e instanceof Error ? e : new Error(e));
+ });
+
+ stream.on("finish", () => {
+ expect(readFileSync(path, "utf8")).toBe("Test file written successfully");
+ done();
+ });
+ });
+
+ it("should work if re-exported by name", () => {
+ const stream = new WriteStream_("test.txt");
+ expect(stream instanceof WriteStream_).toBe(true);
+ expect(stream instanceof WriteStreamStar_).toBe(true);
+ expect(stream instanceof fs.WriteStream).toBe(true);
+ });
+
+ it("should work if re-exported by name, called without new", () => {
+ const stream = WriteStream_("test.txt");
+ expect(stream instanceof WriteStream_).toBe(true);
+ expect(stream instanceof WriteStreamStar_).toBe(true);
+ expect(stream instanceof fs.WriteStream).toBe(true);
+ });
+
+ it("should work if re-exported, as export * from ...", () => {
+ const stream = new WriteStreamStar_("test.txt");
+ expect(stream instanceof WriteStream_).toBe(true);
+ expect(stream instanceof WriteStreamStar_).toBe(true);
+ expect(stream instanceof fs.WriteStream).toBe(true);
+ });
+
+ it("should work if re-exported, as export * from..., called without new", () => {
+ const stream = WriteStreamStar_("test.txt");
+ expect(stream instanceof WriteStream_).toBe(true);
+ expect(stream instanceof WriteStreamStar_).toBe(true);
+ expect(stream instanceof fs.WriteStream).toBe(true);
+ });
+
+ it("should be able to write to a file with re-exported WriteStream", (done) => {
+ const pathToDir = `${tmpdir()}/${Date.now()}`;
+ mkdirSync(pathToDir);
+ const path = `${pathToDir}/fs-writestream-re-exported-test.txt`;
+
+ const stream = new WriteStream_(path, { flags: "w+" });
+ stream.write("Test file written successfully");
+ stream.end();
+
+ stream.on("error", (e) => {
+ done(e instanceof Error ? e : new Error(e));
+ });
+
+ stream.on("finish", () => {
+ expect(readFileSync(path, "utf8")).toBe("Test file written successfully");
+ done();
+ });
+ });
+});
+
+describe("fs.ReadStream", () => {
+ it("should be exported", () => {
+ expect(fs.ReadStream).toBeDefined();
+ });
+
+ it("should be constructable", () => {
+ const stream = new fs.ReadStream("test.txt");
+ expect(stream instanceof fs.ReadStream).toBe(true);
+ });
+
+ it("should be able to read from a file", (done) => {
+ const pathToDir = `${tmpdir()}/${Date.now()}`;
+ mkdirSync(pathToDir);
+ const path = `${pathToDir}fs-readstream-test.txt`;
+
+ writeFileSync(path, "Test file written successfully", {
+ encoding: "utf8",
+ flag: "w+",
+ });
+
+ const stream = new fs.ReadStream(path);
+ stream.setEncoding("utf8");
+ stream.on("error", (e) => {
+ done(e instanceof Error ? e : new Error(e));
+ });
+
+ let data = "";
+
+ stream.on("data", (chunk) => {
+ data += chunk;
+ });
+
+ stream.on("end", () => {
+ expect(data).toBe("Test file written successfully");
+ done();
+ });
+ });
+
+ it("should work if re-exported by name", () => {
+ const stream = new ReadStream_("test.txt");
+ expect(stream instanceof ReadStream_).toBe(true);
+ expect(stream instanceof ReadStreamStar_).toBe(true);
+ expect(stream instanceof fs.ReadStream).toBe(true);
+ });
+
+ it("should work if re-exported by name, called without new", () => {
+ const stream = ReadStream_("test.txt");
+ expect(stream instanceof ReadStream_).toBe(true);
+ expect(stream instanceof ReadStreamStar_).toBe(true);
+ expect(stream instanceof fs.ReadStream).toBe(true);
+ });
+
+ it("should work if re-exported as export * from ...", () => {
+ const stream = new ReadStreamStar_("test.txt");
+ expect(stream instanceof ReadStreamStar_).toBe(true);
+ expect(stream instanceof ReadStream_).toBe(true);
+ expect(stream instanceof fs.ReadStream).toBe(true);
+ });
+
+ it("should work if re-exported as export * from ..., called without new", () => {
+ const stream = ReadStreamStar_("test.txt");
+ expect(stream instanceof ReadStreamStar_).toBe(true);
+ expect(stream instanceof ReadStream_).toBe(true);
+ expect(stream instanceof fs.ReadStream).toBe(true);
+ });
+
+ it("should be able to read from a file, with re-exported ReadStream", (done) => {
+ const pathToDir = `${tmpdir()}/${Date.now()}`;
+ mkdirSync(pathToDir);
+ const path = `${pathToDir}fs-readstream-re-exported-test.txt`;
+
+ writeFileSync(path, "Test file written successfully", {
+ encoding: "utf8",
+ flag: "w+",
+ });
+
+ const stream = new ReadStream_(path);
+ stream.setEncoding("utf8");
+ stream.on("error", (e) => {
+ done(e instanceof Error ? e : new Error(e));
+ });
+
+ let data = "";
+
+ stream.on("data", (chunk) => {
+ data += chunk;
+ });
+
+ stream.on("end", () => {
+ expect(data).toBe("Test file written successfully");
+ done();
+ });
+ });
+});
+
describe("createWriteStream", () => {
it("simple write stream finishes", async () => {
const path = `/tmp/fs.test.js/${Date.now()}.createWriteStream.txt`;
diff --git a/test/fixtures/export-lazy-fs-streams/export-*-from.ts b/test/fixtures/export-lazy-fs-streams/export-*-from.ts
new file mode 100644
index 000000000..1c2b97875
--- /dev/null
+++ b/test/fixtures/export-lazy-fs-streams/export-*-from.ts
@@ -0,0 +1 @@
+export * from "node:fs";
diff --git a/test/fixtures/export-lazy-fs-streams/export-from.ts b/test/fixtures/export-lazy-fs-streams/export-from.ts
new file mode 100644
index 000000000..5258f9fde
--- /dev/null
+++ b/test/fixtures/export-lazy-fs-streams/export-from.ts
@@ -0,0 +1 @@
+export { ReadStream, WriteStream } from "node:fs";