| /* |
| MIT License http://www.opensource.org/licenses/mit-license.php |
| Author Tobias Koppers @sokra |
| */ |
| "use strict"; |
| |
| const util = require("util"); |
| |
| const Tapable = require("tapable/lib/Tapable"); |
| const SyncHook = require("tapable/lib/SyncHook"); |
| const AsyncSeriesBailHook = require("tapable/lib/AsyncSeriesBailHook"); |
| const AsyncSeriesHook = require("tapable/lib/AsyncSeriesHook"); |
| const createInnerContext = require("./createInnerContext"); |
| |
| const REGEXP_NOT_MODULE = /^\.$|^\.[\\\/]|^\.\.$|^\.\.[\/\\]|^\/|^[A-Z]:[\\\/]/i; |
| const REGEXP_DIRECTORY = /[\/\\]$/i; |
| |
| const memoryFsJoin = require("memory-fs/lib/join"); |
| const memoizedJoin = new Map(); |
| const memoryFsNormalize = require("memory-fs/lib/normalize"); |
| |
| function withName(name, hook) { |
| hook.name = name; |
| return hook; |
| } |
| |
| function toCamelCase(str) { |
| return str.replace(/-([a-z])/g, str => str.substr(1).toUpperCase()); |
| } |
| |
| const deprecatedPushToMissing = util.deprecate((set, item) => { |
| set.add(item); |
| }, "Resolver: 'missing' is now a Set. Use add instead of push."); |
| |
| const deprecatedResolveContextInCallback = util.deprecate((x) => { |
| return x; |
| }, "Resolver: The callback argument was splitted into resolveContext and callback."); |
| |
| const deprecatedHookAsString = util.deprecate((x) => { |
| return x; |
| }, "Resolver#doResolve: The type arguments (string) is now a hook argument (Hook). Pass a reference to the hook instead."); |
| |
| class Resolver extends Tapable { |
| constructor(fileSystem) { |
| super(); |
| this.fileSystem = fileSystem; |
| this.hooks = { |
| resolveStep: withName("resolveStep", new SyncHook(["hook", "request"])), |
| noResolve: withName("noResolve", new SyncHook(["request", "error"])), |
| resolve: withName("resolve", new AsyncSeriesBailHook(["request", "resolveContext"])), |
| result: new AsyncSeriesHook(["result", "resolveContext"]) |
| }; |
| this._pluginCompat.tap("Resolver: before/after", options => { |
| if(/^before-/.test(options.name)) { |
| options.name = options.name.substr(7); |
| options.stage = -10; |
| } else if(/^after-/.test(options.name)) { |
| options.name = options.name.substr(6); |
| options.stage = 10; |
| } |
| }); |
| this._pluginCompat.tap("Resolver: step hooks", options => { |
| const name = options.name; |
| const stepHook = !/^resolve(-s|S)tep$|^no(-r|R)esolve$/.test(name); |
| if(stepHook) { |
| options.async = true; |
| this.ensureHook(name); |
| const fn = options.fn; |
| options.fn = (request, resolverContext, callback) => { |
| const innerCallback = (err, result) => { |
| if(err) return callback(err); |
| if(result !== undefined) return callback(null, result); |
| callback(); |
| }; |
| for(const key in resolverContext) { |
| innerCallback[key] = resolverContext[key]; |
| } |
| fn.call(this, request, innerCallback); |
| }; |
| } |
| }); |
| } |
| |
| ensureHook(name) { |
| if(typeof name !== "string") return name; |
| name = toCamelCase(name); |
| if(/^before/.test(name)) { |
| return this.ensureHook(name[6].toLowerCase() + name.substr(7)).withOptions({ |
| stage: -10 |
| }); |
| } |
| if(/^after/.test(name)) { |
| return this.ensureHook(name[5].toLowerCase() + name.substr(6)).withOptions({ |
| stage: 10 |
| }); |
| } |
| const hook = this.hooks[name]; |
| if(!hook) { |
| return this.hooks[name] = withName(name, new AsyncSeriesBailHook(["request", "resolveContext"])); |
| } |
| return hook; |
| } |
| |
| getHook(name) { |
| if(typeof name !== "string") return name; |
| name = toCamelCase(name); |
| if(/^before/.test(name)) { |
| return this.getHook(name[6].toLowerCase() + name.substr(7)).withOptions({ |
| stage: -10 |
| }); |
| } |
| if(/^after/.test(name)) { |
| return this.getHook(name[5].toLowerCase() + name.substr(6)).withOptions({ |
| stage: 10 |
| }); |
| } |
| const hook = this.hooks[name]; |
| if(!hook) { |
| throw new Error(`Hook ${name} doesn't exist`); |
| } |
| return hook; |
| } |
| |
| resolveSync(context, path, request) { |
| let err, result, 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; |
| return result; |
| } |
| |
| resolve(context, path, request, resolveContext, callback) { |
| // TODO remove in enhanced-resolve 5 |
| // For backward compatiblity START |
| if(typeof callback !== "function") { |
| callback = deprecatedResolveContextInCallback(resolveContext); |
| // resolveContext is a function containing additional properties |
| // It's now used for resolveContext and callback |
| } |
| // END |
| const obj = { |
| context: context, |
| path: path, |
| request: request |
| }; |
| |
| const message = "resolve '" + request + "' in '" + path + "'"; |
| |
| // Try to resolve assuming there is no error |
| // We don't log stuff in this case |
| return this.doResolve(this.hooks.resolve, obj, message, { |
| missing: resolveContext.missing, |
| stack: resolveContext.stack |
| }, (err, result) => { |
| if(!err && result) { |
| return callback(null, result.path === false ? false : result.path + (result.query || ""), result); |
| } |
| |
| const localMissing = new Set(); |
| // TODO remove in enhanced-resolve 5 |
| localMissing.push = item => deprecatedPushToMissing(localMissing, item); |
| const log = []; |
| |
| return this.doResolve(this.hooks.resolve, obj, message, { |
| log: msg => { |
| if(resolveContext.log) { |
| resolveContext.log(msg); |
| } |
| log.push(msg); |
| }, |
| missing: localMissing, |
| stack: resolveContext.stack |
| }, (err, result) => { |
| if(err) return callback(err); |
| |
| const error = new Error("Can't " + message); |
| error.details = log.join("\n"); |
| error.missing = Array.from(localMissing); |
| this.hooks.noResolve.call(obj, error); |
| return callback(error); |
| }); |
| }); |
| } |
| |
| doResolve(hook, request, message, resolveContext, callback) { |
| // TODO remove in enhanced-resolve 5 |
| // For backward compatiblity START |
| if(typeof callback !== "function") { |
| callback = deprecatedResolveContextInCallback(resolveContext); |
| // resolveContext is a function containing additional properties |
| // It's now used for resolveContext and callback |
| } |
| if(typeof hook === "string") { |
| const name = toCamelCase(hook); |
| hook = deprecatedHookAsString(this.hooks[name]); |
| if(!hook) { |
| throw new Error(`Hook "${name}" doesn't exist`); |
| } |
| } |
| // END |
| if(typeof callback !== "function") throw new Error("callback is not a function " + Array.from(arguments)); |
| if(!resolveContext) throw new Error("resolveContext is not an object " + Array.from(arguments)); |
| |
| const stackLine = hook.name + ": (" + request.path + ") " + |
| (request.request || "") + (request.query || "") + |
| (request.directory ? " directory" : "") + |
| (request.module ? " module" : ""); |
| |
| let newStack; |
| if(resolveContext.stack) { |
| newStack = new Set(resolveContext.stack); |
| if(resolveContext.stack.has(stackLine)) { |
| // Prevent recursion |
| 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(stackLine); |
| } else { |
| newStack = new Set([stackLine]); |
| } |
| this.hooks.resolveStep.call(hook, request); |
| |
| if(hook.isUsed()) { |
| const innerContext = createInnerContext({ |
| log: resolveContext.log, |
| missing: resolveContext.missing, |
| stack: newStack |
| }, message); |
| return hook.callAsync(request, innerContext, (err, result) => { |
| if(err) return callback(err); |
| if(result) return callback(null, result); |
| callback(); |
| }); |
| } else { |
| callback(); |
| } |
| } |
| |
| parse(identifier) { |
| if(identifier === "") return null; |
| const part = { |
| request: "", |
| query: "", |
| module: false, |
| directory: false, |
| file: false |
| }; |
| const idxQuery = identifier.indexOf("?"); |
| if(idxQuery === 0) { |
| part.query = identifier; |
| } else if(idxQuery > 0) { |
| part.request = identifier.slice(0, idxQuery); |
| part.query = identifier.slice(idxQuery); |
| } else { |
| part.request = identifier; |
| } |
| if(part.request) { |
| part.module = this.isModule(part.request); |
| part.directory = this.isDirectory(part.request); |
| if(part.directory) { |
| part.request = part.request.substr(0, part.request.length - 1); |
| } |
| } |
| return part; |
| } |
| |
| isModule(path) { |
| return !REGEXP_NOT_MODULE.test(path); |
| } |
| |
| isDirectory(path) { |
| return REGEXP_DIRECTORY.test(path); |
| } |
| |
| join(path, request) { |
| let cacheEntry; |
| let pathCache = memoizedJoin.get(path); |
| if(typeof pathCache === "undefined") { |
| memoizedJoin.set(path, pathCache = new Map()); |
| } else { |
| cacheEntry = pathCache.get(request); |
| if(typeof cacheEntry !== "undefined") |
| return cacheEntry; |
| } |
| cacheEntry = memoryFsJoin(path, request); |
| pathCache.set(request, cacheEntry); |
| return cacheEntry; |
| } |
| |
| normalize(path) { |
| return memoryFsNormalize(path); |
| } |
| } |
| |
| module.exports = Resolver; |