fix: disregard TypeScript nodes when pruning CSS (#14446)

* make get_possible_element_siblings non-recursive

* treat slots as blocks

* simplify

* simplify

* add test

* changeset
pull/14444/head
Rich Harris 1 month ago committed by GitHub
parent 3fa08d565c
commit a6ad5af0bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: disregard TypeScript nodes when pruning CSS

@ -881,116 +881,63 @@ function get_element_parent(node) {
} }
/** /**
* Finds the given node's previous sibling in the DOM * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
*
* The Svelte `<slot>` is just a placeholder and is not actually real. Any children nodes
* in `<slot>` are 'flattened' and considered as the same level as the `<slot>`'s siblings
*
* e.g.
* ```html
* <h1>Heading 1</h1>
* <slot>
* <h2>Heading 2</h2>
* </slot>
* ```
*
* is considered to look like:
* ```html
* <h1>Heading 1</h1>
* <h2>Heading 2</h2>
* ```
* @param {Compiler.SvelteNode} node
* @returns {Compiler.SvelteNode}
*/
function find_previous_sibling(node) {
/** @type {Compiler.SvelteNode} */
let current_node = node;
while (
// @ts-expect-error TODO
!current_node.prev &&
// @ts-expect-error TODO
current_node.parent?.type === 'SlotElement'
) {
// @ts-expect-error TODO
current_node = current_node.parent;
}
// @ts-expect-error
current_node = current_node.prev;
while (current_node?.type === 'SlotElement') {
const slot_children = current_node.fragment.nodes;
if (slot_children.length > 0) {
current_node = slot_children[slot_children.length - 1];
} else {
break;
}
}
return current_node;
}
/**
* @param {Compiler.SvelteNode} node
* @param {boolean} adjacent_only * @param {boolean} adjacent_only
* @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>} * @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>}
*/ */
function get_possible_element_siblings(node, adjacent_only) { function get_possible_element_siblings(element, adjacent_only) {
/** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>} */ /** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>} */
const result = new Map(); const result = new Map();
const path = element.metadata.path;
/** @type {Compiler.SvelteNode} */ /** @type {Compiler.SvelteNode} */
let prev = node; let current = element;
while ((prev = find_previous_sibling(prev))) {
if (prev.type === 'RegularElement') { let i = path.length;
const has_slot_attribute = prev.attributes.some(
while (i--) {
const fragment = /** @type {Compiler.AST.Fragment} */ (path[i--]);
let j = fragment.nodes.indexOf(current);
while (j--) {
const node = fragment.nodes[j];
if (node.type === 'RegularElement') {
const has_slot_attribute = node.attributes.some(
(attr) => attr.type === 'Attribute' && attr.name.toLowerCase() === 'slot' (attr) => attr.type === 'Attribute' && attr.name.toLowerCase() === 'slot'
); );
if (!has_slot_attribute) { if (!has_slot_attribute) {
result.set(prev, NODE_DEFINITELY_EXISTS); result.set(node, NODE_DEFINITELY_EXISTS);
if (adjacent_only) { if (adjacent_only) {
return result; return result;
} }
} }
} else if (is_block(prev)) { } else if (is_block(node)) {
const possible_last_child = get_possible_last_child(prev, adjacent_only); if (node.type === 'SlotElement') {
result.set(node, NODE_PROBABLY_EXISTS);
}
const possible_last_child = get_possible_last_child(node, adjacent_only);
add_to_map(possible_last_child, result); add_to_map(possible_last_child, result);
if (adjacent_only && has_definite_elements(possible_last_child)) { if (adjacent_only && has_definite_elements(possible_last_child)) {
return result; return result;
} }
} else if ( } else if (node.type === 'RenderTag' || node.type === 'SvelteElement') {
prev.type === 'SlotElement' || result.set(node, NODE_PROBABLY_EXISTS);
prev.type === 'RenderTag' ||
prev.type === 'SvelteElement'
) {
result.set(prev, NODE_PROBABLY_EXISTS);
// Special case: slots, render tags and svelte:element tags could resolve to no siblings, // Special case: slots, render tags and svelte:element tags could resolve to no siblings,
// so we want to continue until we find a definite sibling even with the adjacent-only combinator // so we want to continue until we find a definite sibling even with the adjacent-only combinator
} }
} }
/** @type {Compiler.SvelteNode | null} */ current = path[i];
let parent = node;
while ( if (!current || !is_block(current)) break;
// @ts-expect-error TODO
(parent = parent?.parent) &&
is_block(parent)
) {
const possible_siblings = get_possible_element_siblings(parent, adjacent_only);
add_to_map(possible_siblings, result);
// @ts-expect-error if (current.type === 'EachBlock' && fragment === current.body) {
if (parent.type === 'EachBlock' && !parent.fallback?.nodes.includes(node)) {
// `{#each ...}<a /><b />{/each}` — `<b>` can be previous sibling of `<a />` // `{#each ...}<a /><b />{/each}` — `<b>` can be previous sibling of `<a />`
add_to_map(get_possible_last_child(parent, adjacent_only), result); add_to_map(get_possible_last_child(current, adjacent_only), result);
}
if (adjacent_only && has_definite_elements(possible_siblings)) {
break;
} }
} }
@ -998,7 +945,7 @@ function get_possible_element_siblings(node, adjacent_only) {
} }
/** /**
* @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock} node * @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement} node
* @param {boolean} adjacent_only * @param {boolean} adjacent_only
* @returns {Map<Compiler.AST.RegularElement, NodeExistsValue>} * @returns {Map<Compiler.AST.RegularElement, NodeExistsValue>}
*/ */
@ -1022,6 +969,7 @@ function get_possible_last_child(node, adjacent_only) {
break; break;
case 'KeyBlock': case 'KeyBlock':
case 'SlotElement':
fragments.push(node.fragment); fragments.push(node.fragment);
break; break;
} }
@ -1029,7 +977,7 @@ function get_possible_last_child(node, adjacent_only) {
/** @type {NodeMap} */ /** @type {NodeMap} */
const result = new Map(); const result = new Map();
let exhaustive = true; let exhaustive = node.type !== 'SlotElement';
for (const fragment of fragments) { for (const fragment of fragments) {
if (fragment == null) { if (fragment == null) {
@ -1121,13 +1069,14 @@ function loop_child(children, adjacent_only) {
/** /**
* @param {Compiler.SvelteNode} node * @param {Compiler.SvelteNode} node
* @returns {node is Compiler.AST.IfBlock | Compiler.AST.EachBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock} * @returns {node is Compiler.AST.IfBlock | Compiler.AST.EachBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement}
*/ */
function is_block(node) { function is_block(node) {
return ( return (
node.type === 'IfBlock' || node.type === 'IfBlock' ||
node.type === 'EachBlock' || node.type === 'EachBlock' ||
node.type === 'AwaitBlock' || node.type === 'AwaitBlock' ||
node.type === 'KeyBlock' node.type === 'KeyBlock' ||
node.type === 'SlotElement'
); );
} }

@ -0,0 +1,20 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css_unused_selector',
end: {
character: 127,
column: 28,
line: 10
},
message: 'Unused CSS selector "[data-active=\'true\'] > span"',
start: {
character: 100,
column: 1,
line: 10
}
}
]
});

@ -0,0 +1,4 @@
/* (unused) [data-active='true'] > span {
background-color: red;
}*/

@ -0,0 +1,13 @@
<script lang="ts">
//
</script>
<div data-active={false as true}>
<span></span>
</div>
<style>
[data-active='true'] > span {
background-color: red;
}
</style>
Loading…
Cancel
Save