diff options
author | 2023-02-14 17:54:50 -0600 | |
---|---|---|
committer | 2023-02-14 15:54:50 -0800 | |
commit | a80981c9662bc439871ca2197a908632c82491d9 (patch) | |
tree | 064202a6a68ec233b2226c1e6ef95abadd50a7bd | |
parent | 6e1a52691a370058e97ddb84c8f7c49e5bbf1e7e (diff) | |
download | bun-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.js | 212 | ||||
-rw-r--r-- | test/bun.js/fs.test.js | 184 | ||||
-rw-r--r-- | test/fixtures/export-lazy-fs-streams/export-*-from.ts | 1 | ||||
-rw-r--r-- | test/fixtures/export-lazy-fs-streams/export-from.ts | 1 |
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"; |