Skip to content

Commit a051f96

Browse files
authored
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 {...} }`
1 parent 7aed6be commit a051f96

File tree

15 files changed

+198
-71
lines changed

15 files changed

+198
-71
lines changed

.changeset/green-starfishes-shave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: relax `:global` selector list validation

documentation/docs/98-reference/.generated/compile-errors.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,31 @@ A top-level `:global {...}` block can only contain rules, not declarations
235235
### css_global_block_invalid_list
236236

237237
```
238-
A `:global` selector cannot be part of a selector list with more than one item
238+
A `:global` selector cannot be part of a selector list with entries that don't contain `:global`
239+
```
240+
241+
The following CSS is invalid:
242+
243+
```css
244+
:global, x {
245+
y {
246+
color: red;
247+
}
248+
}
249+
```
250+
251+
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:
252+
253+
```css
254+
:global {
255+
y {
256+
color: red;
257+
}
258+
}
259+
260+
x y {
261+
color: red;
262+
}
239263
```
240264

241265
### css_global_block_invalid_modifier

packages/svelte/messages/compile-errors/style.md

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,31 @@
1616
1717
## css_global_block_invalid_list
1818

19-
> A `:global` selector cannot be part of a selector list with more than one item
19+
> A `:global` selector cannot be part of a selector list with entries that don't contain `:global`
20+
21+
The following CSS is invalid:
22+
23+
```css
24+
:global, x {
25+
y {
26+
color: red;
27+
}
28+
}
29+
```
30+
31+
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:
32+
33+
```css
34+
:global {
35+
y {
36+
color: red;
37+
}
38+
}
39+
40+
x y {
41+
color: red;
42+
}
43+
```
2044

2145
## css_global_block_invalid_modifier
2246

packages/svelte/src/compiler/errors.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -555,12 +555,12 @@ export function css_global_block_invalid_declaration(node) {
555555
}
556556

557557
/**
558-
* A `:global` selector cannot be part of a selector list with more than one item
558+
* A `:global` selector cannot be part of a selector list with entries that don't contain `:global`
559559
* @param {null | number | NodeLike} node
560560
* @returns {never}
561561
*/
562562
export function css_global_block_invalid_list(node) {
563-
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`);
563+
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`);
564564
}
565565

566566
/**

packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js

Lines changed: 45 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -193,69 +193,69 @@ const css_visitors = {
193193
Rule(node, context) {
194194
node.metadata.parent_rule = context.state.rule;
195195

196-
node.metadata.is_global_block = node.prelude.children.some((selector) => {
196+
// We gotta allow :global x, :global y because CSS preprocessors might generate that from :global { x, y {...} }
197+
for (const complex_selector of node.prelude.children) {
197198
let is_global_block = false;
198199

199-
for (const child of selector.children) {
200+
for (let selector_idx = 0; selector_idx < complex_selector.children.length; selector_idx++) {
201+
const child = complex_selector.children[selector_idx];
200202
const idx = child.selectors.findIndex(is_global_block_selector);
201203

202204
if (is_global_block) {
203205
// All selectors after :global are unscoped
204206
child.metadata.is_global_like = true;
205207
}
206208

207-
if (idx !== -1) {
208-
is_global_block = true;
209+
if (idx === 0) {
210+
if (
211+
child.selectors.length > 1 &&
212+
selector_idx === 0 &&
213+
node.metadata.parent_rule === null
214+
) {
215+
e.css_global_block_invalid_modifier_start(child.selectors[1]);
216+
} else {
217+
// `child` starts with `:global`
218+
node.metadata.is_global_block = is_global_block = true;
219+
220+
for (let i = 1; i < child.selectors.length; i++) {
221+
walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, {
222+
ComplexSelector(node) {
223+
node.metadata.used = true;
224+
}
225+
});
226+
}
209227

210-
for (let i = idx + 1; i < child.selectors.length; i++) {
211-
walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, {
212-
ComplexSelector(node) {
213-
node.metadata.used = true;
214-
}
215-
});
216-
}
217-
}
218-
}
228+
if (child.combinator && child.combinator.name !== ' ') {
229+
e.css_global_block_invalid_combinator(child, child.combinator.name);
230+
}
219231

220-
return is_global_block;
221-
});
232+
const declaration = node.block.children.find((child) => child.type === 'Declaration');
233+
const is_lone_global =
234+
complex_selector.children.length === 1 &&
235+
complex_selector.children[0].selectors.length === 1; // just `:global`, not e.g. `:global x`
222236

223-
if (node.metadata.is_global_block) {
224-
if (node.prelude.children.length > 1) {
225-
e.css_global_block_invalid_list(node.prelude);
226-
}
237+
if (is_lone_global && node.prelude.children.length > 1) {
238+
// `:global, :global x { z { ... } }` would become `x { z { ... } }` which means `z` is always
239+
// constrained by `x`, which is not what the user intended
240+
e.css_global_block_invalid_list(node.prelude);
241+
}
227242

228-
const complex_selector = node.prelude.children[0];
229-
const global_selector = complex_selector.children.find((r, selector_idx) => {
230-
const idx = r.selectors.findIndex(is_global_block_selector);
231-
if (idx === 0) {
232-
if (r.selectors.length > 1 && selector_idx === 0 && node.metadata.parent_rule === null) {
233-
e.css_global_block_invalid_modifier_start(r.selectors[1]);
243+
if (
244+
declaration &&
245+
// :global { color: red; } is invalid, but foo :global { color: red; } is valid
246+
node.prelude.children.length === 1 &&
247+
is_lone_global
248+
) {
249+
e.css_global_block_invalid_declaration(declaration);
250+
}
234251
}
235-
return true;
236252
} else if (idx !== -1) {
237-
e.css_global_block_invalid_modifier(r.selectors[idx]);
253+
e.css_global_block_invalid_modifier(child.selectors[idx]);
238254
}
239-
});
240-
241-
if (!global_selector) {
242-
throw new Error('Internal error: global block without :global selector');
243-
}
244-
245-
if (global_selector.combinator && global_selector.combinator.name !== ' ') {
246-
e.css_global_block_invalid_combinator(global_selector, global_selector.combinator.name);
247255
}
248256

249-
const declaration = node.block.children.find((child) => child.type === 'Declaration');
250-
251-
if (
252-
declaration &&
253-
// :global { color: red; } is invalid, but foo :global { color: red; } is valid
254-
node.prelude.children.length === 1 &&
255-
node.prelude.children[0].children.length === 1 &&
256-
node.prelude.children[0].children[0].selectors.length === 1
257-
) {
258-
e.css_global_block_invalid_declaration(declaration);
257+
if (node.metadata.is_global_block && !is_global_block) {
258+
e.css_global_block_invalid_list(node.prelude);
259259
}
260260
}
261261

packages/svelte/src/compiler/phases/3-transform/css/index.js

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,11 @@ const visitors = {
170170
if (node.metadata.is_global_block) {
171171
const selector = node.prelude.children[0];
172172

173-
if (selector.children.length === 1 && selector.children[0].selectors.length === 1) {
173+
if (
174+
node.prelude.children.length === 1 &&
175+
selector.children.length === 1 &&
176+
selector.children[0].selectors.length === 1
177+
) {
174178
// `:global {...}`
175179
if (state.minify) {
176180
state.code.remove(node.start, node.block.start + 1);
@@ -194,7 +198,7 @@ const visitors = {
194198
SelectorList(node, { state, next, path }) {
195199
// Only add comments if we're not inside a complex selector that itself is unused or a global block
196200
if (
197-
!is_in_global_block(path) &&
201+
(!is_in_global_block(path) || node.children.length > 1) &&
198202
!path.find((n) => n.type === 'ComplexSelector' && !n.metadata.used)
199203
) {
200204
const children = node.children;
@@ -282,13 +286,24 @@ const visitors = {
282286
const global = /** @type {AST.CSS.PseudoClassSelector} */ (relative_selector.selectors[0]);
283287
remove_global_pseudo_class(global, relative_selector.combinator, context.state);
284288

285-
if (
286-
node.metadata.rule?.metadata.parent_rule &&
287-
global.args === null &&
288-
relative_selector.combinator === null
289-
) {
290-
// div { :global.x { ... } } becomes div { &.x { ... } }
291-
context.state.code.prependRight(global.start, '&');
289+
const parent_rule = node.metadata.rule?.metadata.parent_rule;
290+
if (parent_rule && global.args === null) {
291+
if (relative_selector.combinator === null) {
292+
// div { :global.x { ... } } becomes div { &.x { ... } }
293+
context.state.code.prependRight(global.start, '&');
294+
}
295+
296+
// In case of multiple :global selectors in a selector list we gotta delete the comma, too, but only if
297+
// the next selector is used; if it's unused then the comma deletion happens as part of removal of that next selector
298+
if (
299+
parent_rule.prelude.children.length > 1 &&
300+
node.children.length === node.children.findIndex((s) => s === relative_selector) - 1
301+
) {
302+
const next_selector = parent_rule.prelude.children.find((s) => s.start > global.end);
303+
if (next_selector && next_selector.metadata.used) {
304+
context.state.code.update(global.end, next_selector.start, '');
305+
}
306+
}
292307
}
293308
continue;
294309
} else {
@@ -380,7 +395,9 @@ function remove_global_pseudo_class(selector, combinator, state) {
380395
// div :global.x becomes div.x
381396
while (/\s/.test(state.code.original[start - 1])) start--;
382397
}
383-
state.code.remove(start, selector.start + ':global'.length);
398+
399+
// update(...), not remove(...) because there could be a closing unused comment at the end
400+
state.code.update(start, selector.start + ':global'.length, '');
384401
} else {
385402
state.code
386403
.remove(selector.start, selector.start + ':global('.length)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
error: {
5+
code: 'css_global_block_invalid_list',
6+
message:
7+
"A `:global` selector cannot be part of a selector list with entries that don't contain `:global`",
8+
position: [232, 246]
9+
}
10+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<style>
2+
/* valid */
3+
/* We gotta allow `:global x, :global y` and the likes because CSS preprocessors might generate that from e.g. `:global { x, y {...} }` */
4+
:global .x, :global .y {}
5+
.x :global, .y :global {}
6+
7+
/* invalid */
8+
.x :global, .y {}
9+
</style>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
error: {
5+
code: 'css_global_block_invalid_list',
6+
message:
7+
"A `:global` selector cannot be part of a selector list with entries that don't contain `:global`",
8+
position: [24, 43]
9+
}
10+
});
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<style>
2+
/* invalid */
3+
:global, :global .y {
4+
z { color: red }
5+
}
6+
</style>

packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/_config.js

Lines changed: 0 additions & 9 deletions
This file was deleted.

packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/main.svelte

Lines changed: 0 additions & 3 deletions
This file was deleted.

packages/svelte/tests/css/samples/global-block/_config.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ export default test({
1616
column: 16,
1717
character: 932
1818
}
19+
},
20+
{
21+
code: 'css_unused_selector',
22+
message: 'Unused CSS selector "unused :global"',
23+
start: {
24+
line: 100,
25+
column: 29,
26+
character: 1223
27+
},
28+
end: {
29+
line: 100,
30+
column: 43,
31+
character: 1237
32+
}
1933
}
2034
]
2135
});

packages/svelte/tests/css/samples/global-block/expected.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,13 @@
9090
opacity: 1;
9191
}
9292
}
93+
94+
x, y {
95+
color: green;
96+
}
97+
98+
div.svelte-xyz, div.svelte-xyz y /* (unused) unused*/ {
99+
z {
100+
color: green;
101+
}
102+
}

packages/svelte/tests/css/samples/global-block/input.svelte

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,14 @@
9292
opacity: 1;
9393
}
9494
}
95+
96+
:global x, :global y {
97+
color: green;
98+
}
99+
100+
div :global, div :global y, unused :global {
101+
z {
102+
color: green;
103+
}
104+
}
95105
</style>

0 commit comments

Comments
 (0)