ExternalModuleFactoryPlugin.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  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 { ASSET_URL_TYPE } = require("./ModuleSourceTypeConstants");
  9. const ContextElementDependency = require("./dependencies/ContextElementDependency");
  10. const CssImportDependency = require("./dependencies/CssImportDependency");
  11. const CssUrlDependency = require("./dependencies/CssUrlDependency");
  12. const HarmonyImportDependency = require("./dependencies/HarmonyImportDependency");
  13. const ImportDependency = require("./dependencies/ImportDependency");
  14. const { coreModules } = require("./node/nodeBuiltins");
  15. const { cachedSetProperty, resolveByProperty } = require("./util/cleverMerge");
  16. /** @import { ResolveContext } from "enhanced-resolve" */
  17. /**
  18. * @import {
  19. * ResolveOptions,
  20. * ExternalsType,
  21. * ExternalItem,
  22. * ExternalItemValue,
  23. * ExternalItemObjectKnown,
  24. * ExternalItemObjectUnknown,
  25. * ExternalItemInterop,
  26. * Externals
  27. * } from "../declarations/WebpackOptions"
  28. */
  29. /** @import Dependency from "./Dependency" */
  30. /** @import { DependencyMeta, ExternalModuleRequest } from "./ExternalModule" */
  31. /**
  32. * @import {
  33. * IssuerLayer,
  34. * ModuleFactoryCreateDataContextInfo
  35. * } from "./ModuleFactory"
  36. */
  37. /** @import NormalModuleFactory from "./NormalModuleFactory" */
  38. /** @typedef {((context: string, request: string, callback: (err?: Error | null, result?: string | false, resolveRequest?: import("enhanced-resolve").ResolveRequest) => void) => void)} ExternalItemFunctionDataGetResolveCallbackResult */
  39. /** @typedef {((context: string, request: string) => Promise<string>)} ExternalItemFunctionDataGetResolveResult */
  40. /** @typedef {(options?: ResolveOptions) => ExternalItemFunctionDataGetResolveCallbackResult | ExternalItemFunctionDataGetResolveResult} ExternalItemFunctionDataGetResolve */
  41. /**
  42. * Defines the external item function data type used by this module.
  43. * @typedef {object} ExternalItemFunctionData
  44. * @property {string} context the directory in which the request is placed
  45. * @property {ModuleFactoryCreateDataContextInfo} contextInfo contextual information
  46. * @property {string} dependencyType the category of the referencing dependency
  47. * @property {ExternalItemFunctionDataGetResolve} getResolve get a resolve function with the current resolver options
  48. * @property {string} request the request as written by the user in the require/import expression/statement
  49. * @property {string} originalRequest same as `request`, except for an element of a context module (a request containing an expression), where it is the request as written by the user instead of the one relative to the resolved context directory
  50. */
  51. /** @typedef {((data: ExternalItemFunctionData, callback: (err?: (Error | null), result?: ExternalItemValue) => void) => void)} ExternalItemFunctionCallback */
  52. /** @typedef {((data: import("../lib/ExternalModuleFactoryPlugin").ExternalItemFunctionData) => Promise<ExternalItemValue>)} ExternalItemFunctionPromise */
  53. const UNSPECIFIED_EXTERNAL_TYPE_REGEXP = /^[a-z0-9-]+ /;
  54. const EMPTY_RESOLVE_OPTIONS = {};
  55. const NODE_PREFIX = "node:";
  56. /**
  57. * For a node.js core module request, returns its `node:`-prefixed/unprefixed
  58. * counterpart so externals match regardless of which form the code uses.
  59. * @param {string} request the request as written in the import/require
  60. * @returns {string | undefined} the alternate form, or undefined when not a core module
  61. */
  62. const getAlternateCoreModuleRequest = (request) => {
  63. if (request.startsWith(NODE_PREFIX)) {
  64. const name = request.slice(NODE_PREFIX.length);
  65. return coreModules.has(name) ? name : undefined;
  66. }
  67. return coreModules.has(request) ? NODE_PREFIX + request : undefined;
  68. };
  69. // TODO webpack 6 remove this
  70. const callDeprecatedExternals = util.deprecate(
  71. /**
  72. * Handles the callback logic for this hook.
  73. * @param {EXPECTED_FUNCTION} externalsFunction externals function
  74. * @param {string} context context
  75. * @param {string} request request
  76. * @param {(err: Error | null | undefined, value: ExternalValue | undefined, ty: ExternalsType | undefined) => void} cb cb
  77. */
  78. (externalsFunction, context, request, cb) => {
  79. // eslint-disable-next-line no-useless-call
  80. externalsFunction.call(null, context, request, cb);
  81. },
  82. "The externals-function should be defined like ({context, request}, cb) => { ... }",
  83. "DEP_WEBPACK_EXTERNALS_FUNCTION_PARAMETERS"
  84. );
  85. /** @typedef {(layer: string | null) => ExternalItem} ExternalItemByLayerFn */
  86. /** @typedef {ExternalItemObjectKnown & ExternalItemObjectUnknown} ExternalItemObject */
  87. /**
  88. * Defines the external weak cache type used by this module.
  89. * @template {ExternalItemObject} T
  90. * @typedef {WeakMap<T, Map<IssuerLayer, Omit<T, "byLayer">>>} ExternalWeakCache
  91. */
  92. /** @type {ExternalWeakCache<ExternalItemObject>} */
  93. const cache = new WeakMap();
  94. /**
  95. * Returns result.
  96. * @param {ExternalItemObject} obj obj
  97. * @param {IssuerLayer} layer layer
  98. * @returns {Omit<ExternalItemObject, "byLayer">} result
  99. */
  100. const resolveLayer = (obj, layer) => {
  101. let map = cache.get(obj);
  102. if (map === undefined) {
  103. map = new Map();
  104. cache.set(obj, map);
  105. } else {
  106. const cacheEntry = map.get(layer);
  107. if (cacheEntry !== undefined) return cacheEntry;
  108. }
  109. const result = resolveByProperty(obj, "byLayer", layer);
  110. map.set(layer, result);
  111. return result;
  112. };
  113. // the keys `ExternalItemValueWithOptions` allows, mirroring the schema
  114. const OPTIONS_FORM_KEYS = new Set(["external", "sideEffects"]);
  115. /** @typedef {string | string[] | boolean | Record<string, string | string[]>} ExternalTargetValue */
  116. /** @typedef {{ external: ExternalTargetValue, sideEffects?: boolean }} ExternalValueWithOptions */
  117. /** @typedef {ExternalTargetValue | ExternalValueWithOptions} ExternalValue */
  118. const PLUGIN_NAME = "ExternalModuleFactoryPlugin";
  119. class ExternalModuleFactoryPlugin {
  120. /**
  121. * Creates an instance of ExternalModuleFactoryPlugin.
  122. * @param {ExternalsType | ((dependency: Dependency) => ExternalsType)} type default external type
  123. * @param {Externals} externals externals config
  124. */
  125. constructor(type, externals) {
  126. this.type = type;
  127. /** @type {Externals} */
  128. this.externals = externals;
  129. }
  130. /**
  131. * Applies the plugin by registering its hooks on the compiler.
  132. * @param {NormalModuleFactory} normalModuleFactory the normal module factory
  133. * @returns {void}
  134. */
  135. apply(normalModuleFactory) {
  136. const globalType = this.type;
  137. normalModuleFactory.hooks.factorize.tapAsync(
  138. PLUGIN_NAME,
  139. (data, callback) => {
  140. const context = data.context;
  141. const contextInfo = data.contextInfo;
  142. const dependency = data.dependencies[0];
  143. const dependencyType = data.dependencyType;
  144. const request = dependency.request;
  145. // a context element's request is relative to the resolved directory
  146. // (`./file.mjs`), externals are written against what the user wrote
  147. const originalRequest =
  148. dependency instanceof ContextElementDependency
  149. ? dependency.originalRequest
  150. : request;
  151. const hasOriginalRequest = originalRequest !== request;
  152. /** @typedef {(err?: Error | null, externalModule?: ExternalModule) => void} HandleExternalCallback */
  153. /**
  154. * Processes the provided value.
  155. * @param {ExternalValue} value the external config
  156. * @param {ExternalsType | undefined} type type of external
  157. * @param {HandleExternalCallback} callback callback
  158. * @returns {void}
  159. */
  160. const handleExternal = (value, type, callback) => {
  161. /** @type {boolean | undefined} */
  162. let sideEffects;
  163. /** @type {ExternalTargetValue} */
  164. let target;
  165. // the options form carries the target under `external`; a target
  166. // map holding an `external` type key keeps its own meaning
  167. if (
  168. typeof value === "object" &&
  169. value !== null &&
  170. !Array.isArray(value) &&
  171. Object.prototype.hasOwnProperty.call(value, "external") &&
  172. Object.keys(value).every((key) => OPTIONS_FORM_KEYS.has(key))
  173. ) {
  174. const withOptions =
  175. /** @type {ExternalValueWithOptions} */
  176. (value);
  177. sideEffects = withOptions.sideEffects;
  178. target = withOptions.external;
  179. } else {
  180. target = /** @type {ExternalTargetValue} */ (value);
  181. }
  182. if (target === false) {
  183. // Not externals, fallback to original factory
  184. return callback();
  185. }
  186. /** @type {ExternalModuleRequest} */
  187. let externalConfig = target === true ? originalRequest : target;
  188. // `interop` is a reserved key on the object form, not a target;
  189. // pull it out so the rest stays a pure externalsType->request map.
  190. /** @type {ExternalItemInterop | undefined} */
  191. let interop;
  192. if (
  193. typeof externalConfig === "object" &&
  194. externalConfig !== null &&
  195. !Array.isArray(externalConfig) &&
  196. Object.prototype.hasOwnProperty.call(externalConfig, "interop")
  197. ) {
  198. const { interop: interopValue, ...rest } =
  199. /** @type {Record<string, string | string[]> & { interop?: ExternalItemInterop }} */ (
  200. externalConfig
  201. );
  202. interop = interopValue;
  203. externalConfig = rest;
  204. }
  205. // When no explicit type is specified, extract it from the externalConfig
  206. if (type === undefined) {
  207. if (
  208. typeof externalConfig === "string" &&
  209. UNSPECIFIED_EXTERNAL_TYPE_REGEXP.test(externalConfig)
  210. ) {
  211. const idx = externalConfig.indexOf(" ");
  212. type =
  213. /** @type {ExternalsType} */
  214. (externalConfig.slice(0, idx));
  215. externalConfig = externalConfig.slice(idx + 1);
  216. } else if (
  217. Array.isArray(externalConfig) &&
  218. externalConfig.length > 0 &&
  219. UNSPECIFIED_EXTERNAL_TYPE_REGEXP.test(externalConfig[0])
  220. ) {
  221. const firstItem = externalConfig[0];
  222. const idx = firstItem.indexOf(" ");
  223. type = /** @type {ExternalsType} */ (firstItem.slice(0, idx));
  224. externalConfig = [
  225. firstItem.slice(idx + 1),
  226. ...externalConfig.slice(1)
  227. ];
  228. }
  229. }
  230. const defaultType =
  231. typeof globalType === "function"
  232. ? globalType(dependency)
  233. : globalType;
  234. const resolvedType = type || defaultType;
  235. // TODO make it pluggable/add hooks to `ExternalModule` to allow output modules own externals?
  236. /** @type {DependencyMeta | undefined} */
  237. let dependencyMeta;
  238. if (
  239. dependency instanceof HarmonyImportDependency ||
  240. dependency instanceof ImportDependency ||
  241. dependency instanceof ContextElementDependency
  242. ) {
  243. const externalType =
  244. dependency instanceof HarmonyImportDependency
  245. ? "module"
  246. : dependency instanceof ImportDependency
  247. ? "import"
  248. : undefined;
  249. dependencyMeta = {
  250. attributes: dependency.attributes,
  251. phase:
  252. dependency instanceof HarmonyImportDependency ||
  253. dependency instanceof ImportDependency
  254. ? dependency.phase
  255. : undefined,
  256. externalType
  257. };
  258. } else if (dependency instanceof CssImportDependency) {
  259. dependencyMeta = {
  260. layer: dependency.layer,
  261. supports: dependency.supports,
  262. media: dependency.media
  263. };
  264. }
  265. // a css `url()` reads the url out of the external: no js wrapper
  266. // TODO webpack 6 drop "css-url" once the alias is removed
  267. if (
  268. (resolvedType === "asset" ||
  269. resolvedType === ASSET_URL_TYPE ||
  270. resolvedType === "css-url") &&
  271. dependency instanceof CssUrlDependency
  272. ) {
  273. dependencyMeta = { sourceType: ASSET_URL_TYPE };
  274. }
  275. callback(
  276. null,
  277. new ExternalModule(
  278. externalConfig,
  279. resolvedType,
  280. originalRequest,
  281. dependencyMeta,
  282. interop,
  283. sideEffects
  284. )
  285. );
  286. };
  287. /**
  288. * Processes the provided external.
  289. * @param {Externals} externals externals config
  290. * @param {HandleExternalCallback} callback callback
  291. * @returns {void}
  292. */
  293. const handleExternals = (externals, callback) => {
  294. if (typeof externals === "string") {
  295. if (
  296. externals === request ||
  297. (hasOriginalRequest && externals === originalRequest) ||
  298. externals === getAlternateCoreModuleRequest(request)
  299. ) {
  300. return handleExternal(originalRequest, undefined, callback);
  301. }
  302. } else if (Array.isArray(externals)) {
  303. let i = 0;
  304. const next = () => {
  305. /** @type {boolean | undefined} */
  306. let asyncFlag;
  307. /**
  308. * Handle externals and callback.
  309. * @param {(Error | null)=} err err
  310. * @param {ExternalModule=} module module
  311. * @returns {void}
  312. */
  313. const handleExternalsAndCallback = (err, module) => {
  314. if (err) return callback(err);
  315. if (!module) {
  316. if (asyncFlag) {
  317. asyncFlag = false;
  318. return;
  319. }
  320. return next();
  321. }
  322. callback(null, module);
  323. };
  324. do {
  325. asyncFlag = true;
  326. if (i >= externals.length) return callback();
  327. handleExternals(externals[i++], handleExternalsAndCallback);
  328. } while (!asyncFlag);
  329. asyncFlag = false;
  330. };
  331. next();
  332. return;
  333. } else if (externals instanceof RegExp) {
  334. if (
  335. externals.test(request) ||
  336. (hasOriginalRequest && externals.test(originalRequest))
  337. ) {
  338. return handleExternal(originalRequest, undefined, callback);
  339. }
  340. } else if (typeof externals === "function") {
  341. /**
  342. * Processes the provided err.
  343. * @param {Error | null | undefined} err err
  344. * @param {ExternalValue=} value value
  345. * @param {ExternalsType=} type type
  346. * @returns {void}
  347. */
  348. const cb = (err, value, type) => {
  349. if (err) return callback(err);
  350. if (value !== undefined) {
  351. handleExternal(value, type, callback);
  352. } else {
  353. callback();
  354. }
  355. };
  356. if (externals.length === 3) {
  357. // TODO webpack 6 remove this
  358. callDeprecatedExternals(externals, context, request, cb);
  359. } else {
  360. const promise = externals(
  361. {
  362. context,
  363. request,
  364. originalRequest,
  365. dependencyType,
  366. contextInfo,
  367. getResolve: (options) => (context, request, callback) => {
  368. /** @type {ResolveContext} */
  369. const resolveContext = {
  370. fileDependencies: data.fileDependencies,
  371. missingDependencies: data.missingDependencies,
  372. contextDependencies: data.contextDependencies
  373. };
  374. let resolver = normalModuleFactory.getResolver(
  375. "normal",
  376. dependencyType
  377. ? cachedSetProperty(
  378. data.resolveOptions || EMPTY_RESOLVE_OPTIONS,
  379. "dependencyType",
  380. dependencyType
  381. )
  382. : data.resolveOptions
  383. );
  384. if (options) resolver = resolver.withOptions(options);
  385. if (callback) {
  386. resolver.resolve(
  387. {},
  388. context,
  389. request,
  390. resolveContext,
  391. callback
  392. );
  393. } else {
  394. return new Promise((resolve, reject) => {
  395. resolver.resolve(
  396. {},
  397. context,
  398. request,
  399. resolveContext,
  400. (err, result) => {
  401. if (err) reject(err);
  402. else resolve(result);
  403. }
  404. );
  405. });
  406. }
  407. }
  408. },
  409. cb
  410. );
  411. if (promise && promise.then) {
  412. promise.then((r) => cb(null, r), cb);
  413. }
  414. }
  415. return;
  416. } else if (typeof externals === "object") {
  417. const resolvedExternals = resolveLayer(
  418. externals,
  419. /** @type {IssuerLayer} */
  420. (contextInfo.issuerLayer)
  421. );
  422. if (
  423. Object.prototype.hasOwnProperty.call(resolvedExternals, request)
  424. ) {
  425. return handleExternal(
  426. resolvedExternals[request],
  427. undefined,
  428. callback
  429. );
  430. }
  431. if (
  432. hasOriginalRequest &&
  433. Object.prototype.hasOwnProperty.call(
  434. resolvedExternals,
  435. originalRequest
  436. )
  437. ) {
  438. return handleExternal(
  439. resolvedExternals[originalRequest],
  440. undefined,
  441. callback
  442. );
  443. }
  444. // Fall back to the `node:`-prefixed/unprefixed core module form.
  445. const alternateRequest = getAlternateCoreModuleRequest(request);
  446. if (
  447. alternateRequest !== undefined &&
  448. Object.prototype.hasOwnProperty.call(
  449. resolvedExternals,
  450. alternateRequest
  451. )
  452. ) {
  453. return handleExternal(
  454. resolvedExternals[alternateRequest],
  455. undefined,
  456. callback
  457. );
  458. }
  459. }
  460. callback();
  461. };
  462. handleExternals(this.externals, callback);
  463. }
  464. );
  465. }
  466. }
  467. ExternalModuleFactoryPlugin.getAlternateCoreModuleRequest =
  468. getAlternateCoreModuleRequest;
  469. module.exports = ExternalModuleFactoryPlugin;