CssIcssExportDependency.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  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 { interpolate } = require("../TemplatedPathPlugin");
  8. const WebpackError = require("../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 nonNumericOnlyHash = require("../util/nonNumericOnlyHash");
  15. const CssIcssImportDependency = require("./CssIcssImportDependency");
  16. const NullDependency = require("./NullDependency");
  17. const getCssParser = memoize(() => require("../css/CssParser"));
  18. /** @typedef {import("webpack-sources").ReplaceSource} ReplaceSource */
  19. /** @typedef {import("../../declarations/WebpackOptions").HashFunction} HashFunction */
  20. /** @typedef {import("../../declarations/WebpackOptions").CssGeneratorExportsConvention} CssGeneratorExportsConvention */
  21. /** @typedef {import("../../declarations/WebpackOptions").CssGeneratorLocalIdentName} CssGeneratorLocalIdentName */
  22. /** @typedef {import("../CssModule")} CssModule */
  23. /** @typedef {import("../Dependency")} Dependency */
  24. /** @typedef {import("../Dependency").ReferencedExports} ReferencedExports */
  25. /** @typedef {import("../Dependency").ExportsSpec} ExportsSpec */
  26. /** @typedef {import("../Dependency").UpdateHashContext} UpdateHashContext */
  27. /** @typedef {import("../DependencyTemplate").CssDependencyTemplateContext} DependencyTemplateContext */
  28. /** @typedef {import("../ModuleGraph")} ModuleGraph */
  29. /** @typedef {import("../css/CssGenerator")} CssGenerator */
  30. /** @typedef {import("../serialization/ObjectMiddleware").ObjectDeserializerContext} ObjectDeserializerContext */
  31. /** @typedef {import("../serialization/ObjectMiddleware").ObjectSerializerContext} ObjectSerializerContext */
  32. /** @typedef {import("../util/Hash")} Hash */
  33. /** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */
  34. /** @typedef {import("../ChunkGraph")} ChunkGraph */
  35. /** @typedef {import("../RuntimeTemplate")} RuntimeTemplate */
  36. /** @typedef {import("../css/CssParser").Range} Range */
  37. /** @typedef {(name: string) => string} ExportsConventionFn */
  38. /**
  39. * Returns local ident.
  40. * @param {string} local css local
  41. * @param {CssModule} module module
  42. * @param {ChunkGraph} chunkGraph chunk graph
  43. * @param {RuntimeTemplate} runtimeTemplate runtime template
  44. * @returns {string} local ident
  45. */
  46. const getLocalIdent = (local, module, chunkGraph, runtimeTemplate) => {
  47. const generator = /** @type {CssGenerator} */ (module.generator);
  48. const localIdentName =
  49. /** @type {CssGeneratorLocalIdentName} */
  50. (generator.options.localIdentName);
  51. const relativeResourcePath = makePathsRelative(
  52. /** @type {string} */
  53. (runtimeTemplate.compilation.compiler.context),
  54. /** @type {string} */
  55. (module.getResource()),
  56. runtimeTemplate.compilation.compiler.root
  57. );
  58. const { uniqueName } = runtimeTemplate.outputOptions;
  59. let localIdentHash = "";
  60. if (
  61. typeof localIdentName === "function" ||
  62. /\[(?:fullhash|hash)\]/.test(localIdentName)
  63. ) {
  64. const hashSalt = generator.options.localIdentHashSalt;
  65. const hashDigest =
  66. /** @type {string} */
  67. (generator.options.localIdentHashDigest);
  68. const hashDigestLength = generator.options.localIdentHashDigestLength;
  69. const hashFunction =
  70. /** @type {HashFunction} */
  71. (generator.options.localIdentHashFunction);
  72. const hash = createHash(hashFunction);
  73. if (hashSalt) {
  74. hash.update(hashSalt);
  75. }
  76. if (uniqueName) {
  77. hash.update(uniqueName);
  78. }
  79. hash.update(relativeResourcePath);
  80. hash.update(local);
  81. localIdentHash = hash.digest(hashDigest).slice(0, hashDigestLength);
  82. }
  83. let contentHash = "";
  84. if (
  85. typeof localIdentName === "function" ||
  86. /\[contenthash\]/.test(localIdentName)
  87. ) {
  88. const hash = createHash(runtimeTemplate.outputOptions.hashFunction);
  89. const source = module.originalSource();
  90. if (source) {
  91. hash.update(source.buffer());
  92. }
  93. if (module.error) {
  94. hash.update(module.error.toString());
  95. }
  96. const fullContentHash = hash.digest(
  97. runtimeTemplate.outputOptions.hashDigest
  98. );
  99. contentHash = nonNumericOnlyHash(
  100. fullContentHash,
  101. runtimeTemplate.outputOptions.hashDigestLength
  102. );
  103. }
  104. let localIdent = interpolate(localIdentName, {
  105. prepareId: (id) => {
  106. if (typeof id !== "string") return id;
  107. return (
  108. id
  109. .replace(/^([.-]|[^a-z0-9_-])+/i, "")
  110. // 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.
  111. // 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.
  112. .replace(/[^a-z0-9@_-]+/gi, "_")
  113. );
  114. },
  115. filename: relativeResourcePath,
  116. hash: localIdentHash,
  117. local,
  118. contentHash,
  119. chunkGraph,
  120. module
  121. });
  122. // TODO move these things into interpolate
  123. if (/\[local\]/.test(localIdent)) {
  124. localIdent = localIdent.replace(/\[local\]/g, local);
  125. }
  126. if (/\[uniqueName\]/.test(localIdent)) {
  127. localIdent = localIdent.replace(
  128. /\[uniqueName\]/g,
  129. /** @type {string} */ (uniqueName)
  130. );
  131. }
  132. // Protect the first character from unsupported values
  133. return localIdent.replace(/^((-?\d)|--)/, "_$1");
  134. };
  135. /** @typedef {string | [string, string]} Value */
  136. // 0 - replace, 1 - replace, 2 - append, 2 - once
  137. /** @typedef {0 | 1 | 2 | 3 | 4} ExportMode */
  138. // 0 - normal, 1 - custom css variable, 2 - grid custom ident, 3 - composes
  139. /** @typedef {0 | 1 | 2 | 3} ExportType */
  140. class CssIcssExportDependency extends NullDependency {
  141. /**
  142. * Example of dependency:
  143. *
  144. * :export { LOCAL_NAME: EXPORT_NAME }
  145. * @param {string} name export name
  146. * @param {Value} value value or local name and import name
  147. * @param {Range=} range range
  148. * @param {boolean=} interpolate true when value need to be interpolated, otherwise false
  149. * @param {ExportMode=} exportMode export mode
  150. * @param {ExportType=} exportType export type
  151. */
  152. constructor(
  153. name,
  154. value,
  155. range,
  156. interpolate = false,
  157. exportMode = CssIcssExportDependency.EXPORT_MODE.REPLACE,
  158. exportType = CssIcssExportDependency.EXPORT_TYPE.NORMAL
  159. ) {
  160. super();
  161. this.name = name;
  162. this.value = value;
  163. this.range = range;
  164. this.interpolate = interpolate;
  165. this.exportMode = exportMode;
  166. this.exportType = exportType;
  167. /** @type {undefined | string} */
  168. this._hashUpdate = undefined;
  169. }
  170. get type() {
  171. return "css :export";
  172. }
  173. /**
  174. * Gets exports convention names.
  175. * @param {string} name export name
  176. * @param {CssGeneratorExportsConvention} convention convention of the export name
  177. * @returns {string[]} convention results
  178. */
  179. getExportsConventionNames(name, convention) {
  180. if (this._conventionNames) {
  181. return this._conventionNames;
  182. }
  183. this._conventionNames = cssExportConvention(name, convention);
  184. return this._conventionNames;
  185. }
  186. /**
  187. * Returns list of exports referenced by this dependency
  188. * @param {ModuleGraph} moduleGraph module graph
  189. * @param {RuntimeSpec} runtime the runtime for which the module is analysed
  190. * @returns {ReferencedExports} referenced exports
  191. */
  192. getReferencedExports(moduleGraph, runtime) {
  193. if (
  194. this.exportMode === CssIcssExportDependency.EXPORT_MODE.SELF_REFERENCE
  195. ) {
  196. return [
  197. {
  198. name: [this.name],
  199. canMangle: true
  200. }
  201. ];
  202. }
  203. return super.getReferencedExports(moduleGraph, runtime);
  204. }
  205. /**
  206. * Returns the exported names
  207. * @param {ModuleGraph} moduleGraph module graph
  208. * @returns {ExportsSpec | undefined} export names
  209. */
  210. getExports(moduleGraph) {
  211. if (
  212. this.exportMode === CssIcssExportDependency.EXPORT_MODE.NONE ||
  213. this.exportMode === CssIcssExportDependency.EXPORT_MODE.SELF_REFERENCE
  214. ) {
  215. return;
  216. }
  217. const module = /** @type {CssModule} */ (moduleGraph.getParentModule(this));
  218. const generator = /** @type {CssGenerator} */ (module.generator);
  219. const names = this.getExportsConventionNames(
  220. this.name,
  221. /** @type {CssGeneratorExportsConvention} */
  222. (generator.options.exportsConvention)
  223. );
  224. return {
  225. exports: [...names].map((name) => ({
  226. name,
  227. canMangle: true
  228. })),
  229. dependencies: undefined
  230. };
  231. }
  232. /**
  233. * Returns warnings.
  234. * @param {ModuleGraph} moduleGraph module graph
  235. * @returns {WebpackError[] | null | undefined} warnings
  236. */
  237. getWarnings(moduleGraph) {
  238. if (
  239. this.exportMode === CssIcssExportDependency.EXPORT_MODE.SELF_REFERENCE &&
  240. !Array.isArray(this.value)
  241. ) {
  242. const module = moduleGraph.getParentModule(this);
  243. if (
  244. module &&
  245. !moduleGraph.getExportsInfo(module).isExportProvided(this.value)
  246. ) {
  247. const error = new WebpackError(
  248. `Self-referencing name "${this.value}" not found`
  249. );
  250. error.module = module;
  251. return [error];
  252. }
  253. }
  254. return null;
  255. }
  256. /**
  257. * Updates the hash with the data contributed by this instance.
  258. * @param {Hash} hash hash to be updated
  259. * @param {UpdateHashContext} context context
  260. * @returns {void}
  261. */
  262. updateHash(hash, { chunkGraph }) {
  263. if (this._hashUpdate === undefined) {
  264. const module =
  265. /** @type {CssModule} */
  266. (chunkGraph.moduleGraph.getParentModule(this));
  267. const generator = /** @type {CssGenerator} */ (module.generator);
  268. const names = this.getExportsConventionNames(
  269. this.name,
  270. /** @type {CssGeneratorExportsConvention} */
  271. (generator.options.exportsConvention)
  272. );
  273. this._hashUpdate = `exportsConvention|${JSON.stringify(names)}|localIdentName|${JSON.stringify(generator.options.localIdentName)}`;
  274. }
  275. hash.update(this._hashUpdate);
  276. }
  277. /**
  278. * Serializes this instance into the provided serializer context.
  279. * @param {ObjectSerializerContext} context context
  280. */
  281. serialize(context) {
  282. const { write } = context;
  283. write(this.name);
  284. write(this.value);
  285. write(this.range);
  286. write(this.interpolate);
  287. write(this.exportMode);
  288. write(this.exportType);
  289. super.serialize(context);
  290. }
  291. /**
  292. * Restores this instance from the provided deserializer context.
  293. * @param {ObjectDeserializerContext} context context
  294. */
  295. deserialize(context) {
  296. const { read } = context;
  297. this.name = read();
  298. this.value = read();
  299. this.range = read();
  300. this.interpolate = read();
  301. this.exportMode = read();
  302. this.exportType = read();
  303. super.deserialize(context);
  304. }
  305. }
  306. CssIcssExportDependency.Template = class CssIcssExportDependencyTemplate extends (
  307. NullDependency.Template
  308. ) {
  309. // TODO looking how to cache
  310. /**
  311. * Returns found reference.
  312. * @param {string} localName local name
  313. * @param {string} importName import name
  314. * @param {DependencyTemplateContext} templateContext the context object
  315. * @param {Set<CssIcssExportDependency>} seen seen to prevent cyclical problems
  316. * @returns {string | undefined} found reference
  317. */
  318. static resolve(localName, importName, templateContext, seen = new Set()) {
  319. const { moduleGraph } = templateContext;
  320. const importDep =
  321. /** @type {CssIcssImportDependency | undefined} */
  322. (
  323. templateContext.module.dependencies.find(
  324. (d) =>
  325. d instanceof CssIcssImportDependency && d.localName === localName
  326. )
  327. );
  328. if (!importDep) return undefined;
  329. const module = /** @type {CssModule} */ (moduleGraph.getModule(importDep));
  330. if (!module) return undefined;
  331. const exportDep =
  332. /** @type {CssIcssExportDependency} */
  333. (
  334. module.dependencies.find(
  335. (d) => d instanceof CssIcssExportDependency && d.name === importName
  336. )
  337. );
  338. if (!exportDep) return undefined;
  339. if (seen.has(exportDep)) return undefined;
  340. seen.add(exportDep);
  341. const { value, interpolate } = exportDep;
  342. if (Array.isArray(value)) {
  343. return this.resolve(
  344. value[0],
  345. value[1],
  346. {
  347. ...templateContext,
  348. module
  349. },
  350. seen
  351. );
  352. }
  353. if (interpolate) {
  354. return CssIcssExportDependency.Template.getIdentifier(value, exportDep, {
  355. ...templateContext,
  356. module
  357. });
  358. }
  359. return value;
  360. }
  361. /**
  362. * Resolves references.
  363. * @param {CssIcssExportDependency} dep value
  364. * @param {DependencyTemplateContext} templateContext template context
  365. * @param {Set<CssIcssExportDependency>} seen to prevent cyclical problems
  366. * @returns {string[]} final names
  367. */
  368. static resolveReferences(dep, templateContext, seen = new Set()) {
  369. /** @type {string[]} */
  370. const references = [];
  371. if (seen.has(dep)) return references;
  372. seen.add(dep);
  373. if (Array.isArray(dep.value)) {
  374. const importDep =
  375. /** @type {CssIcssImportDependency | undefined} */
  376. (
  377. templateContext.module.dependencies.find(
  378. (d) =>
  379. d instanceof CssIcssImportDependency &&
  380. d.localName === dep.value[0]
  381. )
  382. );
  383. if (!importDep) return references;
  384. const module =
  385. /** @type {CssModule} */
  386. (templateContext.moduleGraph.getModule(importDep));
  387. if (!module) return references;
  388. for (const d of module.dependencies) {
  389. if (d instanceof CssIcssExportDependency && d.name === dep.value[1]) {
  390. if (Array.isArray(d.value)) {
  391. const deepReferences =
  392. CssIcssExportDependencyTemplate.resolveReferences(
  393. d,
  394. {
  395. ...templateContext,
  396. module
  397. },
  398. seen
  399. );
  400. references.push(...deepReferences);
  401. } else {
  402. references.push(
  403. CssIcssExportDependencyTemplate.getIdentifier(d.value, d, {
  404. ...templateContext,
  405. module
  406. })
  407. );
  408. }
  409. }
  410. }
  411. } else {
  412. // Adding basic class
  413. references.push(
  414. CssIcssExportDependencyTemplate.getIdentifier(
  415. dep.value,
  416. dep,
  417. templateContext
  418. )
  419. );
  420. for (const d of templateContext.module.dependencies) {
  421. if (
  422. d instanceof CssIcssExportDependency &&
  423. d.exportType === CssIcssExportDependency.EXPORT_TYPE.COMPOSES &&
  424. d.name === dep.value
  425. ) {
  426. if (Array.isArray(d.value)) {
  427. const deepReferences =
  428. CssIcssExportDependencyTemplate.resolveReferences(
  429. d,
  430. templateContext,
  431. seen
  432. );
  433. references.push(...deepReferences);
  434. } else {
  435. references.push(
  436. CssIcssExportDependencyTemplate.getIdentifier(
  437. d.value,
  438. d,
  439. templateContext
  440. )
  441. );
  442. }
  443. }
  444. }
  445. }
  446. return [...new Set(references)];
  447. }
  448. /**
  449. * Returns identifier.
  450. * @param {string} value value to identifier
  451. * @param {Dependency} dependency the dependency for which the template should be applied
  452. * @param {DependencyTemplateContext} templateContext the context object
  453. * @returns {string} identifier
  454. */
  455. static getIdentifier(value, dependency, templateContext) {
  456. const dep = /** @type {CssIcssExportDependency} */ (dependency);
  457. if (dep.interpolate) {
  458. const { module: m } = templateContext;
  459. const module = /** @type {CssModule} */ (m);
  460. const generator = /** @type {CssGenerator} */ (module.generator);
  461. const local = cssExportConvention(
  462. value,
  463. /** @type {CssGeneratorExportsConvention} */
  464. (generator.options.exportsConvention)
  465. )[0];
  466. const prefix =
  467. dep.exportType === CssIcssExportDependency.EXPORT_TYPE.CUSTOM_VARIABLE
  468. ? "--"
  469. : "";
  470. return (
  471. prefix +
  472. getCssParser().escapeIdentifier(
  473. getLocalIdent(
  474. local,
  475. /** @type {CssModule} */
  476. (m),
  477. templateContext.chunkGraph,
  478. templateContext.runtimeTemplate
  479. )
  480. )
  481. );
  482. }
  483. return value;
  484. }
  485. /**
  486. * Applies the plugin by registering its hooks on the compiler.
  487. * @param {Dependency} dependency the dependency for which the template should be applied
  488. * @param {ReplaceSource} source the current replace source which can be modified
  489. * @param {DependencyTemplateContext} templateContext the context object
  490. * @returns {void}
  491. */
  492. apply(dependency, source, templateContext) {
  493. const dep = /** @type {CssIcssExportDependency} */ (dependency);
  494. if (!dep.range && templateContext.type !== JAVASCRIPT_TYPE) return;
  495. const { module: m, moduleGraph, runtime, cssData } = templateContext;
  496. const module = /** @type {CssModule} */ (m);
  497. const generator = /** @type {CssGenerator} */ (module.generator);
  498. const isReference = Array.isArray(dep.value);
  499. /** @type {string} */
  500. let value;
  501. // The `composes` has more complex logic for collecting all the classes
  502. if (
  503. dep.exportType === CssIcssExportDependency.EXPORT_TYPE.COMPOSES &&
  504. templateContext.type === JAVASCRIPT_TYPE
  505. ) {
  506. value = CssIcssExportDependencyTemplate.resolveReferences(
  507. dep,
  508. templateContext
  509. ).join(" ");
  510. } else if (isReference) {
  511. const resolved = CssIcssExportDependencyTemplate.resolve(
  512. dep.value[0],
  513. dep.value[1],
  514. templateContext
  515. );
  516. // Fallback to the local name if not resolved
  517. value = resolved || dep.value[0];
  518. } else {
  519. value = CssIcssExportDependencyTemplate.getIdentifier(
  520. /** @type {string} */ (dep.value),
  521. dep,
  522. templateContext
  523. );
  524. }
  525. if (
  526. dep.exportType ===
  527. CssIcssExportDependency.EXPORT_TYPE.GRID_CUSTOM_IDENTIFIER
  528. ) {
  529. value += `-${dep.name}`;
  530. }
  531. if (
  532. templateContext.type === JAVASCRIPT_TYPE &&
  533. dep.exportMode !== CssIcssExportDependency.EXPORT_MODE.NONE
  534. ) {
  535. const names = dep.getExportsConventionNames(
  536. dep.name,
  537. /** @type {CssGeneratorExportsConvention} */
  538. (generator.options.exportsConvention)
  539. );
  540. const usedNames =
  541. /** @type {string[]} */
  542. (
  543. names
  544. .map((name) =>
  545. moduleGraph.getExportInfo(module, name).getUsedName(name, runtime)
  546. )
  547. .filter(Boolean)
  548. );
  549. const allNames = new Set([...usedNames, ...names]);
  550. const unescaped = getCssParser().unescapeIdentifier(value);
  551. for (const used of allNames) {
  552. if (dep.exportMode === CssIcssExportDependency.EXPORT_MODE.ONCE) {
  553. if (cssData.exports.has(used)) return;
  554. cssData.exports.set(used, unescaped);
  555. } else {
  556. const originalValue =
  557. dep.exportMode === CssIcssExportDependency.EXPORT_MODE.REPLACE
  558. ? undefined
  559. : cssData.exports.get(used);
  560. cssData.exports.set(
  561. used,
  562. `${originalValue ? `${originalValue}${unescaped ? " " : ""}` : ""}${unescaped}`
  563. );
  564. }
  565. }
  566. } else if (
  567. dep.range &&
  568. templateContext.type === CSS_TYPE &&
  569. dep.exportMode !== CssIcssExportDependency.EXPORT_MODE.APPEND &&
  570. dep.exportMode !== CssIcssExportDependency.EXPORT_MODE.SELF_REFERENCE
  571. ) {
  572. source.replace(dep.range[0], dep.range[1] - 1, value);
  573. }
  574. }
  575. };
  576. /** @type {Record<"NONE" | "REPLACE" | "APPEND" | "ONCE" | "SELF_REFERENCE", ExportMode>} */
  577. CssIcssExportDependency.EXPORT_MODE = {
  578. NONE: 0,
  579. REPLACE: 1,
  580. APPEND: 2,
  581. ONCE: 3,
  582. SELF_REFERENCE: 4
  583. };
  584. /** @type {Record<"NORMAL" | "CUSTOM_VARIABLE" | "GRID_CUSTOM_IDENTIFIER" | "COMPOSES", ExportType>} */
  585. CssIcssExportDependency.EXPORT_TYPE = {
  586. NORMAL: 0,
  587. CUSTOM_VARIABLE: 1,
  588. GRID_CUSTOM_IDENTIFIER: 2,
  589. COMPOSES: 3
  590. };
  591. makeSerializable(
  592. CssIcssExportDependency,
  593. "webpack/lib/dependencies/CssIcssExportDependency"
  594. );
  595. module.exports = CssIcssExportDependency;