VirtualUrlPlugin.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Natsu @xiaoxiaojx
  4. */
  5. "use strict";
  6. const { getContext } = require("loader-runner");
  7. const ModuleNotFoundError = require("../ModuleNotFoundError");
  8. const NormalModule = require("../NormalModule");
  9. const { isAbsolute, join } = require("../util/fs");
  10. const { parseResourceWithoutFragment } = require("../util/identifier");
  11. const DEFAULT_SCHEME = "virtual";
  12. const PLUGIN_NAME = "VirtualUrlPlugin";
  13. /**
  14. * Defines the compiler type used by this module.
  15. * @typedef {import("../Compiler")} Compiler
  16. * @typedef {import("../../declarations/plugins/schemes/VirtualUrlPlugin").VirtualModule} VirtualModuleConfig
  17. * @typedef {import("../../declarations/plugins/schemes/VirtualUrlPlugin").VirtualModuleContent} VirtualModuleInput
  18. * @typedef {import("../../declarations/plugins/schemes/VirtualUrlPlugin").VirtualUrlOptions} VirtualUrlOptions
  19. */
  20. /** @typedef {(loaderContext: LoaderContext<EXPECTED_ANY>) => Promise<string | Buffer> | string | Buffer} SourceFn */
  21. /** @typedef {() => string} VersionFn */
  22. /** @typedef {{ [key: string]: VirtualModuleInput }} VirtualModules */
  23. /**
  24. * Defines the loader context type used by this module.
  25. * @template T
  26. * @typedef {import("../../declarations/LoaderContext").LoaderContext<T>} LoaderContext
  27. */
  28. /**
  29. * Normalizes a virtual module definition into a standard format
  30. * @param {VirtualModuleInput} virtualConfig The virtual module to normalize
  31. * @returns {VirtualModuleConfig} The normalized virtual module
  32. */
  33. function normalizeModule(virtualConfig) {
  34. if (typeof virtualConfig === "string") {
  35. return {
  36. type: "",
  37. source() {
  38. return virtualConfig;
  39. }
  40. };
  41. } else if (typeof virtualConfig === "function") {
  42. return {
  43. type: "",
  44. source: virtualConfig
  45. };
  46. }
  47. return virtualConfig;
  48. }
  49. /** @typedef {{ [key: string]: VirtualModuleConfig }} NormalizedModules */
  50. /**
  51. * Normalizes all virtual modules with the given scheme
  52. * @param {VirtualModules} virtualConfigs The virtual modules to normalize
  53. * @param {string} scheme The URL scheme to use
  54. * @returns {NormalizedModules} The normalized virtual modules
  55. */
  56. function normalizeModules(virtualConfigs, scheme) {
  57. return Object.keys(virtualConfigs).reduce((pre, id) => {
  58. pre[toVid(id, scheme)] = normalizeModule(virtualConfigs[id]);
  59. return pre;
  60. }, /** @type {NormalizedModules} */ ({}));
  61. }
  62. /**
  63. * Converts a module id and scheme to a virtual module id
  64. * @param {string} id The module id
  65. * @param {string} scheme The URL scheme
  66. * @returns {string} The virtual module id
  67. */
  68. function toVid(id, scheme) {
  69. return `${scheme}:${id}`;
  70. }
  71. /**
  72. * Converts a virtual module id to a module id
  73. * @param {string} vid The virtual module id
  74. * @param {string} scheme The URL scheme
  75. * @returns {string} The module id
  76. */
  77. function fromVid(vid, scheme) {
  78. return vid.replace(`${scheme}:`, "");
  79. }
  80. const VALUE_DEP_VERSION = `webpack/${PLUGIN_NAME}/version`;
  81. /**
  82. * Converts a module id and scheme to a cache key
  83. * @param {string} id The module id
  84. * @param {string} scheme The URL scheme
  85. * @returns {string} The cache key
  86. */
  87. function toCacheKey(id, scheme) {
  88. return `${VALUE_DEP_VERSION}/${toVid(id, scheme)}`;
  89. }
  90. class VirtualUrlPlugin {
  91. /**
  92. * Creates an instance of VirtualUrlPlugin.
  93. * @param {VirtualModules} modules The virtual modules
  94. * @param {Omit<VirtualUrlOptions, "modules"> | string=} schemeOrOptions The URL scheme to use
  95. */
  96. constructor(modules, schemeOrOptions) {
  97. /** @type {VirtualUrlOptions} */
  98. this.options = {
  99. modules,
  100. ...(typeof schemeOrOptions === "string"
  101. ? { scheme: schemeOrOptions }
  102. : schemeOrOptions || {})
  103. };
  104. /** @type {string} */
  105. this.scheme = this.options.scheme || DEFAULT_SCHEME;
  106. /** @type {VirtualUrlOptions["context"]} */
  107. this.context = this.options.context || "auto";
  108. /** @type {NormalizedModules} */
  109. this.modules = normalizeModules(this.options.modules, this.scheme);
  110. }
  111. /**
  112. * Applies the plugin by registering its hooks on the compiler.
  113. * @param {Compiler} compiler the compiler instance
  114. * @returns {void}
  115. */
  116. apply(compiler) {
  117. compiler.hooks.validate.tap(PLUGIN_NAME, () => {
  118. compiler.validate(
  119. () => require("../../schemas/plugins/schemes/VirtualUrlPlugin.json"),
  120. this.options,
  121. {
  122. name: "Virtual Url Plugin",
  123. baseDataPath: "options"
  124. },
  125. (options) =>
  126. require("../../schemas/plugins/schemes/VirtualUrlPlugin.check")(
  127. options
  128. )
  129. );
  130. });
  131. const scheme = this.scheme;
  132. const cachedParseResourceWithoutFragment =
  133. parseResourceWithoutFragment.bindCache(compiler.root);
  134. compiler.hooks.compilation.tap(
  135. PLUGIN_NAME,
  136. (compilation, { normalModuleFactory }) => {
  137. compilation.hooks.assetPath.tap(
  138. { name: PLUGIN_NAME, before: "TemplatedPathPlugin" },
  139. (path, data) => {
  140. if (data.filename && this.modules[data.filename]) {
  141. /**
  142. * Returns safe path.
  143. * @param {string} str path
  144. * @returns {string} safe path
  145. */
  146. const toSafePath = (str) =>
  147. `__${str
  148. .replace(/:/g, "__")
  149. .replace(/^[^a-z0-9]+|[^a-z0-9]+$/gi, "")
  150. .replace(/[^a-z0-9._-]+/gi, "_")}`;
  151. // filename: virtual:logo.svg -> __virtual__logo.svg
  152. data.filename = toSafePath(data.filename);
  153. }
  154. return path;
  155. }
  156. );
  157. normalModuleFactory.hooks.resolveForScheme
  158. .for(scheme)
  159. .tap(PLUGIN_NAME, (resourceData) => {
  160. const virtualConfig = this.findVirtualModuleConfigById(
  161. resourceData.resource
  162. );
  163. const url = cachedParseResourceWithoutFragment(
  164. resourceData.resource
  165. );
  166. const path = url.path;
  167. const type = virtualConfig.type || "";
  168. const context = virtualConfig.context || this.context;
  169. resourceData.path = path + type;
  170. resourceData.resource = path;
  171. if (context === "auto") {
  172. const context = getContext(path);
  173. if (context === path) {
  174. resourceData.context = compiler.context;
  175. } else {
  176. const resolvedContext = fromVid(context, scheme);
  177. resourceData.context = isAbsolute(resolvedContext)
  178. ? resolvedContext
  179. : join(
  180. /** @type {import("..").InputFileSystem} */
  181. (compiler.inputFileSystem),
  182. compiler.context,
  183. resolvedContext
  184. );
  185. }
  186. } else if (context && typeof context === "string") {
  187. resourceData.context = context;
  188. } else {
  189. resourceData.context = compiler.context;
  190. }
  191. if (virtualConfig.version) {
  192. const cacheKey = toCacheKey(resourceData.resource, scheme);
  193. const cacheVersion = this.getCacheVersion(virtualConfig.version);
  194. compilation.valueCacheVersions.set(
  195. cacheKey,
  196. /** @type {string} */ (cacheVersion)
  197. );
  198. }
  199. return true;
  200. });
  201. const hooks = NormalModule.getCompilationHooks(compilation);
  202. hooks.readResource
  203. .for(scheme)
  204. .tapAsync(PLUGIN_NAME, async (loaderContext, callback) => {
  205. const { resourcePath } = loaderContext;
  206. const module = /** @type {NormalModule} */ (loaderContext._module);
  207. const cacheKey = toCacheKey(resourcePath, scheme);
  208. const addVersionValueDependency = () => {
  209. if (!module || !module.buildInfo) return;
  210. const buildInfo = module.buildInfo;
  211. if (!buildInfo.valueDependencies) {
  212. buildInfo.valueDependencies = new Map();
  213. }
  214. const cacheVersion = compilation.valueCacheVersions.get(cacheKey);
  215. if (compilation.valueCacheVersions.has(cacheKey)) {
  216. buildInfo.valueDependencies.set(
  217. cacheKey,
  218. /** @type {string} */ (cacheVersion)
  219. );
  220. }
  221. };
  222. try {
  223. const virtualConfig =
  224. this.findVirtualModuleConfigById(resourcePath);
  225. const content = await virtualConfig.source(loaderContext);
  226. addVersionValueDependency();
  227. callback(null, content);
  228. } catch (err) {
  229. callback(/** @type {Error} */ (err));
  230. }
  231. });
  232. }
  233. );
  234. }
  235. /**
  236. * Finds virtual module config by id.
  237. * @param {string} id The module id
  238. * @returns {VirtualModuleConfig} The virtual module config
  239. */
  240. findVirtualModuleConfigById(id) {
  241. const config = this.modules[id];
  242. if (!config) {
  243. throw new ModuleNotFoundError(
  244. null,
  245. new Error(`Can't resolve virtual module ${id}`),
  246. {
  247. name: `virtual module ${id}`
  248. }
  249. );
  250. }
  251. return config;
  252. }
  253. /**
  254. * Get the cache version for a given version value
  255. * @param {VersionFn | true | string} version The version value or function
  256. * @returns {string | undefined} The cache version
  257. */
  258. getCacheVersion(version) {
  259. return version === true
  260. ? undefined
  261. : (typeof version === "function" ? version() : version) || "unset";
  262. }
  263. }
  264. VirtualUrlPlugin.DEFAULT_SCHEME = DEFAULT_SCHEME;
  265. module.exports = VirtualUrlPlugin;