VirtualUrlPlugin.js 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Natsu @xiaoxiaojx
  4. */
  5. "use strict";
  6. const { NormalModule } = require("..");
  7. const ModuleNotFoundError = require("../ModuleNotFoundError");
  8. const { parseResourceWithoutFragment } = require("../util/identifier");
  9. /** @typedef {import("../Compiler")} Compiler */
  10. /** @typedef {import("../NormalModule")} NormalModule */
  11. /** @typedef {import("../Module").BuildInfo} BuildInfo */
  12. /** @typedef {import("../Module").ValueCacheVersions} ValueCacheVersions */
  13. /** @typedef {string | Set<string>} ValueCacheVersion */
  14. /**
  15. * @template T
  16. * @typedef {import("../../declarations/LoaderContext").LoaderContext<T>} LoaderContext
  17. */
  18. const PLUGIN_NAME = "VirtualUrlPlugin";
  19. const DEFAULT_SCHEME = "virtual";
  20. /**
  21. * @typedef {object} VirtualModuleConfig
  22. * @property {string=} type - The module type
  23. * @property {(loaderContext: LoaderContext<EXPECTED_ANY>) => Promise<string> | string} source - The source function
  24. * @property {(() => string) | true | string=} version - Optional version function or value
  25. */
  26. /**
  27. * @typedef {string | ((loaderContext: LoaderContext<EXPECTED_ANY>) => Promise<string> | string) | VirtualModuleConfig} VirtualModuleInput
  28. */
  29. /** @typedef {{[key: string]: VirtualModuleInput}} VirtualModules */
  30. /**
  31. * Normalizes a virtual module definition into a standard format
  32. * @param {VirtualModuleInput} virtualConfig The virtual module to normalize
  33. * @returns {VirtualModuleConfig} The normalized virtual module
  34. */
  35. function normalizeModule(virtualConfig) {
  36. if (typeof virtualConfig === "string") {
  37. return {
  38. type: "",
  39. source() {
  40. return virtualConfig;
  41. }
  42. };
  43. } else if (typeof virtualConfig === "function") {
  44. return {
  45. type: "",
  46. source: virtualConfig
  47. };
  48. }
  49. return virtualConfig;
  50. }
  51. /**
  52. * Normalizes all virtual modules with the given scheme
  53. * @param {VirtualModules} virtualConfigs The virtual modules to normalize
  54. * @param {string} scheme The URL scheme to use
  55. * @returns {{[key: string]: VirtualModuleConfig}} The normalized virtual modules
  56. */
  57. function normalizeModules(virtualConfigs, scheme) {
  58. return Object.keys(virtualConfigs).reduce((pre, id) => {
  59. pre[toVid(id, scheme)] = normalizeModule(virtualConfigs[id]);
  60. return pre;
  61. }, /** @type {{[key: string]: VirtualModuleConfig}} */ ({}));
  62. }
  63. /**
  64. * Converts a module id and scheme to a virtual module id
  65. * @param {string} id The module id
  66. * @param {string} scheme The URL scheme
  67. * @returns {string} The virtual module id
  68. */
  69. function toVid(id, scheme) {
  70. return `${scheme}:${id}`;
  71. }
  72. const VALUE_DEP_VERSION = `webpack/${PLUGIN_NAME}/version`;
  73. /**
  74. * Converts a module id and scheme to a cache key
  75. * @param {string} id The module id
  76. * @param {string} scheme The URL scheme
  77. * @returns {string} The cache key
  78. */
  79. function toCacheKey(id, scheme) {
  80. return `${VALUE_DEP_VERSION}/${toVid(id, scheme)}`;
  81. }
  82. /**
  83. * @typedef {object} VirtualUrlPluginOptions
  84. * @property {VirtualModules} modules - The virtual modules
  85. * @property {string=} scheme - The URL scheme to use
  86. */
  87. class VirtualUrlPlugin {
  88. /**
  89. * @param {VirtualModules} modules The virtual modules
  90. * @param {string=} scheme The URL scheme to use
  91. */
  92. constructor(modules, scheme) {
  93. this.scheme = scheme || DEFAULT_SCHEME;
  94. this.modules = normalizeModules(modules, this.scheme);
  95. }
  96. /**
  97. * Apply the plugin
  98. * @param {Compiler} compiler the compiler instance
  99. * @returns {void}
  100. */
  101. apply(compiler) {
  102. const scheme = this.scheme;
  103. const cachedParseResourceWithoutFragment =
  104. parseResourceWithoutFragment.bindCache(compiler.root);
  105. compiler.hooks.compilation.tap(
  106. PLUGIN_NAME,
  107. (compilation, { normalModuleFactory }) => {
  108. normalModuleFactory.hooks.resolveForScheme
  109. .for(scheme)
  110. .tap(PLUGIN_NAME, resourceData => {
  111. const virtualConfig = this.findVirtualModuleConfigById(
  112. resourceData.resource
  113. );
  114. const url = cachedParseResourceWithoutFragment(
  115. resourceData.resource
  116. );
  117. const path = url.path;
  118. const type = virtualConfig.type;
  119. resourceData.path = path + type;
  120. resourceData.resource = path;
  121. if (virtualConfig.version) {
  122. const cacheKey = toCacheKey(resourceData.resource, scheme);
  123. const cacheVersion = this.getCacheVersion(virtualConfig.version);
  124. compilation.valueCacheVersions.set(
  125. cacheKey,
  126. /** @type {string} */ (cacheVersion)
  127. );
  128. }
  129. return true;
  130. });
  131. const hooks = NormalModule.getCompilationHooks(compilation);
  132. hooks.readResource
  133. .for(scheme)
  134. .tapAsync(PLUGIN_NAME, async (loaderContext, callback) => {
  135. const { resourcePath } = loaderContext;
  136. const module = /** @type {NormalModule} */ (loaderContext._module);
  137. const cacheKey = toCacheKey(resourcePath, scheme);
  138. const addVersionValueDependency = () => {
  139. if (!module || !module.buildInfo) return;
  140. const buildInfo = module.buildInfo;
  141. if (!buildInfo.valueDependencies) {
  142. buildInfo.valueDependencies = new Map();
  143. }
  144. const cacheVersion = compilation.valueCacheVersions.get(cacheKey);
  145. if (compilation.valueCacheVersions.has(cacheKey)) {
  146. buildInfo.valueDependencies.set(
  147. cacheKey,
  148. /** @type {string} */ (cacheVersion)
  149. );
  150. }
  151. };
  152. try {
  153. const virtualConfig =
  154. this.findVirtualModuleConfigById(resourcePath);
  155. const content = await virtualConfig.source(loaderContext);
  156. addVersionValueDependency();
  157. callback(null, content);
  158. } catch (err) {
  159. callback(/** @type {Error} */ (err));
  160. }
  161. });
  162. }
  163. );
  164. }
  165. /**
  166. * @param {string} id The module id
  167. * @returns {VirtualModuleConfig} The virtual module config
  168. */
  169. findVirtualModuleConfigById(id) {
  170. const config = this.modules[id];
  171. if (!config) {
  172. throw new ModuleNotFoundError(
  173. null,
  174. new Error(`Can't resolve virtual module ${id}`),
  175. {
  176. name: `virtual module ${id}`
  177. }
  178. );
  179. }
  180. return config;
  181. }
  182. /**
  183. * Get the cache version for a given version value
  184. * @param {(() => string) | true | string} version The version value or function
  185. * @returns {string | undefined} The cache version
  186. */
  187. getCacheVersion(version) {
  188. return version === true
  189. ? undefined
  190. : (typeof version === "function" ? version() : version) || "unset";
  191. }
  192. }
  193. VirtualUrlPlugin.DEFAULT_SCHEME = DEFAULT_SCHEME;
  194. module.exports = VirtualUrlPlugin;