/**
* File System APIs
*
* The File System APIs enable interacting with the file system in a way modeled
* on standard POSIX functions.
*
* @see {@link https://nodejs.org/api/fs.html}
*
* @module File-System
*/
const binding = process.binding('fs');
const BUFFER_SIZE = 40 * 1024; // 40KB bytes buffer when reading.
/**
* A File object is an object wrapper for a numeric file descriptor.
*/
export class File {
/**
* Creates a new File instance given a file path.
*
* @param {String} path - The file path for the File instance.
* @param {String} [mode] - The mode in which the file is to be opened.
* @returns {File} An instance of the File class.
*/
constructor(path, mode) {
// Check if the path argument is a valid type.
if (typeof path !== 'string') {
throw new TypeError(`The "path" argument must be of type string.`);
}
this._handle = null;
this.path = path;
this.mode = mode;
this.fd = null;
}
/**
* Asynchronously opens the file.
*
* @param {string} mode - The mode in which the file is to be opened.
*/
async open(mode = 'r') {
// Check if the file is already open.
if (this._handle) {
throw new Error(`The file is already open with fd: ${this.fd}`);
}
this._handle = await binding.open(this.path, this.mode || mode);
this.fd = this._handle.fd;
}
/**
* Synchronously opens the file.
*
* @param {string} mode - The mode in which the file is to be opened.
*/
openSync(mode = 'r') {
// Check if the file is already open.
if (this._handle) {
throw new Error(`The file is already open with fd: ${this.fd}`);
}
this._handle = binding.openSync(this.path, this.mode || mode);
this.fd = this._handle.fd;
}
/**
* Reads asynchronously some bytes from the file.
*
* @param {Uint8Array} buffer - The buffer into which the data will be read.
* @param {Number} offset - The starting position in the file from which to begin reading data.
* @returns {Promise<Number>} - The amount of bytes read.
*/
async read(buffer, offset = 0) {
// Check if the file is open.
if (!this._handle) {
throw new Error('The file is not open.');
}
// Provided buffers must be Uint8Arrays.
if (!(buffer instanceof Uint8Array)) {
throw new TypeError(`The "buffer" argument must be of type Uint8Array.`);
}
// Copy bytes into buffer and return bytes read.
return binding.read(this._handle, buffer.buffer, offset);
}
/**
* Reads synchronously some bytes from the file.
*
* @param {Uint8Array} buffer - The buffer into which the data will be read.
* @param {Number} offset - The starting position in the file from which to begin reading data.
* @returns {Number} - The amount of bytes read.
*/
readSync(buffer, offset = 0) {
// Check if the file is open.
if (!this._handle) {
throw new Error('The file is not open.');
}
// Provided buffers must be Uint8Arrays.
if (!(buffer instanceof Uint8Array)) {
throw new TypeError(`The "buffer" argument must be of type Uint8Array.`);
}
// Copy bytes into buffer and return bytes read.
return binding.readSync(this._handle, buffer.buffer, offset);
}
/**
* Writes asynchronously a binary buffer to the file.
*
* @param {Uint8Array} data - The binary data to be written to the file.
*/
async write(data) {
// Check the data argument type.
if (!(data instanceof Uint8Array)) {
throw new TypeError(`The "data" argument must be of type Uint8Array.`);
}
// Check if the file is open.
if (!this._handle) {
throw new Error('The file is not open.');
}
return binding.write(this._handle, data);
}
/**
* Writes synchronously a binary buffer to the file.
*
* @param {Uint8Array} data - The binary data to be written to the file.
*/
writeSync(data) {
// Check the data argument type.
if (!(data instanceof Uint8Array)) {
throw new TypeError(`The "data" argument must be of type Uint8Array.`);
}
// Check if the file is open.
if (!this._handle) {
throw new Error('The file is not open.');
}
binding.writeSync(this._handle, data);
}
/**
* Information about a specific `File` object.
*
* @typedef {Object} FileStats
* @property {number} size - The size of the file in bytes.
* @property {number} [atimeMs] - The timestamp indicating the last time this file was accessed (POSIX Epoch).
* @property {number} [mtimeMs] - The timestamp indicating the last time this file was modified (POSIX Epoch).
* @property {number} [birthtimeMs] - The timestamp indicating the creation time of this file (POSIX Epoch).
* @property {boolean} isFile - Returns `true` if the object describes a regular file.
* @property {boolean} isDirectory - Returns `true` if the object describes a file system directory.
* @property {boolean} isSymbolicLink - Returns `true` if the object describes a symbolic link.
* @property {boolean} [isSocket] - Returns `true` if the object describes a socket.
* @property {boolean} [isFIFO] - Returns `true` if object describes a regular file.
* @property {boolean} [isBlockDevice] - Returns `true` if the object describes a block device.
* @property {boolean} [isCharacterDevice] - Returns `true` if the object describes a character device.
* @property {number} [blocks] - The number of blocks allocated for this file.
* @property {number} [blksize] - The file system block size for i/o operations.
* @property {number} [mode] - A bit-field describing the file type and mode.
* @property {number} [dev] - The numeric identifier of the device containing the file.
* @property {number} [gid] - The numeric group identifier of the group that owns the file (POSIX).
* @property {number} [inode] - The file system specific "Inode" number for the file.
* @property {number} [nlink] - The number of hard-links that exist for the file.
* @property {number} [rdev] - A numeric device identifier if the file represents a device.
*/
/**
* Retrieves asynchronously statistics for the file.
*
* @returns {Promise<FileStats>} Useful information about the file.
*/
async stat() {
// Check if the file is already closed.
if (!this._handle) {
throw new Error('The file is not open.');
}
return binding.stat(this.path);
}
/**
* Retrieves synchronously statistics for the file.
*
* @returns {FileStats} Useful information about the file.
*/
statSync() {
// Check if the file is already closed.
if (!this._handle) {
throw new Error('The file is not open.');
}
return binding.statSync(this.path);
}
/**
* Closes the file asynchronously.
*/
async close() {
// Check if the file is already closed.
if (!this._handle) {
throw new Error('The file is not open.');
}
await binding.close(this._handle);
// Reset file object's attributes.
this._handle = null;
this.fd = null;
}
/**
* Closes the file synchronously.
*/
closeSync() {
// Check if the file is already closed.
if (!this._handle) {
throw new Error('The file is not open.');
}
binding.closeSync(this._handle);
// Reset file object's attributes.
this._handle = null;
this.fd = null;
}
/**
* The `File` instances are asynchronously iterable objects.
* @ignore
*/
async *[Symbol.asyncIterator](signal) {
// Close the file on stream pipeline errors.
if (signal) signal.on('uncaughtStreamException', () => this.close());
let buffer = new Uint8Array(BUFFER_SIZE);
let bytesRead = 0;
let offset = 0;
while ((bytesRead = await this.read(buffer, offset))) {
if (bytesRead === 0) break;
offset += bytesRead;
yield buffer.subarray(0, bytesRead);
}
}
/**
* The `File` instances are iterable objects.
* @ignore
*/
*[Symbol.iterator]() {
let buffer = new Uint8Array(BUFFER_SIZE);
let bytesRead = 0;
let offset = 0;
while ((bytesRead = this.readSync(buffer, offset))) {
if (bytesRead === 0) break;
offset += bytesRead;
yield buffer.subarray(0, bytesRead);
}
}
}
function makeDeferredPromise() {
// Extract the resolve method from the promise.
const promiseExt = {};
const promise = new Promise((resolve, reject) => {
promiseExt.resolve = resolve;
promiseExt.reject = reject;
});
return { promise, promiseExt };
}
/**
* An async iterator yielding file-system events.
*/
class FsWatcher {
#id;
#pushQueue;
#pullQueue;
/**
* Creates a new FsWatcher instance.
*
* @param {String} path - The path to be monitored for changes.
* @param {Boolean} recursive - The watcher will monitor changes in the directory and its subdirectories.
* @returns {FsWatcher} An instance to monitor file or directory changes.
*/
constructor(path, recursive = false) {
this.#pushQueue = [];
this.#pullQueue = [];
this.#id = binding.watch(path, recursive, (event) =>
this._asyncDispatch(event)
);
}
/**
* Stops watching the file system and closes the watcher resource.
*/
close() {
// Check if the resource id is not undefined.
if (!this.#id) {
throw new Error(`FsWatcher is not attached to a resource ID.`);
}
binding.unwatch(this.#id);
this._asyncDispatch(null);
this.#id = undefined;
}
_asyncDispatch(value) {
if (this.#pullQueue.length === 0) {
this.#pushQueue.push(value);
return;
}
const promise = this.#pullQueue.shift();
const action = value instanceof Error ? promise.reject : promise.resolve;
action(value);
}
/**
* Returns a promise which is fulfilled when a new FS event is available.
*
* @ignore
* @returns {Promise<object>}
*/
_next() {
// Check if the resource id is not undefined.
if (!this.#id) {
throw new Error(`FsWatcher is not attached to a resource ID.`);
}
// No available event yet.
if (this.#pushQueue.length === 0) {
const { promise, promiseExt } = makeDeferredPromise();
this.#pullQueue.push(promiseExt);
return promise;
}
const value = this.#pushQueue.shift();
const action = value instanceof Error ? Promise.reject : Promise.resolve;
return action.call(Promise, value);
}
/**
* The FsWatcher should be async iterable.
* @ignore
*/
async *[Symbol.asyncIterator](signal) {
// Close watcher on stream pipeline errors.
if (signal) signal.on('uncaughtStreamException', () => this.close());
let data;
while ((data = await this._next())) {
if (!data) break;
yield data;
}
}
}
/**
* Asynchronously opens a file.
*
* @param {String} path - The file path of the file to be opened.
* @param {String} mode - The mode in which the file is to be opened.
* @returns {Promise<File>} An instance of the File class.
*/
export async function open(path, mode = 'r') {
// Check the data argument type.
if (typeof path !== 'string') {
throw new TypeError('The "path" argument must be of type string.');
}
// Create a new file instance.
const file = new File(path, mode);
await file.open();
return file;
}
/**
* Synchronously opens a file.
*
* @param {String} path - The file path of the file to be opened.
* @param {String} mode - The mode in which the file is to be opened.
* @returns {File} An instance of the File class.
*/
export function openSync(path, mode = 'r') {
// Check the data argument type.
if (typeof path !== 'string') {
throw new TypeError('The "path" argument must be of type string.');
}
// Create a new file instance.
const file = new File(path, mode);
file.openSync();
return file;
}
/**
* Reads asynchronously the entire contents of a file.
*
* @param {String} path - The path of the file to be read.
* @param {(String|Object)} [options] - The options to control the file read operation.
* @param {String} [options.encoding] - The encoding to be used for reading the file.
* @returns {Promise<(String|Uint8Array)>} - The contents of the file.
*/
export async function readFile(path, options = {}) {
// Create a new file instance.
const file = new File(path, 'r');
await file.open();
// Allocate a buffer to store all the bytes from the file.
const stat = await file.stat();
const data = new Uint8Array(stat.size);
let bytesRead = 0;
// Note: Since the file object is async iterable will read the entire content
// of the file using the for-await loop.
for await (let chunk of file) {
data.set(chunk, bytesRead);
bytesRead += chunk.length;
}
await file.close();
// Decode given an encoder.
const encoding = typeof options === 'string' ? options : options.encoding;
if (encoding) {
return new TextDecoder(encoding).decode(data);
}
return data;
}
/**
* Reads synchronously the entire contents of a file.
*
* @param {String} path - The path of the file to be read.
* @param {(String|Object)} [options] - The options to control the file read operation.
* @param {String} [options.encoding] - The encoding to be used for reading the file.
* @returns {(String|Uint8Array)} - The contents of the file.
*/
export function readFileSync(path, options = {}) {
// Create a new file instance.
const file = new File(path, 'r');
file.openSync();
// Allocate a buffer to store all the bytes from the file.
const stat = file.statSync();
const data = new Uint8Array(stat.size);
let bytesRead = 0;
// Note: Since the file object is iterable will read the entire content
// of the file using the for-of loop.
for (let chunk of file) {
data.set(chunk, bytesRead);
bytesRead += chunk.length;
}
file.closeSync();
// Decode given an encoder.
const encoding = typeof options === 'string' ? options : options.encoding;
if (encoding) {
return new TextDecoder(encoding).decode(data);
}
return data;
}
function toUint8Array(data, encoding) {
if (!(data instanceof Uint8Array)) {
return new TextEncoder(encoding).encode(data);
}
return data;
}
/**
* Writes asynchronously contents to a file.
*
* @param {String} path - The path of the file where the data is to be written.
* @param {(String|Uint8Array)} data - The data to write to the file.
* @param {(String|Object)} [options] - The options to control the file write operation.
* @param {String} [options.encoding] - The encoding to be used for writing the file.
* @returns {Promise}
*/
export async function writeFile(path, data, options = {}) {
// Check the data argument type.
if (!(data instanceof Uint8Array) && typeof data !== 'string') {
throw new TypeError(
`The "data" argument must be of type string or Uint8Array.`
);
}
let encoding = typeof options === 'string' ? options : options.encoding;
// Default to utf-8 encoding.
if (!encoding) encoding = 'utf-8';
const data_u8 = toUint8Array(data, encoding);
// Create a file instance.
const file = new File(path, 'w');
// Open file, write data, and close it.
await file.open();
await file.write(data_u8);
await file.close();
}
/**
* Writes synchronously contents to a file.
*
* @param {String} path - The path of the file where the data is to be written.
* @param {String|Uint8Array} data - The data to write to the file.
* @param {String|Object} [options] - The options to control the file write operation.
* @param {String} [options.encoding] - The encoding to be used for writing the file.
*/
export function writeFileSync(path, data, options = {}) {
// Check the data argument type.
if (!(data instanceof Uint8Array) && typeof data !== 'string') {
throw new TypeError(
`The "data" argument must be of type string or Uint8Array.`
);
}
let encoding = typeof options === 'string' ? options : options.encoding;
// Default to utf-8 encoding.
if (!encoding) encoding = 'utf-8';
const data_u8 = toUint8Array(data, encoding);
// Create a file instance.
const file = new File(path, 'w');
// Open file, write data, and close it.
file.openSync();
file.writeSync(data_u8);
file.closeSync();
}
/**
* Copies asynchronously a file from the source path to destination path.
*
* @param {String} source - The path of the source file to be copied.
* @param {String} destination - The path where the source file will be copied to.
* @returns {Promise}
*/
export async function copyFile(source, destination) {
// Check the source argument type.
if (typeof source !== 'string') {
throw new TypeError(`The "source" argument must be of type string.`);
}
// Check the destination argument type.
if (typeof destination !== 'string') {
throw new TypeError(`The "destination" argument must be of type string.`);
}
return writeFile(destination, await readFile(source));
}
/**
* Copies synchronously a file from the source path to destination path.
*
* @param {String} source - The path of the source file to be copied.
* @param {String} destination - The path where the source file will be copied to.
*/
export function copyFileSync(source, destination) {
// Check the source argument type.
if (typeof source !== 'string') {
throw new TypeError(`The "source" argument must be of type string.`);
}
// Check the destination argument type.
if (typeof destination !== 'string') {
throw new TypeError(`The "destination" argument must be of type string.`);
}
return writeFileSync(destination, readFileSync(source));
}
/**
* Retrieves asynchronously statistics for the file.
*
* @param {String} path - The path of the file for which statistics are to be retrieved.
* @returns {Promise<FileStats>} An object containing the statistics of the file.
*/
export async function stat(path) {
// Check the path argument type.
if (typeof path !== 'string') {
throw new TypeError('The "path" argument must be of type string.');
}
// Get path statistics.
const stats = await binding.stat(path);
return stats;
}
/**
* Retrieves synchronously statistics for the file.
*
* @param {String} path - The path of the file for which statistics are to be retrieved.
* @returns {Object} An object containing the statistics of the file.
*/
export function statSync(path) {
// Check the path argument type.
if (typeof path !== 'string') {
throw new TypeError('The "path" argument must be of type string.');
}
// Get path statistics.
const stats = binding.statSync(path);
return stats;
}
/**
* Creates directories asynchronously.
*
* @param {String} path - The path where the new directory will be created.
* @param {Object} [options] - Configuration options for directory creation.
* @param {boolean} [options.recursive] - Will create all directories necessary to reach the specified path.
* @returns {Promise}
*/
export async function mkdir(path, options = {}) {
// Check the path argument type.
if (typeof path !== 'string') {
throw new TypeError('The "path" argument must be of type string.');
}
await binding.mkdir(path, options?.recursive || false);
}
/**
* Creates directories synchronously.
*
* @param {String} path - The path where the new directory will be created.
* @param {Object} [options] - Configuration options for directory creation.
* @param {boolean} [options.recursive] - Will create all directories necessary to reach the specified path.
*/
export function mkdirSync(path, options = {}) {
// Check the path argument type.
if (typeof path !== 'string') {
throw new TypeError('The "path" argument must be of type string.');
}
binding.mkdirSync(path, options?.recursive || false);
}
/**
* Removes empty directories asynchronously.
*
* @param {String} path - The path of the directory to be removed.
* @param {Object} [options] - Configuration options for directory removal.
* @param {number} [options.maxRetries=0] - The maximum number of times to retry the removal in case of failure.
* @param {number} [options.retryDelay=100] - The delay in milliseconds between retries.
* @returns {Promise}
*/
export async function rmdir(path, options = {}, __retries = 0) {
// Check the path argument type.
if (typeof path !== 'string') {
throw new TypeError('The "path" argument must be of type string.');
}
const maxRetries = options?.maxRetries || 0;
const retryDelay = options?.retryDelay || 100;
try {
// Try removing the empty directory.
await binding.rmdir(path);
} catch (err) {
// If we maxed out the retries accept failure.
if (__retries >= maxRetries) throw err;
// Note: Wrapping the setTimeout into a promise is necessary otherwise the
// outer rmdir call won't wait for all the inner ones.
await new Promise((success, failure) => {
// Back-off and retry later.
setTimeout(
() =>
rmdir(path, options, __retries + 1)
.then(success)
.catch(failure),
retryDelay
);
});
}
}
/**
* Removes empty directories synchronously.
*
* @param {String} path - The path of the directory to be removed.
* @param {Object} [options] - Configuration options for directory removal.
* @param {number} [options.maxRetries=0] - The maximum number of times to retry the removal in case of failure.
* @param {number} [options.retryDelay=100] - The delay in milliseconds between retries.
*/
export function rmdirSync(path, options = {}, __retries = 0) {
// Check the path argument type.
if (typeof path !== 'string') {
throw new TypeError('The "path" argument must be of type string.');
}
const maxRetries = options?.maxRetries || 0;
const retryDelay = options?.retryDelay || 100;
try {
// Try removing the empty directory.
binding.rmdirSync(path);
} catch (err) {
// If we maxed out the retries accept failure.
if (__retries >= maxRetries) throw err;
// Back-off and retry later.
setTimeout(() => {
rmdirSync(path, options, __retries + 1);
}, retryDelay);
}
}
/**
* Reads asynchronously the contents of a directory.
*
* @param {String} path - The path of the directory whose contents are to be read.
* @returns {Promise<String[]>} An array of strings, where each string is the name of a file or directory.
*/
export async function readdir(path) {
// Check the path argument type.
if (typeof path !== 'string') {
throw new TypeError('The "path" argument must be of type string.');
}
return binding.readdir(path);
}
/**
* Reads the contents of a directory.
*
* @param {String} path - The path of the directory whose contents are to be read.
* @returns {String[]} An array of strings, where each string is the name of a file or directory.
*/
export function readdirSync(path) {
// Check the path argument type.
if (typeof path !== 'string') {
throw new TypeError('The "path" argument must be of type string.');
}
return binding.readdirSync(path);
}
/**
* Removes files and directories asynchronously.
*
* @param {String} path - The path of the file or directory to be removed.
* @param {Object} [options] - Configuration options for the removal operation.
* @param {boolean} [options.recursive=false] - The method will remove the directory and all its contents recursively.
* @param {number} [options.maxRetries=0] - The maximum number of times to retry the removal in case of failure.
* @param {number} [options.retryDelay=100] - The delay in milliseconds between retries.
* @returns {Promise}
*/
export async function rm(path, options = {}, __retries = 0) {
// Check the path argument type.
if (typeof path !== 'string') {
throw new TypeError('The "path" argument must be of type string.');
}
// Set default options if not specified.
const recursive = options?.recursive || false;
const maxRetries = options?.maxRetries || 0;
const retryDelay = options?.retryDelay || 100;
// Get path's statistics.
const pathStat = await stat(path);
if (pathStat.isDirectory && !recursive) {
await rmdir(path, options);
return;
}
try {
// Try removing file or directory.
await binding.rm(path);
} catch (err) {
// If we maxed out the retries accept failure.
if (__retries >= maxRetries) throw err;
// Note: Wrapping the setTimeout into a promise is necessary otherwise the
// outer rm call won't wait for all the inner ones.
await new Promise((success, failure) => {
// Back-off and retry later.
setTimeout(
() =>
rm(path, options, __retries + 1)
.then(success)
.catch(failure),
retryDelay
);
});
}
}
/**
* Removes files and directories synchronously.
*
* @param {String} path - The path of the file or directory to be removed.
* @param {Object} [options] - Configuration options for the removal operation.
* @param {boolean} [options.recursive=false] - The method will remove the directory and all its contents recursively.
* @param {number} [options.maxRetries=0] - The maximum number of times to retry the removal in case of failure.
* @param {number} [options.retryDelay=100] - The delay in milliseconds between retries.
*/
export function rmSync(path, options = {}, __retries = 0) {
// Check the path argument type.
if (typeof path !== 'string') {
throw new TypeError('The "path" argument must be of type string.');
}
// Set default options if not specified.
const recursive = options?.recursive || false;
const maxRetries = options?.maxRetries || 0;
const retryDelay = options?.retryDelay || 100;
// Get path's statistics.
const pathStat = statSync(path);
if (pathStat.isDirectory && !recursive) {
rmdirSync(path, options);
return;
}
try {
// Try removing file or directory.
binding.rmSync(path);
} catch (err) {
// If we maxed out the retries accept failure.
if (__retries >= maxRetries) throw err;
// Back-off and retry later.
setTimeout(() => {
rmSync(path, options, __retries + 1);
}, retryDelay);
}
}
/**
* Renames oldPath to newPath asynchronously.
*
* @param {String} from - The current path of the file or directory to be renamed.
* @param {String} to - The new path for the file or directory.
* @returns {Promise}
*/
export async function rename(from, to) {
// Check the `from` argument type.
if (typeof from !== 'string') {
throw new TypeError('The "from" argument must be of type string.');
}
// Check the `to` argument type.
if (typeof to !== 'string') {
throw new TypeError('The "to" argument must be of type string.');
}
return binding.rename(from, to);
}
/**
* Renames oldPath to newPath synchronously.
*
* @param {String} from - The current path of the file or directory to be renamed.
* @param {String} to - The new path for the file or directory.
*/
export function renameSync(from, to) {
// Check the `from` argument type.
if (typeof from !== 'string') {
throw new TypeError('The "from" argument must be of type string.');
}
// Check the `to` argument type.
if (typeof to !== 'string') {
throw new TypeError('The "to" argument must be of type string.');
}
binding.renameSync(from, to);
}
/**
* Returns an async iterator that watches for changes over a path.
*
* @param {String} path - The path to be monitored for changes.
* @param {Object} [options] - Configuration options for the file watcher.
* @param {boolean} [options.recursive] - Will monitor the specified directory and its subdirectories for changes.
* @returns {FsWatcher} An instance of the `FsWatcher` class.
*/
export function watch(path, options = {}) {
// Check the `path` argument type.
if (typeof path !== 'string') {
throw new TypeError('The "path" argument must be of type string.');
}
return new FsWatcher(path, options.recursive);
}
/**
* Returns a new readable IO stream.
*
* @param {String} path - The path of the file to be read.
* @param {(String|Object)} [options] - Configuration options for the stream.
* @param {String} [options.encoding] - The encoding to be used for reading the file.
* @returns {AsyncGeneratorFunction} - An instance of a `Readable` stream.
*/
export function createReadStream(path, options = {}) {
// Use passed encoding or default to UTF-8.
const encoding = typeof options === 'string' ? options : options.encoding;
const textDecoder = new TextDecoder(encoding || 'utf-8');
// Create the async generator.
return async function* readStream(signal) {
// Open file and handle broken pipeline clean-ups.
const file = await open(path, options?.mode);
signal.on('uncaughtStreamException', () => file.close());
// Start consuming chunks.
for await (const chunk of file) {
yield encoding ? textDecoder.decode(chunk) : chunk;
}
file.close();
};
}
/**
* Returns a new writable IO stream.
*
* @param {String} path - The path of the file where data will be written.
* @param {(String|Object)} [options] - Configuration options for the stream.
* @param {String} [options.encoding] - The encoding to be used for writing data to the file.
* @returns {Object} An instance of a `Writable` stream.
*/
export function createWriteStream(path, options = {}) {
// We want to open the file the moment the stream becomes active.
let _handle;
const encoding = typeof options === 'string' ? options : options.encoding;
// Every object with `.write()` and `.end()` is a writable stream.
return {
async write(chunk) {
if (!_handle) _handle = await open(path, options.mode || 'w');
const data = toUint8Array(chunk, encoding || 'utf-8');
await _handle.write(data);
},
async end(chunk) {
if (chunk) await this.write(chunk);
if (_handle) await _handle.close();
},
};
}
export default {
File,
open,
openSync,
readFile,
readFileSync,
writeFile,
writeFileSync,
copyFile,
copyFileSync,
stat,
statSync,
mkdir,
mkdirSync,
rmdir,
rmdirSync,
readdir,
readdirSync,
rm,
rmSync,
rename,
renameSync,
watch,
createReadStream,
createWriteStream,
};