ExternalModuleFactoryPlugin.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const util = require("util");
  7. const ExternalModule = require("./ExternalModule");
  8. const ContextElementDependency = require("./dependencies/ContextElementDependency");
  9. const CssImportDependency = require("./dependencies/CssImportDependency");
  10. const CssUrlDependency = require("./dependencies/CssUrlDependency");
  11. const HarmonyImportDependency = require("./dependencies/HarmonyImportDependency");
  12. const ImportDependency = require("./dependencies/ImportDependency");
  13. const { cachedSetProperty, resolveByProperty } = require("./util/cleverMerge");
  14. /** @typedef {import("enhanced-resolve").ResolveContext} ResolveContext */
  15. /** @typedef {import("../declarations/WebpackOptions").ResolveOptions} ResolveOptions */
  16. /** @typedef {import("../declarations/WebpackOptions").ExternalsType} ExternalsType */
  17. /** @typedef {import("../declarations/WebpackOptions").ExternalItem} ExternalItem */
  18. /** @typedef {import("../declarations/WebpackOptions").ExternalItemValue} ExternalItemValue */
  19. /** @typedef {import("../declarations/WebpackOptions").ExternalItemObjectKnown} ExternalItemObjectKnown */
  20. /** @typedef {import("../declarations/WebpackOptions").ExternalItemObjectUnknown} ExternalItemObjectUnknown */
  21. /** @typedef {import("../declarations/WebpackOptions").Externals} Externals */
  22. /** @typedef {import("./Dependency")} Dependency */
  23. /** @typedef {import("./ExternalModule").DependencyMeta} DependencyMeta */
  24. /** @typedef {import("./ModuleFactory").IssuerLayer} IssuerLayer */
  25. /** @typedef {import("./ModuleFactory").ModuleFactoryCreateDataContextInfo} ModuleFactoryCreateDataContextInfo */
  26. /** @typedef {import("./NormalModuleFactory")} NormalModuleFactory */
  27. /** @typedef {((context: string, request: string, callback: (err?: Error | null, result?: string | false, resolveRequest?: import("enhanced-resolve").ResolveRequest) => void) => void)} ExternalItemFunctionDataGetResolveCallbackResult */
  28. /** @typedef {((context: string, request: string) => Promise<string>)} ExternalItemFunctionDataGetResolveResult */
  29. /** @typedef {(options?: ResolveOptions) => ExternalItemFunctionDataGetResolveCallbackResult | ExternalItemFunctionDataGetResolveResult} ExternalItemFunctionDataGetResolve */
  30. /**
  31. * Defines the external item function data type used by this module.
  32. * @typedef {object} ExternalItemFunctionData
  33. * @property {string} context the directory in which the request is placed
  34. * @property {ModuleFactoryCreateDataContextInfo} contextInfo contextual information
  35. * @property {string} dependencyType the category of the referencing dependency
  36. * @property {ExternalItemFunctionDataGetResolve} getResolve get a resolve function with the current resolver options
  37. * @property {string} request the request as written by the user in the require/import expression/statement
  38. */
  39. /** @typedef {((data: ExternalItemFunctionData, callback: (err?: (Error | null), result?: ExternalItemValue) => void) => void)} ExternalItemFunctionCallback */
  40. /** @typedef {((data: import("../lib/ExternalModuleFactoryPlugin").ExternalItemFunctionData) => Promise<ExternalItemValue>)} ExternalItemFunctionPromise */
  41. const UNSPECIFIED_EXTERNAL_TYPE_REGEXP = /^[a-z0-9-]+ /;
  42. const EMPTY_RESOLVE_OPTIONS = {};
  43. // TODO webpack 6 remove this
  44. const callDeprecatedExternals = util.deprecate(
  45. /**
  46. * Handles the callback logic for this hook.
  47. * @param {EXPECTED_FUNCTION} externalsFunction externals function
  48. * @param {string} context context
  49. * @param {string} request request
  50. * @param {(err: Error | null | undefined, value: ExternalValue | undefined, ty: ExternalsType | undefined) => void} cb cb
  51. */
  52. (externalsFunction, context, request, cb) => {
  53. // eslint-disable-next-line no-useless-call
  54. externalsFunction.call(null, context, request, cb);
  55. },
  56. "The externals-function should be defined like ({context, request}, cb) => { ... }",
  57. "DEP_WEBPACK_EXTERNALS_FUNCTION_PARAMETERS"
  58. );
  59. /** @typedef {(layer: string | null) => ExternalItem} ExternalItemByLayerFn */
  60. /** @typedef {ExternalItemObjectKnown & ExternalItemObjectUnknown} ExternalItemObject */
  61. /**
  62. * Defines the external weak cache type used by this module.
  63. * @template {ExternalItemObject} T
  64. * @typedef {WeakMap<T, Map<IssuerLayer, Omit<T, "byLayer">>>} ExternalWeakCache
  65. */
  66. /** @type {ExternalWeakCache<ExternalItemObject>} */
  67. const cache = new WeakMap();
  68. /**
  69. * Returns result.
  70. * @param {ExternalItemObject} obj obj
  71. * @param {IssuerLayer} layer layer
  72. * @returns {Omit<ExternalItemObject, "byLayer">} result
  73. */
  74. const resolveLayer = (obj, layer) => {
  75. let map = cache.get(obj);
  76. if (map === undefined) {
  77. map = new Map();
  78. cache.set(obj, map);
  79. } else {
  80. const cacheEntry = map.get(layer);
  81. if (cacheEntry !== undefined) return cacheEntry;
  82. }
  83. const result = resolveByProperty(obj, "byLayer", layer);
  84. map.set(layer, result);
  85. return result;
  86. };
  87. /** @typedef {string | string[] | boolean | Record<string, string | string[]>} ExternalValue */
  88. const PLUGIN_NAME = "ExternalModuleFactoryPlugin";
  89. class ExternalModuleFactoryPlugin {
  90. /**
  91. * Creates an instance of ExternalModuleFactoryPlugin.
  92. * @param {ExternalsType | ((dependency: Dependency) => ExternalsType)} type default external type
  93. * @param {Externals} externals externals config
  94. */
  95. constructor(type, externals) {
  96. this.type = type;
  97. this.externals = externals;
  98. }
  99. /**
  100. * Applies the plugin by registering its hooks on the compiler.
  101. * @param {NormalModuleFactory} normalModuleFactory the normal module factory
  102. * @returns {void}
  103. */
  104. apply(normalModuleFactory) {
  105. const globalType = this.type;
  106. normalModuleFactory.hooks.factorize.tapAsync(
  107. PLUGIN_NAME,
  108. (data, callback) => {
  109. const context = data.context;
  110. const contextInfo = data.contextInfo;
  111. const dependency = data.dependencies[0];
  112. const dependencyType = data.dependencyType;
  113. /** @typedef {(err?: Error | null, externalModule?: ExternalModule) => void} HandleExternalCallback */
  114. /**
  115. * Processes the provided value.
  116. * @param {ExternalValue} value the external config
  117. * @param {ExternalsType | undefined} type type of external
  118. * @param {HandleExternalCallback} callback callback
  119. * @returns {void}
  120. */
  121. const handleExternal = (value, type, callback) => {
  122. if (value === false) {
  123. // Not externals, fallback to original factory
  124. return callback();
  125. }
  126. /** @type {ExternalValue} */
  127. let externalConfig = value === true ? dependency.request : value;
  128. // When no explicit type is specified, extract it from the externalConfig
  129. if (type === undefined) {
  130. if (
  131. typeof externalConfig === "string" &&
  132. UNSPECIFIED_EXTERNAL_TYPE_REGEXP.test(externalConfig)
  133. ) {
  134. const idx = externalConfig.indexOf(" ");
  135. type =
  136. /** @type {ExternalsType} */
  137. (externalConfig.slice(0, idx));
  138. externalConfig = externalConfig.slice(idx + 1);
  139. } else if (
  140. Array.isArray(externalConfig) &&
  141. externalConfig.length > 0 &&
  142. UNSPECIFIED_EXTERNAL_TYPE_REGEXP.test(externalConfig[0])
  143. ) {
  144. const firstItem = externalConfig[0];
  145. const idx = firstItem.indexOf(" ");
  146. type = /** @type {ExternalsType} */ (firstItem.slice(0, idx));
  147. externalConfig = [
  148. firstItem.slice(idx + 1),
  149. ...externalConfig.slice(1)
  150. ];
  151. }
  152. }
  153. const defaultType =
  154. typeof globalType === "function"
  155. ? globalType(dependency)
  156. : globalType;
  157. const resolvedType = type || defaultType;
  158. // TODO make it pluggable/add hooks to `ExternalModule` to allow output modules own externals?
  159. /** @type {DependencyMeta | undefined} */
  160. let dependencyMeta;
  161. if (
  162. dependency instanceof HarmonyImportDependency ||
  163. dependency instanceof ImportDependency ||
  164. dependency instanceof ContextElementDependency
  165. ) {
  166. const externalType =
  167. dependency instanceof HarmonyImportDependency
  168. ? "module"
  169. : dependency instanceof ImportDependency
  170. ? "import"
  171. : undefined;
  172. dependencyMeta = {
  173. attributes: dependency.attributes,
  174. externalType
  175. };
  176. } else if (dependency instanceof CssImportDependency) {
  177. dependencyMeta = {
  178. layer: dependency.layer,
  179. supports: dependency.supports,
  180. media: dependency.media
  181. };
  182. }
  183. if (
  184. resolvedType === "asset" &&
  185. dependency instanceof CssUrlDependency
  186. ) {
  187. dependencyMeta = { sourceType: "css-url" };
  188. }
  189. callback(
  190. null,
  191. new ExternalModule(
  192. externalConfig,
  193. resolvedType,
  194. dependency.request,
  195. dependencyMeta
  196. )
  197. );
  198. };
  199. /**
  200. * Processes the provided external.
  201. * @param {Externals} externals externals config
  202. * @param {HandleExternalCallback} callback callback
  203. * @returns {void}
  204. */
  205. const handleExternals = (externals, callback) => {
  206. if (typeof externals === "string") {
  207. if (externals === dependency.request) {
  208. return handleExternal(dependency.request, undefined, callback);
  209. }
  210. } else if (Array.isArray(externals)) {
  211. let i = 0;
  212. const next = () => {
  213. /** @type {boolean | undefined} */
  214. let asyncFlag;
  215. /**
  216. * Handle externals and callback.
  217. * @param {(Error | null)=} err err
  218. * @param {ExternalModule=} module module
  219. * @returns {void}
  220. */
  221. const handleExternalsAndCallback = (err, module) => {
  222. if (err) return callback(err);
  223. if (!module) {
  224. if (asyncFlag) {
  225. asyncFlag = false;
  226. return;
  227. }
  228. return next();
  229. }
  230. callback(null, module);
  231. };
  232. do {
  233. asyncFlag = true;
  234. if (i >= externals.length) return callback();
  235. handleExternals(externals[i++], handleExternalsAndCallback);
  236. } while (!asyncFlag);
  237. asyncFlag = false;
  238. };
  239. next();
  240. return;
  241. } else if (externals instanceof RegExp) {
  242. if (externals.test(dependency.request)) {
  243. return handleExternal(dependency.request, undefined, callback);
  244. }
  245. } else if (typeof externals === "function") {
  246. /**
  247. * Processes the provided err.
  248. * @param {Error | null | undefined} err err
  249. * @param {ExternalValue=} value value
  250. * @param {ExternalsType=} type type
  251. * @returns {void}
  252. */
  253. const cb = (err, value, type) => {
  254. if (err) return callback(err);
  255. if (value !== undefined) {
  256. handleExternal(value, type, callback);
  257. } else {
  258. callback();
  259. }
  260. };
  261. if (externals.length === 3) {
  262. // TODO webpack 6 remove this
  263. callDeprecatedExternals(
  264. externals,
  265. context,
  266. dependency.request,
  267. cb
  268. );
  269. } else {
  270. const promise = externals(
  271. {
  272. context,
  273. request: dependency.request,
  274. dependencyType,
  275. contextInfo,
  276. getResolve: (options) => (context, request, callback) => {
  277. /** @type {ResolveContext} */
  278. const resolveContext = {
  279. fileDependencies: data.fileDependencies,
  280. missingDependencies: data.missingDependencies,
  281. contextDependencies: data.contextDependencies
  282. };
  283. let resolver = normalModuleFactory.getResolver(
  284. "normal",
  285. dependencyType
  286. ? cachedSetProperty(
  287. data.resolveOptions || EMPTY_RESOLVE_OPTIONS,
  288. "dependencyType",
  289. dependencyType
  290. )
  291. : data.resolveOptions
  292. );
  293. if (options) resolver = resolver.withOptions(options);
  294. if (callback) {
  295. resolver.resolve(
  296. {},
  297. context,
  298. request,
  299. resolveContext,
  300. callback
  301. );
  302. } else {
  303. return new Promise((resolve, reject) => {
  304. resolver.resolve(
  305. {},
  306. context,
  307. request,
  308. resolveContext,
  309. (err, result) => {
  310. if (err) reject(err);
  311. else resolve(result);
  312. }
  313. );
  314. });
  315. }
  316. }
  317. },
  318. cb
  319. );
  320. if (promise && promise.then) {
  321. promise.then((r) => cb(null, r), cb);
  322. }
  323. }
  324. return;
  325. } else if (typeof externals === "object") {
  326. const resolvedExternals = resolveLayer(
  327. externals,
  328. /** @type {IssuerLayer} */
  329. (contextInfo.issuerLayer)
  330. );
  331. if (
  332. Object.prototype.hasOwnProperty.call(
  333. resolvedExternals,
  334. dependency.request
  335. )
  336. ) {
  337. return handleExternal(
  338. resolvedExternals[dependency.request],
  339. undefined,
  340. callback
  341. );
  342. }
  343. }
  344. callback();
  345. };
  346. handleExternals(this.externals, callback);
  347. }
  348. );
  349. }
  350. }
  351. module.exports = ExternalModuleFactoryPlugin;