From 6c1a39fff198a6f926321e6130976f3312cf51ae Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 26 Nov 2024 10:58:05 -0500 Subject: [PATCH 1/6] make get_possible_element_siblings non-recursive --- .../phases/2-analyze/css/css-prune.js | 80 +++++++++---------- 1 file changed, 40 insertions(+), 40 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 6b9410e4b5ee..e8116a034d2f 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 @@ -932,66 +932,66 @@ function find_previous_sibling(node) { } /** - * @param {Compiler.SvelteNode} node + * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element * @param {boolean} adjacent_only * @returns {Map} */ -function get_possible_element_siblings(node, adjacent_only) { +function get_possible_element_siblings(element, adjacent_only) { /** @type {Map} */ const result = new Map(); /** @type {Compiler.SvelteNode} */ - let prev = node; - while ((prev = find_previous_sibling(prev))) { - if (prev.type === 'RegularElement') { - const has_slot_attribute = prev.attributes.some( - (attr) => attr.type === 'Attribute' && attr.name.toLowerCase() === 'slot' - ); + let node = element; + let path = element.metadata.path; + let i = path.length; + + while (i--) { + let current = node; + + while ((node = find_previous_sibling(node))) { + if (node.type === 'RegularElement') { + const has_slot_attribute = node.attributes.some( + (attr) => attr.type === 'Attribute' && attr.name.toLowerCase() === 'slot' + ); - if (!has_slot_attribute) { - result.set(prev, NODE_DEFINITELY_EXISTS); + if (!has_slot_attribute) { + result.set(node, NODE_DEFINITELY_EXISTS); - if (adjacent_only) { + if (adjacent_only) { + return result; + } + } + } else if (is_block(node)) { + const possible_last_child = get_possible_last_child(node, adjacent_only); + add_to_map(possible_last_child, result); + if (adjacent_only && has_definite_elements(possible_last_child)) { return result; } + } else if ( + node.type === 'SlotElement' || + node.type === 'RenderTag' || + node.type === 'SvelteElement' + ) { + result.set(node, NODE_PROBABLY_EXISTS); + // 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 } - } else if (is_block(prev)) { - const possible_last_child = get_possible_last_child(prev, adjacent_only); - add_to_map(possible_last_child, result); - if (adjacent_only && has_definite_elements(possible_last_child)) { - return result; - } - } else if ( - prev.type === 'SlotElement' || - 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, - // so we want to continue until we find a definite sibling even with the adjacent-only combinator } - } - /** @type {Compiler.SvelteNode | null} */ - let parent = node; + let parent = path[i]; - while ( - // @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); + if (parent.type === 'Fragment') { + parent = path[--i]; + } + + if (!parent || !is_block(parent)) break; - // @ts-expect-error - if (parent.type === 'EachBlock' && !parent.fallback?.nodes.includes(node)) { + if (parent.type === 'EachBlock' && !parent.fallback?.nodes.includes(current)) { // `{#each ...}{/each}` — `` can be previous sibling of `` add_to_map(get_possible_last_child(parent, adjacent_only), result); } - if (adjacent_only && has_definite_elements(possible_siblings)) { - break; - } + node = parent; } return result; From 5ce70131702180c56880771bd5444bad6fbb54d5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 26 Nov 2024 12:11:42 -0500 Subject: [PATCH 2/6] treat slots as blocks --- .../phases/2-analyze/css/css-prune.js | 74 ++++--------------- 1 file changed, 13 insertions(+), 61 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 e8116a034d2f..f3a5d77d7771 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 @@ -880,57 +880,6 @@ function get_element_parent(node) { return null; } -/** - * Finds the given node's previous sibling in the DOM - * - * The Svelte `` is just a placeholder and is not actually real. Any children nodes - * in `` are 'flattened' and considered as the same level as the ``'s siblings - * - * e.g. - * ```html - *

Heading 1

- * - *

Heading 2

- *
- * ``` - * - * is considered to look like: - * ```html - *

Heading 1

- *

Heading 2

- * ``` - * @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.AST.RegularElement | Compiler.AST.SvelteElement} element * @param {boolean} adjacent_only @@ -948,7 +897,8 @@ function get_possible_element_siblings(element, adjacent_only) { while (i--) { let current = node; - while ((node = find_previous_sibling(node))) { + // @ts-expect-error + while ((node = node.prev)) { if (node.type === 'RegularElement') { const has_slot_attribute = node.attributes.some( (attr) => attr.type === 'Attribute' && attr.name.toLowerCase() === 'slot' @@ -962,16 +912,16 @@ function get_possible_element_siblings(element, adjacent_only) { } } } else if (is_block(node)) { + 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); if (adjacent_only && has_definite_elements(possible_last_child)) { return result; } - } else if ( - node.type === 'SlotElement' || - node.type === 'RenderTag' || - node.type === 'SvelteElement' - ) { + } else if (node.type === 'RenderTag' || node.type === 'SvelteElement') { result.set(node, NODE_PROBABLY_EXISTS); // 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 @@ -998,7 +948,7 @@ function get_possible_element_siblings(element, 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 * @returns {Map} */ @@ -1022,6 +972,7 @@ function get_possible_last_child(node, adjacent_only) { break; case 'KeyBlock': + case 'SlotElement': fragments.push(node.fragment); break; } @@ -1029,7 +980,7 @@ function get_possible_last_child(node, adjacent_only) { /** @type {NodeMap} */ const result = new Map(); - let exhaustive = true; + let exhaustive = node.type !== 'SlotElement'; for (const fragment of fragments) { if (fragment == null) { @@ -1121,13 +1072,14 @@ function loop_child(children, adjacent_only) { /** * @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) { return ( node.type === 'IfBlock' || node.type === 'EachBlock' || node.type === 'AwaitBlock' || - node.type === 'KeyBlock' + node.type === 'KeyBlock' || + node.type === 'SlotElement' ); } From 91ba2035bc9cd1544247a2d2deaef4439da2d1e6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 26 Nov 2024 13:00:45 -0500 Subject: [PATCH 3/6] simplify --- .../compiler/phases/2-analyze/css/css-prune.js | 16 +++++----------- 1 file changed, 5 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 f3a5d77d7771..de0eec9aa4b4 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 @@ -895,7 +895,7 @@ function get_possible_element_siblings(element, adjacent_only) { let i = path.length; while (i--) { - let current = node; + let fragment = /** @type {Compiler.AST.Fragment} */ (path[i--]); // @ts-expect-error while ((node = node.prev)) { @@ -928,20 +928,14 @@ function get_possible_element_siblings(element, adjacent_only) { } } - let parent = path[i]; + node = path[i]; - if (parent.type === 'Fragment') { - parent = path[--i]; - } - - if (!parent || !is_block(parent)) break; + if (!node || !is_block(node)) break; - if (parent.type === 'EachBlock' && !parent.fallback?.nodes.includes(current)) { + if (node.type === 'EachBlock' && fragment === node.body) { // `{#each ...}
{/each}` — `` can be previous sibling of `` - add_to_map(get_possible_last_child(parent, adjacent_only), result); + add_to_map(get_possible_last_child(node, adjacent_only), result); } - - node = parent; } return result; From 8b1f4e2096a1c0b443a7e97fa87950b7f688c4f9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 26 Nov 2024 13:08:20 -0500 Subject: [PATCH 4/6] simplify --- .../phases/2-analyze/css/css-prune.js | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 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 de0eec9aa4b4..dec85c03612a 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 @@ -888,17 +888,20 @@ function get_element_parent(node) { function get_possible_element_siblings(element, adjacent_only) { /** @type {Map} */ const result = new Map(); + const path = element.metadata.path; /** @type {Compiler.SvelteNode} */ - let node = element; - let path = element.metadata.path; + let current = element; + let i = path.length; while (i--) { - let fragment = /** @type {Compiler.AST.Fragment} */ (path[i--]); + const fragment = /** @type {Compiler.AST.Fragment} */ (path[i--]); + let j = fragment.nodes.indexOf(current); + + while (j--) { + const node = fragment.nodes[j]; - // @ts-expect-error - while ((node = node.prev)) { if (node.type === 'RegularElement') { const has_slot_attribute = node.attributes.some( (attr) => attr.type === 'Attribute' && attr.name.toLowerCase() === 'slot' @@ -928,13 +931,13 @@ function get_possible_element_siblings(element, adjacent_only) { } } - node = path[i]; + current = path[i]; - if (!node || !is_block(node)) break; + if (!current || !is_block(current)) break; - if (node.type === 'EachBlock' && fragment === node.body) { + if (current.type === 'EachBlock' && fragment === current.body) { // `{#each ...}{/each}` — `` can be previous sibling of `` - add_to_map(get_possible_last_child(node, adjacent_only), result); + add_to_map(get_possible_last_child(current, adjacent_only), result); } } From 8dd5e6d196a3c7b2c9e913e42341d9f80903bbc6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 26 Nov 2024 13:15:46 -0500 Subject: [PATCH 5/6] add test --- .../unused-ts-as-expression/_config.js | 20 +++++++++++++++++++ .../unused-ts-as-expression/expected.css | 4 ++++ .../unused-ts-as-expression/input.svelte | 13 ++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 packages/svelte/tests/css/samples/unused-ts-as-expression/_config.js create mode 100644 packages/svelte/tests/css/samples/unused-ts-as-expression/expected.css create mode 100644 packages/svelte/tests/css/samples/unused-ts-as-expression/input.svelte diff --git a/packages/svelte/tests/css/samples/unused-ts-as-expression/_config.js b/packages/svelte/tests/css/samples/unused-ts-as-expression/_config.js new file mode 100644 index 000000000000..e2679428cc78 --- /dev/null +++ b/packages/svelte/tests/css/samples/unused-ts-as-expression/_config.js @@ -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 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/unused-ts-as-expression/expected.css b/packages/svelte/tests/css/samples/unused-ts-as-expression/expected.css new file mode 100644 index 000000000000..571d2acfbc69 --- /dev/null +++ b/packages/svelte/tests/css/samples/unused-ts-as-expression/expected.css @@ -0,0 +1,4 @@ + + /* (unused) [data-active='true'] > span { + background-color: red; + }*/ diff --git a/packages/svelte/tests/css/samples/unused-ts-as-expression/input.svelte b/packages/svelte/tests/css/samples/unused-ts-as-expression/input.svelte new file mode 100644 index 000000000000..24fc0096bf7e --- /dev/null +++ b/packages/svelte/tests/css/samples/unused-ts-as-expression/input.svelte @@ -0,0 +1,13 @@ + + +
+ +
+ + From 732da8334c23673775243062c83d5f665b9d9ee3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 26 Nov 2024 13:16:24 -0500 Subject: [PATCH 6/6] changeset --- .changeset/tender-balloons-relate.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tender-balloons-relate.md diff --git a/.changeset/tender-balloons-relate.md b/.changeset/tender-balloons-relate.md new file mode 100644 index 000000000000..ea7ce094b624 --- /dev/null +++ b/.changeset/tender-balloons-relate.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: disregard TypeScript nodes when pruning CSS