/** * @typedef {import('unist').Node} Node * @typedef {import('unist').Position} Position * @typedef {import('unist').Point} Point * @typedef {import('./minurl.shared.js').URL} URL * @typedef {import('../index.js').Data} Data * @typedef {import('../index.js').Value} Value */ /** * @typedef {Record & {type: string, position?: Position | undefined}} NodeLike * * @typedef {'ascii' | 'utf8' | 'utf-8' | 'utf16le' | 'ucs2' | 'ucs-2' | 'base64' | 'base64url' | 'latin1' | 'binary' | 'hex'} 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 {Options | URL | Value | VFile} Compatible * Things that can be passed to the constructor. * * @typedef VFileCoreOptions * Set multiple values. * @property {Value | null | undefined} [value] * Set `value`. * @property {string | null | undefined} [cwd] * Set `cwd`. * @property {Array | null | undefined} [history] * Set `history`. * @property {URL | string | null | undefined} [path] * Set `path`. * @property {string | null | undefined} [basename] * Set `basename`. * @property {string | null | undefined} [stem] * Set `stem`. * @property {string | null | undefined} [extname] * Set `extname`. * @property {string | null | undefined} [dirname] * Set `dirname`. * @property {Data | null | undefined} [data] * Set `data`. * * @typedef Map * Raw source map. * * See: * . * @property {number} version * Which version of the source map spec this map is following. * @property {Array} sources * An array of URLs to the original source files. * @property {Array} names * An array of identifiers which can be referenced by individual mappings. * @property {string | undefined} [sourceRoot] * The URL root from which all sources are relative. * @property {Array | undefined} [sourcesContent] * An array of contents of the original source files. * @property {string} mappings * A string of base64 VLQs which contain the actual mappings. * @property {string} file * The generated file this source map is associated with. * * @typedef {{[key: string]: unknown} & VFileCoreOptions} Options * Configuration. * * A bunch of keys that will be shallow copied over to the new file. * * @typedef {Record} ReporterSettings * Configuration for reporters. */ /** * @template {ReporterSettings} Settings * Options type. * @callback Reporter * Type for a reporter. * @param {Array} files * Files to report. * @param {Settings} options * Configuration. * @returns {string} * Report. */ import bufferLike from 'is-buffer' import {VFileMessage} from 'vfile-message' import {path} from './minpath.js' import {proc} from './minproc.js' import {urlToPath, isUrl} from './minurl.js' /** * Order of setting (least specific to most), we need this because otherwise * `{stem: 'a', path: '~/b.js'}` would throw, as a path is needed before a * stem can be set. * * @type {Array<'basename' | 'dirname' | 'extname' | 'history' | 'path' | 'stem'>} */ const order = ['history', 'path', 'basename', 'stem', 'extname', 'dirname'] export class VFile { /** * Create a new virtual file. * * `options` is treated as: * * * `string` or `Buffer` — `{value: options}` * * `URL` — `{path: options}` * * `VFile` — shallow copies its data over to the new file * * `object` — all fields are shallow copied over to the new file * * Path related fields are set in the following order (least specific to * most specific): `history`, `path`, `basename`, `stem`, `extname`, * `dirname`. * * You cannot set `dirname` or `extname` without setting either `history`, * `path`, `basename`, or `stem` too. * * @param {Compatible | null | undefined} [value] * File value. * @returns * New instance. */ constructor(value) { /** @type {Options | VFile} */ let options if (!value) { options = {} } else if (typeof value === 'string' || buffer(value)) { options = {value} } else if (isUrl(value)) { options = {path: value} } else { options = value } /** * Place to store custom information (default: `{}`). * * It’s OK to store custom data directly on the file but moving it to * `data` is recommended. * * @type {Data} */ this.data = {} /** * List of messages associated with the file. * * @type {Array} */ this.messages = [] /** * List of filepaths the file moved between. * * The first is the original path and the last is the current path. * * @type {Array} */ this.history = [] /** * Base of `path` (default: `process.cwd()` or `'/'` in browsers). * * @type {string} */ this.cwd = proc.cwd() /* eslint-disable no-unused-expressions */ /** * Raw value. * * @type {Value} */ this.value // The below are non-standard, they are “well-known”. // As in, used in several tools. /** * Whether a file was saved to disk. * * This is used by vfile reporters. * * @type {boolean} */ this.stored /** * Custom, non-string, compiled, representation. * * This is used by unified to store non-string results. * One example is when turning markdown into React nodes. * * @type {unknown} */ this.result /** * Source map. * * This type is equivalent to the `RawSourceMap` type from the `source-map` * module. * * @type {Map | null | undefined} */ this.map /* eslint-enable no-unused-expressions */ // Set path related properties in the correct order. let index = -1 while (++index < order.length) { const prop = order[index] // Note: we specifically use `in` instead of `hasOwnProperty` to accept // `vfile`s too. if ( prop in options && options[prop] !== undefined && options[prop] !== null ) { // @ts-expect-error: TS doesn’t understand basic reality. this[prop] = prop === 'history' ? [...options[prop]] : options[prop] } } /** @type {string} */ let prop // Set non-path related properties. for (prop in options) { // @ts-expect-error: fine to set other things. if (!order.includes(prop)) { // @ts-expect-error: fine to set other things. this[prop] = options[prop] } } } /** * Get the full path (example: `'~/index.min.js'`). * * @returns {string} */ get path() { return this.history[this.history.length - 1] } /** * Set the full path (example: `'~/index.min.js'`). * * Cannot be nullified. * You can set a file URL (a `URL` object with a `file:` protocol) which will * be turned into a path with `url.fileURLToPath`. * * @param {string | URL} path */ set path(path) { if (isUrl(path)) { path = urlToPath(path) } assertNonEmpty(path, 'path') if (this.path !== path) { this.history.push(path) } } /** * Get the parent path (example: `'~'`). */ get dirname() { return typeof this.path === 'string' ? path.dirname(this.path) : undefined } /** * Set the parent path (example: `'~'`). * * Cannot be set if there’s no `path` yet. */ set dirname(dirname) { assertPath(this.basename, 'dirname') this.path = path.join(dirname || '', this.basename) } /** * Get the basename (including extname) (example: `'index.min.js'`). */ get basename() { return typeof this.path === 'string' ? path.basename(this.path) : undefined } /** * Set basename (including extname) (`'index.min.js'`). * * Cannot contain path separators (`'/'` on unix, macOS, and browsers, `'\'` * on windows). * Cannot be nullified (use `file.path = file.dirname` instead). */ set basename(basename) { assertNonEmpty(basename, 'basename') assertPart(basename, 'basename') this.path = path.join(this.dirname || '', basename) } /** * Get the extname (including dot) (example: `'.js'`). */ get extname() { return typeof this.path === 'string' ? path.extname(this.path) : undefined } /** * Set the extname (including dot) (example: `'.js'`). * * Cannot contain path separators (`'/'` on unix, macOS, and browsers, `'\'` * on windows). * Cannot be set if there’s no `path` yet. */ set extname(extname) { assertPart(extname, 'extname') assertPath(this.dirname, 'extname') if (extname) { if (extname.charCodeAt(0) !== 46 /* `.` */) { throw new Error('`extname` must start with `.`') } if (extname.includes('.', 1)) { throw new Error('`extname` cannot contain multiple dots') } } this.path = path.join(this.dirname, this.stem + (extname || '')) } /** * Get the stem (basename w/o extname) (example: `'index.min'`). */ get stem() { return typeof this.path === 'string' ? path.basename(this.path, this.extname) : undefined } /** * Set the stem (basename w/o extname) (example: `'index.min'`). * * Cannot contain path separators (`'/'` on unix, macOS, and browsers, `'\'` * on windows). * Cannot be nullified (use `file.path = file.dirname` instead). */ set stem(stem) { assertNonEmpty(stem, 'stem') assertPart(stem, 'stem') this.path = path.join(this.dirname || '', stem + (this.extname || '')) } /** * Serialize the file. * * @param {BufferEncoding | null | undefined} [encoding='utf8'] * Character encoding to understand `value` as when it’s a `Buffer` * (default: `'utf8'`). * @returns {string} * Serialized file. */ toString(encoding) { return (this.value || '').toString(encoding || undefined) } /** * Create a warning message associated with the file. * * Its `fatal` is set to `false` and `file` is set to the current file path. * Its added to `file.messages`. * * @param {string | Error | VFileMessage} reason * Reason for message, uses the stack and message of the error if given. * @param {Node | NodeLike | Position | Point | null | undefined} [place] * Place in file where the message occurred. * @param {string | null | undefined} [origin] * Place in code where the message originates (example: * `'my-package:my-rule'` or `'my-rule'`). * @returns {VFileMessage} * Message. */ message(reason, place, origin) { const message = new VFileMessage(reason, place, origin) if (this.path) { message.name = this.path + ':' + message.name message.file = this.path } message.fatal = false this.messages.push(message) return message } /** * Create an info message associated with the file. * * Its `fatal` is set to `null` and `file` is set to the current file path. * Its added to `file.messages`. * * @param {string | Error | VFileMessage} reason * Reason for message, uses the stack and message of the error if given. * @param {Node | NodeLike | Position | Point | null | undefined} [place] * Place in file where the message occurred. * @param {string | null | undefined} [origin] * Place in code where the message originates (example: * `'my-package:my-rule'` or `'my-rule'`). * @returns {VFileMessage} * Message. */ info(reason, place, origin) { const message = this.message(reason, place, origin) message.fatal = null return message } /** * Create a fatal error associated with the file. * * Its `fatal` is set to `true` and `file` is set to the current file path. * Its added to `file.messages`. * * > 👉 **Note**: a fatal error means that a file is no longer processable. * * @param {string | Error | VFileMessage} reason * Reason for message, uses the stack and message of the error if given. * @param {Node | NodeLike | Position | Point | null | undefined} [place] * Place in file where the message occurred. * @param {string | null | undefined} [origin] * Place in code where the message originates (example: * `'my-package:my-rule'` or `'my-rule'`). * @returns {never} * Message. * @throws {VFileMessage} * Message. */ fail(reason, place, origin) { const message = this.message(reason, place, origin) message.fatal = true throw message } } /** * Assert that `part` is not a path (as in, does not contain `path.sep`). * * @param {string | null | undefined} part * File path part. * @param {string} name * Part name. * @returns {void} * Nothing. */ function assertPart(part, name) { if (part && part.includes(path.sep)) { throw new Error( '`' + name + '` cannot be a path: did not expect `' + path.sep + '`' ) } } /** * Assert that `part` is not empty. * * @param {string | undefined} part * Thing. * @param {string} name * Part name. * @returns {asserts part is string} * Nothing. */ function assertNonEmpty(part, name) { if (!part) { throw new Error('`' + name + '` cannot be empty') } } /** * Assert `path` exists. * * @param {string | undefined} path * Path. * @param {string} name * Dependency name. * @returns {asserts path is string} * Nothing. */ function assertPath(path, name) { if (!path) { throw new Error('Setting `' + name + '` requires `path` to be set too') } } /** * Assert `value` is a buffer. * * @param {unknown} value * thing. * @returns {value is Buffer} * Whether `value` is a Node.js buffer. */ function buffer(value) { return bufferLike(value) }