| /* |
| MIT License http://www.opensource.org/licenses/mit-license.php |
| Author Tobias Koppers @sokra |
| */ |
| "use strict"; |
| |
| const path = require("path"); |
| const asyncLib = require("neo-async"); |
| const { |
| Tapable, |
| AsyncSeriesWaterfallHook, |
| SyncWaterfallHook, |
| SyncBailHook, |
| SyncHook, |
| HookMap |
| } = require("tapable"); |
| const NormalModule = require("./NormalModule"); |
| const RawModule = require("./RawModule"); |
| const RuleSet = require("./RuleSet"); |
| const cachedMerge = require("./util/cachedMerge"); |
| |
| const EMPTY_RESOLVE_OPTIONS = {}; |
| |
| const MATCH_RESOURCE_REGEX = /^([^!]+)!=!/; |
| |
| const loaderToIdent = data => { |
| if (!data.options) { |
| return data.loader; |
| } |
| if (typeof data.options === "string") { |
| return data.loader + "?" + data.options; |
| } |
| if (typeof data.options !== "object") { |
| throw new Error("loader options must be string or object"); |
| } |
| if (data.ident) { |
| return data.loader + "??" + data.ident; |
| } |
| return data.loader + "?" + JSON.stringify(data.options); |
| }; |
| |
| const identToLoaderRequest = resultString => { |
| const idx = resultString.indexOf("?"); |
| if (idx >= 0) { |
| const loader = resultString.substr(0, idx); |
| const options = resultString.substr(idx + 1); |
| return { |
| loader, |
| options |
| }; |
| } else { |
| return { |
| loader: resultString, |
| options: undefined |
| }; |
| } |
| }; |
| |
| const dependencyCache = new WeakMap(); |
| |
| class NormalModuleFactory extends Tapable { |
| constructor(context, resolverFactory, options) { |
| super(); |
| this.hooks = { |
| resolver: new SyncWaterfallHook(["resolver"]), |
| factory: new SyncWaterfallHook(["factory"]), |
| beforeResolve: new AsyncSeriesWaterfallHook(["data"]), |
| afterResolve: new AsyncSeriesWaterfallHook(["data"]), |
| createModule: new SyncBailHook(["data"]), |
| module: new SyncWaterfallHook(["module", "data"]), |
| createParser: new HookMap(() => new SyncBailHook(["parserOptions"])), |
| parser: new HookMap(() => new SyncHook(["parser", "parserOptions"])), |
| createGenerator: new HookMap( |
| () => new SyncBailHook(["generatorOptions"]) |
| ), |
| generator: new HookMap( |
| () => new SyncHook(["generator", "generatorOptions"]) |
| ) |
| }; |
| this._pluginCompat.tap("NormalModuleFactory", options => { |
| switch (options.name) { |
| case "before-resolve": |
| case "after-resolve": |
| options.async = true; |
| break; |
| case "parser": |
| this.hooks.parser |
| .for("javascript/auto") |
| .tap(options.fn.name || "unnamed compat plugin", options.fn); |
| return true; |
| } |
| let match; |
| match = /^parser (.+)$/.exec(options.name); |
| if (match) { |
| this.hooks.parser |
| .for(match[1]) |
| .tap( |
| options.fn.name || "unnamed compat plugin", |
| options.fn.bind(this) |
| ); |
| return true; |
| } |
| match = /^create-parser (.+)$/.exec(options.name); |
| if (match) { |
| this.hooks.createParser |
| .for(match[1]) |
| .tap( |
| options.fn.name || "unnamed compat plugin", |
| options.fn.bind(this) |
| ); |
| return true; |
| } |
| }); |
| this.resolverFactory = resolverFactory; |
| this.ruleSet = new RuleSet(options.defaultRules.concat(options.rules)); |
| this.cachePredicate = |
| typeof options.unsafeCache === "function" |
| ? options.unsafeCache |
| : Boolean.bind(null, options.unsafeCache); |
| this.context = context || ""; |
| this.parserCache = Object.create(null); |
| this.generatorCache = Object.create(null); |
| this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => { |
| let resolver = this.hooks.resolver.call(null); |
| |
| // Ignored |
| if (!resolver) return callback(); |
| |
| resolver(result, (err, data) => { |
| if (err) return callback(err); |
| |
| // Ignored |
| if (!data) return callback(); |
| |
| // direct module |
| if (typeof data.source === "function") return callback(null, data); |
| |
| this.hooks.afterResolve.callAsync(data, (err, result) => { |
| if (err) return callback(err); |
| |
| // Ignored |
| if (!result) return callback(); |
| |
| let createdModule = this.hooks.createModule.call(result); |
| if (!createdModule) { |
| if (!result.request) { |
| return callback(new Error("Empty dependency (no request)")); |
| } |
| |
| createdModule = new NormalModule(result); |
| } |
| |
| createdModule = this.hooks.module.call(createdModule, result); |
| |
| return callback(null, createdModule); |
| }); |
| }); |
| }); |
| this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => { |
| const contextInfo = data.contextInfo; |
| const context = data.context; |
| const request = data.request; |
| |
| const loaderResolver = this.getResolver("loader"); |
| const normalResolver = this.getResolver("normal", data.resolveOptions); |
| |
| let matchResource = undefined; |
| let requestWithoutMatchResource = request; |
| const matchResourceMatch = MATCH_RESOURCE_REGEX.exec(request); |
| if (matchResourceMatch) { |
| matchResource = matchResourceMatch[1]; |
| if (/^\.\.?\//.test(matchResource)) { |
| matchResource = path.join(context, matchResource); |
| } |
| requestWithoutMatchResource = request.substr( |
| matchResourceMatch[0].length |
| ); |
| } |
| |
| const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!"); |
| const noAutoLoaders = |
| noPreAutoLoaders || requestWithoutMatchResource.startsWith("!"); |
| const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!"); |
| let elements = requestWithoutMatchResource |
| .replace(/^-?!+/, "") |
| .replace(/!!+/g, "!") |
| .split("!"); |
| let resource = elements.pop(); |
| elements = elements.map(identToLoaderRequest); |
| |
| asyncLib.parallel( |
| [ |
| callback => |
| this.resolveRequestArray( |
| contextInfo, |
| context, |
| elements, |
| loaderResolver, |
| callback |
| ), |
| callback => { |
| if (resource === "" || resource[0] === "?") { |
| return callback(null, { |
| resource |
| }); |
| } |
| |
| normalResolver.resolve( |
| contextInfo, |
| context, |
| resource, |
| {}, |
| (err, resource, resourceResolveData) => { |
| if (err) return callback(err); |
| callback(null, { |
| resourceResolveData, |
| resource |
| }); |
| } |
| ); |
| } |
| ], |
| (err, results) => { |
| if (err) return callback(err); |
| let loaders = results[0]; |
| const resourceResolveData = results[1].resourceResolveData; |
| resource = results[1].resource; |
| |
| // translate option idents |
| try { |
| for (const item of loaders) { |
| if (typeof item.options === "string" && item.options[0] === "?") { |
| const ident = item.options.substr(1); |
| item.options = this.ruleSet.findOptionsByIdent(ident); |
| item.ident = ident; |
| } |
| } |
| } catch (e) { |
| return callback(e); |
| } |
| |
| if (resource === false) { |
| // ignored |
| return callback( |
| null, |
| new RawModule( |
| "/* (ignored) */", |
| `ignored ${context} ${request}`, |
| `${request} (ignored)` |
| ) |
| ); |
| } |
| |
| const userRequest = |
| (matchResource !== undefined ? `${matchResource}!=!` : "") + |
| loaders |
| .map(loaderToIdent) |
| .concat([resource]) |
| .join("!"); |
| |
| let resourcePath = |
| matchResource !== undefined ? matchResource : resource; |
| let resourceQuery = ""; |
| const queryIndex = resourcePath.indexOf("?"); |
| if (queryIndex >= 0) { |
| resourceQuery = resourcePath.substr(queryIndex); |
| resourcePath = resourcePath.substr(0, queryIndex); |
| } |
| |
| const result = this.ruleSet.exec({ |
| resource: resourcePath, |
| realResource: |
| matchResource !== undefined |
| ? resource.replace(/\?.*/, "") |
| : resourcePath, |
| resourceQuery, |
| issuer: contextInfo.issuer, |
| compiler: contextInfo.compiler |
| }); |
| const settings = {}; |
| const useLoadersPost = []; |
| const useLoaders = []; |
| const useLoadersPre = []; |
| for (const r of result) { |
| if (r.type === "use") { |
| if (r.enforce === "post" && !noPrePostAutoLoaders) { |
| useLoadersPost.push(r.value); |
| } else if ( |
| r.enforce === "pre" && |
| !noPreAutoLoaders && |
| !noPrePostAutoLoaders |
| ) { |
| useLoadersPre.push(r.value); |
| } else if ( |
| !r.enforce && |
| !noAutoLoaders && |
| !noPrePostAutoLoaders |
| ) { |
| useLoaders.push(r.value); |
| } |
| } else if ( |
| typeof r.value === "object" && |
| r.value !== null && |
| typeof settings[r.type] === "object" && |
| settings[r.type] !== null |
| ) { |
| settings[r.type] = cachedMerge(settings[r.type], r.value); |
| } else { |
| settings[r.type] = r.value; |
| } |
| } |
| asyncLib.parallel( |
| [ |
| this.resolveRequestArray.bind( |
| this, |
| contextInfo, |
| this.context, |
| useLoadersPost, |
| loaderResolver |
| ), |
| this.resolveRequestArray.bind( |
| this, |
| contextInfo, |
| this.context, |
| useLoaders, |
| loaderResolver |
| ), |
| this.resolveRequestArray.bind( |
| this, |
| contextInfo, |
| this.context, |
| useLoadersPre, |
| loaderResolver |
| ) |
| ], |
| (err, results) => { |
| if (err) return callback(err); |
| if (matchResource === undefined) { |
| loaders = results[0].concat(loaders, results[1], results[2]); |
| } else { |
| loaders = results[0].concat(results[1], loaders, results[2]); |
| } |
| process.nextTick(() => { |
| const type = settings.type; |
| const resolveOptions = settings.resolve; |
| callback(null, { |
| context: context, |
| request: loaders |
| .map(loaderToIdent) |
| .concat([resource]) |
| .join("!"), |
| dependencies: data.dependencies, |
| userRequest, |
| rawRequest: request, |
| loaders, |
| resource, |
| matchResource, |
| resourceResolveData, |
| settings, |
| type, |
| parser: this.getParser(type, settings.parser), |
| generator: this.getGenerator(type, settings.generator), |
| resolveOptions |
| }); |
| }); |
| } |
| ); |
| } |
| ); |
| }); |
| } |
| |
| create(data, callback) { |
| const dependencies = data.dependencies; |
| const cacheEntry = dependencyCache.get(dependencies[0]); |
| if (cacheEntry) return callback(null, cacheEntry); |
| const context = data.context || this.context; |
| const resolveOptions = data.resolveOptions || EMPTY_RESOLVE_OPTIONS; |
| const request = dependencies[0].request; |
| const contextInfo = data.contextInfo || {}; |
| this.hooks.beforeResolve.callAsync( |
| { |
| contextInfo, |
| resolveOptions, |
| context, |
| request, |
| dependencies |
| }, |
| (err, result) => { |
| if (err) return callback(err); |
| |
| // Ignored |
| if (!result) return callback(); |
| |
| const factory = this.hooks.factory.call(null); |
| |
| // Ignored |
| if (!factory) return callback(); |
| |
| factory(result, (err, module) => { |
| if (err) return callback(err); |
| |
| if (module && this.cachePredicate(module)) { |
| for (const d of dependencies) { |
| dependencyCache.set(d, module); |
| } |
| } |
| |
| callback(null, module); |
| }); |
| } |
| ); |
| } |
| |
| resolveRequestArray(contextInfo, context, array, resolver, callback) { |
| if (array.length === 0) return callback(null, []); |
| asyncLib.map( |
| array, |
| (item, callback) => { |
| resolver.resolve( |
| contextInfo, |
| context, |
| item.loader, |
| {}, |
| (err, result) => { |
| if ( |
| err && |
| /^[^/]*$/.test(item.loader) && |
| !/-loader$/.test(item.loader) |
| ) { |
| return resolver.resolve( |
| contextInfo, |
| context, |
| item.loader + "-loader", |
| {}, |
| err2 => { |
| if (!err2) { |
| err.message = |
| err.message + |
| "\n" + |
| "BREAKING CHANGE: It's no longer allowed to omit the '-loader' suffix when using loaders.\n" + |
| ` You need to specify '${item.loader}-loader' instead of '${item.loader}',\n` + |
| " see https://webpack.js.org/migrate/3/#automatic-loader-module-name-extension-removed"; |
| } |
| callback(err); |
| } |
| ); |
| } |
| if (err) return callback(err); |
| |
| const optionsOnly = item.options |
| ? { |
| options: item.options |
| } |
| : undefined; |
| return callback( |
| null, |
| Object.assign({}, item, identToLoaderRequest(result), optionsOnly) |
| ); |
| } |
| ); |
| }, |
| callback |
| ); |
| } |
| |
| getParser(type, parserOptions) { |
| let ident = type; |
| if (parserOptions) { |
| if (parserOptions.ident) { |
| ident = `${type}|${parserOptions.ident}`; |
| } else { |
| ident = JSON.stringify([type, parserOptions]); |
| } |
| } |
| if (ident in this.parserCache) { |
| return this.parserCache[ident]; |
| } |
| return (this.parserCache[ident] = this.createParser(type, parserOptions)); |
| } |
| |
| createParser(type, parserOptions = {}) { |
| const parser = this.hooks.createParser.for(type).call(parserOptions); |
| if (!parser) { |
| throw new Error(`No parser registered for ${type}`); |
| } |
| this.hooks.parser.for(type).call(parser, parserOptions); |
| return parser; |
| } |
| |
| getGenerator(type, generatorOptions) { |
| let ident = type; |
| if (generatorOptions) { |
| if (generatorOptions.ident) { |
| ident = `${type}|${generatorOptions.ident}`; |
| } else { |
| ident = JSON.stringify([type, generatorOptions]); |
| } |
| } |
| if (ident in this.generatorCache) { |
| return this.generatorCache[ident]; |
| } |
| return (this.generatorCache[ident] = this.createGenerator( |
| type, |
| generatorOptions |
| )); |
| } |
| |
| createGenerator(type, generatorOptions = {}) { |
| const generator = this.hooks.createGenerator |
| .for(type) |
| .call(generatorOptions); |
| if (!generator) { |
| throw new Error(`No generator registered for ${type}`); |
| } |
| this.hooks.generator.for(type).call(generator, generatorOptions); |
| return generator; |
| } |
| |
| getResolver(type, resolveOptions) { |
| return this.resolverFactory.get( |
| type, |
| resolveOptions || EMPTY_RESOLVE_OPTIONS |
| ); |
| } |
| } |
| |
| module.exports = NormalModuleFactory; |