kjelsrud.dev/node_modules/vfile/lib/index.js

521 lines
14 KiB
JavaScript
Raw Normal View History

2023-07-19 21:31:30 +02:00
/**
* @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<string, unknown> & {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: <https://github.com/DefinitelyTyped/DefinitelyTyped/blob/90a4ec8/types/node/buffer.d.ts#L170>
*
* @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<string> | 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:
* <https://github.com/mozilla/source-map/blob/58819f0/source-map.d.ts#L15-L23>.
* @property {number} version
* Which version of the source map spec this map is following.
* @property {Array<string>} sources
* An array of URLs to the original source files.
* @property {Array<string>} 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<string> | 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<string, unknown>} ReporterSettings
* Configuration for reporters.
*/
/**
* @template {ReporterSettings} Settings
* Options type.
* @callback Reporter
* Type for a reporter.
* @param {Array<VFile>} 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: `{}`).
*
* Its 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<VFileMessage>}
*/
this.messages = []
/**
* List of filepaths the file moved between.
*
* The first is the original path and the last is the current path.
*
* @type {Array<string>}
*/
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 doesnt 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 theres 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 theres 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 its 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)
}