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 | };
|
---|