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.