ConsumeSharedPlugin.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const ModuleNotFoundError = require("../ModuleNotFoundError");
  7. const RuntimeGlobals = require("../RuntimeGlobals");
  8. const WebpackError = require("../WebpackError");
  9. const { parseOptions } = require("../container/options");
  10. const LazySet = require("../util/LazySet");
  11. const { parseRange } = require("../util/semver");
  12. const ConsumeSharedFallbackDependency = require("./ConsumeSharedFallbackDependency");
  13. const ConsumeSharedModule = require("./ConsumeSharedModule");
  14. const ConsumeSharedRuntimeModule = require("./ConsumeSharedRuntimeModule");
  15. const ProvideForSharedDependency = require("./ProvideForSharedDependency");
  16. const { resolveMatchedConfigs } = require("./resolveMatchedConfigs");
  17. const {
  18. getDescriptionFile,
  19. getRequiredVersionFromDescriptionFile,
  20. isRequiredVersion
  21. } = require("./utils");
  22. /** @typedef {import("enhanced-resolve").ResolveContext} ResolveContext */
  23. /** @typedef {import("../../declarations/plugins/sharing/ConsumeSharedPlugin").ConsumeSharedPluginOptions} ConsumeSharedPluginOptions */
  24. /** @typedef {import("../Compiler")} Compiler */
  25. /** @typedef {import("../Compilation").FileSystemDependencies} FileSystemDependencies */
  26. /** @typedef {import("../ResolverFactory").ResolveOptionsWithDependencyType} ResolveOptionsWithDependencyType */
  27. /** @typedef {import("../util/semver").SemVerRange} SemVerRange */
  28. /** @typedef {import("./ConsumeSharedModule").ConsumeOptions} ConsumeOptions */
  29. /** @typedef {import("./utils").DescriptionFile} DescriptionFile */
  30. /** @type {ResolveOptionsWithDependencyType} */
  31. const RESOLVE_OPTIONS = { dependencyType: "esm" };
  32. const PLUGIN_NAME = "ConsumeSharedPlugin";
  33. class ConsumeSharedPlugin {
  34. /**
  35. * Creates an instance of ConsumeSharedPlugin.
  36. * @param {ConsumeSharedPluginOptions} options options
  37. */
  38. constructor(options) {
  39. this.options = options;
  40. }
  41. /**
  42. * Applies the plugin by registering its hooks on the compiler.
  43. * @param {Compiler} compiler the compiler instance
  44. * @returns {void}
  45. */
  46. apply(compiler) {
  47. // TODO webpack 6 remove string support
  48. if (typeof this.options !== "string") {
  49. compiler.hooks.validate.tap(PLUGIN_NAME, () => {
  50. compiler.validate(
  51. () =>
  52. require("../../schemas/plugins/sharing/ConsumeSharedPlugin.json"),
  53. this.options,
  54. {
  55. name: "Consume Shared Plugin",
  56. baseDataPath: "options"
  57. },
  58. (options) =>
  59. require("../../schemas/plugins/sharing/ConsumeSharedPlugin.check")(
  60. options
  61. )
  62. );
  63. });
  64. }
  65. /** @type {[string, ConsumeOptions][]} */
  66. const consumes = parseOptions(
  67. this.options.consumes,
  68. (item, key) => {
  69. if (Array.isArray(item)) throw new Error("Unexpected array in options");
  70. /** @type {ConsumeOptions} */
  71. const result =
  72. item === key || !isRequiredVersion(item)
  73. ? // item is a request/key
  74. {
  75. import: key,
  76. shareScope: this.options.shareScope || "default",
  77. shareKey: key,
  78. requiredVersion: undefined,
  79. packageName: undefined,
  80. strictVersion: false,
  81. singleton: false,
  82. eager: false
  83. }
  84. : // key is a request/key
  85. // item is a version
  86. {
  87. import: key,
  88. shareScope: this.options.shareScope || "default",
  89. shareKey: key,
  90. requiredVersion: parseRange(item),
  91. strictVersion: true,
  92. packageName: undefined,
  93. singleton: false,
  94. eager: false
  95. };
  96. return result;
  97. },
  98. (item, key) => ({
  99. import: item.import === false ? undefined : item.import || key,
  100. shareScope: item.shareScope || this.options.shareScope || "default",
  101. shareKey: item.shareKey || key,
  102. requiredVersion:
  103. typeof item.requiredVersion === "string"
  104. ? parseRange(item.requiredVersion)
  105. : item.requiredVersion,
  106. strictVersion:
  107. typeof item.strictVersion === "boolean"
  108. ? item.strictVersion
  109. : item.import !== false && !item.singleton,
  110. packageName: item.packageName,
  111. singleton: Boolean(item.singleton),
  112. eager: Boolean(item.eager)
  113. })
  114. );
  115. compiler.hooks.thisCompilation.tap(
  116. PLUGIN_NAME,
  117. (compilation, { normalModuleFactory }) => {
  118. compilation.dependencyFactories.set(
  119. ConsumeSharedFallbackDependency,
  120. normalModuleFactory
  121. );
  122. /** @typedef {Map<string, ConsumeOptions>} Consumes */
  123. /** @type {Consumes} */
  124. let unresolvedConsumes;
  125. /** @type {Consumes} */
  126. let resolvedConsumes;
  127. /** @type {Consumes} */
  128. let prefixedConsumes;
  129. const promise = resolveMatchedConfigs(compilation, consumes).then(
  130. ({ resolved, unresolved, prefixed }) => {
  131. resolvedConsumes = resolved;
  132. unresolvedConsumes = unresolved;
  133. prefixedConsumes = prefixed;
  134. }
  135. );
  136. const resolver = compilation.resolverFactory.get(
  137. "normal",
  138. RESOLVE_OPTIONS
  139. );
  140. /**
  141. * Creates a consume shared module.
  142. * @param {string} context issuer directory
  143. * @param {string} request request
  144. * @param {ConsumeOptions} config options
  145. * @returns {Promise<ConsumeSharedModule>} create module
  146. */
  147. const createConsumeSharedModule = (context, request, config) => {
  148. /**
  149. * Required version warning.
  150. * @param {string} details details
  151. */
  152. const requiredVersionWarning = (details) => {
  153. const error = new WebpackError(
  154. `No required version specified and unable to automatically determine one. ${details}`
  155. );
  156. error.file = `shared module ${request}`;
  157. compilation.warnings.push(error);
  158. };
  159. const directFallback =
  160. config.import &&
  161. /^(?:\.\.?(?:\/|$)|\/|[A-Z]:|\\\\)/i.test(config.import);
  162. return Promise.all([
  163. new Promise(
  164. /**
  165. * Handles the callback logic for this hook.
  166. * @param {(value?: string) => void} resolve resolve
  167. */
  168. (resolve) => {
  169. if (!config.import) {
  170. resolve();
  171. return;
  172. }
  173. /** @type {ResolveContext & { fileDependencies: FileSystemDependencies, contextDependencies: FileSystemDependencies, missingDependencies: FileSystemDependencies }} */
  174. const resolveContext = {
  175. fileDependencies: new LazySet(),
  176. contextDependencies: new LazySet(),
  177. missingDependencies: new LazySet()
  178. };
  179. resolver.resolve(
  180. {},
  181. directFallback ? compiler.context : context,
  182. config.import,
  183. resolveContext,
  184. (err, result) => {
  185. compilation.contextDependencies.addAll(
  186. resolveContext.contextDependencies
  187. );
  188. compilation.fileDependencies.addAll(
  189. resolveContext.fileDependencies
  190. );
  191. compilation.missingDependencies.addAll(
  192. resolveContext.missingDependencies
  193. );
  194. if (err) {
  195. compilation.errors.push(
  196. new ModuleNotFoundError(null, err, {
  197. name: `resolving fallback for shared module ${request}`
  198. })
  199. );
  200. return resolve();
  201. }
  202. resolve(/** @type {string} */ (result));
  203. }
  204. );
  205. }
  206. ),
  207. new Promise(
  208. /**
  209. * Handles the name callback for this hook.
  210. * @param {(value?: SemVerRange) => void} resolve resolve
  211. */
  212. (resolve) => {
  213. if (config.requiredVersion !== undefined) {
  214. resolve(/** @type {SemVerRange} */ (config.requiredVersion));
  215. return;
  216. }
  217. let packageName = config.packageName;
  218. if (packageName === undefined) {
  219. if (/^(?:\/|[A-Z]:|\\\\)/i.test(request)) {
  220. // For relative or absolute requests we don't automatically use a packageName.
  221. // If wished one can specify one with the packageName option.
  222. resolve();
  223. return;
  224. }
  225. const match = /^(?:@[^\\/]+[\\/])?[^\\/]+/.exec(request);
  226. if (!match) {
  227. requiredVersionWarning(
  228. "Unable to extract the package name from request."
  229. );
  230. resolve();
  231. return;
  232. }
  233. packageName = match[0];
  234. }
  235. getDescriptionFile(
  236. compilation.inputFileSystem,
  237. context,
  238. ["package.json"],
  239. (err, result, checkedDescriptionFilePaths) => {
  240. if (err) {
  241. requiredVersionWarning(
  242. `Unable to read description file: ${err}`
  243. );
  244. return resolve();
  245. }
  246. const { data } =
  247. /** @type {DescriptionFile} */
  248. (result || {});
  249. if (!data) {
  250. if (checkedDescriptionFilePaths) {
  251. requiredVersionWarning(
  252. [
  253. `Unable to find required version for "${packageName}" in description file/s`,
  254. checkedDescriptionFilePaths.join("\n"),
  255. "It need to be in dependencies, devDependencies or peerDependencies."
  256. ].join("\n")
  257. );
  258. } else {
  259. requiredVersionWarning(
  260. `Unable to find description file in ${context}.`
  261. );
  262. }
  263. return resolve();
  264. }
  265. if (data.name === packageName) {
  266. // Package self-referencing
  267. return resolve();
  268. }
  269. const requiredVersion =
  270. getRequiredVersionFromDescriptionFile(data, packageName);
  271. if (requiredVersion) {
  272. return resolve(parseRange(requiredVersion));
  273. }
  274. resolve();
  275. },
  276. (result) => {
  277. if (!result) return false;
  278. const maybeRequiredVersion =
  279. getRequiredVersionFromDescriptionFile(
  280. result.data,
  281. packageName
  282. );
  283. return (
  284. result.data.name === packageName ||
  285. typeof maybeRequiredVersion === "string"
  286. );
  287. }
  288. );
  289. }
  290. )
  291. ]).then(
  292. ([importResolved, requiredVersion]) =>
  293. new ConsumeSharedModule(
  294. directFallback ? compiler.context : context,
  295. {
  296. ...config,
  297. importResolved,
  298. import: importResolved ? config.import : undefined,
  299. requiredVersion
  300. }
  301. )
  302. );
  303. };
  304. normalModuleFactory.hooks.factorize.tapPromise(
  305. PLUGIN_NAME,
  306. ({ context, request, dependencies }) =>
  307. // wait for resolving to be complete
  308. promise.then(() => {
  309. if (
  310. dependencies[0] instanceof ConsumeSharedFallbackDependency ||
  311. dependencies[0] instanceof ProvideForSharedDependency
  312. ) {
  313. return;
  314. }
  315. const match = unresolvedConsumes.get(request);
  316. if (match !== undefined) {
  317. return createConsumeSharedModule(context, request, match);
  318. }
  319. for (const [prefix, options] of prefixedConsumes) {
  320. if (request.startsWith(prefix)) {
  321. const remainder = request.slice(prefix.length);
  322. return createConsumeSharedModule(context, request, {
  323. ...options,
  324. import: options.import
  325. ? options.import + remainder
  326. : undefined,
  327. shareKey: options.shareKey + remainder
  328. });
  329. }
  330. }
  331. })
  332. );
  333. normalModuleFactory.hooks.createModule.tapPromise(
  334. PLUGIN_NAME,
  335. ({ resource }, { context, dependencies }) => {
  336. if (
  337. dependencies[0] instanceof ConsumeSharedFallbackDependency ||
  338. dependencies[0] instanceof ProvideForSharedDependency
  339. ) {
  340. return Promise.resolve();
  341. }
  342. const options = resolvedConsumes.get(
  343. /** @type {string} */ (resource)
  344. );
  345. if (options !== undefined) {
  346. return createConsumeSharedModule(
  347. context,
  348. /** @type {string} */ (resource),
  349. options
  350. );
  351. }
  352. return Promise.resolve();
  353. }
  354. );
  355. compilation.hooks.additionalTreeRuntimeRequirements.tap(
  356. PLUGIN_NAME,
  357. (chunk, set) => {
  358. set.add(RuntimeGlobals.module);
  359. set.add(RuntimeGlobals.moduleCache);
  360. set.add(RuntimeGlobals.moduleFactoriesAddOnly);
  361. set.add(RuntimeGlobals.shareScopeMap);
  362. set.add(RuntimeGlobals.initializeSharing);
  363. set.add(RuntimeGlobals.hasOwnProperty);
  364. compilation.addRuntimeModule(
  365. chunk,
  366. new ConsumeSharedRuntimeModule(set)
  367. );
  368. }
  369. );
  370. }
  371. );
  372. }
  373. }
  374. module.exports = ConsumeSharedPlugin;