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>

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy