ProvideSharedPlugin.js 7.5 KB

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