ConsumeSharedPlugin.js 10 KB

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