You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
801 lines
24 KiB
801 lines
24 KiB
4 weeks ago
|
/*
|
||
|
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 {
|
||
|
normalize,
|
||
|
cachedJoin: join,
|
||
|
getType,
|
||
|
PathType
|
||
|
} = require("./util/path");
|
||
|
|
||
|
/** @typedef {import("./ResolverFactory").ResolveOptions} ResolveOptions */
|
||
|
|
||
|
/** @typedef {Error & { details?: string }} ErrorWithDetail */
|
||
|
|
||
|
/** @typedef {(err: ErrorWithDetail | null, res?: string | false, req?: ResolveRequest) => void} ResolveCallback */
|
||
|
|
||
|
/**
|
||
|
* @typedef {Object} PossibleFileSystemError
|
||
|
* @property {string=} code
|
||
|
* @property {number=} errno
|
||
|
* @property {string=} path
|
||
|
* @property {string=} 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]
|
||
|
*/
|
||
|
|
||
|
/** @typedef {function(NodeJS.ErrnoException | null, string=): void} StringCallback */
|
||
|
/** @typedef {function(NodeJS.ErrnoException | null, Buffer=): void} BufferCallback */
|
||
|
/** @typedef {function(NodeJS.ErrnoException | null, (string | Buffer)=): void} StringOrBufferCallback */
|
||
|
/** @typedef {function(NodeJS.ErrnoException | null, IStats=): void} StatsCallback */
|
||
|
/** @typedef {function(NodeJS.ErrnoException | null, IBigIntStats=): void} BigIntStatsCallback */
|
||
|
/** @typedef {function(NodeJS.ErrnoException | null, (IStats | IBigIntStats)=): void} StatsOrBigIntStatsCallback */
|
||
|
/** @typedef {function(NodeJS.ErrnoException | Error | null, JsonObject=): void} ReadJsonCallback */
|
||
|
/** @typedef {function(NodeJS.ErrnoException | null, string[]=): void} ReaddirStringCallback */
|
||
|
/** @typedef {function(NodeJS.ErrnoException | null, Buffer[]=): void} ReaddirBufferCallback */
|
||
|
/** @typedef {function(NodeJS.ErrnoException | null, (string[] | Buffer[])=): void} ReaddirStringOrBufferCallback */
|
||
|
/** @typedef {function(NodeJS.ErrnoException | null, Dirent[]=): void} ReaddirDirentCallback */
|
||
|
|
||
|
/**
|
||
|
* @template T
|
||
|
* @typedef {Object} IStatsBase
|
||
|
* @property {() => boolean} isFile
|
||
|
* @property {() => boolean} isDirectory
|
||
|
* @property {() => boolean} isBlockDevice
|
||
|
* @property {() => boolean} isCharacterDevice
|
||
|
* @property {() => boolean} isSymbolicLink
|
||
|
* @property {() => boolean} isFIFO
|
||
|
* @property {() => boolean} isSocket
|
||
|
* @property {T} dev
|
||
|
* @property {T} ino
|
||
|
* @property {T} mode
|
||
|
* @property {T} nlink
|
||
|
* @property {T} uid
|
||
|
* @property {T} gid
|
||
|
* @property {T} rdev
|
||
|
* @property {T} size
|
||
|
* @property {T} blksize
|
||
|
* @property {T} blocks
|
||
|
* @property {T} atimeMs
|
||
|
* @property {T} mtimeMs
|
||
|
* @property {T} ctimeMs
|
||
|
* @property {T} birthtimeMs
|
||
|
* @property {Date} atime
|
||
|
* @property {Date} mtime
|
||
|
* @property {Date} ctime
|
||
|
* @property {Date} birthtime
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {IStatsBase<number>} IStats
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {IStatsBase<bigint> & { atimeNs: bigint, mtimeNs: bigint, ctimeNs: bigint, birthtimeNs: bigint }} IBigIntStats
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {Object} Dirent
|
||
|
* @property {() => boolean} isFile
|
||
|
* @property {() => boolean} isDirectory
|
||
|
* @property {() => boolean} isBlockDevice
|
||
|
* @property {() => boolean} isCharacterDevice
|
||
|
* @property {() => boolean} isSymbolicLink
|
||
|
* @property {() => boolean} isFIFO
|
||
|
* @property {() => boolean} isSocket
|
||
|
* @property {string} name
|
||
|
* @property {string} path
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {Object} StatOptions
|
||
|
* @property {(boolean | undefined)=} bigint
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {Object} StatSyncOptions
|
||
|
* @property {(boolean | undefined)=} bigint
|
||
|
* @property {(boolean | undefined)=} throwIfNoEntry
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @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 {ObjectEncodingOptions | BufferEncoding | undefined | null} EncodingOption
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @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: ReaddirStringCallback): void;
|
||
|
* (path: PathLike, options: { encoding: 'buffer', withFileTypes?: false | undefined, recursive?: boolean | undefined } | 'buffer', callback: ReaddirBufferCallback): void;
|
||
|
* (path: PathLike, callback: ReaddirStringCallback): void;
|
||
|
* (path: PathLike, options: (ObjectEncodingOptions & { withFileTypes?: false | undefined, recursive?: boolean | undefined }) | BufferEncoding | undefined | null, callback: ReaddirStringOrBufferCallback): void;
|
||
|
* (path: PathLike, options: ObjectEncodingOptions & { withFileTypes: true, recursive?: boolean | undefined }, callback: ReaddirDirentCallback): 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[];
|
||
|
* }} ReaddirSync
|
||
|
|
||
|
/**
|
||
|
* @typedef {function(PathOrFileDescriptor, ReadJsonCallback): void} ReadJson
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {function(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
|
||
|
* @property {Readdir} readdir
|
||
|
* @property {ReadJson=} readJson
|
||
|
* @property {Readlink} readlink
|
||
|
* @property {LStat=} lstat
|
||
|
* @property {Stat} stat
|
||
|
* @property {RealPath=} realpath
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {Object} SyncFileSystem
|
||
|
* @property {ReadFileSync} readFileSync
|
||
|
* @property {ReaddirSync} readdirSync
|
||
|
* @property {ReadJsonSync=} readJsonSync
|
||
|
* @property {ReadlinkSync} readlinkSync
|
||
|
* @property {LStatSync=} lstatSync
|
||
|
* @property {StatSync} statSync
|
||
|
* @property {RealPathSync=} realpathSync
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {Object} ParsedIdentifier
|
||
|
* @property {string} request
|
||
|
* @property {string} query
|
||
|
* @property {string} fragment
|
||
|
* @property {boolean} directory
|
||
|
* @property {boolean} module
|
||
|
* @property {boolean} file
|
||
|
* @property {boolean} internal
|
||
|
*/
|
||
|
|
||
|
/** @typedef {string | number | boolean | null} JsonPrimitive */
|
||
|
/** @typedef {JsonValue[]} JsonArray */
|
||
|
/** @typedef {JsonPrimitive | JsonObject | JsonArray} JsonValue */
|
||
|
/** @typedef {{[Key in string]: JsonValue} & {[Key in string]?: JsonValue | undefined}} JsonObject */
|
||
|
|
||
|
/**
|
||
|
* @typedef {Object} BaseResolveRequest
|
||
|
* @property {string | false} path
|
||
|
* @property {object=} context
|
||
|
* @property {string=} descriptionFilePath
|
||
|
* @property {string=} descriptionFileRoot
|
||
|
* @property {JsonObject=} descriptionFileData
|
||
|
* @property {string=} relativePath
|
||
|
* @property {boolean=} ignoreSymlinks
|
||
|
* @property {boolean=} fullySpecified
|
||
|
* @property {string=} __innerRequest
|
||
|
* @property {string=} __innerRequest_request
|
||
|
* @property {string=} __innerRequest_relativePath
|
||
|
*/
|
||
|
|
||
|
/** @typedef {BaseResolveRequest & Partial<ParsedIdentifier>} ResolveRequest */
|
||
|
|
||
|
/**
|
||
|
* String with special formatting
|
||
|
* @typedef {string} StackEntry
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @template T
|
||
|
* @typedef {{ add: (item: T) => void }} WriteOnlySet
|
||
|
*/
|
||
|
|
||
|
/** @typedef {(function (ResolveRequest): void)} ResolveContextYield */
|
||
|
|
||
|
/**
|
||
|
* Resolve context
|
||
|
* @typedef {Object} ResolveContext
|
||
|
* @property {WriteOnlySet<string>=} contextDependencies
|
||
|
* @property {WriteOnlySet<string>=} fileDependencies files that was found on file system
|
||
|
* @property {WriteOnlySet<string>=} missingDependencies dependencies that was not found on file system
|
||
|
* @property {Set<StackEntry>=} stack set of hooks' calls. For instance, `resolve → parsedResolve → describedResolve`,
|
||
|
* @property {(function(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
|
||
|
* @property {SyncHook<[ResolveRequest, Error]>} noResolve
|
||
|
* @property {ResolveStepHook} resolve
|
||
|
* @property {AsyncSeriesHook<[ResolveRequest, ResolveContext]>} result
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @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
|
||
|
* @returns {StackEntry} stack entry
|
||
|
*/
|
||
|
static createStackEntry(hook, request) {
|
||
|
return (
|
||
|
hook.name +
|
||
|
": (" +
|
||
|
request.path +
|
||
|
") " +
|
||
|
(request.request || "") +
|
||
|
(request.query || "") +
|
||
|
(request.fragment || "") +
|
||
|
(request.directory ? " directory" : "") +
|
||
|
(request.module ? " module" : "")
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {FileSystem} fileSystem a filesystem
|
||
|
* @param {ResolveOptions} options options
|
||
|
*/
|
||
|
constructor(fileSystem, options) {
|
||
|
this.fileSystem = fileSystem;
|
||
|
this.options = options;
|
||
|
/** @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 (/^before/.test(name)) {
|
||
|
return /** @type {ResolveStepHook} */ (
|
||
|
this.ensureHook(name[6].toLowerCase() + name.slice(7)).withOptions({
|
||
|
stage: -10
|
||
|
})
|
||
|
);
|
||
|
}
|
||
|
if (/^after/.test(name)) {
|
||
|
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 (/^before/.test(name)) {
|
||
|
return /** @type {ResolveStepHook} */ (
|
||
|
this.getHook(name[6].toLowerCase() + name.slice(7)).withOptions({
|
||
|
stage: -10
|
||
|
})
|
||
|
);
|
||
|
}
|
||
|
if (/^after/.test(name)) {
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {object} context context information object
|
||
|
* @param {string} path context path
|
||
|
* @param {string} request request string
|
||
|
* @returns {string | false} result
|
||
|
*/
|
||
|
resolveSync(context, path, request) {
|
||
|
/** @type {Error | null | undefined} */
|
||
|
let err = undefined;
|
||
|
/** @type {string | false | undefined} */
|
||
|
let result = undefined;
|
||
|
let sync = false;
|
||
|
this.resolve(context, path, request, {}, (e, r) => {
|
||
|
err = e;
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {object} 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}
|
||
|
*/
|
||
|
resolve(context, path, request, resolveContext, callback) {
|
||
|
if (!context || typeof context !== "object")
|
||
|
return callback(new Error("context argument is not an object"));
|
||
|
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"));
|
||
|
if (!resolveContext)
|
||
|
return callback(new Error("resolveContext argument is not set"));
|
||
|
|
||
|
/** @type {ResolveRequest} */
|
||
|
const obj = {
|
||
|
context: context,
|
||
|
path: path,
|
||
|
request: 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 => {
|
||
|
return callback(
|
||
|
null,
|
||
|
result.path === false
|
||
|
? false
|
||
|
: `${result.path.replace(/#/g, "\0#")}${
|
||
|
result.query ? result.query.replace(/#/g, "\0#") : ""
|
||
|
}${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);
|
||
|
}
|
||
|
);
|
||
|
} else {
|
||
|
// 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 stackEntry = Resolver.createStackEntry(hook, request);
|
||
|
|
||
|
/** @type {Set<string> | undefined} */
|
||
|
let newStack;
|
||
|
if (resolveContext.stack) {
|
||
|
newStack = new Set(resolveContext.stack);
|
||
|
if (resolveContext.stack.has(stackEntry)) {
|
||
|
/**
|
||
|
* Prevent recursion
|
||
|
* @type {Error & {recursion?: boolean}}
|
||
|
*/
|
||
|
const recursionError = new Error(
|
||
|
"Recursion in resolving\nStack:\n " +
|
||
|
Array.from(newStack).join("\n ")
|
||
|
);
|
||
|
recursionError.recursion = true;
|
||
|
if (resolveContext.log)
|
||
|
resolveContext.log("abort resolving because of recursion");
|
||
|
return callback(recursionError);
|
||
|
}
|
||
|
newStack.add(stackEntry);
|
||
|
} else {
|
||
|
// creating a set with new Set([item])
|
||
|
// allocates a new array that has to be garbage collected
|
||
|
// this is an EXTREMELY hot path, so let's avoid it
|
||
|
newStack = new Set();
|
||
|
newStack.add(stackEntry);
|
||
|
}
|
||
|
this.hooks.resolveStep.call(hook, request);
|
||
|
|
||
|
if (hook.isUsed()) {
|
||
|
const innerContext = createInnerContext(
|
||
|
{
|
||
|
log: resolveContext.log,
|
||
|
yield: resolveContext.yield,
|
||
|
fileDependencies: resolveContext.fileDependencies,
|
||
|
contextDependencies: resolveContext.contextDependencies,
|
||
|
missingDependencies: resolveContext.missingDependencies,
|
||
|
stack: newStack
|
||
|
},
|
||
|
message
|
||
|
);
|
||
|
return hook.callAsync(request, innerContext, (err, result) => {
|
||
|
if (err) return callback(err);
|
||
|
if (result) return callback(null, result);
|
||
|
callback();
|
||
|
});
|
||
|
} else {
|
||
|
callback();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {string} identifier identifier
|
||
|
* @returns {ParsedIdentifier} parsed identifier
|
||
|
*/
|
||
|
parse(identifier) {
|
||
|
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) {
|
||
|
part.internal = this.isPrivate(identifier);
|
||
|
part.module = this.isModule(part.request);
|
||
|
part.directory = this.isDirectory(part.request);
|
||
|
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
|
||
|
* @param {string} request request
|
||
|
* @returns {string} joined path
|
||
|
*/
|
||
|
join(path, request) {
|
||
|
return join(path, request);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {string} path path
|
||
|
* @returns {string} normalized path
|
||
|
*/
|
||
|
normalize(path) {
|
||
|
return normalize(path);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = Resolver;
|