feat: allow `:global` in more places (alternative) (#12560)

* `div { :global { &.x { ... } } }` is equivalent to `div:global.x { ... }`, so the latter should be allowed, too

* finalize

* replace obsolete breaking change (which turned out to be a wrong change and was since reverted), add new breaking change note

* changeset

* regenerate types

* Update sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md

Co-authored-by: Rich Harris <rich.harris@vercel.com>

* always remove descendant selector before global

* error on lone `:global` with nested `&`, revert "remove spaces" rule

* regenerate types

* documentation

* oops

* switch to removing descendant combinator

* fix

* revert combinator validation relaxation

* error on first global being modified

* tweak docs

* tweak error messages

* Update documentation/docs/02-template-syntax/05-styles-and-classes.md

Co-authored-by: Rich Harris <rich.harris@vercel.com>

* clarify

* tweak messages

* update tests

* tweak docs

* tweak `:global(...)` docs

* tweak docs

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/12609/head
Simon H 2 months ago committed by GitHub
parent e2f17c8a7b
commit 13d86e9019
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: allow `:global` in more places

@ -25,31 +25,28 @@ This works by adding a class to affected elements, which is based on a hash of t
</style> </style>
``` ```
## :global ## :global(...)
To apply styles to a selector globally, use the `:global(...)` modifier. To apply styles to a single selector globally, use the `:global(...)` modifier:
```svelte ```svelte
<style> <style>
:global(body) { :global(body) {
/* this will apply to <body> */ /* applies to <body> */
margin: 0; margin: 0;
} }
div :global(strong) { div :global(strong) {
/* this will apply to all <strong> elements, in any /* applies to all <strong> elements, in any component,
component, that are inside <div> elements belonging that are inside <div> elements belonging
to this component */ to this component */
color: goldenrod; color: goldenrod;
} }
p:global(.red) { p:global(.big.red) {
/* this will apply to all <p> elements belonging to this /* applies to all <p> elements belonging to this component
component with a class of red, even if class="red" does with `class="big red"`, even if it is applied
not initially appear in the markup, and is instead programmatically (for example by a library) */
added at runtime. This is useful when the class
of the element is dynamically applied, for instance
when updating the element's classList property directly. */
} }
</style> </style>
``` ```
@ -66,6 +63,30 @@ The `-global-` part will be removed when compiled, and the keyframe will then be
</style> </style>
``` ```
## :global
To apply styles to a group of selectors globally, create a `:global {...}` block:
```svelte
<style>
:global {
/* applies to every <div> in your application */
div { ... }
/* applies to every <p> in your application */
p { ... }
}
.a :global {
/* applies to every `.b .c .d` element, in any component,
that is inside an `.a` element in this component */
.b .c .d {...}
}
</style>
```
> The second example above could also be written as an equivalent `.a :global .b .c .d` selector, where everything after the `:global` is unscoped, though the nested form is preferred.
## Nested style tags ## Nested style tags
There should only be 1 top-level `<style>` tag per component. There should only be 1 top-level `<style>` tag per component.

@ -8,35 +8,35 @@
## css_global_block_invalid_combinator ## css_global_block_invalid_combinator
> A :global {...} block cannot follow a %name% combinator > A `:global` selector cannot follow a `%name%` combinator
## css_global_block_invalid_declaration ## css_global_block_invalid_declaration
> A :global {...} block can only contain rules, not declarations > A top-level `:global {...}` block can only contain rules, not declarations
## css_global_block_invalid_list ## css_global_block_invalid_list
> A :global {...} block cannot be part of a selector list with more than one item > A `:global` selector cannot be part of a selector list with more than one item
## css_global_block_invalid_modifier ## css_global_block_invalid_modifier
> A :global {...} block cannot modify an existing selector > A `:global` selector cannot modify an existing selector
## css_global_block_invalid_placement ## css_global_block_invalid_modifier_start
> A :global {...} block can only appear at the end of a selector sequence (did you mean to use :global(...) instead?) > A `:global` selector can only be modified if it is a descendant of other selectors
## css_global_invalid_placement ## css_global_invalid_placement
> :global(...) can be at the start or end of a selector sequence, but not in the middle > `:global(...)` can be at the start or end of a selector sequence, but not in the middle
## css_global_invalid_selector ## css_global_invalid_selector
> :global(...) must contain exactly one selector > `:global(...)` must contain exactly one selector
## css_global_invalid_selector_list ## css_global_invalid_selector_list
> :global(...) must not contain type or universal selectors when used in a compound selector > `:global(...)` must not contain type or universal selectors when used in a compound selector
## css_nesting_selector_invalid_placement ## css_nesting_selector_invalid_placement
@ -48,4 +48,4 @@
## css_type_selector_invalid_placement ## css_type_selector_invalid_placement
> :global(...) must not be followed with a type selector > `:global(...)` must not be followed by a type selector

@ -434,76 +434,76 @@ export function css_expected_identifier(node) {
} }
/** /**
* A :global {...} block cannot follow a %name% combinator * A `:global` selector cannot follow a `%name%` combinator
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node
* @param {string} name * @param {string} name
* @returns {never} * @returns {never}
*/ */
export function css_global_block_invalid_combinator(node, name) { export function css_global_block_invalid_combinator(node, name) {
e(node, "css_global_block_invalid_combinator", `A :global {...} block cannot follow a ${name} combinator`); e(node, "css_global_block_invalid_combinator", `A \`:global\` selector cannot follow a \`${name}\` combinator`);
} }
/** /**
* A :global {...} block can only contain rules, not declarations * A top-level `:global {...}` block can only contain rules, not declarations
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node
* @returns {never} * @returns {never}
*/ */
export function css_global_block_invalid_declaration(node) { export function css_global_block_invalid_declaration(node) {
e(node, "css_global_block_invalid_declaration", "A :global {...} block can only contain rules, not declarations"); e(node, "css_global_block_invalid_declaration", "A top-level `:global {...}` block can only contain rules, not declarations");
} }
/** /**
* A :global {...} block cannot be part of a selector list with more than one item * A `:global` selector cannot be part of a selector list with more than one item
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node
* @returns {never} * @returns {never}
*/ */
export function css_global_block_invalid_list(node) { export function css_global_block_invalid_list(node) {
e(node, "css_global_block_invalid_list", "A :global {...} block cannot be part of a selector list with more than one item"); e(node, "css_global_block_invalid_list", "A `:global` selector cannot be part of a selector list with more than one item");
} }
/** /**
* A :global {...} block cannot modify an existing selector * A `:global` selector cannot modify an existing selector
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node
* @returns {never} * @returns {never}
*/ */
export function css_global_block_invalid_modifier(node) { export function css_global_block_invalid_modifier(node) {
e(node, "css_global_block_invalid_modifier", "A :global {...} block cannot modify an existing selector"); e(node, "css_global_block_invalid_modifier", "A `:global` selector cannot modify an existing selector");
} }
/** /**
* A :global {...} block can only appear at the end of a selector sequence (did you mean to use :global(...) instead?) * A `:global` selector can only be modified if it is a descendant of other selectors
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node
* @returns {never} * @returns {never}
*/ */
export function css_global_block_invalid_placement(node) { export function css_global_block_invalid_modifier_start(node) {
e(node, "css_global_block_invalid_placement", "A :global {...} block can only appear at the end of a selector sequence (did you mean to use :global(...) instead?)"); e(node, "css_global_block_invalid_modifier_start", "A `:global` selector can only be modified if it is a descendant of other selectors");
} }
/** /**
* :global(...) can be at the start or end of a selector sequence, but not in the middle * `:global(...)` can be at the start or end of a selector sequence, but not in the middle
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node
* @returns {never} * @returns {never}
*/ */
export function css_global_invalid_placement(node) { export function css_global_invalid_placement(node) {
e(node, "css_global_invalid_placement", ":global(...) can be at the start or end of a selector sequence, but not in the middle"); e(node, "css_global_invalid_placement", "`:global(...)` can be at the start or end of a selector sequence, but not in the middle");
} }
/** /**
* :global(...) must contain exactly one selector * `:global(...)` must contain exactly one selector
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node
* @returns {never} * @returns {never}
*/ */
export function css_global_invalid_selector(node) { export function css_global_invalid_selector(node) {
e(node, "css_global_invalid_selector", ":global(...) must contain exactly one selector"); e(node, "css_global_invalid_selector", "`:global(...)` must contain exactly one selector");
} }
/** /**
* :global(...) must not contain type or universal selectors when used in a compound selector * `:global(...)` must not contain type or universal selectors when used in a compound selector
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node
* @returns {never} * @returns {never}
*/ */
export function css_global_invalid_selector_list(node) { export function css_global_invalid_selector_list(node) {
e(node, "css_global_invalid_selector_list", ":global(...) must not contain type or universal selectors when used in a compound selector"); e(node, "css_global_invalid_selector_list", "`:global(...)` must not contain type or universal selectors when used in a compound selector");
} }
/** /**
@ -525,12 +525,12 @@ export function css_selector_invalid(node) {
} }
/** /**
* :global(...) must not be followed with a type selector * `:global(...)` must not be followed by a type selector
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node
* @returns {never} * @returns {never}
*/ */
export function css_type_selector_invalid_placement(node) { export function css_type_selector_invalid_placement(node) {
e(node, "css_type_selector_invalid_placement", ":global(...) must not be followed with a type selector"); e(node, "css_type_selector_invalid_placement", "`:global(...)` must not be followed by a type selector");
} }
/** /**

@ -27,10 +27,25 @@ function is_global(relative_selector) {
return ( return (
first.type === 'PseudoClassSelector' && first.type === 'PseudoClassSelector' &&
first.name === 'global' && first.name === 'global' &&
(first.args === null ||
// Only these two selector types keep the whole selector global, because e.g.
// :global(button).x means that the selector is still scoped because of the .x
relative_selector.selectors.every( relative_selector.selectors.every(
(selector) => (selector) =>
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector' selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
) ))
);
}
/**
* True if is `:global`
* @param {Css.SimpleSelector} simple_selector
*/
function is_global_block_selector(simple_selector) {
return (
simple_selector.type === 'PseudoClassSelector' &&
simple_selector.name === 'global' &&
simple_selector.args === null
); );
} }
@ -48,19 +63,12 @@ const analysis_visitors = {
node.metadata.rule = context.state.rule; node.metadata.rule = context.state.rule;
node.metadata.used = node.children.every( node.metadata.used ||= node.children.every(
({ metadata }) => metadata.is_global || metadata.is_global_like ({ metadata }) => metadata.is_global || metadata.is_global_like
); );
}, },
RelativeSelector(node, context) { RelativeSelector(node, context) {
node.metadata.is_global = node.metadata.is_global = node.selectors.length >= 1 && is_global(node);
node.selectors.length >= 1 &&
node.selectors[0].type === 'PseudoClassSelector' &&
node.selectors[0].name === 'global' &&
node.selectors.every(
(selector) =>
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
);
if (node.selectors.length === 1) { if (node.selectors.length === 1) {
const first = node.selectors[0]; const first = node.selectors[0];
@ -85,16 +93,31 @@ const analysis_visitors = {
Rule(node, context) { Rule(node, context) {
node.metadata.parent_rule = context.state.rule; node.metadata.parent_rule = context.state.rule;
// `:global {...}` or `div :global {...}`
node.metadata.is_global_block = node.prelude.children.some((selector) => { node.metadata.is_global_block = node.prelude.children.some((selector) => {
const last = selector.children[selector.children.length - 1]; let is_global_block = false;
const s = last.selectors[last.selectors.length - 1]; for (const child of selector.children) {
const idx = child.selectors.findIndex(is_global_block_selector);
if (s.type === 'PseudoClassSelector' && s.name === 'global' && s.args === null) { if (is_global_block) {
return true; // All selectors after :global are unscoped
child.metadata.is_global_like = true;
}
if (idx !== -1) {
is_global_block = true;
for (let i = idx + 1; i < child.selectors.length; i++) {
walk(/** @type {Css.Node} */ (child.selectors[i]), null, {
ComplexSelector(node) {
node.metadata.used = true;
} }
}); });
}
}
}
return is_global_block;
});
context.next({ context.next({
...context.state, ...context.state,
@ -118,21 +141,35 @@ const validation_visitors = {
} }
const complex_selector = node.prelude.children[0]; const complex_selector = node.prelude.children[0];
const relative_selector = complex_selector.children[complex_selector.children.length - 1]; 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 (relative_selector.selectors.length > 1) { if (!global_selector) {
e.css_global_block_invalid_modifier( throw new Error('Internal error: global block without :global selector');
relative_selector.selectors[relative_selector.selectors.length - 1]
);
} }
if (relative_selector.combinator && relative_selector.combinator.name !== ' ') { if (global_selector.combinator && global_selector.combinator.name !== ' ') {
e.css_global_block_invalid_combinator(relative_selector, relative_selector.combinator.name); e.css_global_block_invalid_combinator(global_selector, global_selector.combinator.name);
} }
const declaration = node.block.children.find((child) => child.type === 'Declaration'); const declaration = node.block.children.find((child) => child.type === 'Declaration');
if (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
) {
e.css_global_block_invalid_declaration(declaration); e.css_global_block_invalid_declaration(declaration);
} }
} }
@ -146,14 +183,7 @@ const validation_visitors = {
if (global) { if (global) {
const idx = node.children.indexOf(global); const idx = node.children.indexOf(global);
if (global.selectors[0].args === null && idx !== node.children.length - 1) { if (global.selectors[0].args !== null && idx !== 0 && idx !== node.children.length - 1) {
// ensure `:global` is only at the end of a selector
e.css_global_block_invalid_placement(global.selectors[0]);
} else if (
global.selectors[0].args !== null &&
idx !== 0 &&
idx !== node.children.length - 1
) {
// ensure `:global(...)` is not used in the middle of a selector (but multiple `global(...)` in sequence are ok) // ensure `:global(...)` is not used in the middle of a selector (but multiple `global(...)` in sequence are ok)
for (let i = idx + 1; i < node.children.length; i++) { for (let i = idx + 1; i < node.children.length; i++) {
if (!is_global(node.children[i])) { if (!is_global(node.children[i])) {
@ -196,8 +226,17 @@ const validation_visitors = {
}, },
NestingSelector(node, context) { NestingSelector(node, context) {
const rule = /** @type {Css.Rule} */ (context.state.rule); const rule = /** @type {Css.Rule} */ (context.state.rule);
if (!rule.metadata.parent_rule) { const parent_rule = rule.metadata.parent_rule;
if (!parent_rule) {
e.css_nesting_selector_invalid_placement(node); e.css_nesting_selector_invalid_placement(node);
} else if (
// :global { &.foo { ... } } is invalid
parent_rule.metadata.is_global_block &&
!parent_rule.metadata.parent_rule &&
parent_rule.prelude.children[0].children.length === 1 &&
parent_rule.prelude.children[0].children[0].selectors.length === 1
) {
e.css_global_block_invalid_modifier_start(node);
} }
} }
}; };

@ -295,12 +295,19 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
return false; return false;
} }
if (name === 'global' && relative_selector.selectors.length === 1) { if (
const args = /** @type {Compiler.Css.SelectorList} */ (selector.args); name === 'global' &&
selector.args !== null &&
relative_selector.selectors.length === 1
) {
const args = selector.args;
const complex_selector = args.children[0]; const complex_selector = args.children[0];
return apply_selector(complex_selector.children, rule, element, stylesheet); return apply_selector(complex_selector.children, rule, element, stylesheet);
} }
// We came across a :global, everything beyond it is global and therefore a potential match
if (name === 'global' && selector.args === null) return true;
if ((name === 'is' || name === 'where') && selector.args) { if ((name === 'is' || name === 'where') && selector.args) {
let matched = false; let matched = false;

@ -92,7 +92,7 @@ const visitors = {
next(); next();
}, },
Declaration(node, { state, next }) { Declaration(node, { state }) {
const property = node.property && remove_css_prefix(node.property.toLowerCase()); const property = node.property && remove_css_prefix(node.property.toLowerCase());
if (property === 'animation' || property === 'animation-name') { if (property === 'animation' || property === 'animation-name') {
let index = node.start + node.property.length + 1; let index = node.start + node.property.length + 1;
@ -140,7 +140,7 @@ const visitors = {
if (node.metadata.is_global_block) { if (node.metadata.is_global_block) {
const selector = node.prelude.children[0]; const selector = node.prelude.children[0];
if (selector.children.length === 1) { if (selector.children.length === 1 && selector.children[0].selectors.length === 1) {
// `:global {...}` // `:global {...}`
state.code.prependRight(node.start, '/* '); state.code.prependRight(node.start, '/* ');
state.code.appendLeft(node.block.start + 1, '*/'); state.code.appendLeft(node.block.start + 1, '*/');
@ -216,16 +216,38 @@ const visitors = {
ComplexSelector(node, context) { ComplexSelector(node, context) {
const before_bumped = context.state.specificity.bumped; const before_bumped = context.state.specificity.bumped;
/** @param {Css.SimpleSelector} selector */ /**
function remove_global_pseudo_class(selector) { * @param {Css.PseudoClassSelector} selector
* @param {Css.Combinator | null} combinator
*/
function remove_global_pseudo_class(selector, combinator) {
if (selector.args === null) {
let start = selector.start;
if (combinator?.name === ' ') {
// div :global.x becomes div.x
while (/\s/.test(context.state.code.original[start - 1])) start--;
}
context.state.code.remove(start, selector.start + ':global'.length);
} else {
context.state.code context.state.code
.remove(selector.start, selector.start + ':global('.length) .remove(selector.start, selector.start + ':global('.length)
.remove(selector.end - 1, selector.end); .remove(selector.end - 1, selector.end);
} }
}
for (const relative_selector of node.children) { for (const relative_selector of node.children) {
if (relative_selector.metadata.is_global) { if (relative_selector.metadata.is_global) {
remove_global_pseudo_class(relative_selector.selectors[0]); const global = /** @type {Css.PseudoClassSelector} */ (relative_selector.selectors[0]);
remove_global_pseudo_class(global, relative_selector.combinator);
if (
node.metadata.rule?.metadata.parent_rule &&
global.args === null &&
relative_selector.combinator === null
) {
// div { :global.x { ... } } becomes div { &.x { ... } }
context.state.code.prependRight(global.start, '&');
}
continue; continue;
} }
@ -241,10 +263,10 @@ const visitors = {
} }
} }
// for any :global() at the middle of compound selector // for any :global() or :global at the middle of compound selector
for (const selector of relative_selector.selectors) { for (const selector of relative_selector.selectors) {
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') { if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
remove_global_pseudo_class(selector); remove_global_pseudo_class(selector, null);
} }
} }

@ -33,17 +33,32 @@ export namespace Css {
metadata: { metadata: {
parent_rule: null | Rule; parent_rule: null | Rule;
has_local_selectors: boolean; has_local_selectors: boolean;
/**
* `true` if the rule contains a `:global` selector, and therefore everything inside should be unscoped
*/
is_global_block: boolean; is_global_block: boolean;
}; };
} }
/**
* A list of selectors, e.g. `a, b, c {}`
*/
export interface SelectorList extends BaseNode { export interface SelectorList extends BaseNode {
type: 'SelectorList'; type: 'SelectorList';
/**
* The `a`, `b` and `c` in `a, b, c {}`
*/
children: ComplexSelector[]; children: ComplexSelector[];
} }
/**
* A complex selector, e.g. `a b c {}`
*/
export interface ComplexSelector extends BaseNode { export interface ComplexSelector extends BaseNode {
type: 'ComplexSelector'; type: 'ComplexSelector';
/**
* The `a`, `b` and `c` in `a b c {}`
*/
children: RelativeSelector[]; children: RelativeSelector[];
metadata: { metadata: {
rule: null | Rule; rule: null | Rule;
@ -51,14 +66,26 @@ export namespace Css {
}; };
} }
/**
* A relative selector, e.g the `a` and `> b` in `a > b {}`
*/
export interface RelativeSelector extends BaseNode { export interface RelativeSelector extends BaseNode {
type: 'RelativeSelector'; type: 'RelativeSelector';
/**
* In `a > b`, `> b` forms one relative selector, and `>` is the combinator. `null` for the first selector.
*/
combinator: null | Combinator; combinator: null | Combinator;
/**
* The `b:is(...)` in `> b:is(...)`
*/
selectors: SimpleSelector[]; selectors: SimpleSelector[];
metadata: { metadata: {
/** :global(..) */ /**
* `true` if the whole selector is unscoped, e.g. `:global(...)` or `:global` or `:global.x`.
* Selectors like `:global(...).x` are not considered global, because they still need scoping.
*/
is_global: boolean; is_global: boolean;
/** :root, :host, ::view-transition */ /** `:root`, `:host`, `::view-transition`, or selectors after a `:global` */
is_global_like: boolean; is_global_like: boolean;
scoped: boolean; scoped: boolean;
}; };

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
error: {
code: 'css_global_block_invalid_combinator',
message: 'A `:global` selector cannot follow a `>` combinator',
position: [79, 88]
}
});

@ -0,0 +1,11 @@
<style>
/* ok */
.x :global.x p {
}
.x :global > p {
}
/* not ok */
.x > :global p {
}
</style>

@ -3,7 +3,7 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'css_global_block_invalid_combinator', code: 'css_global_block_invalid_combinator',
message: 'A :global {...} block cannot follow a > combinator', message: 'A `:global` selector cannot follow a `>` combinator',
position: [12, 21] position: [54, 63]
} }
}); });

@ -1,3 +1,9 @@
<style> <style>
.x > :global {} /* ok */
.x :global {
}
/* not ok */
.x > :global {
}
</style> </style>

@ -3,7 +3,7 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'css_global_block_invalid_declaration', code: 'css_global_block_invalid_declaration',
message: 'A :global {...} block can only contain rules, not declarations', message: 'A top-level `:global {...}` block can only contain rules, not declarations',
position: [24, 34] position: [109, 119]
} }
}); });

@ -1,5 +1,15 @@
<style> <style>
/* ok */
.x :global { .x :global {
color: red; color: red;
} }
:global .y {
color: red;
}
/* not ok */
:global {
color: red;
}
</style> </style>

@ -1,10 +0,0 @@
import { test } from '../../test';
export default test({
error: {
code: 'css_global_block_invalid_placement',
message:
'A :global {...} block can only appear at the end of a selector sequence (did you mean to use :global(...) instead?)',
position: [50, 57]
}
});

@ -1,8 +0,0 @@
<style>
/* ok */
.x :global {
}
/* not ok */
:global .x {
}
</style>

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

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
error: {
code: 'css_global_block_invalid_modifier_start',
message: 'A `:global` selector can only be modified if it is a descendant of other selectors',
position: [75, 77]
}
});

@ -0,0 +1,11 @@
<style>
/* ok */
div :global.x {
color: red;
}
/* not ok */
:global.x {
color: red;
}
</style>

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
error: {
code: 'css_global_block_invalid_modifier_start',
message: 'A `:global` selector can only be modified if it is a descendant of other selectors',
position: [147, 148]
}
});

@ -0,0 +1,22 @@
<style>
/* ok */
div :global {
&.x {
color: red;
}
}
div {
:global {
&.x {
color: red;
}
}
}
/* not ok */
:global {
&.x {
color: red;
}
}
</style>

@ -3,7 +3,7 @@ import { test } from '../../test';
export default test({ export default test({
error: { error: {
code: 'css_global_block_invalid_modifier', code: 'css_global_block_invalid_modifier',
message: 'A :global {...} block cannot modify an existing selector', message: 'A `:global` selector cannot modify an existing selector',
position: [14, 21] position: [70, 77]
} }
}); });

@ -0,0 +1,11 @@
<style>
/* ok */
div :global.x {
color: red;
}
/* not ok */
.x:global {
color: red;
}
</style>

@ -7,14 +7,14 @@ export default test({
code: 'css_unused_selector', code: 'css_unused_selector',
message: 'Unused CSS selector ".unused :global"', message: 'Unused CSS selector ".unused :global"',
start: { start: {
line: 16, line: 69,
column: 1, column: 1,
character: 128 character: 917
}, },
end: { end: {
line: 16, line: 69,
column: 16, column: 16,
character: 143 character: 932
} }
} }
] ]

@ -1,3 +1,4 @@
/* :global {*/ /* :global {*/
.x { .x {
color: green; color: green;
@ -10,6 +11,59 @@
} }
} }
/* some css preprocessors de-nest :global blocks with a single child
(e.g turn `:global { div { ... } }` into `:global div { ... }`),
so we need to support it, too */
div {
.y {
color: green;
}
}
div.svelte-xyz p {
.y {
color: green;
}
}
/* `div { :global { &.x { ...} } }` is allowed ... */
div.svelte-xyz {
/* :global {*/
&.x {
color: green;
}
/*}*/
}
/* ...wich is equivalent to `div :global { &.x { ...} }` ... */
div.svelte-xyz {
&.x {
color: green;
}
}
/* ...so `div :global.x` must be, too ... */
div.svelte-xyz.x {
color: green;
}
/* ...and therefore `div { :global.x { ... }` aswell */
div.svelte-xyz {
&.x {
color: green;
}
}
div.svelte-xyz {
&.x {
color: green;
}
}
div.svelte-xyz:is(html.dark-mode *) {
color: green;
}
/* (unused) .unused :global { /* (unused) .unused :global {
.z { .z {
color: red; color: red;

@ -13,6 +13,59 @@
} }
} }
/* some css preprocessors de-nest :global blocks with a single child
(e.g turn `:global { div { ... } }` into `:global div { ... }`),
so we need to support it, too */
:global div {
.y {
color: green;
}
}
div :global p {
.y {
color: green;
}
}
/* `div { :global { &.x { ...} } }` is allowed ... */
div {
:global {
&.x {
color: green;
}
}
}
/* ...wich is equivalent to `div :global { &.x { ...} }` ... */
div :global {
&.x {
color: green;
}
}
/* ...so `div :global.x` must be, too ... */
div :global.x {
color: green;
}
/* ...and therefore `div { :global.x { ... }` aswell */
div {
:global.x {
color: green;
}
}
div {
& :global.x {
color: green;
}
}
div :global:is(html.dark-mode *) {
color: green;
}
.unused :global { .unused :global {
.z { .z {
color: red; color: red;

@ -1,7 +1,7 @@
[ [
{ {
"code": "css_global_invalid_placement", "code": "css_global_invalid_placement",
"message": ":global(...) can be at the start or end of a selector sequence, but not in the middle", "message": "`:global(...)` can be at the start or end of a selector sequence, but not in the middle",
"start": { "start": {
"line": 8, "line": 8,
"column": 6 "column": 6

@ -1,7 +1,7 @@
[ [
{ {
"code": "css_global_invalid_placement", "code": "css_global_invalid_placement",
"message": ":global(...) can be at the start or end of a selector sequence, but not in the middle", "message": "`:global(...)` can be at the start or end of a selector sequence, but not in the middle",
"start": { "start": {
"line": 5, "line": 5,
"column": 6 "column": 6

@ -1,7 +1,7 @@
[ [
{ {
"code": "css_global_invalid_selector_list", "code": "css_global_invalid_selector_list",
"message": ":global(...) must not contain type or universal selectors when used in a compound selector", "message": "`:global(...)` must not contain type or universal selectors when used in a compound selector",
"start": { "start": {
"line": 2, "line": 2,
"column": 5 "column": 5

@ -1,7 +1,7 @@
[ [
{ {
"code": "css_global_invalid_selector_list", "code": "css_global_invalid_selector_list",
"message": ":global(...) must not contain type or universal selectors when used in a compound selector", "message": "`:global(...)` must not contain type or universal selectors when used in a compound selector",
"start": { "start": {
"line": 2, "line": 2,
"column": 5 "column": 5

@ -1,7 +1,7 @@
[ [
{ {
"code": "css_global_invalid_selector_list", "code": "css_global_invalid_selector_list",
"message": ":global(...) must not contain type or universal selectors when used in a compound selector", "message": "`:global(...)` must not contain type or universal selectors when used in a compound selector",
"start": { "start": {
"line": 5, "line": 5,
"column": 5 "column": 5

@ -1,7 +1,7 @@
[ [
{ {
"code": "css_global_invalid_placement", "code": "css_global_invalid_placement",
"message": ":global(...) can be at the start or end of a selector sequence, but not in the middle", "message": "`:global(...)` can be at the start or end of a selector sequence, but not in the middle",
"start": { "start": {
"line": 2, "line": 2,
"column": 6 "column": 6

@ -1,7 +1,7 @@
[ [
{ {
"code": "css_global_invalid_selector", "code": "css_global_invalid_selector",
"message": ":global(...) must contain exactly one selector", "message": "`:global(...)` must contain exactly one selector",
"start": { "start": {
"line": 11, "line": 11,
"column": 5 "column": 5

@ -1,7 +1,7 @@
[ [
{ {
"code": "css_global_invalid_selector", "code": "css_global_invalid_selector",
"message": ":global(...) must contain exactly one selector", "message": "`:global(...)` must contain exactly one selector",
"start": { "start": {
"line": 5, "line": 5,
"column": 5 "column": 5

@ -1,7 +1,7 @@
[ [
{ {
"code": "css_global_invalid_selector", "code": "css_global_invalid_selector",
"message": ":global(...) must contain exactly one selector", "message": "`:global(...)` must contain exactly one selector",
"start": { "start": {
"line": 2, "line": 2,
"column": 5 "column": 5

@ -1,7 +1,7 @@
[ [
{ {
"code": "css_global_invalid_selector", "code": "css_global_invalid_selector",
"message": ":global(...) must contain exactly one selector", "message": "`:global(...)` must contain exactly one selector",
"start": { "start": {
"line": 5, "line": 5,
"column": 1 "column": 1

@ -1,7 +1,7 @@
[ [
{ {
"code": "css_global_invalid_selector", "code": "css_global_invalid_selector",
"message": ":global(...) must contain exactly one selector", "message": "`:global(...)` must contain exactly one selector",
"start": { "start": {
"line": 5, "line": 5,
"column": 1 "column": 1

@ -1,7 +1,7 @@
[ [
{ {
"code": "css_global_invalid_selector_list", "code": "css_global_invalid_selector_list",
"message": ":global(...) must not contain type or universal selectors when used in a compound selector", "message": "`:global(...)` must not contain type or universal selectors when used in a compound selector",
"start": { "start": {
"line": 20, "line": 20,
"column": 6 "column": 6

@ -1,7 +1,7 @@
[ [
{ {
"code": "css_global_invalid_selector", "code": "css_global_invalid_selector",
"message": ":global(...) must contain exactly one selector", "message": "`:global(...)` must contain exactly one selector",
"start": { "start": {
"line": 11, "line": 11,
"column": 5 "column": 5

@ -1,7 +1,7 @@
[ [
{ {
"code": "css_type_selector_invalid_placement", "code": "css_type_selector_invalid_placement",
"message": ":global(...) must not be followed with a type selector", "message": "`:global(...)` must not be followed by a type selector",
"start": { "start": {
"line": 17, "line": 17,
"column": 14 "column": 14

@ -1322,17 +1322,32 @@ declare module 'svelte/compiler' {
metadata: { metadata: {
parent_rule: null | Rule; parent_rule: null | Rule;
has_local_selectors: boolean; has_local_selectors: boolean;
/**
* `true` if the rule contains a `:global` selector, and therefore everything inside should be unscoped
*/
is_global_block: boolean; is_global_block: boolean;
}; };
} }
/**
* A list of selectors, e.g. `a, b, c {}`
*/
export interface SelectorList extends BaseNode { export interface SelectorList extends BaseNode {
type: 'SelectorList'; type: 'SelectorList';
/**
* The `a`, `b` and `c` in `a, b, c {}`
*/
children: ComplexSelector[]; children: ComplexSelector[];
} }
/**
* A complex selector, e.g. `a b c {}`
*/
export interface ComplexSelector extends BaseNode { export interface ComplexSelector extends BaseNode {
type: 'ComplexSelector'; type: 'ComplexSelector';
/**
* The `a`, `b` and `c` in `a b c {}`
*/
children: RelativeSelector[]; children: RelativeSelector[];
metadata: { metadata: {
rule: null | Rule; rule: null | Rule;
@ -1340,14 +1355,26 @@ declare module 'svelte/compiler' {
}; };
} }
/**
* A relative selector, e.g the `a` and `> b` in `a > b {}`
*/
export interface RelativeSelector extends BaseNode { export interface RelativeSelector extends BaseNode {
type: 'RelativeSelector'; type: 'RelativeSelector';
/**
* In `a > b`, `> b` forms one relative selector, and `>` is the combinator. `null` for the first selector.
*/
combinator: null | Combinator; combinator: null | Combinator;
/**
* The `b:is(...)` in `> b:is(...)`
*/
selectors: SimpleSelector[]; selectors: SimpleSelector[];
metadata: { metadata: {
/** :global(..) */ /**
* `true` if the whole selector is unscoped, e.g. `:global(...)` or `:global` or `:global.x`.
* Selectors like `:global(...).x` are not considered global, because they still need scoping.
*/
is_global: boolean; is_global: boolean;
/** :root, :host, ::view-transition */ /** `:root`, `:host`, `::view-transition`, or selectors after a `:global` */
is_global_like: boolean; is_global_like: boolean;
scoped: boolean; scoped: boolean;
}; };

@ -202,9 +202,18 @@ This is because the Svelte compiler treated the assignment to `foo.value` as an
Assignments to destructured parts of a `@const` declaration are no longer allowed. It was an oversight that this was ever allowed. Assignments to destructured parts of a `@const` declaration are no longer allowed. It was an oversight that this was ever allowed.
### Stricter CSS `:global` selector validation ### :is(...) and :where(...) are scoped
Previously, a compound selector starting with a global modifier which has universal or type selectors (like `:global(span).foo`) was valid. In Svelte 5, this is a validation error instead. The reason is that in this selector the resulting CSS would be equivalent to one without `:global` - in other words, `:global` is ignored in this case. Previously, Svelte did not analyse selectors inside `:is(...)` and `:where(...)`, effectively treating them as global. Svelte 5 analyses them in the context of the current component. As such, some selectors may now be treated as unused if they were relying on this treatment. To fix this, use `:global(...)` inside the `:is(...)/:where(...)` selectors.
When using Tailwind's `@apply` directive, add a `:global` selector to preserve rules that use Tailwind-generated `:is(...)` selectors:
```diff
- main {
+ main :global {
@apply bg-blue-100 dark:bg-blue-900
}
```
### CSS hash position no longer deterministic ### CSS hash position no longer deterministic

Loading…
Cancel
Save