From 90106ad65c0d680f944854cc6aa1e8ab6eb92e1f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 09:03:06 -0500 Subject: [PATCH 01/22] fix traversal, but break some other stuff --- .../phases/2-analyze/css/css-prune.js | 31 +++---------------- .../expected.css | 4 +++ .../input.svelte | 9 ++++++ 3 files changed, 17 insertions(+), 27 deletions(-) create mode 100644 packages/svelte/tests/css/samples/descendant-selector-unmatched/expected.css create mode 100644 packages/svelte/tests/css/samples/descendant-selector-unmatched/input.svelte diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index 7da86538b771..b2805fed6156 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -146,36 +146,13 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { relative_selector.combinator.type === 'Combinator' && relative_selector.combinator.name === ' ' ) { - // TODO this is incorrect, it will match `.this-matches .this-does-not .this-does {...}` - for (const ancestor_selector of relative_selectors) { - if (ancestor_selector.metadata.is_global) { - continue; - } + /** @type {typeof element | null} */ + let parent = element; - if (ancestor_selector.metadata.is_host) { + while ((parent = get_element_parent(parent))) { + if (apply_selector(relative_selectors.slice(), rule, parent, stylesheet)) { return mark(relative_selector, element); } - - /** @type {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} */ - let parent = element; - let matched = false; - while ((parent = get_element_parent(parent))) { - if ( - relative_selector_might_apply_to_node(ancestor_selector, rule, parent, stylesheet) !== - NO_MATCH - ) { - mark(ancestor_selector, parent); - matched = true; - } - } - - if (matched) { - return mark(relative_selector, element); - } - } - - if (relative_selectors.every((relative_selector) => relative_selector.metadata.is_global)) { - return mark(relative_selector, element); } return false; diff --git a/packages/svelte/tests/css/samples/descendant-selector-unmatched/expected.css b/packages/svelte/tests/css/samples/descendant-selector-unmatched/expected.css new file mode 100644 index 000000000000..52c8c72109e8 --- /dev/null +++ b/packages/svelte/tests/css/samples/descendant-selector-unmatched/expected.css @@ -0,0 +1,4 @@ + + /* (unused) x y z { + color: red; + }*/ diff --git a/packages/svelte/tests/css/samples/descendant-selector-unmatched/input.svelte b/packages/svelte/tests/css/samples/descendant-selector-unmatched/input.svelte new file mode 100644 index 000000000000..c31ae21efcad --- /dev/null +++ b/packages/svelte/tests/css/samples/descendant-selector-unmatched/input.svelte @@ -0,0 +1,9 @@ + + + + + From 8e40501bf608deb55118e8e6fb943836ebd2d4df Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 12:02:53 -0500 Subject: [PATCH 02/22] man this is fucken hard --- .../src/compiler/phases/1-parse/read/style.js | 6 +- .../phases/2-analyze/css/css-prune.js | 55 ++++++++++++---- .../src/compiler/phases/2-analyze/index.js | 53 --------------- .../compiler/phases/3-transform/css/index.js | 5 ++ .../src/compiler/phases/3-transform/index.js | 64 +++++++++++++++++-- packages/svelte/src/compiler/types/css.ts | 3 + .../css/samples/child-combinator/expected.css | 2 +- .../samples/dynamic-element-tag/expected.css | 2 +- .../expected.css | 2 +- .../expected.html | 4 +- .../general-siblings-combinator/expected.css | 2 +- .../siblings-combinator-star/expected.css | 2 +- .../siblings-combinator-star/expected.html | 4 +- .../samples/siblings-combinator/expected.css | 2 +- 14 files changed, 125 insertions(+), 81 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index dc08c8115102..6ccb65523d7e 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -114,7 +114,8 @@ function read_rule(parser) { start, end: parser.index, metadata: { - parent_rule: null + parent_rule: null, + has_local_selectors: false } }; } @@ -182,7 +183,8 @@ function read_selector(parser, inside_pseudo_class = false) { is_global: false, is_host: false, is_root: false, - scoped: false + scoped: false, + selected: new Set() } }; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index b2805fed6156..cd7f7e3c14b9 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -49,7 +49,8 @@ const nesting_selector = { is_global: false, is_host: false, is_root: false, - scoped: false + scoped: false, + selected: new Set() } }; @@ -66,6 +67,7 @@ export function prune(stylesheet, element) { const visitors = { ComplexSelector(node, context) { const selectors = truncate(node); + const inner = selectors[selectors.length - 1]; if (node.metadata.rule?.metadata.parent_rule) { const has_explicit_nesting_selector = selectors.some((selector) => @@ -90,6 +92,7 @@ const visitors = { context.state.stylesheet ) ) { + mark(inner, context.state.element); node.metadata.used = true; } @@ -138,7 +141,7 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { } if (applies === UNKNOWN_SELECTOR) { - return mark(relative_selector, element); + return true; } if (relative_selector.combinator) { @@ -146,12 +149,30 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { relative_selector.combinator.type === 'Combinator' && relative_selector.combinator.name === ' ' ) { - /** @type {typeof element | null} */ + /** @type {import('#compiler').TemplateNode | null} */ let parent = element; + let crossed_component_boundary = false; - while ((parent = get_element_parent(parent))) { - if (apply_selector(relative_selectors.slice(), rule, parent, stylesheet)) { - return mark(relative_selector, element); + while ((parent = /** @type {import('#compiler').TemplateNode | null} */ (parent.parent))) { + if (parent.type === 'Component' || parent.type === 'SvelteComponent') { + crossed_component_boundary = true; + + // ensure that any _other_ elements that use this selector end up getting scoped + // e.g. if you have `` and ``, we need to make sure that the + // `y` selector gets scoped + relative_selectors[relative_selectors.length - 1].metadata.scoped = true; + } + + if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') { + if (apply_selector(relative_selectors.slice(), rule, parent, stylesheet)) { + if (crossed_component_boundary) { + mark(relative_selectors[relative_selectors.length - 1], parent); + } else { + relative_selector.metadata.selected.add(element); + } + + return true; + } } } @@ -167,7 +188,7 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { has_global_parent || apply_selector(relative_selectors, rule, get_element_parent(element), stylesheet) ) { - return mark(relative_selector, element); + return true; } return false; @@ -191,7 +212,8 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { if (siblings.size === 0 && get_element_parent(element) !== null) { return false; } - return mark(relative_selector, element); + mark(relative_selector, element); + return true; } for (const possible_sibling of siblings.keys()) { @@ -205,10 +227,20 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { } // TODO other combinators - return mark(relative_selector, element); + return true; } - return mark(relative_selector, element); + // if this is the left-most non-global selector, mark it — we want + // `x y z {...}` to become `x.blah y z.blah {...}` + if ( + !relative_selectors.some( + ({ metadata }) => metadata.is_global || metadata.is_host || metadata.is_root + ) + ) { + mark(relative_selector, element); + } + + return true; } /** @@ -219,7 +251,8 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { */ function mark(relative_selector, element) { relative_selector.metadata.scoped = true; - element.metadata.scoped = true; + relative_selector.metadata.selected.add(element); + // element.metadata.scoped = true; return true; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index f19b60c03f95..85c277128ab9 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -466,59 +466,6 @@ export function analyze_component(root, options) { for (const element of analysis.elements) { prune(analysis.css.ast, element); } - - outer: for (const element of analysis.elements) { - if (element.metadata.scoped) { - // Dynamic elements in dom mode always use spread for attributes and therefore shouldn't have a class attribute added to them - // TODO this happens during the analysis phase, which shouldn't know anything about client vs server - if (element.type === 'SvelteElement' && options.generate === 'client') continue; - - /** @type {import('#compiler').Attribute | undefined} */ - let class_attribute = undefined; - - for (const attribute of element.attributes) { - if (attribute.type === 'SpreadAttribute') { - // The spread method appends the hash to the end of the class attribute on its own - continue outer; - } - - if (attribute.type !== 'Attribute') continue; - if (attribute.name.toLowerCase() !== 'class') continue; - - class_attribute = attribute; - } - - if (class_attribute && class_attribute.value !== true) { - const chunks = class_attribute.value; - - if (chunks.length === 1 && chunks[0].type === 'Text') { - chunks[0].data += ` ${analysis.css.hash}`; - } else { - chunks.push({ - type: 'Text', - data: ` ${analysis.css.hash}`, - raw: ` ${analysis.css.hash}`, - start: -1, - end: -1, - parent: null - }); - } - } else { - element.attributes.push( - create_attribute('class', -1, -1, [ - { - type: 'Text', - data: analysis.css.hash, - raw: analysis.css.hash, - parent: null, - start: -1, - end: -1 - } - ]) - ); - } - } - } } // TODO 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 f873c5d3a93a..4a7911bb795b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/css/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/css/index.js @@ -261,6 +261,11 @@ const visitors = { context.next(); }, + RelativeSelector(node, context) { + for (const element of node.metadata.selected) { + element.metadata.scoped = true; + } + }, PseudoClassSelector(node, context) { if (node.name === 'is' || node.name === 'where') { context.next(); diff --git a/packages/svelte/src/compiler/phases/3-transform/index.js b/packages/svelte/src/compiler/phases/3-transform/index.js index 708125f19e13..f81695bf64dc 100644 --- a/packages/svelte/src/compiler/phases/3-transform/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/index.js @@ -5,6 +5,7 @@ import { client_component, client_module } from './client/transform-client.js'; import { getLocator } from 'locate-character'; import { render_stylesheet } from './css/index.js'; import { merge_with_preprocessor_map, get_source_name } from '../../utils/mapped_code.js'; +import { create_attribute } from '../nodes.js'; /** * @param {import('../types').ComponentAnalysis} analysis @@ -24,6 +25,64 @@ export function transform_component(analysis, source, options) { }; } + const css = + analysis.css.ast && !analysis.inject_styles + ? render_stylesheet(source, analysis, options) + : null; + + outer: for (const element of analysis.elements) { + if (element.metadata.scoped) { + // Dynamic elements in dom mode always use spread for attributes and therefore shouldn't have a class attribute added to them + // TODO this happens during the analysis phase, which shouldn't know anything about client vs server + if (element.type === 'SvelteElement' && options.generate === 'client') continue; + + /** @type {import('#compiler').Attribute | undefined} */ + let class_attribute = undefined; + + for (const attribute of element.attributes) { + if (attribute.type === 'SpreadAttribute') { + // The spread method appends the hash to the end of the class attribute on its own + continue outer; + } + + if (attribute.type !== 'Attribute') continue; + if (attribute.name.toLowerCase() !== 'class') continue; + + class_attribute = attribute; + } + + if (class_attribute && class_attribute.value !== true) { + const chunks = class_attribute.value; + + if (chunks.length === 1 && chunks[0].type === 'Text') { + chunks[0].data += ` ${analysis.css.hash}`; + } else { + chunks.push({ + type: 'Text', + data: ` ${analysis.css.hash}`, + raw: ` ${analysis.css.hash}`, + start: -1, + end: -1, + parent: null + }); + } + } else { + element.attributes.push( + create_attribute('class', -1, -1, [ + { + type: 'Text', + data: analysis.css.hash, + raw: analysis.css.hash, + parent: null, + start: -1, + end: -1 + } + ]) + ); + } + } + } + const program = options.generate === 'server' ? server_component(analysis, options) @@ -51,11 +110,6 @@ export function transform_component(analysis, source, options) { }); merge_with_preprocessor_map(js, options, js_source_name); - const css = - analysis.css.ast && !analysis.inject_styles - ? render_stylesheet(source, analysis, options) - : null; - return { js, css, diff --git a/packages/svelte/src/compiler/types/css.ts b/packages/svelte/src/compiler/types/css.ts index b6f22874a04b..187b8ff577cc 100644 --- a/packages/svelte/src/compiler/types/css.ts +++ b/packages/svelte/src/compiler/types/css.ts @@ -1,3 +1,5 @@ +import type { RegularElement, SvelteElement } from './template'; + export interface BaseNode { start: number; end: number; @@ -54,6 +56,7 @@ export interface RelativeSelector extends BaseNode { is_host: boolean; is_root: boolean; scoped: boolean; + selected: Set; }; } diff --git a/packages/svelte/tests/css/samples/child-combinator/expected.css b/packages/svelte/tests/css/samples/child-combinator/expected.css index 3143c231be9f..4d7a2096cae8 100644 --- a/packages/svelte/tests/css/samples/child-combinator/expected.css +++ b/packages/svelte/tests/css/samples/child-combinator/expected.css @@ -2,6 +2,6 @@ background-color: red; } - main.svelte-xyz div:where(.svelte-xyz) > button:where(.svelte-xyz) { + main.svelte-xyz div > button:where(.svelte-xyz) { background-color: blue; } diff --git a/packages/svelte/tests/css/samples/dynamic-element-tag/expected.css b/packages/svelte/tests/css/samples/dynamic-element-tag/expected.css index 87d8325df18a..5d0d09629a76 100644 --- a/packages/svelte/tests/css/samples/dynamic-element-tag/expected.css +++ b/packages/svelte/tests/css/samples/dynamic-element-tag/expected.css @@ -7,7 +7,7 @@ h2.svelte-xyz span:where(.svelte-xyz) { color: red; } - h2.svelte-xyz > span:where(.svelte-xyz) > b:where(.svelte-xyz) { + h2.svelte-xyz > span > b:where(.svelte-xyz) { color: red; } h2.svelte-xyz span b:where(.svelte-xyz) { diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-star/expected.css b/packages/svelte/tests/css/samples/general-siblings-combinator-star/expected.css index 530213faac1c..7ec653d1beb9 100644 --- a/packages/svelte/tests/css/samples/general-siblings-combinator-star/expected.css +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-star/expected.css @@ -1,4 +1,4 @@ - .match.svelte-xyz > :where(.svelte-xyz) ~ :where(.svelte-xyz) { + .match.svelte-xyz > * ~ :where(.svelte-xyz) { margin-left: 4px; } /* (unused) .not-match > * ~ * { diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-star/expected.html b/packages/svelte/tests/css/samples/general-siblings-combinator-star/expected.html index 1cfae6e6f7ff..c97af84a65e5 100644 --- a/packages/svelte/tests/css/samples/general-siblings-combinator-star/expected.html +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-star/expected.html @@ -2,6 +2,6 @@
+
-
-
\ No newline at end of file + diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator/expected.css b/packages/svelte/tests/css/samples/general-siblings-combinator/expected.css index ff9874b9e45e..ffdd14f5c44b 100644 --- a/packages/svelte/tests/css/samples/general-siblings-combinator/expected.css +++ b/packages/svelte/tests/css/samples/general-siblings-combinator/expected.css @@ -1,6 +1,6 @@ div.svelte-xyz ~ article:where(.svelte-xyz) { color: green; } span.svelte-xyz ~ b:where(.svelte-xyz) { color: green; } - div.svelte-xyz span:where(.svelte-xyz) ~ b:where(.svelte-xyz) { color: green; } + div.svelte-xyz span ~ b:where(.svelte-xyz) { color: green; } .a.svelte-xyz ~ article:where(.svelte-xyz) { color: green; } div.svelte-xyz ~ .b:where(.svelte-xyz) { color: green; } .a.svelte-xyz ~ .c:where(.svelte-xyz) { color: green; } diff --git a/packages/svelte/tests/css/samples/siblings-combinator-star/expected.css b/packages/svelte/tests/css/samples/siblings-combinator-star/expected.css index 4986c4f715c2..1de61b6842d0 100644 --- a/packages/svelte/tests/css/samples/siblings-combinator-star/expected.css +++ b/packages/svelte/tests/css/samples/siblings-combinator-star/expected.css @@ -1,4 +1,4 @@ - .match.svelte-xyz > :where(.svelte-xyz) + :where(.svelte-xyz) { + .match.svelte-xyz > * + :where(.svelte-xyz) { margin-left: 4px; } /* (unused) .not-match > * + * { diff --git a/packages/svelte/tests/css/samples/siblings-combinator-star/expected.html b/packages/svelte/tests/css/samples/siblings-combinator-star/expected.html index 1cfae6e6f7ff..c97af84a65e5 100644 --- a/packages/svelte/tests/css/samples/siblings-combinator-star/expected.html +++ b/packages/svelte/tests/css/samples/siblings-combinator-star/expected.html @@ -2,6 +2,6 @@
+
-
-
\ No newline at end of file + diff --git a/packages/svelte/tests/css/samples/siblings-combinator/expected.css b/packages/svelte/tests/css/samples/siblings-combinator/expected.css index 97a7a4689793..5622a66a303f 100644 --- a/packages/svelte/tests/css/samples/siblings-combinator/expected.css +++ b/packages/svelte/tests/css/samples/siblings-combinator/expected.css @@ -16,7 +16,7 @@ span.svelte-xyz + b:where(.svelte-xyz) { color: green; } - div.svelte-xyz span:where(.svelte-xyz) + b:where(.svelte-xyz) { + div.svelte-xyz span + b:where(.svelte-xyz) { color: green; } .a.svelte-xyz + article:where(.svelte-xyz) { From 4985377e263dabd36c30459a5caa2bb60cd3574a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 12:11:23 -0500 Subject: [PATCH 03/22] fixes --- .../svelte/src/compiler/phases/2-analyze/css/css-prune.js | 8 ++++++++ .../svelte/src/compiler/phases/3-transform/css/index.js | 6 ++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index cd7f7e3c14b9..93cf00ecb89c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -149,6 +149,14 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { relative_selector.combinator.type === 'Combinator' && relative_selector.combinator.name === ' ' ) { + if ( + relative_selectors.every( + ({ metadata }) => metadata.is_global || metadata.is_host || metadata.is_root + ) + ) { + return true; + } + /** @type {import('#compiler').TemplateNode | null} */ let parent = element; let crossed_component_boundary = false; 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 4a7911bb795b..65c671c5e957 100644 --- a/packages/svelte/src/compiler/phases/3-transform/css/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/css/index.js @@ -262,8 +262,10 @@ const visitors = { context.next(); }, RelativeSelector(node, context) { - for (const element of node.metadata.selected) { - element.metadata.scoped = true; + if (node.metadata.scoped) { + for (const element of node.metadata.selected) { + element.metadata.scoped = true; + } } }, PseudoClassSelector(node, context) { From 76ca6d4408132df5ef0da5be29805afeeafe2542 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 12:31:56 -0500 Subject: [PATCH 04/22] getting closer --- .../phases/2-analyze/css/css-prune.js | 22 +++++++++---------- .../compiler/phases/3-transform/css/index.js | 2 ++ 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index 93cf00ecb89c..235df1b1b38a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -269,11 +269,11 @@ const regex_backslash_and_following_character = /\\(.)/g; /** * @param {import('#compiler').Css.RelativeSelector} relative_selector * @param {import('#compiler').Css.Rule} rule - * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node + * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element * @param {import('#compiler').Css.StyleSheet} stylesheet * @returns {NO_MATCH | POSSIBLE_MATCH | UNKNOWN_SELECTOR} */ -function relative_selector_might_apply_to_node(relative_selector, rule, node, stylesheet) { +function relative_selector_might_apply_to_node(relative_selector, rule, element, stylesheet) { if (relative_selector.metadata.is_host || relative_selector.metadata.is_root) return NO_MATCH; let i = relative_selector.selectors.length; @@ -298,7 +298,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, node, st let matched = false; for (const complex_selector of selector.args.children) { - if (apply_selector(truncate(complex_selector), rule, node, stylesheet)) { + if (apply_selector(truncate(complex_selector), rule, element, stylesheet)) { complex_selector.metadata.used = true; matched = true; } @@ -317,11 +317,11 @@ function relative_selector_might_apply_to_node(relative_selector, rule, node, st } case 'AttributeSelector': { - const whitelisted = whitelist_attribute_selector.get(node.name.toLowerCase()); + const whitelisted = whitelist_attribute_selector.get(element.name.toLowerCase()); if ( !whitelisted?.includes(selector.name.toLowerCase()) && !attribute_matches( - node, + element, selector.name, selector.value && unquote(selector.value), selector.matcher, @@ -335,8 +335,8 @@ function relative_selector_might_apply_to_node(relative_selector, rule, node, st case 'ClassSelector': { if ( - !attribute_matches(node, 'class', name, '~=', false) && - !node.attributes.some( + !attribute_matches(element, 'class', name, '~=', false) && + !element.attributes.some( (attribute) => attribute.type === 'ClassDirective' && attribute.name === name ) ) { @@ -347,7 +347,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, node, st } case 'IdSelector': { - if (!attribute_matches(node, 'id', name, '=', false)) { + if (!attribute_matches(element, 'id', name, '=', false)) { return NO_MATCH; } @@ -356,9 +356,9 @@ function relative_selector_might_apply_to_node(relative_selector, rule, node, st case 'TypeSelector': { if ( - node.name.toLowerCase() !== name.toLowerCase() && + element.name.toLowerCase() !== name.toLowerCase() && name !== '*' && - node.type !== 'SvelteElement' + element.type !== 'SvelteElement' ) { return NO_MATCH; } @@ -372,7 +372,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, node, st const parent = /** @type {import('#compiler').Css.Rule} */ (rule.metadata.parent_rule); for (const complex_selector of parent.prelude.children) { - if (apply_selector(truncate(complex_selector), parent, node, stylesheet)) { + if (apply_selector(truncate(complex_selector), parent, element, stylesheet)) { complex_selector.metadata.used = true; matched = true; } 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 65c671c5e957..11254e7fb85c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/css/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/css/index.js @@ -267,6 +267,8 @@ const visitors = { element.metadata.scoped = true; } } + + context.next(); }, PseudoClassSelector(node, context) { if (node.name === 'is' || node.name === 'where') { From a03849cd3530ca0c6bff189dd48247796a4e253d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 12:59:54 -0500 Subject: [PATCH 05/22] be conservative for now --- .../src/compiler/phases/2-analyze/css/css-prune.js | 10 ++++++---- playgrounds/sandbox/run.js | 1 + 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index 235df1b1b38a..2745b20bf969 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -160,6 +160,7 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { /** @type {import('#compiler').TemplateNode | null} */ let parent = element; let crossed_component_boundary = false; + let matched = false; while ((parent = /** @type {import('#compiler').TemplateNode | null} */ (parent.parent))) { if (parent.type === 'Component' || parent.type === 'SvelteComponent') { @@ -167,7 +168,7 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { // ensure that any _other_ elements that use this selector end up getting scoped // e.g. if you have `` and ``, we need to make sure that the - // `y` selector gets scoped + // `y` selector gets scoped, otherwise it might falsely apply to `` relative_selectors[relative_selectors.length - 1].metadata.scoped = true; } @@ -176,15 +177,16 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { if (crossed_component_boundary) { mark(relative_selectors[relative_selectors.length - 1], parent); } else { - relative_selector.metadata.selected.add(element); + mark(relative_selectors[relative_selectors.length - 1], parent); + // relative_selector.metadata.selected.add(element); } - return true; + matched = true; } } } - return false; + return matched; } if (relative_selector.combinator.name === '>') { diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js index 1dca76e867bb..9163ef0cb701 100644 --- a/playgrounds/sandbox/run.js +++ b/playgrounds/sandbox/run.js @@ -30,6 +30,7 @@ const svelte_modules = glob('**/*.svelte', { cwd: `${cwd}/input` }); const js_modules = glob('**/*.js', { cwd: `${cwd}/input` }); for (const generate of ['client', 'server']) { + console.error(`\n--- generating ${generate} ---\n`); for (const file of svelte_modules) { const input = `${cwd}/input/${file}`; const source = fs.readFileSync(input, 'utf-8'); From 7ac1225317f38463b3f16c3d5a28b912671426e8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 13:38:53 -0500 Subject: [PATCH 06/22] tidy --- packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index 2745b20bf969..ebeae3ff01ce 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -262,8 +262,6 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { function mark(relative_selector, element) { relative_selector.metadata.scoped = true; relative_selector.metadata.selected.add(element); - // element.metadata.scoped = true; - return true; } const regex_backslash_and_following_character = /\\(.)/g; From 4f0b788d05a9ee19f475664be2f277b9cdc4fa34 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 13:42:09 -0500 Subject: [PATCH 07/22] invert --- .../phases/2-analyze/css/css-prune.js | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index ebeae3ff01ce..b77d23ea6f0c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -126,7 +126,9 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { return relative_selectors.every(({ metadata }) => metadata.is_global || metadata.is_host); } - const relative_selector = relative_selectors.pop(); + const parent_selectors = relative_selectors.slice(); + const relative_selector = parent_selectors.pop(); + if (!relative_selector) return false; const applies = relative_selector_might_apply_to_node( @@ -150,7 +152,7 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { relative_selector.combinator.name === ' ' ) { if ( - relative_selectors.every( + parent_selectors.every( ({ metadata }) => metadata.is_global || metadata.is_host || metadata.is_root ) ) { @@ -169,15 +171,15 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { // ensure that any _other_ elements that use this selector end up getting scoped // e.g. if you have `` and ``, we need to make sure that the // `y` selector gets scoped, otherwise it might falsely apply to `` - relative_selectors[relative_selectors.length - 1].metadata.scoped = true; + parent_selectors[parent_selectors.length - 1].metadata.scoped = true; } if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') { - if (apply_selector(relative_selectors.slice(), rule, parent, stylesheet)) { + if (apply_selector(parent_selectors, rule, parent, stylesheet)) { if (crossed_component_boundary) { - mark(relative_selectors[relative_selectors.length - 1], parent); + mark(parent_selectors[parent_selectors.length - 1], parent); } else { - mark(relative_selectors[relative_selectors.length - 1], parent); + mark(parent_selectors[parent_selectors.length - 1], parent); // relative_selector.metadata.selected.add(element); } @@ -190,13 +192,13 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { } if (relative_selector.combinator.name === '>') { - const has_global_parent = relative_selectors.every( + const has_global_parent = parent_selectors.every( (relative_selector) => relative_selector.metadata.is_global ); if ( has_global_parent || - apply_selector(relative_selectors, rule, get_element_parent(element), stylesheet) + apply_selector(parent_selectors, rule, get_element_parent(element), stylesheet) ) { return true; } @@ -214,7 +216,7 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { // NOTE: if we have :global(), we couldn't figure out what is selected within `:global` due to the // css-tree limitation that does not parse the inner selector of :global // so unless we are sure there will be no sibling to match, we will consider it as matched - const has_global = relative_selectors.some( + const has_global = parent_selectors.some( (relative_selector) => relative_selector.metadata.is_global ); @@ -227,7 +229,7 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { } for (const possible_sibling of siblings.keys()) { - if (apply_selector(relative_selectors.slice(), rule, possible_sibling, stylesheet)) { + if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) { mark(relative_selector, element); has_match = true; } @@ -243,7 +245,7 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { // if this is the left-most non-global selector, mark it — we want // `x y z {...}` to become `x.blah y z.blah {...}` if ( - !relative_selectors.some( + !parent_selectors.some( ({ metadata }) => metadata.is_global || metadata.is_host || metadata.is_root ) ) { From 168eac7c9d9efdace3e0c7eb6313ef3d039709a0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 13:46:30 -0500 Subject: [PATCH 08/22] invert --- .../svelte/src/compiler/phases/2-analyze/css/css-prune.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index b77d23ea6f0c..354b37209e00 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -278,10 +278,7 @@ const regex_backslash_and_following_character = /\\(.)/g; function relative_selector_might_apply_to_node(relative_selector, rule, element, stylesheet) { if (relative_selector.metadata.is_host || relative_selector.metadata.is_root) return NO_MATCH; - let i = relative_selector.selectors.length; - while (i--) { - const selector = relative_selector.selectors[i]; - + for (const selector of relative_selector.selectors) { if (selector.type === 'Percentage' || selector.type === 'Nth') continue; const name = selector.name.replace(regex_backslash_and_following_character, '$1'); From 0dbcfa2f637fa910e13bc371be8af84e9442bb8e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 13:52:40 -0500 Subject: [PATCH 09/22] simplify --- .../phases/2-analyze/css/css-prune.js | 40 +++++++------------ 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index 354b37209e00..f705be13e34c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -11,10 +11,6 @@ import { error } from '../../../errors.js'; */ /** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */ -const NO_MATCH = 'NO_MATCH'; -const POSSIBLE_MATCH = 'POSSIBLE_MATCH'; -const UNKNOWN_SELECTOR = 'UNKNOWN_SELECTOR'; - const NODE_PROBABLY_EXISTS = 0; const NODE_DEFINITELY_EXISTS = 1; @@ -131,21 +127,17 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { if (!relative_selector) return false; - const applies = relative_selector_might_apply_to_node( + const possible_match = relative_selector_might_apply_to_node( relative_selector, rule, element, stylesheet ); - if (applies === NO_MATCH) { + if (!possible_match) { return false; } - if (applies === UNKNOWN_SELECTOR) { - return true; - } - if (relative_selector.combinator) { if ( relative_selector.combinator.type === 'Combinator' && @@ -269,15 +261,15 @@ function mark(relative_selector, element) { const regex_backslash_and_following_character = /\\(.)/g; /** + * Ensure that `element` satisfies each simple selector in `relative_selector` + * * @param {import('#compiler').Css.RelativeSelector} relative_selector * @param {import('#compiler').Css.Rule} rule * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element * @param {import('#compiler').Css.StyleSheet} stylesheet - * @returns {NO_MATCH | POSSIBLE_MATCH | UNKNOWN_SELECTOR} + * @returns {boolean} */ function relative_selector_might_apply_to_node(relative_selector, rule, element, stylesheet) { - if (relative_selector.metadata.is_host || relative_selector.metadata.is_root) return NO_MATCH; - for (const selector of relative_selector.selectors) { if (selector.type === 'Percentage' || selector.type === 'Nth') continue; @@ -286,11 +278,11 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, switch (selector.type) { case 'PseudoClassSelector': { if (name === 'host' || name === 'root') { - return NO_MATCH; + return false; } if (name === 'global' && relative_selector.selectors.length === 1) { - return NO_MATCH; + return false; } if ((name === 'is' || name === 'where') && selector.args) { @@ -304,7 +296,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, } if (!matched) { - return NO_MATCH; + return false; } } @@ -327,7 +319,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, selector.flags?.includes('i') ?? false ) ) { - return NO_MATCH; + return false; } break; } @@ -339,7 +331,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, (attribute) => attribute.type === 'ClassDirective' && attribute.name === name ) ) { - return NO_MATCH; + return false; } break; @@ -347,7 +339,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, case 'IdSelector': { if (!attribute_matches(element, 'id', name, '=', false)) { - return NO_MATCH; + return false; } break; @@ -359,7 +351,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, name !== '*' && element.type !== 'SvelteElement' ) { - return NO_MATCH; + return false; } break; @@ -378,18 +370,16 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, } if (!matched) { - return NO_MATCH; + return false; } break; } - - default: - return UNKNOWN_SELECTOR; } } - return POSSIBLE_MATCH; + // possible match + return true; } /** From 05701bdf3ad013ec444e4587c0cf057e6458978e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 14:37:28 -0500 Subject: [PATCH 10/22] switch --- .../phases/2-analyze/css/css-prune.js | 149 +++++++++--------- 1 file changed, 75 insertions(+), 74 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index f705be13e34c..8bf7155ce315 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -139,99 +139,100 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { } if (relative_selector.combinator) { - if ( - relative_selector.combinator.type === 'Combinator' && - relative_selector.combinator.name === ' ' - ) { - if ( - parent_selectors.every( - ({ metadata }) => metadata.is_global || metadata.is_host || metadata.is_root - ) - ) { - return true; - } + switch (relative_selector.combinator.name) { + case ' ': { + if ( + parent_selectors.every( + ({ metadata }) => metadata.is_global || metadata.is_host || metadata.is_root + ) + ) { + return true; + } - /** @type {import('#compiler').TemplateNode | null} */ - let parent = element; - let crossed_component_boundary = false; - let matched = false; + /** @type {import('#compiler').TemplateNode | null} */ + let parent = element; + let crossed_component_boundary = false; + let matched = false; - while ((parent = /** @type {import('#compiler').TemplateNode | null} */ (parent.parent))) { - if (parent.type === 'Component' || parent.type === 'SvelteComponent') { - crossed_component_boundary = true; + while ((parent = /** @type {import('#compiler').TemplateNode | null} */ (parent.parent))) { + if (parent.type === 'Component' || parent.type === 'SvelteComponent') { + crossed_component_boundary = true; - // ensure that any _other_ elements that use this selector end up getting scoped - // e.g. if you have `` and ``, we need to make sure that the - // `y` selector gets scoped, otherwise it might falsely apply to `` - parent_selectors[parent_selectors.length - 1].metadata.scoped = true; - } + // ensure that any _other_ elements that use this selector end up getting scoped + // e.g. if you have `` and ``, we need to make sure that the + // `y` selector gets scoped, otherwise it might falsely apply to `` + parent_selectors[parent_selectors.length - 1].metadata.scoped = true; + } - if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') { - if (apply_selector(parent_selectors, rule, parent, stylesheet)) { - if (crossed_component_boundary) { - mark(parent_selectors[parent_selectors.length - 1], parent); - } else { - mark(parent_selectors[parent_selectors.length - 1], parent); - // relative_selector.metadata.selected.add(element); - } + if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') { + if (apply_selector(parent_selectors, rule, parent, stylesheet)) { + if (crossed_component_boundary) { + mark(parent_selectors[parent_selectors.length - 1], parent); + } else { + mark(parent_selectors[parent_selectors.length - 1], parent); + // relative_selector.metadata.selected.add(element); + } - matched = true; + matched = true; + } } } - } - return matched; - } - - if (relative_selector.combinator.name === '>') { - const has_global_parent = parent_selectors.every( - (relative_selector) => relative_selector.metadata.is_global - ); - - if ( - has_global_parent || - apply_selector(parent_selectors, rule, get_element_parent(element), stylesheet) - ) { - return true; + return matched; } - return false; - } + case '>': { + const has_global_parent = parent_selectors.every( + (relative_selector) => relative_selector.metadata.is_global + ); - if (relative_selector.combinator.name === '+' || relative_selector.combinator.name === '~') { - const siblings = get_possible_element_siblings( - element, - relative_selector.combinator.name === '+' - ); - - let has_match = false; - // NOTE: if we have :global(), we couldn't figure out what is selected within `:global` due to the - // css-tree limitation that does not parse the inner selector of :global - // so unless we are sure there will be no sibling to match, we will consider it as matched - const has_global = parent_selectors.some( - (relative_selector) => relative_selector.metadata.is_global - ); - - if (has_global) { - if (siblings.size === 0 && get_element_parent(element) !== null) { - return false; + if ( + has_global_parent || + apply_selector(parent_selectors, rule, get_element_parent(element), stylesheet) + ) { + return true; } - mark(relative_selector, element); - return true; + + return false; } - for (const possible_sibling of siblings.keys()) { - if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) { + case '+': + case '~': { + const siblings = get_possible_element_siblings( + element, + relative_selector.combinator.name === '+' + ); + + let has_match = false; + // NOTE: if we have :global(), we couldn't figure out what is selected within `:global` due to the + // css-tree limitation that does not parse the inner selector of :global + // so unless we are sure there will be no sibling to match, we will consider it as matched + const has_global = parent_selectors.some( + (relative_selector) => relative_selector.metadata.is_global + ); + + if (has_global) { + if (siblings.size === 0 && get_element_parent(element) !== null) { + return false; + } mark(relative_selector, element); - has_match = true; + return true; } + + for (const possible_sibling of siblings.keys()) { + if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) { + mark(relative_selector, element); + has_match = true; + } + } + + return has_match; } - return has_match; + default: + // TODO other combinators + return true; } - - // TODO other combinators - return true; } // if this is the left-most non-global selector, mark it — we want From e5eef742d038dca788448b1a581f136a763f1c0e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 14:45:31 -0500 Subject: [PATCH 11/22] for now --- .../tests/css/samples/dynamic-element-tag/expected.css | 2 +- .../expected.css | 2 +- .../expected.html | 2 +- .../tests/css/samples/preserve-specificity/expected.css | 2 +- .../tests/css/samples/preserve-specificity/expected.html | 8 ++++---- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/svelte/tests/css/samples/dynamic-element-tag/expected.css b/packages/svelte/tests/css/samples/dynamic-element-tag/expected.css index 5d0d09629a76..80f7facd5a81 100644 --- a/packages/svelte/tests/css/samples/dynamic-element-tag/expected.css +++ b/packages/svelte/tests/css/samples/dynamic-element-tag/expected.css @@ -10,7 +10,7 @@ h2.svelte-xyz > span > b:where(.svelte-xyz) { color: red; } - h2.svelte-xyz span b:where(.svelte-xyz) { + h2.svelte-xyz span:where(.svelte-xyz) b:where(.svelte-xyz) { color: red; } h2.svelte-xyz b:where(.svelte-xyz) { diff --git a/packages/svelte/tests/css/samples/omit-scoping-attribute-whitespace-multiple/expected.css b/packages/svelte/tests/css/samples/omit-scoping-attribute-whitespace-multiple/expected.css index 772529431050..9fc102a2d0e7 100644 --- a/packages/svelte/tests/css/samples/omit-scoping-attribute-whitespace-multiple/expected.css +++ b/packages/svelte/tests/css/samples/omit-scoping-attribute-whitespace-multiple/expected.css @@ -1,3 +1,3 @@ - div.svelte-xyz section p:where(.svelte-xyz) { + div.svelte-xyz section:where(.svelte-xyz) p:where(.svelte-xyz) { color: red; } diff --git a/packages/svelte/tests/css/samples/omit-scoping-attribute-whitespace-multiple/expected.html b/packages/svelte/tests/css/samples/omit-scoping-attribute-whitespace-multiple/expected.html index 53c470b0c197..d6ee64cfb46c 100644 --- a/packages/svelte/tests/css/samples/omit-scoping-attribute-whitespace-multiple/expected.html +++ b/packages/svelte/tests/css/samples/omit-scoping-attribute-whitespace-multiple/expected.html @@ -1 +1 @@ -

this is styled

\ No newline at end of file +

this is styled

diff --git a/packages/svelte/tests/css/samples/preserve-specificity/expected.css b/packages/svelte/tests/css/samples/preserve-specificity/expected.css index 897ce85faaa9..4e179e066f24 100644 --- a/packages/svelte/tests/css/samples/preserve-specificity/expected.css +++ b/packages/svelte/tests/css/samples/preserve-specificity/expected.css @@ -1,4 +1,4 @@ - a.svelte-xyz b c span:where(.svelte-xyz) { + a.svelte-xyz b:where(.svelte-xyz) c:where(.svelte-xyz) span:where(.svelte-xyz) { color: red; font-size: 2em; font-family: 'Comic Sans MS'; diff --git a/packages/svelte/tests/css/samples/preserve-specificity/expected.html b/packages/svelte/tests/css/samples/preserve-specificity/expected.html index 171d90d362ba..3ad77eebb664 100644 --- a/packages/svelte/tests/css/samples/preserve-specificity/expected.html +++ b/packages/svelte/tests/css/samples/preserve-specificity/expected.html @@ -1,12 +1,12 @@ - - + + Big red Comic Sans - + Big red Comic Sans - \ No newline at end of file + From bcb0eb1070a3e8b6fee2e46b917b0dc9b8939c37 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 15:52:52 -0500 Subject: [PATCH 12/22] progress --- .../phases/2-analyze/css/css-prune.js | 81 ++++++++----------- .../compiler/phases/3-transform/css/index.js | 1 + .../global-with-child-combinator-2/_config.js | 9 +-- .../expected.css | 2 +- .../expected.html | 4 +- .../input.svelte | 4 +- .../global-with-child-combinator-3/_config.js | 5 -- .../expected.css | 3 - .../expected.html | 3 - .../input.svelte | 9 --- 10 files changed, 41 insertions(+), 80 deletions(-) delete mode 100644 packages/svelte/tests/css/samples/global-with-child-combinator-3/_config.js delete mode 100644 packages/svelte/tests/css/samples/global-with-child-combinator-3/expected.css delete mode 100644 packages/svelte/tests/css/samples/global-with-child-combinator-3/expected.html delete mode 100644 packages/svelte/tests/css/samples/global-with-child-combinator-3/input.svelte diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index 8bf7155ce315..eb63c345e36c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -113,15 +113,11 @@ function truncate(node) { /** * @param {import('#compiler').Css.RelativeSelector[]} relative_selectors * @param {import('#compiler').Css.Rule} rule - * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} element + * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element * @param {import('#compiler').Css.StyleSheet} stylesheet * @returns {boolean} */ function apply_selector(relative_selectors, rule, element, stylesheet) { - if (!element) { - return relative_selectors.every(({ metadata }) => metadata.is_global || metadata.is_host); - } - const parent_selectors = relative_selectors.slice(); const relative_selector = parent_selectors.pop(); @@ -139,69 +135,47 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { } if (relative_selector.combinator) { - switch (relative_selector.combinator.name) { - case ' ': { - if ( - parent_selectors.every( - ({ metadata }) => metadata.is_global || metadata.is_host || metadata.is_root - ) - ) { - return true; - } + const name = relative_selector.combinator.name; - /** @type {import('#compiler').TemplateNode | null} */ - let parent = element; + switch (name) { + case ' ': + case '>': { + let parent = /** @type {import('#compiler').TemplateNode | null} */ (element.parent); + + let parent_found = false; + let parent_matched = false; let crossed_component_boundary = false; - let matched = false; - while ((parent = /** @type {import('#compiler').TemplateNode | null} */ (parent.parent))) { + while (parent) { if (parent.type === 'Component' || parent.type === 'SvelteComponent') { crossed_component_boundary = true; - - // ensure that any _other_ elements that use this selector end up getting scoped - // e.g. if you have `` and ``, we need to make sure that the - // `y` selector gets scoped, otherwise it might falsely apply to `` - parent_selectors[parent_selectors.length - 1].metadata.scoped = true; } if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') { + parent_found = true; + if (apply_selector(parent_selectors, rule, parent, stylesheet)) { - if (crossed_component_boundary) { + if (name === ' ' || crossed_component_boundary) { mark(parent_selectors[parent_selectors.length - 1], parent); } else { - mark(parent_selectors[parent_selectors.length - 1], parent); - // relative_selector.metadata.selected.add(element); + parent_selectors[parent_selectors.length - 1].metadata.selected.add(parent); } - matched = true; + parent_matched = true; } - } - } - return matched; - } - - case '>': { - const has_global_parent = parent_selectors.every( - (relative_selector) => relative_selector.metadata.is_global - ); + if (name === '>') return parent_matched; + } - if ( - has_global_parent || - apply_selector(parent_selectors, rule, get_element_parent(element), stylesheet) - ) { - return true; + parent = /** @type {import('#compiler').TemplateNode | null} */ (parent.parent); } - return false; + return parent_matched || parent_selectors.every(is_global); } case '+': case '~': { - const siblings = get_possible_element_siblings( - element, - relative_selector.combinator.name === '+' - ); + const siblings = get_possible_element_siblings(element, name === '+'); let has_match = false; // NOTE: if we have :global(), we couldn't figure out what is selected within `:global` due to the @@ -259,6 +233,17 @@ function mark(relative_selector, element) { relative_selector.metadata.selected.add(element); } +/** @param {import('#compiler').Css.RelativeSelector} selector */ +function is_global(selector) { + if (selector.metadata.is_global || selector.metadata.is_host || selector.metadata.is_root) { + return true; + } + + // TODO :is(...), :where(...) + + return false; +} + const regex_backslash_and_following_character = /\\(.)/g; /** @@ -283,7 +268,9 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, } if (name === 'global' && relative_selector.selectors.length === 1) { - return false; + const args = /** @type {import('#compiler').Css.SelectorList} */ (selector.args); + const complex_selector = args.children[0]; + return apply_selector(complex_selector.children, rule, element, stylesheet); } if ((name === 'is' || name === 'where') && selector.args) { 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 11254e7fb85c..a330303f982d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/css/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/css/index.js @@ -201,6 +201,7 @@ const visitors = { for (const relative_selector of node.children) { if (relative_selector.metadata.is_global) { remove_global_pseudo_class(relative_selector.selectors[0]); + continue; } if (relative_selector.metadata.scoped) { diff --git a/packages/svelte/tests/css/samples/global-with-child-combinator-2/_config.js b/packages/svelte/tests/css/samples/global-with-child-combinator-2/_config.js index cbffb98fdc8b..292c6c49ac9d 100644 --- a/packages/svelte/tests/css/samples/global-with-child-combinator-2/_config.js +++ b/packages/svelte/tests/css/samples/global-with-child-combinator-2/_config.js @@ -1,12 +1,5 @@ import { test } from '../../test'; export default test({ - warnings: [ - { - code: 'css-unused-selector', - message: 'Unused CSS selector "a:global(.foo) > div"', - start: { character: 91, column: 1, line: 8 }, - end: { character: 111, column: 21, line: 8 } - } - ] + warnings: [] }); diff --git a/packages/svelte/tests/css/samples/global-with-child-combinator-2/expected.css b/packages/svelte/tests/css/samples/global-with-child-combinator-2/expected.css index 8b0a7637aea0..a7938255785f 100644 --- a/packages/svelte/tests/css/samples/global-with-child-combinator-2/expected.css +++ b/packages/svelte/tests/css/samples/global-with-child-combinator-2/expected.css @@ -1,3 +1,3 @@ - div > div.svelte-xyz { + a > b > div.svelte-xyz { color: red; } diff --git a/packages/svelte/tests/css/samples/global-with-child-combinator-2/expected.html b/packages/svelte/tests/css/samples/global-with-child-combinator-2/expected.html index 32ff99e34f39..a956085c56eb 100644 --- a/packages/svelte/tests/css/samples/global-with-child-combinator-2/expected.html +++ b/packages/svelte/tests/css/samples/global-with-child-combinator-2/expected.html @@ -1,3 +1,3 @@
-
-
\ No newline at end of file +
+ diff --git a/packages/svelte/tests/css/samples/global-with-child-combinator-2/input.svelte b/packages/svelte/tests/css/samples/global-with-child-combinator-2/input.svelte index 8cd223842213..146f302633cd 100644 --- a/packages/svelte/tests/css/samples/global-with-child-combinator-2/input.svelte +++ b/packages/svelte/tests/css/samples/global-with-child-combinator-2/input.svelte @@ -1,9 +1,9 @@
-
+
\ No newline at end of file diff --git a/packages/svelte/tests/css/samples/global-with-child-combinator-3/_config.js b/packages/svelte/tests/css/samples/global-with-child-combinator-3/_config.js deleted file mode 100644 index 292c6c49ac9d..000000000000 --- a/packages/svelte/tests/css/samples/global-with-child-combinator-3/_config.js +++ /dev/null @@ -1,5 +0,0 @@ -import { test } from '../../test'; - -export default test({ - warnings: [] -}); diff --git a/packages/svelte/tests/css/samples/global-with-child-combinator-3/expected.css b/packages/svelte/tests/css/samples/global-with-child-combinator-3/expected.css deleted file mode 100644 index a7938255785f..000000000000 --- a/packages/svelte/tests/css/samples/global-with-child-combinator-3/expected.css +++ /dev/null @@ -1,3 +0,0 @@ - a > b > div.svelte-xyz { - color: red; - } diff --git a/packages/svelte/tests/css/samples/global-with-child-combinator-3/expected.html b/packages/svelte/tests/css/samples/global-with-child-combinator-3/expected.html deleted file mode 100644 index 32ff99e34f39..000000000000 --- a/packages/svelte/tests/css/samples/global-with-child-combinator-3/expected.html +++ /dev/null @@ -1,3 +0,0 @@ -
-
-
\ No newline at end of file diff --git a/packages/svelte/tests/css/samples/global-with-child-combinator-3/input.svelte b/packages/svelte/tests/css/samples/global-with-child-combinator-3/input.svelte deleted file mode 100644 index 146f302633cd..000000000000 --- a/packages/svelte/tests/css/samples/global-with-child-combinator-3/input.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - -
-
-
\ No newline at end of file From 109d192dcfd38574c078571530c9ea9d3979c495 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 16:03:56 -0500 Subject: [PATCH 13/22] i think it works? --- .../phases/2-analyze/css/css-prune.js | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index eb63c345e36c..f97122e14843 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -170,7 +170,7 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { parent = /** @type {import('#compiler').TemplateNode | null} */ (parent.parent); } - return parent_matched || parent_selectors.every(is_global); + return parent_matched || parent_selectors.every((selector) => is_global(selector, rule)); } case '+': @@ -233,13 +233,38 @@ function mark(relative_selector, element) { relative_selector.metadata.selected.add(element); } -/** @param {import('#compiler').Css.RelativeSelector} selector */ -function is_global(selector) { +/** + * @param {import('#compiler').Css.RelativeSelector} selector + * @param {import('#compiler').Css.Rule} rule + */ +function is_global(selector, rule) { if (selector.metadata.is_global || selector.metadata.is_host || selector.metadata.is_root) { return true; } - // TODO :is(...), :where(...) + if (selector.selectors.every((s) => s.type === 'NestingSelector')) { + const parent_rule = /** @type {import('#compiler').Css.Rule} */ (rule.metadata.parent_rule); + for (const complex_selector of parent_rule.prelude.children) { + for (const relative_selector of complex_selector.children) { + if (is_global(relative_selector, parent_rule)) { + return true; // we only need one for it to be considered a match + } + } + } + } + + if (selector.selectors.length === 1) { + const s = selector.selectors[0]; + if (s.type === 'PseudoClassSelector' && (s.name === 'is' || s.name === 'where') && s.args) { + for (const complex_selector of s.args.children) { + for (const relative_selector of complex_selector.children) { + if (is_global(relative_selector, rule)) { + return true; // we only need one for it to be considered a match + } + } + } + } + } return false; } From c9a79f0f78afab1f727f6a5f8c27d4404da6f6ff Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 16:31:22 -0500 Subject: [PATCH 14/22] fix --- .../svelte/src/compiler/phases/1-parse/read/style.js | 3 +-- .../src/compiler/phases/2-analyze/css/css-prune.js | 8 ++++---- .../svelte/src/compiler/phases/3-transform/css/index.js | 9 --------- packages/svelte/src/compiler/types/css.ts | 1 - 4 files changed, 5 insertions(+), 16 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 6ccb65523d7e..5cde3bc3c6fe 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -183,8 +183,7 @@ function read_selector(parser, inside_pseudo_class = false) { is_global: false, is_host: false, is_root: false, - scoped: false, - selected: new Set() + scoped: false } }; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index f97122e14843..3fddbfc52a09 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -45,8 +45,7 @@ const nesting_selector = { is_global: false, is_host: false, is_root: false, - scoped: false, - selected: new Set() + scoped: false } }; @@ -158,7 +157,8 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { if (name === ' ' || crossed_component_boundary) { mark(parent_selectors[parent_selectors.length - 1], parent); } else { - parent_selectors[parent_selectors.length - 1].metadata.selected.add(parent); + parent.metadata.scoped = true; + // parent_selectors[parent_selectors.length - 1].metadata.selected.add(parent); } parent_matched = true; @@ -230,7 +230,7 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { */ function mark(relative_selector, element) { relative_selector.metadata.scoped = true; - relative_selector.metadata.selected.add(element); + element.metadata.scoped = true; } /** 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 a330303f982d..ebb65a768c1a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/css/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/css/index.js @@ -262,15 +262,6 @@ const visitors = { context.next(); }, - RelativeSelector(node, context) { - if (node.metadata.scoped) { - for (const element of node.metadata.selected) { - element.metadata.scoped = true; - } - } - - context.next(); - }, PseudoClassSelector(node, context) { if (node.name === 'is' || node.name === 'where') { context.next(); diff --git a/packages/svelte/src/compiler/types/css.ts b/packages/svelte/src/compiler/types/css.ts index 187b8ff577cc..5c7a2b5c8346 100644 --- a/packages/svelte/src/compiler/types/css.ts +++ b/packages/svelte/src/compiler/types/css.ts @@ -56,7 +56,6 @@ export interface RelativeSelector extends BaseNode { is_host: boolean; is_root: boolean; scoped: boolean; - selected: Set; }; } From 7c9b11d3ea01a54c5a76c284be41946104183b3f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 16:39:51 -0500 Subject: [PATCH 15/22] tidy up --- .../svelte/src/compiler/phases/2-analyze/css/css-prune.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index 3fddbfc52a09..df84be5a626b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -141,7 +141,6 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { case '>': { let parent = /** @type {import('#compiler').TemplateNode | null} */ (element.parent); - let parent_found = false; let parent_matched = false; let crossed_component_boundary = false; @@ -151,14 +150,10 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { } if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') { - parent_found = true; - if (apply_selector(parent_selectors, rule, parent, stylesheet)) { + // TODO the `name === ' '` causes false positives, but removing it causes false negatives... if (name === ' ' || crossed_component_boundary) { mark(parent_selectors[parent_selectors.length - 1], parent); - } else { - parent.metadata.scoped = true; - // parent_selectors[parent_selectors.length - 1].metadata.selected.add(parent); } parent_matched = true; From 25568672c07fafa2d4b1681ecdf97b3ff7e97216 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 16:42:25 -0500 Subject: [PATCH 16/22] revert some stuff --- .../src/compiler/phases/2-analyze/index.js | 53 ++++++++++++++++ .../src/compiler/phases/3-transform/index.js | 63 ++----------------- 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 85c277128ab9..f19b60c03f95 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -466,6 +466,59 @@ export function analyze_component(root, options) { for (const element of analysis.elements) { prune(analysis.css.ast, element); } + + outer: for (const element of analysis.elements) { + if (element.metadata.scoped) { + // Dynamic elements in dom mode always use spread for attributes and therefore shouldn't have a class attribute added to them + // TODO this happens during the analysis phase, which shouldn't know anything about client vs server + if (element.type === 'SvelteElement' && options.generate === 'client') continue; + + /** @type {import('#compiler').Attribute | undefined} */ + let class_attribute = undefined; + + for (const attribute of element.attributes) { + if (attribute.type === 'SpreadAttribute') { + // The spread method appends the hash to the end of the class attribute on its own + continue outer; + } + + if (attribute.type !== 'Attribute') continue; + if (attribute.name.toLowerCase() !== 'class') continue; + + class_attribute = attribute; + } + + if (class_attribute && class_attribute.value !== true) { + const chunks = class_attribute.value; + + if (chunks.length === 1 && chunks[0].type === 'Text') { + chunks[0].data += ` ${analysis.css.hash}`; + } else { + chunks.push({ + type: 'Text', + data: ` ${analysis.css.hash}`, + raw: ` ${analysis.css.hash}`, + start: -1, + end: -1, + parent: null + }); + } + } else { + element.attributes.push( + create_attribute('class', -1, -1, [ + { + type: 'Text', + data: analysis.css.hash, + raw: analysis.css.hash, + parent: null, + start: -1, + end: -1 + } + ]) + ); + } + } + } } // TODO diff --git a/packages/svelte/src/compiler/phases/3-transform/index.js b/packages/svelte/src/compiler/phases/3-transform/index.js index f81695bf64dc..05d0ec371b2f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/index.js @@ -25,64 +25,6 @@ export function transform_component(analysis, source, options) { }; } - const css = - analysis.css.ast && !analysis.inject_styles - ? render_stylesheet(source, analysis, options) - : null; - - outer: for (const element of analysis.elements) { - if (element.metadata.scoped) { - // Dynamic elements in dom mode always use spread for attributes and therefore shouldn't have a class attribute added to them - // TODO this happens during the analysis phase, which shouldn't know anything about client vs server - if (element.type === 'SvelteElement' && options.generate === 'client') continue; - - /** @type {import('#compiler').Attribute | undefined} */ - let class_attribute = undefined; - - for (const attribute of element.attributes) { - if (attribute.type === 'SpreadAttribute') { - // The spread method appends the hash to the end of the class attribute on its own - continue outer; - } - - if (attribute.type !== 'Attribute') continue; - if (attribute.name.toLowerCase() !== 'class') continue; - - class_attribute = attribute; - } - - if (class_attribute && class_attribute.value !== true) { - const chunks = class_attribute.value; - - if (chunks.length === 1 && chunks[0].type === 'Text') { - chunks[0].data += ` ${analysis.css.hash}`; - } else { - chunks.push({ - type: 'Text', - data: ` ${analysis.css.hash}`, - raw: ` ${analysis.css.hash}`, - start: -1, - end: -1, - parent: null - }); - } - } else { - element.attributes.push( - create_attribute('class', -1, -1, [ - { - type: 'Text', - data: analysis.css.hash, - raw: analysis.css.hash, - parent: null, - start: -1, - end: -1 - } - ]) - ); - } - } - } - const program = options.generate === 'server' ? server_component(analysis, options) @@ -110,6 +52,11 @@ export function transform_component(analysis, source, options) { }); merge_with_preprocessor_map(js, options, js_source_name); + const css = + analysis.css.ast && !analysis.inject_styles + ? render_stylesheet(source, analysis, options) + : null; + return { js, css, From 9ea0340893b74d35d2c5c9d6f31e7a87136f7a16 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 16:43:17 -0500 Subject: [PATCH 17/22] remove some junk --- packages/svelte/src/compiler/phases/3-transform/index.js | 1 - packages/svelte/src/compiler/types/css.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/index.js b/packages/svelte/src/compiler/phases/3-transform/index.js index 05d0ec371b2f..708125f19e13 100644 --- a/packages/svelte/src/compiler/phases/3-transform/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/index.js @@ -5,7 +5,6 @@ import { client_component, client_module } from './client/transform-client.js'; import { getLocator } from 'locate-character'; import { render_stylesheet } from './css/index.js'; import { merge_with_preprocessor_map, get_source_name } from '../../utils/mapped_code.js'; -import { create_attribute } from '../nodes.js'; /** * @param {import('../types').ComponentAnalysis} analysis diff --git a/packages/svelte/src/compiler/types/css.ts b/packages/svelte/src/compiler/types/css.ts index 5c7a2b5c8346..b6f22874a04b 100644 --- a/packages/svelte/src/compiler/types/css.ts +++ b/packages/svelte/src/compiler/types/css.ts @@ -1,5 +1,3 @@ -import type { RegularElement, SvelteElement } from './template'; - export interface BaseNode { start: number; end: number; From 9284e4a7323102fd77e55ef62d1adfe44ce09a3f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 17:00:54 -0500 Subject: [PATCH 18/22] handle weird cases --- .../phases/2-analyze/css/css-prune.js | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index df84be5a626b..5535fada3a45 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -237,31 +237,39 @@ function is_global(selector, rule) { return true; } - if (selector.selectors.every((s) => s.type === 'NestingSelector')) { - const parent_rule = /** @type {import('#compiler').Css.Rule} */ (rule.metadata.parent_rule); - for (const complex_selector of parent_rule.prelude.children) { - for (const relative_selector of complex_selector.children) { - if (is_global(relative_selector, parent_rule)) { - return true; // we only need one for it to be considered a match + for (const s of selector.selectors) { + if (s.type === 'PseudoClassSelector') { + if ((s.name === 'is' || s.name === 'where') && s.args) { + const has_global_selectors = s.args.children.some((complex_selector) => { + return complex_selector.children.every((relative_selector) => + is_global(relative_selector, rule) + ); + }); + + if (has_global_selectors) { + continue; } } } - } - if (selector.selectors.length === 1) { - const s = selector.selectors[0]; - if (s.type === 'PseudoClassSelector' && (s.name === 'is' || s.name === 'where') && s.args) { - for (const complex_selector of s.args.children) { - for (const relative_selector of complex_selector.children) { - if (is_global(relative_selector, rule)) { - return true; // we only need one for it to be considered a match - } - } + if (s.type === 'NestingSelector') { + const parent_rule = /** @type {import('#compiler').Css.Rule} */ (rule.metadata.parent_rule); + + const has_global_selectors = parent_rule.prelude.children.some((complex_selector) => { + return complex_selector.children.every((relative_selector) => + is_global(relative_selector, parent_rule) + ); + }); + + if (has_global_selectors) { + continue; } } + + return false; } - return false; + return true; } const regex_backslash_and_following_character = /\\(.)/g; From b43132e328c208973cda661f7c150a3306638654 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 17:06:24 -0500 Subject: [PATCH 19/22] update --- .../phases/2-analyze/css/css-prune.js | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index 5535fada3a45..cef232293fac 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -172,30 +172,20 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { case '~': { const siblings = get_possible_element_siblings(element, name === '+'); - let has_match = false; - // NOTE: if we have :global(), we couldn't figure out what is selected within `:global` due to the - // css-tree limitation that does not parse the inner selector of :global - // so unless we are sure there will be no sibling to match, we will consider it as matched - const has_global = parent_selectors.some( - (relative_selector) => relative_selector.metadata.is_global - ); - - if (has_global) { - if (siblings.size === 0 && get_element_parent(element) !== null) { - return false; - } - mark(relative_selector, element); - return true; - } + let sibling_matched = false; for (const possible_sibling of siblings.keys()) { if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) { mark(relative_selector, element); - has_match = true; + sibling_matched = true; } } - return has_match; + return ( + sibling_matched || + (get_element_parent(element) === null && + parent_selectors.every((selector) => is_global(selector, rule))) + ); } default: From b889a6d40b055cf8ad662462460bf80040772f76 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 17:09:54 -0500 Subject: [PATCH 20/22] tweak --- .../svelte/src/compiler/phases/2-analyze/css/css-prune.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index cef232293fac..36a85767cf61 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -196,11 +196,8 @@ function apply_selector(relative_selectors, rule, element, stylesheet) { // if this is the left-most non-global selector, mark it — we want // `x y z {...}` to become `x.blah y z.blah {...}` - if ( - !parent_selectors.some( - ({ metadata }) => metadata.is_global || metadata.is_host || metadata.is_root - ) - ) { + const parent = parent_selectors[parent_selectors.length - 1]; + if (!parent || is_global(parent, rule)) { mark(relative_selector, element); } From a5d4c421f1bf5846d88d4563189da38f5013f3b1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 17:19:51 -0500 Subject: [PATCH 21/22] shrink --- .../phases/2-analyze/css/css-prune.js | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index 36a85767cf61..c299612fd140 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -216,6 +216,10 @@ function mark(relative_selector, element) { } /** + * Returns `true` if the relative selector is global, meaning + * it's a `:global(...)` or `:host` or `:root` selector, or + * is an `:is(...)` or `:where(...)` selector that contains + * a global selector * @param {import('#compiler').Css.RelativeSelector} selector * @param {import('#compiler').Css.Rule} rule */ @@ -225,35 +229,30 @@ function is_global(selector, rule) { } for (const s of selector.selectors) { + /** @type {import('#compiler').Css.SelectorList | null} */ + let selector_list = null; + let owner = rule; + if (s.type === 'PseudoClassSelector') { if ((s.name === 'is' || s.name === 'where') && s.args) { - const has_global_selectors = s.args.children.some((complex_selector) => { - return complex_selector.children.every((relative_selector) => - is_global(relative_selector, rule) - ); - }); - - if (has_global_selectors) { - continue; - } + selector_list = s.args; } } if (s.type === 'NestingSelector') { - const parent_rule = /** @type {import('#compiler').Css.Rule} */ (rule.metadata.parent_rule); + owner = /** @type {import('#compiler').Css.Rule} */ (rule.metadata.parent_rule); + selector_list = owner.prelude; + } - const has_global_selectors = parent_rule.prelude.children.some((complex_selector) => { - return complex_selector.children.every((relative_selector) => - is_global(relative_selector, parent_rule) - ); - }); + const has_global_selectors = selector_list?.children.some((complex_selector) => { + return complex_selector.children.every((relative_selector) => + is_global(relative_selector, owner) + ); + }); - if (has_global_selectors) { - continue; - } + if (!has_global_selectors) { + return false; } - - return false; } return true; From 7a79279c3bbfef7990f7e0d3c7fe9c0c8407370d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 16 Feb 2024 17:26:58 -0500 Subject: [PATCH 22/22] changeset --- .changeset/beige-mirrors-listen.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/beige-mirrors-listen.md diff --git a/.changeset/beige-mirrors-listen.md b/.changeset/beige-mirrors-listen.md new file mode 100644 index 000000000000..896268149061 --- /dev/null +++ b/.changeset/beige-mirrors-listen.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly scope CSS selectors with descendant combinators