From c4ac0e01e75eb20e99f6a31e974c6bc5b5f00dc7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 28 Nov 2024 07:02:18 -0500 Subject: [PATCH] fix: better account for render tags when pruning CSS (#14456) Fixes #14399 Add a mechanism to connect render tags to snippets to know where to walk when coming across render tags --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Simon Holthausen --- .changeset/twelve-squids-wink.md | 5 + .../src/compiler/phases/1-parse/state/tag.js | 8 +- .../phases/2-analyze/css/css-prune.js | 270 +++++++----------- .../compiler/phases/2-analyze/css/utils.js | 11 +- .../src/compiler/phases/2-analyze/index.js | 16 +- .../phases/2-analyze/visitors/RenderTag.js | 19 +- .../phases/2-analyze/visitors/SnippetBlock.js | 2 + .../2-analyze/visitors/shared/component.js | 51 ++++ .../2-analyze/visitors/shared/snippets.js | 17 ++ .../svelte/src/compiler/phases/types.d.ts | 13 +- .../svelte/src/compiler/types/template.d.ts | 21 ++ .../css/samples/child-combinator/expected.css | 2 +- .../samples/dynamic-element-tag/expected.css | 2 +- .../expected.css | 4 + .../input.svelte | 13 + .../_config.js | 22 +- .../expected.css | 13 +- .../input.svelte | 12 +- .../expected.css | 2 +- .../expected.html | 2 +- .../general-siblings-combinator/expected.css | 2 +- .../samples/has-with-render-tag/_config.js | 20 ++ .../samples/has-with-render-tag/expected.css | 11 + .../samples/has-with-render-tag/input.svelte | 26 ++ .../Child.svelte | 5 + .../expected.css | 4 + .../input.svelte | 15 + .../Child.svelte | 5 + .../expected.css | 4 + .../input.svelte | 17 ++ .../expected.css | 4 + .../input.svelte | 13 + .../expected.css | 4 + .../input.svelte | 13 + .../Child.svelte | 1 + .../expected.css | 4 + .../input.svelte | 15 + .../siblings-combinator-slot/_config.js | 10 +- .../siblings-combinator-slot/expected.css | 7 +- .../siblings-combinator-slot/input.svelte | 6 +- .../siblings-combinator-star/expected.css | 2 +- .../siblings-combinator-star/expected.html | 2 +- .../samples/siblings-combinator/expected.css | 2 +- .../tests/css/samples/snippets/_config.js | 18 +- .../tests/css/samples/snippets/expected.css | 6 +- .../tests/css/samples/snippets/input.svelte | 4 +- 46 files changed, 493 insertions(+), 232 deletions(-) create mode 100644 .changeset/twelve-squids-wink.md create mode 100644 packages/svelte/src/compiler/phases/2-analyze/visitors/shared/snippets.js create mode 100644 packages/svelte/tests/css/samples/general-siblings-combinator-render-tag/expected.css create mode 100644 packages/svelte/tests/css/samples/general-siblings-combinator-render-tag/input.svelte create mode 100644 packages/svelte/tests/css/samples/has-with-render-tag/_config.js create mode 100644 packages/svelte/tests/css/samples/has-with-render-tag/expected.css create mode 100644 packages/svelte/tests/css/samples/has-with-render-tag/input.svelte create mode 100644 packages/svelte/tests/css/samples/siblings-combinator-component-default-snippet/Child.svelte create mode 100644 packages/svelte/tests/css/samples/siblings-combinator-component-default-snippet/expected.css create mode 100644 packages/svelte/tests/css/samples/siblings-combinator-component-default-snippet/input.svelte create mode 100644 packages/svelte/tests/css/samples/siblings-combinator-component-named-snippet/Child.svelte create mode 100644 packages/svelte/tests/css/samples/siblings-combinator-component-named-snippet/expected.css create mode 100644 packages/svelte/tests/css/samples/siblings-combinator-component-named-snippet/input.svelte create mode 100644 packages/svelte/tests/css/samples/siblings-combinator-missing-fallback/expected.css create mode 100644 packages/svelte/tests/css/samples/siblings-combinator-missing-fallback/input.svelte create mode 100644 packages/svelte/tests/css/samples/siblings-combinator-render-tag/expected.css create mode 100644 packages/svelte/tests/css/samples/siblings-combinator-render-tag/input.svelte create mode 100644 packages/svelte/tests/css/samples/siblings-combinator-slot-named-between-default/Child.svelte create mode 100644 packages/svelte/tests/css/samples/siblings-combinator-slot-named-between-default/expected.css create mode 100644 packages/svelte/tests/css/samples/siblings-combinator-slot-named-between-default/input.svelte diff --git a/.changeset/twelve-squids-wink.md b/.changeset/twelve-squids-wink.md new file mode 100644 index 000000000000..0fde3dfb8654 --- /dev/null +++ b/.changeset/twelve-squids-wink.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: better account for render tags when pruning CSS diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 1632099c2930..3366c9ec97ae 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -314,7 +314,10 @@ function open(parser) { name }, parameters: function_expression.params, - body: create_fragment() + body: create_fragment(), + metadata: { + sites: new Set() + } }); parser.stack.push(block); parser.fragments.push(block.body); @@ -605,7 +608,8 @@ function special(parser) { metadata: { dynamic: false, args_with_call_expression: new Set(), - path: [] + path: [], + snippets: 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 583b2c9f2db9..0c38114a306e 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 @@ -1,16 +1,9 @@ -/** @import { Visitors } from 'zimmerframe' */ /** @import * as Compiler from '#compiler' */ import { walk } from 'zimmerframe'; import { get_parent_rules, get_possible_values, is_outer_global } from './utils.js'; import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js'; import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js'; -/** - * @typedef {{ - * element: Compiler.AST.RegularElement | Compiler.AST.SvelteElement; - * from_render_tag: boolean; - * }} State - */ /** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */ const NODE_PROBABLY_EXISTS = 0; @@ -53,78 +46,32 @@ const nesting_selector = { /** * * @param {Compiler.Css.StyleSheet} stylesheet - * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag} element + * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element */ export function prune(stylesheet, element) { - if (element.type === 'RenderTag') { - const parent = get_element_parent(element); - if (!parent) return; - - walk(stylesheet, { element: parent, from_render_tag: true }, visitors); - } else { - walk(stylesheet, { element, from_render_tag: false }, visitors); - } -} - -/** @type {Visitors} */ -const visitors = { - Rule(node, context) { - if (node.metadata.is_global_block) { - context.visit(node.prelude); - } else { - context.next(); - } - }, - ComplexSelector(node, context) { - const selectors = get_relative_selectors(node); - const inner = selectors[selectors.length - 1]; - - if (context.state.from_render_tag) { - // We're searching for a match that crosses a render tag boundary. That means we have to both traverse up - // the element tree (to see if we find an entry point) but also remove selectors from the end (assuming - // they are part of the render tag we don't see). We do all possible combinations of both until we find a match. - /** @type {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null} */ - let element = context.state.element; - - while (element) { - const selectors_to_check = selectors.slice(); - - while (selectors_to_check.length > 0) { - selectors_to_check.pop(); - - if ( - apply_selector( - selectors_to_check, - /** @type {Compiler.Css.Rule} */ (node.metadata.rule), - element, - context.state - ) - ) { - mark(inner, element); - node.metadata.used = true; - return; - } - } - - element = get_element_parent(element); + walk(/** @type {Compiler.Css.Node} */ (stylesheet), null, { + Rule(node, context) { + if (node.metadata.is_global_block) { + context.visit(node.prelude); + } else { + context.next(); + } + }, + ComplexSelector(node) { + const selectors = get_relative_selectors(node); + + if ( + apply_selector(selectors, /** @type {Compiler.Css.Rule} */ (node.metadata.rule), element) + ) { + node.metadata.used = true; } - } else if ( - apply_selector( - selectors, - /** @type {Compiler.Css.Rule} */ (node.metadata.rule), - context.state.element, - context.state - ) - ) { - mark(inner, context.state.element); - node.metadata.used = true; - } - // note: we don't call context.next() here, we only recurse into - // selectors that don't belong to rules (i.e. inside `:is(...)` etc) - // when we encounter them below - } -}; + // note: we don't call context.next() here, we only recurse into + // selectors that don't belong to rules (i.e. inside `:is(...)` etc) + // when we encounter them below + } + }); +} /** * Retrieves the relative selectors (minus the trailing globals) from a complex selector. @@ -147,6 +94,7 @@ function get_relative_selectors(node) { has_explicit_nesting_selector = true; } }); + // if we found one we can break from the others if (has_explicit_nesting_selector) break; } @@ -199,89 +147,63 @@ function truncate(node) { * @param {Compiler.Css.RelativeSelector[]} relative_selectors * @param {Compiler.Css.Rule} rule * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element - * @param {State} state * @returns {boolean} */ -function apply_selector(relative_selectors, rule, element, state) { +function apply_selector(relative_selectors, rule, element) { const parent_selectors = relative_selectors.slice(); const relative_selector = parent_selectors.pop(); - if (!relative_selector) return false; + const matched = + !!relative_selector && + relative_selector_might_apply_to_node(relative_selector, rule, element) && + apply_combinator(relative_selector, parent_selectors, rule, element); - const possible_match = relative_selector_might_apply_to_node( - relative_selector, - rule, - element, - state - ); - - if (!possible_match) { - return false; - } - - if (relative_selector.combinator) { - return apply_combinator( - relative_selector.combinator, - relative_selector, - parent_selectors, - rule, - element, - state - ); - } + if (matched) { + if (!is_outer_global(relative_selector)) { + relative_selector.metadata.scoped = true; + } - // if this is the left-most non-global selector, mark it — we want - // `x y z {...}` to become `x.blah y z.blah {...}` - const parent = parent_selectors[parent_selectors.length - 1]; - if (!parent || is_global(parent, rule)) { - mark(relative_selector, element); + element.metadata.scoped = true; } - return true; + return matched; } /** - * @param {Compiler.Css.Combinator} combinator * @param {Compiler.Css.RelativeSelector} relative_selector * @param {Compiler.Css.RelativeSelector[]} parent_selectors * @param {Compiler.Css.Rule} rule - * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element - * @param {State} state + * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node * @returns {boolean} */ -function apply_combinator(combinator, relative_selector, parent_selectors, rule, element, state) { - const name = combinator.name; +function apply_combinator(relative_selector, parent_selectors, rule, node) { + if (!relative_selector.combinator) return true; + + const name = relative_selector.combinator.name; switch (name) { case ' ': case '>': { let parent_matched = false; - let crossed_component_boundary = false; - const path = element.metadata.path; + const path = node.metadata.path; let i = path.length; while (i--) { const parent = path[i]; - if (parent.type === 'Component' || parent.type === 'SvelteComponent') { - crossed_component_boundary = true; - } - if (parent.type === 'SnippetBlock') { - // We assume the snippet might be rendered in a place where the parent selectors match. - // (We could do more static analysis and check the render tag reference to see if this snippet block continues - // with elements that actually match the selector, but that would be a lot of work for little gain) - return true; + for (const site of parent.metadata.sites) { + if (apply_combinator(relative_selector, parent_selectors, rule, site)) { + return true; + } + } + + return false; } if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') { - if (apply_selector(parent_selectors, rule, parent, state)) { - // 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); - } - + if (apply_selector(parent_selectors, rule, parent)) { parent_matched = true; } @@ -294,7 +216,7 @@ function apply_combinator(combinator, relative_selector, parent_selectors, rule, case '+': case '~': { - const siblings = get_possible_element_siblings(element, name === '+'); + const siblings = get_possible_element_siblings(node, name === '+'); let sibling_matched = false; @@ -302,18 +224,16 @@ function apply_combinator(combinator, relative_selector, parent_selectors, rule, if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') { // `{@render foo()}

foo

` with `:global(.x) + p` is a match if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) { - mark(relative_selector, element); sibling_matched = true; } - } else if (apply_selector(parent_selectors, rule, possible_sibling, state)) { - mark(relative_selector, element); + } else if (apply_selector(parent_selectors, rule, possible_sibling)) { sibling_matched = true; } } return ( sibling_matched || - (get_element_parent(element) === null && + (get_element_parent(node) === null && parent_selectors.every((selector) => is_global(selector, rule))) ); } @@ -324,19 +244,6 @@ function apply_combinator(combinator, relative_selector, parent_selectors, rule, } } -/** - * Mark both the compound selector and the node it selects as encapsulated, - * for transformation in a later step - * @param {Compiler.Css.RelativeSelector} relative_selector - * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element - */ -function mark(relative_selector, element) { - if (!is_outer_global(relative_selector)) { - relative_selector.metadata.scoped = true; - } - element.metadata.scoped = true; -} - /** * Returns `true` if the relative selector is global, meaning * it's a `:global(...)` or unscopeable selector, or @@ -388,10 +295,9 @@ const regex_backslash_and_following_character = /\\(.)/g; * @param {Compiler.Css.RelativeSelector} relative_selector * @param {Compiler.Css.Rule} rule * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element - * @param {State} state - * @returns {boolean } + * @returns {boolean} */ -function relative_selector_might_apply_to_node(relative_selector, rule, element, state) { +function relative_selector_might_apply_to_node(relative_selector, rule, element) { // Sort :has(...) selectors in one bucket and everything else into another const has_selectors = []; const other_selectors = []; @@ -416,7 +322,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, // If this is a :has inside a global selector, we gotta include the element itself, too, // because the global selector might be for an element that's outside the component (e.g. :root). - const rules = [rule, ...get_parent_rules(rule)]; + const rules = get_parent_rules(rule); const include_self = rules.some((r) => r.prelude.children.some((c) => c.children.some((s) => is_global(s, r)))) || rules[rules.length - 1].prelude.children.some((c) => @@ -429,10 +335,12 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, descendant_elements.push(element); } - walk( - /** @type {Compiler.SvelteNode} */ (element.fragment), - { is_child: true }, - { + /** + * @param {Compiler.SvelteNode} node + * @param {{ is_child: boolean }} state + */ + function walk_children(node, state) { + walk(node, state, { _(node, context) { if (node.type === 'RegularElement' || node.type === 'SvelteElement') { descendant_elements.push(node); @@ -445,12 +353,18 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, } else { context.next(); } + } else if (node.type === 'RenderTag') { + for (const snippet of node.metadata.snippets) { + walk_children(snippet.body, context.state); + } } else { context.next(); } } - } - ); + }); + } + + walk_children(element.fragment, { is_child: true }); // :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes // upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the @@ -486,7 +400,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, if ( selectors.length === 0 /* is :global(...) */ || (element.metadata.scoped && selector_matched) || - apply_selector(selectors, rule, element, state) + apply_selector(selectors, rule, element) ) { complex_selector.metadata.used = true; selector_matched = matched = true; @@ -516,7 +430,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, ) { const args = selector.args; const complex_selector = args.children[0]; - return apply_selector(complex_selector.children, rule, element, state); + return apply_selector(complex_selector.children, rule, element); } // We came across a :global, everything beyond it is global and therefore a potential match @@ -565,7 +479,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, if (is_global) { complex_selector.metadata.used = true; matched = true; - } else if (apply_selector(relative, rule, element, state)) { + } else if (apply_selector(relative, rule, element)) { complex_selector.metadata.used = true; matched = true; } else if (complex_selector.children.length > 1 && (name == 'is' || name == 'where')) { @@ -649,7 +563,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, for (const complex_selector of parent.prelude.children) { if ( - apply_selector(get_relative_selectors(complex_selector), parent, element, state) || + apply_selector(get_relative_selectors(complex_selector), parent, element) || complex_selector.children.every((s) => is_global(s, parent)) ) { complex_selector.metadata.used = true; @@ -703,17 +617,28 @@ function get_following_sibling_elements(element, include_self) { // ...then walk them, starting from the node after the one // containing the element in question - for (const node of nodes.slice(nodes.indexOf(start) + 1)) { + + /** @param {Compiler.SvelteNode} node */ + function get_siblings(node) { walk(node, null, { RegularElement(node) { siblings.push(node); }, SvelteElement(node) { siblings.push(node); + }, + RenderTag(node) { + for (const snippet of node.metadata.snippets) { + get_siblings(snippet.body); + } } }); } + for (const node of nodes.slice(nodes.indexOf(start) + 1)) { + get_siblings(node); + } + if (include_self) { siblings.push(element); } @@ -858,7 +783,7 @@ function unquote(str) { } /** - * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag} node + * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node * @returns {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null} */ function get_element_parent(node) { @@ -877,7 +802,7 @@ function get_element_parent(node) { } /** - * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} node + * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node * @param {boolean} adjacent_only * @returns {Map} */ @@ -929,7 +854,28 @@ function get_possible_element_siblings(node, adjacent_only) { current = path[i]; - if (!current || !is_block(current)) break; + if (!current) break; + + if ( + current.type === 'Component' || + current.type === 'SvelteComponent' || + current.type === 'SvelteSelf' + ) { + continue; + } + + if (current.type === 'SnippetBlock') { + for (const site of current.metadata.sites) { + const siblings = get_possible_element_siblings(site, adjacent_only); + add_to_map(siblings, result); + + if (adjacent_only && current.metadata.sites.size === 1 && has_definite_elements(siblings)) { + return result; + } + } + } + + if (!is_block(current)) break; if (current.type === 'EachBlock' && fragment === current.body) { // `{#each ...}{/each}` — `` can be previous sibling of `` diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/utils.js b/packages/svelte/src/compiler/phases/2-analyze/css/utils.js index 0226a150c97d..07171a23bb9d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/utils.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/utils.js @@ -39,15 +39,14 @@ export function get_possible_values(chunk) { * @param {Css.Rule | null} rule */ export function get_parent_rules(rule) { - const parents = []; + const rules = []; - let parent = rule?.metadata.parent_rule; - while (parent) { - parents.push(parent); - parent = parent.metadata.parent_rule; + while (rule) { + rules.push(rule); + rule = rule.metadata.parent_rule; } - return parents; + return rules; } /** diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index ce34544eb4c8..60f48ac70c9b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -438,7 +438,9 @@ export function analyze_component(root, source, options) { : '', keyframes: [] }, - source + source, + snippet_renderers: new Map(), + snippets: new Set() }; if (!runes) { @@ -698,6 +700,16 @@ export function analyze_component(root, source, options) { ); } + for (const [node, resolved] of analysis.snippet_renderers) { + if (!resolved) { + node.metadata.snippets = analysis.snippets; + } + + for (const snippet of node.metadata.snippets) { + snippet.metadata.sites.add(node); + } + } + if ( analysis.uses_render_tags && (analysis.uses_slots || (!analysis.custom_element && analysis.slot_names.size > 0)) @@ -726,8 +738,6 @@ export function analyze_component(root, source, options) { } outer: for (const node of analysis.elements) { - if (node.type === 'RenderTag') continue; - if (node.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 diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js index 57bd80ff1cfe..045224276a2e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js @@ -4,6 +4,7 @@ import { unwrap_optional } from '../../../utils/ast.js'; import * as e from '../../../errors.js'; import { validate_opening_tag } from './shared/utils.js'; import { mark_subtree_dynamic } from './shared/fragment.js'; +import { is_resolved_snippet } from './shared/snippets.js'; /** * @param {AST.RenderTag} node @@ -13,13 +14,25 @@ export function RenderTag(node, context) { validate_opening_tag(node, context.state, '@'); node.metadata.path = [...context.path]; - context.state.analysis.elements.push(node); const callee = unwrap_optional(node.expression).callee; - node.metadata.dynamic = - callee.type !== 'Identifier' || context.state.scope.get(callee.name)?.kind !== 'normal'; + const binding = callee.type === 'Identifier' ? context.state.scope.get(callee.name) : null; + node.metadata.dynamic = binding?.kind !== 'normal'; + + /** + * If we can't unambiguously resolve this to a declaration, we + * must assume the worst and link the render tag to every snippet + */ + let resolved = callee.type === 'Identifier' && is_resolved_snippet(binding); + + if (binding?.initial?.type === 'SnippetBlock') { + // if this render tag unambiguously references a local snippet, our job is easy + node.metadata.snippets.add(binding.initial); + } + + context.state.analysis.snippet_renderers.set(node, resolved); context.state.analysis.uses_render_tags = true; const raw_args = unwrap_optional(node.expression).arguments; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js index 356cf2eeae68..c94b65e361d6 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js @@ -8,6 +8,8 @@ import * as e from '../../../errors.js'; * @param {Context} context */ export function SnippetBlock(node, context) { + context.state.analysis.snippets.add(node); + validate_block_not_empty(node.body, context); if (context.state.analysis.runes) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js index 9a432341a064..84a7e2f1303f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js @@ -9,12 +9,63 @@ import { validate_slot_attribute } from './attribute.js'; import { mark_subtree_dynamic } from './fragment.js'; +import { is_resolved_snippet } from './snippets.js'; /** * @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node * @param {Context} context */ export function visit_component(node, context) { + node.metadata.path = [...context.path]; + + // link this node to all the snippets that it could render, so that we can prune CSS correctly + node.metadata.snippets = new Set(); + + // 'resolved' means we know which snippets this component might render. if it is `false`, + // then `node.metadata.snippets` is populated with every locally defined snippet + // once analysis is complete + let resolved = true; + + for (const attribute of node.attributes) { + if (attribute.type === 'SpreadAttribute' || attribute.type === 'BindDirective') { + resolved = false; + continue; + } + + if (attribute.type !== 'Attribute' || !is_expression_attribute(attribute)) { + continue; + } + + const expression = get_attribute_expression(attribute); + + // given an attribute like `foo={bar}`, if `bar` resolves to an import or a prop + // then we know it doesn't reference a locally defined snippet. if it resolves + // to a `{#snippet bar()}` then we know _which_ snippet it resolves to. in all + // other cases, we can't know (without much more complex static analysis) which + // snippets the component might render, so we treat the component as unresolved + if (expression.type === 'Identifier') { + const binding = context.state.scope.get(expression.name); + + resolved &&= is_resolved_snippet(binding); + + if (binding?.initial?.type === 'SnippetBlock') { + node.metadata.snippets.add(binding.initial); + } + } else { + resolved = false; + } + } + + if (resolved) { + for (const child of node.fragment.nodes) { + if (child.type === 'SnippetBlock') { + node.metadata.snippets.add(child); + } + } + } + + context.state.analysis.snippet_renderers.set(node, resolved); + mark_subtree_dynamic(context.path); for (const attribute of node.attributes) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/snippets.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/snippets.js new file mode 100644 index 000000000000..124ae0b3158e --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/snippets.js @@ -0,0 +1,17 @@ +/** @import { Binding } from '#compiler' */ + +/** + * Returns `true` if a binding unambiguously resolves to a specific + * snippet declaration, or is external to the current component + * @param {Binding | null} binding + */ +export function is_resolved_snippet(binding) { + return ( + !binding || + binding.declaration_kind === 'import' || + binding.kind === 'prop' || + binding.kind === 'rest_prop' || + binding.kind === 'bindable_prop' || + binding?.initial?.type === 'SnippetBlock' + ); +} diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index f001a0c48cc2..fed5d66b2c4a 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -37,7 +37,7 @@ export interface ComponentAnalysis extends Analysis { instance: Js; template: Template; /** Used for CSS pruning and scoping */ - elements: Array; + elements: Array; runes: boolean; exports: Array<{ name: string; alias: string | null }>; /** Whether the component uses `$$props` */ @@ -72,6 +72,17 @@ export interface ComponentAnalysis extends Analysis { keyframes: string[]; }; source: string; + /** + * Every render tag/component, and whether it could be definitively resolved or not + */ + snippet_renderers: Map< + AST.RenderTag | AST.Component | AST.SvelteComponent | AST.SvelteSelf, + boolean + >; + /** + * Every snippet that is declared locally + */ + snippets: Set; } declare module 'estree' { diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 0be0b25ab458..efce217e8250 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -166,6 +166,9 @@ export namespace AST { dynamic: boolean; args_with_call_expression: Set; path: SvelteNode[]; + /** The set of locally-defined snippets that this render tag could correspond to, + * used for CSS pruning purposes */ + snippets: Set; }; } @@ -278,6 +281,10 @@ export namespace AST { metadata: { scopes: Record; dynamic: boolean; + /** The set of locally-defined snippets that this component tag could render, + * used for CSS pruning purposes */ + snippets: Set; + path: SvelteNode[]; }; } @@ -318,6 +325,10 @@ export namespace AST { /** @internal */ metadata: { scopes: Record; + /** The set of locally-defined snippets that this component tag could render, + * used for CSS pruning purposes */ + snippets: Set; + path: SvelteNode[]; }; } @@ -369,6 +380,10 @@ export namespace AST { /** @internal */ metadata: { scopes: Record; + /** The set of locally-defined snippets that this component tag could render, + * used for CSS pruning purposes */ + snippets: Set; + path: SvelteNode[]; }; } @@ -438,6 +453,12 @@ export namespace AST { expression: Identifier; parameters: Pattern[]; body: Fragment; + /** @internal */ + metadata: { + /** The set of components/render tags that could render this snippet, + * used for CSS pruning */ + sites: Set; + }; } export interface Attribute extends BaseNode { diff --git a/packages/svelte/tests/css/samples/child-combinator/expected.css b/packages/svelte/tests/css/samples/child-combinator/expected.css index 4d7a2096cae8..3143c231be9f 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 > button:where(.svelte-xyz) { + main.svelte-xyz div:where(.svelte-xyz) > 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 80f7facd5a81..edb81ed12f5c 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 > b:where(.svelte-xyz) { + h2.svelte-xyz > span:where(.svelte-xyz) > b:where(.svelte-xyz) { color: red; } h2.svelte-xyz span:where(.svelte-xyz) b:where(.svelte-xyz) { diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-render-tag/expected.css b/packages/svelte/tests/css/samples/general-siblings-combinator-render-tag/expected.css new file mode 100644 index 000000000000..37cb0b8e1fab --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-render-tag/expected.css @@ -0,0 +1,4 @@ + + h1.svelte-xyz ~ p:where(.svelte-xyz) { + color: green; + } diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-render-tag/input.svelte b/packages/svelte/tests/css/samples/general-siblings-combinator-render-tag/input.svelte new file mode 100644 index 000000000000..5a24653dcf1d --- /dev/null +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-render-tag/input.svelte @@ -0,0 +1,13 @@ +{#snippet foo()} +

this should be green

+{/snippet} + +

Hello

+ +{@render foo()} + + diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js index 6fe2e4ac591c..97e470d1c325 100644 --- a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/_config.js @@ -2,12 +2,6 @@ import { test } from '../../test'; export default test({ warnings: [ - { - code: 'css_unused_selector', - message: 'Unused CSS selector ".a ~ .b"', - start: { character: 110, column: 1, line: 10 }, - end: { character: 117, column: 8, line: 10 } - }, { code: 'css_unused_selector', message: 'Unused CSS selector ".b ~ .c"', @@ -17,26 +11,26 @@ export default test({ { code: 'css_unused_selector', message: 'Unused CSS selector ".c ~ .f"', - start: { character: 164, column: 1, line: 12 }, - end: { character: 171, column: 8, line: 12 } + start: { character: 162, column: 1, line: 12 }, + end: { character: 169, column: 8, line: 12 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".f ~ .g"', - start: { character: 191, column: 1, line: 13 }, - end: { character: 198, column: 8, line: 13 } + start: { character: 187, column: 1, line: 13 }, + end: { character: 194, column: 8, line: 13 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".b ~ .f"', - start: { character: 218, column: 1, line: 14 }, - end: { character: 225, column: 8, line: 14 } + start: { character: 212, column: 1, line: 14 }, + end: { character: 219, column: 8, line: 14 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".b ~ .g"', - start: { character: 245, column: 1, line: 15 }, - end: { character: 252, column: 8, line: 15 } + start: { character: 237, column: 1, line: 15 }, + end: { character: 244, column: 8, line: 15 } } ] }); diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/expected.css b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/expected.css index 06590583e8e9..67a19d10c996 100644 --- a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/expected.css +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/expected.css @@ -1,10 +1,11 @@ + .d.svelte-xyz ~ .e:where(.svelte-xyz) { color: green; } .a.svelte-xyz ~ .g:where(.svelte-xyz) { color: green; } + .a.svelte-xyz ~ .b:where(.svelte-xyz) { color: green; } /* no match */ - /* (unused) .a ~ .b { color: green; }*/ - /* (unused) .b ~ .c { color: green; }*/ - /* (unused) .c ~ .f { color: green; }*/ - /* (unused) .f ~ .g { color: green; }*/ - /* (unused) .b ~ .f { color: green; }*/ - /* (unused) .b ~ .g { color: green; }*/ + /* (unused) .b ~ .c { color: red; }*/ + /* (unused) .c ~ .f { color: red; }*/ + /* (unused) .f ~ .g { color: red; }*/ + /* (unused) .b ~ .f { color: red; }*/ + /* (unused) .b ~ .g { color: red; }*/ diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/input.svelte b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/input.svelte index afb3b5226ca4..2e2846fa87a6 100644 --- a/packages/svelte/tests/css/samples/general-siblings-combinator-slot/input.svelte +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-slot/input.svelte @@ -5,14 +5,14 @@
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 7ec653d1beb9..530213faac1c 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) { + .match.svelte-xyz > :where(.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 c97af84a65e5..def6560c2023 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 @@
-
+
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 ffdd14f5c44b..ff9874b9e45e 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 ~ b:where(.svelte-xyz) { color: green; } + div.svelte-xyz span:where(.svelte-xyz) ~ 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/has-with-render-tag/_config.js b/packages/svelte/tests/css/samples/has-with-render-tag/_config.js new file mode 100644 index 000000000000..db6da8c6bd26 --- /dev/null +++ b/packages/svelte/tests/css/samples/has-with-render-tag/_config.js @@ -0,0 +1,20 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + { + code: 'css_unused_selector', + message: 'Unused CSS selector "z:has(+ y)"', + start: { + line: 23, + column: 1, + character: 217 + }, + end: { + line: 23, + column: 11, + character: 227 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/has-with-render-tag/expected.css b/packages/svelte/tests/css/samples/has-with-render-tag/expected.css new file mode 100644 index 000000000000..8efb84f56578 --- /dev/null +++ b/packages/svelte/tests/css/samples/has-with-render-tag/expected.css @@ -0,0 +1,11 @@ + + x.svelte-xyz:has(y:where(.svelte-xyz)) { + color: green; + } + p.svelte-xyz:has(+ y:where(.svelte-xyz)) { + color: green; + } + + /* (unused) z:has(+ y) { + color: red; + }*/ diff --git a/packages/svelte/tests/css/samples/has-with-render-tag/input.svelte b/packages/svelte/tests/css/samples/has-with-render-tag/input.svelte new file mode 100644 index 000000000000..8e54e05d0833 --- /dev/null +++ b/packages/svelte/tests/css/samples/has-with-render-tag/input.svelte @@ -0,0 +1,26 @@ +{#snippet foo()} + +{/snippet} + + + this should be green + {@render foo()} + + + +

this should be green

+ {@render foo()} +
+ + diff --git a/packages/svelte/tests/css/samples/siblings-combinator-component-default-snippet/Child.svelte b/packages/svelte/tests/css/samples/siblings-combinator-component-default-snippet/Child.svelte new file mode 100644 index 000000000000..17d34e7cf6aa --- /dev/null +++ b/packages/svelte/tests/css/samples/siblings-combinator-component-default-snippet/Child.svelte @@ -0,0 +1,5 @@ + + +{@render children()} diff --git a/packages/svelte/tests/css/samples/siblings-combinator-component-default-snippet/expected.css b/packages/svelte/tests/css/samples/siblings-combinator-component-default-snippet/expected.css new file mode 100644 index 000000000000..5e7756a4eaf8 --- /dev/null +++ b/packages/svelte/tests/css/samples/siblings-combinator-component-default-snippet/expected.css @@ -0,0 +1,4 @@ + + x.svelte-xyz + y:where(.svelte-xyz) { + color: green; + } diff --git a/packages/svelte/tests/css/samples/siblings-combinator-component-default-snippet/input.svelte b/packages/svelte/tests/css/samples/siblings-combinator-component-default-snippet/input.svelte new file mode 100644 index 000000000000..dff35608ea38 --- /dev/null +++ b/packages/svelte/tests/css/samples/siblings-combinator-component-default-snippet/input.svelte @@ -0,0 +1,15 @@ + + + + + + this should be green + + + diff --git a/packages/svelte/tests/css/samples/siblings-combinator-component-named-snippet/Child.svelte b/packages/svelte/tests/css/samples/siblings-combinator-component-named-snippet/Child.svelte new file mode 100644 index 000000000000..1df9f35e50be --- /dev/null +++ b/packages/svelte/tests/css/samples/siblings-combinator-component-named-snippet/Child.svelte @@ -0,0 +1,5 @@ + + +{@render foo()} diff --git a/packages/svelte/tests/css/samples/siblings-combinator-component-named-snippet/expected.css b/packages/svelte/tests/css/samples/siblings-combinator-component-named-snippet/expected.css new file mode 100644 index 000000000000..5e7756a4eaf8 --- /dev/null +++ b/packages/svelte/tests/css/samples/siblings-combinator-component-named-snippet/expected.css @@ -0,0 +1,4 @@ + + x.svelte-xyz + y:where(.svelte-xyz) { + color: green; + } diff --git a/packages/svelte/tests/css/samples/siblings-combinator-component-named-snippet/input.svelte b/packages/svelte/tests/css/samples/siblings-combinator-component-named-snippet/input.svelte new file mode 100644 index 000000000000..c0ec3bc4dbcb --- /dev/null +++ b/packages/svelte/tests/css/samples/siblings-combinator-component-named-snippet/input.svelte @@ -0,0 +1,17 @@ + + + + + + {#snippet foo()} + this should be green + {/snippet} + + + diff --git a/packages/svelte/tests/css/samples/siblings-combinator-missing-fallback/expected.css b/packages/svelte/tests/css/samples/siblings-combinator-missing-fallback/expected.css new file mode 100644 index 000000000000..3e6114b40577 --- /dev/null +++ b/packages/svelte/tests/css/samples/siblings-combinator-missing-fallback/expected.css @@ -0,0 +1,4 @@ + + x.svelte-xyz + z:where(.svelte-xyz) { + color: green; + } diff --git a/packages/svelte/tests/css/samples/siblings-combinator-missing-fallback/input.svelte b/packages/svelte/tests/css/samples/siblings-combinator-missing-fallback/input.svelte new file mode 100644 index 000000000000..df935eecd08c --- /dev/null +++ b/packages/svelte/tests/css/samples/siblings-combinator-missing-fallback/input.svelte @@ -0,0 +1,13 @@ + + + + fallback content + + +this should be green if the slot fallback is not rendered + + diff --git a/packages/svelte/tests/css/samples/siblings-combinator-render-tag/expected.css b/packages/svelte/tests/css/samples/siblings-combinator-render-tag/expected.css new file mode 100644 index 000000000000..da1c5241a989 --- /dev/null +++ b/packages/svelte/tests/css/samples/siblings-combinator-render-tag/expected.css @@ -0,0 +1,4 @@ + + h1.svelte-xyz + p:where(.svelte-xyz) { + color: green; + } diff --git a/packages/svelte/tests/css/samples/siblings-combinator-render-tag/input.svelte b/packages/svelte/tests/css/samples/siblings-combinator-render-tag/input.svelte new file mode 100644 index 000000000000..035ee3744594 --- /dev/null +++ b/packages/svelte/tests/css/samples/siblings-combinator-render-tag/input.svelte @@ -0,0 +1,13 @@ +{#snippet foo()} +

this should be green

+{/snippet} + +

Hello

+ +{@render foo()} + + diff --git a/packages/svelte/tests/css/samples/siblings-combinator-slot-named-between-default/Child.svelte b/packages/svelte/tests/css/samples/siblings-combinator-slot-named-between-default/Child.svelte new file mode 100644 index 000000000000..4fa864ce7aa9 --- /dev/null +++ b/packages/svelte/tests/css/samples/siblings-combinator-slot-named-between-default/Child.svelte @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/css/samples/siblings-combinator-slot-named-between-default/expected.css b/packages/svelte/tests/css/samples/siblings-combinator-slot-named-between-default/expected.css new file mode 100644 index 000000000000..5300323d850a --- /dev/null +++ b/packages/svelte/tests/css/samples/siblings-combinator-slot-named-between-default/expected.css @@ -0,0 +1,4 @@ + + .a.svelte-xyz + .c:where(.svelte-xyz) { + color: green; + } diff --git a/packages/svelte/tests/css/samples/siblings-combinator-slot-named-between-default/input.svelte b/packages/svelte/tests/css/samples/siblings-combinator-slot-named-between-default/input.svelte new file mode 100644 index 000000000000..95c6e48952bf --- /dev/null +++ b/packages/svelte/tests/css/samples/siblings-combinator-slot-named-between-default/input.svelte @@ -0,0 +1,15 @@ + + + +
a
+
b
+
c
+
+ + diff --git a/packages/svelte/tests/css/samples/siblings-combinator-slot/_config.js b/packages/svelte/tests/css/samples/siblings-combinator-slot/_config.js index d13a8d726bb6..2786baeff80b 100644 --- a/packages/svelte/tests/css/samples/siblings-combinator-slot/_config.js +++ b/packages/svelte/tests/css/samples/siblings-combinator-slot/_config.js @@ -2,12 +2,6 @@ import { test } from '../../test'; export default test({ warnings: [ - { - code: 'css_unused_selector', - message: 'Unused CSS selector ".a + .b"', - start: { character: 83, column: 1, line: 9 }, - end: { character: 90, column: 8, line: 9 } - }, { code: 'css_unused_selector', message: 'Unused CSS selector ".b + .c"', @@ -17,8 +11,8 @@ export default test({ { code: 'css_unused_selector', message: 'Unused CSS selector ".c + .f"', - start: { character: 137, column: 1, line: 11 }, - end: { character: 144, column: 8, line: 11 } + start: { character: 135, column: 1, line: 11 }, + end: { character: 142, column: 8, line: 11 } } ] }); diff --git a/packages/svelte/tests/css/samples/siblings-combinator-slot/expected.css b/packages/svelte/tests/css/samples/siblings-combinator-slot/expected.css index f4c2572bdcef..643f6cf13f77 100644 --- a/packages/svelte/tests/css/samples/siblings-combinator-slot/expected.css +++ b/packages/svelte/tests/css/samples/siblings-combinator-slot/expected.css @@ -1,6 +1,7 @@ + .d.svelte-xyz + .e:where(.svelte-xyz) { color: green; } + .a.svelte-xyz + .b:where(.svelte-xyz) { color: green; } /* no match */ - /* (unused) .a + .b { color: green; }*/ - /* (unused) .b + .c { color: green; }*/ - /* (unused) .c + .f { color: green; }*/ + /* (unused) .b + .c { color: red; }*/ + /* (unused) .c + .f { color: red; }*/ diff --git a/packages/svelte/tests/css/samples/siblings-combinator-slot/input.svelte b/packages/svelte/tests/css/samples/siblings-combinator-slot/input.svelte index 630bc2fe9704..1b543f97b713 100644 --- a/packages/svelte/tests/css/samples/siblings-combinator-slot/input.svelte +++ b/packages/svelte/tests/css/samples/siblings-combinator-slot/input.svelte @@ -4,11 +4,11 @@
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 1de61b6842d0..4986c4f715c2 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) { + .match.svelte-xyz > :where(.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 c97af84a65e5..def6560c2023 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 @@
-
+
diff --git a/packages/svelte/tests/css/samples/siblings-combinator/expected.css b/packages/svelte/tests/css/samples/siblings-combinator/expected.css index 5622a66a303f..97a7a4689793 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 + b:where(.svelte-xyz) { + div.svelte-xyz span:where(.svelte-xyz) + b:where(.svelte-xyz) { color: green; } .a.svelte-xyz + article:where(.svelte-xyz) { diff --git a/packages/svelte/tests/css/samples/snippets/_config.js b/packages/svelte/tests/css/samples/snippets/_config.js index f8b3b4ca39ee..8e3135260b37 100644 --- a/packages/svelte/tests/css/samples/snippets/_config.js +++ b/packages/svelte/tests/css/samples/snippets/_config.js @@ -2,18 +2,32 @@ import { test } from '../../test'; export default test({ warnings: [ + { + code: 'css_unused_selector', + message: 'Unused CSS selector "p .foo"', + start: { + line: 28, + column: 1, + character: 356 + }, + end: { + line: 28, + column: 7, + character: 362 + } + }, { code: 'css_unused_selector', message: 'Unused CSS selector "span div"', start: { line: 31, column: 1, - character: 461 + character: 383 }, end: { line: 31, column: 9, - character: 469 + character: 391 } } ] diff --git a/packages/svelte/tests/css/samples/snippets/expected.css b/packages/svelte/tests/css/samples/snippets/expected.css index 85ae5b4c25ff..62fadcd9e2e8 100644 --- a/packages/svelte/tests/css/samples/snippets/expected.css +++ b/packages/svelte/tests/css/samples/snippets/expected.css @@ -11,9 +11,9 @@ p.svelte-xyz span:where(.svelte-xyz) { color: green; } - p.svelte-xyz .foo:where(.svelte-xyz) { - color: purple; /* doesn't match, but our static analysis doesn't handle this currently */ - } + /* (unused) p .foo { + color: red; + }*/ /* (unused) span div { color: red; }*/ diff --git a/packages/svelte/tests/css/samples/snippets/input.svelte b/packages/svelte/tests/css/samples/snippets/input.svelte index b657e7e2bb87..5dbbad0152f2 100644 --- a/packages/svelte/tests/css/samples/snippets/input.svelte +++ b/packages/svelte/tests/css/samples/snippets/input.svelte @@ -26,9 +26,9 @@ color: green; } p .foo { - color: purple; /* doesn't match, but our static analysis doesn't handle this currently */ + color: red; } span div { color: red; } - \ No newline at end of file +