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