postcss-merge-rules 8.0.5 → 9.0.1
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 +8 -13
- package/src/index.js +547 -518
- package/src/lib/computeBrowsersToSupport.js +10 -12
- package/src/lib/declarations.js +146 -0
- package/src/lib/ensureCompatibility.js +4 -4
- package/src/lib/getBrowsersForWebBuild.js +2 -4
- package/src/lib/propertyRelations.js +2 -5
- package/src/lib/rule-meta.js +58 -0
- package/src/lib/rule-rewrite.js +170 -0
- package/types/index.d.ts +15 -2
- package/types/index.d.ts.map +1 -1
- package/types/lib/computeBrowsersToSupport.d.ts +2 -2
- package/types/lib/computeBrowsersToSupport.d.ts.map +1 -1
- package/types/lib/declarations.d.ts +31 -0
- package/types/lib/declarations.d.ts.map +1 -0
- package/types/lib/ensureCompatibility.d.ts +54 -59
- package/types/lib/ensureCompatibility.d.ts.map +1 -1
- package/types/lib/getBrowsersForWebBuild.d.ts +2 -2
- package/types/lib/propertyRelations.d.ts +1 -4
- package/types/lib/propertyRelations.d.ts.map +1 -1
- package/types/lib/rule-meta.d.ts +21 -0
- package/types/lib/rule-meta.d.ts.map +1 -0
- package/types/lib/rule-rewrite.d.ts +40 -0
- package/types/lib/rule-rewrite.d.ts.map +1 -0
package/src/index.js
CHANGED
|
@@ -1,73 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
/** @import browserslist from 'browserslist' */
|
|
6
|
-
|
|
7
|
-
const { sameParent } = require('cssnano-utils');
|
|
8
|
-
const {
|
|
1
|
+
import getBrowsersList from '#getBrowsersList';
|
|
2
|
+
import cssnanoUtils from 'cssnano-utils';
|
|
3
|
+
import {
|
|
9
4
|
ensureCompatibility,
|
|
10
5
|
sameVendor,
|
|
11
6
|
noVendor,
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @param {Declaration[]} array
|
|
28
|
-
* @param {Declaration} decl
|
|
29
|
-
* @return {number}
|
|
30
|
-
*/
|
|
31
|
-
function indexOfDeclaration(array, decl) {
|
|
32
|
-
return array.findIndex((d) => declarationIsEqual(d, decl));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Returns filtered array of matched or unmatched declarations
|
|
37
|
-
* @param {Declaration[]} a
|
|
38
|
-
* @param {Declaration[]} b
|
|
39
|
-
* @param {boolean} [not=false]
|
|
40
|
-
* @return {Declaration[]}
|
|
41
|
-
*/
|
|
42
|
-
function intersect(a, b, not) {
|
|
43
|
-
return a.filter((c) => {
|
|
44
|
-
const index = indexOfDeclaration(b, c) !== -1;
|
|
45
|
-
return not ? !index : index;
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* @param {Declaration[]} a
|
|
51
|
-
* @param {Declaration[]} b
|
|
52
|
-
* @return {boolean}
|
|
53
|
-
*/
|
|
54
|
-
function sameDeclarationsAndOrder(a, b) {
|
|
55
|
-
if (a.length !== b.length) {
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
return a.every((d, index) => declarationIsEqual(d, b[index]));
|
|
59
|
-
}
|
|
7
|
+
} from './lib/ensureCompatibility.js';
|
|
8
|
+
import {
|
|
9
|
+
filterRuleIntersections,
|
|
10
|
+
indexOfDeclaration,
|
|
11
|
+
intersect,
|
|
12
|
+
sameDeclarationsAndOrder,
|
|
13
|
+
} from './lib/declarations.js';
|
|
14
|
+
import { flush, getDecls, getMeta } from './lib/rule-meta.js';
|
|
15
|
+
import {
|
|
16
|
+
buildMergedRule,
|
|
17
|
+
mergeParents,
|
|
18
|
+
mergeWithNextRule,
|
|
19
|
+
} from './lib/rule-rewrite.js';
|
|
60
20
|
|
|
21
|
+
/** @import browserslist from 'browserslist' */
|
|
22
|
+
const { sameParent } = cssnanoUtils;
|
|
23
|
+
/** @import {Declaration, Rule} from 'postcss' */
|
|
61
24
|
/**
|
|
62
|
-
* RuleMeta stores metadata about a `Rule` during the merging process.
|
|
63
|
-
* It tracks selectors and declarations without re-parsing the AST many times.
|
|
64
|
-
*
|
|
65
25
|
* @typedef {Object} RuleMeta
|
|
66
26
|
* @property {string[]} selectors - Array of selector strings for the rule
|
|
67
27
|
* @property {Declaration[]} declarations - Array of declaration nodes for the rule
|
|
68
28
|
* @property {boolean} dirty - Whether the selectors have been modified and need flushing
|
|
69
29
|
*/
|
|
70
|
-
|
|
71
30
|
/**
|
|
72
31
|
* @param {Rule} ruleA
|
|
73
32
|
* @param {Rule} ruleB
|
|
@@ -130,378 +89,24 @@ function canMerge(
|
|
|
130
89
|
function isRuleOrAtRule(node) {
|
|
131
90
|
return node.type === 'rule' || node.type === 'atrule';
|
|
132
91
|
}
|
|
133
|
-
/**
|
|
134
|
-
* @param {import('postcss').ChildNode} node
|
|
135
|
-
* @return {node is Declaration}
|
|
136
|
-
*/
|
|
137
|
-
function isDeclaration(node) {
|
|
138
|
-
return node.type === 'decl';
|
|
139
|
-
}
|
|
140
92
|
|
|
141
93
|
/**
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* This metadata caches selectors and declarations to avoid expensive AST
|
|
145
|
-
* re-parsing, especially for the selectors.
|
|
146
|
-
*
|
|
147
|
-
* @param {Rule} rule The PostCSS rule to get metadata for.
|
|
148
|
-
* @param {WeakMap<Rule, RuleMeta>} [ruleMeta] The metadata cache.
|
|
149
|
-
* @return {RuleMeta} The rule's virtual metadata.
|
|
94
|
+
* @param {Rule | import('postcss').Container<import('postcss').ChildNode>} node
|
|
95
|
+
* @return {import('postcss').Container<import('postcss').ChildNode> | undefined}
|
|
150
96
|
*/
|
|
151
|
-
function
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if (!meta && rule.nodes) {
|
|
155
|
-
meta = {
|
|
156
|
-
selectors: rule.selectors,
|
|
157
|
-
declarations: rule.nodes.filter(isDeclaration),
|
|
158
|
-
dirty: false,
|
|
159
|
-
};
|
|
160
|
-
ruleMeta.set(rule, meta);
|
|
161
|
-
}
|
|
162
|
-
return meta ?? { selectors: [], declarations: [], dirty: false };
|
|
163
|
-
}
|
|
164
|
-
return {
|
|
165
|
-
selectors: rule?.selectors ?? [],
|
|
166
|
-
declarations: rule?.nodes?.filter(isDeclaration) ?? [],
|
|
167
|
-
dirty: false,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Commits virtual metadata changes back to the actual PostCSS rule.
|
|
173
|
-
*
|
|
174
|
-
* @param {Rule} rule The PostCSS rule to flush.
|
|
175
|
-
* @param {WeakMap<Rule, RuleMeta>} ruleMeta The metadata cache.
|
|
176
|
-
*/
|
|
177
|
-
function flush(rule, ruleMeta) {
|
|
178
|
-
const meta = ruleMeta.get(rule);
|
|
179
|
-
if (meta && meta.dirty) {
|
|
180
|
-
rule.selector = meta.selectors.join(',');
|
|
181
|
-
meta.dirty = false;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* @param {Rule} rule
|
|
187
|
-
* @return {Declaration[]}
|
|
188
|
-
*/
|
|
189
|
-
function getDecls(rule) {
|
|
190
|
-
return rule.nodes.filter(isDeclaration);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* @param {...Rule} rules
|
|
195
|
-
* @return {number}
|
|
196
|
-
*/
|
|
197
|
-
function ruleLength(...rules) {
|
|
198
|
-
return rules.map((r) => (r.nodes.length ? String(r) : '')).join('').length;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* @param {Rule} first
|
|
203
|
-
* @param {Rule} second
|
|
204
|
-
* @return {boolean} merged
|
|
205
|
-
*/
|
|
206
|
-
function mergeParents(first, second) {
|
|
207
|
-
// Null check for detached rules
|
|
208
|
-
if (!first.parent || !second.parent) {
|
|
209
|
-
return false;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Check if parents share node
|
|
213
|
-
if (first.parent === second.parent) {
|
|
214
|
-
return false;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// sameParent() already called by canMerge()
|
|
218
|
-
|
|
219
|
-
second.remove();
|
|
220
|
-
first.parent.append(second);
|
|
221
|
-
return true;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* @param {Rule} second
|
|
226
|
-
* @return {Rule | null}
|
|
227
|
-
*/
|
|
228
|
-
function getNextRule(second) {
|
|
229
|
-
let nextRule = second.next();
|
|
230
|
-
if (!nextRule) {
|
|
231
|
-
// Grab next cousin
|
|
232
|
-
const parentSibling =
|
|
233
|
-
/** @type {import('postcss').Container | undefined} */ (
|
|
234
|
-
/** @type {import('postcss').Container<import('postcss').ChildNode>} */ (
|
|
235
|
-
second.parent
|
|
236
|
-
).next()
|
|
237
|
-
);
|
|
238
|
-
nextRule = parentSibling && parentSibling.nodes && parentSibling.nodes[0];
|
|
239
|
-
}
|
|
240
|
-
return nextRule?.type === 'rule' ? nextRule : null;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* @param {Rule} first
|
|
245
|
-
* @param {Rule} second
|
|
246
|
-
* @param {Declaration[]} intersection
|
|
247
|
-
* @param {string[]} browsers
|
|
248
|
-
* @param {Map<string, boolean>} compatibilityCache
|
|
249
|
-
* @param {WeakSet<Rule>} ruleCache
|
|
250
|
-
* @param {WeakMap<Rule, RuleMeta>} ruleMeta
|
|
251
|
-
* @return {{first: Rule, second: Rule, intersection: Declaration[]}}
|
|
252
|
-
*/
|
|
253
|
-
function mergeWithNextRule(
|
|
254
|
-
first,
|
|
255
|
-
second,
|
|
256
|
-
intersection,
|
|
257
|
-
browsers,
|
|
258
|
-
compatibilityCache,
|
|
259
|
-
ruleCache,
|
|
260
|
-
ruleMeta
|
|
261
|
-
) {
|
|
262
|
-
const nextRule = getNextRule(second);
|
|
263
|
-
if (
|
|
264
|
-
!nextRule ||
|
|
265
|
-
!canMerge(
|
|
266
|
-
second,
|
|
267
|
-
nextRule,
|
|
268
|
-
browsers,
|
|
269
|
-
compatibilityCache,
|
|
270
|
-
ruleCache,
|
|
271
|
-
ruleMeta
|
|
272
|
-
)
|
|
273
|
-
) {
|
|
274
|
-
return { first, second, intersection };
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const nextIntersection = intersect(
|
|
278
|
-
getMeta(second, ruleMeta).declarations,
|
|
279
|
-
getMeta(nextRule, ruleMeta).declarations
|
|
97
|
+
function getParent(node) {
|
|
98
|
+
return /** @type {import('postcss').Container<import('postcss').ChildNode> | undefined} */ (
|
|
99
|
+
node.parent
|
|
280
100
|
);
|
|
281
|
-
if (nextIntersection.length <= intersection.length) {
|
|
282
|
-
return { first, second, intersection };
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
mergeParents(second, nextRule);
|
|
286
|
-
return { first: second, second: nextRule, intersection: nextIntersection };
|
|
287
101
|
}
|
|
288
102
|
|
|
289
|
-
/**
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
* @param {Declaration} candidate
|
|
295
|
-
* @param {number} candidateIndex
|
|
296
|
-
* @param {Declaration[]} hoistCandidates
|
|
297
|
-
* @param {Declaration[]} earlierRuleDeclarations
|
|
298
|
-
* @return {boolean}
|
|
299
|
-
*/
|
|
300
|
-
function hoistingPreservesOverrideOrder(
|
|
301
|
-
candidate,
|
|
302
|
-
candidateIndex,
|
|
303
|
-
hoistCandidates,
|
|
304
|
-
earlierRuleDeclarations
|
|
305
|
-
) {
|
|
306
|
-
const indexInEarlierRule = indexOfDeclaration(
|
|
307
|
-
earlierRuleDeclarations,
|
|
308
|
-
candidate
|
|
309
|
-
);
|
|
310
|
-
const overridesInEarlierRule = earlierRuleDeclarations
|
|
311
|
-
.slice(indexInEarlierRule + 1)
|
|
312
|
-
.filter((d) => isConflictingProp(d.prop, candidate.prop));
|
|
313
|
-
if (overridesInEarlierRule.length === 0) {
|
|
314
|
-
return true;
|
|
315
|
-
}
|
|
316
|
-
const overridesAmongCandidates = hoistCandidates
|
|
317
|
-
.slice(candidateIndex + 1)
|
|
318
|
-
.filter((d) => isConflictingProp(d.prop, candidate.prop));
|
|
319
|
-
if (overridesInEarlierRule.length !== overridesAmongCandidates.length) {
|
|
320
|
-
return false;
|
|
321
|
-
}
|
|
322
|
-
return overridesInEarlierRule.every((d, index) =>
|
|
323
|
-
declarationIsEqual(d, overridesAmongCandidates[index])
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* True if the later rule has an unclaimed declaration equal to `candidate`.
|
|
329
|
-
* Records its index in `claimedIndices`, so no other candidate can match the
|
|
330
|
-
* same declaration. Always false when the later rule contains `all`,
|
|
331
|
-
* since `all` resets everything except `direction`/`unicode-bidi`.
|
|
332
|
-
*
|
|
333
|
-
* @param {Declaration} candidate
|
|
334
|
-
* @param {Declaration[]} laterDeclarations
|
|
335
|
-
* @param {Set<number>} claimedIndices
|
|
336
|
-
* @return {boolean}
|
|
337
|
-
*/
|
|
338
|
-
function claimMatchInLaterRule(candidate, laterDeclarations, claimedIndices) {
|
|
339
|
-
const matchIndex = laterDeclarations.findIndex(
|
|
340
|
-
(d, index) =>
|
|
341
|
-
!claimedIndices.has(index) && isConflictingProp(d.prop, candidate.prop)
|
|
342
|
-
);
|
|
343
|
-
if (matchIndex === -1) {
|
|
344
|
-
return false;
|
|
345
|
-
}
|
|
346
|
-
if (!declarationIsEqual(laterDeclarations[matchIndex], candidate)) {
|
|
347
|
-
return false;
|
|
348
|
-
}
|
|
349
|
-
if (
|
|
350
|
-
candidate.prop.toLowerCase() !== 'direction' &&
|
|
351
|
-
candidate.prop.toLowerCase() !== 'unicode-bidi' &&
|
|
352
|
-
laterDeclarations.some(
|
|
353
|
-
(declaration) => declaration.prop.toLowerCase() === 'all'
|
|
354
|
-
)
|
|
355
|
-
) {
|
|
356
|
-
return false;
|
|
357
|
-
}
|
|
358
|
-
claimedIndices.add(matchIndex);
|
|
359
|
-
return true;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
/**
|
|
363
|
-
* Narrows the declarations shared by two adjacent rules down to those that can
|
|
364
|
-
* safely move into a merged rule, along with the indices of the later rule's
|
|
365
|
-
* declarations they claim.
|
|
366
|
-
*
|
|
367
|
-
* The merged rule is emitted after the earlier rule's remaining declarations,
|
|
368
|
-
* so hoisting a declaration that a later declaration in the earlier rule
|
|
369
|
-
* overrides would reverse the cascade and make the previously dead value
|
|
370
|
-
* effective again, unless that overriding declaration is hoisted too, so both
|
|
371
|
-
* move together and keep their relative order.
|
|
372
|
-
*
|
|
373
|
-
* Dropping a candidate can invalidate one that was only admitted because it
|
|
374
|
-
* travelled with it, so the pass repeats until the surviving set stops
|
|
375
|
-
* shrinking.
|
|
376
|
-
*
|
|
377
|
-
* @param {Declaration[]} hoistCandidates
|
|
378
|
-
* @param {Declaration[]} earlierRuleDeclarations
|
|
379
|
-
* @param {Declaration[]} laterRuleDeclarations
|
|
380
|
-
* @return {{intersection: Declaration[], claimedIndices: Set<number>}}
|
|
381
|
-
*/
|
|
382
|
-
function filterRuleIntersections(
|
|
383
|
-
hoistCandidates,
|
|
384
|
-
earlierRuleDeclarations,
|
|
385
|
-
laterRuleDeclarations
|
|
386
|
-
) {
|
|
387
|
-
let remainingCandidates = hoistCandidates;
|
|
388
|
-
for (;;) {
|
|
389
|
-
// A candidate rejected by the override check never claims a match.
|
|
390
|
-
const claimedIndices = new Set();
|
|
391
|
-
const survivors = remainingCandidates.filter(
|
|
392
|
-
(candidate, candidateIndex) =>
|
|
393
|
-
hoistingPreservesOverrideOrder(
|
|
394
|
-
candidate,
|
|
395
|
-
candidateIndex,
|
|
396
|
-
remainingCandidates,
|
|
397
|
-
earlierRuleDeclarations
|
|
398
|
-
) &&
|
|
399
|
-
claimMatchInLaterRule(candidate, laterRuleDeclarations, claimedIndices)
|
|
400
|
-
);
|
|
401
|
-
if (
|
|
402
|
-
survivors.length === remainingCandidates.length ||
|
|
403
|
-
survivors.length === 0
|
|
404
|
-
) {
|
|
405
|
-
return { intersection: survivors, claimedIndices };
|
|
406
|
-
}
|
|
407
|
-
remainingCandidates = survivors;
|
|
103
|
+
/** @param {Rule | null} rule @param {import('postcss').Container<import('postcss').ChildNode>} container */
|
|
104
|
+
function isDescendant(rule, container) {
|
|
105
|
+
let parent = rule ? getParent(rule) : undefined;
|
|
106
|
+
for (; parent; parent = getParent(parent)) {
|
|
107
|
+
if (parent === container) return true;
|
|
408
108
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* @param {Rule} first
|
|
413
|
-
* @param {Rule} second
|
|
414
|
-
* @param {Declaration[]} intersection
|
|
415
|
-
* @param {Set<number>} claimedIndices Positions of the declarations the
|
|
416
|
-
* intersection claimed in `second`, which the merged rule replaces.
|
|
417
|
-
* @param {WeakSet<Rule>} ruleCache
|
|
418
|
-
* @param {WeakMap<Rule, RuleMeta>} ruleMeta
|
|
419
|
-
* @return {Rule}
|
|
420
|
-
*/
|
|
421
|
-
function buildMergedRule(
|
|
422
|
-
first,
|
|
423
|
-
second,
|
|
424
|
-
intersection,
|
|
425
|
-
claimedIndices,
|
|
426
|
-
ruleCache,
|
|
427
|
-
ruleMeta
|
|
428
|
-
) {
|
|
429
|
-
const receivingBlock = second.clone();
|
|
430
|
-
const firstSelectors = getMeta(first, ruleMeta).selectors;
|
|
431
|
-
const secondSelectors = getMeta(second, ruleMeta).selectors;
|
|
432
|
-
|
|
433
|
-
receivingBlock.selector = [...firstSelectors, ...secondSelectors].join();
|
|
434
|
-
receivingBlock.nodes = [];
|
|
435
|
-
|
|
436
|
-
/** @type {import('postcss').Container<import('postcss').ChildNode>} */ (
|
|
437
|
-
second.parent
|
|
438
|
-
).insertBefore(second, receivingBlock);
|
|
439
|
-
|
|
440
|
-
const firstClone = first.clone({ selectors: firstSelectors });
|
|
441
|
-
const secondClone = second.clone({ selectors: secondSelectors });
|
|
442
|
-
|
|
443
|
-
/**
|
|
444
|
-
* @param {(decl: Declaration) => void} callback
|
|
445
|
-
* @this void
|
|
446
|
-
* @return {(decl: Declaration) => void}
|
|
447
|
-
*/
|
|
448
|
-
function moveDecl(callback) {
|
|
449
|
-
return (decl) => {
|
|
450
|
-
if (indexOfDeclaration(intersection, decl) !== -1) {
|
|
451
|
-
callback.call(this, decl);
|
|
452
|
-
}
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
firstClone.walkDecls(
|
|
456
|
-
moveDecl(
|
|
457
|
-
/**
|
|
458
|
-
* @param {Declaration} decl
|
|
459
|
-
*/
|
|
460
|
-
(decl) => {
|
|
461
|
-
decl.remove();
|
|
462
|
-
receivingBlock.append(decl);
|
|
463
|
-
}
|
|
464
|
-
)
|
|
465
|
-
);
|
|
466
|
-
// Remove exactly the declarations that were claimed: matching by value would
|
|
467
|
-
// also delete a duplicate that re-asserts the value later in the rule, after
|
|
468
|
-
// an overriding declaration that stays behind.
|
|
469
|
-
let laterIndex = 0;
|
|
470
|
-
secondClone.walkDecls((decl) => {
|
|
471
|
-
if (claimedIndices.has(laterIndex++)) {
|
|
472
|
-
decl.remove();
|
|
473
|
-
}
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
// Ensure original rules are flushed for accurate length comparison
|
|
477
|
-
if (ruleMeta) {
|
|
478
|
-
flush(first, ruleMeta);
|
|
479
|
-
flush(second, ruleMeta);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
const merged = ruleLength(firstClone, receivingBlock, secondClone);
|
|
483
|
-
const original = ruleLength(first, second);
|
|
484
|
-
if (merged < original) {
|
|
485
|
-
first.replaceWith(firstClone);
|
|
486
|
-
second.replaceWith(secondClone);
|
|
487
|
-
for (const r of [firstClone, receivingBlock, secondClone]) {
|
|
488
|
-
if (r.nodes.length === 0) {
|
|
489
|
-
r.remove();
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
if (!secondClone.parent) {
|
|
493
|
-
ruleCache?.add(receivingBlock);
|
|
494
|
-
return receivingBlock;
|
|
495
|
-
}
|
|
496
|
-
ruleCache?.add(receivingBlock);
|
|
497
|
-
ruleCache?.add(secondClone);
|
|
498
|
-
ruleMeta?.delete(first);
|
|
499
|
-
ruleMeta?.delete(second);
|
|
500
|
-
return secondClone;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
receivingBlock.remove();
|
|
504
|
-
return second;
|
|
109
|
+
return false;
|
|
505
110
|
}
|
|
506
111
|
|
|
507
112
|
/**
|
|
@@ -511,7 +116,8 @@ function buildMergedRule(
|
|
|
511
116
|
* @param {Map<string, boolean>} compatibilityCache
|
|
512
117
|
* @param {WeakSet<Rule>} ruleCache
|
|
513
118
|
* @param {WeakMap<Rule, RuleMeta>} ruleMeta
|
|
514
|
-
* @
|
|
119
|
+
* @param {(rule: Rule, oldParent: import('postcss').Container, newParent: import('postcss').Container) => void} [onMove]
|
|
120
|
+
* @return {{rule: Rule, replacements: Rule[], replaced: Rule[], changed: Rule[], moved: boolean}}
|
|
515
121
|
*/
|
|
516
122
|
function partialMerge(
|
|
517
123
|
first,
|
|
@@ -519,7 +125,8 @@ function partialMerge(
|
|
|
519
125
|
browsers,
|
|
520
126
|
compatibilityCache,
|
|
521
127
|
ruleCache,
|
|
522
|
-
ruleMeta
|
|
128
|
+
ruleMeta,
|
|
129
|
+
onMove
|
|
523
130
|
) {
|
|
524
131
|
if (ruleMeta) {
|
|
525
132
|
flush(first, ruleMeta);
|
|
@@ -528,7 +135,13 @@ function partialMerge(
|
|
|
528
135
|
const metaSecond = getMeta(second, ruleMeta);
|
|
529
136
|
let intersection = intersect(metaFirst.declarations, metaSecond.declarations);
|
|
530
137
|
if (intersection.length === 0) {
|
|
531
|
-
return
|
|
138
|
+
return {
|
|
139
|
+
rule: second,
|
|
140
|
+
replacements: [],
|
|
141
|
+
replaced: [],
|
|
142
|
+
changed: [],
|
|
143
|
+
moved: false,
|
|
144
|
+
};
|
|
532
145
|
}
|
|
533
146
|
const mergedNext = mergeWithNextRule(
|
|
534
147
|
first,
|
|
@@ -537,7 +150,9 @@ function partialMerge(
|
|
|
537
150
|
browsers,
|
|
538
151
|
compatibilityCache,
|
|
539
152
|
ruleCache,
|
|
540
|
-
ruleMeta
|
|
153
|
+
ruleMeta,
|
|
154
|
+
canMerge,
|
|
155
|
+
onMove
|
|
541
156
|
);
|
|
542
157
|
const mergedFirst = mergedNext.first;
|
|
543
158
|
const mergedSecond = mergedNext.second;
|
|
@@ -557,10 +172,16 @@ function partialMerge(
|
|
|
557
172
|
|
|
558
173
|
if (intersection.length === 0) {
|
|
559
174
|
// Nothing to merge
|
|
560
|
-
return
|
|
175
|
+
return {
|
|
176
|
+
rule: mergedSecond,
|
|
177
|
+
replacements: [],
|
|
178
|
+
replaced: [],
|
|
179
|
+
changed: mergedNext.moved ? [mergedFirst, mergedSecond] : [],
|
|
180
|
+
moved: mergedNext.moved,
|
|
181
|
+
};
|
|
561
182
|
}
|
|
562
183
|
|
|
563
|
-
|
|
184
|
+
const merged = buildMergedRule(
|
|
564
185
|
mergedFirst,
|
|
565
186
|
mergedSecond,
|
|
566
187
|
intersection,
|
|
@@ -568,6 +189,12 @@ function partialMerge(
|
|
|
568
189
|
ruleCache,
|
|
569
190
|
ruleMeta
|
|
570
191
|
);
|
|
192
|
+
return {
|
|
193
|
+
...merged,
|
|
194
|
+
replaced: merged.replacements.length ? [mergedFirst, mergedSecond] : [],
|
|
195
|
+
changed: mergedNext.moved ? [mergedFirst, mergedSecond] : [],
|
|
196
|
+
moved: mergedNext.moved,
|
|
197
|
+
};
|
|
571
198
|
}
|
|
572
199
|
|
|
573
200
|
/**
|
|
@@ -575,98 +202,501 @@ function partialMerge(
|
|
|
575
202
|
* @param {Map<string, boolean>} compatibilityCache
|
|
576
203
|
* @param {WeakSet<Rule>} ruleCache
|
|
577
204
|
* @param {WeakMap<Rule, RuleMeta>} ruleMeta
|
|
578
|
-
* @return {{
|
|
205
|
+
* @return {{ run: (root: import('postcss').Root) => void }}
|
|
579
206
|
*/
|
|
580
207
|
function selectorMerger(browsers, compatibilityCache, ruleCache, ruleMeta) {
|
|
581
|
-
/** @
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
208
|
+
/** @typedef {RuleMeta & {selectorKey: string, contentKey: string, declarationIds: number[], declarationIdSet: Set<number>, previous: Rule | null, next: Rule | null, active: boolean, version: number, sourceOrder: number}} ActiveMeta */
|
|
209
|
+
/** @type {WeakMap<Rule, ActiveMeta>} */
|
|
210
|
+
const active = new WeakMap();
|
|
211
|
+
/** @type {Map<string, number>} */
|
|
212
|
+
const declarationIds = new Map();
|
|
213
|
+
let nextDeclarationId = 0;
|
|
214
|
+
let nextSourceOrder = 0;
|
|
215
|
+
/** @typedef {{first: Rule | null, last: Rule | null}} Boundary */
|
|
216
|
+
/** @type {WeakMap<import('postcss').Container, Boundary>} */
|
|
217
|
+
const boundaries = new WeakMap();
|
|
218
|
+
|
|
219
|
+
/** @param {Declaration} declaration */
|
|
220
|
+
function getDeclarationId(declaration) {
|
|
221
|
+
const key = `${declaration.prop}:${declaration.value}:${declaration.important}`;
|
|
222
|
+
let id = declarationIds.get(key);
|
|
223
|
+
if (id === undefined) {
|
|
224
|
+
id = nextDeclarationId++;
|
|
225
|
+
declarationIds.set(key, id);
|
|
226
|
+
}
|
|
227
|
+
return id;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** @param {Rule} rule @param {number} [sourceOrder] */
|
|
231
|
+
function refresh(rule, sourceOrder) {
|
|
232
|
+
const previous = active.get(rule);
|
|
233
|
+
const base = getMeta(rule, ruleMeta);
|
|
234
|
+
const ids = base.declarations.map(getDeclarationId);
|
|
235
|
+
/** @type {ActiveMeta} */
|
|
236
|
+
const meta = Object.assign(base, {
|
|
237
|
+
selectorKey: base.selectors.join(','),
|
|
238
|
+
contentKey: `${base.selectors.join(',')}|${ids.join(',')}`,
|
|
239
|
+
declarationIds: ids,
|
|
240
|
+
declarationIdSet: new Set(ids),
|
|
241
|
+
previous: previous?.previous ?? null,
|
|
242
|
+
next: previous?.next ?? null,
|
|
243
|
+
active: true,
|
|
244
|
+
version: (previous?.version ?? 0) + 1,
|
|
245
|
+
sourceOrder: sourceOrder ?? previous?.sourceOrder ?? nextSourceOrder++,
|
|
246
|
+
});
|
|
247
|
+
active.set(rule, meta);
|
|
248
|
+
return meta;
|
|
249
|
+
}
|
|
250
|
+
/** @param {Rule} first @param {Rule} second */
|
|
251
|
+
function hasPossibleSharedDeclaration(first, second) {
|
|
252
|
+
const a = active.get(first);
|
|
253
|
+
const b = active.get(second);
|
|
254
|
+
if (!a?.active || !b?.active) return false;
|
|
255
|
+
// Empty rules and equivalent at-rule boundaries retain the legacy
|
|
256
|
+
// structural rewrites even though they cannot share a declaration.
|
|
257
|
+
const structuralRewrite =
|
|
258
|
+
(a.declarations.length === 0 && b.declarations.length === 0) ||
|
|
259
|
+
(first.parent !== second.parent && sameParent(first, second));
|
|
260
|
+
if (a.selectorKey === b.selectorKey || structuralRewrite) return true;
|
|
261
|
+
for (const id of a.declarationIds) {
|
|
262
|
+
if (b.declarationIdSet.has(id)) return true;
|
|
263
|
+
}
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
/** @param {Rule} first @param {Rule} second */
|
|
267
|
+
function estimatedBenefit(first, second) {
|
|
268
|
+
const a = active.get(first);
|
|
269
|
+
const b = active.get(second);
|
|
270
|
+
if (!a || !b) return 0;
|
|
271
|
+
if (a.selectorKey === b.selectorKey) {
|
|
272
|
+
return a.declarationIds.length + b.declarationIds.length;
|
|
273
|
+
}
|
|
274
|
+
let benefit = 0;
|
|
275
|
+
for (const id of a.declarationIds) {
|
|
276
|
+
if (b.declarationIdSet.has(id)) benefit++;
|
|
277
|
+
}
|
|
278
|
+
return benefit;
|
|
279
|
+
}
|
|
280
|
+
/** @param {Rule} rule */
|
|
281
|
+
function detach(rule) {
|
|
282
|
+
const meta = active.get(rule);
|
|
283
|
+
if (!meta?.active) return;
|
|
284
|
+
const { previous, next } = meta;
|
|
285
|
+
let container = getParent(rule);
|
|
286
|
+
for (; container; container = getParent(container)) {
|
|
287
|
+
const boundary = boundaries.get(container);
|
|
288
|
+
if (!boundary) continue;
|
|
289
|
+
if (boundary.first === rule) {
|
|
290
|
+
boundary.first = next && isDescendant(next, container) ? next : null;
|
|
291
|
+
}
|
|
292
|
+
if (boundary.last === rule) {
|
|
293
|
+
boundary.last =
|
|
294
|
+
previous && isDescendant(previous, container) ? previous : null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (previous) {
|
|
298
|
+
const previousMeta = active.get(previous);
|
|
299
|
+
if (previousMeta) previousMeta.next = next;
|
|
300
|
+
}
|
|
301
|
+
if (next) {
|
|
302
|
+
const nextMeta = active.get(next);
|
|
303
|
+
if (nextMeta) nextMeta.previous = previous;
|
|
304
|
+
}
|
|
305
|
+
meta.active = false;
|
|
306
|
+
}
|
|
307
|
+
/** @param {Rule[]} rules */
|
|
308
|
+
function captureBoundaries(rules) {
|
|
309
|
+
const captured = new Map();
|
|
310
|
+
for (const rule of rules) {
|
|
311
|
+
for (
|
|
312
|
+
let container = getParent(rule);
|
|
313
|
+
container;
|
|
314
|
+
container = getParent(container)
|
|
597
315
|
) {
|
|
598
|
-
|
|
599
|
-
|
|
316
|
+
const boundary = boundaries.get(container);
|
|
317
|
+
if (boundary && !captured.has(container)) {
|
|
318
|
+
captured.set(container, {
|
|
319
|
+
first: boundary.first,
|
|
320
|
+
last: boundary.last,
|
|
321
|
+
});
|
|
600
322
|
}
|
|
601
|
-
cache = rule;
|
|
602
|
-
return;
|
|
603
323
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
324
|
+
}
|
|
325
|
+
return captured;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Repair the active order for a rule moved by mergeParents. The old and new
|
|
329
|
+
* ancestor paths are the only paths whose first/last descendant can change.
|
|
330
|
+
* @param {Rule} rule
|
|
331
|
+
* @param {import('postcss').Container} oldParent
|
|
332
|
+
* @param {import('postcss').Container} newParent
|
|
333
|
+
*/
|
|
334
|
+
// eslint-disable-next-line complexity
|
|
335
|
+
function repairMove(rule, oldParent, newParent) {
|
|
336
|
+
const oldAncestors = [];
|
|
337
|
+
/** @type {import('postcss').Container<import('postcss').ChildNode> | undefined} */
|
|
338
|
+
let container = oldParent;
|
|
339
|
+
for (; container; container = getParent(container)) {
|
|
340
|
+
oldAncestors.push(container);
|
|
341
|
+
}
|
|
342
|
+
const newAncestors = [];
|
|
343
|
+
container = newParent;
|
|
344
|
+
for (; container; container = getParent(container)) {
|
|
345
|
+
newAncestors.push(container);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const meta = active.get(rule);
|
|
349
|
+
if (!meta?.active) return;
|
|
350
|
+
const { previous, next } = meta;
|
|
351
|
+
if (previous) {
|
|
352
|
+
const previousMeta = active.get(previous);
|
|
353
|
+
if (previousMeta) previousMeta.next = next;
|
|
354
|
+
}
|
|
355
|
+
if (next) {
|
|
356
|
+
const nextMeta = active.get(next);
|
|
357
|
+
if (nextMeta) nextMeta.previous = previous;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
for (const ancestor of oldAncestors) {
|
|
361
|
+
const boundary = boundaries.get(ancestor);
|
|
362
|
+
if (!boundary) continue;
|
|
363
|
+
if (boundary.first === rule) {
|
|
364
|
+
boundary.first = next && isDescendant(next, ancestor) ? next : null;
|
|
365
|
+
}
|
|
366
|
+
if (boundary.last === rule) {
|
|
367
|
+
boundary.last =
|
|
368
|
+
previous && isDescendant(previous, ancestor) ? previous : null;
|
|
609
369
|
}
|
|
370
|
+
}
|
|
610
371
|
|
|
611
|
-
|
|
612
|
-
|
|
372
|
+
const destinationBoundary = boundaries.get(newParent);
|
|
373
|
+
const destinationLast = destinationBoundary?.last ?? null;
|
|
374
|
+
meta.previous = destinationLast;
|
|
375
|
+
meta.next = destinationLast
|
|
376
|
+
? (active.get(destinationLast)?.next ?? null)
|
|
377
|
+
: null;
|
|
378
|
+
if (destinationLast) {
|
|
379
|
+
const destinationMeta = active.get(destinationLast);
|
|
380
|
+
if (destinationMeta) destinationMeta.next = rule;
|
|
381
|
+
}
|
|
382
|
+
if (meta.next) {
|
|
383
|
+
const nextMeta = active.get(meta.next);
|
|
384
|
+
if (nextMeta) nextMeta.previous = rule;
|
|
385
|
+
}
|
|
613
386
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
if (
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
387
|
+
for (const ancestor of newAncestors) {
|
|
388
|
+
const boundary = boundaries.get(ancestor);
|
|
389
|
+
if (!boundary) continue;
|
|
390
|
+
boundary.first ??= rule;
|
|
391
|
+
boundary.last = rule;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/** @param {import('postcss').Container<import('postcss').ChildNode>} container @param {{previous: Rule | null}} state */
|
|
395
|
+
function indexContainer(container, state) {
|
|
396
|
+
/** @type {Rule | null} */
|
|
397
|
+
let first = null;
|
|
398
|
+
/** @type {Rule | null} */
|
|
399
|
+
let last = null;
|
|
400
|
+
for (const node of container.nodes ?? []) {
|
|
401
|
+
if (node.type === 'rule') {
|
|
402
|
+
const rule = /** @type {Rule} */ (node);
|
|
403
|
+
const meta = refresh(rule);
|
|
404
|
+
meta.previous = state.previous;
|
|
405
|
+
if (state.previous) {
|
|
406
|
+
/** @type {ActiveMeta} */ (active.get(state.previous)).next = rule;
|
|
407
|
+
}
|
|
408
|
+
meta.next = null;
|
|
409
|
+
first ??= rule;
|
|
410
|
+
state.previous = rule;
|
|
411
|
+
last = rule;
|
|
412
|
+
indexContainer(rule, state);
|
|
413
|
+
const nested = boundaries.get(rule);
|
|
414
|
+
if (nested?.last) last = nested.last;
|
|
415
|
+
state.previous = last;
|
|
416
|
+
boundaries.set(rule, { first: rule, last });
|
|
417
|
+
continue;
|
|
631
418
|
}
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
419
|
+
if ('nodes' in node && node.nodes) {
|
|
420
|
+
indexContainer(
|
|
421
|
+
/** @type {import('postcss').Container} */ (node),
|
|
422
|
+
state
|
|
423
|
+
);
|
|
424
|
+
const nested = boundaries.get(
|
|
425
|
+
/** @type {import('postcss').Container} */ (node)
|
|
426
|
+
);
|
|
427
|
+
if (nested?.first) {
|
|
428
|
+
if (!first) first = nested.first;
|
|
429
|
+
last = nested.last;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
boundaries.set(container, { first, last });
|
|
434
|
+
}
|
|
435
|
+
/** @param {import('postcss').Root} root */
|
|
436
|
+
function seed(root) {
|
|
437
|
+
// indexContainer establishes links as it descends, then exposes the root
|
|
438
|
+
// entry point used to seed candidates.
|
|
439
|
+
indexContainer(root, { previous: null });
|
|
440
|
+
return boundaries.get(root)?.first ?? null;
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
// Keep the heap scheduling and rewrite repair together for the AST mutation.
|
|
444
|
+
// eslint-disable-next-line complexity
|
|
445
|
+
run(root) {
|
|
446
|
+
/** @typedef {{first: Rule, second: Rule, firstVersion: number, secondVersion: number, benefit: number, firstSourceOrder: number, contentKey: string}} Candidate */
|
|
447
|
+
/** @type {Candidate[]} */
|
|
448
|
+
let candidates = [];
|
|
449
|
+
let needsGlobalReseed = false;
|
|
450
|
+
/** @param {Candidate} a @param {Candidate} b */
|
|
451
|
+
// The comparator stays local with the heap's candidate type and ordering contract.
|
|
452
|
+
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
453
|
+
const comesBefore = (a, b) => {
|
|
454
|
+
if (a.benefit !== b.benefit) return a.benefit > b.benefit;
|
|
455
|
+
if (a.firstSourceOrder !== b.firstSourceOrder) {
|
|
456
|
+
return a.firstSourceOrder < b.firstSourceOrder;
|
|
457
|
+
}
|
|
458
|
+
return a.contentKey < b.contentKey;
|
|
459
|
+
};
|
|
460
|
+
/** @param {Candidate} candidate */
|
|
461
|
+
const pushCandidate = (candidate) => {
|
|
462
|
+
let index = candidates.length;
|
|
463
|
+
candidates.push(candidate);
|
|
464
|
+
while (index > 0) {
|
|
465
|
+
const parent = Math.floor((index - 1) / 2);
|
|
466
|
+
if (comesBefore(candidates[parent], candidate)) break;
|
|
467
|
+
candidates[index] = candidates[parent];
|
|
468
|
+
index = parent;
|
|
469
|
+
}
|
|
470
|
+
candidates[index] = candidate;
|
|
471
|
+
};
|
|
472
|
+
const popCandidate = () => {
|
|
473
|
+
const candidate = candidates[0];
|
|
474
|
+
const last = candidates.pop();
|
|
475
|
+
if (last && candidates.length) {
|
|
476
|
+
let index = 0;
|
|
477
|
+
while (true) {
|
|
478
|
+
const left = index * 2 + 1;
|
|
479
|
+
if (left >= candidates.length) break;
|
|
480
|
+
let child = left;
|
|
481
|
+
const right = left + 1;
|
|
482
|
+
if (
|
|
483
|
+
right < candidates.length &&
|
|
484
|
+
comesBefore(candidates[right], candidates[left])
|
|
485
|
+
)
|
|
486
|
+
child = right;
|
|
487
|
+
if (!comesBefore(candidates[child], last)) break;
|
|
488
|
+
candidates[index] = candidates[child];
|
|
489
|
+
index = child;
|
|
646
490
|
}
|
|
647
|
-
|
|
491
|
+
candidates[index] = last;
|
|
492
|
+
}
|
|
493
|
+
return candidate;
|
|
494
|
+
};
|
|
495
|
+
/** @param {Rule | null} first @param {Rule | null} second */
|
|
496
|
+
const enqueue = (first, second) => {
|
|
497
|
+
if (!first || !second || !hasPossibleSharedDeclaration(first, second)) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const firstMeta = active.get(first);
|
|
501
|
+
const secondMeta = active.get(second);
|
|
502
|
+
if (!firstMeta || !secondMeta) return;
|
|
503
|
+
pushCandidate({
|
|
504
|
+
first,
|
|
505
|
+
second,
|
|
506
|
+
firstVersion: firstMeta.version,
|
|
507
|
+
secondVersion: secondMeta.version,
|
|
508
|
+
benefit: estimatedBenefit(first, second),
|
|
509
|
+
firstSourceOrder: firstMeta.sourceOrder,
|
|
510
|
+
contentKey: `${firstMeta.contentKey}|${secondMeta.contentKey}`,
|
|
648
511
|
});
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
512
|
+
};
|
|
513
|
+
/** @param {Rule} rule */
|
|
514
|
+
const enqueueNeighbors = (rule) => {
|
|
515
|
+
const meta = active.get(rule);
|
|
516
|
+
if (!meta?.active) return;
|
|
517
|
+
enqueue(meta.previous, rule);
|
|
518
|
+
enqueue(rule, meta.next);
|
|
519
|
+
};
|
|
520
|
+
const reseedCandidates = () => {
|
|
521
|
+
candidates = [];
|
|
522
|
+
const initialRule = seed(root);
|
|
523
|
+
for (
|
|
524
|
+
let rule = initialRule;
|
|
525
|
+
rule;
|
|
526
|
+
rule = active.get(rule)?.next ?? null
|
|
527
|
+
) {
|
|
528
|
+
enqueue(rule, active.get(rule)?.next ?? null);
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
reseedCandidates();
|
|
532
|
+
/** @param {Rule} first @param {Rule} second */
|
|
533
|
+
const checkedCanMerge = (first, second) => {
|
|
534
|
+
return canMerge(
|
|
535
|
+
first,
|
|
536
|
+
second,
|
|
537
|
+
browsers,
|
|
538
|
+
compatibilityCache,
|
|
539
|
+
ruleCache,
|
|
540
|
+
ruleMeta
|
|
541
|
+
);
|
|
542
|
+
};
|
|
543
|
+
while (candidates.length || needsGlobalReseed) {
|
|
544
|
+
if (!candidates.length) {
|
|
545
|
+
needsGlobalReseed = false;
|
|
546
|
+
reseedCandidates();
|
|
547
|
+
if (!candidates.length) break;
|
|
548
|
+
}
|
|
549
|
+
const candidate = popCandidate();
|
|
550
|
+
const first = candidate.first;
|
|
551
|
+
const second = candidate.second;
|
|
552
|
+
const firstMeta = active.get(first);
|
|
553
|
+
const secondMeta = active.get(second);
|
|
554
|
+
if (
|
|
555
|
+
!firstMeta?.active ||
|
|
556
|
+
!secondMeta?.active ||
|
|
557
|
+
firstMeta.next !== second ||
|
|
558
|
+
firstMeta.version !== candidate.firstVersion ||
|
|
559
|
+
secondMeta.version !== candidate.secondVersion
|
|
560
|
+
)
|
|
561
|
+
continue;
|
|
562
|
+
if (!checkedCanMerge(first, second)) continue;
|
|
563
|
+
// Equivalent at-rule moves preserve depth-first leaf-rule order.
|
|
564
|
+
const oldParent = second.parent;
|
|
565
|
+
const newParent = first.parent;
|
|
566
|
+
const moved = mergeParents(first, second);
|
|
567
|
+
if (moved && oldParent && newParent) {
|
|
568
|
+
repairMove(second, oldParent, newParent);
|
|
569
|
+
}
|
|
570
|
+
if (
|
|
571
|
+
sameDeclarationsAndOrder(
|
|
572
|
+
getMeta(second, ruleMeta).declarations,
|
|
573
|
+
getMeta(first, ruleMeta).declarations
|
|
574
|
+
)
|
|
575
|
+
) {
|
|
576
|
+
const metaSecond = getMeta(second, ruleMeta);
|
|
577
|
+
metaSecond.selectors = [
|
|
578
|
+
...getMeta(first, ruleMeta).selectors,
|
|
579
|
+
...metaSecond.selectors,
|
|
580
|
+
];
|
|
581
|
+
metaSecond.dirty = true;
|
|
582
|
+
flush(second, ruleMeta);
|
|
583
|
+
detach(first);
|
|
584
|
+
first.remove();
|
|
585
|
+
ruleMeta?.delete(first);
|
|
586
|
+
refresh(second);
|
|
587
|
+
ruleCache?.add(second);
|
|
588
|
+
enqueueNeighbors(second);
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
if (
|
|
592
|
+
getMeta(first, ruleMeta).selectors.join(',') ===
|
|
593
|
+
getMeta(second, ruleMeta).selectors.join(',')
|
|
594
|
+
) {
|
|
595
|
+
const cachedDecls = getMeta(first, ruleMeta).declarations;
|
|
596
|
+
second.walk((node) => {
|
|
597
|
+
if (
|
|
598
|
+
node.type === 'decl' &&
|
|
599
|
+
indexOfDeclaration(cachedDecls, node) !== -1
|
|
600
|
+
)
|
|
601
|
+
node.remove();
|
|
602
|
+
else first.append(node);
|
|
603
|
+
});
|
|
604
|
+
getMeta(first, ruleMeta).declarations = getDecls(first);
|
|
605
|
+
detach(second);
|
|
606
|
+
second.remove();
|
|
607
|
+
ruleMeta?.delete(second);
|
|
608
|
+
refresh(first);
|
|
609
|
+
enqueueNeighbors(first);
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
const replacedRules = [first, second];
|
|
613
|
+
const capturedBoundaries = captureBoundaries(replacedRules);
|
|
614
|
+
const outcome = partialMerge(
|
|
615
|
+
first,
|
|
616
|
+
second,
|
|
617
|
+
browsers,
|
|
618
|
+
compatibilityCache,
|
|
619
|
+
ruleCache,
|
|
620
|
+
ruleMeta,
|
|
621
|
+
(rule, movedFrom, movedTo) => repairMove(rule, movedFrom, movedTo)
|
|
622
|
+
);
|
|
623
|
+
if (outcome.replacements.length) {
|
|
624
|
+
const firstWasBoundary =
|
|
625
|
+
!outcome.moved &&
|
|
626
|
+
outcome.replaced.some((rule) =>
|
|
627
|
+
[...capturedBoundaries.values()].some((b) => b.first === rule)
|
|
628
|
+
);
|
|
629
|
+
const lastWasBoundary =
|
|
630
|
+
!outcome.moved &&
|
|
631
|
+
outcome.replaced.some((rule) =>
|
|
632
|
+
[...capturedBoundaries.values()].some((b) => b.last === rule)
|
|
633
|
+
);
|
|
634
|
+
const previous = active.get(outcome.replaced[0])?.previous ?? null;
|
|
635
|
+
const lastReplaced = /** @type {Rule} */ (outcome.replaced.at(-1));
|
|
636
|
+
const next = active.get(lastReplaced)?.next ?? null;
|
|
637
|
+
const sourceOrder = active.get(outcome.replaced[0])?.sourceOrder;
|
|
638
|
+
for (const rule of outcome.replaced) {
|
|
639
|
+
detach(rule);
|
|
640
|
+
ruleMeta?.delete(rule);
|
|
641
|
+
}
|
|
642
|
+
let prior = previous;
|
|
643
|
+
for (const replacement of outcome.replacements) {
|
|
644
|
+
const meta = refresh(replacement, sourceOrder);
|
|
645
|
+
meta.previous = prior;
|
|
646
|
+
if (prior) {
|
|
647
|
+
/** @type {ActiveMeta} */ (active.get(prior)).next = replacement;
|
|
648
|
+
}
|
|
649
|
+
prior = replacement;
|
|
650
|
+
}
|
|
651
|
+
if (prior) {
|
|
652
|
+
/** @type {ActiveMeta} */ (active.get(prior)).next = next;
|
|
653
|
+
}
|
|
654
|
+
if (next) {
|
|
655
|
+
/** @type {ActiveMeta} */ (active.get(next)).previous = prior;
|
|
656
|
+
}
|
|
657
|
+
if (firstWasBoundary && outcome.replacements[0]?.parent) {
|
|
658
|
+
for (
|
|
659
|
+
let container = getParent(outcome.replacements[0]);
|
|
660
|
+
container;
|
|
661
|
+
container = getParent(container)
|
|
662
|
+
) {
|
|
663
|
+
const boundary = boundaries.get(container);
|
|
664
|
+
if (boundary) boundary.first = outcome.replacements[0];
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
const lastReplacement = /** @type {Rule} */ (
|
|
668
|
+
outcome.replacements.at(-1)
|
|
669
|
+
);
|
|
670
|
+
if (lastWasBoundary && lastReplacement.parent) {
|
|
671
|
+
for (
|
|
672
|
+
let container = getParent(lastReplacement);
|
|
673
|
+
container;
|
|
674
|
+
container = getParent(container)
|
|
675
|
+
) {
|
|
676
|
+
const boundary = boundaries.get(container);
|
|
677
|
+
if (boundary) boundary.last = lastReplacement;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (outcome.moved) {
|
|
681
|
+
// repairMove keeps the current queue safe to drain. Rebuild once
|
|
682
|
+
// afterwards so a cascade of independent moves does not rescan
|
|
683
|
+
// the complete rule list after every boundary change.
|
|
684
|
+
needsGlobalReseed = true;
|
|
685
|
+
}
|
|
686
|
+
for (const replacement of outcome.replacements) {
|
|
687
|
+
enqueueNeighbors(replacement);
|
|
688
|
+
}
|
|
689
|
+
enqueue(previous, outcome.replacements[0] ?? next);
|
|
690
|
+
enqueue(outcome.replacements.at(-1) ?? previous, next);
|
|
691
|
+
} else if (moved || outcome.moved) {
|
|
692
|
+
for (const changed of [first, second, ...outcome.changed]) {
|
|
693
|
+
refresh(changed);
|
|
694
|
+
enqueueNeighbors(changed);
|
|
695
|
+
}
|
|
696
|
+
needsGlobalReseed = true;
|
|
697
|
+
}
|
|
669
698
|
}
|
|
699
|
+
root.walkRules((rule) => flush(rule, ruleMeta));
|
|
670
700
|
},
|
|
671
701
|
};
|
|
672
702
|
}
|
|
@@ -703,21 +733,20 @@ function pluginCreator(opts = {}) {
|
|
|
703
733
|
* @param {import('postcss').Root} css
|
|
704
734
|
*/
|
|
705
735
|
OnceExit(css) {
|
|
706
|
-
const
|
|
736
|
+
const merger = selectorMerger(
|
|
707
737
|
browsers,
|
|
708
738
|
compatibilityCache,
|
|
709
739
|
ruleCache,
|
|
710
740
|
ruleMeta
|
|
711
741
|
);
|
|
712
|
-
|
|
713
|
-
clean();
|
|
742
|
+
merger.run(css);
|
|
714
743
|
},
|
|
715
744
|
};
|
|
716
745
|
},
|
|
717
746
|
};
|
|
718
747
|
}
|
|
719
|
-
|
|
748
|
+
/** @type {true} */
|
|
720
749
|
pluginCreator.postcss = true;
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
750
|
+
const moduleExports = pluginCreator;
|
|
751
|
+
|
|
752
|
+
export { moduleExports as default, moduleExports as 'module.exports' };
|