Skip to content

Commit a6ad5af

Browse files
authored
fix: disregard TypeScript nodes when pruning CSS (#14446)
* make get_possible_element_siblings non-recursive * treat slots as blocks * simplify * simplify * add test * changeset
1 parent 3fa08d5 commit a6ad5af

File tree

5 files changed

+88
-97
lines changed

5 files changed

+88
-97
lines changed

.changeset/tender-balloons-relate.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: disregard TypeScript nodes when pruning CSS

packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js

Lines changed: 46 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -881,124 +881,71 @@ function get_element_parent(node) {
881881
}
882882

883883
/**
884-
* Finds the given node's previous sibling in the DOM
885-
*
886-
* The Svelte `<slot>` is just a placeholder and is not actually real. Any children nodes
887-
* in `<slot>` are 'flattened' and considered as the same level as the `<slot>`'s siblings
888-
*
889-
* e.g.
890-
* ```html
891-
* <h1>Heading 1</h1>
892-
* <slot>
893-
* <h2>Heading 2</h2>
894-
* </slot>
895-
* ```
896-
*
897-
* is considered to look like:
898-
* ```html
899-
* <h1>Heading 1</h1>
900-
* <h2>Heading 2</h2>
901-
* ```
902-
* @param {Compiler.SvelteNode} node
903-
* @returns {Compiler.SvelteNode}
904-
*/
905-
function find_previous_sibling(node) {
906-
/** @type {Compiler.SvelteNode} */
907-
let current_node = node;
908-
909-
while (
910-
// @ts-expect-error TODO
911-
!current_node.prev &&
912-
// @ts-expect-error TODO
913-
current_node.parent?.type === 'SlotElement'
914-
) {
915-
// @ts-expect-error TODO
916-
current_node = current_node.parent;
917-
}
918-
919-
// @ts-expect-error
920-
current_node = current_node.prev;
921-
922-
while (current_node?.type === 'SlotElement') {
923-
const slot_children = current_node.fragment.nodes;
924-
if (slot_children.length > 0) {
925-
current_node = slot_children[slot_children.length - 1];
926-
} else {
927-
break;
928-
}
929-
}
930-
931-
return current_node;
932-
}
933-
934-
/**
935-
* @param {Compiler.SvelteNode} node
884+
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
936885
* @param {boolean} adjacent_only
937886
* @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>}
938887
*/
939-
function get_possible_element_siblings(node, adjacent_only) {
888+
function get_possible_element_siblings(element, adjacent_only) {
940889
/** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>} */
941890
const result = new Map();
891+
const path = element.metadata.path;
942892

943893
/** @type {Compiler.SvelteNode} */
944-
let prev = node;
945-
while ((prev = find_previous_sibling(prev))) {
946-
if (prev.type === 'RegularElement') {
947-
const has_slot_attribute = prev.attributes.some(
948-
(attr) => attr.type === 'Attribute' && attr.name.toLowerCase() === 'slot'
949-
);
894+
let current = element;
895+
896+
let i = path.length;
897+
898+
while (i--) {
899+
const fragment = /** @type {Compiler.AST.Fragment} */ (path[i--]);
900+
let j = fragment.nodes.indexOf(current);
901+
902+
while (j--) {
903+
const node = fragment.nodes[j];
904+
905+
if (node.type === 'RegularElement') {
906+
const has_slot_attribute = node.attributes.some(
907+
(attr) => attr.type === 'Attribute' && attr.name.toLowerCase() === 'slot'
908+
);
950909

951-
if (!has_slot_attribute) {
952-
result.set(prev, NODE_DEFINITELY_EXISTS);
910+
if (!has_slot_attribute) {
911+
result.set(node, NODE_DEFINITELY_EXISTS);
953912

954-
if (adjacent_only) {
913+
if (adjacent_only) {
914+
return result;
915+
}
916+
}
917+
} else if (is_block(node)) {
918+
if (node.type === 'SlotElement') {
919+
result.set(node, NODE_PROBABLY_EXISTS);
920+
}
921+
922+
const possible_last_child = get_possible_last_child(node, adjacent_only);
923+
add_to_map(possible_last_child, result);
924+
if (adjacent_only && has_definite_elements(possible_last_child)) {
955925
return result;
956926
}
927+
} else if (node.type === 'RenderTag' || node.type === 'SvelteElement') {
928+
result.set(node, NODE_PROBABLY_EXISTS);
929+
// Special case: slots, render tags and svelte:element tags could resolve to no siblings,
930+
// so we want to continue until we find a definite sibling even with the adjacent-only combinator
957931
}
958-
} else if (is_block(prev)) {
959-
const possible_last_child = get_possible_last_child(prev, adjacent_only);
960-
add_to_map(possible_last_child, result);
961-
if (adjacent_only && has_definite_elements(possible_last_child)) {
962-
return result;
963-
}
964-
} else if (
965-
prev.type === 'SlotElement' ||
966-
prev.type === 'RenderTag' ||
967-
prev.type === 'SvelteElement'
968-
) {
969-
result.set(prev, NODE_PROBABLY_EXISTS);
970-
// Special case: slots, render tags and svelte:element tags could resolve to no siblings,
971-
// so we want to continue until we find a definite sibling even with the adjacent-only combinator
972932
}
973-
}
974933

975-
/** @type {Compiler.SvelteNode | null} */
976-
let parent = node;
934+
current = path[i];
977935

978-
while (
979-
// @ts-expect-error TODO
980-
(parent = parent?.parent) &&
981-
is_block(parent)
982-
) {
983-
const possible_siblings = get_possible_element_siblings(parent, adjacent_only);
984-
add_to_map(possible_siblings, result);
936+
if (!current || !is_block(current)) break;
985937

986-
// @ts-expect-error
987-
if (parent.type === 'EachBlock' && !parent.fallback?.nodes.includes(node)) {
938+
if (current.type === 'EachBlock' && fragment === current.body) {
988939
// `{#each ...}<a /><b />{/each}` — `<b>` can be previous sibling of `<a />`
989-
add_to_map(get_possible_last_child(parent, adjacent_only), result);
990-
}
991-
992-
if (adjacent_only && has_definite_elements(possible_siblings)) {
993-
break;
940+
add_to_map(get_possible_last_child(current, adjacent_only), result);
994941
}
995942
}
996943

997944
return result;
998945
}
999946

1000947
/**
1001-
* @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock} node
948+
* @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement} node
1002949
* @param {boolean} adjacent_only
1003950
* @returns {Map<Compiler.AST.RegularElement, NodeExistsValue>}
1004951
*/
@@ -1022,14 +969,15 @@ function get_possible_last_child(node, adjacent_only) {
1022969
break;
1023970

1024971
case 'KeyBlock':
972+
case 'SlotElement':
1025973
fragments.push(node.fragment);
1026974
break;
1027975
}
1028976

1029977
/** @type {NodeMap} */
1030978
const result = new Map();
1031979

1032-
let exhaustive = true;
980+
let exhaustive = node.type !== 'SlotElement';
1033981

1034982
for (const fragment of fragments) {
1035983
if (fragment == null) {
@@ -1121,13 +1069,14 @@ function loop_child(children, adjacent_only) {
11211069

11221070
/**
11231071
* @param {Compiler.SvelteNode} node
1124-
* @returns {node is Compiler.AST.IfBlock | Compiler.AST.EachBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock}
1072+
* @returns {node is Compiler.AST.IfBlock | Compiler.AST.EachBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement}
11251073
*/
11261074
function is_block(node) {
11271075
return (
11281076
node.type === 'IfBlock' ||
11291077
node.type === 'EachBlock' ||
11301078
node.type === 'AwaitBlock' ||
1131-
node.type === 'KeyBlock'
1079+
node.type === 'KeyBlock' ||
1080+
node.type === 'SlotElement'
11321081
);
11331082
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
warnings: [
5+
{
6+
code: 'css_unused_selector',
7+
end: {
8+
character: 127,
9+
column: 28,
10+
line: 10
11+
},
12+
message: 'Unused CSS selector "[data-active=\'true\'] > span"',
13+
start: {
14+
character: 100,
15+
column: 1,
16+
line: 10
17+
}
18+
}
19+
]
20+
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
/* (unused) [data-active='true'] > span {
3+
background-color: red;
4+
}*/
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<script lang="ts">
2+
//
3+
</script>
4+
5+
<div data-active={false as true}>
6+
<span></span>
7+
</div>
8+
9+
<style>
10+
[data-active='true'] > span {
11+
background-color: red;
12+
}
13+
</style>

0 commit comments

Comments
 (0)