postcss-merge-rules 9.0.0 → 9.0.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/README.md +1 -1
- package/package.json +8 -5
- package/src/index.js +536 -505
- package/src/lib/declarations.js +146 -0
- package/src/lib/rule-meta.js +58 -0
- package/src/lib/rule-rewrite.js +170 -0
- package/types/index.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/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
|
@@ -5,67 +5,28 @@ import {
|
|
|
5
5
|
sameVendor,
|
|
6
6
|
noVendor,
|
|
7
7
|
} from './lib/ensureCompatibility.js';
|
|
8
|
-
import {
|
|
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';
|
|
9
20
|
|
|
10
21
|
/** @import browserslist from 'browserslist' */
|
|
11
22
|
const { sameParent } = cssnanoUtils;
|
|
12
23
|
/** @import {Declaration, Rule} from 'postcss' */
|
|
13
24
|
/**
|
|
14
|
-
* @param {Declaration} a
|
|
15
|
-
* @param {Declaration} b
|
|
16
|
-
* @return {boolean}
|
|
17
|
-
*/
|
|
18
|
-
function declarationIsEqual(a, b) {
|
|
19
|
-
return (
|
|
20
|
-
a.important === b.important && a.prop === b.prop && a.value === b.value
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* @param {Declaration[]} array
|
|
26
|
-
* @param {Declaration} decl
|
|
27
|
-
* @return {number}
|
|
28
|
-
*/
|
|
29
|
-
function indexOfDeclaration(array, decl) {
|
|
30
|
-
return array.findIndex((d) => declarationIsEqual(d, decl));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Returns filtered array of matched or unmatched declarations
|
|
35
|
-
* @param {Declaration[]} a
|
|
36
|
-
* @param {Declaration[]} b
|
|
37
|
-
* @param {boolean} [not=false]
|
|
38
|
-
* @return {Declaration[]}
|
|
39
|
-
*/
|
|
40
|
-
function intersect(a, b, not) {
|
|
41
|
-
return a.filter((c) => {
|
|
42
|
-
const index = indexOfDeclaration(b, c) !== -1;
|
|
43
|
-
return not ? !index : index;
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* @param {Declaration[]} a
|
|
49
|
-
* @param {Declaration[]} b
|
|
50
|
-
* @return {boolean}
|
|
51
|
-
*/
|
|
52
|
-
function sameDeclarationsAndOrder(a, b) {
|
|
53
|
-
if (a.length !== b.length) {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
return a.every((d, index) => declarationIsEqual(d, b[index]));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* RuleMeta stores metadata about a `Rule` during the merging process.
|
|
61
|
-
* It tracks selectors and declarations without re-parsing the AST many times.
|
|
62
|
-
*
|
|
63
25
|
* @typedef {Object} RuleMeta
|
|
64
26
|
* @property {string[]} selectors - Array of selector strings for the rule
|
|
65
27
|
* @property {Declaration[]} declarations - Array of declaration nodes for the rule
|
|
66
28
|
* @property {boolean} dirty - Whether the selectors have been modified and need flushing
|
|
67
29
|
*/
|
|
68
|
-
|
|
69
30
|
/**
|
|
70
31
|
* @param {Rule} ruleA
|
|
71
32
|
* @param {Rule} ruleB
|
|
@@ -128,378 +89,24 @@ function canMerge(
|
|
|
128
89
|
function isRuleOrAtRule(node) {
|
|
129
90
|
return node.type === 'rule' || node.type === 'atrule';
|
|
130
91
|
}
|
|
131
|
-
/**
|
|
132
|
-
* @param {import('postcss').ChildNode} node
|
|
133
|
-
* @return {node is Declaration}
|
|
134
|
-
*/
|
|
135
|
-
function isDeclaration(node) {
|
|
136
|
-
return node.type === 'decl';
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Retrieves or initializes virtual metadata for a PostCSS rule.
|
|
141
|
-
*
|
|
142
|
-
* This metadata caches selectors and declarations to avoid expensive AST
|
|
143
|
-
* re-parsing, especially for the selectors.
|
|
144
|
-
*
|
|
145
|
-
* @param {Rule} rule The PostCSS rule to get metadata for.
|
|
146
|
-
* @param {WeakMap<Rule, RuleMeta>} [ruleMeta] The metadata cache.
|
|
147
|
-
* @return {RuleMeta} The rule's virtual metadata.
|
|
148
|
-
*/
|
|
149
|
-
function getMeta(rule, ruleMeta) {
|
|
150
|
-
if (ruleMeta && rule) {
|
|
151
|
-
let meta = ruleMeta.get(rule);
|
|
152
|
-
if (!meta && rule.nodes) {
|
|
153
|
-
meta = {
|
|
154
|
-
selectors: rule.selectors,
|
|
155
|
-
declarations: rule.nodes.filter(isDeclaration),
|
|
156
|
-
dirty: false,
|
|
157
|
-
};
|
|
158
|
-
ruleMeta.set(rule, meta);
|
|
159
|
-
}
|
|
160
|
-
return meta ?? { selectors: [], declarations: [], dirty: false };
|
|
161
|
-
}
|
|
162
|
-
return {
|
|
163
|
-
selectors: rule?.selectors ?? [],
|
|
164
|
-
declarations: rule?.nodes?.filter(isDeclaration) ?? [],
|
|
165
|
-
dirty: false,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Commits virtual metadata changes back to the actual PostCSS rule.
|
|
171
|
-
*
|
|
172
|
-
* @param {Rule} rule The PostCSS rule to flush.
|
|
173
|
-
* @param {WeakMap<Rule, RuleMeta>} ruleMeta The metadata cache.
|
|
174
|
-
*/
|
|
175
|
-
function flush(rule, ruleMeta) {
|
|
176
|
-
const meta = ruleMeta.get(rule);
|
|
177
|
-
if (meta && meta.dirty) {
|
|
178
|
-
rule.selector = meta.selectors.join(',');
|
|
179
|
-
meta.dirty = false;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* @param {Rule} rule
|
|
185
|
-
* @return {Declaration[]}
|
|
186
|
-
*/
|
|
187
|
-
function getDecls(rule) {
|
|
188
|
-
return rule.nodes.filter(isDeclaration);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* @param {...Rule} rules
|
|
193
|
-
* @return {number}
|
|
194
|
-
*/
|
|
195
|
-
function ruleLength(...rules) {
|
|
196
|
-
return rules.map((r) => (r.nodes.length ? String(r) : '')).join('').length;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* @param {Rule} first
|
|
201
|
-
* @param {Rule} second
|
|
202
|
-
* @return {boolean} merged
|
|
203
|
-
*/
|
|
204
|
-
function mergeParents(first, second) {
|
|
205
|
-
// Null check for detached rules
|
|
206
|
-
if (!first.parent || !second.parent) {
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Check if parents share node
|
|
211
|
-
if (first.parent === second.parent) {
|
|
212
|
-
return false;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// sameParent() already called by canMerge()
|
|
216
|
-
|
|
217
|
-
second.remove();
|
|
218
|
-
first.parent.append(second);
|
|
219
|
-
return true;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* @param {Rule} second
|
|
224
|
-
* @return {Rule | null}
|
|
225
|
-
*/
|
|
226
|
-
function getNextRule(second) {
|
|
227
|
-
let nextRule = second.next();
|
|
228
|
-
if (!nextRule) {
|
|
229
|
-
// Grab next cousin
|
|
230
|
-
const parentSibling =
|
|
231
|
-
/** @type {import('postcss').Container | undefined} */ (
|
|
232
|
-
/** @type {import('postcss').Container<import('postcss').ChildNode>} */ (
|
|
233
|
-
second.parent
|
|
234
|
-
).next()
|
|
235
|
-
);
|
|
236
|
-
nextRule = parentSibling && parentSibling.nodes && parentSibling.nodes[0];
|
|
237
|
-
}
|
|
238
|
-
return nextRule?.type === 'rule' ? nextRule : null;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* @param {Rule} first
|
|
243
|
-
* @param {Rule} second
|
|
244
|
-
* @param {Declaration[]} intersection
|
|
245
|
-
* @param {string[]} browsers
|
|
246
|
-
* @param {Map<string, boolean>} compatibilityCache
|
|
247
|
-
* @param {WeakSet<Rule>} ruleCache
|
|
248
|
-
* @param {WeakMap<Rule, RuleMeta>} ruleMeta
|
|
249
|
-
* @return {{first: Rule, second: Rule, intersection: Declaration[]}}
|
|
250
|
-
*/
|
|
251
|
-
function mergeWithNextRule(
|
|
252
|
-
first,
|
|
253
|
-
second,
|
|
254
|
-
intersection,
|
|
255
|
-
browsers,
|
|
256
|
-
compatibilityCache,
|
|
257
|
-
ruleCache,
|
|
258
|
-
ruleMeta
|
|
259
|
-
) {
|
|
260
|
-
const nextRule = getNextRule(second);
|
|
261
|
-
if (
|
|
262
|
-
!nextRule ||
|
|
263
|
-
!canMerge(
|
|
264
|
-
second,
|
|
265
|
-
nextRule,
|
|
266
|
-
browsers,
|
|
267
|
-
compatibilityCache,
|
|
268
|
-
ruleCache,
|
|
269
|
-
ruleMeta
|
|
270
|
-
)
|
|
271
|
-
) {
|
|
272
|
-
return { first, second, intersection };
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const nextIntersection = intersect(
|
|
276
|
-
getMeta(second, ruleMeta).declarations,
|
|
277
|
-
getMeta(nextRule, ruleMeta).declarations
|
|
278
|
-
);
|
|
279
|
-
if (nextIntersection.length <= intersection.length) {
|
|
280
|
-
return { first, second, intersection };
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
mergeParents(second, nextRule);
|
|
284
|
-
return { first: second, second: nextRule, intersection: nextIntersection };
|
|
285
|
-
}
|
|
286
92
|
|
|
287
93
|
/**
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* hoisted, in the same relative order, so declarations move together.
|
|
291
|
-
*
|
|
292
|
-
* @param {Declaration} candidate
|
|
293
|
-
* @param {number} candidateIndex
|
|
294
|
-
* @param {Declaration[]} hoistCandidates
|
|
295
|
-
* @param {Declaration[]} earlierRuleDeclarations
|
|
296
|
-
* @return {boolean}
|
|
94
|
+
* @param {Rule | import('postcss').Container<import('postcss').ChildNode>} node
|
|
95
|
+
* @return {import('postcss').Container<import('postcss').ChildNode> | undefined}
|
|
297
96
|
*/
|
|
298
|
-
function
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
hoistCandidates,
|
|
302
|
-
earlierRuleDeclarations
|
|
303
|
-
) {
|
|
304
|
-
const indexInEarlierRule = indexOfDeclaration(
|
|
305
|
-
earlierRuleDeclarations,
|
|
306
|
-
candidate
|
|
307
|
-
);
|
|
308
|
-
const overridesInEarlierRule = earlierRuleDeclarations
|
|
309
|
-
.slice(indexInEarlierRule + 1)
|
|
310
|
-
.filter((d) => isConflictingProp(d.prop, candidate.prop));
|
|
311
|
-
if (overridesInEarlierRule.length === 0) {
|
|
312
|
-
return true;
|
|
313
|
-
}
|
|
314
|
-
const overridesAmongCandidates = hoistCandidates
|
|
315
|
-
.slice(candidateIndex + 1)
|
|
316
|
-
.filter((d) => isConflictingProp(d.prop, candidate.prop));
|
|
317
|
-
if (overridesInEarlierRule.length !== overridesAmongCandidates.length) {
|
|
318
|
-
return false;
|
|
319
|
-
}
|
|
320
|
-
return overridesInEarlierRule.every((d, index) =>
|
|
321
|
-
declarationIsEqual(d, overridesAmongCandidates[index])
|
|
97
|
+
function getParent(node) {
|
|
98
|
+
return /** @type {import('postcss').Container<import('postcss').ChildNode> | undefined} */ (
|
|
99
|
+
node.parent
|
|
322
100
|
);
|
|
323
101
|
}
|
|
324
102
|
|
|
325
|
-
/**
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
*
|
|
331
|
-
* @param {Declaration} candidate
|
|
332
|
-
* @param {Declaration[]} laterDeclarations
|
|
333
|
-
* @param {Set<number>} claimedIndices
|
|
334
|
-
* @return {boolean}
|
|
335
|
-
*/
|
|
336
|
-
function claimMatchInLaterRule(candidate, laterDeclarations, claimedIndices) {
|
|
337
|
-
const matchIndex = laterDeclarations.findIndex(
|
|
338
|
-
(d, index) =>
|
|
339
|
-
!claimedIndices.has(index) && isConflictingProp(d.prop, candidate.prop)
|
|
340
|
-
);
|
|
341
|
-
if (matchIndex === -1) {
|
|
342
|
-
return false;
|
|
343
|
-
}
|
|
344
|
-
if (!declarationIsEqual(laterDeclarations[matchIndex], candidate)) {
|
|
345
|
-
return false;
|
|
346
|
-
}
|
|
347
|
-
if (
|
|
348
|
-
candidate.prop.toLowerCase() !== 'direction' &&
|
|
349
|
-
candidate.prop.toLowerCase() !== 'unicode-bidi' &&
|
|
350
|
-
laterDeclarations.some(
|
|
351
|
-
(declaration) => declaration.prop.toLowerCase() === 'all'
|
|
352
|
-
)
|
|
353
|
-
) {
|
|
354
|
-
return false;
|
|
355
|
-
}
|
|
356
|
-
claimedIndices.add(matchIndex);
|
|
357
|
-
return true;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Narrows the declarations shared by two adjacent rules down to those that can
|
|
362
|
-
* safely move into a merged rule, along with the indices of the later rule's
|
|
363
|
-
* declarations they claim.
|
|
364
|
-
*
|
|
365
|
-
* The merged rule is emitted after the earlier rule's remaining declarations,
|
|
366
|
-
* so hoisting a declaration that a later declaration in the earlier rule
|
|
367
|
-
* overrides would reverse the cascade and make the previously dead value
|
|
368
|
-
* effective again, unless that overriding declaration is hoisted too, so both
|
|
369
|
-
* move together and keep their relative order.
|
|
370
|
-
*
|
|
371
|
-
* Dropping a candidate can invalidate one that was only admitted because it
|
|
372
|
-
* travelled with it, so the pass repeats until the surviving set stops
|
|
373
|
-
* shrinking.
|
|
374
|
-
*
|
|
375
|
-
* @param {Declaration[]} hoistCandidates
|
|
376
|
-
* @param {Declaration[]} earlierRuleDeclarations
|
|
377
|
-
* @param {Declaration[]} laterRuleDeclarations
|
|
378
|
-
* @return {{intersection: Declaration[], claimedIndices: Set<number>}}
|
|
379
|
-
*/
|
|
380
|
-
function filterRuleIntersections(
|
|
381
|
-
hoistCandidates,
|
|
382
|
-
earlierRuleDeclarations,
|
|
383
|
-
laterRuleDeclarations
|
|
384
|
-
) {
|
|
385
|
-
let remainingCandidates = hoistCandidates;
|
|
386
|
-
for (;;) {
|
|
387
|
-
// A candidate rejected by the override check never claims a match.
|
|
388
|
-
const claimedIndices = new Set();
|
|
389
|
-
const survivors = remainingCandidates.filter(
|
|
390
|
-
(candidate, candidateIndex) =>
|
|
391
|
-
hoistingPreservesOverrideOrder(
|
|
392
|
-
candidate,
|
|
393
|
-
candidateIndex,
|
|
394
|
-
remainingCandidates,
|
|
395
|
-
earlierRuleDeclarations
|
|
396
|
-
) &&
|
|
397
|
-
claimMatchInLaterRule(candidate, laterRuleDeclarations, claimedIndices)
|
|
398
|
-
);
|
|
399
|
-
if (
|
|
400
|
-
survivors.length === remainingCandidates.length ||
|
|
401
|
-
survivors.length === 0
|
|
402
|
-
) {
|
|
403
|
-
return { intersection: survivors, claimedIndices };
|
|
404
|
-
}
|
|
405
|
-
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;
|
|
406
108
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
/**
|
|
410
|
-
* @param {Rule} first
|
|
411
|
-
* @param {Rule} second
|
|
412
|
-
* @param {Declaration[]} intersection
|
|
413
|
-
* @param {Set<number>} claimedIndices Positions of the declarations the
|
|
414
|
-
* intersection claimed in `second`, which the merged rule replaces.
|
|
415
|
-
* @param {WeakSet<Rule>} ruleCache
|
|
416
|
-
* @param {WeakMap<Rule, RuleMeta>} ruleMeta
|
|
417
|
-
* @return {Rule}
|
|
418
|
-
*/
|
|
419
|
-
function buildMergedRule(
|
|
420
|
-
first,
|
|
421
|
-
second,
|
|
422
|
-
intersection,
|
|
423
|
-
claimedIndices,
|
|
424
|
-
ruleCache,
|
|
425
|
-
ruleMeta
|
|
426
|
-
) {
|
|
427
|
-
const receivingBlock = second.clone();
|
|
428
|
-
const firstSelectors = getMeta(first, ruleMeta).selectors;
|
|
429
|
-
const secondSelectors = getMeta(second, ruleMeta).selectors;
|
|
430
|
-
|
|
431
|
-
receivingBlock.selector = [...firstSelectors, ...secondSelectors].join();
|
|
432
|
-
receivingBlock.nodes = [];
|
|
433
|
-
|
|
434
|
-
/** @type {import('postcss').Container<import('postcss').ChildNode>} */ (
|
|
435
|
-
second.parent
|
|
436
|
-
).insertBefore(second, receivingBlock);
|
|
437
|
-
|
|
438
|
-
const firstClone = first.clone({ selectors: firstSelectors });
|
|
439
|
-
const secondClone = second.clone({ selectors: secondSelectors });
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* @param {(decl: Declaration) => void} callback
|
|
443
|
-
* @this void
|
|
444
|
-
* @return {(decl: Declaration) => void}
|
|
445
|
-
*/
|
|
446
|
-
function moveDecl(callback) {
|
|
447
|
-
return (decl) => {
|
|
448
|
-
if (indexOfDeclaration(intersection, decl) !== -1) {
|
|
449
|
-
callback.call(this, decl);
|
|
450
|
-
}
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
firstClone.walkDecls(
|
|
454
|
-
moveDecl(
|
|
455
|
-
/**
|
|
456
|
-
* @param {Declaration} decl
|
|
457
|
-
*/
|
|
458
|
-
(decl) => {
|
|
459
|
-
decl.remove();
|
|
460
|
-
receivingBlock.append(decl);
|
|
461
|
-
}
|
|
462
|
-
)
|
|
463
|
-
);
|
|
464
|
-
// Remove exactly the declarations that were claimed: matching by value would
|
|
465
|
-
// also delete a duplicate that re-asserts the value later in the rule, after
|
|
466
|
-
// an overriding declaration that stays behind.
|
|
467
|
-
let laterIndex = 0;
|
|
468
|
-
secondClone.walkDecls((decl) => {
|
|
469
|
-
if (claimedIndices.has(laterIndex++)) {
|
|
470
|
-
decl.remove();
|
|
471
|
-
}
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
// Ensure original rules are flushed for accurate length comparison
|
|
475
|
-
if (ruleMeta) {
|
|
476
|
-
flush(first, ruleMeta);
|
|
477
|
-
flush(second, ruleMeta);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
const merged = ruleLength(firstClone, receivingBlock, secondClone);
|
|
481
|
-
const original = ruleLength(first, second);
|
|
482
|
-
if (merged < original) {
|
|
483
|
-
first.replaceWith(firstClone);
|
|
484
|
-
second.replaceWith(secondClone);
|
|
485
|
-
for (const r of [firstClone, receivingBlock, secondClone]) {
|
|
486
|
-
if (r.nodes.length === 0) {
|
|
487
|
-
r.remove();
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
if (!secondClone.parent) {
|
|
491
|
-
ruleCache?.add(receivingBlock);
|
|
492
|
-
return receivingBlock;
|
|
493
|
-
}
|
|
494
|
-
ruleCache?.add(receivingBlock);
|
|
495
|
-
ruleCache?.add(secondClone);
|
|
496
|
-
ruleMeta?.delete(first);
|
|
497
|
-
ruleMeta?.delete(second);
|
|
498
|
-
return secondClone;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
receivingBlock.remove();
|
|
502
|
-
return second;
|
|
109
|
+
return false;
|
|
503
110
|
}
|
|
504
111
|
|
|
505
112
|
/**
|
|
@@ -509,7 +116,8 @@ function buildMergedRule(
|
|
|
509
116
|
* @param {Map<string, boolean>} compatibilityCache
|
|
510
117
|
* @param {WeakSet<Rule>} ruleCache
|
|
511
118
|
* @param {WeakMap<Rule, RuleMeta>} ruleMeta
|
|
512
|
-
* @
|
|
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}}
|
|
513
121
|
*/
|
|
514
122
|
function partialMerge(
|
|
515
123
|
first,
|
|
@@ -517,7 +125,8 @@ function partialMerge(
|
|
|
517
125
|
browsers,
|
|
518
126
|
compatibilityCache,
|
|
519
127
|
ruleCache,
|
|
520
|
-
ruleMeta
|
|
128
|
+
ruleMeta,
|
|
129
|
+
onMove
|
|
521
130
|
) {
|
|
522
131
|
if (ruleMeta) {
|
|
523
132
|
flush(first, ruleMeta);
|
|
@@ -526,7 +135,13 @@ function partialMerge(
|
|
|
526
135
|
const metaSecond = getMeta(second, ruleMeta);
|
|
527
136
|
let intersection = intersect(metaFirst.declarations, metaSecond.declarations);
|
|
528
137
|
if (intersection.length === 0) {
|
|
529
|
-
return
|
|
138
|
+
return {
|
|
139
|
+
rule: second,
|
|
140
|
+
replacements: [],
|
|
141
|
+
replaced: [],
|
|
142
|
+
changed: [],
|
|
143
|
+
moved: false,
|
|
144
|
+
};
|
|
530
145
|
}
|
|
531
146
|
const mergedNext = mergeWithNextRule(
|
|
532
147
|
first,
|
|
@@ -535,7 +150,9 @@ function partialMerge(
|
|
|
535
150
|
browsers,
|
|
536
151
|
compatibilityCache,
|
|
537
152
|
ruleCache,
|
|
538
|
-
ruleMeta
|
|
153
|
+
ruleMeta,
|
|
154
|
+
canMerge,
|
|
155
|
+
onMove
|
|
539
156
|
);
|
|
540
157
|
const mergedFirst = mergedNext.first;
|
|
541
158
|
const mergedSecond = mergedNext.second;
|
|
@@ -555,10 +172,16 @@ function partialMerge(
|
|
|
555
172
|
|
|
556
173
|
if (intersection.length === 0) {
|
|
557
174
|
// Nothing to merge
|
|
558
|
-
return
|
|
175
|
+
return {
|
|
176
|
+
rule: mergedSecond,
|
|
177
|
+
replacements: [],
|
|
178
|
+
replaced: [],
|
|
179
|
+
changed: mergedNext.moved ? [mergedFirst, mergedSecond] : [],
|
|
180
|
+
moved: mergedNext.moved,
|
|
181
|
+
};
|
|
559
182
|
}
|
|
560
183
|
|
|
561
|
-
|
|
184
|
+
const merged = buildMergedRule(
|
|
562
185
|
mergedFirst,
|
|
563
186
|
mergedSecond,
|
|
564
187
|
intersection,
|
|
@@ -566,6 +189,12 @@ function partialMerge(
|
|
|
566
189
|
ruleCache,
|
|
567
190
|
ruleMeta
|
|
568
191
|
);
|
|
192
|
+
return {
|
|
193
|
+
...merged,
|
|
194
|
+
replaced: merged.replacements.length ? [mergedFirst, mergedSecond] : [],
|
|
195
|
+
changed: mergedNext.moved ? [mergedFirst, mergedSecond] : [],
|
|
196
|
+
moved: mergedNext.moved,
|
|
197
|
+
};
|
|
569
198
|
}
|
|
570
199
|
|
|
571
200
|
/**
|
|
@@ -573,98 +202,501 @@ function partialMerge(
|
|
|
573
202
|
* @param {Map<string, boolean>} compatibilityCache
|
|
574
203
|
* @param {WeakSet<Rule>} ruleCache
|
|
575
204
|
* @param {WeakMap<Rule, RuleMeta>} ruleMeta
|
|
576
|
-
* @return {{
|
|
205
|
+
* @return {{ run: (root: import('postcss').Root) => void }}
|
|
577
206
|
*/
|
|
578
207
|
function selectorMerger(browsers, compatibilityCache, ruleCache, ruleMeta) {
|
|
579
|
-
/** @
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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)
|
|
595
315
|
) {
|
|
596
|
-
|
|
597
|
-
|
|
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
|
+
});
|
|
598
322
|
}
|
|
599
|
-
cache = rule;
|
|
600
|
-
return;
|
|
601
323
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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;
|
|
607
365
|
}
|
|
366
|
+
if (boundary.last === rule) {
|
|
367
|
+
boundary.last =
|
|
368
|
+
previous && isDescendant(previous, ancestor) ? previous : null;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
608
371
|
|
|
609
|
-
|
|
610
|
-
|
|
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
|
+
}
|
|
611
386
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
if (
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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;
|
|
629
418
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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;
|
|
644
490
|
}
|
|
645
|
-
|
|
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}`,
|
|
646
511
|
});
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
+
}
|
|
667
698
|
}
|
|
699
|
+
root.walkRules((rule) => flush(rule, ruleMeta));
|
|
668
700
|
},
|
|
669
701
|
};
|
|
670
702
|
}
|
|
@@ -701,14 +733,13 @@ function pluginCreator(opts = {}) {
|
|
|
701
733
|
* @param {import('postcss').Root} css
|
|
702
734
|
*/
|
|
703
735
|
OnceExit(css) {
|
|
704
|
-
const
|
|
736
|
+
const merger = selectorMerger(
|
|
705
737
|
browsers,
|
|
706
738
|
compatibilityCache,
|
|
707
739
|
ruleCache,
|
|
708
740
|
ruleMeta
|
|
709
741
|
);
|
|
710
|
-
|
|
711
|
-
clean();
|
|
742
|
+
merger.run(css);
|
|
712
743
|
},
|
|
713
744
|
};
|
|
714
745
|
},
|