/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const util = require("util"); const ExternalModule = require("./ExternalModule"); const ContextElementDependency = require("./dependencies/ContextElementDependency"); const CssImportDependency = require("./dependencies/CssImportDependency"); const HarmonyImportDependency = require("./dependencies/HarmonyImportDependency"); const ImportDependency = require("./dependencies/ImportDependency"); const { resolveByProperty, cachedSetProperty } = require("./util/cleverMerge"); /** @typedef {import("../declarations/WebpackOptions").Externals} Externals */ /** @typedef {import("./Compilation").DepConstructor} DepConstructor */ /** @typedef {import("./ExternalModule").DependencyMeta} DependencyMeta */ /** @typedef {import("./Module")} Module */ /** @typedef {import("./NormalModuleFactory")} NormalModuleFactory */ const UNSPECIFIED_EXTERNAL_TYPE_REGEXP = /^[a-z0-9-]+ /; const EMPTY_RESOLVE_OPTIONS = {}; // TODO webpack 6 remove this const callDeprecatedExternals = util.deprecate( (externalsFunction, context, request, cb) => { // eslint-disable-next-line no-useless-call externalsFunction.call(null, context, request, cb); }, "The externals-function should be defined like ({context, request}, cb) => { ... }", "DEP_WEBPACK_EXTERNALS_FUNCTION_PARAMETERS" ); const cache = new WeakMap(); /** * @param {object} obj obj * @param {TODO} layer layer * @returns {object} result */ const resolveLayer = (obj, layer) => { let map = cache.get(obj); if (map === undefined) { map = new Map(); cache.set(obj, map); } else { const cacheEntry = map.get(layer); if (cacheEntry !== undefined) return cacheEntry; } const result = resolveByProperty(obj, "byLayer", layer); map.set(layer, result); return result; }; /** @typedef {string|string[]|boolean|Record} ExternalValue */ /** @typedef {string|undefined} ExternalType */ class ExternalModuleFactoryPlugin { /** * @param {string | undefined} type default external type * @param {Externals} externals externals config */ constructor(type, externals) { this.type = type; this.externals = externals; } /** * @param {NormalModuleFactory} normalModuleFactory the normal module factory * @returns {void} */ apply(normalModuleFactory) { const globalType = this.type; normalModuleFactory.hooks.factorize.tapAsync( "ExternalModuleFactoryPlugin", (data, callback) => { const context = data.context; const contextInfo = data.contextInfo; const dependency = data.dependencies[0]; const dependencyType = data.dependencyType; /** * @param {ExternalValue} value the external config * @param {ExternalType | undefined} type type of external * @param {function((Error | null)=, ExternalModule=): void} callback callback * @returns {void} */ const handleExternal = (value, type, callback) => { if (value === false) { // Not externals, fallback to original factory return callback(); } /** @type {string | string[] | Record} */ let externalConfig = value === true ? dependency.request : value; // When no explicit type is specified, extract it from the externalConfig if (type === undefined) { if ( typeof externalConfig === "string" && UNSPECIFIED_EXTERNAL_TYPE_REGEXP.test(externalConfig) ) { const idx = externalConfig.indexOf(" "); type = externalConfig.slice(0, idx); externalConfig = externalConfig.slice(idx + 1); } else if ( Array.isArray(externalConfig) && externalConfig.length > 0 && UNSPECIFIED_EXTERNAL_TYPE_REGEXP.test(externalConfig[0]) ) { const firstItem = externalConfig[0]; const idx = firstItem.indexOf(" "); type = firstItem.slice(0, idx); externalConfig = [ firstItem.slice(idx + 1), ...externalConfig.slice(1) ]; } } // TODO make it pluggable/add hooks to `ExternalModule` to allow output modules own externals? /** @type {DependencyMeta | undefined} */ let dependencyMeta; if ( dependency instanceof HarmonyImportDependency || dependency instanceof ImportDependency || dependency instanceof ContextElementDependency ) { const externalType = dependency instanceof HarmonyImportDependency ? "module" : dependency instanceof ImportDependency ? "import" : undefined; dependencyMeta = { attributes: dependency.assertions, externalType }; } else if (dependency instanceof CssImportDependency) { dependencyMeta = { layer: dependency.layer, supports: dependency.supports, media: dependency.media }; } callback( null, new ExternalModule( externalConfig, /** @type {string} */ (type || globalType), dependency.request, dependencyMeta ) ); }; /** * @param {Externals} externals externals config * @param {function((Error | null)=, ExternalModule=): void} callback callback * @returns {void} */ const handleExternals = (externals, callback) => { if (typeof externals === "string") { if (externals === dependency.request) { return handleExternal(dependency.request, undefined, callback); } } else if (Array.isArray(externals)) { let i = 0; const next = () => { /** @type {boolean | undefined} */ let asyncFlag; /** * @param {(Error | null)=} err err * @param {ExternalModule=} module module * @returns {void} */ const handleExternalsAndCallback = (err, module) => { if (err) return callback(err); if (!module) { if (asyncFlag) { asyncFlag = false; return; } return next(); } callback(null, module); }; do { asyncFlag = true; if (i >= externals.length) return callback(); handleExternals(externals[i++], handleExternalsAndCallback); } while (!asyncFlag); asyncFlag = false; }; next(); return; } else if (externals instanceof RegExp) { if (externals.test(dependency.request)) { return handleExternal(dependency.request, undefined, callback); } } else if (typeof externals === "function") { const cb = (err, value, type) => { if (err) return callback(err); if (value !== undefined) { handleExternal(value, type, callback); } else { callback(); } }; if (externals.length === 3) { // TODO webpack 6 remove this callDeprecatedExternals( externals, context, dependency.request, cb ); } else { const promise = externals( { context, request: dependency.request, dependencyType, contextInfo, getResolve: options => (context, request, callback) => { const resolveContext = { fileDependencies: data.fileDependencies, missingDependencies: data.missingDependencies, contextDependencies: data.contextDependencies }; let resolver = normalModuleFactory.getResolver( "normal", dependencyType ? cachedSetProperty( data.resolveOptions || EMPTY_RESOLVE_OPTIONS, "dependencyType", dependencyType ) : data.resolveOptions ); if (options) resolver = resolver.withOptions(options); if (callback) { resolver.resolve( {}, context, request, resolveContext, callback ); } else { return new Promise((resolve, reject) => { resolver.resolve( {}, context, request, resolveContext, (err, result) => { if (err) reject(err); else resolve(result); } ); }); } } }, cb ); if (promise && promise.then) promise.then(r => cb(null, r), cb); } return; } else if (typeof externals === "object") { const resolvedExternals = resolveLayer( externals, contextInfo.issuerLayer ); if ( Object.prototype.hasOwnProperty.call( resolvedExternals, dependency.request ) ) { return handleExternal( resolvedExternals[dependency.request], undefined, callback ); } } callback(); }; handleExternals(this.externals, callback); } ); } } module.exports = ExternalModuleFactoryPlugin;