postcss-merge-rules 5.0.6 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -4
- package/src/index.js +59 -32
- package/src/lib/ensureCompatibility.js +40 -8
- package/types/index.d.ts +9 -0
- package/types/lib/ensureCompatibility.d.ts +71 -0
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postcss-merge-rules",
|
|
3
|
-
"version": "5.0
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "Merge CSS rules with PostCSS.",
|
|
5
|
+
"types": "types/index.d.ts",
|
|
5
6
|
"main": "src/index.js",
|
|
6
7
|
"files": [
|
|
7
8
|
"LICENSE-MIT",
|
|
8
|
-
"src"
|
|
9
|
+
"src",
|
|
10
|
+
"types"
|
|
9
11
|
],
|
|
10
12
|
"keywords": [
|
|
11
13
|
"css",
|
|
@@ -24,7 +26,7 @@
|
|
|
24
26
|
"dependencies": {
|
|
25
27
|
"browserslist": "^4.16.6",
|
|
26
28
|
"caniuse-api": "^3.0.0",
|
|
27
|
-
"cssnano-utils": "^3.0
|
|
29
|
+
"cssnano-utils": "^3.1.0",
|
|
28
30
|
"postcss-selector-parser": "^6.0.5"
|
|
29
31
|
},
|
|
30
32
|
"bugs": {
|
|
@@ -34,8 +36,9 @@
|
|
|
34
36
|
"node": "^10 || ^12 || >=14.0"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
39
|
+
"@types/caniuse-api": "^3.0.2",
|
|
37
40
|
"postcss": "^8.2.15",
|
|
38
|
-
"postcss-discard-comments": "^5.0
|
|
41
|
+
"postcss-discard-comments": "^5.1.0"
|
|
39
42
|
},
|
|
40
43
|
"peerDependencies": {
|
|
41
44
|
"postcss": "^8.2.15"
|
package/src/index.js
CHANGED
|
@@ -8,8 +8,8 @@ const {
|
|
|
8
8
|
} = require('./lib/ensureCompatibility');
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* @param {postcss.Declaration} a
|
|
12
|
-
* @param {postcss.Declaration} b
|
|
11
|
+
* @param {import('postcss').Declaration} a
|
|
12
|
+
* @param {import('postcss').Declaration} b
|
|
13
13
|
* @return {boolean}
|
|
14
14
|
*/
|
|
15
15
|
function declarationIsEqual(a, b) {
|
|
@@ -19,8 +19,8 @@ function declarationIsEqual(a, b) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* @param {postcss.Declaration[]} array
|
|
23
|
-
* @param {postcss.Declaration} decl
|
|
22
|
+
* @param {import('postcss').Declaration[]} array
|
|
23
|
+
* @param {import('postcss').Declaration} decl
|
|
24
24
|
* @return {number}
|
|
25
25
|
*/
|
|
26
26
|
function indexOfDeclaration(array, decl) {
|
|
@@ -29,10 +29,10 @@ function indexOfDeclaration(array, decl) {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Returns filtered array of matched or unmatched declarations
|
|
32
|
-
* @param {postcss.Declaration[]} a
|
|
33
|
-
* @param {postcss.Declaration[]} b
|
|
32
|
+
* @param {import('postcss').Declaration[]} a
|
|
33
|
+
* @param {import('postcss').Declaration[]} b
|
|
34
34
|
* @param {boolean} [not=false]
|
|
35
|
-
* @return {postcss.Declaration[]}
|
|
35
|
+
* @return {import('postcss').Declaration[]}
|
|
36
36
|
*/
|
|
37
37
|
function intersect(a, b, not) {
|
|
38
38
|
return a.filter((c) => {
|
|
@@ -42,8 +42,8 @@ function intersect(a, b, not) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
* @param {postcss.Declaration[]} a
|
|
46
|
-
* @param {postcss.Declaration[]} b
|
|
45
|
+
* @param {import('postcss').Declaration[]} a
|
|
46
|
+
* @param {import('postcss').Declaration[]} b
|
|
47
47
|
* @return {boolean}
|
|
48
48
|
*/
|
|
49
49
|
function sameDeclarationsAndOrder(a, b) {
|
|
@@ -54,8 +54,8 @@ function sameDeclarationsAndOrder(a, b) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
|
-
* @param {postcss.Rule} ruleA
|
|
58
|
-
* @param {postcss.Rule} ruleB
|
|
57
|
+
* @param {import('postcss').Rule} ruleA
|
|
58
|
+
* @param {import('postcss').Rule} ruleB
|
|
59
59
|
* @param {string[]=} browsers
|
|
60
60
|
* @param {Map<string, boolean>=} compatibilityCache
|
|
61
61
|
* @return {boolean}
|
|
@@ -70,8 +70,11 @@ function canMerge(ruleA, ruleB, browsers, compatibilityCache) {
|
|
|
70
70
|
return false;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
const parent = sameParent(
|
|
74
|
-
|
|
73
|
+
const parent = sameParent(
|
|
74
|
+
/** @type {any} */ (ruleA),
|
|
75
|
+
/** @type {any} */ (ruleB)
|
|
76
|
+
);
|
|
77
|
+
const { name } = /** @type {any} */ (ruleA).parent;
|
|
75
78
|
if (parent && name && name.includes('keyframes')) {
|
|
76
79
|
return false;
|
|
77
80
|
}
|
|
@@ -79,22 +82,29 @@ function canMerge(ruleA, ruleB, browsers, compatibilityCache) {
|
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
/**
|
|
82
|
-
* @param {postcss.Rule} rule
|
|
83
|
-
* @return {postcss.Declaration[]}
|
|
85
|
+
* @param {import('postcss').Rule} rule
|
|
86
|
+
* @return {import('postcss').Declaration[]}
|
|
84
87
|
*/
|
|
85
88
|
function getDecls(rule) {
|
|
86
|
-
return
|
|
89
|
+
return /** @type {import('postcss').Declaration[]} */ (
|
|
90
|
+
rule.nodes.filter((node) => node.type === 'decl')
|
|
91
|
+
);
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
/** @type {(...rules: import('postcss').Rule[]) => string} */
|
|
89
95
|
const joinSelectors = (...rules) => rules.map((s) => s.selector).join();
|
|
90
96
|
|
|
97
|
+
/**
|
|
98
|
+
* @param {...import('postcss').Rule} rules
|
|
99
|
+
* @return {number}
|
|
100
|
+
*/
|
|
91
101
|
function ruleLength(...rules) {
|
|
92
102
|
return rules.map((r) => (r.nodes.length ? String(r) : '')).join('').length;
|
|
93
103
|
}
|
|
94
104
|
|
|
95
105
|
/**
|
|
96
106
|
* @param {string} prop
|
|
97
|
-
* @return {{prefix: string
|
|
107
|
+
* @return {{prefix: string?, base:string?, rest:string[]}}
|
|
98
108
|
*/
|
|
99
109
|
function splitProp(prop) {
|
|
100
110
|
// Treat vendor prefixed properties as if they were unprefixed;
|
|
@@ -155,8 +165,8 @@ function isConflictingProp(propA, propB) {
|
|
|
155
165
|
}
|
|
156
166
|
|
|
157
167
|
/**
|
|
158
|
-
* @param {postcss.Rule} first
|
|
159
|
-
* @param {postcss.Rule} second
|
|
168
|
+
* @param {import('postcss').Rule} first
|
|
169
|
+
* @param {import('postcss').Rule} second
|
|
160
170
|
* @return {boolean} merged
|
|
161
171
|
*/
|
|
162
172
|
function mergeParents(first, second) {
|
|
@@ -178,9 +188,9 @@ function mergeParents(first, second) {
|
|
|
178
188
|
}
|
|
179
189
|
|
|
180
190
|
/**
|
|
181
|
-
* @param {postcss.Rule} first
|
|
182
|
-
* @param {postcss.Rule} second
|
|
183
|
-
* @return {postcss.Rule} mergedRule
|
|
191
|
+
* @param {import('postcss').Rule} first
|
|
192
|
+
* @param {import('postcss').Rule} second
|
|
193
|
+
* @return {import('postcss').Rule} mergedRule
|
|
184
194
|
*/
|
|
185
195
|
function partialMerge(first, second) {
|
|
186
196
|
let intersection = intersect(getDecls(first), getDecls(second));
|
|
@@ -190,7 +200,11 @@ function partialMerge(first, second) {
|
|
|
190
200
|
let nextRule = second.next();
|
|
191
201
|
if (!nextRule) {
|
|
192
202
|
// Grab next cousin
|
|
193
|
-
|
|
203
|
+
/** @type {any} */
|
|
204
|
+
const parentSibling =
|
|
205
|
+
/** @type {import('postcss').Container<import('postcss').ChildNode>} */ (
|
|
206
|
+
second.parent
|
|
207
|
+
).next();
|
|
194
208
|
nextRule = parentSibling && parentSibling.nodes && parentSibling.nodes[0];
|
|
195
209
|
}
|
|
196
210
|
if (nextRule && nextRule.type === 'rule' && canMerge(second, nextRule)) {
|
|
@@ -262,14 +276,17 @@ function partialMerge(first, second) {
|
|
|
262
276
|
receivingBlock.selector = joinSelectors(first, second);
|
|
263
277
|
receivingBlock.nodes = [];
|
|
264
278
|
|
|
265
|
-
|
|
279
|
+
/** @type {import('postcss').Container<import('postcss').ChildNode>} */ (
|
|
280
|
+
second.parent
|
|
281
|
+
).insertBefore(second, receivingBlock);
|
|
266
282
|
|
|
267
283
|
const firstClone = first.clone();
|
|
268
284
|
const secondClone = second.clone();
|
|
269
285
|
|
|
270
286
|
/**
|
|
271
|
-
* @param {function(postcss.Declaration):void} callback
|
|
272
|
-
* @
|
|
287
|
+
* @param {function(import('postcss').Declaration):void} callback
|
|
288
|
+
* @this {import('postcss').Rule}
|
|
289
|
+
* @return {function(import('postcss').Declaration)}
|
|
273
290
|
*/
|
|
274
291
|
function moveDecl(callback) {
|
|
275
292
|
return (decl) => {
|
|
@@ -308,10 +325,10 @@ function partialMerge(first, second) {
|
|
|
308
325
|
/**
|
|
309
326
|
* @param {string[]} browsers
|
|
310
327
|
* @param {Map<string, boolean>} compatibilityCache
|
|
311
|
-
* @return {function(postcss.Rule)}
|
|
328
|
+
* @return {function(import('postcss').Rule)}
|
|
312
329
|
*/
|
|
313
330
|
function selectorMerger(browsers, compatibilityCache) {
|
|
314
|
-
/** @type {postcss.Rule} */
|
|
331
|
+
/** @type {import('postcss').Rule | null} */
|
|
315
332
|
let cache = null;
|
|
316
333
|
return function (rule) {
|
|
317
334
|
// Prime the cache with the first rule, or alternately ensure that it is
|
|
@@ -343,10 +360,16 @@ function selectorMerger(browsers, compatibilityCache) {
|
|
|
343
360
|
if (cache.selector === rule.selector) {
|
|
344
361
|
const cached = getDecls(cache);
|
|
345
362
|
rule.walk((decl) => {
|
|
346
|
-
if (
|
|
347
|
-
|
|
363
|
+
if (
|
|
364
|
+
~indexOfDeclaration(
|
|
365
|
+
cached,
|
|
366
|
+
/** @type {import('postcss').Declaration} */ (decl)
|
|
367
|
+
)
|
|
368
|
+
) {
|
|
369
|
+
decl.remove();
|
|
370
|
+
return;
|
|
348
371
|
}
|
|
349
|
-
cache.append(decl);
|
|
372
|
+
/** @type {import('postcss').Rule} */ (cache).append(decl);
|
|
350
373
|
});
|
|
351
374
|
rule.remove();
|
|
352
375
|
return;
|
|
@@ -356,12 +379,16 @@ function selectorMerger(browsers, compatibilityCache) {
|
|
|
356
379
|
cache = partialMerge(cache, rule);
|
|
357
380
|
};
|
|
358
381
|
}
|
|
359
|
-
|
|
382
|
+
/**
|
|
383
|
+
* @type {import('postcss').PluginCreator<void>}
|
|
384
|
+
* @return {import('postcss').Plugin}
|
|
385
|
+
*/
|
|
360
386
|
function pluginCreator() {
|
|
361
387
|
return {
|
|
362
388
|
postcssPlugin: 'postcss-merge-rules',
|
|
363
389
|
|
|
364
390
|
prepare(result) {
|
|
391
|
+
/** @type {typeof result.opts & browserslist.Options} */
|
|
365
392
|
const resultOpts = result.opts || {};
|
|
366
393
|
const browsers = browserslist(null, {
|
|
367
394
|
stats: resultOpts.stats,
|
|
@@ -15,21 +15,35 @@ const formValidation = 'form-validation';
|
|
|
15
15
|
const vendorPrefix =
|
|
16
16
|
/-(ah|apple|atsc|epub|hp|khtml|moz|ms|o|rim|ro|tc|wap|webkit|xv)-/;
|
|
17
17
|
|
|
18
|
+
const level2Sel = new Set(['=', '~=', '|=']);
|
|
19
|
+
const level3Sel = new Set(['^=', '$=', '*=']);
|
|
20
|
+
|
|
18
21
|
/**
|
|
19
22
|
* @param {string} selector
|
|
20
|
-
* @return {
|
|
23
|
+
* @return {RegExpMatchArray | null}
|
|
21
24
|
*/
|
|
22
25
|
function filterPrefixes(selector) {
|
|
23
26
|
return selector.match(vendorPrefix);
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Internet Explorer use :-ms-input-placeholder.
|
|
31
|
+
* Microsoft Edge use ::-ms-input-placeholder.
|
|
32
|
+
*
|
|
33
|
+
* @type {(selector: string) => number}
|
|
34
|
+
*/
|
|
28
35
|
const findMsInputPlaceholder = (selector) =>
|
|
29
36
|
~selector.search(/-ms-input-placeholder/i);
|
|
30
37
|
|
|
38
|
+
/**
|
|
39
|
+
* @param {string[]} selectorsA
|
|
40
|
+
* @param {string[]} selectorsB
|
|
41
|
+
* @return {boolean}
|
|
42
|
+
*/
|
|
31
43
|
function sameVendor(selectorsA, selectorsB) {
|
|
44
|
+
/** @type {(selectors: string[]) => string} */
|
|
32
45
|
let same = (selectors) => selectors.map(filterPrefixes).join();
|
|
46
|
+
/** @type {(selectors: string[]) => string | undefined} */
|
|
33
47
|
let findMsVendor = (selectors) => selectors.find(findMsInputPlaceholder);
|
|
34
48
|
return (
|
|
35
49
|
same(selectorsA) === same(selectorsB) &&
|
|
@@ -99,10 +113,18 @@ const pseudoElements = {
|
|
|
99
113
|
':visited': cssSel2,
|
|
100
114
|
};
|
|
101
115
|
|
|
116
|
+
/**
|
|
117
|
+
* @param {string} selector
|
|
118
|
+
* @return {boolean}
|
|
119
|
+
*/
|
|
102
120
|
function isCssMixin(selector) {
|
|
103
121
|
return selector[selector.length - 1] === ':';
|
|
104
122
|
}
|
|
105
123
|
|
|
124
|
+
/**
|
|
125
|
+
* @param {string} selector
|
|
126
|
+
* @return {boolean}
|
|
127
|
+
*/
|
|
106
128
|
function isHostPseudoClass(selector) {
|
|
107
129
|
return selector.includes(':host');
|
|
108
130
|
}
|
|
@@ -110,18 +132,28 @@ function isHostPseudoClass(selector) {
|
|
|
110
132
|
const isSupportedCache = new Map();
|
|
111
133
|
|
|
112
134
|
// Move to util in future
|
|
135
|
+
/**
|
|
136
|
+
* @param {string} feature
|
|
137
|
+
* @param {string[] | undefined} browsers
|
|
138
|
+
*/
|
|
113
139
|
function isSupportedCached(feature, browsers) {
|
|
114
140
|
const key = JSON.stringify({ feature, browsers });
|
|
115
141
|
let result = isSupportedCache.get(key);
|
|
116
142
|
|
|
117
143
|
if (!result) {
|
|
118
|
-
result = isSupported(feature, browsers);
|
|
144
|
+
result = isSupported(feature, /** @type {string[]} */ (browsers));
|
|
119
145
|
isSupportedCache.set(key, result);
|
|
120
146
|
}
|
|
121
147
|
|
|
122
148
|
return result;
|
|
123
149
|
}
|
|
124
150
|
|
|
151
|
+
/**
|
|
152
|
+
* @param {string[]} selectors
|
|
153
|
+
* @param{string[]=} browsers
|
|
154
|
+
* @param{Map<string,boolean>=} compatibilityCache
|
|
155
|
+
* @return {boolean}
|
|
156
|
+
*/
|
|
125
157
|
function ensureCompatibility(selectors, browsers, compatibilityCache) {
|
|
126
158
|
// Should not merge mixins
|
|
127
159
|
if (selectors.some(isCssMixin)) {
|
|
@@ -144,7 +176,8 @@ function ensureCompatibility(selectors, browsers, compatibilityCache) {
|
|
|
144
176
|
ast.walk((node) => {
|
|
145
177
|
const { type, value } = node;
|
|
146
178
|
if (type === 'pseudo') {
|
|
147
|
-
const entry =
|
|
179
|
+
const entry =
|
|
180
|
+
pseudoElements[/** @type {keyof pseudoElements} */ (value)];
|
|
148
181
|
if (!entry && noVendor(value)) {
|
|
149
182
|
compatible = false;
|
|
150
183
|
}
|
|
@@ -165,14 +198,13 @@ function ensureCompatibility(selectors, browsers, compatibilityCache) {
|
|
|
165
198
|
if (!node.operator) {
|
|
166
199
|
compatible = isSupportedCached(cssSel2, browsers);
|
|
167
200
|
}
|
|
168
|
-
|
|
169
201
|
if (value) {
|
|
170
202
|
// [foo="bar"], [foo~="bar"], [foo|="bar"]
|
|
171
|
-
if (
|
|
203
|
+
if (level2Sel.has(/** @type {string} */ (node.operator))) {
|
|
172
204
|
compatible = isSupportedCached(cssSel2, browsers);
|
|
173
205
|
}
|
|
174
206
|
// [foo^="bar"], [foo$="bar"], [foo*="bar"]
|
|
175
|
-
if (
|
|
207
|
+
if (level3Sel.has(/** @type {string} */ (node.operator))) {
|
|
176
208
|
compatible = isSupportedCached(cssSel3, browsers);
|
|
177
209
|
}
|
|
178
210
|
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string[]} selectorsA
|
|
3
|
+
* @param {string[]} selectorsB
|
|
4
|
+
* @return {boolean}
|
|
5
|
+
*/
|
|
6
|
+
export function sameVendor(selectorsA: string[], selectorsB: string[]): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* @param {string} selector
|
|
9
|
+
* @return {boolean}
|
|
10
|
+
*/
|
|
11
|
+
export function noVendor(selector: string): boolean;
|
|
12
|
+
export const pseudoElements: {
|
|
13
|
+
':active': string;
|
|
14
|
+
':after': string;
|
|
15
|
+
':any-link': string;
|
|
16
|
+
':before': string;
|
|
17
|
+
':checked': string;
|
|
18
|
+
':default': string;
|
|
19
|
+
':dir': string;
|
|
20
|
+
':disabled': string;
|
|
21
|
+
':empty': string;
|
|
22
|
+
':enabled': string;
|
|
23
|
+
':first-child': string;
|
|
24
|
+
':first-letter': string;
|
|
25
|
+
':first-line': string;
|
|
26
|
+
':first-of-type': string;
|
|
27
|
+
':focus': string;
|
|
28
|
+
':focus-within': string;
|
|
29
|
+
':focus-visible': string;
|
|
30
|
+
':has': string;
|
|
31
|
+
':hover': string;
|
|
32
|
+
':in-range': string;
|
|
33
|
+
':indeterminate': string;
|
|
34
|
+
':invalid': string;
|
|
35
|
+
':is': string;
|
|
36
|
+
':lang': string;
|
|
37
|
+
':last-child': string;
|
|
38
|
+
':last-of-type': string;
|
|
39
|
+
':link': string;
|
|
40
|
+
':matches': string;
|
|
41
|
+
':not': string;
|
|
42
|
+
':nth-child': string;
|
|
43
|
+
':nth-last-child': string;
|
|
44
|
+
':nth-last-of-type': string;
|
|
45
|
+
':nth-of-type': string;
|
|
46
|
+
':only-child': string;
|
|
47
|
+
':only-of-type': string;
|
|
48
|
+
':optional': string;
|
|
49
|
+
':out-of-range': string;
|
|
50
|
+
':placeholder-shown': string;
|
|
51
|
+
':required': string;
|
|
52
|
+
':root': string;
|
|
53
|
+
':target': string;
|
|
54
|
+
'::after': string;
|
|
55
|
+
'::backdrop': string;
|
|
56
|
+
'::before': string;
|
|
57
|
+
'::first-letter': string;
|
|
58
|
+
'::first-line': string;
|
|
59
|
+
'::marker': string;
|
|
60
|
+
'::placeholder': string;
|
|
61
|
+
'::selection': string;
|
|
62
|
+
':valid': string;
|
|
63
|
+
':visited': string;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* @param {string[]} selectors
|
|
67
|
+
* @param{string[]=} browsers
|
|
68
|
+
* @param{Map<string,boolean>=} compatibilityCache
|
|
69
|
+
* @return {boolean}
|
|
70
|
+
*/
|
|
71
|
+
export function ensureCompatibility(selectors: string[], browsers?: string[] | undefined, compatibilityCache?: Map<string, boolean> | undefined): boolean;
|