Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/merge-nested-at-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@compiled/css': patch
---

Merge duplicate children in nested at-rules.
108 changes: 108 additions & 0 deletions packages/css/src/plugins/__tests__/merge-duplicate-at-rules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,112 @@ describe('discard duplicate at-rule children plugin', () => {
"
`);
});

it('should move merged top-level at-rules after regular rules', () => {
Comment thread
KSHITIZ6341 marked this conversation as resolved.
const actual = transform`
@media (min-width:500px) { color: red; }
.default { color: blue; }
@media (min-width:500px) { background: white; }
`;

expect(actual).toMatchInlineSnapshot(`
"
.default { color: blue; }
@media (min-width:500px) { color: red; background: white; }
"
`);
});

it('should remove duplicate children from nested at-rules', () => {
const actual = transform`
@supports not (height: 1lh) {
@media (min-width:500px) {
color: red;
color: green;
}
}

@supports not (height: 1lh) {
@media (min-width:500px) {
color: red;
color: blue;
}
}
`;

expect(actual).toMatchInlineSnapshot(`
"
@supports not (height: 1lh) {
@media (min-width:500px) {
color: red;
color: green;
color: blue;
}
}
"
`);
});

it('should keep nested at-rules scoped to their parent conditions', () => {
const actual = transform`
@supports (display: grid) {
@media (min-width:500px) {
color: red;
}
}

@supports (display: flex) {
@media (min-width:500px) {
color: blue;
}
}
`;

expect(actual).toMatchInlineSnapshot(`
"
@supports (display: grid) {
@media (min-width:500px) {
color: red;
}
}
@supports (display: flex) {
@media (min-width:500px) {
color: blue;
}
}
"
`);
});

it('should preserve nested non-atomic at-rules', () => {
const actual = transform`
@supports (display: grid) {
@media (min-width:500px) {
.cc-first { color: red; }
}
}

@supports (display: grid) {
@media (min-width:500px) {
.cc-second { color: blue; }
}
}
`;

expect(actual).toMatchInlineSnapshot(`
"
@supports (display: grid) {
@media (min-width:500px) {
.cc-first { color: red; }
}
}

@supports (display: grid) {
@media (min-width:500px) {
.cc-second { color: blue; }
}
}
"
`);
});
});
81 changes: 45 additions & 36 deletions packages/css/src/plugins/merge-duplicate-at-rules.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import type { AtRule, ChildNode, Plugin } from 'postcss';
import type { AtRule, Container, Plugin } from 'postcss';

import { isNonAtomicNode } from '../utils/non-atomic';

/**
* Plugin to remove duplicate children found in at-rules.
* Currently does not handle nested at-rules.
*
* Before:
*
* ```css
Expand All @@ -19,42 +17,53 @@ import { isNonAtomicNode } from '../utils/non-atomic';
* @media (min-width:500px){._171dak0l{border:2px solid red}._1swkri7e:before{content:'large screen'}}
* ```
*/
const mergeAtRules = (container: Container, moveAtRulesToEnd = false): void => {
const atRuleStore = new Map<string, AtRule>();

for (const node of [...container.nodes]) {
if (node.type !== 'atrule' || isNonAtomicNode(node)) {
continue;
}

const name = `${node.name}\0${node.params}`;
const existingAtRule = atRuleStore.get(name);

if (!existingAtRule) {
atRuleStore.set(name, node);
continue;
}

const existingChildren = new Set(existingAtRule.nodes?.map((child) => child.toString()));
for (const child of [...(node.nodes || [])]) {
const stringifiedChild = child.toString();
if (!existingChildren.has(stringifiedChild)) {
existingAtRule.append(child);
existingChildren.add(stringifiedChild);
}
}

node.remove();
}

for (const node of container.nodes) {
if (node.type === 'atrule' && !isNonAtomicNode(node)) {
mergeAtRules(node);
}
}

if (moveAtRulesToEnd) {
for (const atRule of atRuleStore.values()) {
atRule.remove();
container.append(atRule);
}
}
};

export const mergeDuplicateAtRules = (): Plugin => {
return {
postcssPlugin: 'merge-duplicate-at-rules',
prepare() {
const atRuleStore: Record<string, { node: AtRule; children: Record<string, ChildNode> }> = {};
return {
AtRule(atRule) {
// Skip non-atomic cssMapScoped at-rules — merging would break source-order cascade.
const hasNonAtomic = isNonAtomicNode(atRule);
if (hasNonAtomic) return;

const name = atRule.name + atRule.params;
if (!atRuleStore[name]) {
atRuleStore[name] = {
node: atRule,
children: {},
};
}

atRule.each((node) => {
const stringifiedNode = node.toString();
if (!atRuleStore[name].children[stringifiedNode]) {
atRuleStore[name].children[stringifiedNode] = node;
}
});

atRule.remove();
},
OnceExit(root) {
for (const key in atRuleStore) {
const { node, children } = atRuleStore[key];
node.nodes = Object.values(children);
root.append(node);
}
},
};
OnceExit(root) {
mergeAtRules(root, true);
},
};
};
Expand Down
Loading