ExternalModuleFactoryPlugin.js 10 KB

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