always remove descendant selector before global

pull/12509/head
Simon Holthausen 2 years ago
parent b24253eecf
commit d39e8ca600

@ -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

@ -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

@ -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

@ -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);
}
}

@ -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;

@ -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]
}
});

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

@ -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]
}
});

@ -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_placement',
message: ':global at the start of a selector cannot have modifiers',
position: [81, 90]
}
});

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

@ -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
}
}
]

@ -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;
}

@ -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;
}

@ -10,7 +10,7 @@
color: green;
}
p:where(.svelte-xyz) {
p:where(.svelte-xyz) {
.y {
color: green;
}

@ -556,17 +556,22 @@ let props = $props();
If you're using TypeScript, you can declare the prop types:
<!-- prettier-ignore -->
```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.

Loading…
Cancel
Save