minifyStyles.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. 'use strict';
  2. /**
  3. * @typedef {import('../lib/types').XastElement} XastElement
  4. */
  5. const csso = require('csso');
  6. exports.type = 'visitor';
  7. exports.name = 'minifyStyles';
  8. exports.active = true;
  9. exports.description =
  10. 'minifies styles and removes unused styles based on usage data';
  11. /**
  12. * Minifies styles (<style> element + style attribute) using CSSO
  13. *
  14. * @author strarsis <strarsis@gmail.com>
  15. *
  16. * @type {import('../lib/types').Plugin<csso.MinifyOptions & Omit<csso.CompressOptions, 'usage'> & {
  17. * usage?: boolean | {
  18. * force?: boolean,
  19. * ids?: boolean,
  20. * classes?: boolean,
  21. * tags?: boolean
  22. * }
  23. * }>}
  24. */
  25. exports.fn = (_root, { usage, ...params }) => {
  26. let enableTagsUsage = true;
  27. let enableIdsUsage = true;
  28. let enableClassesUsage = true;
  29. // force to use usage data even if it unsafe (document contains <script> or on* attributes)
  30. let forceUsageDeoptimized = false;
  31. if (typeof usage === 'boolean') {
  32. enableTagsUsage = usage;
  33. enableIdsUsage = usage;
  34. enableClassesUsage = usage;
  35. } else if (usage) {
  36. enableTagsUsage = usage.tags == null ? true : usage.tags;
  37. enableIdsUsage = usage.ids == null ? true : usage.ids;
  38. enableClassesUsage = usage.classes == null ? true : usage.classes;
  39. forceUsageDeoptimized = usage.force == null ? false : usage.force;
  40. }
  41. /**
  42. * @type {Array<XastElement>}
  43. */
  44. const styleElements = [];
  45. /**
  46. * @type {Array<XastElement>}
  47. */
  48. const elementsWithStyleAttributes = [];
  49. let deoptimized = false;
  50. /**
  51. * @type {Set<string>}
  52. */
  53. const tagsUsage = new Set();
  54. /**
  55. * @type {Set<string>}
  56. */
  57. const idsUsage = new Set();
  58. /**
  59. * @type {Set<string>}
  60. */
  61. const classesUsage = new Set();
  62. return {
  63. element: {
  64. enter: (node) => {
  65. // detect deoptimisations
  66. if (node.name === 'script') {
  67. deoptimized = true;
  68. }
  69. for (const name of Object.keys(node.attributes)) {
  70. if (name.startsWith('on')) {
  71. deoptimized = true;
  72. }
  73. }
  74. // collect tags, ids and classes usage
  75. tagsUsage.add(node.name);
  76. if (node.attributes.id != null) {
  77. idsUsage.add(node.attributes.id);
  78. }
  79. if (node.attributes.class != null) {
  80. for (const className of node.attributes.class.split(/\s+/)) {
  81. classesUsage.add(className);
  82. }
  83. }
  84. // collect style elements or elements with style attribute
  85. if (node.name === 'style' && node.children.length !== 0) {
  86. styleElements.push(node);
  87. } else if (node.attributes.style != null) {
  88. elementsWithStyleAttributes.push(node);
  89. }
  90. },
  91. },
  92. root: {
  93. exit: () => {
  94. /**
  95. * @type {csso.Usage}
  96. */
  97. const cssoUsage = {};
  98. if (deoptimized === false || forceUsageDeoptimized === true) {
  99. if (enableTagsUsage && tagsUsage.size !== 0) {
  100. cssoUsage.tags = Array.from(tagsUsage);
  101. }
  102. if (enableIdsUsage && idsUsage.size !== 0) {
  103. cssoUsage.ids = Array.from(idsUsage);
  104. }
  105. if (enableClassesUsage && classesUsage.size !== 0) {
  106. cssoUsage.classes = Array.from(classesUsage);
  107. }
  108. }
  109. // minify style elements
  110. for (const node of styleElements) {
  111. if (
  112. node.children[0].type === 'text' ||
  113. node.children[0].type === 'cdata'
  114. ) {
  115. const cssText = node.children[0].value;
  116. const minified = csso.minify(cssText, {
  117. ...params,
  118. usage: cssoUsage,
  119. }).css;
  120. // preserve cdata if necessary
  121. // TODO split cdata -> text optimisation into separate plugin
  122. if (cssText.indexOf('>') >= 0 || cssText.indexOf('<') >= 0) {
  123. node.children[0].type = 'cdata';
  124. node.children[0].value = minified;
  125. } else {
  126. node.children[0].type = 'text';
  127. node.children[0].value = minified;
  128. }
  129. }
  130. }
  131. // minify style attributes
  132. for (const node of elementsWithStyleAttributes) {
  133. // style attribute
  134. const elemStyle = node.attributes.style;
  135. node.attributes.style = csso.minifyBlock(elemStyle, {
  136. ...params,
  137. }).css;
  138. }
  139. },
  140. },
  141. };
  142. };