Mixins override order of rules in declarations; unexpected cascade behavior
I’m using the snippet found in the docs to apply mixins as at-rules. But its behavior is unlike SASS and PostCSS Mixins as it renders the rules inside a second declaration below where it’s supposed to be.
How can I fix this?
(I’m not good with JavaScript)
Input
@mixin layout__fullscreen {
position: fixed;
inset: 0;
}
.nav {
@apply layout__fullscreen;
bottom: unset;
}
Current Output
.nav {
bottom: unset;
}
.nav {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
Expected Output
.nav {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
bottom: unset;
}
Current Lightning CSS configuration
let mixins = new Map();
let { code } = transform({
minify: false,
sourceMap: false,
targets: browserslistToTargets(browserslist("> 0.2% and not dead")),
drafts: {
nesting: true,
customMedia: true
},
customAtRules: {
mixin: {
prelude: "<custom-ident>",
body: "style-block"
},
apply: {
prelude: "<custom-ident>"
}
},
visitor: {
Rule: {
custom: {
mixin(rule) {
mixins.set(rule.prelude.value, rule.body.value);
return [];
},
apply(rule) {
return mixins.get(rule.prelude.value);
}
}
}
},
code: Buffer.from(content)
});
Lightning CSS is correct as far as I can tell. The issue is when you’re transforming the nodes, you’re only touching the custom node, which will basically result in the following:
.nav {
& {
position: fixed;
inset: 0;
}
bottom: unset;
}
Then Lightning CSS will try and flatten that out into 2 selectors because it thinks that’s what you’re asking it to do.
To fix this, you want to “hoist” up the declarations into the parent style node instead. I’m sure you could simplify this, but here’s my implementation:
Note: you’ll want to add this in addition to the other code
visitor: {
// special handling: if a mixin is easily “flattenable,” then hoist it
// up into its parent class
style(rule) {
const hasMixin = rule.value.rules.some((r) => r.type === 'custom' && r.value.name === 'mixin');
if (hasMixin) {
const name = rule.value.rules.find((r) => r.type === 'custom' && r.value.name === 'mixin')?.value.prelude?.value;
const mixin = getMixin(name);
const isDeclarationOnlyMixin = mixin.every(
(m) => (!m.value.rules || !m.value.rules.length) && m.value.declarations,
);
if (isDeclarationOnlyMixin) {
return {
...rule,
value: {
...rule.value,
rules: rule.value.rules.filter(
(r) => !(r.type === 'custom' && r.value.name === 'mixin'),
),
declarations: {
...rule.value.declarations,
declarations: [
...rule.value.declarations.declarations,
...mixin.map((m) => m.value.declarations.declarations).flat(),
],
},
},
};
}
}
},
}
@drwpow Thank you for jumping into this issue. At a glance, I’m not parsing what your code does but I will try to implement this in the next following weeks with a new project.
@patrulea Yeah I’ll admit it’s not straightforward; main point is if you start at the style node (the parent node) and inspect it, you can probably see how to combine the declarations together. That’s all my code is doing—merging 2 declarations, just in a verbose way