diff --git a/packages/svelte/messages/compile-errors/style.md b/packages/svelte/messages/compile-errors/style.md
index f2a1c61e64..cce919a0c6 100644
--- a/packages/svelte/messages/compile-errors/style.md
+++ b/packages/svelte/messages/compile-errors/style.md
@@ -18,6 +18,14 @@
> A :global {...} block cannot be part of a selector list with more than one item
+## css_global_block_invalid_modifier
+
+> A :global {...} block cannot modify an existing selector
+
+## css_global_block_invalid_placement
+
+> :global at the start of a selector cannot have modifiers
+
## css_global_invalid_placement
> :global(...) can be at the start or end of a selector sequence, but not in the middle
diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js
index 3e229870b4..ea92bbefe1 100644
--- a/packages/svelte/src/compiler/errors.js
+++ b/packages/svelte/src/compiler/errors.js
@@ -461,6 +461,24 @@ 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");
}
+/**
+ * A :global {...} block cannot modify an existing selector
+ * @param {null | number | NodeLike} node
+ * @returns {never}
+ */
+export function css_global_block_invalid_modifier(node) {
+ e(node, "css_global_block_invalid_modifier", "A :global {...} block cannot modify an existing selector");
+}
+
+/**
+ * :global at the start of a selector cannot have modifiers
+ * @param {null | number | NodeLike} node
+ * @returns {never}
+ */
+export function css_global_block_invalid_placement(node) {
+ e(node, "css_global_block_invalid_placement", ":global at the start of a selector cannot have modifiers");
+}
+
/**
* :global(...) can be at the start or end of a selector sequence, but not in the middle
* @param {null | number | NodeLike} node
diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js
index 0b28668dec..fc7141cbab 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js
@@ -27,10 +27,25 @@ function is_global(relative_selector) {
return (
first.type === 'PseudoClassSelector' &&
first.name === 'global' &&
- relative_selector.selectors.every(
- (selector) =>
- selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
- )
+ (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(
+ (selector) =>
+ 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
);
}
@@ -53,16 +68,7 @@ const analysis_visitors = {
);
},
RelativeSelector(node, context) {
- node.metadata.is_global =
- node.selectors.length >= 1 &&
- node.selectors[0].type === 'PseudoClassSelector' &&
- node.selectors[0].name === 'global' &&
- // we want :global(...) and :global or :global.x, but not div:global
- (node.selectors[0].args === null ||
- node.selectors.every(
- (selector) =>
- selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
- ));
+ node.metadata.is_global = node.selectors.length >= 1 && is_global(node);
if (node.selectors.length === 1) {
const first = node.selectors[0];
@@ -87,18 +93,14 @@ const analysis_visitors = {
Rule(node, context) {
node.metadata.parent_rule = context.state.rule;
- // `:global {...}` or `div :global {...}` or `:global div`
node.metadata.is_global_block = node.prelude.children.some((selector) => {
let is_global_block = false;
for (const child of selector.children) {
- const idx = child.selectors.findIndex(
- (s) => s.type === 'PseudoClassSelector' && s.name === 'global' && s.args === null
- );
+ const idx = child.selectors.findIndex(is_global_block_selector);
if (is_global_block) {
- // Do this here, not after setting it to true:
- // All selectors after :global are unscoped, but in e.g. div:global, div is still scoped
+ // All selectors after :global are unscoped
child.metadata.is_global_like = true;
}
@@ -139,19 +141,44 @@ const validation_visitors = {
}
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) => {
+ return r.selectors.some(is_global_block_selector);
+ });
+
+ if (!global_selector) {
+ throw new Error('Internal error: global block without :global selector');
+ }
+
+ const global_block_selector_idx =
+ global_selector.selectors.findIndex(is_global_block_selector);
+ const starts_with_nesting_selector =
+ complex_selector.children[0].selectors.length === 1 &&
+ complex_selector.children[0].selectors[0].type === 'NestingSelector';
+
+ if (
+ (!global_selector.combinator &&
+ global_selector.selectors.length > 1 &&
+ (global_block_selector_idx === 0 ||
+ (global_block_selector_idx === 1 && starts_with_nesting_selector))) ||
+ (global_selector.combinator?.name === ' ' && starts_with_nesting_selector)
+ ) {
+ // div { :global.x { ... } } desugars to div :global.x { ... } which results in div.svelte-hash.x { ... }
+ // but it would be very hard to code-mod that and certainly doesn't look like it to the user,
+ // therefore we make this an error
+ e.css_global_block_invalid_placement(global_selector);
+ }
if (
- relative_selector.combinator &&
- relative_selector.combinator.name !== ' ' &&
- relative_selector.selectors.length === 1
+ global_selector.combinator &&
+ // p :global {...} or p > :global.x {...} is valid
+ global_selector.combinator.name !== ' ' &&
+ global_selector.selectors.length === 1
) {
- const s = relative_selector.selectors[0];
- if (s.type === 'PseudoClassSelector' && s.name === 'global' && s.args === null) {
- e.css_global_block_invalid_combinator(
- relative_selector,
- relative_selector.combinator.name
- );
+ const next =
+ complex_selector.children[complex_selector.children.indexOf(global_selector) + 1];
+ // p > :global div {...} is valid, but p > :global > div {...} or p > :global {...} is not
+ if (!next || next.combinator?.name !== ' ') {
+ e.css_global_block_invalid_combinator(global_selector, global_selector.combinator.name);
}
}
@@ -159,7 +186,7 @@ const validation_visitors = {
if (
declaration &&
- // :global { color: red; } is invalid, but foo :global { color: red; } or foo:global { color: red; } is valid
+ // :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
diff --git a/packages/svelte/src/compiler/phases/3-transform/css/index.js b/packages/svelte/src/compiler/phases/3-transform/css/index.js
index 99f56191ec..794e098dd5 100644
--- a/packages/svelte/src/compiler/phases/3-transform/css/index.js
+++ b/packages/svelte/src/compiler/phases/3-transform/css/index.js
@@ -216,10 +216,18 @@ const visitors = {
ComplexSelector(node, context) {
const before_bumped = context.state.specificity.bumped;
- /** @param {Css.PseudoClassSelector} 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) {
- context.state.code.remove(selector.start, selector.start + ':global'.length);
+ 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
.remove(selector.start, selector.start + ':global('.length)
@@ -230,7 +238,8 @@ const visitors = {
for (const relative_selector of node.children) {
if (relative_selector.metadata.is_global) {
remove_global_pseudo_class(
- /** @type {Css.PseudoClassSelector} */ (relative_selector.selectors[0])
+ /** @type {Css.PseudoClassSelector} */ (relative_selector.selectors[0]),
+ relative_selector.combinator
);
continue;
}
@@ -250,7 +259,7 @@ const visitors = {
// for any :global() or :global at the middle of compound selector
for (const selector of relative_selector.selectors) {
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
- remove_global_pseudo_class(selector);
+ remove_global_pseudo_class(selector, null);
}
}
diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts
index 1e5895da6b..2771efd493 100644
--- a/packages/svelte/src/compiler/types/css.d.ts
+++ b/packages/svelte/src/compiler/types/css.d.ts
@@ -33,17 +33,32 @@ export namespace Css {
metadata: {
parent_rule: null | Rule;
has_local_selectors: boolean;
+ /**
+ * `true` if the rule contains a `:global` selector, and therefore everything inside should be unscoped
+ */
is_global_block: boolean;
};
}
+ /**
+ * A list of selectors, e.g. `a, b, c {}`
+ */
export interface SelectorList extends BaseNode {
type: 'SelectorList';
+ /**
+ * The `a`, `b` and `c` in `a, b, c {}`
+ */
children: ComplexSelector[];
}
+ /**
+ * A complex selector, e.g. `a b c {}`
+ */
export interface ComplexSelector extends BaseNode {
type: 'ComplexSelector';
+ /**
+ * The `a`, `b` and `c` in `a b c {}`
+ */
children: RelativeSelector[];
metadata: {
rule: null | Rule;
@@ -51,12 +66,24 @@ export namespace Css {
};
}
+ /**
+ * A relative selector, e.g the `a` and `> b` in `a > b {}`
+ */
export interface RelativeSelector extends BaseNode {
type: 'RelativeSelector';
+ /**
+ * In `a > b`, `> b` forms one relative selector, and `>` is the combinator. `null` for the first selector.
+ */
combinator: null | Combinator;
+ /**
+ * The `b:is(...)` in `> b:is(...)`
+ */
selectors: SimpleSelector[];
metadata: {
- /** `:global(...)` or `:global` or `:global.x` (but not `.x:global`) */
+ /**
+ * `true` if the whole selector is unscoped, e.g. `:global(...)` or `:global` or `:global.x`.
+ * Selectors like `:global(...).x` or `div:global` are not considered global, because they still need scoping.
+ */
is_global: boolean;
/** `:root`, `:host`, `::view-transition`, or selectors after a `:global` */
is_global_like: boolean;
diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-combinator-2/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-block-combinator-2/_config.js
new file mode 100644
index 0000000000..374f32da9f
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-combinator-2/_config.js
@@ -0,0 +1,9 @@
+import { test } from '../../test';
+
+export default test({
+ error: {
+ code: 'css_global_block_invalid_combinator',
+ message: 'A :global {...} block cannot follow a > combinator',
+ position: [87, 96]
+ }
+});
diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-combinator-2/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-block-combinator-2/main.svelte
new file mode 100644
index 0000000000..791a3aa2c6
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-combinator-2/main.svelte
@@ -0,0 +1,11 @@
+
diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-placement-1/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-placement-1/_config.js
new file mode 100644
index 0000000000..f85a68eaff
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/css-global-placement-1/_config.js
@@ -0,0 +1,9 @@
+import { test } from '../../test';
+
+export default test({
+ error: {
+ code: 'css_global_block_invalid_placement',
+ message: ':global at the start of a selector cannot have modifiers',
+ position: [68, 77]
+ }
+});
diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-placement-1/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-placement-1/main.svelte
new file mode 100644
index 0000000000..027874c7a7
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/css-global-placement-1/main.svelte
@@ -0,0 +1,11 @@
+
diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-placement-2/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-placement-2/_config.js
new file mode 100644
index 0000000000..3f4201a000
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/css-global-placement-2/_config.js
@@ -0,0 +1,9 @@
+import { test } from '../../test';
+
+export default test({
+ error: {
+ code: 'css_global_block_invalid_placement',
+ message: ':global at the start of a selector cannot have modifiers',
+ position: [81, 90]
+ }
+});
diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-placement-2/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-placement-2/main.svelte
new file mode 100644
index 0000000000..c4b77a2a6f
--- /dev/null
+++ b/packages/svelte/tests/compiler-errors/samples/css-global-placement-2/main.svelte
@@ -0,0 +1,13 @@
+
diff --git a/packages/svelte/tests/css/samples/global-block/_config.js b/packages/svelte/tests/css/samples/global-block/_config.js
index 32bb6cb394..a2550776b3 100644
--- a/packages/svelte/tests/css/samples/global-block/_config.js
+++ b/packages/svelte/tests/css/samples/global-block/_config.js
@@ -7,14 +7,14 @@ export default test({
code: 'css_unused_selector',
message: 'Unused CSS selector ".unused :global"',
start: {
- line: 41,
+ line: 53,
column: 1,
- character: 647
+ character: 752
},
end: {
- line: 41,
+ line: 53,
column: 16,
- character: 662
+ character: 767
}
}
]
diff --git a/packages/svelte/tests/css/samples/global-block/expected.css b/packages/svelte/tests/css/samples/global-block/expected.css
index e50d8207a7..3a025f5a05 100644
--- a/packages/svelte/tests/css/samples/global-block/expected.css
+++ b/packages/svelte/tests/css/samples/global-block/expected.css
@@ -5,7 +5,7 @@
}
/*}*/
- div.svelte-xyz {
+ div.svelte-xyz {
.y {
color: green;
}
@@ -20,18 +20,30 @@
}
}
- div.svelte-xyz p {
+ div.svelte-xyz p {
.y {
color: green;
}
}
/* div :global { &.x { ...} } is allowed, so div :global.x must be, too */
- div.svelte-xyz .x {
+ div.svelte-xyz {
+ &.x {
+ color: green;
+ }
+ }
+ div.svelte-xyz.x {
color: green;
}
/* div { :global { &.x { ...} } } is allowed, so div:global.x must be, too */
+ div.svelte-xyz {
+ /* :global {*/
+ &.x {
+ color: green;
+ }
+ /*}*/
+ }
div.svelte-xyz:is(html.dark-mode *) {
color: green;
}
diff --git a/packages/svelte/tests/css/samples/global-block/input.svelte b/packages/svelte/tests/css/samples/global-block/input.svelte
index 3aaa63849d..b77768f649 100644
--- a/packages/svelte/tests/css/samples/global-block/input.svelte
+++ b/packages/svelte/tests/css/samples/global-block/input.svelte
@@ -29,11 +29,23 @@
}
/* div :global { &.x { ...} } is allowed, so div :global.x must be, too */
+ div :global {
+ &.x {
+ color: green;
+ }
+ }
div :global.x {
color: green;
}
/* div { :global { &.x { ...} } } is allowed, so div:global.x must be, too */
+ div {
+ :global {
+ &.x {
+ color: green;
+ }
+ }
+ }
div:global:is(html.dark-mode *) {
color: green;
}
diff --git a/packages/svelte/tests/css/samples/global-nested-block/expected.css b/packages/svelte/tests/css/samples/global-nested-block/expected.css
index fd76204493..22d855b4c6 100644
--- a/packages/svelte/tests/css/samples/global-nested-block/expected.css
+++ b/packages/svelte/tests/css/samples/global-nested-block/expected.css
@@ -10,7 +10,7 @@
color: green;
}
- p:where(.svelte-xyz) {
+ p:where(.svelte-xyz) {
.y {
color: green;
}
diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md
index 879e39e5f0..5f0ee7c0ea 100644
--- a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md
+++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md
@@ -556,17 +556,22 @@ let props = $props();
If you're using TypeScript, you can declare the prop types:
+
```ts
-type MyProps = any;
-// ---cut---
-let { a, b, c, ...everythingElse }: MyProps = $props();
+interface MyProps {
+ required: string;
+ optional?: number;
+ partOfEverythingElse?: boolean;
+};
+
+let { required, optional, ...everythingElse }: MyProps = $props();
```
> In an earlier preview, `$props()` took a type argument. This caused bugs, since in a case like this...
>
> ```ts
> // @errors: 2558
-> let { x = 42 } = $props<{ x: string }>();
+> let { x = 42 } = $props<{ x?: string }>();
> ```
>
> ...TypeScript [widens the type](https://www.typescriptlang.org/play?#code/CYUwxgNghgTiAEAzArgOzAFwJYHtXwBIAHGHIgZwB4AVeAXnilQE8A+ACgEoAueagbgBQgiCAzwA3vAAe9eABYATPAC+c4qQqUp03uQwwsqAOaqOnIfCsB6a-AB6AfiA) of `x` to be `string | number`, instead of erroring.