[6a3a178] | 1 | /*
|
---|
| 2 | MIT License http://www.opensource.org/licenses/mit-license.php
|
---|
| 3 | Author Tobias Koppers @sokra
|
---|
| 4 | */
|
---|
| 5 |
|
---|
| 6 | "use strict";
|
---|
| 7 |
|
---|
| 8 | const { AsyncSeriesBailHook, AsyncSeriesHook, SyncHook } = require("tapable");
|
---|
| 9 | const createInnerContext = require("./createInnerContext");
|
---|
| 10 | const { parseIdentifier } = require("./util/identifier");
|
---|
| 11 | const {
|
---|
| 12 | normalize,
|
---|
| 13 | cachedJoin: join,
|
---|
| 14 | getType,
|
---|
| 15 | PathType
|
---|
| 16 | } = require("./util/path");
|
---|
| 17 |
|
---|
| 18 | /** @typedef {import("./ResolverFactory").ResolveOptions} ResolveOptions */
|
---|
| 19 |
|
---|
| 20 | /**
|
---|
| 21 | * @typedef {Object} FileSystemStats
|
---|
| 22 | * @property {function(): boolean} isDirectory
|
---|
| 23 | * @property {function(): boolean} isFile
|
---|
| 24 | */
|
---|
| 25 |
|
---|
| 26 | /**
|
---|
| 27 | * @typedef {Object} FileSystemDirent
|
---|
| 28 | * @property {Buffer | string} name
|
---|
| 29 | * @property {function(): boolean} isDirectory
|
---|
| 30 | * @property {function(): boolean} isFile
|
---|
| 31 | */
|
---|
| 32 |
|
---|
| 33 | /**
|
---|
| 34 | * @typedef {Object} PossibleFileSystemError
|
---|
| 35 | * @property {string=} code
|
---|
| 36 | * @property {number=} errno
|
---|
| 37 | * @property {string=} path
|
---|
| 38 | * @property {string=} syscall
|
---|
| 39 | */
|
---|
| 40 |
|
---|
| 41 | /**
|
---|
| 42 | * @template T
|
---|
| 43 | * @callback FileSystemCallback
|
---|
| 44 | * @param {PossibleFileSystemError & Error | null | undefined} err
|
---|
| 45 | * @param {T=} result
|
---|
| 46 | */
|
---|
| 47 |
|
---|
| 48 | /**
|
---|
| 49 | * @typedef {Object} FileSystem
|
---|
| 50 | * @property {(function(string, FileSystemCallback<Buffer | string>): void) & function(string, object, FileSystemCallback<Buffer | string>): void} readFile
|
---|
| 51 | * @property {(function(string, FileSystemCallback<(Buffer | string)[] | FileSystemDirent[]>): void) & function(string, object, FileSystemCallback<(Buffer | string)[] | FileSystemDirent[]>): void} readdir
|
---|
| 52 | * @property {((function(string, FileSystemCallback<object>): void) & function(string, object, FileSystemCallback<object>): void)=} readJson
|
---|
| 53 | * @property {(function(string, FileSystemCallback<Buffer | string>): void) & function(string, object, FileSystemCallback<Buffer | string>): void} readlink
|
---|
| 54 | * @property {(function(string, FileSystemCallback<FileSystemStats>): void) & function(string, object, FileSystemCallback<Buffer | string>): void=} lstat
|
---|
| 55 | * @property {(function(string, FileSystemCallback<FileSystemStats>): void) & function(string, object, FileSystemCallback<Buffer | string>): void} stat
|
---|
| 56 | */
|
---|
| 57 |
|
---|
| 58 | /**
|
---|
| 59 | * @typedef {Object} SyncFileSystem
|
---|
| 60 | * @property {function(string, object=): Buffer | string} readFileSync
|
---|
| 61 | * @property {function(string, object=): (Buffer | string)[] | FileSystemDirent[]} readdirSync
|
---|
| 62 | * @property {(function(string, object=): object)=} readJsonSync
|
---|
| 63 | * @property {function(string, object=): Buffer | string} readlinkSync
|
---|
| 64 | * @property {function(string, object=): FileSystemStats=} lstatSync
|
---|
| 65 | * @property {function(string, object=): FileSystemStats} statSync
|
---|
| 66 | */
|
---|
| 67 |
|
---|
| 68 | /**
|
---|
| 69 | * @typedef {Object} ParsedIdentifier
|
---|
| 70 | * @property {string} request
|
---|
| 71 | * @property {string} query
|
---|
| 72 | * @property {string} fragment
|
---|
| 73 | * @property {boolean} directory
|
---|
| 74 | * @property {boolean} module
|
---|
| 75 | * @property {boolean} file
|
---|
| 76 | * @property {boolean} internal
|
---|
| 77 | */
|
---|
| 78 |
|
---|
| 79 | /**
|
---|
| 80 | * @typedef {Object} BaseResolveRequest
|
---|
| 81 | * @property {string | false} path
|
---|
| 82 | * @property {string=} descriptionFilePath
|
---|
| 83 | * @property {string=} descriptionFileRoot
|
---|
| 84 | * @property {object=} descriptionFileData
|
---|
| 85 | * @property {string=} relativePath
|
---|
| 86 | * @property {boolean=} ignoreSymlinks
|
---|
| 87 | * @property {boolean=} fullySpecified
|
---|
| 88 | */
|
---|
| 89 |
|
---|
| 90 | /** @typedef {BaseResolveRequest & Partial<ParsedIdentifier>} ResolveRequest */
|
---|
| 91 |
|
---|
| 92 | /**
|
---|
| 93 | * String with special formatting
|
---|
| 94 | * @typedef {string} StackEntry
|
---|
| 95 | */
|
---|
| 96 |
|
---|
| 97 | /** @template T @typedef {{ add: (T) => void }} WriteOnlySet */
|
---|
| 98 |
|
---|
| 99 | /**
|
---|
| 100 | * Resolve context
|
---|
| 101 | * @typedef {Object} ResolveContext
|
---|
| 102 | * @property {WriteOnlySet<string>=} contextDependencies
|
---|
| 103 | * @property {WriteOnlySet<string>=} fileDependencies files that was found on file system
|
---|
| 104 | * @property {WriteOnlySet<string>=} missingDependencies dependencies that was not found on file system
|
---|
| 105 | * @property {Set<StackEntry>=} stack set of hooks' calls. For instance, `resolve → parsedResolve → describedResolve`,
|
---|
| 106 | * @property {(function(string): void)=} log log function
|
---|
| 107 | */
|
---|
| 108 |
|
---|
| 109 | /** @typedef {AsyncSeriesBailHook<[ResolveRequest, ResolveContext], ResolveRequest | null>} ResolveStepHook */
|
---|
| 110 |
|
---|
| 111 | /**
|
---|
| 112 | * @param {string} str input string
|
---|
| 113 | * @returns {string} in camel case
|
---|
| 114 | */
|
---|
| 115 | function toCamelCase(str) {
|
---|
| 116 | return str.replace(/-([a-z])/g, str => str.substr(1).toUpperCase());
|
---|
| 117 | }
|
---|
| 118 |
|
---|
| 119 | class Resolver {
|
---|
| 120 | /**
|
---|
| 121 | * @param {ResolveStepHook} hook hook
|
---|
| 122 | * @param {ResolveRequest} request request
|
---|
| 123 | * @returns {StackEntry} stack entry
|
---|
| 124 | */
|
---|
| 125 | static createStackEntry(hook, request) {
|
---|
| 126 | return (
|
---|
| 127 | hook.name +
|
---|
| 128 | ": (" +
|
---|
| 129 | request.path +
|
---|
| 130 | ") " +
|
---|
| 131 | (request.request || "") +
|
---|
| 132 | (request.query || "") +
|
---|
| 133 | (request.fragment || "") +
|
---|
| 134 | (request.directory ? " directory" : "") +
|
---|
| 135 | (request.module ? " module" : "")
|
---|
| 136 | );
|
---|
| 137 | }
|
---|
| 138 |
|
---|
| 139 | /**
|
---|
| 140 | * @param {FileSystem} fileSystem a filesystem
|
---|
| 141 | * @param {ResolveOptions} options options
|
---|
| 142 | */
|
---|
| 143 | constructor(fileSystem, options) {
|
---|
| 144 | this.fileSystem = fileSystem;
|
---|
| 145 | this.options = options;
|
---|
| 146 | this.hooks = {
|
---|
| 147 | /** @type {SyncHook<[ResolveStepHook, ResolveRequest], void>} */
|
---|
| 148 | resolveStep: new SyncHook(["hook", "request"], "resolveStep"),
|
---|
| 149 | /** @type {SyncHook<[ResolveRequest, Error]>} */
|
---|
| 150 | noResolve: new SyncHook(["request", "error"], "noResolve"),
|
---|
| 151 | /** @type {ResolveStepHook} */
|
---|
| 152 | resolve: new AsyncSeriesBailHook(
|
---|
| 153 | ["request", "resolveContext"],
|
---|
| 154 | "resolve"
|
---|
| 155 | ),
|
---|
| 156 | /** @type {AsyncSeriesHook<[ResolveRequest, ResolveContext]>} */
|
---|
| 157 | result: new AsyncSeriesHook(["result", "resolveContext"], "result")
|
---|
| 158 | };
|
---|
| 159 | }
|
---|
| 160 |
|
---|
| 161 | /**
|
---|
| 162 | * @param {string | ResolveStepHook} name hook name or hook itself
|
---|
| 163 | * @returns {ResolveStepHook} the hook
|
---|
| 164 | */
|
---|
| 165 | ensureHook(name) {
|
---|
| 166 | if (typeof name !== "string") {
|
---|
| 167 | return name;
|
---|
| 168 | }
|
---|
| 169 | name = toCamelCase(name);
|
---|
| 170 | if (/^before/.test(name)) {
|
---|
| 171 | return /** @type {ResolveStepHook} */ (this.ensureHook(
|
---|
| 172 | name[6].toLowerCase() + name.substr(7)
|
---|
| 173 | ).withOptions({
|
---|
| 174 | stage: -10
|
---|
| 175 | }));
|
---|
| 176 | }
|
---|
| 177 | if (/^after/.test(name)) {
|
---|
| 178 | return /** @type {ResolveStepHook} */ (this.ensureHook(
|
---|
| 179 | name[5].toLowerCase() + name.substr(6)
|
---|
| 180 | ).withOptions({
|
---|
| 181 | stage: 10
|
---|
| 182 | }));
|
---|
| 183 | }
|
---|
| 184 | const hook = this.hooks[name];
|
---|
| 185 | if (!hook) {
|
---|
| 186 | return (this.hooks[name] = new AsyncSeriesBailHook(
|
---|
| 187 | ["request", "resolveContext"],
|
---|
| 188 | name
|
---|
| 189 | ));
|
---|
| 190 | }
|
---|
| 191 | return hook;
|
---|
| 192 | }
|
---|
| 193 |
|
---|
| 194 | /**
|
---|
| 195 | * @param {string | ResolveStepHook} name hook name or hook itself
|
---|
| 196 | * @returns {ResolveStepHook} the hook
|
---|
| 197 | */
|
---|
| 198 | getHook(name) {
|
---|
| 199 | if (typeof name !== "string") {
|
---|
| 200 | return name;
|
---|
| 201 | }
|
---|
| 202 | name = toCamelCase(name);
|
---|
| 203 | if (/^before/.test(name)) {
|
---|
| 204 | return /** @type {ResolveStepHook} */ (this.getHook(
|
---|
| 205 | name[6].toLowerCase() + name.substr(7)
|
---|
| 206 | ).withOptions({
|
---|
| 207 | stage: -10
|
---|
| 208 | }));
|
---|
| 209 | }
|
---|
| 210 | if (/^after/.test(name)) {
|
---|
| 211 | return /** @type {ResolveStepHook} */ (this.getHook(
|
---|
| 212 | name[5].toLowerCase() + name.substr(6)
|
---|
| 213 | ).withOptions({
|
---|
| 214 | stage: 10
|
---|
| 215 | }));
|
---|
| 216 | }
|
---|
| 217 | const hook = this.hooks[name];
|
---|
| 218 | if (!hook) {
|
---|
| 219 | throw new Error(`Hook ${name} doesn't exist`);
|
---|
| 220 | }
|
---|
| 221 | return hook;
|
---|
| 222 | }
|
---|
| 223 |
|
---|
| 224 | /**
|
---|
| 225 | * @param {object} context context information object
|
---|
| 226 | * @param {string} path context path
|
---|
| 227 | * @param {string} request request string
|
---|
| 228 | * @returns {string | false} result
|
---|
| 229 | */
|
---|
| 230 | resolveSync(context, path, request) {
|
---|
| 231 | /** @type {Error | null | undefined} */
|
---|
| 232 | let err = undefined;
|
---|
| 233 | /** @type {string | false | undefined} */
|
---|
| 234 | let result = undefined;
|
---|
| 235 | let sync = false;
|
---|
| 236 | this.resolve(context, path, request, {}, (e, r) => {
|
---|
| 237 | err = e;
|
---|
| 238 | result = r;
|
---|
| 239 | sync = true;
|
---|
| 240 | });
|
---|
| 241 | if (!sync) {
|
---|
| 242 | throw new Error(
|
---|
| 243 | "Cannot 'resolveSync' because the fileSystem is not sync. Use 'resolve'!"
|
---|
| 244 | );
|
---|
| 245 | }
|
---|
| 246 | if (err) throw err;
|
---|
| 247 | if (result === undefined) throw new Error("No result");
|
---|
| 248 | return result;
|
---|
| 249 | }
|
---|
| 250 |
|
---|
| 251 | /**
|
---|
| 252 | * @param {object} context context information object
|
---|
| 253 | * @param {string} path context path
|
---|
| 254 | * @param {string} request request string
|
---|
| 255 | * @param {ResolveContext} resolveContext resolve context
|
---|
| 256 | * @param {function(Error | null, (string|false)=, ResolveRequest=): void} callback callback function
|
---|
| 257 | * @returns {void}
|
---|
| 258 | */
|
---|
| 259 | resolve(context, path, request, resolveContext, callback) {
|
---|
| 260 | if (!context || typeof context !== "object")
|
---|
| 261 | return callback(new Error("context argument is not an object"));
|
---|
| 262 | if (typeof path !== "string")
|
---|
| 263 | return callback(new Error("path argument is not a string"));
|
---|
| 264 | if (typeof request !== "string")
|
---|
| 265 | return callback(new Error("path argument is not a string"));
|
---|
| 266 | if (!resolveContext)
|
---|
| 267 | return callback(new Error("resolveContext argument is not set"));
|
---|
| 268 |
|
---|
| 269 | const obj = {
|
---|
| 270 | context: context,
|
---|
| 271 | path: path,
|
---|
| 272 | request: request
|
---|
| 273 | };
|
---|
| 274 |
|
---|
| 275 | const message = `resolve '${request}' in '${path}'`;
|
---|
| 276 |
|
---|
| 277 | const finishResolved = result => {
|
---|
| 278 | return callback(
|
---|
| 279 | null,
|
---|
| 280 | result.path === false
|
---|
| 281 | ? false
|
---|
| 282 | : `${result.path.replace(/#/g, "\0#")}${
|
---|
| 283 | result.query ? result.query.replace(/#/g, "\0#") : ""
|
---|
| 284 | }${result.fragment || ""}`,
|
---|
| 285 | result
|
---|
| 286 | );
|
---|
| 287 | };
|
---|
| 288 |
|
---|
| 289 | const finishWithoutResolve = log => {
|
---|
| 290 | /**
|
---|
| 291 | * @type {Error & {details?: string}}
|
---|
| 292 | */
|
---|
| 293 | const error = new Error("Can't " + message);
|
---|
| 294 | error.details = log.join("\n");
|
---|
| 295 | this.hooks.noResolve.call(obj, error);
|
---|
| 296 | return callback(error);
|
---|
| 297 | };
|
---|
| 298 |
|
---|
| 299 | if (resolveContext.log) {
|
---|
| 300 | // We need log anyway to capture it in case of an error
|
---|
| 301 | const parentLog = resolveContext.log;
|
---|
| 302 | const log = [];
|
---|
| 303 | return this.doResolve(
|
---|
| 304 | this.hooks.resolve,
|
---|
| 305 | obj,
|
---|
| 306 | message,
|
---|
| 307 | {
|
---|
| 308 | log: msg => {
|
---|
| 309 | parentLog(msg);
|
---|
| 310 | log.push(msg);
|
---|
| 311 | },
|
---|
| 312 | fileDependencies: resolveContext.fileDependencies,
|
---|
| 313 | contextDependencies: resolveContext.contextDependencies,
|
---|
| 314 | missingDependencies: resolveContext.missingDependencies,
|
---|
| 315 | stack: resolveContext.stack
|
---|
| 316 | },
|
---|
| 317 | (err, result) => {
|
---|
| 318 | if (err) return callback(err);
|
---|
| 319 |
|
---|
| 320 | if (result) return finishResolved(result);
|
---|
| 321 |
|
---|
| 322 | return finishWithoutResolve(log);
|
---|
| 323 | }
|
---|
| 324 | );
|
---|
| 325 | } else {
|
---|
| 326 | // Try to resolve assuming there is no error
|
---|
| 327 | // We don't log stuff in this case
|
---|
| 328 | return this.doResolve(
|
---|
| 329 | this.hooks.resolve,
|
---|
| 330 | obj,
|
---|
| 331 | message,
|
---|
| 332 | {
|
---|
| 333 | log: undefined,
|
---|
| 334 | fileDependencies: resolveContext.fileDependencies,
|
---|
| 335 | contextDependencies: resolveContext.contextDependencies,
|
---|
| 336 | missingDependencies: resolveContext.missingDependencies,
|
---|
| 337 | stack: resolveContext.stack
|
---|
| 338 | },
|
---|
| 339 | (err, result) => {
|
---|
| 340 | if (err) return callback(err);
|
---|
| 341 |
|
---|
| 342 | if (result) return finishResolved(result);
|
---|
| 343 |
|
---|
| 344 | // log is missing for the error details
|
---|
| 345 | // so we redo the resolving for the log info
|
---|
| 346 | // this is more expensive to the success case
|
---|
| 347 | // is assumed by default
|
---|
| 348 |
|
---|
| 349 | const log = [];
|
---|
| 350 |
|
---|
| 351 | return this.doResolve(
|
---|
| 352 | this.hooks.resolve,
|
---|
| 353 | obj,
|
---|
| 354 | message,
|
---|
| 355 | {
|
---|
| 356 | log: msg => log.push(msg),
|
---|
| 357 | stack: resolveContext.stack
|
---|
| 358 | },
|
---|
| 359 | (err, result) => {
|
---|
| 360 | if (err) return callback(err);
|
---|
| 361 |
|
---|
| 362 | return finishWithoutResolve(log);
|
---|
| 363 | }
|
---|
| 364 | );
|
---|
| 365 | }
|
---|
| 366 | );
|
---|
| 367 | }
|
---|
| 368 | }
|
---|
| 369 |
|
---|
| 370 | doResolve(hook, request, message, resolveContext, callback) {
|
---|
| 371 | const stackEntry = Resolver.createStackEntry(hook, request);
|
---|
| 372 |
|
---|
| 373 | let newStack;
|
---|
| 374 | if (resolveContext.stack) {
|
---|
| 375 | newStack = new Set(resolveContext.stack);
|
---|
| 376 | if (resolveContext.stack.has(stackEntry)) {
|
---|
| 377 | /**
|
---|
| 378 | * Prevent recursion
|
---|
| 379 | * @type {Error & {recursion?: boolean}}
|
---|
| 380 | */
|
---|
| 381 | const recursionError = new Error(
|
---|
| 382 | "Recursion in resolving\nStack:\n " +
|
---|
| 383 | Array.from(newStack).join("\n ")
|
---|
| 384 | );
|
---|
| 385 | recursionError.recursion = true;
|
---|
| 386 | if (resolveContext.log)
|
---|
| 387 | resolveContext.log("abort resolving because of recursion");
|
---|
| 388 | return callback(recursionError);
|
---|
| 389 | }
|
---|
| 390 | newStack.add(stackEntry);
|
---|
| 391 | } else {
|
---|
| 392 | newStack = new Set([stackEntry]);
|
---|
| 393 | }
|
---|
| 394 | this.hooks.resolveStep.call(hook, request);
|
---|
| 395 |
|
---|
| 396 | if (hook.isUsed()) {
|
---|
| 397 | const innerContext = createInnerContext(
|
---|
| 398 | {
|
---|
| 399 | log: resolveContext.log,
|
---|
| 400 | fileDependencies: resolveContext.fileDependencies,
|
---|
| 401 | contextDependencies: resolveContext.contextDependencies,
|
---|
| 402 | missingDependencies: resolveContext.missingDependencies,
|
---|
| 403 | stack: newStack
|
---|
| 404 | },
|
---|
| 405 | message
|
---|
| 406 | );
|
---|
| 407 | return hook.callAsync(request, innerContext, (err, result) => {
|
---|
| 408 | if (err) return callback(err);
|
---|
| 409 | if (result) return callback(null, result);
|
---|
| 410 | callback();
|
---|
| 411 | });
|
---|
| 412 | } else {
|
---|
| 413 | callback();
|
---|
| 414 | }
|
---|
| 415 | }
|
---|
| 416 |
|
---|
| 417 | /**
|
---|
| 418 | * @param {string} identifier identifier
|
---|
| 419 | * @returns {ParsedIdentifier} parsed identifier
|
---|
| 420 | */
|
---|
| 421 | parse(identifier) {
|
---|
| 422 | const part = {
|
---|
| 423 | request: "",
|
---|
| 424 | query: "",
|
---|
| 425 | fragment: "",
|
---|
| 426 | module: false,
|
---|
| 427 | directory: false,
|
---|
| 428 | file: false,
|
---|
| 429 | internal: false
|
---|
| 430 | };
|
---|
| 431 |
|
---|
| 432 | const parsedIdentifier = parseIdentifier(identifier);
|
---|
| 433 |
|
---|
| 434 | if (!parsedIdentifier) return part;
|
---|
| 435 |
|
---|
| 436 | [part.request, part.query, part.fragment] = parsedIdentifier;
|
---|
| 437 |
|
---|
| 438 | if (part.request.length > 0) {
|
---|
| 439 | part.internal = this.isPrivate(identifier);
|
---|
| 440 | part.module = this.isModule(part.request);
|
---|
| 441 | part.directory = this.isDirectory(part.request);
|
---|
| 442 | if (part.directory) {
|
---|
| 443 | part.request = part.request.substr(0, part.request.length - 1);
|
---|
| 444 | }
|
---|
| 445 | }
|
---|
| 446 |
|
---|
| 447 | return part;
|
---|
| 448 | }
|
---|
| 449 |
|
---|
| 450 | isModule(path) {
|
---|
| 451 | return getType(path) === PathType.Normal;
|
---|
| 452 | }
|
---|
| 453 |
|
---|
| 454 | isPrivate(path) {
|
---|
| 455 | return getType(path) === PathType.Internal;
|
---|
| 456 | }
|
---|
| 457 |
|
---|
| 458 | /**
|
---|
| 459 | * @param {string} path a path
|
---|
| 460 | * @returns {boolean} true, if the path is a directory path
|
---|
| 461 | */
|
---|
| 462 | isDirectory(path) {
|
---|
| 463 | return path.endsWith("/");
|
---|
| 464 | }
|
---|
| 465 |
|
---|
| 466 | join(path, request) {
|
---|
| 467 | return join(path, request);
|
---|
| 468 | }
|
---|
| 469 |
|
---|
| 470 | normalize(path) {
|
---|
| 471 | return normalize(path);
|
---|
| 472 | }
|
---|
| 473 | }
|
---|
| 474 |
|
---|
| 475 | module.exports = Resolver;
|
---|