CssModulesPlugin.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { ConcatSource } = require("webpack-sources");
  7. const HotUpdateChunk = require("../HotUpdateChunk");
  8. const RuntimeGlobals = require("../RuntimeGlobals");
  9. const SelfModuleFactory = require("../SelfModuleFactory");
  10. const CssExportDependency = require("../dependencies/CssExportDependency");
  11. const CssImportDependency = require("../dependencies/CssImportDependency");
  12. const CssLocalIdentifierDependency = require("../dependencies/CssLocalIdentifierDependency");
  13. const CssSelfLocalIdentifierDependency = require("../dependencies/CssSelfLocalIdentifierDependency");
  14. const CssUrlDependency = require("../dependencies/CssUrlDependency");
  15. const StaticExportsDependency = require("../dependencies/StaticExportsDependency");
  16. const { compareModulesByIdentifier } = require("../util/comparators");
  17. const createSchemaValidation = require("../util/create-schema-validation");
  18. const createHash = require("../util/createHash");
  19. const memoize = require("../util/memoize");
  20. const nonNumericOnlyHash = require("../util/nonNumericOnlyHash");
  21. const CssExportsGenerator = require("./CssExportsGenerator");
  22. const CssGenerator = require("./CssGenerator");
  23. const CssParser = require("./CssParser");
  24. /** @typedef {import("webpack-sources").Source} Source */
  25. /** @typedef {import("../../declarations/WebpackOptions").CssExperimentOptions} CssExperimentOptions */
  26. /** @typedef {import("../Chunk")} Chunk */
  27. /** @typedef {import("../Compiler")} Compiler */
  28. /** @typedef {import("../Module")} Module */
  29. const getCssLoadingRuntimeModule = memoize(() =>
  30. require("./CssLoadingRuntimeModule")
  31. );
  32. const getSchema = name => {
  33. const { definitions } = require("../../schemas/WebpackOptions.json");
  34. return {
  35. definitions,
  36. oneOf: [{ $ref: `#/definitions/${name}` }]
  37. };
  38. };
  39. const validateGeneratorOptions = createSchemaValidation(
  40. require("../../schemas/plugins/css/CssGeneratorOptions.check.js"),
  41. () => getSchema("CssGeneratorOptions"),
  42. {
  43. name: "Css Modules Plugin",
  44. baseDataPath: "parser"
  45. }
  46. );
  47. const validateParserOptions = createSchemaValidation(
  48. require("../../schemas/plugins/css/CssParserOptions.check.js"),
  49. () => getSchema("CssParserOptions"),
  50. {
  51. name: "Css Modules Plugin",
  52. baseDataPath: "parser"
  53. }
  54. );
  55. const escapeCss = (str, omitOptionalUnderscore) => {
  56. const escaped = `${str}`.replace(
  57. // cspell:word uffff
  58. /[^a-zA-Z0-9_\u0081-\uffff-]/g,
  59. s => `\\${s}`
  60. );
  61. return !omitOptionalUnderscore && /^(?!--)[0-9_-]/.test(escaped)
  62. ? `_${escaped}`
  63. : escaped;
  64. };
  65. const plugin = "CssModulesPlugin";
  66. class CssModulesPlugin {
  67. /**
  68. * @param {CssExperimentOptions} options options
  69. */
  70. constructor({ exportsOnly = false }) {
  71. this._exportsOnly = exportsOnly;
  72. }
  73. /**
  74. * Apply the plugin
  75. * @param {Compiler} compiler the compiler instance
  76. * @returns {void}
  77. */
  78. apply(compiler) {
  79. compiler.hooks.compilation.tap(
  80. plugin,
  81. (compilation, { normalModuleFactory }) => {
  82. const selfFactory = new SelfModuleFactory(compilation.moduleGraph);
  83. compilation.dependencyFactories.set(
  84. CssUrlDependency,
  85. normalModuleFactory
  86. );
  87. compilation.dependencyTemplates.set(
  88. CssUrlDependency,
  89. new CssUrlDependency.Template()
  90. );
  91. compilation.dependencyTemplates.set(
  92. CssLocalIdentifierDependency,
  93. new CssLocalIdentifierDependency.Template()
  94. );
  95. compilation.dependencyFactories.set(
  96. CssSelfLocalIdentifierDependency,
  97. selfFactory
  98. );
  99. compilation.dependencyTemplates.set(
  100. CssSelfLocalIdentifierDependency,
  101. new CssSelfLocalIdentifierDependency.Template()
  102. );
  103. compilation.dependencyTemplates.set(
  104. CssExportDependency,
  105. new CssExportDependency.Template()
  106. );
  107. compilation.dependencyFactories.set(
  108. CssImportDependency,
  109. normalModuleFactory
  110. );
  111. compilation.dependencyTemplates.set(
  112. CssImportDependency,
  113. new CssImportDependency.Template()
  114. );
  115. compilation.dependencyTemplates.set(
  116. StaticExportsDependency,
  117. new StaticExportsDependency.Template()
  118. );
  119. normalModuleFactory.hooks.createParser
  120. .for("css")
  121. .tap(plugin, parserOptions => {
  122. validateParserOptions(parserOptions);
  123. return new CssParser();
  124. });
  125. normalModuleFactory.hooks.createParser
  126. .for("css/global")
  127. .tap(plugin, parserOptions => {
  128. validateParserOptions(parserOptions);
  129. return new CssParser({
  130. allowPseudoBlocks: false,
  131. allowModeSwitch: false
  132. });
  133. });
  134. normalModuleFactory.hooks.createParser
  135. .for("css/module")
  136. .tap(plugin, parserOptions => {
  137. validateParserOptions(parserOptions);
  138. return new CssParser({
  139. defaultMode: "local"
  140. });
  141. });
  142. normalModuleFactory.hooks.createGenerator
  143. .for("css")
  144. .tap(plugin, generatorOptions => {
  145. validateGeneratorOptions(generatorOptions);
  146. return this._exportsOnly
  147. ? new CssExportsGenerator()
  148. : new CssGenerator();
  149. });
  150. normalModuleFactory.hooks.createGenerator
  151. .for("css/global")
  152. .tap(plugin, generatorOptions => {
  153. validateGeneratorOptions(generatorOptions);
  154. return this._exportsOnly
  155. ? new CssExportsGenerator()
  156. : new CssGenerator();
  157. });
  158. normalModuleFactory.hooks.createGenerator
  159. .for("css/module")
  160. .tap(plugin, generatorOptions => {
  161. validateGeneratorOptions(generatorOptions);
  162. return this._exportsOnly
  163. ? new CssExportsGenerator()
  164. : new CssGenerator();
  165. });
  166. const orderedCssModulesPerChunk = new WeakMap();
  167. compilation.hooks.afterCodeGeneration.tap("CssModulesPlugin", () => {
  168. const { chunkGraph } = compilation;
  169. for (const chunk of compilation.chunks) {
  170. if (CssModulesPlugin.chunkHasCss(chunk, chunkGraph)) {
  171. orderedCssModulesPerChunk.set(
  172. chunk,
  173. this.getOrderedChunkCssModules(chunk, chunkGraph, compilation)
  174. );
  175. }
  176. }
  177. });
  178. compilation.hooks.contentHash.tap("CssModulesPlugin", chunk => {
  179. const {
  180. chunkGraph,
  181. outputOptions: {
  182. hashSalt,
  183. hashDigest,
  184. hashDigestLength,
  185. hashFunction
  186. }
  187. } = compilation;
  188. const modules = orderedCssModulesPerChunk.get(chunk);
  189. if (modules === undefined) return;
  190. const hash = createHash(hashFunction);
  191. if (hashSalt) hash.update(hashSalt);
  192. for (const module of modules) {
  193. hash.update(chunkGraph.getModuleHash(module, chunk.runtime));
  194. }
  195. const digest = /** @type {string} */ (hash.digest(hashDigest));
  196. chunk.contentHash.css = nonNumericOnlyHash(digest, hashDigestLength);
  197. });
  198. compilation.hooks.renderManifest.tap(plugin, (result, options) => {
  199. const { chunkGraph } = compilation;
  200. const { hash, chunk, codeGenerationResults } = options;
  201. if (chunk instanceof HotUpdateChunk) return result;
  202. const modules = orderedCssModulesPerChunk.get(chunk);
  203. if (modules !== undefined) {
  204. result.push({
  205. render: () =>
  206. this.renderChunk({
  207. chunk,
  208. chunkGraph,
  209. codeGenerationResults,
  210. uniqueName: compilation.outputOptions.uniqueName,
  211. modules
  212. }),
  213. filenameTemplate: CssModulesPlugin.getChunkFilenameTemplate(
  214. chunk,
  215. compilation.outputOptions
  216. ),
  217. pathOptions: {
  218. hash,
  219. runtime: chunk.runtime,
  220. chunk,
  221. contentHashType: "css"
  222. },
  223. identifier: `css${chunk.id}`,
  224. hash: chunk.contentHash.css
  225. });
  226. }
  227. return result;
  228. });
  229. const enabledChunks = new WeakSet();
  230. const handler = (chunk, set) => {
  231. if (enabledChunks.has(chunk)) {
  232. return;
  233. }
  234. enabledChunks.add(chunk);
  235. set.add(RuntimeGlobals.publicPath);
  236. set.add(RuntimeGlobals.getChunkCssFilename);
  237. set.add(RuntimeGlobals.hasOwnProperty);
  238. set.add(RuntimeGlobals.moduleFactoriesAddOnly);
  239. set.add(RuntimeGlobals.makeNamespaceObject);
  240. const CssLoadingRuntimeModule = getCssLoadingRuntimeModule();
  241. compilation.addRuntimeModule(chunk, new CssLoadingRuntimeModule(set));
  242. };
  243. compilation.hooks.runtimeRequirementInTree
  244. .for(RuntimeGlobals.hasCssModules)
  245. .tap(plugin, handler);
  246. compilation.hooks.runtimeRequirementInTree
  247. .for(RuntimeGlobals.ensureChunkHandlers)
  248. .tap(plugin, handler);
  249. compilation.hooks.runtimeRequirementInTree
  250. .for(RuntimeGlobals.hmrDownloadUpdateHandlers)
  251. .tap(plugin, handler);
  252. }
  253. );
  254. }
  255. getModulesInOrder(chunk, modules, compilation) {
  256. if (!modules) return [];
  257. const modulesList = [...modules];
  258. // Get ordered list of modules per chunk group
  259. // Lists are in reverse order to allow to use Array.pop()
  260. const modulesByChunkGroup = Array.from(chunk.groupsIterable, chunkGroup => {
  261. const sortedModules = modulesList
  262. .map(module => {
  263. return {
  264. module,
  265. index: chunkGroup.getModulePostOrderIndex(module)
  266. };
  267. })
  268. .filter(item => item.index !== undefined)
  269. .sort((a, b) => b.index - a.index)
  270. .map(item => item.module);
  271. return { list: sortedModules, set: new Set(sortedModules) };
  272. });
  273. if (modulesByChunkGroup.length === 1)
  274. return modulesByChunkGroup[0].list.reverse();
  275. const compareModuleLists = ({ list: a }, { list: b }) => {
  276. if (a.length === 0) {
  277. return b.length === 0 ? 0 : 1;
  278. } else {
  279. if (b.length === 0) return -1;
  280. return compareModulesByIdentifier(a[a.length - 1], b[b.length - 1]);
  281. }
  282. };
  283. modulesByChunkGroup.sort(compareModuleLists);
  284. const finalModules = [];
  285. for (;;) {
  286. const failedModules = new Set();
  287. const list = modulesByChunkGroup[0].list;
  288. if (list.length === 0) {
  289. // done, everything empty
  290. break;
  291. }
  292. let selectedModule = list[list.length - 1];
  293. let hasFailed = undefined;
  294. outer: for (;;) {
  295. for (const { list, set } of modulesByChunkGroup) {
  296. if (list.length === 0) continue;
  297. const lastModule = list[list.length - 1];
  298. if (lastModule === selectedModule) continue;
  299. if (!set.has(selectedModule)) continue;
  300. failedModules.add(selectedModule);
  301. if (failedModules.has(lastModule)) {
  302. // There is a conflict, try other alternatives
  303. hasFailed = lastModule;
  304. continue;
  305. }
  306. selectedModule = lastModule;
  307. hasFailed = false;
  308. continue outer; // restart
  309. }
  310. break;
  311. }
  312. if (hasFailed) {
  313. // There is a not resolve-able conflict with the selectedModule
  314. if (compilation) {
  315. // TODO print better warning
  316. compilation.warnings.push(
  317. new Error(
  318. `chunk ${
  319. chunk.name || chunk.id
  320. }\nConflicting order between ${hasFailed.readableIdentifier(
  321. compilation.requestShortener
  322. )} and ${selectedModule.readableIdentifier(
  323. compilation.requestShortener
  324. )}`
  325. )
  326. );
  327. }
  328. selectedModule = hasFailed;
  329. }
  330. // Insert the selected module into the final modules list
  331. finalModules.push(selectedModule);
  332. // Remove the selected module from all lists
  333. for (const { list, set } of modulesByChunkGroup) {
  334. const lastModule = list[list.length - 1];
  335. if (lastModule === selectedModule) list.pop();
  336. else if (hasFailed && set.has(selectedModule)) {
  337. const idx = list.indexOf(selectedModule);
  338. if (idx >= 0) list.splice(idx, 1);
  339. }
  340. }
  341. modulesByChunkGroup.sort(compareModuleLists);
  342. }
  343. return finalModules;
  344. }
  345. getOrderedChunkCssModules(chunk, chunkGraph, compilation) {
  346. return [
  347. ...this.getModulesInOrder(
  348. chunk,
  349. chunkGraph.getOrderedChunkModulesIterableBySourceType(
  350. chunk,
  351. "css-import",
  352. compareModulesByIdentifier
  353. ),
  354. compilation
  355. ),
  356. ...this.getModulesInOrder(
  357. chunk,
  358. chunkGraph.getOrderedChunkModulesIterableBySourceType(
  359. chunk,
  360. "css",
  361. compareModulesByIdentifier
  362. ),
  363. compilation
  364. )
  365. ];
  366. }
  367. renderChunk({
  368. uniqueName,
  369. chunk,
  370. chunkGraph,
  371. codeGenerationResults,
  372. modules
  373. }) {
  374. const source = new ConcatSource();
  375. const metaData = [];
  376. for (const module of modules) {
  377. try {
  378. const codeGenResult = codeGenerationResults.get(module, chunk.runtime);
  379. const s =
  380. codeGenResult.sources.get("css") ||
  381. codeGenResult.sources.get("css-import");
  382. if (s) {
  383. source.add(s);
  384. source.add("\n");
  385. }
  386. const exports =
  387. codeGenResult.data && codeGenResult.data.get("css-exports");
  388. const moduleId = chunkGraph.getModuleId(module) + "";
  389. metaData.push(
  390. `${
  391. exports
  392. ? Array.from(exports, ([n, v]) => {
  393. const shortcutValue = `${
  394. uniqueName ? uniqueName + "-" : ""
  395. }${moduleId}-${n}`;
  396. return v === shortcutValue
  397. ? `${escapeCss(n)}/`
  398. : v === "--" + shortcutValue
  399. ? `${escapeCss(n)}%`
  400. : `${escapeCss(n)}(${escapeCss(v)})`;
  401. }).join("")
  402. : ""
  403. }${escapeCss(moduleId)}`
  404. );
  405. } catch (e) {
  406. e.message += `\nduring rendering of css ${module.identifier()}`;
  407. throw e;
  408. }
  409. }
  410. source.add(
  411. `head{--webpack-${escapeCss(
  412. (uniqueName ? uniqueName + "-" : "") + chunk.id,
  413. true
  414. )}:${metaData.join(",")};}`
  415. );
  416. return source;
  417. }
  418. static getChunkFilenameTemplate(chunk, outputOptions) {
  419. if (chunk.cssFilenameTemplate) {
  420. return chunk.cssFilenameTemplate;
  421. } else if (chunk.canBeInitial()) {
  422. return outputOptions.cssFilename;
  423. } else {
  424. return outputOptions.cssChunkFilename;
  425. }
  426. }
  427. static chunkHasCss(chunk, chunkGraph) {
  428. return (
  429. !!chunkGraph.getChunkModulesIterableBySourceType(chunk, "css") ||
  430. !!chunkGraph.getChunkModulesIterableBySourceType(chunk, "css-import")
  431. );
  432. }
  433. }
  434. module.exports = CssModulesPlugin;