/** * @typedef {import('vfile').VFileOptions} Options * @typedef {import('vfile').VFileValue} Value */ /** * @typedef {'ascii' | 'base64' | 'base64url' | 'binary' | 'hex' | 'latin1' | 'ucs-2' | 'ucs2' | 'utf-8' | 'utf16le' | 'utf8'} BufferEncoding * Encodings supported by the buffer class. * * This is a copy of the types from Node, copied to prevent Node globals from * being needed. * Copied from: . * * @typedef ReadOptions * Configuration for `fs.readFile`. * @property {BufferEncoding | null | undefined} [encoding] * Encoding to read file as, will turn `file.value` into a string if passed. * @property {string | undefined} [flag] * File system flags to use. * * @typedef WriteOptions * Configuration for `fs.writeFile`. * @property {BufferEncoding | null | undefined} [encoding] * Encoding to write file as. * @property {string | undefined} [flag] * File system flags to use. * @property {number | string | undefined} [mode] * File mode (permission and sticky bits) if the file was newly created. * * @typedef {URL | Value} Path * URL to file or path to file. * * > 👉 **Note**: `Value` is used here because it’s a smarter `Buffer` * @typedef {Options | Path | VFile} Compatible * URL to file, path to file, options for file, or actual file. */ /** * @callback Callback * Callback called after reading or writing a file. * @param {NodeJS.ErrnoException | undefined} error * Error when reading or writing was not successful. * @param {VFile | null | undefined} file * File when reading or writing was successful. * @returns {undefined} * Nothing. * * @callback Resolve * @param {VFile} result * File. * @returns {void} * Nothing (note: has to be `void` for TSs `Promise` interface). * * @callback Reject * @param {NodeJS.ErrnoException} error * Error. * @param {VFile | undefined} [result] * File. * @returns {void} * Nothing (note: has to be `void` for TSs `Promise` interface). */ import fs from 'node:fs' import path from 'node:path' import {VFile} from 'vfile' // To do: next major: remove `toVFile`, only accept `VFile`s, // do not return anything. /** * Create a virtual file and read it in, async. * * @overload * @param {Compatible} description * @param {BufferEncoding | ReadOptions | null | undefined} options * @param {Callback} callback * @returns {undefined} * * @overload * @param {Compatible} description * @param {Callback} callback * @returns {undefined} * * @overload * @param {Compatible} description * @param {BufferEncoding | ReadOptions | null | undefined} [options] * @returns {Promise} * * @param {Compatible} description * Path to file, file options, or file itself. * @param {BufferEncoding | Callback | ReadOptions | null | undefined} [options] * Encoding to use or Node.JS read options. * @param {Callback | null | undefined} [callback] * Callback called when done. * @returns {Promise | undefined} * Nothing when a callback is given, otherwise promise that resolves to given * file or new file. */ export function read(description, options, callback) { const file = toVFile(description) if (!callback && typeof options === 'function') { callback = options options = undefined } if (!callback) { return new Promise(executor) } executor(resolve, callback) /** * @param {VFile} result */ function resolve(result) { // @ts-expect-error: `callback` always defined. callback(undefined, result) } /** * @param {Resolve} resolve * @param {Reject} reject * @returns {void} * Nothing (note: has to be `void` for TSs `Promise` interface). */ function executor(resolve, reject) { /** @type {string} */ let fp try { fp = path.resolve(file.cwd, file.path) } catch (error) { const exception = /** @type {NodeJS.ErrnoException} */ (error) return reject(exception) } // @ts-expect-error: `options` is not a callback. fs.readFile(fp, options, done) /** * @param {NodeJS.ErrnoException | undefined} error * @param {Value} result */ function done(error, result) { if (error) { reject(error) } else { file.value = result resolve(file) } } } } /** * Create a virtual file and read it in, synchronously. * * @param {Compatible} description * Path to file, file options, or file itself. * @param {BufferEncoding | ReadOptions | null | undefined} [options] * Encoding to use or Node.JS read options. * @returns {VFile} * Given file or new file. */ export function readSync(description, options) { const file = toVFile(description) file.value = fs.readFileSync(path.resolve(file.cwd, file.path), options) return file } /** * Create a virtual file from a description. * * This is like `VFile`, but it accepts a file path instead of file contents. * * If `options` is a string, URL, or buffer, it’s used as the path. * Otherwise, if it’s a file, that’s returned instead. * Otherwise, the options are passed through to `new VFile()`. * * @param {Compatible | null | undefined} [description] * Path to file, file options, or file itself. * @returns {VFile} * Given file or new file. */ export function toVFile(description) { if (typeof description === 'string' || description instanceof URL) { description = {path: description} } else if (isUint8Array(description)) { description = {path: new TextDecoder().decode(description)} } return looksLikeAVFile(description) ? description : new VFile(description) } /** * Create a virtual file and write it, async. * * @overload * @param {Compatible} description * @param {BufferEncoding | WriteOptions | null | undefined} options * @param {Callback} callback * @returns {undefined} * * @overload * @param {Compatible} description * @param {Callback} callback * @returns {undefined} * * @overload * @param {Compatible} description * @param {BufferEncoding | WriteOptions | null | undefined} [options] * @returns {Promise} * * @param {Compatible} description * Path to file, file options, or file itself. * @param {BufferEncoding | Callback | WriteOptions | null | undefined} [options] * Encoding to use or Node.JS write options. * @param {Callback | null | undefined} [callback] * Callback called when done. * @returns * Nothing when a callback is given, otherwise promise that resolves to given * file or new file. */ export function write(description, options, callback) { const file = toVFile(description) // Weird, right? Otherwise `fs` doesn’t accept it. if (!callback && typeof options === 'function') { callback = options options = undefined } if (!callback) { return new Promise(executor) } executor(resolve, callback) /** * @param {VFile} result */ function resolve(result) { // @ts-expect-error: `callback` always defined. callback(undefined, result) } /** * @param {Resolve} resolve * @param {Reject} reject */ function executor(resolve, reject) { /** @type {string} */ let fp try { fp = path.resolve(file.cwd, file.path) } catch (error) { const exception = /** @type {NodeJS.ErrnoException} */ (error) return reject(exception) } // @ts-expect-error: `options` is not a callback. fs.writeFile(fp, file.value || '', options || undefined, done) /** * @param {NodeJS.ErrnoException | undefined} error */ function done(error) { if (error) { reject(error) } else { resolve(file) } } } } /** * Create a virtual file and write it, synchronously. * * @param {Compatible} description * Path to file, file options, or file itself. * @param {BufferEncoding | WriteOptions | null | undefined} [options] * Encoding to use or Node.JS write options. * @returns {VFile} * Given file or new file. */ export function writeSync(description, options) { const file = toVFile(description) fs.writeFileSync(path.resolve(file.cwd, file.path), file.value || '', options) return file } /** * Check if something looks like a vfile. * * @param {Compatible | null | undefined} value * Value. * @returns {value is VFile} * Whether `value` looks like a `VFile`. */ function looksLikeAVFile(value) { return Boolean( value && typeof value === 'object' && 'message' in value && 'messages' in value ) } /** * Check whether `value` is an `Uint8Array`. * * @param {unknown} value * thing. * @returns {value is Uint8Array} * Whether `value` is an `Uint8Array`. */ function isUint8Array(value) { return Boolean( value && typeof value === 'object' && 'byteLength' in value && 'byteOffset' in value ) }