postcss-merge-rules 5.0.6 → 5.1.2

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 CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "postcss-merge-rules",
3
- "version": "5.0.6",
3
+ "version": "5.1.2",
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.2",
29
+ "cssnano-utils": "^3.1.0",
28
30
  "postcss-selector-parser": "^6.0.5"
29
31
  },
30
32
  "bugs": {
@@ -34,11 +36,11 @@
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.3"
41
+ "postcss-discard-comments": "^5.1.2"
39
42
  },
40
43
  "peerDependencies": {
41
44
  "postcss": "^8.2.15"
42
- },
43
- "readme": "# [postcss][postcss]-merge-rules\n\n> Merge CSS rules with PostCSS.\n\n## Install\n\nWith [npm](https://npmjs.org/package/postcss-merge-rules) do:\n\n```\nnpm install postcss-merge-rules --save\n```\n\n## Examples\n\nThis module will attempt to merge *adjacent* CSS rules:\n\n### By declarations\n\n#### Input\n\n```css\na {\n color: blue;\n font-weight: bold\n}\n\np {\n color: blue;\n font-weight: bold\n}\n```\n\n#### Output\n\n```css\na,p {\n color: blue;\n font-weight: bold\n}\n```\n\n### By selectors\n\n#### Input\n\n```css\na {\n color: blue\n}\n\na {\n font-weight: bold\n}\n```\n\n#### Output\n\n```css\na {\n color: blue;\n font-weight: bold\n}\n```\n\n### By partial declarations\n\n#### Input\n\n```css\na {\n font-weight: bold\n}\n\np {\n color: blue;\n font-weight: bold\n}\n```\n\n#### Output\n\n```css\na,p {\n font-weight: bold\n}\n\np {\n color: blue\n}\n```\n\n## Usage\n\nSee the [PostCSS documentation](https://github.com/postcss/postcss#usage) for\nexamples for your environment.\n\n## Contributors\n\nSee [CONTRIBUTORS.md](https://github.com/cssnano/cssnano/blob/master/CONTRIBUTORS.md).\n\n## License\n\nMIT © [Ben Briggs](http://beneb.info)\n\n[postcss]: https://github.com/postcss/postcss\n"
45
+ }
44
46
  }
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,21 +29,21 @@ 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) => {
39
- const index = ~indexOfDeclaration(b, c);
39
+ const index = indexOfDeclaration(b, c) !== -1;
40
40
  return not ? !index : index;
41
41
  });
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,31 +70,52 @@ function canMerge(ruleA, ruleB, browsers, compatibilityCache) {
70
70
  return false;
71
71
  }
72
72
 
73
- const parent = sameParent(ruleA, ruleB);
74
- const { name } = ruleA.parent;
75
- if (parent && name && name.includes('keyframes')) {
73
+ const parent = sameParent(
74
+ /** @type {any} */ (ruleA),
75
+ /** @type {any} */ (ruleB)
76
+ );
77
+ if (
78
+ parent &&
79
+ ruleA.parent &&
80
+ ruleA.parent.type === 'atrule' &&
81
+ /** @type {import('postcss').AtRule} */ (ruleA.parent).name.includes(
82
+ 'keyframes'
83
+ )
84
+ ) {
76
85
  return false;
77
86
  }
78
87
  return parent && (selectors.every(noVendor) || sameVendor(a, b));
79
88
  }
80
89
 
81
90
  /**
82
- * @param {postcss.Rule} rule
83
- * @return {postcss.Declaration[]}
91
+ * @param {import('postcss').ChildNode} node
92
+ * @return {node is import('postcss').Declaration}
93
+ */
94
+ function isDeclaration(node) {
95
+ return node.type === 'decl';
96
+ }
97
+ /**
98
+ * @param {import('postcss').Rule} rule
99
+ * @return {import('postcss').Declaration[]}
84
100
  */
85
101
  function getDecls(rule) {
86
- return rule.nodes.filter((node) => node.type === 'decl');
102
+ return rule.nodes.filter(isDeclaration);
87
103
  }
88
104
 
105
+ /** @type {(...rules: import('postcss').Rule[]) => string} */
89
106
  const joinSelectors = (...rules) => rules.map((s) => s.selector).join();
90
107
 
108
+ /**
109
+ * @param {...import('postcss').Rule} rules
110
+ * @return {number}
111
+ */
91
112
  function ruleLength(...rules) {
92
113
  return rules.map((r) => (r.nodes.length ? String(r) : '')).join('').length;
93
114
  }
94
115
 
95
116
  /**
96
117
  * @param {string} prop
97
- * @return {{prefix: string, base:string, rest:string[]}}
118
+ * @return {{prefix: string?, base:string?, rest:string[]}}
98
119
  */
99
120
  function splitProp(prop) {
100
121
  // Treat vendor prefixed properties as if they were unprefixed;
@@ -155,8 +176,8 @@ function isConflictingProp(propA, propB) {
155
176
  }
156
177
 
157
178
  /**
158
- * @param {postcss.Rule} first
159
- * @param {postcss.Rule} second
179
+ * @param {import('postcss').Rule} first
180
+ * @param {import('postcss').Rule} second
160
181
  * @return {boolean} merged
161
182
  */
162
183
  function mergeParents(first, second) {
@@ -178,9 +199,9 @@ function mergeParents(first, second) {
178
199
  }
179
200
 
180
201
  /**
181
- * @param {postcss.Rule} first
182
- * @param {postcss.Rule} second
183
- * @return {postcss.Rule} mergedRule
202
+ * @param {import('postcss').Rule} first
203
+ * @param {import('postcss').Rule} second
204
+ * @return {import('postcss').Rule} mergedRule
184
205
  */
185
206
  function partialMerge(first, second) {
186
207
  let intersection = intersect(getDecls(first), getDecls(second));
@@ -190,7 +211,11 @@ function partialMerge(first, second) {
190
211
  let nextRule = second.next();
191
212
  if (!nextRule) {
192
213
  // Grab next cousin
193
- const parentSibling = second.parent.next();
214
+ /** @type {any} */
215
+ const parentSibling =
216
+ /** @type {import('postcss').Container<import('postcss').ChildNode>} */ (
217
+ second.parent
218
+ ).next();
194
219
  nextRule = parentSibling && parentSibling.nodes && parentSibling.nodes[0];
195
220
  }
196
221
  if (nextRule && nextRule.type === 'rule' && canMerge(second, nextRule)) {
@@ -262,18 +287,21 @@ function partialMerge(first, second) {
262
287
  receivingBlock.selector = joinSelectors(first, second);
263
288
  receivingBlock.nodes = [];
264
289
 
265
- second.parent.insertBefore(second, receivingBlock);
290
+ /** @type {import('postcss').Container<import('postcss').ChildNode>} */ (
291
+ second.parent
292
+ ).insertBefore(second, receivingBlock);
266
293
 
267
294
  const firstClone = first.clone();
268
295
  const secondClone = second.clone();
269
296
 
270
297
  /**
271
- * @param {function(postcss.Declaration):void} callback
272
- * @return {function(postcss.Declaration)}
298
+ * @param {function(import('postcss').Declaration):void} callback
299
+ * @this {import('postcss').Rule}
300
+ * @return {function(import('postcss').Declaration)}
273
301
  */
274
302
  function moveDecl(callback) {
275
303
  return (decl) => {
276
- if (~indexOfDeclaration(intersection, decl)) {
304
+ if (indexOfDeclaration(intersection, decl) !== -1) {
277
305
  callback.call(this, decl);
278
306
  }
279
307
  };
@@ -308,10 +336,10 @@ function partialMerge(first, second) {
308
336
  /**
309
337
  * @param {string[]} browsers
310
338
  * @param {Map<string, boolean>} compatibilityCache
311
- * @return {function(postcss.Rule)}
339
+ * @return {function(import('postcss').Rule)}
312
340
  */
313
341
  function selectorMerger(browsers, compatibilityCache) {
314
- /** @type {postcss.Rule} */
342
+ /** @type {import('postcss').Rule | null} */
315
343
  let cache = null;
316
344
  return function (rule) {
317
345
  // Prime the cache with the first rule, or alternately ensure that it is
@@ -342,11 +370,12 @@ function selectorMerger(browsers, compatibilityCache) {
342
370
  // e.g. a { color: blue } a { font-weight: bold }
343
371
  if (cache.selector === rule.selector) {
344
372
  const cached = getDecls(cache);
345
- rule.walk((decl) => {
346
- if (~indexOfDeclaration(cached, decl)) {
347
- return decl.remove();
373
+ rule.walk((node) => {
374
+ if (node.type === 'decl' && indexOfDeclaration(cached, node) !== -1) {
375
+ node.remove();
376
+ return;
348
377
  }
349
- cache.append(decl);
378
+ /** @type {import('postcss').Rule} */ (cache).append(node);
350
379
  });
351
380
  rule.remove();
352
381
  return;
@@ -356,12 +385,16 @@ function selectorMerger(browsers, compatibilityCache) {
356
385
  cache = partialMerge(cache, rule);
357
386
  };
358
387
  }
359
-
388
+ /**
389
+ * @type {import('postcss').PluginCreator<void>}
390
+ * @return {import('postcss').Plugin}
391
+ */
360
392
  function pluginCreator() {
361
393
  return {
362
394
  postcssPlugin: 'postcss-merge-rules',
363
395
 
364
396
  prepare(result) {
397
+ /** @type {typeof result.opts & browserslist.Options} */
365
398
  const resultOpts = result.opts || {};
366
399
  const browsers = browserslist(null, {
367
400
  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 {string[]}
23
+ * @return {RegExpMatchArray | null}
21
24
  */
22
25
  function filterPrefixes(selector) {
23
26
  return selector.match(vendorPrefix);
24
27
  }
25
28
 
26
- // Internet Explorer use :-ms-input-placeholder.
27
- // Microsoft Edge use ::-ms-input-placeholder.
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 = pseudoElements[value];
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 (['=', '~=', '|='].includes(node.operator)) {
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 (['^=', '$=', '*='].includes(node.operator)) {
207
+ if (level3Sel.has(/** @type {string} */ (node.operator))) {
176
208
  compatible = isSupportedCached(cssSel3, browsers);
177
209
  }
178
210
  }
@@ -0,0 +1,9 @@
1
+ export = pluginCreator;
2
+ /**
3
+ * @type {import('postcss').PluginCreator<void>}
4
+ * @return {import('postcss').Plugin}
5
+ */
6
+ declare function pluginCreator(): import('postcss').Plugin;
7
+ declare namespace pluginCreator {
8
+ const postcss: true;
9
+ }
@@ -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;