ConsumeSharedPlugin.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  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. let unresolvedConsumes, resolvedConsumes, prefixedConsumes;
  112. const promise = resolveMatchedConfigs(compilation, this._consumes).then(
  113. ({ resolved, unresolved, prefixed }) => {
  114. resolvedConsumes = resolved;
  115. unresolvedConsumes = unresolved;
  116. prefixedConsumes = prefixed;
  117. }
  118. );
  119. const resolver = compilation.resolverFactory.get(
  120. "normal",
  121. RESOLVE_OPTIONS
  122. );
  123. /**
  124. * @param {string} context issuer directory
  125. * @param {string} request request
  126. * @param {ConsumeOptions} config options
  127. * @returns {Promise<ConsumeSharedModule>} create module
  128. */
  129. const createConsumeSharedModule = (context, request, config) => {
  130. const requiredVersionWarning = details => {
  131. const error = new WebpackError(
  132. `No required version specified and unable to automatically determine one. ${details}`
  133. );
  134. error.file = `shared module ${request}`;
  135. compilation.warnings.push(error);
  136. };
  137. const directFallback =
  138. config.import &&
  139. /^(\.\.?(\/|$)|\/|[A-Za-z]:|\\\\)/.test(config.import);
  140. return Promise.all([
  141. new Promise(resolve => {
  142. if (!config.import) return resolve();
  143. const resolveContext = {
  144. /** @type {LazySet<string>} */
  145. fileDependencies: new LazySet(),
  146. /** @type {LazySet<string>} */
  147. contextDependencies: new LazySet(),
  148. /** @type {LazySet<string>} */
  149. missingDependencies: new LazySet()
  150. };
  151. resolver.resolve(
  152. {},
  153. directFallback ? compiler.context : context,
  154. config.import,
  155. resolveContext,
  156. (err, result) => {
  157. compilation.contextDependencies.addAll(
  158. resolveContext.contextDependencies
  159. );
  160. compilation.fileDependencies.addAll(
  161. resolveContext.fileDependencies
  162. );
  163. compilation.missingDependencies.addAll(
  164. resolveContext.missingDependencies
  165. );
  166. if (err) {
  167. compilation.errors.push(
  168. new ModuleNotFoundError(null, err, {
  169. name: `resolving fallback for shared module ${request}`
  170. })
  171. );
  172. return resolve();
  173. }
  174. resolve(result);
  175. }
  176. );
  177. }),
  178. new Promise(resolve => {
  179. if (config.requiredVersion !== undefined)
  180. return resolve(config.requiredVersion);
  181. let packageName = config.packageName;
  182. if (packageName === undefined) {
  183. if (/^(\/|[A-Za-z]:|\\\\)/.test(request)) {
  184. // For relative or absolute requests we don't automatically use a packageName.
  185. // If wished one can specify one with the packageName option.
  186. return resolve();
  187. }
  188. const match = /^((?:@[^\\/]+[\\/])?[^\\/]+)/.exec(request);
  189. if (!match) {
  190. requiredVersionWarning(
  191. "Unable to extract the package name from request."
  192. );
  193. return resolve();
  194. }
  195. packageName = match[0];
  196. }
  197. getDescriptionFile(
  198. compilation.inputFileSystem,
  199. context,
  200. ["package.json"],
  201. (err, result) => {
  202. if (err) {
  203. requiredVersionWarning(
  204. `Unable to read description file: ${err}`
  205. );
  206. return resolve();
  207. }
  208. const { data, path: descriptionPath } = result;
  209. if (!data) {
  210. requiredVersionWarning(
  211. `Unable to find description file in ${context}.`
  212. );
  213. return resolve();
  214. }
  215. const requiredVersion = getRequiredVersionFromDescriptionFile(
  216. data,
  217. packageName
  218. );
  219. if (typeof requiredVersion !== "string") {
  220. requiredVersionWarning(
  221. `Unable to find required version for "${packageName}" in description file (${descriptionPath}). It need to be in dependencies, devDependencies or peerDependencies.`
  222. );
  223. return resolve();
  224. }
  225. resolve(parseRange(requiredVersion));
  226. }
  227. );
  228. })
  229. ]).then(([importResolved, requiredVersion]) => {
  230. return new ConsumeSharedModule(
  231. directFallback ? compiler.context : context,
  232. {
  233. ...config,
  234. importResolved,
  235. import: importResolved ? config.import : undefined,
  236. requiredVersion
  237. }
  238. );
  239. });
  240. };
  241. normalModuleFactory.hooks.factorize.tapPromise(
  242. PLUGIN_NAME,
  243. ({ context, request, dependencies }) =>
  244. // wait for resolving to be complete
  245. promise.then(() => {
  246. if (
  247. dependencies[0] instanceof ConsumeSharedFallbackDependency ||
  248. dependencies[0] instanceof ProvideForSharedDependency
  249. ) {
  250. return;
  251. }
  252. const match = unresolvedConsumes.get(request);
  253. if (match !== undefined) {
  254. return createConsumeSharedModule(context, request, match);
  255. }
  256. for (const [prefix, options] of prefixedConsumes) {
  257. if (request.startsWith(prefix)) {
  258. const remainder = request.slice(prefix.length);
  259. return createConsumeSharedModule(context, request, {
  260. ...options,
  261. import: options.import
  262. ? options.import + remainder
  263. : undefined,
  264. shareKey: options.shareKey + remainder
  265. });
  266. }
  267. }
  268. })
  269. );
  270. normalModuleFactory.hooks.createModule.tapPromise(
  271. PLUGIN_NAME,
  272. ({ resource }, { context, dependencies }) => {
  273. if (
  274. dependencies[0] instanceof ConsumeSharedFallbackDependency ||
  275. dependencies[0] instanceof ProvideForSharedDependency
  276. ) {
  277. return Promise.resolve();
  278. }
  279. const options = resolvedConsumes.get(resource);
  280. if (options !== undefined) {
  281. return createConsumeSharedModule(context, resource, options);
  282. }
  283. return Promise.resolve();
  284. }
  285. );
  286. compilation.hooks.additionalTreeRuntimeRequirements.tap(
  287. PLUGIN_NAME,
  288. (chunk, set) => {
  289. set.add(RuntimeGlobals.module);
  290. set.add(RuntimeGlobals.moduleCache);
  291. set.add(RuntimeGlobals.moduleFactoriesAddOnly);
  292. set.add(RuntimeGlobals.shareScopeMap);
  293. set.add(RuntimeGlobals.initializeSharing);
  294. set.add(RuntimeGlobals.hasOwnProperty);
  295. compilation.addRuntimeModule(
  296. chunk,
  297. new ConsumeSharedRuntimeModule(set)
  298. );
  299. }
  300. );
  301. }
  302. );
  303. }
  304. }
  305. module.exports = ConsumeSharedPlugin;