CssUrlDependency.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Ivan Kopeykin @vankop
  4. */
  5. "use strict";
  6. const { ASSET_URL_TYPE } = require("../ModuleSourceTypeConstants");
  7. const RawDataUrlModule = require("../asset/RawDataUrlModule");
  8. const makeSerializable = require("../util/makeSerializable");
  9. const memoize = require("../util/memoize");
  10. const ModuleDependency = require("./ModuleDependency");
  11. /** @import { ReplaceSource } from "webpack-sources" */
  12. /** @import CodeGenerationResults from "../CodeGenerationResults" */
  13. /** @import Dependency, { UpdateHashContext } from "../Dependency" */
  14. /**
  15. * @import {
  16. * CssDependencyTemplateContext as DependencyTemplateContext
  17. * } from "../DependencyTemplate"
  18. */
  19. /** @import Module from "../Module" */
  20. /** @import { Range } from "../javascript/JavascriptParser" */
  21. /** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext<["string" | "url" | "src", true | undefined, true | undefined, ("high" | "low" | "auto" | undefined), string | undefined, string | undefined, string | undefined]>} ObjectDeserializerContext */
  22. /** @typedef {import("../serialization/ObjectMiddleware").ObjectSerializerContext<["string" | "url" | "src", true | undefined, true | undefined, ("high" | "low" | "auto" | undefined), string | undefined, string | undefined, string | undefined]>} ObjectSerializerContext */
  23. /** @import Hash from "../util/Hash" */
  24. /** @import { RuntimeSpec } from "../util/runtime" */
  25. /** @import { NormalModuleBuildInfo } from "../NormalModule" */
  26. const getIgnoredRawDataUrlModule = memoize(
  27. () => new RawDataUrlModule("data:,", "ignored-asset", "(ignored asset)")
  28. );
  29. class CssUrlDependency extends ModuleDependency {
  30. /**
  31. * Creates an instance of CssUrlDependency.
  32. * @param {string} request request
  33. * @param {Range} range range of the argument
  34. * @param {"string" | "url" | "src"} urlType dependency type e.g. url() or string
  35. */
  36. constructor(request, range, urlType) {
  37. super(request);
  38. this.range = range;
  39. this.urlType = urlType;
  40. /** @type {true | undefined} */
  41. this.prefetch = undefined;
  42. /** @type {true | undefined} */
  43. this.preload = undefined;
  44. /** @type {"low" | "high" | "auto" | undefined} */
  45. this.fetchPriority = undefined;
  46. /** @type {string | undefined} */
  47. this.asAttribute = undefined;
  48. /** @type {string | undefined} */
  49. this.typeAttribute = undefined;
  50. /** @type {string | undefined} */
  51. this.mediaAttribute = undefined;
  52. }
  53. get type() {
  54. return "css url()";
  55. }
  56. get category() {
  57. return "url";
  58. }
  59. get referencedSourceType() {
  60. return ASSET_URL_TYPE;
  61. }
  62. /**
  63. * Creates an ignored module.
  64. * @param {string} context context directory
  65. * @returns {Module} ignored module
  66. */
  67. createIgnoredModule(context) {
  68. return getIgnoredRawDataUrlModule();
  69. }
  70. /**
  71. * Updates the hash with the data contributed by this instance.
  72. * @param {Hash} hash hash to be updated
  73. * @param {UpdateHashContext} context context
  74. * @returns {void}
  75. */
  76. updateHash(hash, context) {
  77. // The dependency template substitutes the referenced asset's hashed
  78. // filename into the rendered CSS at code-generation time. Folding the
  79. // asset module's content hash into the dependency hash ensures the
  80. // CSS module's hash invalidates — and the CSS chunk's contenthash
  81. // updates — whenever the referenced asset's content changes.
  82. const { chunkGraph } = context;
  83. const module = chunkGraph.moduleGraph.getModule(this);
  84. if (!module) return;
  85. const buildInfo = /** @type {NormalModuleBuildInfo | undefined} */ (
  86. module.buildInfo
  87. );
  88. if (buildInfo && buildInfo.hash) {
  89. hash.update(/** @type {string} */ (buildInfo.hash));
  90. }
  91. }
  92. /**
  93. * Serializes this instance into the provided serializer context.
  94. * @param {ObjectSerializerContext} context context
  95. */
  96. serialize(context) {
  97. context
  98. .write(this.urlType)
  99. .write(this.prefetch)
  100. .write(this.preload)
  101. .write(this.fetchPriority)
  102. .write(this.asAttribute)
  103. .write(this.typeAttribute)
  104. .write(this.mediaAttribute);
  105. super.serialize(context);
  106. }
  107. /**
  108. * Restores this instance from the provided deserializer context.
  109. * @param {ObjectDeserializerContext} context context
  110. */
  111. deserialize(context) {
  112. this.urlType = context.read();
  113. const c1 = context.rest;
  114. this.prefetch = c1.read();
  115. const c2 = c1.rest;
  116. this.preload = c2.read();
  117. const c3 = c2.rest;
  118. this.fetchPriority = c3.read();
  119. const c4 = c3.rest;
  120. this.asAttribute = c4.read();
  121. const c5 = c4.rest;
  122. this.typeAttribute = c5.read();
  123. const c6 = c5.rest;
  124. this.mediaAttribute = c6.read();
  125. super.deserialize(c6.rest);
  126. }
  127. }
  128. // Hoisted out of `cssEscapeString` so the patterns + replacer aren't
  129. // recompiled/reallocated per `url()` / `src()` reference at code-gen.
  130. const CSS_ESCAPE_UNQUOTED_REGEXP = /[\n\t ()'"\\]/g;
  131. const CSS_ESCAPE_DOUBLE_REGEXP = /[\n"\\]/g;
  132. const CSS_ESCAPE_SINGLE_REGEXP = /[\n'\\]/g;
  133. /**
  134. * @param {string} m matched character
  135. * @returns {string} the character backslash-escaped
  136. */
  137. const cssEscapeReplacer = (m) => `\\${m}`;
  138. /**
  139. * Returns string in quotes if needed.
  140. * @param {string} str string
  141. * @returns {string} string in quotes if needed
  142. */
  143. const cssEscapeString = (str) => {
  144. let countWhiteOrBracket = 0;
  145. let countQuotation = 0;
  146. let countApostrophe = 0;
  147. for (let i = 0; i < str.length; i++) {
  148. const cc = str.charCodeAt(i);
  149. switch (cc) {
  150. case 9: // tab
  151. case 10: // nl
  152. case 32: // space
  153. case 40: // (
  154. case 41: // )
  155. countWhiteOrBracket++;
  156. break;
  157. case 34:
  158. countQuotation++;
  159. break;
  160. case 39:
  161. countApostrophe++;
  162. break;
  163. }
  164. }
  165. if (countWhiteOrBracket < 2) {
  166. return str.replace(CSS_ESCAPE_UNQUOTED_REGEXP, cssEscapeReplacer);
  167. } else if (countQuotation <= countApostrophe) {
  168. return `"${str.replace(CSS_ESCAPE_DOUBLE_REGEXP, cssEscapeReplacer)}"`;
  169. }
  170. return `'${str.replace(CSS_ESCAPE_SINGLE_REGEXP, cssEscapeReplacer)}'`;
  171. };
  172. CssUrlDependency.Template = class CssUrlDependencyTemplate extends (
  173. ModuleDependency.Template
  174. ) {
  175. /**
  176. * Applies the plugin by registering its hooks on the compiler.
  177. * @param {Dependency} dependency the dependency for which the template should be applied
  178. * @param {ReplaceSource} source the current replace source which can be modified
  179. * @param {DependencyTemplateContext} templateContext the context object
  180. * @returns {void}
  181. */
  182. apply(
  183. dependency,
  184. source,
  185. { type, moduleGraph, runtimeTemplate, codeGenerationResults }
  186. ) {
  187. if (type === "javascript") return;
  188. const dep = /** @type {CssUrlDependency} */ (dependency);
  189. const module = /** @type {Module} */ (moduleGraph.getModule(dep));
  190. /** @type {string | undefined} */
  191. let newValue;
  192. switch (dep.urlType) {
  193. case "string":
  194. newValue = cssEscapeString(
  195. this.assetUrl({
  196. module,
  197. codeGenerationResults
  198. })
  199. );
  200. break;
  201. case "url":
  202. newValue = `url(${cssEscapeString(
  203. this.assetUrl({
  204. module,
  205. codeGenerationResults
  206. })
  207. )})`;
  208. break;
  209. case "src":
  210. newValue = `src(${cssEscapeString(
  211. this.assetUrl({
  212. module,
  213. codeGenerationResults
  214. })
  215. )})`;
  216. break;
  217. }
  218. source.replace(
  219. dep.range[0],
  220. dep.range[1] - 1,
  221. /** @type {string} */ (newValue)
  222. );
  223. }
  224. /**
  225. * Returns the url of the asset.
  226. * @param {object} options options object
  227. * @param {Module} options.module the module
  228. * @param {RuntimeSpec=} options.runtime runtime
  229. * @param {CodeGenerationResults} options.codeGenerationResults the code generation results
  230. * @returns {string} the url of the asset
  231. */
  232. assetUrl({ runtime, module, codeGenerationResults }) {
  233. if (!module) {
  234. return "data:,";
  235. }
  236. const codeGen = codeGenerationResults.get(module, runtime);
  237. const data = codeGen.data;
  238. if (!data) return "data:,";
  239. const url = data.get("url");
  240. if (!url || !url[ASSET_URL_TYPE]) return "data:,";
  241. return url[ASSET_URL_TYPE];
  242. }
  243. };
  244. makeSerializable(CssUrlDependency, "webpack/lib/dependencies/CssUrlDependency");
  245. module.exports = CssUrlDependency;