fix: take snippets into account when scoping CSS (#13589)

fixes #10143
pull/13603/head
Simon H 2 months ago committed by GitHub
parent 6f03561ae4
commit 829be3d611
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: take snippets into account when scoping CSS

@ -9,6 +9,7 @@ import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js';
* @typedef {{
* stylesheet: Compiler.Css.StyleSheet;
* element: Compiler.AST.RegularElement | Compiler.AST.SvelteElement;
* from_render_tag: boolean;
* }} State
*/
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
@ -53,10 +54,17 @@ const nesting_selector = {
/**
*
* @param {Compiler.Css.StyleSheet} stylesheet
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag} element
*/
export function prune(stylesheet, element) {
walk(stylesheet, { stylesheet, element }, visitors);
if (element.type === 'RenderTag') {
const parent = get_element_parent(element);
if (!parent) return;
walk(stylesheet, { stylesheet, element: parent, from_render_tag: true }, visitors);
} else {
walk(stylesheet, { stylesheet, element, from_render_tag: false }, visitors);
}
}
/** @type {Visitors<Compiler.Css.Node, State>} */
@ -101,7 +109,37 @@ const visitors = {
}
}
if (
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.stylesheet,
true
)
) {
mark(inner, element);
node.metadata.used = true;
return;
}
}
element = get_element_parent(element);
}
} else if (
apply_selector(
selectors,
/** @type {Compiler.Css.Rule} */ (node.metadata.rule),
@ -230,6 +268,13 @@ function apply_combinator(
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;
}
if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
if (apply_selector(parent_selectors, rule, parent, stylesheet, check_has)) {
// TODO the `name === ' '` causes false positives, but removing it causes false negatives...
@ -724,7 +769,7 @@ function unquote(str) {
}
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} node
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag} node
* @returns {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null}
*/
function get_element_parent(node) {

@ -691,6 +691,8 @@ export function analyze_component(root, source, options) {
}
outer: for (const element of analysis.elements) {
if (element.type === 'RenderTag') continue;
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

@ -12,6 +12,8 @@ import { mark_subtree_dynamic } from './shared/fragment.js';
export function RenderTag(node, context) {
validate_opening_tag(node, context.state, '@');
context.state.analysis.elements.push(node);
const callee = unwrap_optional(node.expression).callee;
node.metadata.dynamic =

@ -36,7 +36,8 @@ export interface ComponentAnalysis extends Analysis {
root: ScopeRoot;
instance: Js;
template: Template;
elements: Array<AST.RegularElement | AST.SvelteElement>;
/** Used for CSS pruning and scoping */
elements: Array<AST.RegularElement | AST.SvelteElement | AST.RenderTag>;
runes: boolean;
exports: Array<{ name: string; alias: string | null }>;
/** Whether the component uses `$$props` */

@ -0,0 +1,20 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css_unused_selector',
message: 'Unused CSS selector "span div"',
start: {
line: 31,
column: 1,
character: 461
},
end: {
line: 31,
column: 9,
character: 469
}
}
]
});

@ -0,0 +1,19 @@
div.svelte-xyz > span:where(.svelte-xyz) {
color: green;
}
div.svelte-xyz span:where(.svelte-xyz) {
color: green;
}
div.svelte-xyz span {
color: green;
}
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) span div {
color: red;
}*/

@ -0,0 +1,2 @@
<div class="svelte-xyz"><span class="svelte-xyz">Hello world</span></div>
<p class="svelte-xyz"><strong><span class="svelte-xyz">Hello world</span></strong></p>

@ -0,0 +1,34 @@
{#snippet my_snippet()}
<span>Hello world</span>
{/snippet}
<div>{@render my_snippet()}</div>
<p>
{#snippet my_snippet()}
<span>Hello world</span>
{/snippet}
<strong>{@render my_snippet()}</strong>
</p>
<style>
div > span {
color: green;
}
div span {
color: green;
}
div :global(span) {
color: green;
}
p span {
color: green;
}
p .foo {
color: purple; /* doesn't match, but our static analysis doesn't handle this currently */
}
span div {
color: red;
}
</style>
Loading…
Cancel
Save