328 lines
8.7 KiB
JavaScript
328 lines
8.7 KiB
JavaScript
/**
|
||
* @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: <https://github.com/DefinitelyTyped/DefinitelyTyped/blob/1761eec/types/node/buffer.d.ts#L223>.
|
||
*
|
||
* @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<VFile>}
|
||
*
|
||
* @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<VFile> | 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<VFile>}
|
||
*
|
||
* @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
|
||
)
|
||
}
|