ProvideSharedPlugin.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra and Zackary Jackson @ScriptedAlchemy
  4. */
  5. "use strict";
  6. const { parseOptions } = require("../container/options");
  7. const WebpackError = require("../errors/WebpackError");
  8. const { ABSOLUTE_PATH_REGEXP } = require("../util/identifier");
  9. const ProvideForSharedDependency = require("./ProvideForSharedDependency");
  10. const ProvideSharedDependency = require("./ProvideSharedDependency");
  11. const ProvideSharedModuleFactory = require("./ProvideSharedModuleFactory");
  12. /**
  13. * @import {
  14. * ProvideSharedPluginOptions
  15. * } from "../../declarations/plugins/sharing/ProvideSharedPlugin"
  16. */
  17. /** @import Compilation from "../Compilation" */
  18. /** @import Compiler from "../Compiler" */
  19. /** @import { NormalModuleCreateData } from "../NormalModule" */
  20. /**
  21. * Defines the provide options type used by this module.
  22. * @typedef {object} ProvideOptions
  23. * @property {string} shareKey
  24. * @property {string} shareScope
  25. * @property {string | undefined | false} version
  26. * @property {boolean} eager
  27. */
  28. /** @typedef {Map<string, { config: ProvideOptions, version: string | undefined | false }>} ResolvedProvideMap */
  29. const PLUGIN_NAME = "ProvideSharedPlugin";
  30. class ProvideSharedPlugin {
  31. /**
  32. * Creates an instance of ProvideSharedPlugin.
  33. * @param {ProvideSharedPluginOptions} options options
  34. */
  35. constructor(options) {
  36. /** @type {ProvideSharedPluginOptions} */
  37. this.options = options;
  38. }
  39. /**
  40. * Applies the plugin by registering its hooks on the compiler.
  41. * @param {Compiler} compiler the compiler instance
  42. * @returns {void}
  43. */
  44. apply(compiler) {
  45. compiler.hooks.validate.tap(PLUGIN_NAME, () => {
  46. compiler.validate(
  47. () => require("../../schemas/plugins/sharing/ProvideSharedPlugin.json"),
  48. this.options,
  49. {
  50. name: "Provide Shared Plugin",
  51. baseDataPath: "options"
  52. },
  53. (options) =>
  54. require("../../schemas/plugins/sharing/ProvideSharedPlugin.check")(
  55. options
  56. )
  57. );
  58. });
  59. /** @type {[string, ProvideOptions][]} */
  60. const provides = parseOptions(
  61. this.options.provides,
  62. (item) => {
  63. if (Array.isArray(item)) {
  64. throw new Error("Unexpected array of provides");
  65. }
  66. /** @type {ProvideOptions} */
  67. const result = {
  68. shareKey: item,
  69. version: undefined,
  70. shareScope: this.options.shareScope || "default",
  71. eager: false
  72. };
  73. return result;
  74. },
  75. (item) => ({
  76. shareKey: /** @type {string} */ (item.shareKey),
  77. version: item.version,
  78. shareScope: item.shareScope || this.options.shareScope || "default",
  79. eager: Boolean(item.eager)
  80. })
  81. ).sort(([a], [b]) => {
  82. if (a < b) return -1;
  83. if (b < a) return 1;
  84. return 0;
  85. });
  86. /** @type {WeakMap<Compilation, ResolvedProvideMap>} */
  87. const compilationData = new WeakMap();
  88. compiler.hooks.compilation.tap(
  89. PLUGIN_NAME,
  90. (compilation, { normalModuleFactory }) => {
  91. /** @type {ResolvedProvideMap} */
  92. const resolvedProvideMap = new Map();
  93. /** @type {Map<string, ProvideOptions>} */
  94. const matchProvides = new Map();
  95. /** @type {Map<string, ProvideOptions>} */
  96. const prefixMatchProvides = new Map();
  97. for (const [request, config] of provides) {
  98. if (/^\.\.?(?:\/|$)/.test(request)) {
  99. // relative request
  100. resolvedProvideMap.set(request, {
  101. config,
  102. version: config.version
  103. });
  104. } else if (ABSOLUTE_PATH_REGEXP.test(request)) {
  105. // absolute path
  106. resolvedProvideMap.set(request, {
  107. config,
  108. version: config.version
  109. });
  110. } else if (request.endsWith("/")) {
  111. // module request prefix
  112. prefixMatchProvides.set(request, config);
  113. } else {
  114. // module request
  115. matchProvides.set(request, config);
  116. }
  117. }
  118. compilationData.set(compilation, resolvedProvideMap);
  119. /**
  120. * Provide shared module.
  121. * @param {string} key key
  122. * @param {ProvideOptions} config config
  123. * @param {NormalModuleCreateData["resource"]} resource resource
  124. * @param {NormalModuleCreateData["resourceResolveData"]} resourceResolveData resource resolve data
  125. */
  126. const provideSharedModule = (
  127. key,
  128. config,
  129. resource,
  130. resourceResolveData
  131. ) => {
  132. let version = config.version;
  133. if (version === undefined) {
  134. let details = "";
  135. if (!resourceResolveData) {
  136. details = "No resolve data provided from resolver.";
  137. } else {
  138. const descriptionFileData =
  139. resourceResolveData.descriptionFileData;
  140. if (!descriptionFileData) {
  141. details =
  142. "No description file (usually package.json) found. Add description file with name and version, or manually specify version in shared config.";
  143. } else if (!descriptionFileData.version) {
  144. details = `No version in description file (usually package.json). Add version to description file ${resourceResolveData.descriptionFilePath}, or manually specify version in shared config.`;
  145. } else {
  146. version = /** @type {string | false | undefined} */ (
  147. descriptionFileData.version
  148. );
  149. }
  150. }
  151. if (!version) {
  152. const error = new WebpackError(
  153. `No version specified and unable to automatically determine one. ${details}`
  154. );
  155. error.file = `shared module ${key} -> ${resource}`;
  156. compilation.warnings.push(error);
  157. }
  158. }
  159. resolvedProvideMap.set(resource, {
  160. config,
  161. version
  162. });
  163. };
  164. normalModuleFactory.hooks.module.tap(
  165. PLUGIN_NAME,
  166. (module, { resource, resourceResolveData }, resolveData) => {
  167. if (resolvedProvideMap.has(resource)) {
  168. return module;
  169. }
  170. const { request } = resolveData;
  171. {
  172. const config = matchProvides.get(request);
  173. if (config !== undefined) {
  174. provideSharedModule(
  175. request,
  176. config,
  177. resource,
  178. resourceResolveData
  179. );
  180. resolveData.cacheable = false;
  181. }
  182. }
  183. for (const [prefix, config] of prefixMatchProvides) {
  184. if (request.startsWith(prefix)) {
  185. const remainder = request.slice(prefix.length);
  186. provideSharedModule(
  187. resource,
  188. {
  189. ...config,
  190. shareKey: config.shareKey + remainder
  191. },
  192. resource,
  193. resourceResolveData
  194. );
  195. resolveData.cacheable = false;
  196. }
  197. }
  198. return module;
  199. }
  200. );
  201. }
  202. );
  203. compiler.hooks.finishMake.tapPromise(PLUGIN_NAME, (compilation) => {
  204. const resolvedProvideMap = compilationData.get(compilation);
  205. if (!resolvedProvideMap) return Promise.resolve();
  206. return Promise.all(
  207. Array.from(
  208. resolvedProvideMap,
  209. ([resource, { config, version }]) =>
  210. new Promise((resolve, reject) => {
  211. compilation.addInclude(
  212. compiler.context,
  213. new ProvideSharedDependency(
  214. config.shareScope,
  215. config.shareKey,
  216. version || false,
  217. resource,
  218. config.eager
  219. ),
  220. {
  221. name: undefined
  222. },
  223. (err) => {
  224. if (err) return reject(err);
  225. resolve(null);
  226. }
  227. );
  228. })
  229. )
  230. ).then(() => {});
  231. });
  232. compiler.hooks.compilation.tap(
  233. PLUGIN_NAME,
  234. (compilation, { normalModuleFactory }) => {
  235. compilation.dependencyFactories.set(
  236. ProvideForSharedDependency,
  237. normalModuleFactory
  238. );
  239. compilation.dependencyFactories.set(
  240. ProvideSharedDependency,
  241. new ProvideSharedModuleFactory()
  242. );
  243. }
  244. );
  245. }
  246. }
  247. module.exports = ProvideSharedPlugin;