CssGenerator.js 17 KB

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