Skip to content

Commit 9721d56

Browse files
authored
feat: implement :global {...} CSS blocks (#11276)
* feat: implement `:global {...}` CSS blocks * tests for compiler errors * regenerate types * lint
1 parent 11c7cd5 commit 9721d56

File tree

21 files changed

+208
-16
lines changed

21 files changed

+208
-16
lines changed

.changeset/small-apples-eat.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+
feat: implement `:global {...}` CSS blocks

packages/svelte/src/compiler/errors.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@ const css = {
103103
/** @param {string} message */
104104
'css-parse-error': (message) => message,
105105
'invalid-css-empty-declaration': () => `Declaration cannot be empty`,
106+
'invalid-css-global-block-list': () =>
107+
`A :global {...} block cannot be part of a selector list with more than one item`,
108+
'invalid-css-global-block-modifier': () =>
109+
`A :global {...} block cannot modify an existing selector`,
110+
/** @param {string} name */
111+
'invalid-css-global-block-combinator': (name) =>
112+
`A :global {...} block cannot follow a ${name} combinator`,
113+
'invalid-css-global-block-declaration': () =>
114+
`A :global {...} block can only contain rules, not declarations`,
106115
'invalid-css-global-placement': () =>
107116
`:global(...) can be at the start or end of a selector sequence, but not in the middle`,
108117
'invalid-css-global-selector': () => `:global(...) must contain exactly one selector`,

packages/svelte/src/compiler/phases/1-parse/read/style.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { error } from '../../../errors.js';
33
const REGEX_MATCHER = /^[~^$*|]?=/;
44
const REGEX_CLOSING_BRACKET = /[\s\]]/;
55
const REGEX_ATTRIBUTE_FLAGS = /^[a-zA-Z]+/; // only `i` and `s` are valid today, but make it future-proof
6-
const REGEX_COMBINATOR_WHITESPACE = /^\s*(\+|~|>|\|\|)\s*/;
76
const REGEX_COMBINATOR = /^(\+|~|>|\|\|)/;
87
const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/;
98
const REGEX_NTH_OF =
@@ -116,7 +115,8 @@ function read_rule(parser) {
116115
end: parser.index,
117116
metadata: {
118117
parent_rule: null,
119-
has_local_selectors: false
118+
has_local_selectors: false,
119+
is_global_block: false
120120
}
121121
};
122122
}
@@ -252,8 +252,6 @@ function read_selector(parser, inside_pseudo_class = false) {
252252
if (parser.eat('(')) {
253253
args = read_selector_list(parser, true);
254254
parser.eat(')', true);
255-
} else if (name === 'global') {
256-
error(parser.index, 'invalid-css-global-selector');
257255
}
258256

259257
relative_selector.selectors.push({

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,17 @@ const analysis_visitors = {
6969
Rule(node, context) {
7070
node.metadata.parent_rule = context.state.rule;
7171

72+
// `:global {...}` or `div :global {...}`
73+
node.metadata.is_global_block = node.prelude.children.some((selector) => {
74+
const last = selector.children[selector.children.length - 1];
75+
76+
const s = last.selectors[last.selectors.length - 1];
77+
78+
if (s.type === 'PseudoClassSelector' && s.name === 'global' && s.args === null) {
79+
return true;
80+
}
81+
});
82+
7283
context.next({
7384
...context.state,
7485
rule: node
@@ -84,6 +95,39 @@ const analysis_visitors = {
8495

8596
/** @type {Visitors} */
8697
const validation_visitors = {
98+
Rule(node, context) {
99+
if (node.metadata.is_global_block) {
100+
if (node.prelude.children.length > 1) {
101+
error(node.prelude, 'invalid-css-global-block-list');
102+
}
103+
104+
const complex_selector = node.prelude.children[0];
105+
const relative_selector = complex_selector.children[complex_selector.children.length - 1];
106+
107+
if (relative_selector.selectors.length > 1) {
108+
error(
109+
relative_selector.selectors[relative_selector.selectors.length - 1],
110+
'invalid-css-global-block-modifier'
111+
);
112+
}
113+
114+
if (relative_selector.combinator && relative_selector.combinator.name !== ' ') {
115+
error(
116+
relative_selector,
117+
'invalid-css-global-block-combinator',
118+
relative_selector.combinator.name
119+
);
120+
}
121+
122+
const declaration = node.block.children.find((child) => child.type === 'Declaration');
123+
124+
if (declaration) {
125+
error(declaration, 'invalid-css-global-block-declaration');
126+
}
127+
}
128+
129+
context.next();
130+
},
87131
ComplexSelector(node, context) {
88132
// ensure `:global(...)` is not used in the middle of a selector
89133
{

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { walk } from 'zimmerframe';
22
import { get_possible_values } from './utils.js';
33
import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js';
4-
import { error } from '../../../errors.js';
54

65
/**
76
* @typedef {{
@@ -60,6 +59,13 @@ export function prune(stylesheet, element) {
6059

6160
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, State>} */
6261
const visitors = {
62+
Rule(node, context) {
63+
if (node.metadata.is_global_block) {
64+
context.visit(node.prelude);
65+
} else {
66+
context.next();
67+
}
68+
},
6369
ComplexSelector(node, context) {
6470
const selectors = truncate(node);
6571
const inner = selectors[selectors.length - 1];

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,12 @@ const visitors = {
3030
}
3131

3232
context.next();
33+
},
34+
Rule(node, context) {
35+
if (node.metadata.is_global_block) {
36+
context.visit(node.prelude);
37+
} else {
38+
context.next();
39+
}
3340
}
3441
};

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ const visitors = {
116116
}
117117
}
118118
},
119-
Rule(node, { state, next }) {
119+
Rule(node, { state, next, visit }) {
120120
// keep empty rules in dev, because it's convenient to
121121
// see them in devtools
122122
if (!state.dev && is_empty(node)) {
@@ -134,6 +134,26 @@ const visitors = {
134134
return;
135135
}
136136

137+
if (node.metadata.is_global_block) {
138+
const selector = node.prelude.children[0];
139+
140+
if (selector.children.length === 1) {
141+
// `:global {...}`
142+
state.code.prependRight(node.start, '/* ');
143+
state.code.appendLeft(node.block.start + 1, '*/');
144+
145+
state.code.prependRight(node.block.end - 1, '/*');
146+
state.code.appendLeft(node.block.end, '*/');
147+
148+
// don't recurse into selector or body
149+
return;
150+
}
151+
152+
// don't recurse into body
153+
visit(node.prelude);
154+
return;
155+
}
156+
137157
next();
138158
},
139159
SelectorList(node, { state, next, path }) {
@@ -275,6 +295,10 @@ const visitors = {
275295

276296
/** @param {import('#compiler').Css.Rule} rule */
277297
function is_empty(rule) {
298+
if (rule.metadata.is_global_block) {
299+
return rule.block.children.length === 0;
300+
}
301+
278302
for (const child of rule.block.children) {
279303
if (child.type === 'Declaration') {
280304
return false;

packages/svelte/src/compiler/types/css.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export namespace Css {
3333
metadata: {
3434
parent_rule: null | Rule;
3535
has_local_selectors: boolean;
36+
is_global_block: boolean;
3637
};
3738
}
3839

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
error: {
5+
code: 'invalid-css-global-block-combinator',
6+
message: 'A :global {...} block cannot follow a > combinator',
7+
position: [12, 21]
8+
}
9+
});
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
<style>
2-
:global {}
2+
.x > :global {}
33
</style>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
error: {
5+
code: 'invalid-css-global-block-declaration',
6+
message: 'A :global {...} block can only contain rules, not declarations',
7+
position: [24, 34]
8+
}
9+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<style>
2+
.x :global {
3+
color: red;
4+
}
5+
</style>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
error: {
5+
code: 'invalid-css-global-block-modifier',
6+
message: 'A :global {...} block cannot modify an existing selector',
7+
position: [14, 21]
8+
}
9+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<style>
2+
.x .y:global {}
3+
</style>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
error: {
5+
code: 'invalid-css-global-block-list',
6+
message: 'A :global {...} block cannot be part of a selector list with more than one item',
7+
position: [9, 31]
8+
}
9+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<style>
2+
.x :global, .y :global {}
3+
</style>

packages/svelte/tests/compiler-errors/samples/css-global-without-selector/_config.js

Lines changed: 0 additions & 9 deletions
This file was deleted.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
warnings: [
5+
{
6+
filename: 'SvelteComponent.svelte',
7+
code: 'css-unused-selector',
8+
message: 'Unused CSS selector ".unused :global"',
9+
start: {
10+
line: 16,
11+
column: 1,
12+
character: 128
13+
},
14+
end: {
15+
line: 16,
16+
column: 16,
17+
character: 143
18+
}
19+
}
20+
]
21+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/* :global {*/
2+
.x {
3+
color: green;
4+
}
5+
/*}*/
6+
7+
div.svelte-xyz {
8+
.y {
9+
color: green;
10+
}
11+
}
12+
13+
/* (unused) .unused :global {
14+
.z {
15+
color: red;
16+
}
17+
}*/
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<div>{@html whatever}</div>
2+
3+
<style>
4+
:global {
5+
.x {
6+
color: green;
7+
}
8+
}
9+
10+
div :global {
11+
.y {
12+
color: green;
13+
}
14+
}
15+
16+
.unused :global {
17+
.z {
18+
color: red;
19+
}
20+
}
21+
</style>

packages/svelte/types/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,6 +1122,7 @@ declare module 'svelte/compiler' {
11221122
metadata: {
11231123
parent_rule: null | Rule;
11241124
has_local_selectors: boolean;
1125+
is_global_block: boolean;
11251126
};
11261127
}
11271128

0 commit comments

Comments
 (0)