Skip to content

fix cascade layers in combination with nesting and name defining at rules #739

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
2 changes: 1 addition & 1 deletion cli/csstools-cli/dist/cli.cjs

Large diffs are not rendered by default.

15 changes: 13 additions & 2 deletions plugin-packs/postcss-preset-env/.tape.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import postcssTape from '../../packages/postcss-tape/dist/index.mjs';
import plugin from 'postcss-preset-env';
import postcssImport from 'postcss-import';

const orderDetectionPlugin = (prop, changeWhenMatches) => {
return {
Expand Down Expand Up @@ -172,7 +173,7 @@ postcssTape(plugin)({
stage: 0,
browsers: '> 0%'
},
warnings: 1
warnings: 0
},
'layers-basic:preserve:true': {
message: 'supports layers usage with { preserve: true }',
Expand All @@ -181,7 +182,7 @@ postcssTape(plugin)({
stage: 0,
browsers: '> 0%'
},
warnings: 1
warnings: 0
},
'client-side-polyfills:stage-1': {
message: 'stable client side polyfill behavior',
Expand Down Expand Up @@ -403,4 +404,14 @@ postcssTape(plugin)({
}
},
},
'postcss-import/styles': {
message: 'works well with "postcss-import"',
plugins: [
postcssImport(),
plugin({
stage: 0,
browsers: '> 0%'
})
]
}
});
2 changes: 1 addition & 1 deletion plugin-packs/postcss-preset-env/dist/index.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion plugin-packs/postcss-preset-env/dist/index.mjs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
// ids ordered by required execution, then alphabetically
export default [
'cascade-layers',
'custom-media-queries',
'custom-properties',
'environment-variables', // run environment-variables here to access transpiled custom media params and properties
Expand Down Expand Up @@ -37,4 +36,5 @@ export default [
'system-ui-font-family',
'stepped-value-functions',
'trigonometric-functions',
'cascade-layers',
];
205 changes: 122 additions & 83 deletions plugin-packs/postcss-preset-env/test/layers-basic.expect.css

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
:--button {
color: var(--color-red);

@media (--dark) {
color: var(--color-blue);
}

@layer foo {
text-align: left;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --light (prefers-color-scheme: light);
@custom-media --tablet (width >= 768px);

@custom-selector :--h h1, h2, h3, h4, h5, h6;
@custom-selector :--button button, input[type="submit"];

:root {
--color-red: red;
--color-blue: blue;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@import url(./imports/extensions.css) layer(extensions);
@import url(./imports/components.css) layer(components);
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@


:root {
--color-red: red;
--color-blue: blue;
}
button:not(.does-not-exist):not(#\#) {
text-align: left;
}
input[type="submit"]:not(#\#) {
text-align: left;
}
button:not(.does-not-exist):not(#\#):not(#\#) {
color: red;
color: var(--color-red);
}
input[type="submit"]:not(#\#):not(#\#) {
color: red;
color: var(--color-red);
}
@media (prefers-color-scheme: dark) {
button:not(.does-not-exist):not(#\#):not(#\#) {
color: blue;
color: var(--color-blue);
}
input[type="submit"]:not(#\#):not(#\#) {
color: blue;
color: var(--color-blue);
}
}
7 changes: 7 additions & 0 deletions plugins/postcss-cascade-layers/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changes to PostCSS Cascade Layers

### Unreleased

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

_This will be the last time we change this after several times back and forth.
We are sticking with this configuration now._

### 2.0.0 (November 14, 2022)

- Run `postcss-cascade-layers` early compared to other PostCSS plugins (breaking)
Expand Down
2 changes: 1 addition & 1 deletion plugins/postcss-cascade-layers/dist/index.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion plugins/postcss-cascade-layers/dist/index.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion plugins/postcss-cascade-layers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const creator: PluginCreator<pluginOptions> = (opts?: pluginOptions) => {

return {
postcssPlugin: 'postcss-cascade-layers',
Once(root: Container, { result }: { result: Result }) {
OnceExit(root: Container, { result }: { result: Result }) {

// Warnings
if (options.onRevertLayerKeyword) {
Expand Down
3 changes: 3 additions & 0 deletions plugins/postcss-custom-media/.tape.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ postcssTape(plugin)({
message: 'supports basic usage (old)',
warnings: 1,
},
'cascade-layers': {
message: 'supports cascade layers',
},
'examples/example': {
message: 'minimal example',
},
Expand Down
4 changes: 4 additions & 0 deletions plugins/postcss-custom-media/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changes to PostCSS Custom Media

### Unreleased

- Added: Support for Cascade Layers.

### 9.0.1 (November 19, 2022)

- Fixed: avoid complex generated CSS when `@custom-media` contains only a single simple media feature.
Expand Down
3 changes: 3 additions & 0 deletions plugins/postcss-custom-media/dist/cascade-layers.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import type { Node, Root } from 'postcss';
export declare function collectCascadeLayerOrder(root: Root): WeakMap<Node, number>;
export declare function cascadeLayerNumberForNode(node: Node, layers: WeakMap<Node, number>): number;
2 changes: 1 addition & 1 deletion plugins/postcss-custom-media/dist/index.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion plugins/postcss-custom-media/dist/index.mjs

Large diffs are not rendered by default.

180 changes: 180 additions & 0 deletions plugins/postcss-custom-media/src/cascade-layers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import type { AtRule, Container, Document, Node, Root } from 'postcss';

export function collectCascadeLayerOrder(root: Root) {
const references: Map<Node, string> = new Map();
const referencesForLayerNames: Map<Node, string> = new Map();

const layers: Array<Array<string>> = [];
const anonLayerCounter = 1;

root.walkAtRules((node) => {
if (node.name.toLowerCase() !== 'layer') {
return;
}

{
// We do not want to process anything except for `@layer` rules
// and maybe `@layer` rules inside other `@later` rules.
//
// Traverse up the tree and abort when we find something unexpected
let parent: Container | Document = node.parent;
while (parent) {
if (parent.type === 'atrule' && (parent as AtRule).name.toLowerCase() === 'layer') {
parent = parent.parent;
continue;
}

if (parent === node.root()) {
break;
}

return;
}
}

let currentLayerNames = [];
if (node.nodes) { // @layer { .foo {} }
currentLayerNames.push(normalizeLayerName(node.params, anonLayerCounter));
} else if (node.params.trim()) { // @layer a, b;
currentLayerNames = node.params.split(',').map((layerName) => {
return layerName.trim();
});
} else { // @layer;
return;
}

{
// Stitch the layer names of the current node together with those of ancestors.
// @layer foo { @layer bar { .any {} } }
// -> "foo.bar"
let parent: Container | Document = node.parent;
while (parent && parent.type === 'atrule' && (parent as AtRule).name.toLowerCase() === 'layer') {
const parentLayerName = referencesForLayerNames.get(parent);
if (!parentLayerName) {
parent = parent.parent;
continue;
}

currentLayerNames = currentLayerNames.map((layerName) => {
return parentLayerName + '.' + layerName;
});

parent = parent.parent;
}
}

// Add the new layer names to "layers".
addLayerToModel(layers, currentLayerNames);

if (node.nodes) {
// Implicit layers have higher priority than nested layers.
// This requires some trickery.
//
// 1. connect the node to the real layer
// 2. connect the node to an implicit layer
// 3. use the real layer to resolve other real layer names
// 4. use the implicit layer later

const implicitLayerName = currentLayerNames[0] + '.' + 'csstools-implicit-layer';
references.set(node, implicitLayerName);
referencesForLayerNames.set(node, currentLayerNames[0]);
}
});

for (const layerName of references.values()) {
// Add the implicit layer names to "layers".
// By doing this after all the real layers we are sure that the implicit layers have the right order in "layers".
addLayerToModel(layers, [layerName]);
}

const finalLayers = layers.map((x) => x.join('.'));

const out: WeakMap<Node, number> = new WeakMap();
for (const [node, layerName] of references) {
out.set(node, finalLayers.indexOf(layerName));
}

return out;
}

// -1 : node was not found
// any number : node was found, higher numbers have higher priority
// Infinity : node wasn't layered, highest priority
export function cascadeLayerNumberForNode(node: Node, layers: WeakMap<Node, number>) {
if (node.parent && node.parent.type === 'atrule' && (node.parent as AtRule).name.toLowerCase() === 'layer') {
if (!layers.has(node.parent)) {
return -1;
}

return layers.get(node.parent);
}

return Infinity;
}

function normalizeLayerName(layerName, counter) {
return layerName.trim() || `csstools-anon-layer--${counter++}`;
}

// Insert new items after the most similar current item
//
// [["a", "b"]]
// insert "a.first"
// [["a", "a.first", "b"]]
//
// [["a", "a.first", "a.second", "b"]]
// insert "a.first.foo"
// [["a", "a.first", "a.first.foo", "a.second", "b"]]
//
// [["a", "b"]]
// insert "c"
// [["a", "b", "c"]]
function addLayerToModel(layers, currentLayerNames) {
currentLayerNames.forEach((layerName) => {
const allLayerNameParts = layerName.split('.');

ALL_LAYER_NAME_PARTS_LOOP: for (let x = 0; x < allLayerNameParts.length; x++) {
const layerNameParts = allLayerNameParts.slice(0, x + 1);

let layerWithMostEqualSegments = -1;
let mostEqualSegments = 0;

for (let i = 0; i < layers.length; i++) {
const existingLayerParts = layers[i];

let numberOfEqualSegments = 0;

LAYER_PARTS_LOOP: for (let j = 0; j < existingLayerParts.length; j++) {
const existingLayerPart = existingLayerParts[j];
const layerPart = layerNameParts[j];

if (layerPart === existingLayerPart && (j + 1) === layerNameParts.length) {
continue ALL_LAYER_NAME_PARTS_LOOP; // layer already exists in model
}

if (layerPart === existingLayerPart) {
numberOfEqualSegments++;
continue;
}

if (layerPart !== existingLayerPart) {
break LAYER_PARTS_LOOP;
}
}

if (numberOfEqualSegments >= mostEqualSegments) {
layerWithMostEqualSegments = i;
mostEqualSegments = numberOfEqualSegments;
}
}

if (layerWithMostEqualSegments === -1) {
layers.push(layerNameParts);
} else {
layers.splice(layerWithMostEqualSegments+1, 0, layerNameParts);
}
}
});

return layers;
}
20 changes: 15 additions & 5 deletions plugins/postcss-custom-media/src/custom-media-from-root.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { MediaQuery } from '@csstools/media-query-list-parser';
import type { ChildNode, Container, Document, Root as PostCSSRoot } from 'postcss';
import { collectCascadeLayerOrder, cascadeLayerNumberForNode } from './cascade-layers';
import { isProcessableCustomMediaRule } from './is-processable-custom-media-rule';
import { removeCyclicReferences } from './toposort';
import { parseCustomMedia } from './transform-at-media/custom-media';
Expand All @@ -8,8 +9,11 @@ import { parseCustomMedia } from './transform-at-media/custom-media';
export default function getCustomMedia(root: PostCSSRoot, result, opts: { preserve?: boolean }): Map<string, { truthy: Array<MediaQuery>, falsy: Array<MediaQuery> }> {
// initialize custom media
const customMedia: Map<string, { truthy: Array<MediaQuery>, falsy: Array<MediaQuery> }> = new Map();
const customMediaCascadeLayerMapping: Map<string, number> = new Map();
const customMediaGraph: Array<[string, string]> = [];

const cascadeLayersOrder = collectCascadeLayerOrder(root);

root.walkAtRules((atRule) => {
if (!isProcessableCustomMediaRule(atRule)) {
return;
Expand All @@ -24,12 +28,18 @@ export default function getCustomMedia(root: PostCSSRoot, result, opts: { preser
return;
}

customMedia.set(parsed.name, {
truthy: parsed.truthy,
falsy: parsed.falsy,
});
const thisCascadeLayer = cascadeLayerNumberForNode(atRule, cascadeLayersOrder);
const existingCascadeLayer = customMediaCascadeLayerMapping.get(parsed.name) ?? -1;

if (thisCascadeLayer >= existingCascadeLayer) {
customMediaCascadeLayerMapping.set(parsed.name, thisCascadeLayer);
customMedia.set(parsed.name, {
truthy: parsed.truthy,
falsy: parsed.falsy,
});

customMediaGraph.push(...parsed.dependsOn);
customMediaGraph.push(...parsed.dependsOn);
}

if (!opts.preserve) {
const parent = atRule.parent;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { AtRule, ChildNode, Container, Document } from 'postcss';

const allowedParentAtRules = new Set(['scope', 'container']);
const allowedParentAtRules = new Set(['scope', 'container', 'layer']);

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