fix: relax `:global` selector list validation (#15762)

We have to allow `:global x, :global y` selector lists because CSS preprocessors might generate that from `:global { x, y {...} }`
pull/15764/head
Simon H 5 months ago committed by GitHub
parent 7aed6beeaa
commit a051f96ed6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: relax `:global` selector list validation

@ -235,7 +235,31 @@ A top-level `:global {...}` block can only contain rules, not declarations
### css_global_block_invalid_list
```
A `:global` selector cannot be part of a selector list with more than one item
A `:global` selector cannot be part of a selector list with entries that don't contain `:global`
```
The following CSS is invalid:
```css
:global, x {
y {
color: red;
}
}
```
This is mixing a `:global` block, which means "everything in here is unscoped", with a scoped selector (`x` in this case). As a result it's not possible to transform the inner selector (`y` in this case) into something that satisfies both requirements. You therefore have to split this up into two selectors:
```css
:global {
y {
color: red;
}
}
x y {
color: red;
}
```
### css_global_block_invalid_modifier

@ -16,7 +16,31 @@
## css_global_block_invalid_list
> A `:global` selector cannot be part of a selector list with more than one item
> A `:global` selector cannot be part of a selector list with entries that don't contain `:global`
The following CSS is invalid:
```css
:global, x {
y {
color: red;
}
}
```
This is mixing a `:global` block, which means "everything in here is unscoped", with a scoped selector (`x` in this case). As a result it's not possible to transform the inner selector (`y` in this case) into something that satisfies both requirements. You therefore have to split this up into two selectors:
```css
:global {
y {
color: red;
}
}
x y {
color: red;
}
```
## css_global_block_invalid_modifier

@ -555,12 +555,12 @@ export function css_global_block_invalid_declaration(node) {
}
/**
* A `:global` selector cannot be part of a selector list with more than one item
* A `:global` selector cannot be part of a selector list with entries that don't contain `:global`
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function css_global_block_invalid_list(node) {
e(node, 'css_global_block_invalid_list', `A \`:global\` selector cannot be part of a selector list with more than one item\nhttps://svelte.dev/e/css_global_block_invalid_list`);
e(node, 'css_global_block_invalid_list', `A \`:global\` selector cannot be part of a selector list with entries that don't contain \`:global\`\nhttps://svelte.dev/e/css_global_block_invalid_list`);
}
/**

@ -193,10 +193,12 @@ const css_visitors = {
Rule(node, context) {
node.metadata.parent_rule = context.state.rule;
node.metadata.is_global_block = node.prelude.children.some((selector) => {
// We gotta allow :global x, :global y because CSS preprocessors might generate that from :global { x, y {...} }
for (const complex_selector of node.prelude.children) {
let is_global_block = false;
for (const child of selector.children) {
for (let selector_idx = 0; selector_idx < complex_selector.children.length; selector_idx++) {
const child = complex_selector.children[selector_idx];
const idx = child.selectors.findIndex(is_global_block_selector);
if (is_global_block) {
@ -204,60 +206,58 @@ const css_visitors = {
child.metadata.is_global_like = true;
}
if (idx !== -1) {
is_global_block = true;
if (idx === 0) {
if (
child.selectors.length > 1 &&
selector_idx === 0 &&
node.metadata.parent_rule === null
) {
e.css_global_block_invalid_modifier_start(child.selectors[1]);
} else {
// `child` starts with `:global`
node.metadata.is_global_block = is_global_block = true;
for (let i = idx + 1; i < child.selectors.length; i++) {
for (let i = 1; i < child.selectors.length; i++) {
walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, {
ComplexSelector(node) {
node.metadata.used = true;
}
});
}
}
}
return is_global_block;
});
if (node.metadata.is_global_block) {
if (node.prelude.children.length > 1) {
e.css_global_block_invalid_list(node.prelude);
}
const complex_selector = node.prelude.children[0];
const global_selector = complex_selector.children.find((r, selector_idx) => {
const idx = r.selectors.findIndex(is_global_block_selector);
if (idx === 0) {
if (r.selectors.length > 1 && selector_idx === 0 && node.metadata.parent_rule === null) {
e.css_global_block_invalid_modifier_start(r.selectors[1]);
}
return true;
} else if (idx !== -1) {
e.css_global_block_invalid_modifier(r.selectors[idx]);
if (child.combinator && child.combinator.name !== ' ') {
e.css_global_block_invalid_combinator(child, child.combinator.name);
}
});
if (!global_selector) {
throw new Error('Internal error: global block without :global selector');
}
const declaration = node.block.children.find((child) => child.type === 'Declaration');
const is_lone_global =
complex_selector.children.length === 1 &&
complex_selector.children[0].selectors.length === 1; // just `:global`, not e.g. `:global x`
if (global_selector.combinator && global_selector.combinator.name !== ' ') {
e.css_global_block_invalid_combinator(global_selector, global_selector.combinator.name);
if (is_lone_global && node.prelude.children.length > 1) {
// `:global, :global x { z { ... } }` would become `x { z { ... } }` which means `z` is always
// constrained by `x`, which is not what the user intended
e.css_global_block_invalid_list(node.prelude);
}
const declaration = node.block.children.find((child) => child.type === 'Declaration');
if (
declaration &&
// :global { color: red; } is invalid, but foo :global { color: red; } is valid
node.prelude.children.length === 1 &&
node.prelude.children[0].children.length === 1 &&
node.prelude.children[0].children[0].selectors.length === 1
is_lone_global
) {
e.css_global_block_invalid_declaration(declaration);
}
}
} else if (idx !== -1) {
e.css_global_block_invalid_modifier(child.selectors[idx]);
}
}
if (node.metadata.is_global_block && !is_global_block) {
e.css_global_block_invalid_list(node.prelude);
}
}
const state = { ...context.state, rule: node };

@ -170,7 +170,11 @@ const visitors = {
if (node.metadata.is_global_block) {
const selector = node.prelude.children[0];
if (selector.children.length === 1 && selector.children[0].selectors.length === 1) {
if (
node.prelude.children.length === 1 &&
selector.children.length === 1 &&
selector.children[0].selectors.length === 1
) {
// `:global {...}`
if (state.minify) {
state.code.remove(node.start, node.block.start + 1);
@ -194,7 +198,7 @@ const visitors = {
SelectorList(node, { state, next, path }) {
// Only add comments if we're not inside a complex selector that itself is unused or a global block
if (
!is_in_global_block(path) &&
(!is_in_global_block(path) || node.children.length > 1) &&
!path.find((n) => n.type === 'ComplexSelector' && !n.metadata.used)
) {
const children = node.children;
@ -282,14 +286,25 @@ const visitors = {
const global = /** @type {AST.CSS.PseudoClassSelector} */ (relative_selector.selectors[0]);
remove_global_pseudo_class(global, relative_selector.combinator, context.state);
if (
node.metadata.rule?.metadata.parent_rule &&
global.args === null &&
relative_selector.combinator === null
) {
const parent_rule = node.metadata.rule?.metadata.parent_rule;
if (parent_rule && global.args === null) {
if (relative_selector.combinator === null) {
// div { :global.x { ... } } becomes div { &.x { ... } }
context.state.code.prependRight(global.start, '&');
}
// In case of multiple :global selectors in a selector list we gotta delete the comma, too, but only if
// the next selector is used; if it's unused then the comma deletion happens as part of removal of that next selector
if (
parent_rule.prelude.children.length > 1 &&
node.children.length === node.children.findIndex((s) => s === relative_selector) - 1
) {
const next_selector = parent_rule.prelude.children.find((s) => s.start > global.end);
if (next_selector && next_selector.metadata.used) {
context.state.code.update(global.end, next_selector.start, '');
}
}
}
continue;
} else {
// for any :global() or :global at the middle of compound selector
@ -380,7 +395,9 @@ function remove_global_pseudo_class(selector, combinator, state) {
// div :global.x becomes div.x
while (/\s/.test(state.code.original[start - 1])) start--;
}
state.code.remove(start, selector.start + ':global'.length);
// update(...), not remove(...) because there could be a closing unused comment at the end
state.code.update(start, selector.start + ':global'.length, '');
} else {
state.code
.remove(selector.start, selector.start + ':global('.length)

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
error: {
code: 'css_global_block_invalid_list',
message:
"A `:global` selector cannot be part of a selector list with entries that don't contain `:global`",
position: [232, 246]
}
});

@ -0,0 +1,9 @@
<style>
/* valid */
/* We gotta allow `:global x, :global y` and the likes because CSS preprocessors might generate that from e.g. `:global { x, y {...} }` */
:global .x, :global .y {}
.x :global, .y :global {}
/* invalid */
.x :global, .y {}
</style>

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
error: {
code: 'css_global_block_invalid_list',
message:
"A `:global` selector cannot be part of a selector list with entries that don't contain `:global`",
position: [24, 43]
}
});

@ -0,0 +1,6 @@
<style>
/* invalid */
:global, :global .y {
z { color: red }
}
</style>

@ -1,9 +0,0 @@
import { test } from '../../test';
export default test({
error: {
code: 'css_global_block_invalid_list',
message: 'A `:global` selector cannot be part of a selector list with more than one item',
position: [9, 31]
}
});

@ -16,6 +16,20 @@ export default test({
column: 16,
character: 932
}
},
{
code: 'css_unused_selector',
message: 'Unused CSS selector "unused :global"',
start: {
line: 100,
column: 29,
character: 1223
},
end: {
line: 100,
column: 43,
character: 1237
}
}
]
});

@ -90,3 +90,13 @@
opacity: 1;
}
}
x, y {
color: green;
}
div.svelte-xyz, div.svelte-xyz y /* (unused) unused*/ {
z {
color: green;
}
}

@ -92,4 +92,14 @@
opacity: 1;
}
}
:global x, :global y {
color: green;
}
div :global, div :global y, unused :global {
z {
color: green;
}
}
</style>

Loading…
Cancel
Save