Skip to content

Commit 7c43a17

Browse files
authored
fix cascade layers in combination with nesting and name defining at rules (#739)
* fix cascade layers * fix * merge * custom properties * postcss-custom-selectors * document issue * fix * cleanup
1 parent 6a58ace commit 7c43a17

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+1336
-221
lines changed

cli/csstools-cli/dist/cli.cjs

+1-1
Large diffs are not rendered by default.

plugin-packs/postcss-preset-env/.tape.mjs

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import postcssTape from '../../packages/postcss-tape/dist/index.mjs';
22
import plugin from 'postcss-preset-env';
3+
import postcssImport from 'postcss-import';
34

45
const orderDetectionPlugin = (prop, changeWhenMatches) => {
56
return {
@@ -172,7 +173,7 @@ postcssTape(plugin)({
172173
stage: 0,
173174
browsers: '> 0%'
174175
},
175-
warnings: 1
176+
warnings: 0
176177
},
177178
'layers-basic:preserve:true': {
178179
message: 'supports layers usage with { preserve: true }',
@@ -181,7 +182,7 @@ postcssTape(plugin)({
181182
stage: 0,
182183
browsers: '> 0%'
183184
},
184-
warnings: 1
185+
warnings: 0
185186
},
186187
'client-side-polyfills:stage-1': {
187188
message: 'stable client side polyfill behavior',
@@ -403,4 +404,14 @@ postcssTape(plugin)({
403404
}
404405
},
405406
},
407+
'postcss-import/styles': {
408+
message: 'works well with "postcss-import"',
409+
plugins: [
410+
postcssImport(),
411+
plugin({
412+
stage: 0,
413+
browsers: '> 0%'
414+
})
415+
]
416+
}
406417
});

plugin-packs/postcss-preset-env/dist/index.cjs

+1-1
Large diffs are not rendered by default.

plugin-packs/postcss-preset-env/dist/index.mjs

+1-1
Large diffs are not rendered by default.

plugin-packs/postcss-preset-env/src/lib/ids-by-execution-order.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
// ids ordered by required execution, then alphabetically
22
export default [
3-
'cascade-layers',
43
'custom-media-queries',
54
'custom-properties',
65
'environment-variables', // run environment-variables here to access transpiled custom media params and properties
@@ -37,4 +36,5 @@ export default [
3736
'system-ui-font-family',
3837
'stepped-value-functions',
3938
'trigonometric-functions',
39+
'cascade-layers',
4040
];

plugin-packs/postcss-preset-env/test/layers-basic.expect.css

+122-83
Large diffs are not rendered by default.

plugin-packs/postcss-preset-env/test/layers-basic.preserve.true.expect.css

+146-92
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
:--button {
2+
color: var(--color-red);
3+
4+
@media (--dark) {
5+
color: var(--color-blue);
6+
}
7+
8+
@layer foo {
9+
text-align: left;
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
@custom-media --dark (prefers-color-scheme: dark);
2+
@custom-media --light (prefers-color-scheme: light);
3+
@custom-media --tablet (width >= 768px);
4+
5+
@custom-selector :--h h1, h2, h3, h4, h5, h6;
6+
@custom-selector :--button button, input[type="submit"];
7+
8+
:root {
9+
--color-red: red;
10+
--color-blue: blue;
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@import url(./imports/extensions.css) layer(extensions);
2+
@import url(./imports/components.css) layer(components);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
2+
3+
:root {
4+
--color-red: red;
5+
--color-blue: blue;
6+
}
7+
button:not(.does-not-exist):not(#\#) {
8+
text-align: left;
9+
}
10+
input[type="submit"]:not(#\#) {
11+
text-align: left;
12+
}
13+
button:not(.does-not-exist):not(#\#):not(#\#) {
14+
color: red;
15+
color: var(--color-red);
16+
}
17+
input[type="submit"]:not(#\#):not(#\#) {
18+
color: red;
19+
color: var(--color-red);
20+
}
21+
@media (prefers-color-scheme: dark) {
22+
button:not(.does-not-exist):not(#\#):not(#\#) {
23+
color: blue;
24+
color: var(--color-blue);
25+
}
26+
input[type="submit"]:not(#\#):not(#\#) {
27+
color: blue;
28+
color: var(--color-blue);
29+
}
30+
}

plugins/postcss-cascade-layers/CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changes to PostCSS Cascade Layers
22

3+
### Unreleased
4+
5+
- Run `postcss-cascade-layers` late compared to other PostCSS plugins (breaking)
6+
7+
_This will be the last time we change this after several times back and forth.
8+
We are sticking with this configuration now._
9+
310
### 2.0.0 (November 14, 2022)
411

512
- Run `postcss-cascade-layers` early compared to other PostCSS plugins (breaking)

plugins/postcss-cascade-layers/dist/index.cjs

+1-1
Large diffs are not rendered by default.

plugins/postcss-cascade-layers/dist/index.mjs

+1-1
Large diffs are not rendered by default.

plugins/postcss-cascade-layers/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ const creator: PluginCreator<pluginOptions> = (opts?: pluginOptions) => {
2525

2626
return {
2727
postcssPlugin: 'postcss-cascade-layers',
28-
Once(root: Container, { result }: { result: Result }) {
28+
OnceExit(root: Container, { result }: { result: Result }) {
2929

3030
// Warnings
3131
if (options.onRevertLayerKeyword) {

plugins/postcss-custom-media/.tape.mjs

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ postcssTape(plugin)({
1515
message: 'supports basic usage (old)',
1616
warnings: 1,
1717
},
18+
'cascade-layers': {
19+
message: 'supports cascade layers',
20+
},
1821
'examples/example': {
1922
message: 'minimal example',
2023
},

plugins/postcss-custom-media/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changes to PostCSS Custom Media
22

3+
### Unreleased
4+
5+
- Added: Support for Cascade Layers.
6+
37
### 9.0.1 (November 19, 2022)
48

59
- Fixed: avoid complex generated CSS when `@custom-media` contains only a single simple media feature.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import type { Node, Root } from 'postcss';
2+
export declare function collectCascadeLayerOrder(root: Root): WeakMap<Node, number>;
3+
export declare function cascadeLayerNumberForNode(node: Node, layers: WeakMap<Node, number>): number;

plugins/postcss-custom-media/dist/index.cjs

+1-1
Large diffs are not rendered by default.

plugins/postcss-custom-media/dist/index.mjs

+1-1
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import type { AtRule, Container, Document, Node, Root } from 'postcss';
2+
3+
export function collectCascadeLayerOrder(root: Root) {
4+
const references: Map<Node, string> = new Map();
5+
const referencesForLayerNames: Map<Node, string> = new Map();
6+
7+
const layers: Array<Array<string>> = [];
8+
const anonLayerCounter = 1;
9+
10+
root.walkAtRules((node) => {
11+
if (node.name.toLowerCase() !== 'layer') {
12+
return;
13+
}
14+
15+
{
16+
// We do not want to process anything except for `@layer` rules
17+
// and maybe `@layer` rules inside other `@later` rules.
18+
//
19+
// Traverse up the tree and abort when we find something unexpected
20+
let parent: Container | Document = node.parent;
21+
while (parent) {
22+
if (parent.type === 'atrule' && (parent as AtRule).name.toLowerCase() === 'layer') {
23+
parent = parent.parent;
24+
continue;
25+
}
26+
27+
if (parent === node.root()) {
28+
break;
29+
}
30+
31+
return;
32+
}
33+
}
34+
35+
let currentLayerNames = [];
36+
if (node.nodes) { // @layer { .foo {} }
37+
currentLayerNames.push(normalizeLayerName(node.params, anonLayerCounter));
38+
} else if (node.params.trim()) { // @layer a, b;
39+
currentLayerNames = node.params.split(',').map((layerName) => {
40+
return layerName.trim();
41+
});
42+
} else { // @layer;
43+
return;
44+
}
45+
46+
{
47+
// Stitch the layer names of the current node together with those of ancestors.
48+
// @layer foo { @layer bar { .any {} } }
49+
// -> "foo.bar"
50+
let parent: Container | Document = node.parent;
51+
while (parent && parent.type === 'atrule' && (parent as AtRule).name.toLowerCase() === 'layer') {
52+
const parentLayerName = referencesForLayerNames.get(parent);
53+
if (!parentLayerName) {
54+
parent = parent.parent;
55+
continue;
56+
}
57+
58+
currentLayerNames = currentLayerNames.map((layerName) => {
59+
return parentLayerName + '.' + layerName;
60+
});
61+
62+
parent = parent.parent;
63+
}
64+
}
65+
66+
// Add the new layer names to "layers".
67+
addLayerToModel(layers, currentLayerNames);
68+
69+
if (node.nodes) {
70+
// Implicit layers have higher priority than nested layers.
71+
// This requires some trickery.
72+
//
73+
// 1. connect the node to the real layer
74+
// 2. connect the node to an implicit layer
75+
// 3. use the real layer to resolve other real layer names
76+
// 4. use the implicit layer later
77+
78+
const implicitLayerName = currentLayerNames[0] + '.' + 'csstools-implicit-layer';
79+
references.set(node, implicitLayerName);
80+
referencesForLayerNames.set(node, currentLayerNames[0]);
81+
}
82+
});
83+
84+
for (const layerName of references.values()) {
85+
// Add the implicit layer names to "layers".
86+
// By doing this after all the real layers we are sure that the implicit layers have the right order in "layers".
87+
addLayerToModel(layers, [layerName]);
88+
}
89+
90+
const finalLayers = layers.map((x) => x.join('.'));
91+
92+
const out: WeakMap<Node, number> = new WeakMap();
93+
for (const [node, layerName] of references) {
94+
out.set(node, finalLayers.indexOf(layerName));
95+
}
96+
97+
return out;
98+
}
99+
100+
// -1 : node was not found
101+
// any number : node was found, higher numbers have higher priority
102+
// Infinity : node wasn't layered, highest priority
103+
export function cascadeLayerNumberForNode(node: Node, layers: WeakMap<Node, number>) {
104+
if (node.parent && node.parent.type === 'atrule' && (node.parent as AtRule).name.toLowerCase() === 'layer') {
105+
if (!layers.has(node.parent)) {
106+
return -1;
107+
}
108+
109+
return layers.get(node.parent);
110+
}
111+
112+
return Infinity;
113+
}
114+
115+
function normalizeLayerName(layerName, counter) {
116+
return layerName.trim() || `csstools-anon-layer--${counter++}`;
117+
}
118+
119+
// Insert new items after the most similar current item
120+
//
121+
// [["a", "b"]]
122+
// insert "a.first"
123+
// [["a", "a.first", "b"]]
124+
//
125+
// [["a", "a.first", "a.second", "b"]]
126+
// insert "a.first.foo"
127+
// [["a", "a.first", "a.first.foo", "a.second", "b"]]
128+
//
129+
// [["a", "b"]]
130+
// insert "c"
131+
// [["a", "b", "c"]]
132+
function addLayerToModel(layers, currentLayerNames) {
133+
currentLayerNames.forEach((layerName) => {
134+
const allLayerNameParts = layerName.split('.');
135+
136+
ALL_LAYER_NAME_PARTS_LOOP: for (let x = 0; x < allLayerNameParts.length; x++) {
137+
const layerNameParts = allLayerNameParts.slice(0, x + 1);
138+
139+
let layerWithMostEqualSegments = -1;
140+
let mostEqualSegments = 0;
141+
142+
for (let i = 0; i < layers.length; i++) {
143+
const existingLayerParts = layers[i];
144+
145+
let numberOfEqualSegments = 0;
146+
147+
LAYER_PARTS_LOOP: for (let j = 0; j < existingLayerParts.length; j++) {
148+
const existingLayerPart = existingLayerParts[j];
149+
const layerPart = layerNameParts[j];
150+
151+
if (layerPart === existingLayerPart && (j + 1) === layerNameParts.length) {
152+
continue ALL_LAYER_NAME_PARTS_LOOP; // layer already exists in model
153+
}
154+
155+
if (layerPart === existingLayerPart) {
156+
numberOfEqualSegments++;
157+
continue;
158+
}
159+
160+
if (layerPart !== existingLayerPart) {
161+
break LAYER_PARTS_LOOP;
162+
}
163+
}
164+
165+
if (numberOfEqualSegments >= mostEqualSegments) {
166+
layerWithMostEqualSegments = i;
167+
mostEqualSegments = numberOfEqualSegments;
168+
}
169+
}
170+
171+
if (layerWithMostEqualSegments === -1) {
172+
layers.push(layerNameParts);
173+
} else {
174+
layers.splice(layerWithMostEqualSegments+1, 0, layerNameParts);
175+
}
176+
}
177+
});
178+
179+
return layers;
180+
}

plugins/postcss-custom-media/src/custom-media-from-root.ts

+15-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { MediaQuery } from '@csstools/media-query-list-parser';
22
import type { ChildNode, Container, Document, Root as PostCSSRoot } from 'postcss';
3+
import { collectCascadeLayerOrder, cascadeLayerNumberForNode } from './cascade-layers';
34
import { isProcessableCustomMediaRule } from './is-processable-custom-media-rule';
45
import { removeCyclicReferences } from './toposort';
56
import { parseCustomMedia } from './transform-at-media/custom-media';
@@ -8,8 +9,11 @@ import { parseCustomMedia } from './transform-at-media/custom-media';
89
export default function getCustomMedia(root: PostCSSRoot, result, opts: { preserve?: boolean }): Map<string, { truthy: Array<MediaQuery>, falsy: Array<MediaQuery> }> {
910
// initialize custom media
1011
const customMedia: Map<string, { truthy: Array<MediaQuery>, falsy: Array<MediaQuery> }> = new Map();
12+
const customMediaCascadeLayerMapping: Map<string, number> = new Map();
1113
const customMediaGraph: Array<[string, string]> = [];
1214

15+
const cascadeLayersOrder = collectCascadeLayerOrder(root);
16+
1317
root.walkAtRules((atRule) => {
1418
if (!isProcessableCustomMediaRule(atRule)) {
1519
return;
@@ -24,12 +28,18 @@ export default function getCustomMedia(root: PostCSSRoot, result, opts: { preser
2428
return;
2529
}
2630

27-
customMedia.set(parsed.name, {
28-
truthy: parsed.truthy,
29-
falsy: parsed.falsy,
30-
});
31+
const thisCascadeLayer = cascadeLayerNumberForNode(atRule, cascadeLayersOrder);
32+
const existingCascadeLayer = customMediaCascadeLayerMapping.get(parsed.name) ?? -1;
33+
34+
if (thisCascadeLayer >= existingCascadeLayer) {
35+
customMediaCascadeLayerMapping.set(parsed.name, thisCascadeLayer);
36+
customMedia.set(parsed.name, {
37+
truthy: parsed.truthy,
38+
falsy: parsed.falsy,
39+
});
3140

32-
customMediaGraph.push(...parsed.dependsOn);
41+
customMediaGraph.push(...parsed.dependsOn);
42+
}
3343

3444
if (!opts.preserve) {
3545
const parent = atRule.parent;

plugins/postcss-custom-media/src/is-processable-custom-media-rule.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { AtRule, ChildNode, Container, Document } from 'postcss';
22

3-
const allowedParentAtRules = new Set(['scope', 'container']);
3+
const allowedParentAtRules = new Set(['scope', 'container', 'layer']);
44

55
export function isProcessableCustomMediaRule(atRule: AtRule): boolean {
66
if (atRule.name.toLowerCase() !== 'custom-media') {

0 commit comments

Comments
 (0)