CoolFace
Apppublic

AK-21/Graphite-Industrial-Intelligence

sourceHugging Faceupdated 3mo agoView on Hugging Face
0likes
index.js644 linesDownload Raw Back to lib
1/**2 * @import {Node, Point, Position} from 'unist'3 * @import {Options as MessageOptions} from 'vfile-message'4 * @import {Compatible, Data, Map, Options, Value} from 'vfile'5 */6 7/**8 * @typedef {object & {type: string, position?: Position | undefined}} NodeLike9 */10 11import {VFileMessage} from 'vfile-message'12import {minpath} from '#minpath'13import {minproc} from '#minproc'14import {urlToPath, isUrl} from '#minurl'15 16/**17 * Order of setting (least specific to most), we need this because otherwise18 * `{stem: 'a', path: '~/b.js'}` would throw, as a path is needed before a19 * stem can be set.20 */21const order = /** @type {const} */ ([22  'history',23  'path',24  'basename',25  'stem',26  'extname',27  'dirname'28])29 30export class VFile {31  /**32   * Create a new virtual file.33   *34   * `options` is treated as:35   *36   * *   `string` or `Uint8Array` — `{value: options}`37   * *   `URL` — `{path: options}`38   * *   `VFile` — shallow copies its data over to the new file39   * *   `object` — all fields are shallow copied over to the new file40   *41   * Path related fields are set in the following order (least specific to42   * most specific): `history`, `path`, `basename`, `stem`, `extname`,43   * `dirname`.44   *45   * You cannot set `dirname` or `extname` without setting either `history`,46   * `path`, `basename`, or `stem` too.47   *48   * @param {Compatible | null | undefined} [value]49   *   File value.50   * @returns51   *   New instance.52   */53  constructor(value) {54    /** @type {Options | VFile} */55    let options56 57    if (!value) {58      options = {}59    } else if (isUrl(value)) {60      options = {path: value}61    } else if (typeof value === 'string' || isUint8Array(value)) {62      options = {value}63    } else {64      options = value65    }66 67    /* eslint-disable no-unused-expressions */68 69    /**70     * Base of `path` (default: `process.cwd()` or `'/'` in browsers).71     *72     * @type {string}73     */74    // Prevent calling `cwd` (which could be expensive) if it’s not needed;75    // the empty string will be overridden in the next block.76    this.cwd = 'cwd' in options ? '' : minproc.cwd()77 78    /**79     * Place to store custom info (default: `{}`).80     *81     * It’s OK to store custom data directly on the file but moving it to82     * `data` is recommended.83     *84     * @type {Data}85     */86    this.data = {}87 88    /**89     * List of file paths the file moved between.90     *91     * The first is the original path and the last is the current path.92     *93     * @type {Array<string>}94     */95    this.history = []96 97    /**98     * List of messages associated with the file.99     *100     * @type {Array<VFileMessage>}101     */102    this.messages = []103 104    /**105     * Raw value.106     *107     * @type {Value}108     */109    this.value110 111    // The below are non-standard, they are “well-known”.112    // As in, used in several tools.113    /**114     * Source map.115     *116     * This type is equivalent to the `RawSourceMap` type from the `source-map`117     * module.118     *119     * @type {Map | null | undefined}120     */121    this.map122 123    /**124     * Custom, non-string, compiled, representation.125     *126     * This is used by unified to store non-string results.127     * One example is when turning markdown into React nodes.128     *129     * @type {unknown}130     */131    this.result132 133    /**134     * Whether a file was saved to disk.135     *136     * This is used by vfile reporters.137     *138     * @type {boolean}139     */140    this.stored141    /* eslint-enable no-unused-expressions */142 143    // Set path related properties in the correct order.144    let index = -1145 146    while (++index < order.length) {147      const field = order[index]148 149      // Note: we specifically use `in` instead of `hasOwnProperty` to accept150      // `vfile`s too.151      if (152        field in options &&153        options[field] !== undefined &&154        options[field] !== null155      ) {156        // @ts-expect-error: TS doesn’t understand basic reality.157        this[field] = field === 'history' ? [...options[field]] : options[field]158      }159    }160 161    /** @type {string} */162    let field163 164    // Set non-path related properties.165    for (field in options) {166      // @ts-expect-error: fine to set other things.167      if (!order.includes(field)) {168        // @ts-expect-error: fine to set other things.169        this[field] = options[field]170      }171    }172  }173 174  /**175   * Get the basename (including extname) (example: `'index.min.js'`).176   *177   * @returns {string | undefined}178   *   Basename.179   */180  get basename() {181    return typeof this.path === 'string'182      ? minpath.basename(this.path)183      : undefined184  }185 186  /**187   * Set basename (including extname) (`'index.min.js'`).188   *189   * Cannot contain path separators (`'/'` on unix, macOS, and browsers, `'\'`190   * on windows).191   * Cannot be nullified (use `file.path = file.dirname` instead).192   *193   * @param {string} basename194   *   Basename.195   * @returns {undefined}196   *   Nothing.197   */198  set basename(basename) {199    assertNonEmpty(basename, 'basename')200    assertPart(basename, 'basename')201    this.path = minpath.join(this.dirname || '', basename)202  }203 204  /**205   * Get the parent path (example: `'~'`).206   *207   * @returns {string | undefined}208   *   Dirname.209   */210  get dirname() {211    return typeof this.path === 'string'212      ? minpath.dirname(this.path)213      : undefined214  }215 216  /**217   * Set the parent path (example: `'~'`).218   *219   * Cannot be set if there’s no `path` yet.220   *221   * @param {string | undefined} dirname222   *   Dirname.223   * @returns {undefined}224   *   Nothing.225   */226  set dirname(dirname) {227    assertPath(this.basename, 'dirname')228    this.path = minpath.join(dirname || '', this.basename)229  }230 231  /**232   * Get the extname (including dot) (example: `'.js'`).233   *234   * @returns {string | undefined}235   *   Extname.236   */237  get extname() {238    return typeof this.path === 'string'239      ? minpath.extname(this.path)240      : undefined241  }242 243  /**244   * Set the extname (including dot) (example: `'.js'`).245   *246   * Cannot contain path separators (`'/'` on unix, macOS, and browsers, `'\'`247   * on windows).248   * Cannot be set if there’s no `path` yet.249   *250   * @param {string | undefined} extname251   *   Extname.252   * @returns {undefined}253   *   Nothing.254   */255  set extname(extname) {256    assertPart(extname, 'extname')257    assertPath(this.dirname, 'extname')258 259    if (extname) {260      if (extname.codePointAt(0) !== 46 /* `.` */) {261        throw new Error('`extname` must start with `.`')262      }263 264      if (extname.includes('.', 1)) {265        throw new Error('`extname` cannot contain multiple dots')266      }267    }268 269    this.path = minpath.join(this.dirname, this.stem + (extname || ''))270  }271 272  /**273   * Get the full path (example: `'~/index.min.js'`).274   *275   * @returns {string}276   *   Path.277   */278  get path() {279    return this.history[this.history.length - 1]280  }281 282  /**283   * Set the full path (example: `'~/index.min.js'`).284   *285   * Cannot be nullified.286   * You can set a file URL (a `URL` object with a `file:` protocol) which will287   * be turned into a path with `url.fileURLToPath`.288   *289   * @param {URL | string} path290   *   Path.291   * @returns {undefined}292   *   Nothing.293   */294  set path(path) {295    if (isUrl(path)) {296      path = urlToPath(path)297    }298 299    assertNonEmpty(path, 'path')300 301    if (this.path !== path) {302      this.history.push(path)303    }304  }305 306  /**307   * Get the stem (basename w/o extname) (example: `'index.min'`).308   *309   * @returns {string | undefined}310   *   Stem.311   */312  get stem() {313    return typeof this.path === 'string'314      ? minpath.basename(this.path, this.extname)315      : undefined316  }317 318  /**319   * Set the stem (basename w/o extname) (example: `'index.min'`).320   *321   * Cannot contain path separators (`'/'` on unix, macOS, and browsers, `'\'`322   * on windows).323   * Cannot be nullified (use `file.path = file.dirname` instead).324   *325   * @param {string} stem326   *   Stem.327   * @returns {undefined}328   *   Nothing.329   */330  set stem(stem) {331    assertNonEmpty(stem, 'stem')332    assertPart(stem, 'stem')333    this.path = minpath.join(this.dirname || '', stem + (this.extname || ''))334  }335 336  // Normal prototypal methods.337  /**338   * Create a fatal message for `reason` associated with the file.339   *340   * The `fatal` field of the message is set to `true` (error; file not usable)341   * and the `file` field is set to the current file path.342   * The message is added to the `messages` field on `file`.343   *344   * > 🪦 **Note**: also has obsolete signatures.345   *346   * @overload347   * @param {string} reason348   * @param {MessageOptions | null | undefined} [options]349   * @returns {never}350   *351   * @overload352   * @param {string} reason353   * @param {Node | NodeLike | null | undefined} parent354   * @param {string | null | undefined} [origin]355   * @returns {never}356   *357   * @overload358   * @param {string} reason359   * @param {Point | Position | null | undefined} place360   * @param {string | null | undefined} [origin]361   * @returns {never}362   *363   * @overload364   * @param {string} reason365   * @param {string | null | undefined} [origin]366   * @returns {never}367   *368   * @overload369   * @param {Error | VFileMessage} cause370   * @param {Node | NodeLike | null | undefined} parent371   * @param {string | null | undefined} [origin]372   * @returns {never}373   *374   * @overload375   * @param {Error | VFileMessage} cause376   * @param {Point | Position | null | undefined} place377   * @param {string | null | undefined} [origin]378   * @returns {never}379   *380   * @overload381   * @param {Error | VFileMessage} cause382   * @param {string | null | undefined} [origin]383   * @returns {never}384   *385   * @param {Error | VFileMessage | string} causeOrReason386   *   Reason for message, should use markdown.387   * @param {Node | NodeLike | MessageOptions | Point | Position | string | null | undefined} [optionsOrParentOrPlace]388   *   Configuration (optional).389   * @param {string | null | undefined} [origin]390   *   Place in code where the message originates (example:391   *   `'my-package:my-rule'` or `'my-rule'`).392   * @returns {never}393   *   Never.394   * @throws {VFileMessage}395   *   Message.396   */397  fail(causeOrReason, optionsOrParentOrPlace, origin) {398    // @ts-expect-error: the overloads are fine.399    const message = this.message(causeOrReason, optionsOrParentOrPlace, origin)400 401    message.fatal = true402 403    throw message404  }405 406  /**407   * Create an info message for `reason` associated with the file.408   *409   * The `fatal` field of the message is set to `undefined` (info; change410   * likely not needed) and the `file` field is set to the current file path.411   * The message is added to the `messages` field on `file`.412   *413   * > 🪦 **Note**: also has obsolete signatures.414   *415   * @overload416   * @param {string} reason417   * @param {MessageOptions | null | undefined} [options]418   * @returns {VFileMessage}419   *420   * @overload421   * @param {string} reason422   * @param {Node | NodeLike | null | undefined} parent423   * @param {string | null | undefined} [origin]424   * @returns {VFileMessage}425   *426   * @overload427   * @param {string} reason428   * @param {Point | Position | null | undefined} place429   * @param {string | null | undefined} [origin]430   * @returns {VFileMessage}431   *432   * @overload433   * @param {string} reason434   * @param {string | null | undefined} [origin]435   * @returns {VFileMessage}436   *437   * @overload438   * @param {Error | VFileMessage} cause439   * @param {Node | NodeLike | null | undefined} parent440   * @param {string | null | undefined} [origin]441   * @returns {VFileMessage}442   *443   * @overload444   * @param {Error | VFileMessage} cause445   * @param {Point | Position | null | undefined} place446   * @param {string | null | undefined} [origin]447   * @returns {VFileMessage}448   *449   * @overload450   * @param {Error | VFileMessage} cause451   * @param {string | null | undefined} [origin]452   * @returns {VFileMessage}453   *454   * @param {Error | VFileMessage | string} causeOrReason455   *   Reason for message, should use markdown.456   * @param {Node | NodeLike | MessageOptions | Point | Position | string | null | undefined} [optionsOrParentOrPlace]457   *   Configuration (optional).458   * @param {string | null | undefined} [origin]459   *   Place in code where the message originates (example:460   *   `'my-package:my-rule'` or `'my-rule'`).461   * @returns {VFileMessage}462   *   Message.463   */464  info(causeOrReason, optionsOrParentOrPlace, origin) {465    // @ts-expect-error: the overloads are fine.466    const message = this.message(causeOrReason, optionsOrParentOrPlace, origin)467 468    message.fatal = undefined469 470    return message471  }472 473  /**474   * Create a message for `reason` associated with the file.475   *476   * The `fatal` field of the message is set to `false` (warning; change may be477   * needed) and the `file` field is set to the current file path.478   * The message is added to the `messages` field on `file`.479   *480   * > 🪦 **Note**: also has obsolete signatures.481   *482   * @overload483   * @param {string} reason484   * @param {MessageOptions | null | undefined} [options]485   * @returns {VFileMessage}486   *487   * @overload488   * @param {string} reason489   * @param {Node | NodeLike | null | undefined} parent490   * @param {string | null | undefined} [origin]491   * @returns {VFileMessage}492   *493   * @overload494   * @param {string} reason495   * @param {Point | Position | null | undefined} place496   * @param {string | null | undefined} [origin]497   * @returns {VFileMessage}498   *499   * @overload500   * @param {string} reason501   * @param {string | null | undefined} [origin]502   * @returns {VFileMessage}503   *504   * @overload505   * @param {Error | VFileMessage} cause506   * @param {Node | NodeLike | null | undefined} parent507   * @param {string | null | undefined} [origin]508   * @returns {VFileMessage}509   *510   * @overload511   * @param {Error | VFileMessage} cause512   * @param {Point | Position | null | undefined} place513   * @param {string | null | undefined} [origin]514   * @returns {VFileMessage}515   *516   * @overload517   * @param {Error | VFileMessage} cause518   * @param {string | null | undefined} [origin]519   * @returns {VFileMessage}520   *521   * @param {Error | VFileMessage | string} causeOrReason522   *   Reason for message, should use markdown.523   * @param {Node | NodeLike | MessageOptions | Point | Position | string | null | undefined} [optionsOrParentOrPlace]524   *   Configuration (optional).525   * @param {string | null | undefined} [origin]526   *   Place in code where the message originates (example:527   *   `'my-package:my-rule'` or `'my-rule'`).528   * @returns {VFileMessage}529   *   Message.530   */531  message(causeOrReason, optionsOrParentOrPlace, origin) {532    const message = new VFileMessage(533      // @ts-expect-error: the overloads are fine.534      causeOrReason,535      optionsOrParentOrPlace,536      origin537    )538 539    if (this.path) {540      message.name = this.path + ':' + message.name541      message.file = this.path542    }543 544    message.fatal = false545 546    this.messages.push(message)547 548    return message549  }550 551  /**552   * Serialize the file.553   *554   * > **Note**: which encodings are supported depends on the engine.555   * > For info on Node.js, see:556   * > <https://nodejs.org/api/util.html#whatwg-supported-encodings>.557   *558   * @param {string | null | undefined} [encoding='utf8']559   *   Character encoding to understand `value` as when it’s a `Uint8Array`560   *   (default: `'utf-8'`).561   * @returns {string}562   *   Serialized file.563   */564  toString(encoding) {565    if (this.value === undefined) {566      return ''567    }568 569    if (typeof this.value === 'string') {570      return this.value571    }572 573    const decoder = new TextDecoder(encoding || undefined)574    return decoder.decode(this.value)575  }576}577 578/**579 * Assert that `part` is not a path (as in, does not contain `path.sep`).580 *581 * @param {string | null | undefined} part582 *   File path part.583 * @param {string} name584 *   Part name.585 * @returns {undefined}586 *   Nothing.587 */588function assertPart(part, name) {589  if (part && part.includes(minpath.sep)) {590    throw new Error(591      '`' + name + '` cannot be a path: did not expect `' + minpath.sep + '`'592    )593  }594}595 596/**597 * Assert that `part` is not empty.598 *599 * @param {string | undefined} part600 *   Thing.601 * @param {string} name602 *   Part name.603 * @returns {asserts part is string}604 *   Nothing.605 */606function assertNonEmpty(part, name) {607  if (!part) {608    throw new Error('`' + name + '` cannot be empty')609  }610}611 612/**613 * Assert `path` exists.614 *615 * @param {string | undefined} path616 *   Path.617 * @param {string} name618 *   Dependency name.619 * @returns {asserts path is string}620 *   Nothing.621 */622function assertPath(path, name) {623  if (!path) {624    throw new Error('Setting `' + name + '` requires `path` to be set too')625  }626}627 628/**629 * Assert `value` is an `Uint8Array`.630 *631 * @param {unknown} value632 *   thing.633 * @returns {value is Uint8Array}634 *   Whether `value` is an `Uint8Array`.635 */636function isUint8Array(value) {637  return Boolean(638    value &&639      typeof value === 'object' &&640      'byteLength' in value &&641      'byteOffset' in value642  )643}644