BOC v1.0: RS256 auth, Ledger integration, Prometheus metrics, CI/CD, backup
This commit is contained in:
+1226
@@ -0,0 +1,1226 @@
|
||||
/*
|
||||
MIT License http://www.opensource.org/licenses/mit-license.php
|
||||
Author Tobias Koppers @sokra
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const { AsyncSeriesBailHook, AsyncSeriesHook, SyncHook } = require("tapable");
|
||||
const createInnerContext = require("./createInnerContext");
|
||||
const { parseIdentifier } = require("./util/identifier");
|
||||
const {
|
||||
PathType,
|
||||
createCachedBasename,
|
||||
createCachedDirname,
|
||||
createCachedJoin,
|
||||
getType,
|
||||
normalize,
|
||||
} = require("./util/path");
|
||||
|
||||
/* eslint-disable jsdoc/check-alignment */
|
||||
// TODO in the next major release use only `Promise.withResolvers()`
|
||||
const _withResolvers =
|
||||
// eslint-disable-next-line n/no-unsupported-features/es-syntax
|
||||
Promise.withResolvers
|
||||
? /**
|
||||
* @param {Resolver} self resolver
|
||||
* @param {Context} context context information object
|
||||
* @param {string} path context path
|
||||
* @param {string} request request string
|
||||
* @param {ResolveContext} resolveContext resolve context
|
||||
* @returns {Promise<string | false>} result
|
||||
*/
|
||||
(self, context, path, request, resolveContext) => {
|
||||
// eslint-disable-next-line n/no-unsupported-features/es-syntax
|
||||
const { promise, resolve, reject } = Promise.withResolvers();
|
||||
self.resolve(context, path, request, resolveContext, (err, res) => {
|
||||
if (err) reject(err);
|
||||
else resolve(/** @type {string | false} */ (res));
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
: /**
|
||||
* @param {Resolver} self resolver
|
||||
* @param {Context} context context information object
|
||||
* @param {string} path context path
|
||||
* @param {string} request request string
|
||||
* @param {ResolveContext} resolveContext resolve context
|
||||
* @returns {Promise<string | false>} result
|
||||
*/
|
||||
(self, context, path, request, resolveContext) =>
|
||||
new Promise((resolve, reject) => {
|
||||
self.resolve(context, path, request, resolveContext, (err, res) => {
|
||||
if (err) reject(err);
|
||||
else resolve(/** @type {string | false} */ (res));
|
||||
});
|
||||
});
|
||||
/* eslint-enable jsdoc/check-alignment */
|
||||
|
||||
/** @typedef {import("./AliasUtils").AliasOption} AliasOption */
|
||||
/** @typedef {import("./util/path").CachedJoin} CachedJoin */
|
||||
/** @typedef {import("./util/path").CachedDirname} CachedDirname */
|
||||
/** @typedef {import("./util/path").CachedBasename} CachedBasename */
|
||||
|
||||
/**
|
||||
* @typedef {object} JoinCacheEntry
|
||||
* @property {CachedJoin["fn"]} fn cached join function
|
||||
* @property {CachedJoin["cache"]} cache the underlying cache map
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} DirnameCacheEntry
|
||||
* @property {CachedDirname["fn"]} fn cached dirname function
|
||||
* @property {CachedDirname["cache"]} cache the underlying cache map
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} BasenameCacheEntry
|
||||
* @property {CachedBasename["fn"]} fn cached dirname function
|
||||
* @property {CachedBasename["cache"]} cache the underlying cache map
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} PathCacheFunctions
|
||||
* @property {JoinCacheEntry} join cached join
|
||||
* @property {DirnameCacheEntry} dirname cached dirname
|
||||
* @property {BasenameCacheEntry} basename cached basename
|
||||
*/
|
||||
|
||||
/** @type {WeakMap<FileSystem, PathCacheFunctions>} */
|
||||
const _pathCacheByFs = new WeakMap();
|
||||
|
||||
const HASH_ESCAPE_RE = /#/g;
|
||||
|
||||
/** @typedef {import("./ResolverFactory").ResolveOptions} ResolveOptions */
|
||||
|
||||
/**
|
||||
* @typedef {object} KnownContext
|
||||
* @property {string[]=} environments environments
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line jsdoc/reject-any-type
|
||||
/** @typedef {KnownContext & Record<any, any>} Context */
|
||||
|
||||
/** @typedef {Error & { details?: string }} ErrorWithDetail */
|
||||
|
||||
/** @typedef {(err: ErrorWithDetail | null, res?: string | false, req?: ResolveRequest) => void} ResolveCallback */
|
||||
|
||||
/**
|
||||
* @typedef {object} PossibleFileSystemError
|
||||
* @property {string=} code code
|
||||
* @property {number=} errno number
|
||||
* @property {string=} path path
|
||||
* @property {string=} syscall syscall
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @callback FileSystemCallback
|
||||
* @param {PossibleFileSystemError & Error | null} err
|
||||
* @param {T=} result
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {string | Buffer | URL} PathLike
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {PathLike | number} PathOrFileDescriptor
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} ObjectEncodingOptions
|
||||
* @property {BufferEncoding | null | undefined=} encoding encoding
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {ObjectEncodingOptions | BufferEncoding | undefined | null} EncodingOption
|
||||
*/
|
||||
|
||||
/** @typedef {(err: NodeJS.ErrnoException | null, result?: string) => void} StringCallback */
|
||||
/** @typedef {(err: NodeJS.ErrnoException | null, result?: Buffer) => void} BufferCallback */
|
||||
/** @typedef {(err: NodeJS.ErrnoException | null, result?: (string | Buffer)) => void} StringOrBufferCallback */
|
||||
/** @typedef {(err: NodeJS.ErrnoException | null, result?: IStats) => void} StatsCallback */
|
||||
/** @typedef {(err: NodeJS.ErrnoException | null, result?: IBigIntStats) => void} BigIntStatsCallback */
|
||||
/** @typedef {(err: NodeJS.ErrnoException | null, result?: (IStats | IBigIntStats)) => void} StatsOrBigIntStatsCallback */
|
||||
/** @typedef {(err: NodeJS.ErrnoException | Error | null, result?: JsonObject) => void} ReadJsonCallback */
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {object} IStatsBase
|
||||
* @property {() => boolean} isFile is file
|
||||
* @property {() => boolean} isDirectory is directory
|
||||
* @property {() => boolean} isBlockDevice is block device
|
||||
* @property {() => boolean} isCharacterDevice is character device
|
||||
* @property {() => boolean} isSymbolicLink is symbolic link
|
||||
* @property {() => boolean} isFIFO is FIFO
|
||||
* @property {() => boolean} isSocket is socket
|
||||
* @property {T} dev dev
|
||||
* @property {T} ino ino
|
||||
* @property {T} mode mode
|
||||
* @property {T} nlink nlink
|
||||
* @property {T} uid uid
|
||||
* @property {T} gid gid
|
||||
* @property {T} rdev rdev
|
||||
* @property {T} size size
|
||||
* @property {T} blksize blksize
|
||||
* @property {T} blocks blocks
|
||||
* @property {T} atimeMs atime ms
|
||||
* @property {T} mtimeMs mtime ms
|
||||
* @property {T} ctimeMs ctime ms
|
||||
* @property {T} birthtimeMs birthtime ms
|
||||
* @property {Date} atime atime
|
||||
* @property {Date} mtime mtime
|
||||
* @property {Date} ctime ctime
|
||||
* @property {Date} birthtime birthtime
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {IStatsBase<number>} IStats
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {IStatsBase<bigint> & { atimeNs: bigint, mtimeNs: bigint, ctimeNs: bigint, birthtimeNs: bigint }} IBigIntStats
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {string | Buffer} [T=string]
|
||||
* @typedef {object} Dirent
|
||||
* @property {() => boolean} isFile true when is file, otherwise false
|
||||
* @property {() => boolean} isDirectory true when is directory, otherwise false
|
||||
* @property {() => boolean} isBlockDevice true when is block device, otherwise false
|
||||
* @property {() => boolean} isCharacterDevice true when is character device, otherwise false
|
||||
* @property {() => boolean} isSymbolicLink true when is symbolic link, otherwise false
|
||||
* @property {() => boolean} isFIFO true when is FIFO, otherwise false
|
||||
* @property {() => boolean} isSocket true when is socket, otherwise false
|
||||
* @property {T} name name
|
||||
* @property {string} parentPath path
|
||||
* @property {string=} path path
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} StatOptions
|
||||
* @property {(boolean | undefined)=} bigint need bigint values
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} StatSyncOptions
|
||||
* @property {(boolean | undefined)=} bigint need bigint values
|
||||
* @property {(boolean | undefined)=} throwIfNoEntry throw if no entry
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* (path: PathOrFileDescriptor, options: ({ encoding?: null | undefined, flag?: string | undefined } & import("events").Abortable) | undefined | null, callback: BufferCallback): void,
|
||||
* (path: PathOrFileDescriptor, options: ({ encoding: BufferEncoding, flag?: string | undefined } & import("events").Abortable) | BufferEncoding, callback: StringCallback): void,
|
||||
* (path: PathOrFileDescriptor, options: (ObjectEncodingOptions & { flag?: string | undefined } & import("events").Abortable) | BufferEncoding | undefined | null, callback: StringOrBufferCallback): void,
|
||||
* (path: PathOrFileDescriptor, callback: BufferCallback): void,
|
||||
* }} ReadFile
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {"buffer" | { encoding: "buffer" }} BufferEncodingOption
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* (path: PathOrFileDescriptor, options?: { encoding?: null | undefined, flag?: string | undefined } | null): Buffer,
|
||||
* (path: PathOrFileDescriptor, options: { encoding: BufferEncoding, flag?: string | undefined } | BufferEncoding): string,
|
||||
* (path: PathOrFileDescriptor, options?: (ObjectEncodingOptions & { flag?: string | undefined }) | BufferEncoding | null): string | Buffer,
|
||||
* }} ReadFileSync
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* (path: PathLike, options: { encoding: BufferEncoding | null, withFileTypes?: false | undefined, recursive?: boolean | undefined } | BufferEncoding | undefined | null, callback: (err: NodeJS.ErrnoException | null, files?: string[]) => void): void,
|
||||
* (path: PathLike, options: { encoding: "buffer", withFileTypes?: false | undefined, recursive?: boolean | undefined } | "buffer", callback: (err: NodeJS.ErrnoException | null, files?: Buffer[]) => void): void,
|
||||
* (path: PathLike, options: (ObjectEncodingOptions & { withFileTypes?: false | undefined, recursive?: boolean | undefined }) | BufferEncoding | undefined | null, callback: (err: NodeJS.ErrnoException | null, files?: string[] | Buffer[]) => void): void,
|
||||
* (path: PathLike, callback: (err: NodeJS.ErrnoException | null, files?: string[]) => void): void,
|
||||
* (path: PathLike, options: ObjectEncodingOptions & { withFileTypes: true, recursive?: boolean | undefined }, callback: (err: NodeJS.ErrnoException | null, files?: Dirent<string>[]) => void): void,
|
||||
* (path: PathLike, options: { encoding: "buffer", withFileTypes: true, recursive?: boolean | undefined }, callback: (err: NodeJS.ErrnoException | null, files: Dirent<Buffer>[]) => void): void,
|
||||
* }} Readdir
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* (path: PathLike, options?: { encoding: BufferEncoding | null, withFileTypes?: false | undefined, recursive?: boolean | undefined } | BufferEncoding | null): string[],
|
||||
* (path: PathLike, options: { encoding: "buffer", withFileTypes?: false | undefined, recursive?: boolean | undefined } | "buffer"): Buffer[],
|
||||
* (path: PathLike, options?: (ObjectEncodingOptions & { withFileTypes?: false | undefined, recursive?: boolean | undefined }) | BufferEncoding | null): string[] | Buffer[],
|
||||
* (path: PathLike, options: ObjectEncodingOptions & { withFileTypes: true, recursive?: boolean | undefined }): Dirent[],
|
||||
* (path: PathLike, options: { encoding: "buffer", withFileTypes: true, recursive?: boolean | undefined }): Dirent<Buffer>[],
|
||||
* }} ReaddirSync
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {(pathOrFileDescription: PathOrFileDescriptor, callback: ReadJsonCallback) => void} ReadJson
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {(pathOrFileDescription: PathOrFileDescriptor) => JsonObject} ReadJsonSync
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* (path: PathLike, options: EncodingOption, callback: StringCallback): void,
|
||||
* (path: PathLike, options: BufferEncodingOption, callback: BufferCallback): void,
|
||||
* (path: PathLike, options: EncodingOption, callback: StringOrBufferCallback): void,
|
||||
* (path: PathLike, callback: StringCallback): void,
|
||||
* }} Readlink
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* (path: PathLike, options?: EncodingOption): string,
|
||||
* (path: PathLike, options: BufferEncodingOption): Buffer,
|
||||
* (path: PathLike, options?: EncodingOption): string | Buffer,
|
||||
* }} ReadlinkSync
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* (path: PathLike, callback: StatsCallback): void,
|
||||
* (path: PathLike, options: (StatOptions & { bigint?: false | undefined }) | undefined, callback: StatsCallback): void,
|
||||
* (path: PathLike, options: StatOptions & { bigint: true }, callback: BigIntStatsCallback): void,
|
||||
* (path: PathLike, options: StatOptions | undefined, callback: StatsOrBigIntStatsCallback): void,
|
||||
* }} LStat
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* (path: PathLike, options?: undefined): IStats,
|
||||
* (path: PathLike, options?: StatSyncOptions & { bigint?: false | undefined, throwIfNoEntry: false }): IStats | undefined,
|
||||
* (path: PathLike, options: StatSyncOptions & { bigint: true, throwIfNoEntry: false }): IBigIntStats | undefined,
|
||||
* (path: PathLike, options?: StatSyncOptions & { bigint?: false | undefined }): IStats,
|
||||
* (path: PathLike, options: StatSyncOptions & { bigint: true }): IBigIntStats,
|
||||
* (path: PathLike, options: StatSyncOptions & { bigint: boolean, throwIfNoEntry?: false | undefined }): IStats | IBigIntStats,
|
||||
* (path: PathLike, options?: StatSyncOptions): IStats | IBigIntStats | undefined,
|
||||
* }} LStatSync
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* (path: PathLike, callback: StatsCallback): void,
|
||||
* (path: PathLike, options: (StatOptions & { bigint?: false | undefined }) | undefined, callback: StatsCallback): void,
|
||||
* (path: PathLike, options: StatOptions & { bigint: true }, callback: BigIntStatsCallback): void,
|
||||
* (path: PathLike, options: StatOptions | undefined, callback: StatsOrBigIntStatsCallback): void,
|
||||
* }} Stat
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* (path: PathLike, options?: undefined): IStats,
|
||||
* (path: PathLike, options?: StatSyncOptions & { bigint?: false | undefined, throwIfNoEntry: false }): IStats | undefined,
|
||||
* (path: PathLike, options: StatSyncOptions & { bigint: true, throwIfNoEntry: false }): IBigIntStats | undefined,
|
||||
* (path: PathLike, options?: StatSyncOptions & { bigint?: false | undefined }): IStats,
|
||||
* (path: PathLike, options: StatSyncOptions & { bigint: true }): IBigIntStats,
|
||||
* (path: PathLike, options: StatSyncOptions & { bigint: boolean, throwIfNoEntry?: false | undefined }): IStats | IBigIntStats,
|
||||
* (path: PathLike, options?: StatSyncOptions): IStats | IBigIntStats | undefined,
|
||||
* }} StatSync
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* (path: PathLike, options: EncodingOption, callback: StringCallback): void,
|
||||
* (path: PathLike, options: BufferEncodingOption, callback: BufferCallback): void,
|
||||
* (path: PathLike, options: EncodingOption, callback: StringOrBufferCallback): void,
|
||||
* (path: PathLike, callback: StringCallback): void,
|
||||
* }} RealPath
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* (path: PathLike, options?: EncodingOption): string,
|
||||
* (path: PathLike, options: BufferEncodingOption): Buffer,
|
||||
* (path: PathLike, options?: EncodingOption): string | Buffer,
|
||||
* }} RealPathSync
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} FileSystem
|
||||
* @property {ReadFile} readFile read file method
|
||||
* @property {Readdir} readdir readdir method
|
||||
* @property {ReadJson=} readJson read json method
|
||||
* @property {Readlink} readlink read link method
|
||||
* @property {LStat=} lstat lstat method
|
||||
* @property {Stat} stat stat method
|
||||
* @property {RealPath=} realpath realpath method
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} SyncFileSystem
|
||||
* @property {ReadFileSync} readFileSync read file sync method
|
||||
* @property {ReaddirSync} readdirSync read dir sync method
|
||||
* @property {ReadJsonSync=} readJsonSync read json sync method
|
||||
* @property {ReadlinkSync} readlinkSync read link sync method
|
||||
* @property {LStatSync=} lstatSync lstat sync method
|
||||
* @property {StatSync} statSync stat sync method
|
||||
* @property {RealPathSync=} realpathSync real path sync method
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} ParsedIdentifier
|
||||
* @property {string} request request
|
||||
* @property {string} query query
|
||||
* @property {string} fragment fragment
|
||||
* @property {boolean} directory is directory
|
||||
* @property {boolean} module is module
|
||||
* @property {boolean} file is file
|
||||
* @property {boolean} internal is internal
|
||||
*/
|
||||
|
||||
/** @typedef {string | number | boolean | null} JsonPrimitive */
|
||||
/** @typedef {JsonValue[]} JsonArray */
|
||||
/** @typedef {JsonPrimitive | JsonObject | JsonArray} JsonValue */
|
||||
/** @typedef {{ [Key in string]?: JsonValue | undefined }} JsonObject */
|
||||
|
||||
/**
|
||||
* @typedef {object} TsconfigPathsMap
|
||||
* @property {TsconfigPathsData} main main tsconfig paths data
|
||||
* @property {string} mainContext main tsconfig base URL (absolute path)
|
||||
* @property {{ [baseUrl: string]: TsconfigPathsData }} refs referenced tsconfig paths data mapped by baseUrl
|
||||
* @property {{ [context: string]: TsconfigPathsData }} allContexts all contexts (main + refs) for quick lookup
|
||||
* @property {string[]} contextList precomputed `Object.keys(allContexts)` — read-only; used on the `_selectPathsDataForContext` hot path
|
||||
* @property {Set<string>} fileDependencies file dependencies
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} TsconfigPathsData
|
||||
* @property {import("./AliasUtils").CompiledAliasOptions} alias tsconfig file data
|
||||
* @property {string[]} modules tsconfig file data
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} BaseResolveRequest
|
||||
* @property {string | false} path path
|
||||
* @property {Context=} context content
|
||||
* @property {string=} descriptionFilePath description file path
|
||||
* @property {string=} descriptionFileRoot description file root
|
||||
* @property {JsonObject=} descriptionFileData description file data
|
||||
* @property {TsconfigPathsMap | null | undefined=} tsconfigPathsMap tsconfig paths map
|
||||
* @property {string=} relativePath relative path
|
||||
* @property {boolean=} ignoreSymlinks true when need to ignore symlinks, otherwise false
|
||||
* @property {boolean=} fullySpecified true when full specified, otherwise false
|
||||
* @property {string=} __innerRequest inner request for internal usage
|
||||
* @property {string=} __innerRequest_request inner request for internal usage
|
||||
* @property {string=} __innerRequest_relativePath inner relative path for internal usage
|
||||
*/
|
||||
|
||||
/** @typedef {BaseResolveRequest & Partial<ParsedIdentifier>} ResolveRequest */
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {{ add: (item: T) => void }} WriteOnlySet
|
||||
*/
|
||||
|
||||
/** @typedef {(request: ResolveRequest) => void} ResolveContextYield */
|
||||
|
||||
/**
|
||||
* Singly-linked stack entry that also exposes a Set-like API
|
||||
* (`has`, `size`, iteration). Each `doResolve` call prepends a new
|
||||
* `StackEntry` that points at the previous tip via `.parent`, so pushing
|
||||
* is O(1) in time and memory. Recursion detection walks the linked list
|
||||
* (O(n)) but the stack is typically shallow, so this is cheaper overall
|
||||
* than cloning a `Set` per call.
|
||||
*/
|
||||
class StackEntry {
|
||||
/**
|
||||
* @param {ResolveStepHook} hook hook
|
||||
* @param {ResolveRequest} request request
|
||||
* @param {StackEntry=} parent previous tip
|
||||
* @param {Set<string>=} preSeeded entries pre-seeded via the legacy `Set<string>` API
|
||||
*/
|
||||
constructor(hook, request, parent, preSeeded) {
|
||||
this.name = hook.name;
|
||||
this.path = request.path;
|
||||
this.request = request.request || "";
|
||||
this.query = request.query || "";
|
||||
this.fragment = request.fragment || "";
|
||||
this.directory = Boolean(request.directory);
|
||||
this.module = Boolean(request.module);
|
||||
/** @type {StackEntry | undefined} */
|
||||
this.parent = parent;
|
||||
/**
|
||||
* Strings seeded by callers that still pass `stack: new Set([...])`.
|
||||
* Propagated through the chain so deeper `doResolve` calls still see
|
||||
* them during recursion checks. `undefined` in the common case so
|
||||
* there is no extra work on the hot path.
|
||||
* @type {Set<string> | undefined}
|
||||
*/
|
||||
this.preSeeded = preSeeded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk the linked list looking for an entry with the same request shape.
|
||||
* Set-compatible: callers that used `stack.has(entry)` keep working.
|
||||
*
|
||||
* NOTE: kept monomorphic on purpose. An earlier draft accepted a string
|
||||
* query too (so pre-5.21 plugins keeping their own `Set<string>` of
|
||||
* seen entries could probe the live stack with the formatted form),
|
||||
* but adding the second shape regressed `doResolve`'s heap profile by
|
||||
* ~1 MiB / 200 resolves on stack-churn — V8 keeps a polymorphic
|
||||
* call-site state for `parent.has(stackEntry)` once `has` has two
|
||||
* argument shapes. Plugins that need string membership can reach for
|
||||
* `[...stack].find(e => e.includes(formattedString))` via the
|
||||
* `String`-method proxies on `StackEntry` instead.
|
||||
* @param {StackEntry} query entry to look for
|
||||
* @returns {boolean} whether the stack already contains an equivalent entry
|
||||
*/
|
||||
has(query) {
|
||||
/** @type {StackEntry | undefined} */
|
||||
let node = this;
|
||||
while (node) {
|
||||
if (
|
||||
node.name === query.name &&
|
||||
node.path === query.path &&
|
||||
node.request === query.request &&
|
||||
node.query === query.query &&
|
||||
node.fragment === query.fragment &&
|
||||
node.directory === query.directory &&
|
||||
node.module === query.module
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
node = node.parent;
|
||||
}
|
||||
return this.preSeeded !== undefined && this.preSeeded.has(query.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of entries on the stack (oldest-to-newest length).
|
||||
* @returns {number} size
|
||||
*/
|
||||
get size() {
|
||||
let count = this.preSeeded ? this.preSeeded.size : 0;
|
||||
/** @type {StackEntry | undefined} */
|
||||
let node = this;
|
||||
while (node) {
|
||||
count++;
|
||||
node = node.parent;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate entries from oldest (root) to newest (tip), matching how a
|
||||
* `Set` that was populated in insertion order would iterate. Pre-seeded
|
||||
* legacy `Set<string>` entries come first so error-message output stays
|
||||
* ordered oldest-to-newest.
|
||||
*
|
||||
* Yields each entry as its formatted `toString()` form. Plugins written
|
||||
* against the pre-5.21 `Set<string>` shape — e.g.
|
||||
* `[...resolveContext.stack].find(a => a.includes("module:"))` — keep
|
||||
* working unchanged because each yielded value is a plain string with
|
||||
* all of `String.prototype` available natively. Resolves that never
|
||||
* iterate the stack pay nothing; iteration costs one `toString()`
|
||||
* allocation per stack frame.
|
||||
* @returns {IterableIterator<string>} iterator
|
||||
*/
|
||||
*[Symbol.iterator]() {
|
||||
if (this.preSeeded !== undefined) {
|
||||
for (const entry of this.preSeeded) yield entry;
|
||||
}
|
||||
/** @type {StackEntry[]} */
|
||||
const entries = [];
|
||||
/** @type {StackEntry | undefined} */
|
||||
let node = this;
|
||||
while (node) {
|
||||
entries.push(node);
|
||||
node = node.parent;
|
||||
}
|
||||
for (let i = entries.length - 1; i >= 0; i--) yield entries[i].toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Human-readable form used in recursion error messages, logs, and the
|
||||
* iterator above. Not memoized: caching would require an extra slot on
|
||||
* every `StackEntry`, which costs heap even on resolves that never look
|
||||
* at the formatted form.
|
||||
* @returns {string} formatted entry
|
||||
*/
|
||||
toString() {
|
||||
return `${this.name}: (${this.path}) ${this.request}${this.query}${
|
||||
this.fragment
|
||||
}${this.directory ? " directory" : ""}${this.module ? " module" : ""}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve context
|
||||
* @typedef {object} ResolveContext
|
||||
* @property {WriteOnlySet<string>=} contextDependencies directories that was found on file system
|
||||
* @property {WriteOnlySet<string>=} fileDependencies files that was found on file system
|
||||
* @property {WriteOnlySet<string>=} missingDependencies dependencies that was not found on file system
|
||||
* @property {StackEntry | Set<string>=} stack tip of the resolver call stack (a singly-linked list with Set-like API). For instance, `resolve → parsedResolve → describedResolve`. Accepts a legacy `Set<string>` for back-compat with older callers; it is normalized internally without a hot-path branch.
|
||||
* @property {((str: string) => void)=} log log function
|
||||
* @property {ResolveContextYield=} yield yield result, if provided plugins can return several results
|
||||
*/
|
||||
|
||||
/** @typedef {AsyncSeriesBailHook<[ResolveRequest, ResolveContext], ResolveRequest | null>} ResolveStepHook */
|
||||
|
||||
/**
|
||||
* @typedef {object} KnownHooks
|
||||
* @property {SyncHook<[ResolveStepHook, ResolveRequest], void>} resolveStep resolve step hook
|
||||
* @property {SyncHook<[ResolveRequest, Error]>} noResolve no resolve hook
|
||||
* @property {ResolveStepHook} resolve resolve hook
|
||||
* @property {AsyncSeriesHook<[ResolveRequest, ResolveContext]>} result result hook
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{ [key: string]: ResolveStepHook }} EnsuredHooks
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {string} str input string
|
||||
* @returns {string} in camel case
|
||||
*/
|
||||
function toCamelCase(str) {
|
||||
return str.replace(/-([a-z])/g, (str) => str.slice(1).toUpperCase());
|
||||
}
|
||||
|
||||
class Resolver {
|
||||
/**
|
||||
* @param {ResolveStepHook} hook hook
|
||||
* @param {ResolveRequest} request request
|
||||
* @param {StackEntry=} parent previous tip of the stack
|
||||
* @param {Set<string>=} preSeeded entries pre-seeded via the legacy `Set<string>` API
|
||||
* @returns {StackEntry} stack entry
|
||||
*/
|
||||
static createStackEntry(hook, request, parent, preSeeded) {
|
||||
return new StackEntry(hook, request, parent, preSeeded);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {FileSystem} fileSystem a filesystem
|
||||
* @param {ResolveOptions} options options
|
||||
*/
|
||||
constructor(fileSystem, options) {
|
||||
/** @type {FileSystem} */
|
||||
this.fileSystem = fileSystem;
|
||||
/** @type {ResolveOptions} */
|
||||
this.options = options;
|
||||
let pathCache = _pathCacheByFs.get(fileSystem);
|
||||
if (!pathCache) {
|
||||
pathCache = {
|
||||
join: createCachedJoin(),
|
||||
dirname: createCachedDirname(),
|
||||
basename: createCachedBasename(),
|
||||
};
|
||||
_pathCacheByFs.set(fileSystem, pathCache);
|
||||
}
|
||||
/** @type {PathCacheFunctions} */
|
||||
this.pathCache = pathCache;
|
||||
/** @type {KnownHooks} */
|
||||
this.hooks = {
|
||||
resolveStep: new SyncHook(["hook", "request"], "resolveStep"),
|
||||
noResolve: new SyncHook(["request", "error"], "noResolve"),
|
||||
resolve: new AsyncSeriesBailHook(
|
||||
["request", "resolveContext"],
|
||||
"resolve",
|
||||
),
|
||||
result: new AsyncSeriesHook(["result", "resolveContext"], "result"),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | ResolveStepHook} name hook name or hook itself
|
||||
* @returns {ResolveStepHook} the hook
|
||||
*/
|
||||
ensureHook(name) {
|
||||
if (typeof name !== "string") {
|
||||
return name;
|
||||
}
|
||||
name = toCamelCase(name);
|
||||
if (name.startsWith("before")) {
|
||||
return /** @type {ResolveStepHook} */ (
|
||||
this.ensureHook(name[6].toLowerCase() + name.slice(7)).withOptions({
|
||||
stage: -10,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (name.startsWith("after")) {
|
||||
return /** @type {ResolveStepHook} */ (
|
||||
this.ensureHook(name[5].toLowerCase() + name.slice(6)).withOptions({
|
||||
stage: 10,
|
||||
})
|
||||
);
|
||||
}
|
||||
/** @type {ResolveStepHook} */
|
||||
const hook = /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name];
|
||||
if (!hook) {
|
||||
/** @type {KnownHooks & EnsuredHooks} */
|
||||
(this.hooks)[name] = new AsyncSeriesBailHook(
|
||||
["request", "resolveContext"],
|
||||
name,
|
||||
);
|
||||
|
||||
return /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name];
|
||||
}
|
||||
return hook;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | ResolveStepHook} name hook name or hook itself
|
||||
* @returns {ResolveStepHook} the hook
|
||||
*/
|
||||
getHook(name) {
|
||||
if (typeof name !== "string") {
|
||||
return name;
|
||||
}
|
||||
name = toCamelCase(name);
|
||||
if (name.startsWith("before")) {
|
||||
return /** @type {ResolveStepHook} */ (
|
||||
this.getHook(name[6].toLowerCase() + name.slice(7)).withOptions({
|
||||
stage: -10,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (name.startsWith("after")) {
|
||||
return /** @type {ResolveStepHook} */ (
|
||||
this.getHook(name[5].toLowerCase() + name.slice(6)).withOptions({
|
||||
stage: 10,
|
||||
})
|
||||
);
|
||||
}
|
||||
/** @type {ResolveStepHook} */
|
||||
const hook = /** @type {KnownHooks & EnsuredHooks} */ (this.hooks)[name];
|
||||
if (!hook) {
|
||||
throw new Error(`Hook ${name} doesn't exist`);
|
||||
}
|
||||
return hook;
|
||||
}
|
||||
|
||||
/**
|
||||
* @overload
|
||||
* @param {string} path context path
|
||||
* @param {string} request request string
|
||||
* @param {ResolveContext=} resolveContext resolve context
|
||||
* @returns {string | false} result
|
||||
*/
|
||||
/**
|
||||
* @overload
|
||||
* @param {Context} context context information object
|
||||
* @param {string} path context path
|
||||
* @param {string} request request string
|
||||
* @param {ResolveContext=} resolveContext resolve context
|
||||
* @returns {string | false} result
|
||||
*/
|
||||
/**
|
||||
* @param {Context | string} context context information object or context path when no context is provided
|
||||
* @param {string | ResolveContext=} path context path or resolve context when no context is provided
|
||||
* @param {string | ResolveContext=} request request string or resolve context when no context is provided
|
||||
* @param {ResolveContext=} resolveContext resolve context
|
||||
* @returns {string | false} result
|
||||
*/
|
||||
resolveSync(context, path, request, resolveContext) {
|
||||
/** @type {Error | null | undefined} */
|
||||
let err;
|
||||
/** @type {string | false | undefined} */
|
||||
let result;
|
||||
let sync = false;
|
||||
// `|| {}` so the underlying `resolve()` hits its 5-arg fast path
|
||||
// (skips the overload-shifting prologue) regardless of whether the
|
||||
// caller supplied a resolveContext.
|
||||
this.resolve(
|
||||
/** @type {Context} */ (context),
|
||||
/** @type {string} */ (path),
|
||||
/** @type {string} */ (request),
|
||||
/** @type {ResolveContext} */ (resolveContext) || {},
|
||||
(_err, r) => {
|
||||
err = _err;
|
||||
result = r;
|
||||
sync = true;
|
||||
},
|
||||
);
|
||||
if (!sync) {
|
||||
throw new Error(
|
||||
"Cannot 'resolveSync' because the fileSystem is not sync. Use 'resolve'!",
|
||||
);
|
||||
}
|
||||
if (err) throw err;
|
||||
if (result === undefined) throw new Error("No result");
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @overload
|
||||
* @param {string} path context path
|
||||
* @param {string} request request string
|
||||
* @param {ResolveContext=} resolveContext resolve context
|
||||
* @returns {Promise<string | false>} result
|
||||
*/
|
||||
/**
|
||||
* @overload
|
||||
* @param {Context} context context information object
|
||||
* @param {string} path context path
|
||||
* @param {string} request request string
|
||||
* @param {ResolveContext=} resolveContext resolve context
|
||||
* @returns {Promise<string | false>} result
|
||||
*/
|
||||
/**
|
||||
* @param {Context | string} context context information object or context path when no context is provided
|
||||
* @param {string | ResolveContext=} path context path or resolve context when no context is provided
|
||||
* @param {string | ResolveContext=} request request string or resolve context when no context is provided
|
||||
* @param {ResolveContext=} resolveContext resolve context
|
||||
* @returns {Promise<string | false>} result
|
||||
*/
|
||||
resolvePromise(context, path, request, resolveContext) {
|
||||
// `|| {}` ensures the 5-arg fast path inside `resolve()` is reached
|
||||
// even when the caller doesn't pass a resolveContext.
|
||||
return _withResolvers(
|
||||
this,
|
||||
/** @type {Context} */ (context),
|
||||
/** @type {string} */ (path),
|
||||
/** @type {string} */ (request),
|
||||
/** @type {ResolveContext} */ (resolveContext) || {},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @overload
|
||||
* @param {string} path context path
|
||||
* @param {string} request request string
|
||||
* @param {ResolveCallback} callback callback function
|
||||
* @returns {void}
|
||||
*/
|
||||
/**
|
||||
* @overload
|
||||
* @param {string} path context path
|
||||
* @param {string} request request string
|
||||
* @param {ResolveContext} resolveContext resolve context
|
||||
* @param {ResolveCallback} callback callback function
|
||||
* @returns {void}
|
||||
*/
|
||||
/**
|
||||
* @overload
|
||||
* @param {Context} context context information object
|
||||
* @param {string} path context path
|
||||
* @param {string} request request string
|
||||
* @param {ResolveCallback} callback callback function
|
||||
* @returns {void}
|
||||
*/
|
||||
/**
|
||||
* @overload
|
||||
* @param {Context} context context information object
|
||||
* @param {string} path context path
|
||||
* @param {string} request request string
|
||||
* @param {ResolveContext} resolveContext resolve context
|
||||
* @param {ResolveCallback} callback callback function
|
||||
* @returns {void}
|
||||
*/
|
||||
/**
|
||||
* @param {Context | string} context context information object or context path when no context is provided
|
||||
* @param {string | ResolveContext | ResolveCallback=} path context path or (when no context) resolve context or callback
|
||||
* @param {string | ResolveContext | ResolveCallback=} request request string or (when no context) resolve context or callback
|
||||
* @param {ResolveContext | ResolveCallback=} resolveContext resolve context or callback when no resolve context is provided
|
||||
* @param {ResolveCallback=} callback callback function
|
||||
* @returns {void}
|
||||
*/
|
||||
resolve(context, path, request, resolveContext, callback) {
|
||||
// Fast path for the common 5-arg call (`resolver.resolve(ctx, from,
|
||||
// req, resolveCtx, cb)`) — every call from `resolveSync` /
|
||||
// `resolvePromise` plus the vast majority of direct API callers.
|
||||
// PR #536 added runtime overload-shifting to support optional
|
||||
// `context` / `resolveContext`; that adds several `typeof` checks
|
||||
// per resolve which show up as a measurable instruction-count
|
||||
// regression on every benchmark that calls into this method. Skip
|
||||
// the shifting entirely when all 5 args are already well-typed.
|
||||
if (
|
||||
typeof callback === "function" &&
|
||||
typeof context === "object" &&
|
||||
context !== null &&
|
||||
typeof resolveContext === "object" &&
|
||||
resolveContext !== null
|
||||
) {
|
||||
// proceed straight to per-arg validation below
|
||||
} else {
|
||||
// Slow path: shift positional args based on what was supplied.
|
||||
// Shift when context is omitted (first positional arg is the path string).
|
||||
if (typeof context === "string") {
|
||||
// Keep an already-supplied callback (resolveSync / resolvePromise
|
||||
// always pass one in the 5th position).
|
||||
if (typeof callback !== "function") {
|
||||
callback = /** @type {ResolveCallback | undefined} */ (
|
||||
resolveContext
|
||||
);
|
||||
}
|
||||
resolveContext =
|
||||
/** @type {ResolveContext | ResolveCallback | undefined} */ (request);
|
||||
request = /** @type {string} */ (path);
|
||||
path = context;
|
||||
context = {};
|
||||
}
|
||||
// 4-arg form: the resolveContext slot holds the callback.
|
||||
if (typeof resolveContext === "function") {
|
||||
callback = resolveContext;
|
||||
resolveContext = {};
|
||||
} else if (!resolveContext || typeof resolveContext !== "object") {
|
||||
resolveContext = {};
|
||||
}
|
||||
if (typeof callback !== "function") {
|
||||
throw new TypeError("callback argument is not a function");
|
||||
}
|
||||
if (!context || typeof context !== "object") {
|
||||
context = {};
|
||||
}
|
||||
}
|
||||
if (typeof path !== "string") {
|
||||
return callback(new Error("path argument is not a string"));
|
||||
}
|
||||
if (typeof request !== "string") {
|
||||
return callback(new Error("request argument is not a string"));
|
||||
}
|
||||
|
||||
/** @type {ResolveRequest} */
|
||||
const obj = {
|
||||
context,
|
||||
path,
|
||||
request,
|
||||
};
|
||||
|
||||
/** @type {ResolveContextYield | undefined} */
|
||||
let yield_;
|
||||
let yieldCalled = false;
|
||||
/** @type {ResolveContextYield | undefined} */
|
||||
let finishYield;
|
||||
if (typeof resolveContext.yield === "function") {
|
||||
const old = resolveContext.yield;
|
||||
/**
|
||||
* @param {ResolveRequest} obj object
|
||||
*/
|
||||
yield_ = (obj) => {
|
||||
old(obj);
|
||||
yieldCalled = true;
|
||||
};
|
||||
/**
|
||||
* @param {ResolveRequest} result result
|
||||
* @returns {void}
|
||||
*/
|
||||
finishYield = (result) => {
|
||||
if (result) {
|
||||
/** @type {ResolveContextYield} */ (yield_)(result);
|
||||
}
|
||||
callback(null);
|
||||
};
|
||||
}
|
||||
|
||||
const message = `resolve '${request}' in '${path}'`;
|
||||
|
||||
/**
|
||||
* @param {ResolveRequest} result result
|
||||
* @returns {void}
|
||||
*/
|
||||
const finishResolved = (result) => {
|
||||
const resultPath = result.path;
|
||||
if (resultPath === false) return callback(null, false, result);
|
||||
const escapedPath = resultPath.includes("#")
|
||||
? resultPath.replace(HASH_ESCAPE_RE, "\0#")
|
||||
: resultPath;
|
||||
const resultQuery = result.query;
|
||||
let escapedQuery;
|
||||
if (resultQuery) {
|
||||
escapedQuery = resultQuery.includes("#")
|
||||
? resultQuery.replace(HASH_ESCAPE_RE, "\0#")
|
||||
: resultQuery;
|
||||
} else {
|
||||
escapedQuery = "";
|
||||
}
|
||||
return callback(
|
||||
null,
|
||||
`${escapedPath}${escapedQuery}${result.fragment || ""}`,
|
||||
result,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string[]} log logs
|
||||
* @returns {void}
|
||||
*/
|
||||
const finishWithoutResolve = (log) => {
|
||||
/**
|
||||
* @type {ErrorWithDetail}
|
||||
*/
|
||||
const error = new Error(`Can't ${message}`);
|
||||
error.details = log.join("\n");
|
||||
this.hooks.noResolve.call(obj, error);
|
||||
return callback(error);
|
||||
};
|
||||
|
||||
if (resolveContext.log) {
|
||||
// We need log anyway to capture it in case of an error
|
||||
const parentLog = resolveContext.log;
|
||||
/** @type {string[]} */
|
||||
const log = [];
|
||||
return this.doResolve(
|
||||
this.hooks.resolve,
|
||||
obj,
|
||||
message,
|
||||
{
|
||||
log: (msg) => {
|
||||
parentLog(msg);
|
||||
log.push(msg);
|
||||
},
|
||||
yield: yield_,
|
||||
fileDependencies: resolveContext.fileDependencies,
|
||||
contextDependencies: resolveContext.contextDependencies,
|
||||
missingDependencies: resolveContext.missingDependencies,
|
||||
stack: resolveContext.stack,
|
||||
},
|
||||
(err, result) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
if (yieldCalled || (result && yield_)) {
|
||||
return /** @type {ResolveContextYield} */ (finishYield)(
|
||||
/** @type {ResolveRequest} */ (result),
|
||||
);
|
||||
}
|
||||
|
||||
if (result) return finishResolved(result);
|
||||
|
||||
return finishWithoutResolve(log);
|
||||
},
|
||||
);
|
||||
}
|
||||
// Try to resolve assuming there is no error
|
||||
// We don't log stuff in this case
|
||||
return this.doResolve(
|
||||
this.hooks.resolve,
|
||||
obj,
|
||||
message,
|
||||
{
|
||||
log: undefined,
|
||||
yield: yield_,
|
||||
fileDependencies: resolveContext.fileDependencies,
|
||||
contextDependencies: resolveContext.contextDependencies,
|
||||
missingDependencies: resolveContext.missingDependencies,
|
||||
stack: resolveContext.stack,
|
||||
},
|
||||
(err, result) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
if (yieldCalled || (result && yield_)) {
|
||||
return /** @type {ResolveContextYield} */ (finishYield)(
|
||||
/** @type {ResolveRequest} */ (result),
|
||||
);
|
||||
}
|
||||
|
||||
if (result) return finishResolved(result);
|
||||
|
||||
// log is missing for the error details
|
||||
// so we redo the resolving for the log info
|
||||
// this is more expensive to the success case
|
||||
// is assumed by default
|
||||
/** @type {string[]} */
|
||||
const log = [];
|
||||
|
||||
return this.doResolve(
|
||||
this.hooks.resolve,
|
||||
obj,
|
||||
message,
|
||||
{
|
||||
log: (msg) => log.push(msg),
|
||||
yield: yield_,
|
||||
stack: resolveContext.stack,
|
||||
},
|
||||
(err, result) => {
|
||||
if (err) return callback(err);
|
||||
|
||||
// In a case that there is a race condition and yield will be called
|
||||
if (yieldCalled || (result && yield_)) {
|
||||
return /** @type {ResolveContextYield} */ (finishYield)(
|
||||
/** @type {ResolveRequest} */ (result),
|
||||
);
|
||||
}
|
||||
|
||||
return finishWithoutResolve(log);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ResolveStepHook} hook hook
|
||||
* @param {ResolveRequest} request request
|
||||
* @param {null | string} message string
|
||||
* @param {ResolveContext} resolveContext resolver context
|
||||
* @param {(err?: null | Error, result?: ResolveRequest) => void} callback callback
|
||||
* @returns {void}
|
||||
*/
|
||||
doResolve(hook, request, message, resolveContext, callback) {
|
||||
const rawStack = resolveContext.stack;
|
||||
/** @type {StackEntry | undefined} */
|
||||
let parent;
|
||||
/** @type {Set<string> | undefined} */
|
||||
let preSeeded;
|
||||
if (rawStack instanceof StackEntry) {
|
||||
parent = rawStack;
|
||||
preSeeded = rawStack.preSeeded;
|
||||
} else if (rawStack) {
|
||||
// TODO in the next major remove `Set<string>` support in favor of `StackEntry`
|
||||
// Legacy `stack: new Set<string>()` API: don't link the Set into
|
||||
// the parent chain (it would pollute iteration and field-compare
|
||||
// walks). Carry the strings on the StackEntry itself instead so
|
||||
// deeper `doResolve` calls keep seeing pre-seeded entries.
|
||||
preSeeded = /** @type {Set<string>} */ (rawStack);
|
||||
}
|
||||
// Prepend a new linked-list node. O(1) allocation, no Set clone.
|
||||
const stackEntry = Resolver.createStackEntry(
|
||||
hook,
|
||||
request,
|
||||
parent,
|
||||
preSeeded,
|
||||
);
|
||||
|
||||
// When `parent` exists, its `has()` already consults `preSeeded`
|
||||
// (inherited from the same chain), so we only need the direct Set
|
||||
// lookup on the very first `doResolve` call (no parent yet).
|
||||
if (
|
||||
parent !== undefined
|
||||
? parent.has(stackEntry)
|
||||
: preSeeded !== undefined && preSeeded.has(stackEntry.toString())
|
||||
) {
|
||||
/**
|
||||
* Prevent recursion
|
||||
* @type {Error & { recursion?: boolean }}
|
||||
*/
|
||||
const recursionError = new Error(
|
||||
`Recursion in resolving\nStack:\n ${[...stackEntry].join("\n ")}`,
|
||||
);
|
||||
recursionError.recursion = true;
|
||||
if (resolveContext.log) {
|
||||
resolveContext.log("abort resolving because of recursion");
|
||||
}
|
||||
return callback(recursionError);
|
||||
}
|
||||
this.hooks.resolveStep.call(hook, request);
|
||||
|
||||
if (hook.isUsed()) {
|
||||
// Pass `resolveContext` and the override fields (stack, message)
|
||||
// directly instead of constructing an intermediate options-object
|
||||
// literal — `createInnerContext` reads from the parent and
|
||||
// allocates exactly one inner context per step. See the comment
|
||||
// on `createInnerContext` itself for the allocation rationale.
|
||||
const innerContext = createInnerContext(
|
||||
resolveContext,
|
||||
stackEntry,
|
||||
message,
|
||||
);
|
||||
return hook.callAsync(request, innerContext, (err, result) => {
|
||||
if (err) return callback(err);
|
||||
if (result) return callback(null, result);
|
||||
callback();
|
||||
});
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} identifier identifier
|
||||
* @returns {ParsedIdentifier} parsed identifier
|
||||
*/
|
||||
parse(identifier) {
|
||||
/** @type {ParsedIdentifier} */
|
||||
const part = {
|
||||
request: "",
|
||||
query: "",
|
||||
fragment: "",
|
||||
module: false,
|
||||
directory: false,
|
||||
file: false,
|
||||
internal: false,
|
||||
};
|
||||
|
||||
const parsedIdentifier = parseIdentifier(identifier);
|
||||
|
||||
if (!parsedIdentifier) return part;
|
||||
|
||||
[part.request, part.query, part.fragment] = parsedIdentifier;
|
||||
|
||||
if (part.request.length > 0) {
|
||||
// `getType` looks at the prefix of its input and the prefix is
|
||||
// identical between `identifier` and `part.request` in every
|
||||
// non-`\0`-escape case (slicing off `?query` / `#fragment` doesn't
|
||||
// touch the head). `parseIdentifier`'s common fast path returns
|
||||
// the same `identifier` reference as `parsedIdentifier[0]`, so a
|
||||
// pointer-equality check detects the case where we can compute
|
||||
// `getType` once and use it for both `module` and `internal`. The
|
||||
// `\0#…` escape path produces a fresh `part.request` and falls
|
||||
// through to the second `getType(identifier)` call to preserve
|
||||
// the original `internal` flag.
|
||||
const requestType = getType(part.request);
|
||||
part.module = requestType === PathType.Normal;
|
||||
part.internal =
|
||||
identifier === part.request
|
||||
? requestType === PathType.Internal
|
||||
: getType(identifier) === PathType.Internal;
|
||||
// `isDirectory` is just `endsWith("/")` — inline so `parse()`
|
||||
// doesn't pay for the extra method dispatch on every resolve.
|
||||
part.directory = part.request.endsWith("/");
|
||||
if (part.directory) {
|
||||
part.request = part.request.slice(0, -1);
|
||||
}
|
||||
}
|
||||
|
||||
return part;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path path
|
||||
* @returns {boolean} true, if the path is a module
|
||||
*/
|
||||
isModule(path) {
|
||||
return getType(path) === PathType.Normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path path
|
||||
* @returns {boolean} true, if the path is private
|
||||
*/
|
||||
isPrivate(path) {
|
||||
return getType(path) === PathType.Internal;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path a path
|
||||
* @returns {boolean} true, if the path is a directory path
|
||||
*/
|
||||
isDirectory(path) {
|
||||
return path.endsWith("/");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path path
|
||||
* @returns {string} normalized path
|
||||
*/
|
||||
normalize(path) {
|
||||
return normalize(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path path
|
||||
* @param {string} request request
|
||||
* @returns {string} joined path
|
||||
*/
|
||||
join(path, request) {
|
||||
return this.pathCache.join.fn(path, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path path
|
||||
* @returns {string} parent directory
|
||||
*/
|
||||
dirname(path) {
|
||||
return this.pathCache.dirname.fn(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path the path to evaluate
|
||||
* @param {string=} suffix an extension to remove from the result
|
||||
* @returns {string} the last portion of a path
|
||||
*/
|
||||
basename(path, suffix) {
|
||||
return this.pathCache.basename.fn(path, suffix);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Resolver;
|
||||
Reference in New Issue
Block a user