CssGenerator.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Sergey Melyukov @smelukov
  4. */
  5. "use strict";
  6. const { ConcatSource, RawSource, ReplaceSource } = require("webpack-sources");
  7. const { UsageState } = require("../ExportsInfo");
  8. const Generator = require("../Generator");
  9. const InitFragment = require("../InitFragment");
  10. const {
  11. CSS_TYPE,
  12. CSS_TYPES,
  13. JAVASCRIPT_AND_CSS_TYPES,
  14. JAVASCRIPT_TYPE,
  15. JAVASCRIPT_TYPES
  16. } = require("../ModuleSourceTypeConstants");
  17. const RuntimeGlobals = require("../RuntimeGlobals");
  18. const Template = require("../Template");
  19. const CssImportDependency = require("../dependencies/CssImportDependency");
  20. const HarmonyImportSideEffectDependency = require("../dependencies/HarmonyImportSideEffectDependency");
  21. const { getUndoPath } = require("../util/identifier");
  22. const memoize = require("../util/memoize");
  23. /** @typedef {import("webpack-sources").Source} Source */
  24. /** @typedef {import("../../declarations/WebpackOptions").CssModuleGeneratorOptions} CssModuleGeneratorOptions */
  25. /** @typedef {import("../Compilation").DependencyConstructor} DependencyConstructor */
  26. /** @typedef {import("../CodeGenerationResults")} CodeGenerationResults */
  27. /** @typedef {import("../Dependency")} Dependency */
  28. /** @typedef {import("../DependencyTemplate").CssData} CssData */
  29. /** @typedef {import("../DependencyTemplate").CssDependencyTemplateContext} DependencyTemplateContext */
  30. /** @typedef {import("../Generator").GenerateContext} GenerateContext */
  31. /** @typedef {import("../Generator").UpdateHashContext} UpdateHashContext */
  32. /** @typedef {import("../Module").BuildInfo} BuildInfo */
  33. /** @typedef {import("../Module").BuildMeta} BuildMeta */
  34. /** @typedef {import("../Module").ConcatenationBailoutReasonContext} ConcatenationBailoutReasonContext */
  35. /** @typedef {import("../Module").SourceType} SourceType */
  36. /** @typedef {import("../Module").SourceTypes} SourceTypes */
  37. /** @typedef {import("../ModuleGraph")} ModuleGraph */
  38. /** @typedef {import("../NormalModule")} NormalModule */
  39. /** @typedef {import("../util/Hash")} Hash */
  40. /** @typedef {import("./CssModulesPlugin").ModuleFactoryCacheEntry} ModuleFactoryCacheEntry */
  41. /** @typedef {import("../CssModule")} CssModule */
  42. /** @typedef {import("../Compilation")} Compilation */
  43. /** @typedef {import("../Module").RuntimeRequirements} RuntimeRequirements */
  44. /** @typedef {import("../../declarations/WebpackOptions").CssParserExportType} CssParserExportType */
  45. const getPropertyName = memoize(() => require("../util/propertyName"));
  46. const getCssModulesPlugin = memoize(() => require("./CssModulesPlugin"));
  47. class CssGenerator extends Generator {
  48. /**
  49. * @param {CssModuleGeneratorOptions} options options
  50. * @param {ModuleGraph} moduleGraph the module graph
  51. */
  52. constructor(options, moduleGraph) {
  53. super();
  54. this.options = options;
  55. this._exportsOnly = options.exportsOnly;
  56. this._esModule = options.esModule;
  57. this._moduleGraph = moduleGraph;
  58. /** @type {WeakMap<Source, ModuleFactoryCacheEntry>} */
  59. this._moduleFactoryCache = new WeakMap();
  60. }
  61. /**
  62. * @param {NormalModule} module module for which the bailout reason should be determined
  63. * @param {ConcatenationBailoutReasonContext} context context
  64. * @returns {string | undefined} reason why this module can't be concatenated, undefined when it can be concatenated
  65. */
  66. getConcatenationBailoutReason(module, context) {
  67. if (!this._esModule) {
  68. return "Module is not an ECMAScript module";
  69. }
  70. return undefined;
  71. }
  72. /**
  73. * Generate JavaScript code that requires and concatenates all CSS imports
  74. * @param {NormalModule} module the module to generate CSS text for
  75. * @param {GenerateContext} generateContext the generate context
  76. * @returns {{ expr: string, type: CssParserExportType }[]} JavaScript code that concatenates all imported CSS
  77. */
  78. _generateImportCode(module, generateContext) {
  79. const moduleGraph = generateContext.moduleGraph;
  80. /** @type {{ expr: string, type: CssParserExportType }[]} */
  81. const parts = [];
  82. // Iterate through module.dependencies to maintain source order
  83. for (const dep of module.dependencies) {
  84. if (dep instanceof CssImportDependency) {
  85. /** @type {CssModule} */
  86. const depModule = /** @type {CssModule} */ (moduleGraph.getModule(dep));
  87. const importVar = generateContext.runtimeTemplate.moduleExports({
  88. module: depModule,
  89. chunkGraph: generateContext.chunkGraph,
  90. request: /** @type {CssModule} */ (depModule).userRequest,
  91. weak: false,
  92. runtimeRequirements: generateContext.runtimeRequirements
  93. });
  94. generateContext.runtimeRequirements.add(
  95. RuntimeGlobals.compatGetDefaultExport
  96. );
  97. parts.push({
  98. expr: `(${RuntimeGlobals.compatGetDefaultExport}(${importVar})() || "")`,
  99. type: /** @type {CssParserExportType} */ (
  100. /** @type {BuildMeta} */ (depModule.buildMeta).exportType
  101. )
  102. });
  103. }
  104. }
  105. return parts;
  106. }
  107. /**
  108. * Generate CSS code for the current module
  109. * @param {NormalModule} module the module to generate CSS code for
  110. * @param {GenerateContext} generateContext the generate context
  111. * @returns {string} the CSS code as string
  112. */
  113. _generateModuleCode(module, generateContext) {
  114. const moduleSourceContent = /** @type {Source} */ (
  115. this.generate(module, {
  116. ...generateContext,
  117. type: CSS_TYPE
  118. })
  119. );
  120. if (!moduleSourceContent) {
  121. return "";
  122. }
  123. const compilation = generateContext.runtimeTemplate.compilation;
  124. const { path: filename } = compilation.getPathWithInfo(
  125. compilation.outputOptions.cssChunkFilename,
  126. {
  127. runtime: generateContext.runtime,
  128. contentHashType: "css"
  129. }
  130. );
  131. const undoPath = getUndoPath(
  132. filename,
  133. compilation.outputOptions.path,
  134. false
  135. );
  136. const CssModulesPlugin = getCssModulesPlugin();
  137. const hooks = CssModulesPlugin.getCompilationHooks(compilation);
  138. const renderedSource = CssModulesPlugin.renderModule(
  139. /** @type {CssModule} */ (module),
  140. {
  141. undoPath,
  142. moduleSourceContent,
  143. moduleFactoryCache: this._moduleFactoryCache,
  144. runtimeTemplate: generateContext.runtimeTemplate
  145. },
  146. hooks
  147. );
  148. if (!renderedSource) {
  149. return "";
  150. }
  151. const content = renderedSource.source();
  152. return typeof content === "string" ? content : content.toString("utf8");
  153. }
  154. /**
  155. * @param {NormalModule} module the current module
  156. * @param {Dependency} dependency the dependency to generate
  157. * @param {InitFragment<GenerateContext>[]} initFragments mutable list of init fragments
  158. * @param {ReplaceSource} source the current replace source which can be modified
  159. * @param {GenerateContext & { cssData: CssData }} generateContext the render context
  160. * @returns {void}
  161. */
  162. sourceDependency(module, dependency, initFragments, source, generateContext) {
  163. const constructor =
  164. /** @type {DependencyConstructor} */
  165. (dependency.constructor);
  166. const template = generateContext.dependencyTemplates.get(constructor);
  167. if (!template) {
  168. throw new Error(
  169. `No template for dependency: ${dependency.constructor.name}`
  170. );
  171. }
  172. /** @type {DependencyTemplateContext} */
  173. /** @type {InitFragment<GenerateContext>[] | undefined} */
  174. let chunkInitFragments;
  175. /** @type {DependencyTemplateContext} */
  176. const templateContext = {
  177. runtimeTemplate: generateContext.runtimeTemplate,
  178. dependencyTemplates: generateContext.dependencyTemplates,
  179. moduleGraph: generateContext.moduleGraph,
  180. chunkGraph: generateContext.chunkGraph,
  181. module,
  182. runtime: generateContext.runtime,
  183. runtimeRequirements: generateContext.runtimeRequirements,
  184. concatenationScope: generateContext.concatenationScope,
  185. codeGenerationResults:
  186. /** @type {CodeGenerationResults} */
  187. (generateContext.codeGenerationResults),
  188. initFragments,
  189. cssData: generateContext.cssData,
  190. type: generateContext.type,
  191. get chunkInitFragments() {
  192. if (!chunkInitFragments) {
  193. const data =
  194. /** @type {NonNullable<GenerateContext["getData"]>} */
  195. (generateContext.getData)();
  196. chunkInitFragments = data.get("chunkInitFragments");
  197. if (!chunkInitFragments) {
  198. chunkInitFragments = [];
  199. data.set("chunkInitFragments", chunkInitFragments);
  200. }
  201. }
  202. return chunkInitFragments;
  203. }
  204. };
  205. template.apply(dependency, source, templateContext);
  206. }
  207. /**
  208. * @param {NormalModule} module the module to generate
  209. * @param {InitFragment<GenerateContext>[]} initFragments mutable list of init fragments
  210. * @param {ReplaceSource} source the current replace source which can be modified
  211. * @param {GenerateContext & { cssData: CssData }} generateContext the generateContext
  212. * @returns {void}
  213. */
  214. sourceModule(module, initFragments, source, generateContext) {
  215. for (const dependency of module.dependencies) {
  216. this.sourceDependency(
  217. module,
  218. dependency,
  219. initFragments,
  220. source,
  221. generateContext
  222. );
  223. }
  224. if (module.presentationalDependencies !== undefined) {
  225. for (const dependency of module.presentationalDependencies) {
  226. this.sourceDependency(
  227. module,
  228. dependency,
  229. initFragments,
  230. source,
  231. generateContext
  232. );
  233. }
  234. }
  235. }
  236. /**
  237. * @param {NormalModule} module module for which the code should be generated
  238. * @param {GenerateContext} generateContext context for generate
  239. * @returns {Source | null} generated code
  240. */
  241. generate(module, generateContext) {
  242. const exportType = /** @type {BuildMeta} */ (module.buildMeta).exportType;
  243. const source =
  244. generateContext.type === JAVASCRIPT_TYPE
  245. ? exportType === "link"
  246. ? new ReplaceSource(new RawSource(""))
  247. : new ReplaceSource(/** @type {Source} */ (module.originalSource()))
  248. : new ReplaceSource(/** @type {Source} */ (module.originalSource()));
  249. /** @type {InitFragment<GenerateContext>[]} */
  250. const initFragments = [];
  251. /** @type {CssData} */
  252. const cssData = {
  253. esModule: /** @type {boolean} */ (this._esModule),
  254. exports: new Map()
  255. };
  256. this.sourceModule(module, initFragments, source, {
  257. ...generateContext,
  258. cssData
  259. });
  260. const generateCssText = () => {
  261. const importCode = this._generateImportCode(module, generateContext);
  262. const moduleCode = this._generateModuleCode(module, generateContext);
  263. if (importCode.length > 0) {
  264. if (
  265. exportType === "css-style-sheet" ||
  266. importCode.some((part) => part.type !== exportType)
  267. ) {
  268. generateContext.runtimeRequirements.add(
  269. RuntimeGlobals.cssMergeStyleSheets
  270. );
  271. return `${RuntimeGlobals.cssMergeStyleSheets}([${[...importCode.map((part) => part.expr), JSON.stringify(moduleCode)].join(", ")}])`;
  272. }
  273. return generateContext.runtimeTemplate.concatenation(
  274. ...importCode,
  275. moduleCode
  276. );
  277. }
  278. return JSON.stringify(moduleCode);
  279. };
  280. /**
  281. * @returns {string | null} the default export
  282. */
  283. const generateJSDefaultExport = () => {
  284. switch (exportType) {
  285. case "text": {
  286. return generateCssText();
  287. }
  288. case "css-style-sheet": {
  289. const constOrVar = generateContext.runtimeTemplate.renderConst();
  290. return `(${generateContext.runtimeTemplate.basicFunction("", [
  291. `${constOrVar} cssText = ${generateCssText()};`,
  292. `${constOrVar} sheet = new CSSStyleSheet();`,
  293. "sheet.replaceSync(cssText);",
  294. "return sheet;"
  295. ])})()`;
  296. }
  297. default:
  298. return null;
  299. }
  300. };
  301. switch (generateContext.type) {
  302. case JAVASCRIPT_TYPE: {
  303. const isCSSModule = /** @type {BuildMeta} */ (module.buildMeta)
  304. .isCSSModule;
  305. const defaultExport = generateJSDefaultExport();
  306. /**
  307. * @param {string} name the export name
  308. * @param {string} value the export value
  309. * @returns {string} the value to be used in the export
  310. */
  311. const stringifyExportValue = (name, value) => {
  312. if (defaultExport) {
  313. return name === "default" ? value : JSON.stringify(value);
  314. }
  315. return JSON.stringify(value);
  316. };
  317. /** @type {BuildInfo} */
  318. (module.buildInfo).cssData = cssData;
  319. // Required for HMR
  320. if (module.hot) {
  321. generateContext.runtimeRequirements.add(RuntimeGlobals.module);
  322. }
  323. if (defaultExport) {
  324. cssData.exports.set("default", /** @type {string} */ (defaultExport));
  325. }
  326. if (cssData.exports.size === 0 && !isCSSModule) {
  327. return new RawSource("");
  328. }
  329. if (generateContext.concatenationScope) {
  330. const source = new ConcatSource();
  331. /** @type {Set<string>} */
  332. const usedIdentifiers = new Set();
  333. const { RESERVED_IDENTIFIER } = getPropertyName();
  334. for (const [name, v] of cssData.exports) {
  335. const usedName = generateContext.moduleGraph
  336. .getExportInfo(module, name)
  337. .getUsedName(name, generateContext.runtime);
  338. if (!usedName) {
  339. continue;
  340. }
  341. let identifier = Template.toIdentifier(usedName);
  342. if (RESERVED_IDENTIFIER.has(identifier)) {
  343. identifier = `_${identifier}`;
  344. }
  345. let i = 0;
  346. while (usedIdentifiers.has(identifier)) {
  347. identifier = Template.toIdentifier(name + i);
  348. i += 1;
  349. }
  350. usedIdentifiers.add(identifier);
  351. generateContext.concatenationScope.registerExport(name, identifier);
  352. source.add(
  353. `${generateContext.runtimeTemplate.renderConst()} ${identifier} = ${stringifyExportValue(name, v)};\n`
  354. );
  355. }
  356. return source;
  357. }
  358. const needNsObj =
  359. this._esModule &&
  360. generateContext.moduleGraph
  361. .getExportsInfo(module)
  362. .otherExportsInfo.getUsed(generateContext.runtime) !==
  363. UsageState.Unused;
  364. if (needNsObj) {
  365. generateContext.runtimeRequirements.add(
  366. RuntimeGlobals.makeNamespaceObject
  367. );
  368. }
  369. // Should be after `concatenationScope` to allow module inlining
  370. generateContext.runtimeRequirements.add(RuntimeGlobals.module);
  371. if (!isCSSModule && !needNsObj) {
  372. return new RawSource(
  373. `${module.moduleArgument}.exports = ${defaultExport}`
  374. );
  375. }
  376. /** @type {string[]} */
  377. const exports = [];
  378. for (const [name, v] of cssData.exports) {
  379. exports.push(
  380. `\t${JSON.stringify(name)}: ${stringifyExportValue(name, v)}`
  381. );
  382. }
  383. return new RawSource(
  384. `${needNsObj ? `${RuntimeGlobals.makeNamespaceObject}(` : ""}${
  385. module.moduleArgument
  386. }.exports = {\n${exports.join(",\n")}\n}${needNsObj ? ")" : ""};`
  387. );
  388. }
  389. case CSS_TYPE: {
  390. if (!this._generatesJsOnly(module)) {
  391. generateContext.runtimeRequirements.add(RuntimeGlobals.hasCssModules);
  392. }
  393. return InitFragment.addToSource(source, initFragments, generateContext);
  394. }
  395. default:
  396. return null;
  397. }
  398. }
  399. /**
  400. * @param {Error} error the error
  401. * @param {NormalModule} module module for which the code should be generated
  402. * @param {GenerateContext} generateContext context for generate
  403. * @returns {Source | null} generated code
  404. */
  405. generateError(error, module, generateContext) {
  406. switch (generateContext.type) {
  407. case JAVASCRIPT_TYPE: {
  408. return new RawSource(
  409. `throw new Error(${JSON.stringify(error.message)});`
  410. );
  411. }
  412. case CSS_TYPE: {
  413. return new RawSource(`/**\n ${error.message} \n**/`);
  414. }
  415. default:
  416. return null;
  417. }
  418. }
  419. /**
  420. * @param {NormalModule} module fresh module
  421. * @returns {SourceTypes} available types (do not mutate)
  422. */
  423. getTypes(module) {
  424. const exportType = /** @type {BuildMeta} */ (module.buildMeta).exportType;
  425. const sourceTypes = new Set();
  426. const connections = this._moduleGraph.getIncomingConnections(module);
  427. for (const connection of connections) {
  428. if (
  429. exportType === "link" &&
  430. connection.dependency instanceof CssImportDependency
  431. ) {
  432. continue;
  433. }
  434. // when no hmr required, css module js output contains no sideEffects at all
  435. // js sideeffect connection doesn't require js type output
  436. if (connection.dependency instanceof HarmonyImportSideEffectDependency) {
  437. continue;
  438. }
  439. if (!connection.originModule) {
  440. continue;
  441. }
  442. if (connection.originModule.type.split("/")[0] !== CSS_TYPE) {
  443. sourceTypes.add(JAVASCRIPT_TYPE);
  444. } else {
  445. const originModule = /** @type {CssModule} */ connection.originModule;
  446. const originExportType = /** @type {BuildMeta} */ (
  447. originModule.buildMeta
  448. ).exportType;
  449. if (
  450. /** @type {boolean} */ (
  451. originExportType && originExportType !== "link"
  452. )
  453. ) {
  454. sourceTypes.add(JAVASCRIPT_TYPE);
  455. }
  456. }
  457. }
  458. if (this._generatesJsOnly(module)) {
  459. if (sourceTypes.has(JAVASCRIPT_TYPE)) {
  460. return JAVASCRIPT_TYPES;
  461. }
  462. return new Set();
  463. }
  464. if (sourceTypes.has(JAVASCRIPT_TYPE)) {
  465. return JAVASCRIPT_AND_CSS_TYPES;
  466. }
  467. return CSS_TYPES;
  468. }
  469. /**
  470. * @param {NormalModule} module the module
  471. * @param {SourceType=} type source type
  472. * @returns {number} estimate size of the module
  473. */
  474. getSize(module, type) {
  475. switch (type) {
  476. case JAVASCRIPT_TYPE: {
  477. const cssData = /** @type {BuildInfo} */ (module.buildInfo).cssData;
  478. if (!cssData) {
  479. return 42;
  480. }
  481. if (cssData.exports.size === 0) {
  482. if (/** @type {BuildMeta} */ (module.buildMeta).isCSSModule) {
  483. return 42;
  484. }
  485. return 0;
  486. }
  487. const exports = cssData.exports;
  488. const stringifiedExports = JSON.stringify(
  489. [...exports].reduce((obj, [key, value]) => {
  490. obj[key] = value;
  491. return obj;
  492. }, /** @type {Record<string, string>} */ ({}))
  493. );
  494. return stringifiedExports.length + 42;
  495. }
  496. case CSS_TYPE: {
  497. const originalSource = module.originalSource();
  498. if (!originalSource) {
  499. return 0;
  500. }
  501. return originalSource.size();
  502. }
  503. default:
  504. return 0;
  505. }
  506. }
  507. /**
  508. * @param {Hash} hash hash that will be modified
  509. * @param {UpdateHashContext} updateHashContext context for updating hash
  510. */
  511. updateHash(hash, { module }) {
  512. hash.update(/** @type {boolean} */ (this._esModule).toString());
  513. }
  514. /**
  515. * @param {NormalModule} module module
  516. * @returns {boolean} true if the module only outputs JavaScript
  517. */
  518. _generatesJsOnly(module) {
  519. const exportType = /** @type {BuildMeta} */ (module.buildMeta).exportType;
  520. return (
  521. this._exportsOnly ||
  522. /** @type {boolean} */ (exportType && exportType !== "link")
  523. );
  524. }
  525. }
  526. module.exports = CssGenerator;
  527. module.exports = CssGenerator;