ExternalModuleFactoryPlugin.js 12 KB

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