AK-21/Graphite-Industrial-Intelligence
0
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 