CssIcssExportDependency.js 33 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Ivan Kopeykin @vankop
  4. */
  5. "use strict";
  6. const { JAVASCRIPT_TYPE } = require("../ModuleSourceTypeConstants");
  7. const { interpolate } = require("../TemplatedPathPlugin");
  8. const WebpackError = require("../errors/WebpackError");
  9. const { cssExportConvention } = require("../util/conventions");
  10. const createHash = require("../util/createHash");
  11. const { makePathsRelative } = require("../util/identifier");
  12. const makeSerializable = require("../util/makeSerializable");
  13. const memoize = require("../util/memoize");
  14. const { digestNonNumericOnly } = require("../util/nonNumericOnlyHash");
  15. const { updateHashFromSource } = require("../util/source");
  16. const CssIcssImportDependency = require("./CssIcssImportDependency");
  17. const NullDependency = require("./NullDependency");
  18. const getCssTokens = memoize(() => require("../css/syntax"));
  19. /** @import { ReplaceSource } from "webpack-sources" */
  20. /**
  21. * @import {
  22. * CssGeneratorExportsConvention,
  23. * CssGeneratorLocalIdentName,
  24. * HashFunction
  25. * } from "../../declarations/WebpackOptions"
  26. */
  27. /** @import CssGenerator from "../css/CssGenerator" */
  28. /** @import CssModule from "../css/CssModule" */
  29. /** @import ChunkGraph from "../ChunkGraph" */
  30. /**
  31. * @import Dependency, {
  32. * ExportsSpec,
  33. * ReferencedExports,
  34. * UpdateHashContext,
  35. * DependencyLocation
  36. * } from "../Dependency"
  37. */
  38. /**
  39. * @import {
  40. * CssDependencyTemplateContext as DependencyTemplateContext
  41. * } from "../DependencyTemplate"
  42. */
  43. /** @import ModuleGraph from "../ModuleGraph" */
  44. /** @import RuntimeTemplate from "../RuntimeTemplate" */
  45. /** @import { Range } from "../css/CssParser" */
  46. /** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext<(number | string | Value | Range | boolean | ExportMode | ExportType | DependencyLocation | undefined)[]>} ObjectDeserializerContext */
  47. /** @typedef {import("../serialization/ObjectMiddleware").ObjectSerializerContext<(number | string | Value | Range | boolean | ExportMode | ExportType | DependencyLocation | undefined)[]>} ObjectSerializerContext */
  48. /** @import Hash from "../util/Hash" */
  49. /** @import { RuntimeSpec } from "../util/runtime" */
  50. /** @import WebpackErrorType from "../errors/WebpackError" */
  51. /** @typedef {string | [string, string] | [string, string, string]} Value */
  52. /** @typedef {(name: string) => string | string[]} ExportsConventionFn */
  53. // 0 - none, 1 - replace, 2 - append, 3 - once, 4 - self-reference
  54. /** @typedef {0 | 1 | 2 | 3 | 4} ExportMode */
  55. // 0 - normal, 1 - custom css variable, 2 - grid custom ident, 3 - composes
  56. /** @typedef {0 | 1 | 2 | 3} ExportType */
  57. /** @type {Record<"NONE" | "REPLACE" | "APPEND" | "ONCE" | "SELF_REFERENCE", ExportMode>} */
  58. const EXPORT_MODE = {
  59. NONE: 0,
  60. REPLACE: 1,
  61. APPEND: 2,
  62. ONCE: 3,
  63. SELF_REFERENCE: 4
  64. };
  65. /** @type {Record<"NORMAL" | "CUSTOM_VARIABLE" | "GRID_CUSTOM_IDENTIFIER" | "COMPOSES", ExportType>} */
  66. const EXPORT_TYPE = {
  67. NORMAL: 0,
  68. CUSTOM_VARIABLE: 1,
  69. GRID_CUSTOM_IDENTIFIER: 2,
  70. COMPOSES: 3
  71. };
  72. // Hoisted out of `getLocalIdent`'s `prepareId` so the patterns + the replacer
  73. // aren't recompiled/reallocated per interpolated id.
  74. const IDENT_LEADING_INVALID_REGEXP = /^([.-]|[^a-z0-9_-])+/i;
  75. const IDENT_INVALID_CHARS_REGEXP = /[^a-z0-9@_-]+/gi;
  76. const IDENT_LOCAL_PLACEHOLDER_REGEXP = /\[local\]/g;
  77. const IDENT_LEADING_PROTECT_REGEXP = /^((-?\d)|--)/;
  78. /**
  79. * `interpolate` `prepareId` hook — sanitize an id segment into a valid CSS ident.
  80. * Pure (captures nothing), so it's shared rather than allocated per call.
  81. * @param {string | number} id id segment
  82. * @returns {string | number} sanitized id segment
  83. */
  84. const prepareLocalIdentId = (id) => {
  85. if (typeof id !== "string") return id;
  86. return (
  87. id
  88. .replace(IDENT_LEADING_INVALID_REGEXP, "")
  89. // We keep the `@` symbol because it can be used in the package name (e.g. `@company/package`), and if we replace it with `_`, a class conflict may occur.
  90. // For example - `@import "@foo/package/style.module.css"` and `@import "foo/package/style.module.css"` (`foo` is a package, `package` is just a directory) will create a class conflict.
  91. .replace(IDENT_INVALID_CHARS_REGEXP, "_")
  92. );
  93. };
  94. /**
  95. * Returns local ident.
  96. * @param {string} local css local
  97. * @param {CssModule} module module
  98. * @param {ChunkGraph} chunkGraph chunk graph
  99. * @param {RuntimeTemplate} runtimeTemplate runtime template
  100. * @returns {string} local ident
  101. */
  102. const getLocalIdent = (local, module, chunkGraph, runtimeTemplate) => {
  103. const generator = /** @type {CssGenerator} */ (module.generator);
  104. const localIdentName =
  105. /** @type {CssGeneratorLocalIdentName} */
  106. (generator.options.localIdentName);
  107. const relativeResourcePath = makePathsRelative(
  108. /** @type {string} */
  109. (runtimeTemplate.compilation.compiler.context),
  110. /** @type {string} */
  111. (module.getResource()),
  112. runtimeTemplate.compilation.compiler.root
  113. );
  114. const { uniqueName } = runtimeTemplate.outputOptions;
  115. let localIdentHash = "";
  116. let localIdentHashFull = "";
  117. if (generator._localIdentNeedsHash) {
  118. const hashSalt = generator.options.localIdentHashSalt;
  119. const hashDigest =
  120. /** @type {string} */
  121. (generator.options.localIdentHashDigest);
  122. const hashDigestLength = generator.options.localIdentHashDigestLength;
  123. const hashFunction =
  124. /** @type {HashFunction} */
  125. (generator.options.localIdentHashFunction);
  126. const hash = createHash(hashFunction);
  127. if (hashSalt) {
  128. hash.update(hashSalt);
  129. }
  130. if (uniqueName) {
  131. hash.update(uniqueName);
  132. }
  133. hash.update(relativeResourcePath);
  134. hash.update(local);
  135. // Keep the untruncated digest so an inline `[fullhash:<digest>]` re-encodes
  136. // with full entropy (see the `fullHash`/`fullHashDigest` passed below).
  137. localIdentHashFull = hash.digest(hashDigest);
  138. localIdentHash = localIdentHashFull.slice(0, hashDigestLength);
  139. }
  140. let contentHash = "";
  141. if (generator._localIdentNeedsContentHash) {
  142. const hash = createHash(runtimeTemplate.outputOptions.hashFunction);
  143. const source = module.originalSource();
  144. if (source) {
  145. updateHashFromSource(hash, source);
  146. }
  147. if (module.error) {
  148. hash.update(module.error.toString());
  149. }
  150. contentHash = digestNonNumericOnly(
  151. hash,
  152. /** @type {string} */ (runtimeTemplate.outputOptions.hashDigest),
  153. /** @type {number} */ (runtimeTemplate.outputOptions.hashDigestLength)
  154. );
  155. }
  156. let localIdent = interpolate(localIdentName, {
  157. prepareId: prepareLocalIdentId,
  158. filename: relativeResourcePath,
  159. hash: localIdentHash,
  160. // css-loader semantics: `[hash]` is the local ident hash, not the module
  161. // hash (use `[modulehash]` for that).
  162. hashAsFullHash: true,
  163. // `[hash]`/`[contenthash]` are encoded with the output digest, but the
  164. // `[fullhash]` local ident hash uses `localIdentHashDigest`, so re-encode it
  165. // from its own (full) digest.
  166. hashDigest: runtimeTemplate.outputOptions.hashDigest,
  167. fullHash: localIdentHashFull,
  168. fullHashDigest:
  169. /** @type {string} */
  170. (generator.options.localIdentHashDigest),
  171. local,
  172. uniqueName,
  173. contentHash,
  174. chunkGraph,
  175. module
  176. });
  177. // TODO move this into interpolate
  178. // `replace` on a non-matching global regexp returns the string unchanged, so
  179. // the `.test` guard is redundant — one scan instead of two.
  180. localIdent = localIdent.replace(IDENT_LOCAL_PLACEHOLDER_REGEXP, local);
  181. // Protect the first character from unsupported values
  182. return localIdent.replace(IDENT_LEADING_PROTECT_REGEXP, "_$1");
  183. };
  184. /**
  185. * Computes the interpolated identifier for `(module, value, exportType)`.
  186. * Module-level reference so `moduleGraph.cached` can use it as a stable
  187. * computer key — repeated lookups during a build skip `cssExportConvention`,
  188. * `getLocalIdent` (with its content / path hashing) and `escapeIdentifier`.
  189. * Parameters are ordered by rising cardinality because `cached` keys a trie on
  190. * them — see the call site.
  191. * @param {ModuleGraph} _moduleGraph module graph (unused, kept for `cached` signature)
  192. * @param {CssModule} module css module the value resolves in
  193. * @param {ChunkGraph} chunkGraph chunk graph
  194. * @param {RuntimeTemplate} runtimeTemplate runtime template
  195. * @param {ExportType} exportType export type discriminator
  196. * @param {string} value raw value to interpolate
  197. * @returns {string} interpolated identifier
  198. */
  199. const computeInterpolatedIdentifier = (
  200. _moduleGraph,
  201. module,
  202. chunkGraph,
  203. runtimeTemplate,
  204. exportType,
  205. value
  206. ) => {
  207. const generator = /** @type {CssGenerator} */ (module.generator);
  208. const local = cssExportConvention(
  209. value,
  210. /** @type {CssGeneratorExportsConvention} */
  211. (generator.options.exportsConvention)
  212. )[0];
  213. const prefix = exportType === EXPORT_TYPE.CUSTOM_VARIABLE ? "--" : "";
  214. return (
  215. prefix +
  216. getCssTokens().escapeIdentifier(
  217. getLocalIdent(local, module, chunkGraph, runtimeTemplate),
  218. runtimeTemplate.compilation.compiler.root
  219. )
  220. );
  221. };
  222. /**
  223. * One `:export { name: value }` style entry — a plain object so a module holds
  224. * a single `Dependency` plus a flat array instead of thousands of `Dependency`
  225. * instances. `_conventionNames`/`_valueConventionNames` memoize the convention
  226. * expansion per entry.
  227. * @typedef {object} CssExportEntry
  228. * @property {string} name export name
  229. * @property {Value} value value or [localName, importName, request?]
  230. * @property {Range=} range source range to replace, when present
  231. * @property {boolean} interpolate whether the value needs interpolation
  232. * @property {ExportMode} exportMode export mode
  233. * @property {ExportType} exportType export type
  234. * @property {number} locStartLine per-export source start line (1-based; flat numbers instead of a nested `DependencyLocation` — entries are per-export hot allocations)
  235. * @property {number} locStartColumn per-export source start column
  236. * @property {number} locEndLine per-export source end line
  237. * @property {number} locEndColumn per-export source end column
  238. * @property {string[]=} _conventionNames memoized convention names for `name`
  239. * @property {string[]=} _valueConventionNames memoized convention names for `value`
  240. */
  241. /**
  242. * Memoized `cssExportConvention(entry.name, convention)`.
  243. * @param {CssExportEntry} entry entry
  244. * @param {CssGeneratorExportsConvention} convention convention
  245. * @returns {string[]} convention names
  246. */
  247. const entryConventionNames = (entry, convention) => {
  248. if (entry._conventionNames) return entry._conventionNames;
  249. entry._conventionNames = cssExportConvention(entry.name, convention);
  250. return entry._conventionNames;
  251. };
  252. /**
  253. * Memoized `cssExportConvention(entry.value, convention)`. Caller guarantees
  254. * `typeof entry.value === "string"`.
  255. * @param {CssExportEntry} entry entry
  256. * @param {CssGeneratorExportsConvention} convention convention
  257. * @returns {string[]} convention names
  258. */
  259. const entryValueConventionNames = (entry, convention) => {
  260. if (entry._valueConventionNames) return entry._valueConventionNames;
  261. entry._valueConventionNames = cssExportConvention(
  262. /** @type {string} */ (entry.value),
  263. convention
  264. );
  265. return entry._valueConventionNames;
  266. };
  267. class CssIcssExportDependency extends NullDependency {
  268. /**
  269. * One consolidated `:export` dependency for a whole CSS module.
  270. * @param {CssExportEntry[]} entries export entries in source order
  271. */
  272. constructor(entries) {
  273. super();
  274. /** @type {CssExportEntry[]} */
  275. this.entries = entries;
  276. /** @type {undefined | string} */
  277. this._hashUpdate = undefined;
  278. /** @type {undefined | Map<string, CssExportEntry[]>} lazily-built `name -> entries` index, reused across every importer's codegen */
  279. this._entriesByName = undefined;
  280. }
  281. /**
  282. * The `entries` grouped by export name, built once and reused. `entries` is
  283. * immutable after construction/deserialization, so this stays valid for the
  284. * dependency's lifetime (the composes/`@value` target of many importers).
  285. * @returns {Map<string, CssExportEntry[]>} entries grouped by export name
  286. */
  287. getEntriesByName() {
  288. if (this._entriesByName === undefined) {
  289. /** @type {Map<string, CssExportEntry[]>} */
  290. const map = new Map();
  291. for (const e of this.entries) {
  292. const list = map.get(e.name);
  293. if (list) list.push(e);
  294. else map.set(e.name, [e]);
  295. }
  296. this._entriesByName = map;
  297. }
  298. return this._entriesByName;
  299. }
  300. get type() {
  301. return "css :export";
  302. }
  303. /**
  304. * Returns the exported names
  305. * @param {ModuleGraph} moduleGraph module graph
  306. * @returns {ExportsSpec | undefined} export names
  307. */
  308. getExports(moduleGraph) {
  309. const module = /** @type {CssModule} */ (moduleGraph.getParentModule(this));
  310. const generator = /** @type {CssGenerator} */ (module.generator);
  311. const convention =
  312. /** @type {CssGeneratorExportsConvention} */
  313. (generator.options.exportsConvention);
  314. /** @type {{ name: string, canMangle: boolean }[]} */
  315. const exports = [];
  316. for (const entry of this.entries) {
  317. if (
  318. entry.exportMode === EXPORT_MODE.NONE ||
  319. entry.exportMode === EXPORT_MODE.SELF_REFERENCE
  320. ) {
  321. continue;
  322. }
  323. for (const name of entryConventionNames(entry, convention)) {
  324. exports.push({ name, canMangle: true });
  325. }
  326. }
  327. if (exports.length === 0) return undefined;
  328. return { exports, dependencies: undefined };
  329. }
  330. /**
  331. * Returns list of exports referenced by this dependency
  332. * @param {ModuleGraph} moduleGraph module graph
  333. * @param {RuntimeSpec} runtime the runtime for which the module is analysed
  334. * @returns {ReferencedExports} referenced exports
  335. */
  336. getReferencedExports(moduleGraph, runtime) {
  337. // `composes: foo;` (SELF_REFERENCE) references the composed class
  338. // (`entry.value`), not the composing class. Every other entry mode
  339. // keeps the whole exports object referenced (legacy behaviour), so the
  340. // presence of any such entry subsumes the self-reference names.
  341. let hasNonSelfReference = false;
  342. /** @type {ReferencedExports} */
  343. const referenced = [];
  344. let generator;
  345. for (const entry of this.entries) {
  346. if (
  347. entry.exportMode === EXPORT_MODE.SELF_REFERENCE &&
  348. typeof entry.value === "string"
  349. ) {
  350. const module =
  351. /** @type {CssModule} */
  352. (moduleGraph.getParentModule(this));
  353. generator = generator || /** @type {CssGenerator} */ (module.generator);
  354. const names = entryValueConventionNames(
  355. entry,
  356. /** @type {CssGeneratorExportsConvention} */
  357. (generator.options.exportsConvention)
  358. );
  359. for (const name of names) {
  360. referenced.push({ name: [name], canMangle: true });
  361. }
  362. } else {
  363. hasNonSelfReference = true;
  364. }
  365. }
  366. if (hasNonSelfReference) {
  367. return super.getReferencedExports(moduleGraph, runtime);
  368. }
  369. if (referenced.length === 0) {
  370. return super.getReferencedExports(moduleGraph, runtime);
  371. }
  372. return referenced;
  373. }
  374. /**
  375. * Returns warnings.
  376. * @param {ModuleGraph} moduleGraph module graph
  377. * @returns {WebpackError[] | null | undefined} warnings
  378. */
  379. getWarnings(moduleGraph) {
  380. /** @type {WebpackErrorType[] | null} */
  381. let warnings = null;
  382. let module;
  383. let exportsInfo;
  384. let convention;
  385. for (const entry of this.entries) {
  386. if (
  387. entry.exportMode !== EXPORT_MODE.SELF_REFERENCE ||
  388. typeof entry.value !== "string"
  389. ) {
  390. continue;
  391. }
  392. if (module === undefined) {
  393. module =
  394. /** @type {CssModule | undefined} */
  395. (moduleGraph.getParentModule(this));
  396. if (!module) return warnings;
  397. const generator = /** @type {CssGenerator} */ (module.generator);
  398. convention =
  399. /** @type {CssGeneratorExportsConvention} */
  400. (generator.options.exportsConvention);
  401. exportsInfo = moduleGraph.getExportsInfo(module);
  402. }
  403. const names = entryValueConventionNames(
  404. entry,
  405. /** @type {CssGeneratorExportsConvention} */ (convention)
  406. );
  407. const ei = /** @type {NonNullable<typeof exportsInfo>} */ (exportsInfo);
  408. const isProvided = names.some((name) => ei.isExportProvided(name));
  409. if (!isProvided) {
  410. const error = new WebpackError(
  411. `Self-referencing name "${entry.value}" not found`
  412. );
  413. error.module = module;
  414. // Per-entry loc so Compilation attributes the warning precisely
  415. // (it can't use the consolidated dependency's single loc). Built
  416. // on demand — entries carry flat numbers, not location objects.
  417. if (entry.locStartLine > 0) {
  418. error.loc = {
  419. start: { line: entry.locStartLine, column: entry.locStartColumn },
  420. end: { line: entry.locEndLine, column: entry.locEndColumn }
  421. };
  422. }
  423. if (warnings === null) warnings = [];
  424. warnings.push(error);
  425. }
  426. }
  427. return warnings;
  428. }
  429. /**
  430. * Updates the hash with the data contributed by this instance.
  431. * @param {Hash} hash hash to be updated
  432. * @param {UpdateHashContext} context context
  433. * @returns {void}
  434. */
  435. updateHash(hash, { chunkGraph }) {
  436. if (this._hashUpdate === undefined) {
  437. const module =
  438. /** @type {CssModule} */
  439. (chunkGraph.moduleGraph.getParentModule(this));
  440. const generator = /** @type {CssGenerator} */ (module.generator);
  441. const convention =
  442. /** @type {CssGeneratorExportsConvention} */
  443. (generator.options.exportsConvention);
  444. const localIdentName = JSON.stringify(generator.options.localIdentName);
  445. // Concatenate each entry's contribution with no separator so the byte
  446. // stream matches the legacy per-export `updateHash` sequence exactly
  447. // (keeps module hashes stable for import-free modules).
  448. let update = "";
  449. for (const entry of this.entries) {
  450. const names = entryConventionNames(entry, convention);
  451. update += `exportsConvention|${JSON.stringify(names)}|localIdentName|${localIdentName}|value|${JSON.stringify(entry.value)}|range|${JSON.stringify(entry.range)}|interpolate|${entry.interpolate}|exportMode|${entry.exportMode}|exportType|${entry.exportType}`;
  452. }
  453. this._hashUpdate = update;
  454. }
  455. hash.update(this._hashUpdate);
  456. }
  457. /**
  458. * Serializes this instance into the provided serializer context.
  459. * @param {ObjectSerializerContext} context context
  460. */
  461. serialize(context) {
  462. const { write } = context;
  463. write(this.entries.length);
  464. for (const entry of this.entries) {
  465. write(entry.name);
  466. write(entry.value);
  467. write(entry.range);
  468. write(entry.interpolate);
  469. write(entry.exportMode);
  470. write(entry.exportType);
  471. write(entry.locStartLine);
  472. write(entry.locStartColumn);
  473. write(entry.locEndLine);
  474. write(entry.locEndColumn);
  475. }
  476. super.serialize(context);
  477. }
  478. /**
  479. * Restores this instance from the provided deserializer context.
  480. * @param {ObjectDeserializerContext} context context
  481. */
  482. deserialize(context) {
  483. const { read } = context;
  484. const length = /** @type {number} */ (read());
  485. /** @type {CssExportEntry[]} */
  486. const entries = [];
  487. for (let i = 0; i < length; i++) {
  488. entries.push({
  489. name: /** @type {string} */ (read()),
  490. value: /** @type {Value} */ (read()),
  491. range: /** @type {Range=} */ (read()),
  492. interpolate: /** @type {boolean} */ (read()),
  493. exportMode: /** @type {ExportMode} */ (read()),
  494. exportType: /** @type {ExportType} */ (read()),
  495. locStartLine: /** @type {number} */ (read()),
  496. locStartColumn: /** @type {number} */ (read()),
  497. locEndLine: /** @type {number} */ (read()),
  498. locEndColumn: /** @type {number} */ (read())
  499. });
  500. }
  501. this.entries = entries;
  502. this._hashUpdate = undefined;
  503. this._entriesByName = undefined;
  504. super.deserialize(context);
  505. }
  506. }
  507. /**
  508. * @typedef {object} ResolutionCache
  509. * @property {Map<CssModule, Map<string, CssExportEntry[]>>} exports module -> (export name -> entries) across all its export deps
  510. * @property {Map<CssIcssImportDependency, CssModule | null>} targets import dep -> resolved target module
  511. */
  512. /**
  513. * Creates the transient per-`apply` cache shared by `resolve` and
  514. * `resolveReferences`. It is GC'd when the codegen call returns and is never
  515. * retained on the dependency.
  516. * @returns {ResolutionCache} empty resolution cache
  517. */
  518. const createResolutionCache = () => ({
  519. exports: new Map(),
  520. targets: new Map()
  521. });
  522. /**
  523. * Shared read-only index for modules that own no export dep.
  524. * @type {Map<string, CssExportEntry[]>}
  525. */
  526. const EMPTY_INDEX = new Map();
  527. /**
  528. * Per-module `local name -> import deps` index, keyed by module and validated by
  529. * `buildInfo` identity (a fresh object per build). `apply` runs once per source
  530. * type (CSS and JS), so this survives both passes — the index is built once per
  531. * build instead of once per `apply`. Weakly held, so it never keeps a module
  532. * alive.
  533. * @type {WeakMap<CssModule, { buildInfo: unknown, index: Map<string, CssIcssImportDependency[]> }>}
  534. */
  535. const moduleImportIndexes = new WeakMap();
  536. /**
  537. * Get (or lazily resolve) the target module of an `@value`/composes import dep.
  538. * The same import dep is followed by many entries within one codegen, so the
  539. * `moduleGraph.getModule` lookup is memoized per dep.
  540. * @param {ResolutionCache} cache resolution cache
  541. * @param {ModuleGraph} moduleGraph module graph
  542. * @param {CssIcssImportDependency} importDep import dependency
  543. * @returns {CssModule | null} resolved target module, or `null` if none
  544. */
  545. const getTargetModule = (cache, moduleGraph, importDep) => {
  546. let module = cache.targets.get(importDep);
  547. if (module === undefined) {
  548. module = /** @type {CssModule | null} */ (moduleGraph.getModule(importDep));
  549. cache.targets.set(importDep, module);
  550. }
  551. return module;
  552. };
  553. /**
  554. * Get (or lazily build) the `name -> entries` index for `module`. There is one
  555. * consolidated `CssIcssExportDependency` per module, and its own index is cached
  556. * on the dep — so a module composed/imported by many others builds its index
  557. * once per build rather than once per importer, and resolution does a single Map
  558. * lookup instead of re-scanning `module.dependencies` per reference.
  559. * @param {ResolutionCache} cache resolution cache
  560. * @param {CssModule} module css module
  561. * @returns {Map<string, CssExportEntry[]>} entries grouped by export name
  562. */
  563. const getExportIndex = (cache, module) => {
  564. let map = cache.exports.get(module);
  565. if (map === undefined) {
  566. map = EMPTY_INDEX;
  567. for (const dep of module.dependencies) {
  568. if (dep instanceof CssIcssExportDependency) {
  569. map = dep.getEntriesByName();
  570. break;
  571. }
  572. }
  573. cache.exports.set(module, map);
  574. }
  575. return map;
  576. };
  577. /**
  578. * Get (or lazily build) the `local name -> import deps` index for `module`, so
  579. * `findImportDep` is a Map lookup instead of a linear scan of every dependency.
  580. * Cached per build on the module (validated by `buildInfo` identity) so both the
  581. * CSS and JS codegen passes reuse one scan of `module.dependencies`.
  582. * @param {CssModule} module css module
  583. * @returns {Map<string, CssIcssImportDependency[]>} import deps grouped by local name
  584. */
  585. const getImportIndex = (module) => {
  586. const buildInfo = module.buildInfo;
  587. const cached = moduleImportIndexes.get(module);
  588. if (cached !== undefined && cached.buildInfo === buildInfo) {
  589. return cached.index;
  590. }
  591. /** @type {Map<string, CssIcssImportDependency[]>} */
  592. const index = new Map();
  593. for (const d of module.dependencies) {
  594. if (!(d instanceof CssIcssImportDependency)) continue;
  595. const list = index.get(d.localName);
  596. if (list) list.push(d);
  597. else index.set(d.localName, [d]);
  598. }
  599. moduleImportIndexes.set(module, { buildInfo, index });
  600. return index;
  601. };
  602. /**
  603. * Finds the active `CssIcssImportDependency` for a given local name. When a
  604. * `request` is provided the lookup also requires the import dependency's
  605. * `request` to match, so references between two `@value foo from "..."`
  606. * declarations resolve through the import in scope at the reference site.
  607. * @param {CssModule} module module owning the dependencies to search
  608. * @param {string} localName local name
  609. * @param {string=} request user request of the `@value` import to match
  610. * @returns {CssIcssImportDependency | undefined} matching import dep, if any
  611. */
  612. const findImportDep = (module, localName, request) => {
  613. const list = getImportIndex(module).get(localName);
  614. if (list === undefined) return undefined;
  615. // Import deps keep dependency order, so `list[0]` is the first match (the
  616. // legacy `firstMatch` fallback when no request-specific match exists).
  617. if (request === undefined) return list[0];
  618. for (const d of list) if (d.request === request) return d;
  619. return list[0];
  620. };
  621. CssIcssExportDependency.Template = class CssIcssExportDependencyTemplate extends (
  622. NullDependency.Template
  623. ) {
  624. /**
  625. * Resolves a `[localName, importName, request]` reference to its final
  626. * value by following `@value` imports into other modules' consolidated
  627. * export dependencies.
  628. * @param {string} localName local name
  629. * @param {string} importName import name
  630. * @param {DependencyTemplateContext} templateContext the context object
  631. * @param {string | undefined} request user request of the `@value` import
  632. * @param {Set<CssExportEntry>=} seen cycle guard
  633. * @param {ResolutionCache=} cache transient per-codegen index (shared with `resolveReferences` within one `apply`)
  634. * @returns {string | undefined} found reference
  635. */
  636. static resolve(
  637. localName,
  638. importName,
  639. templateContext,
  640. request,
  641. seen = new Set(),
  642. cache = createResolutionCache()
  643. ) {
  644. const { moduleGraph } = templateContext;
  645. const importDep = findImportDep(
  646. /** @type {CssModule} */ (templateContext.module),
  647. localName,
  648. request
  649. );
  650. if (!importDep) return undefined;
  651. const module = getTargetModule(cache, moduleGraph, importDep);
  652. if (!module) return undefined;
  653. const matches = getExportIndex(cache, module).get(importName);
  654. // Skip demoted (`NONE`) cross-kind entries so `@value name from` picks the same winner as the JS export map.
  655. let exportEntry;
  656. if (matches) {
  657. for (const m of matches) {
  658. if (m.exportMode !== EXPORT_MODE.NONE) {
  659. exportEntry = m;
  660. break;
  661. }
  662. }
  663. }
  664. if (!exportEntry) return undefined;
  665. if (seen.has(exportEntry)) return undefined;
  666. seen.add(exportEntry);
  667. const { value, interpolate } = exportEntry;
  668. if (Array.isArray(value)) {
  669. return CssIcssExportDependencyTemplate.resolve(
  670. value[0],
  671. value[1],
  672. { ...templateContext, module },
  673. value[2],
  674. seen,
  675. cache
  676. );
  677. }
  678. if (interpolate) {
  679. return CssIcssExportDependencyTemplate.getIdentifier(value, exportEntry, {
  680. ...templateContext,
  681. module
  682. });
  683. }
  684. return value;
  685. }
  686. /**
  687. * Resolves all references of a `composes` entry, deduplicated.
  688. * @param {CssExportEntry} entry composes entry
  689. * @param {DependencyTemplateContext} templateContext template context
  690. * @param {Set<CssExportEntry>} seen cycle guard
  691. * @param {ResolutionCache} cache transient per-codegen index, discarded after the module's `apply` (so it isn't retained on the dependency)
  692. * @returns {string[]} final names
  693. */
  694. static resolveReferences(entry, templateContext, seen, cache) {
  695. /** @type {string[]} */
  696. const references = [];
  697. if (seen.has(entry)) return references;
  698. seen.add(entry);
  699. if (Array.isArray(entry.value)) {
  700. const importDep = findImportDep(
  701. /** @type {CssModule} */ (templateContext.module),
  702. entry.value[0],
  703. entry.value[2]
  704. );
  705. if (!importDep) return references;
  706. const module = getTargetModule(
  707. cache,
  708. templateContext.moduleGraph,
  709. importDep
  710. );
  711. if (!module) return references;
  712. // Same overridden context and target name for every matched entry — build once, not per entry.
  713. const subContext = { ...templateContext, module };
  714. const name = /** @type {string[]} */ (entry.value)[1];
  715. const matches = getExportIndex(cache, module).get(name);
  716. if (matches) {
  717. for (const d of matches) {
  718. if (d.exportMode === EXPORT_MODE.NONE) continue;
  719. if (Array.isArray(d.value)) {
  720. references.push(
  721. ...CssIcssExportDependencyTemplate.resolveReferences(
  722. d,
  723. subContext,
  724. seen,
  725. cache
  726. )
  727. );
  728. } else {
  729. references.push(
  730. CssIcssExportDependencyTemplate.getIdentifier(
  731. d.value,
  732. d,
  733. subContext
  734. )
  735. );
  736. }
  737. }
  738. }
  739. } else {
  740. references.push(
  741. CssIcssExportDependencyTemplate.getIdentifier(
  742. /** @type {string} */ (entry.value),
  743. entry,
  744. templateContext
  745. )
  746. );
  747. const matches = getExportIndex(
  748. cache,
  749. /** @type {CssModule} */ (templateContext.module)
  750. ).get(/** @type {string} */ (entry.value));
  751. if (matches) {
  752. for (const d of matches) {
  753. if (d.exportMode === EXPORT_MODE.NONE) continue;
  754. if (d.exportType !== EXPORT_TYPE.COMPOSES) continue;
  755. if (Array.isArray(d.value)) {
  756. references.push(
  757. ...CssIcssExportDependencyTemplate.resolveReferences(
  758. d,
  759. templateContext,
  760. seen,
  761. cache
  762. )
  763. );
  764. } else {
  765. references.push(
  766. CssIcssExportDependencyTemplate.getIdentifier(
  767. d.value,
  768. d,
  769. templateContext
  770. )
  771. );
  772. }
  773. }
  774. }
  775. }
  776. return references.length > 1 ? [...new Set(references)] : references;
  777. }
  778. /**
  779. * Returns the interpolated identifier for an entry value.
  780. * @param {string} value value to identifier
  781. * @param {CssExportEntry} entry the entry
  782. * @param {DependencyTemplateContext} templateContext the context object
  783. * @returns {string} identifier
  784. */
  785. static getIdentifier(value, entry, templateContext) {
  786. if (!entry.interpolate) return value;
  787. const { moduleGraph, module, chunkGraph, runtimeTemplate } =
  788. templateContext;
  789. // Key order is by rising cardinality: `moduleGraph.cached` is a trie that
  790. // allocates a node per level per new prefix, so the per-export `value` goes
  791. // last — otherwise every export name forks the trie and re-allocates the
  792. // three elements below it, which are constant for the whole compilation.
  793. return moduleGraph.cached(
  794. computeInterpolatedIdentifier,
  795. /** @type {CssModule} */ (module),
  796. chunkGraph,
  797. runtimeTemplate,
  798. entry.exportType,
  799. value
  800. );
  801. }
  802. /**
  803. * Applies the plugin by registering its hooks on the compiler.
  804. * @param {Dependency} dependency the dependency for which the template should be applied
  805. * @param {ReplaceSource} source the current replace source which can be modified
  806. * @param {DependencyTemplateContext} templateContext the context object
  807. * @returns {void}
  808. */
  809. apply(dependency, source, templateContext) {
  810. const dep = /** @type {CssIcssExportDependency} */ (dependency);
  811. const { module: m, moduleGraph, runtime, cssData } = templateContext;
  812. const module = /** @type {CssModule} */ (m);
  813. const generator = /** @type {CssGenerator} */ (module.generator);
  814. const isJs = templateContext.type === JAVASCRIPT_TYPE;
  815. // All entries belong to this one module, so resolve its ExportsInfo once
  816. // instead of re-resolving `module -> exportsInfo` per export name.
  817. const exportsInfo = isJs ? moduleGraph.getExportsInfo(module) : undefined;
  818. // Transient per-module indexes for composes resolution within this
  819. // module's codegen; GC'd when apply returns (never retained on the dependency).
  820. const cache = createResolutionCache();
  821. for (const entry of dep.entries) {
  822. if (!entry.range && !isJs) continue;
  823. const isReference = Array.isArray(entry.value);
  824. /** @type {string} */
  825. let value;
  826. if (entry.exportType === EXPORT_TYPE.COMPOSES && isJs) {
  827. value = CssIcssExportDependencyTemplate.resolveReferences(
  828. entry,
  829. templateContext,
  830. new Set(),
  831. cache
  832. ).join(" ");
  833. } else if (isReference) {
  834. const resolved = CssIcssExportDependencyTemplate.resolve(
  835. /** @type {string[]} */ (entry.value)[0],
  836. /** @type {string[]} */ (entry.value)[1],
  837. templateContext,
  838. /** @type {string[]} */ (entry.value)[2],
  839. new Set(),
  840. cache
  841. );
  842. value = resolved || /** @type {string[]} */ (entry.value)[0];
  843. } else {
  844. value = CssIcssExportDependencyTemplate.getIdentifier(
  845. /** @type {string} */ (entry.value),
  846. entry,
  847. templateContext
  848. );
  849. }
  850. if (entry.exportType === EXPORT_TYPE.GRID_CUSTOM_IDENTIFIER) {
  851. value += `-${entry.name}`;
  852. }
  853. if (isJs && entry.exportMode !== EXPORT_MODE.NONE) {
  854. const names = entryConventionNames(
  855. entry,
  856. /** @type {CssGeneratorExportsConvention} */
  857. (generator.options.exportsConvention)
  858. );
  859. /** @type {Set<string>} */
  860. const allNames = new Set();
  861. for (const name of names) {
  862. if (
  863. /** @type {NonNullable<typeof exportsInfo>} */ (exportsInfo)
  864. .getExportInfo(name)
  865. .getUsedName(name, runtime)
  866. ) {
  867. allNames.add(name);
  868. }
  869. }
  870. const unescaped = getCssTokens().unescapeIdentifier(
  871. value,
  872. templateContext.runtimeTemplate.compilation.compiler.root
  873. );
  874. const depLocLine = entry.locStartLine;
  875. const depLocColumn = entry.locStartColumn;
  876. for (const used of allNames) {
  877. if (entry.exportMode === EXPORT_MODE.ONCE) {
  878. if (cssData.exports.has(used)) continue;
  879. cssData.exports.set(used, unescaped);
  880. if (
  881. depLocLine > 0 &&
  882. cssData.exportLocs &&
  883. !cssData.exportLocs.has(used)
  884. ) {
  885. cssData.exportLocs.set(used, {
  886. line: depLocLine,
  887. column: depLocColumn
  888. });
  889. }
  890. } else {
  891. const originalValue =
  892. entry.exportMode === EXPORT_MODE.REPLACE
  893. ? undefined
  894. : cssData.exports.get(used);
  895. cssData.exports.set(
  896. used,
  897. `${originalValue ? `${originalValue}${unescaped ? " " : ""}` : ""}${unescaped}`
  898. );
  899. if (
  900. depLocLine > 0 &&
  901. cssData.exportLocs &&
  902. !cssData.exportLocs.has(used)
  903. ) {
  904. cssData.exportLocs.set(used, {
  905. line: depLocLine,
  906. column: depLocColumn
  907. });
  908. }
  909. }
  910. }
  911. } else if (
  912. entry.range &&
  913. templateContext.type !== JAVASCRIPT_TYPE &&
  914. entry.exportMode !== EXPORT_MODE.APPEND &&
  915. entry.exportMode !== EXPORT_MODE.SELF_REFERENCE
  916. ) {
  917. source.replace(entry.range[0], entry.range[1] - 1, value);
  918. }
  919. }
  920. }
  921. };
  922. /** @type {Record<"NONE" | "REPLACE" | "APPEND" | "ONCE" | "SELF_REFERENCE", ExportMode>} */
  923. CssIcssExportDependency.EXPORT_MODE = EXPORT_MODE;
  924. /** @type {Record<"NORMAL" | "CUSTOM_VARIABLE" | "GRID_CUSTOM_IDENTIFIER" | "COMPOSES", ExportType>} */
  925. CssIcssExportDependency.EXPORT_TYPE = EXPORT_TYPE;
  926. makeSerializable(
  927. CssIcssExportDependency,
  928. "webpack/lib/dependencies/CssIcssExportDependency"
  929. );
  930. module.exports = CssIcssExportDependency;