CssModulesPlugin.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { ConcatSource, PrefixSource } = require("webpack-sources");
  7. const CssModule = require("../CssModule");
  8. const HotUpdateChunk = require("../HotUpdateChunk");
  9. const {
  10. CSS_MODULE_TYPE,
  11. CSS_MODULE_TYPE_GLOBAL,
  12. CSS_MODULE_TYPE_MODULE,
  13. CSS_MODULE_TYPE_AUTO
  14. } = require("../ModuleTypeConstants");
  15. const RuntimeGlobals = require("../RuntimeGlobals");
  16. const SelfModuleFactory = require("../SelfModuleFactory");
  17. const WebpackError = require("../WebpackError");
  18. const CssExportDependency = require("../dependencies/CssExportDependency");
  19. const CssImportDependency = require("../dependencies/CssImportDependency");
  20. const CssLocalIdentifierDependency = require("../dependencies/CssLocalIdentifierDependency");
  21. const CssSelfLocalIdentifierDependency = require("../dependencies/CssSelfLocalIdentifierDependency");
  22. const CssUrlDependency = require("../dependencies/CssUrlDependency");
  23. const StaticExportsDependency = require("../dependencies/StaticExportsDependency");
  24. const { compareModulesByIdentifier } = require("../util/comparators");
  25. const createSchemaValidation = require("../util/create-schema-validation");
  26. const createHash = require("../util/createHash");
  27. const memoize = require("../util/memoize");
  28. const nonNumericOnlyHash = require("../util/nonNumericOnlyHash");
  29. const CssExportsGenerator = require("./CssExportsGenerator");
  30. const CssGenerator = require("./CssGenerator");
  31. const CssParser = require("./CssParser");
  32. /** @typedef {import("webpack-sources").Source} Source */
  33. /** @typedef {import("../../declarations/WebpackOptions").CssExperimentOptions} CssExperimentOptions */
  34. /** @typedef {import("../../declarations/WebpackOptions").Output} OutputOptions */
  35. /** @typedef {import("../Chunk")} Chunk */
  36. /** @typedef {import("../ChunkGraph")} ChunkGraph */
  37. /** @typedef {import("../CodeGenerationResults")} CodeGenerationResults */
  38. /** @typedef {import("../Compilation")} Compilation */
  39. /** @typedef {import("../Compiler")} Compiler */
  40. /** @typedef {import("../Module")} Module */
  41. /** @typedef {import("../util/memoize")} Memoize */
  42. const getCssLoadingRuntimeModule = memoize(() =>
  43. require("./CssLoadingRuntimeModule")
  44. );
  45. /**
  46. * @param {string} name name
  47. * @returns {{oneOf: [{$ref: string}], definitions: *}} schema
  48. */
  49. const getSchema = name => {
  50. const { definitions } = require("../../schemas/WebpackOptions.json");
  51. return {
  52. definitions,
  53. oneOf: [{ $ref: `#/definitions/${name}` }]
  54. };
  55. };
  56. const validateGeneratorOptions = createSchemaValidation(
  57. require("../../schemas/plugins/css/CssGeneratorOptions.check.js"),
  58. () => getSchema("CssGeneratorOptions"),
  59. {
  60. name: "Css Modules Plugin",
  61. baseDataPath: "parser"
  62. }
  63. );
  64. const validateParserOptions = createSchemaValidation(
  65. require("../../schemas/plugins/css/CssParserOptions.check.js"),
  66. () => getSchema("CssParserOptions"),
  67. {
  68. name: "Css Modules Plugin",
  69. baseDataPath: "parser"
  70. }
  71. );
  72. /**
  73. * @param {string} str string
  74. * @param {boolean=} omitOptionalUnderscore if true, optional underscore is not added
  75. * @returns {string} escaped string
  76. */
  77. const escapeCss = (str, omitOptionalUnderscore) => {
  78. const escaped = `${str}`.replace(
  79. // cspell:word uffff
  80. /[^a-zA-Z0-9_\u0081-\uffff-]/g,
  81. s => `\\${s}`
  82. );
  83. return !omitOptionalUnderscore && /^(?!--)[0-9_-]/.test(escaped)
  84. ? `_${escaped}`
  85. : escaped;
  86. };
  87. const plugin = "CssModulesPlugin";
  88. class CssModulesPlugin {
  89. /**
  90. * @param {CssExperimentOptions} options options
  91. */
  92. constructor({ exportsOnly = false }) {
  93. this._exportsOnly = exportsOnly;
  94. }
  95. /**
  96. * Apply the plugin
  97. * @param {Compiler} compiler the compiler instance
  98. * @returns {void}
  99. */
  100. apply(compiler) {
  101. compiler.hooks.compilation.tap(
  102. plugin,
  103. (compilation, { normalModuleFactory }) => {
  104. const selfFactory = new SelfModuleFactory(compilation.moduleGraph);
  105. compilation.dependencyFactories.set(
  106. CssUrlDependency,
  107. normalModuleFactory
  108. );
  109. compilation.dependencyTemplates.set(
  110. CssUrlDependency,
  111. new CssUrlDependency.Template()
  112. );
  113. compilation.dependencyTemplates.set(
  114. CssLocalIdentifierDependency,
  115. new CssLocalIdentifierDependency.Template()
  116. );
  117. compilation.dependencyFactories.set(
  118. CssSelfLocalIdentifierDependency,
  119. selfFactory
  120. );
  121. compilation.dependencyTemplates.set(
  122. CssSelfLocalIdentifierDependency,
  123. new CssSelfLocalIdentifierDependency.Template()
  124. );
  125. compilation.dependencyTemplates.set(
  126. CssExportDependency,
  127. new CssExportDependency.Template()
  128. );
  129. compilation.dependencyFactories.set(
  130. CssImportDependency,
  131. normalModuleFactory
  132. );
  133. compilation.dependencyTemplates.set(
  134. CssImportDependency,
  135. new CssImportDependency.Template()
  136. );
  137. compilation.dependencyTemplates.set(
  138. StaticExportsDependency,
  139. new StaticExportsDependency.Template()
  140. );
  141. for (const type of [
  142. CSS_MODULE_TYPE,
  143. CSS_MODULE_TYPE_GLOBAL,
  144. CSS_MODULE_TYPE_MODULE,
  145. CSS_MODULE_TYPE_AUTO
  146. ]) {
  147. normalModuleFactory.hooks.createParser
  148. .for(type)
  149. .tap(plugin, parserOptions => {
  150. validateParserOptions(parserOptions);
  151. switch (type) {
  152. case CSS_MODULE_TYPE:
  153. case CSS_MODULE_TYPE_AUTO:
  154. return new CssParser();
  155. case CSS_MODULE_TYPE_GLOBAL:
  156. return new CssParser({
  157. allowModeSwitch: false
  158. });
  159. case CSS_MODULE_TYPE_MODULE:
  160. return new CssParser({
  161. defaultMode: "local"
  162. });
  163. }
  164. });
  165. normalModuleFactory.hooks.createGenerator
  166. .for(type)
  167. .tap(plugin, generatorOptions => {
  168. validateGeneratorOptions(generatorOptions);
  169. return this._exportsOnly
  170. ? new CssExportsGenerator()
  171. : new CssGenerator();
  172. });
  173. normalModuleFactory.hooks.createModuleClass
  174. .for(type)
  175. .tap(plugin, (createData, resolveData) => {
  176. if (resolveData.dependencies.length > 0) {
  177. // When CSS is imported from CSS there is only one dependency
  178. const dependency = resolveData.dependencies[0];
  179. if (dependency instanceof CssImportDependency) {
  180. const parent =
  181. /** @type {CssModule} */
  182. (compilation.moduleGraph.getParentModule(dependency));
  183. if (parent instanceof CssModule) {
  184. /** @type {import("../CssModule").Inheritance | undefined} */
  185. let inheritance;
  186. if (
  187. (parent.cssLayer !== null &&
  188. parent.cssLayer !== undefined) ||
  189. parent.supports ||
  190. parent.media
  191. ) {
  192. if (!inheritance) {
  193. inheritance = [];
  194. }
  195. inheritance.push([
  196. parent.cssLayer,
  197. parent.supports,
  198. parent.media
  199. ]);
  200. }
  201. if (parent.inheritance) {
  202. if (!inheritance) {
  203. inheritance = [];
  204. }
  205. inheritance.push(...parent.inheritance);
  206. }
  207. return new CssModule({
  208. ...createData,
  209. cssLayer: dependency.layer,
  210. supports: dependency.supports,
  211. media: dependency.media,
  212. inheritance
  213. });
  214. }
  215. return new CssModule({
  216. ...createData,
  217. cssLayer: dependency.layer,
  218. supports: dependency.supports,
  219. media: dependency.media
  220. });
  221. }
  222. }
  223. return new CssModule(createData);
  224. });
  225. }
  226. const orderedCssModulesPerChunk = new WeakMap();
  227. compilation.hooks.afterCodeGeneration.tap("CssModulesPlugin", () => {
  228. const { chunkGraph } = compilation;
  229. for (const chunk of compilation.chunks) {
  230. if (CssModulesPlugin.chunkHasCss(chunk, chunkGraph)) {
  231. orderedCssModulesPerChunk.set(
  232. chunk,
  233. this.getOrderedChunkCssModules(chunk, chunkGraph, compilation)
  234. );
  235. }
  236. }
  237. });
  238. compilation.hooks.contentHash.tap("CssModulesPlugin", chunk => {
  239. const {
  240. chunkGraph,
  241. outputOptions: {
  242. hashSalt,
  243. hashDigest,
  244. hashDigestLength,
  245. hashFunction
  246. }
  247. } = compilation;
  248. const modules = orderedCssModulesPerChunk.get(chunk);
  249. if (modules === undefined) return;
  250. const hash = createHash(hashFunction);
  251. if (hashSalt) hash.update(hashSalt);
  252. for (const module of modules) {
  253. hash.update(chunkGraph.getModuleHash(module, chunk.runtime));
  254. }
  255. const digest = /** @type {string} */ (hash.digest(hashDigest));
  256. chunk.contentHash.css = nonNumericOnlyHash(digest, hashDigestLength);
  257. });
  258. compilation.hooks.renderManifest.tap(plugin, (result, options) => {
  259. const { chunkGraph } = compilation;
  260. const { hash, chunk, codeGenerationResults } = options;
  261. if (chunk instanceof HotUpdateChunk) return result;
  262. /** @type {CssModule[] | undefined} */
  263. const modules = orderedCssModulesPerChunk.get(chunk);
  264. if (modules !== undefined) {
  265. result.push({
  266. render: () =>
  267. this.renderChunk({
  268. chunk,
  269. chunkGraph,
  270. codeGenerationResults,
  271. uniqueName: compilation.outputOptions.uniqueName,
  272. modules
  273. }),
  274. filenameTemplate: CssModulesPlugin.getChunkFilenameTemplate(
  275. chunk,
  276. compilation.outputOptions
  277. ),
  278. pathOptions: {
  279. hash,
  280. runtime: chunk.runtime,
  281. chunk,
  282. contentHashType: "css"
  283. },
  284. identifier: `css${chunk.id}`,
  285. hash: chunk.contentHash.css
  286. });
  287. }
  288. return result;
  289. });
  290. const globalChunkLoading = compilation.outputOptions.chunkLoading;
  291. /**
  292. * @param {Chunk} chunk the chunk
  293. * @returns {boolean} true, when enabled
  294. */
  295. const isEnabledForChunk = chunk => {
  296. const options = chunk.getEntryOptions();
  297. const chunkLoading =
  298. options && options.chunkLoading !== undefined
  299. ? options.chunkLoading
  300. : globalChunkLoading;
  301. return chunkLoading === "jsonp";
  302. };
  303. const onceForChunkSet = new WeakSet();
  304. /**
  305. * @param {Chunk} chunk chunk to check
  306. * @param {Set<string>} set runtime requirements
  307. */
  308. const handler = (chunk, set) => {
  309. if (onceForChunkSet.has(chunk)) return;
  310. onceForChunkSet.add(chunk);
  311. if (!isEnabledForChunk(chunk)) return;
  312. set.add(RuntimeGlobals.publicPath);
  313. set.add(RuntimeGlobals.getChunkCssFilename);
  314. set.add(RuntimeGlobals.hasOwnProperty);
  315. set.add(RuntimeGlobals.moduleFactoriesAddOnly);
  316. set.add(RuntimeGlobals.makeNamespaceObject);
  317. const CssLoadingRuntimeModule = getCssLoadingRuntimeModule();
  318. compilation.addRuntimeModule(chunk, new CssLoadingRuntimeModule(set));
  319. };
  320. compilation.hooks.runtimeRequirementInTree
  321. .for(RuntimeGlobals.hasCssModules)
  322. .tap(plugin, handler);
  323. compilation.hooks.runtimeRequirementInTree
  324. .for(RuntimeGlobals.ensureChunkHandlers)
  325. .tap(plugin, handler);
  326. compilation.hooks.runtimeRequirementInTree
  327. .for(RuntimeGlobals.hmrDownloadUpdateHandlers)
  328. .tap(plugin, handler);
  329. }
  330. );
  331. }
  332. /**
  333. * @param {Chunk} chunk chunk
  334. * @param {Iterable<Module>} modules unordered modules
  335. * @param {Compilation} compilation compilation
  336. * @returns {Module[]} ordered modules
  337. */
  338. getModulesInOrder(chunk, modules, compilation) {
  339. if (!modules) return [];
  340. /** @type {Module[]} */
  341. const modulesList = [...modules];
  342. // Get ordered list of modules per chunk group
  343. // Lists are in reverse order to allow to use Array.pop()
  344. const modulesByChunkGroup = Array.from(chunk.groupsIterable, chunkGroup => {
  345. const sortedModules = modulesList
  346. .map(module => {
  347. return {
  348. module,
  349. index: chunkGroup.getModulePostOrderIndex(module)
  350. };
  351. })
  352. .filter(item => item.index !== undefined)
  353. .sort(
  354. (a, b) =>
  355. /** @type {number} */ (b.index) - /** @type {number} */ (a.index)
  356. )
  357. .map(item => item.module);
  358. return { list: sortedModules, set: new Set(sortedModules) };
  359. });
  360. if (modulesByChunkGroup.length === 1)
  361. return modulesByChunkGroup[0].list.reverse();
  362. const compareModuleLists = ({ list: a }, { list: b }) => {
  363. if (a.length === 0) {
  364. return b.length === 0 ? 0 : 1;
  365. } else {
  366. if (b.length === 0) return -1;
  367. return compareModulesByIdentifier(a[a.length - 1], b[b.length - 1]);
  368. }
  369. };
  370. modulesByChunkGroup.sort(compareModuleLists);
  371. /** @type {Module[]} */
  372. const finalModules = [];
  373. for (;;) {
  374. const failedModules = new Set();
  375. const list = modulesByChunkGroup[0].list;
  376. if (list.length === 0) {
  377. // done, everything empty
  378. break;
  379. }
  380. /** @type {Module} */
  381. let selectedModule = list[list.length - 1];
  382. let hasFailed = undefined;
  383. outer: for (;;) {
  384. for (const { list, set } of modulesByChunkGroup) {
  385. if (list.length === 0) continue;
  386. const lastModule = list[list.length - 1];
  387. if (lastModule === selectedModule) continue;
  388. if (!set.has(selectedModule)) continue;
  389. failedModules.add(selectedModule);
  390. if (failedModules.has(lastModule)) {
  391. // There is a conflict, try other alternatives
  392. hasFailed = lastModule;
  393. continue;
  394. }
  395. selectedModule = lastModule;
  396. hasFailed = false;
  397. continue outer; // restart
  398. }
  399. break;
  400. }
  401. if (hasFailed) {
  402. // There is a not resolve-able conflict with the selectedModule
  403. if (compilation) {
  404. // TODO print better warning
  405. compilation.warnings.push(
  406. new WebpackError(
  407. `chunk ${chunk.name || chunk.id}\nConflicting order between ${
  408. /** @type {Module} */
  409. (hasFailed).readableIdentifier(compilation.requestShortener)
  410. } and ${selectedModule.readableIdentifier(
  411. compilation.requestShortener
  412. )}`
  413. )
  414. );
  415. }
  416. selectedModule = /** @type {Module} */ (hasFailed);
  417. }
  418. // Insert the selected module into the final modules list
  419. finalModules.push(selectedModule);
  420. // Remove the selected module from all lists
  421. for (const { list, set } of modulesByChunkGroup) {
  422. const lastModule = list[list.length - 1];
  423. if (lastModule === selectedModule) list.pop();
  424. else if (hasFailed && set.has(selectedModule)) {
  425. const idx = list.indexOf(selectedModule);
  426. if (idx >= 0) list.splice(idx, 1);
  427. }
  428. }
  429. modulesByChunkGroup.sort(compareModuleLists);
  430. }
  431. return finalModules;
  432. }
  433. /**
  434. * @param {Chunk} chunk chunk
  435. * @param {ChunkGraph} chunkGraph chunk graph
  436. * @param {Compilation} compilation compilation
  437. * @returns {Module[]} ordered css modules
  438. */
  439. getOrderedChunkCssModules(chunk, chunkGraph, compilation) {
  440. return [
  441. ...this.getModulesInOrder(
  442. chunk,
  443. /** @type {Iterable<Module>} */
  444. (
  445. chunkGraph.getOrderedChunkModulesIterableBySourceType(
  446. chunk,
  447. "css-import",
  448. compareModulesByIdentifier
  449. )
  450. ),
  451. compilation
  452. ),
  453. ...this.getModulesInOrder(
  454. chunk,
  455. /** @type {Iterable<Module>} */
  456. (
  457. chunkGraph.getOrderedChunkModulesIterableBySourceType(
  458. chunk,
  459. "css",
  460. compareModulesByIdentifier
  461. )
  462. ),
  463. compilation
  464. )
  465. ];
  466. }
  467. /**
  468. * @param {Object} options options
  469. * @param {string | undefined} options.uniqueName unique name
  470. * @param {Chunk} options.chunk chunk
  471. * @param {ChunkGraph} options.chunkGraph chunk graph
  472. * @param {CodeGenerationResults} options.codeGenerationResults code generation results
  473. * @param {CssModule[]} options.modules ordered css modules
  474. * @returns {Source} generated source
  475. */
  476. renderChunk({
  477. uniqueName,
  478. chunk,
  479. chunkGraph,
  480. codeGenerationResults,
  481. modules
  482. }) {
  483. const source = new ConcatSource();
  484. /** @type {string[]} */
  485. const metaData = [];
  486. for (const module of modules) {
  487. try {
  488. const codeGenResult = codeGenerationResults.get(module, chunk.runtime);
  489. let moduleSource =
  490. /** @type {Source} */
  491. (
  492. codeGenResult.sources.get("css") ||
  493. codeGenResult.sources.get("css-import")
  494. );
  495. let inheritance = [[module.cssLayer, module.supports, module.media]];
  496. if (module.inheritance) {
  497. inheritance.push(...module.inheritance);
  498. }
  499. for (let i = 0; i < inheritance.length; i++) {
  500. const layer = inheritance[i][0];
  501. const supports = inheritance[i][1];
  502. const media = inheritance[i][2];
  503. if (media) {
  504. moduleSource = new ConcatSource(
  505. `@media ${media} {\n`,
  506. new PrefixSource("\t", moduleSource),
  507. "}\n"
  508. );
  509. }
  510. if (supports) {
  511. moduleSource = new ConcatSource(
  512. `@supports (${supports}) {\n`,
  513. new PrefixSource("\t", moduleSource),
  514. "}\n"
  515. );
  516. }
  517. // Layer can be anonymous
  518. if (layer !== undefined && layer !== null) {
  519. moduleSource = new ConcatSource(
  520. `@layer${layer ? ` ${layer}` : ""} {\n`,
  521. new PrefixSource("\t", moduleSource),
  522. "}\n"
  523. );
  524. }
  525. }
  526. if (moduleSource) {
  527. source.add(moduleSource);
  528. source.add("\n");
  529. }
  530. /** @type {Map<string, string> | undefined} */
  531. const exports =
  532. codeGenResult.data && codeGenResult.data.get("css-exports");
  533. let moduleId = chunkGraph.getModuleId(module) + "";
  534. // When `optimization.moduleIds` is `named` the module id is a path, so we need to normalize it between platforms
  535. if (typeof moduleId === "string") {
  536. moduleId = moduleId.replace(/\\/g, "/");
  537. }
  538. metaData.push(
  539. `${
  540. exports
  541. ? Array.from(exports, ([n, v]) => {
  542. const shortcutValue = `${
  543. uniqueName ? uniqueName + "-" : ""
  544. }${moduleId}-${n}`;
  545. return v === shortcutValue
  546. ? `${escapeCss(n)}/`
  547. : v === "--" + shortcutValue
  548. ? `${escapeCss(n)}%`
  549. : `${escapeCss(n)}(${escapeCss(v)})`;
  550. }).join("")
  551. : ""
  552. }${escapeCss(moduleId)}`
  553. );
  554. } catch (e) {
  555. /** @type {Error} */
  556. (e).message += `\nduring rendering of css ${module.identifier()}`;
  557. throw e;
  558. }
  559. }
  560. source.add(
  561. `head{--webpack-${escapeCss(
  562. (uniqueName ? uniqueName + "-" : "") + chunk.id,
  563. true
  564. )}:${metaData.join(",")};}`
  565. );
  566. return source;
  567. }
  568. /**
  569. * @param {Chunk} chunk chunk
  570. * @param {OutputOptions} outputOptions output options
  571. * @returns {Chunk["cssFilenameTemplate"] | OutputOptions["cssFilename"] | OutputOptions["cssChunkFilename"]} used filename template
  572. */
  573. static getChunkFilenameTemplate(chunk, outputOptions) {
  574. if (chunk.cssFilenameTemplate) {
  575. return chunk.cssFilenameTemplate;
  576. } else if (chunk.canBeInitial()) {
  577. return outputOptions.cssFilename;
  578. } else {
  579. return outputOptions.cssChunkFilename;
  580. }
  581. }
  582. /**
  583. * @param {Chunk} chunk chunk
  584. * @param {ChunkGraph} chunkGraph chunk graph
  585. * @returns {boolean} true, when the chunk has css
  586. */
  587. static chunkHasCss(chunk, chunkGraph) {
  588. return (
  589. !!chunkGraph.getChunkModulesIterableBySourceType(chunk, "css") ||
  590. !!chunkGraph.getChunkModulesIterableBySourceType(chunk, "css-import")
  591. );
  592. }
  593. }
  594. module.exports = CssModulesPlugin;