CssIcssExportDependency.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Ivan Kopeykin @vankop
  4. */
  5. "use strict";
  6. const { CSS_TYPE, JAVASCRIPT_TYPE } = require("../ModuleSourceTypeConstants");
  7. const WebpackError = require("../WebpackError");
  8. const { cssExportConvention } = require("../util/conventions");
  9. const createHash = require("../util/createHash");
  10. const { makePathsRelative } = require("../util/identifier");
  11. const makeSerializable = require("../util/makeSerializable");
  12. const memoize = require("../util/memoize");
  13. const nonNumericOnlyHash = require("../util/nonNumericOnlyHash");
  14. const CssIcssImportDependency = require("./CssIcssImportDependency");
  15. const NullDependency = require("./NullDependency");
  16. const getCssParser = memoize(() => require("../css/CssParser"));
  17. /** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */
  18. /** @typedef {import("../../declarations/WebpackOptions").CssGeneratorExportsConvention} CssGeneratorExportsConvention */
  19. /** @typedef {import("../../declarations/WebpackOptions").CssGeneratorLocalIdentName} CssGeneratorLocalIdentName */
  20. /** @typedef {import("../CssModule")} CssModule */
  21. /** @typedef {import("../Dependency")} Dependency */
  22. /** @typedef {import("../Dependency").ReferencedExports} ReferencedExports */
  23. /** @typedef {import("../Dependency").ExportsSpec} ExportsSpec */
  24. /** @typedef {import("../Dependency").UpdateHashContext} UpdateHashContext */
  25. /** @typedef {import("../DependencyTemplate").CssDependencyTemplateContext} DependencyTemplateContext */
  26. /** @typedef {import("../ModuleGraph")} ModuleGraph */
  27. /** @typedef {import("../css/CssGenerator")} CssGenerator */
  28. /** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
  29. /** @typedef {import("../serialization/ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */
  30. /** @typedef {import("../util/Hash")} Hash */
  31. /** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */
  32. /** @typedef {import("../ChunkGraph")} ChunkGraph */
  33. /** @typedef {import("../RuntimeTemplate")} RuntimeTemplate */
  34. /** @typedef {import("../css/CssParser").Range} Range */
  35. /**
  36. * @param {string} local css local
  37. * @param {CssModule} module module
  38. * @param {ChunkGraph} chunkGraph chunk graph
  39. * @param {RuntimeTemplate} runtimeTemplate runtime template
  40. * @returns {string} local ident
  41. */
  42. const getLocalIdent = (local, module, chunkGraph, runtimeTemplate) => {
  43. const generator = /** @type {CssGenerator} */ (module.generator);
  44. const localIdentName =
  45. /** @type {CssGeneratorLocalIdentName} */
  46. (generator.options.localIdentName);
  47. const relativeResourcePath = makePathsRelative(
  48. /** @type {string} */
  49. (module.context),
  50. /** @type {string} */
  51. (module.getResource()),
  52. runtimeTemplate.compilation.compiler.root
  53. );
  54. const { uniqueName } = runtimeTemplate.outputOptions;
  55. let localIdentHash = "";
  56. if (/\[(fullhash|hash)\]/.test(localIdentName)) {
  57. const hashSalt = generator.options.localIdentHashSalt;
  58. const hashDigest =
  59. /** @type {string} */
  60. (generator.options.localIdentHashDigest);
  61. const hashDigestLength = generator.options.localIdentHashDigestLength;
  62. const { hashFunction } = runtimeTemplate.outputOptions;
  63. const hash = createHash(hashFunction);
  64. if (hashSalt) {
  65. hash.update(hashSalt);
  66. }
  67. if (uniqueName) {
  68. hash.update(uniqueName);
  69. }
  70. hash.update(relativeResourcePath);
  71. hash.update(local);
  72. localIdentHash = hash.digest(hashDigest).slice(0, hashDigestLength);
  73. }
  74. let contentHash = "";
  75. if (/\[contenthash\]/.test(localIdentName)) {
  76. const hash = createHash(runtimeTemplate.outputOptions.hashFunction);
  77. const source = module.originalSource();
  78. if (source) {
  79. hash.update(source.buffer());
  80. }
  81. if (module.error) {
  82. hash.update(module.error.toString());
  83. }
  84. const fullContentHash = hash.digest(
  85. runtimeTemplate.outputOptions.hashDigest
  86. );
  87. contentHash = nonNumericOnlyHash(
  88. fullContentHash,
  89. runtimeTemplate.outputOptions.hashDigestLength
  90. );
  91. }
  92. let localIdent = runtimeTemplate.compilation.getPath(localIdentName, {
  93. prepareId: (id) => {
  94. if (typeof id !== "string") return id;
  95. return id
  96. .replace(/^([.-]|[^a-zA-Z0-9_-])+/, "")
  97. .replace(/[^a-zA-Z0-9_-]+/g, "_");
  98. },
  99. filename: relativeResourcePath,
  100. hash: localIdentHash,
  101. contentHash,
  102. chunkGraph,
  103. module
  104. });
  105. if (/\[local\]/.test(localIdentName)) {
  106. localIdent = localIdent.replace(/\[local\]/g, local);
  107. }
  108. if (/\[uniqueName\]/.test(localIdentName)) {
  109. localIdent = localIdent.replace(
  110. /\[uniqueName\]/g,
  111. /** @type {string} */ (uniqueName)
  112. );
  113. }
  114. // Protect the first character from unsupported values
  115. return localIdent.replace(/^((-?[0-9])|--)/, "_$1");
  116. };
  117. // 0 - replace, 1 - replace, 2 - append, 2 - once
  118. /** @typedef {0 | 1 | 2 | 3 | 4} ExportMode */
  119. // 0 - normal, 1 - custom css variable, 2 - grid custom ident
  120. /** @typedef {0 | 1 | 2} ExportType */
  121. class CssIcssExportDependency extends NullDependency {
  122. /**
  123. * Example of dependency:
  124. *
  125. * :export { LOCAL_NAME: EXPORT_NAME }
  126. * @param {string} name export name
  127. * @param {string | [string, string, boolean]} value export value or true when we need interpolate name as a value
  128. * @param {Range=} range range
  129. * @param {boolean=} interpolate true when value need to be interpolated, otherwise false
  130. * @param {ExportMode=} exportMode export mode
  131. * @param {ExportType=} exportType export type
  132. */
  133. constructor(
  134. name,
  135. value,
  136. range,
  137. interpolate = false,
  138. exportMode = CssIcssExportDependency.EXPORT_MODE.REPLACE,
  139. exportType = CssIcssExportDependency.EXPORT_TYPE.NORMAL
  140. ) {
  141. super();
  142. this.name = name;
  143. this.value = value;
  144. this.range = range;
  145. this.interpolate = interpolate;
  146. this.exportMode = exportMode;
  147. this.exportType = exportType;
  148. this._hashUpdate = undefined;
  149. }
  150. get type() {
  151. return "css :export";
  152. }
  153. /**
  154. * @param {string} name export name
  155. * @param {CssGeneratorExportsConvention} convention convention of the export name
  156. * @returns {string[]} convention results
  157. */
  158. getExportsConventionNames(name, convention) {
  159. if (this._conventionNames) {
  160. return this._conventionNames;
  161. }
  162. this._conventionNames = cssExportConvention(name, convention);
  163. return this._conventionNames;
  164. }
  165. /**
  166. * Returns list of exports referenced by this dependency
  167. * @param {ModuleGraph} moduleGraph module graph
  168. * @param {RuntimeSpec} runtime the runtime for which the module is analysed
  169. * @returns {ReferencedExports} referenced exports
  170. */
  171. getReferencedExports(moduleGraph, runtime) {
  172. if (
  173. this.exportMode === CssIcssExportDependency.EXPORT_MODE.SELF_REFERENCE
  174. ) {
  175. return [
  176. {
  177. name: [this.name],
  178. canMangle: true
  179. }
  180. ];
  181. }
  182. return super.getReferencedExports(moduleGraph, runtime);
  183. }
  184. /**
  185. * Returns the exported names
  186. * @param {ModuleGraph} moduleGraph module graph
  187. * @returns {ExportsSpec | undefined} export names
  188. */
  189. getExports(moduleGraph) {
  190. if (
  191. this.exportMode === CssIcssExportDependency.EXPORT_MODE.NONE ||
  192. this.exportMode === CssIcssExportDependency.EXPORT_MODE.SELF_REFERENCE
  193. ) {
  194. return;
  195. }
  196. const module = /** @type {CssModule} */ (moduleGraph.getParentModule(this));
  197. const generator = /** @type {CssGenerator} */ (module.generator);
  198. const names = this.getExportsConventionNames(
  199. this.name,
  200. /** @type {CssGeneratorExportsConvention} */
  201. (generator.options.exportsConvention)
  202. );
  203. return {
  204. exports: [
  205. ...names,
  206. ...(Array.isArray(this.value) ? [this.value[1]] : [])
  207. ].map((name) => ({
  208. name,
  209. canMangle: true
  210. })),
  211. dependencies: undefined
  212. };
  213. }
  214. /**
  215. * Returns warnings
  216. * @param {ModuleGraph} moduleGraph module graph
  217. * @returns {WebpackError[] | null | undefined} warnings
  218. */
  219. getWarnings(moduleGraph) {
  220. if (
  221. this.exportMode === CssIcssExportDependency.EXPORT_MODE.SELF_REFERENCE &&
  222. !Array.isArray(this.value)
  223. ) {
  224. const module = moduleGraph.getParentModule(this);
  225. if (
  226. module &&
  227. !moduleGraph.getExportsInfo(module).isExportProvided(this.value)
  228. ) {
  229. const error = new WebpackError(
  230. `Self-referencing name "${this.value}" not found`
  231. );
  232. error.module = module;
  233. return [error];
  234. }
  235. }
  236. return null;
  237. }
  238. /**
  239. * Update the hash
  240. * @param {Hash} hash hash to be updated
  241. * @param {UpdateHashContext} context context
  242. * @returns {void}
  243. */
  244. updateHash(hash, { chunkGraph }) {
  245. if (this._hashUpdate === undefined) {
  246. const module =
  247. /** @type {CssModule} */
  248. (chunkGraph.moduleGraph.getParentModule(this));
  249. const generator = /** @type {CssGenerator} */ (module.generator);
  250. const names = this.getExportsConventionNames(
  251. this.name,
  252. /** @type {CssGeneratorExportsConvention} */
  253. (generator.options.exportsConvention)
  254. );
  255. this._hashUpdate = `exportsConvention|${JSON.stringify(names)}|localIdentName|${JSON.stringify(generator.options.localIdentName)}`;
  256. }
  257. hash.update(this._hashUpdate);
  258. }
  259. /**
  260. * @param {ObjectSerializerContext} context context
  261. */
  262. serialize(context) {
  263. const { write } = context;
  264. write(this.name);
  265. write(this.value);
  266. write(this.range);
  267. write(this.interpolate);
  268. write(this.exportMode);
  269. write(this.exportType);
  270. super.serialize(context);
  271. }
  272. /**
  273. * @param {ObjectDeserializerContext} context context
  274. */
  275. deserialize(context) {
  276. const { read } = context;
  277. this.name = read();
  278. this.value = read();
  279. this.range = read();
  280. this.interpolate = read();
  281. this.exportMode = read();
  282. this.exportType = read();
  283. super.deserialize(context);
  284. }
  285. }
  286. CssIcssExportDependency.Template = class CssIcssExportDependencyTemplate extends (
  287. NullDependency.Template
  288. ) {
  289. // TODO looking how to cache
  290. /**
  291. * @param {string} symbol the name of symbol
  292. * @param {DependencyTemplateContext} templateContext the context object
  293. * @returns {string | undefined} found reference
  294. */
  295. static findReference(symbol, templateContext) {
  296. for (const item of templateContext.module.dependencies) {
  297. if (item instanceof CssIcssImportDependency) {
  298. // Looking for the referring module
  299. const module = templateContext.moduleGraph.getModule(item);
  300. if (!module) {
  301. return undefined;
  302. }
  303. for (let i = module.dependencies.length - 1; i >= 0; i--) {
  304. const nestedDep = module.dependencies[i];
  305. if (
  306. nestedDep instanceof CssIcssExportDependency &&
  307. symbol === nestedDep.name
  308. ) {
  309. if (Array.isArray(nestedDep.value)) {
  310. return this.findReference(nestedDep.value[1], {
  311. ...templateContext,
  312. module
  313. });
  314. }
  315. return CssIcssExportDependency.Template.getIdentifier(
  316. nestedDep.value,
  317. nestedDep,
  318. {
  319. ...templateContext,
  320. module
  321. }
  322. );
  323. }
  324. }
  325. }
  326. }
  327. }
  328. /**
  329. * @param {string} value value to identifier
  330. * @param {Dependency} dependency the dependency for which the template should be applied
  331. * @param {DependencyTemplateContext} templateContext the context object
  332. * @returns {string} identifier
  333. */
  334. static getIdentifier(value, dependency, templateContext) {
  335. const dep = /** @type {CssIcssExportDependency} */ (dependency);
  336. if (dep.interpolate) {
  337. const { module: m } = templateContext;
  338. const module = /** @type {CssModule} */ (m);
  339. const generator = /** @type {CssGenerator} */ (module.generator);
  340. const local = cssExportConvention(
  341. value,
  342. /** @type {CssGeneratorExportsConvention} */
  343. (generator.options.exportsConvention)
  344. )[0];
  345. const prefix =
  346. dep.exportType === CssIcssExportDependency.EXPORT_TYPE.CUSTOM_VARIABLE
  347. ? "--"
  348. : "";
  349. return (
  350. prefix +
  351. getCssParser().escapeIdentifier(
  352. getLocalIdent(
  353. local,
  354. /** @type {CssModule} */
  355. (templateContext.module),
  356. templateContext.chunkGraph,
  357. templateContext.runtimeTemplate
  358. )
  359. )
  360. );
  361. }
  362. return /** @type {string} */ (dep.value);
  363. }
  364. /**
  365. * @param {Dependency} dependency the dependency for which the template should be applied
  366. * @param {ReplaceSource} source the current replace source which can be modified
  367. * @param {DependencyTemplateContext} templateContext the context object
  368. * @returns {void}
  369. */
  370. apply(dependency, source, templateContext) {
  371. const dep = /** @type {CssIcssExportDependency} */ (dependency);
  372. if (!dep.range && templateContext.type !== JAVASCRIPT_TYPE) return;
  373. const { cssData } = templateContext;
  374. const { module: m, moduleGraph, runtime } = templateContext;
  375. const module = /** @type {CssModule} */ (m);
  376. const generator = /** @type {CssGenerator} */ (module.generator);
  377. const names = dep.getExportsConventionNames(
  378. dep.name,
  379. /** @type {CssGeneratorExportsConvention} */
  380. (generator.options.exportsConvention)
  381. );
  382. const usedNames =
  383. /** @type {string[]} */
  384. (
  385. names
  386. .map((name) =>
  387. moduleGraph.getExportInfo(module, name).getUsedName(name, runtime)
  388. )
  389. .filter(Boolean)
  390. );
  391. const allNames = new Set([...usedNames, ...names]);
  392. const isReference = Array.isArray(dep.value);
  393. /** @type {string} */
  394. let value;
  395. if (isReference && dep.value[2] === true) {
  396. const resolved = CssIcssExportDependencyTemplate.findReference(
  397. dep.value[1],
  398. templateContext
  399. );
  400. // Fallback to the original name if not found
  401. value = resolved || dep.value[0];
  402. } else {
  403. value = isReference ? dep.value[1] : /** @type {string} */ (dep.value);
  404. }
  405. if (dep.interpolate) {
  406. value = CssIcssExportDependencyTemplate.getIdentifier(
  407. value,
  408. dep,
  409. templateContext
  410. );
  411. }
  412. if (
  413. dep.exportType ===
  414. CssIcssExportDependency.EXPORT_TYPE.GRID_CUSTOM_IDENTIFIER
  415. ) {
  416. value += `-${dep.name}`;
  417. }
  418. if (
  419. templateContext.type === JAVASCRIPT_TYPE &&
  420. dep.exportMode !== CssIcssExportDependency.EXPORT_MODE.NONE
  421. ) {
  422. for (const used of allNames) {
  423. if (dep.exportMode === CssIcssExportDependency.EXPORT_MODE.ONCE) {
  424. const newValue = getCssParser().unescapeIdentifier(value);
  425. if (isReference) {
  426. cssData.exports.set(dep.value[1], newValue);
  427. }
  428. if (cssData.exports.has(used)) return;
  429. cssData.exports.set(used, newValue);
  430. } else {
  431. const originalValue =
  432. dep.exportMode === CssIcssExportDependency.EXPORT_MODE.REPLACE
  433. ? undefined
  434. : cssData.exports.get(used);
  435. const newValue =
  436. dep.exportMode ===
  437. CssIcssExportDependency.EXPORT_MODE.SELF_REFERENCE
  438. ? cssData.exports.get(
  439. isReference ? dep.value[0] : /** @type {string} */ (dep.value)
  440. ) || value
  441. : getCssParser().unescapeIdentifier(value);
  442. cssData.exports.set(
  443. used,
  444. `${originalValue ? `${originalValue}${newValue ? " " : ""}` : ""}${newValue}`
  445. );
  446. }
  447. }
  448. } else if (
  449. dep.range &&
  450. templateContext.type === CSS_TYPE &&
  451. dep.exportMode !== CssIcssExportDependency.EXPORT_MODE.APPEND &&
  452. dep.exportMode !== CssIcssExportDependency.EXPORT_MODE.SELF_REFERENCE
  453. ) {
  454. source.replace(dep.range[0], dep.range[1] - 1, value);
  455. }
  456. }
  457. };
  458. /** @type {Record<"NONE" | "REPLACE" | "APPEND" | "ONCE" | "SELF_REFERENCE", ExportMode>} */
  459. CssIcssExportDependency.EXPORT_MODE = {
  460. NONE: 0,
  461. REPLACE: 1,
  462. APPEND: 2,
  463. ONCE: 3,
  464. SELF_REFERENCE: 4
  465. };
  466. /** @type {Record<"NORMAL" | "CUSTOM_VARIABLE" | "GRID_CUSTOM_IDENTIFIER", ExportType>} */
  467. CssIcssExportDependency.EXPORT_TYPE = {
  468. NORMAL: 0,
  469. CUSTOM_VARIABLE: 1,
  470. GRID_CUSTOM_IDENTIFIER: 2
  471. };
  472. makeSerializable(
  473. CssIcssExportDependency,
  474. "webpack/lib/dependencies/CssIcssExportDependency"
  475. );
  476. module.exports = CssIcssExportDependency;