From 45fa678242bec9809b9458a7e0102ceeacc369af Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:24:02 +0100 Subject: [PATCH 1/5] fix: account for `:has(...)` as part of `:root` (#14229) We previously marked all `:root` selectors as global-like, which excempted them from further analysis. This causes problems: - things like `:not(...)` are never visited and therefore never marked as used -> we gotta do that directly when coming across this - `:has(...)` was never visited, too. Just marking it as used is not enough though, because we might need to scope its contents Therefore the logic is enhanced to account for these special cases. Fixes #14118 While fixing this I cleaned up some inconsistencies in what we mark as global. This simplified code and fixed some adjacent bugs, which conindicentally also fixes #14189 --- .changeset/beige-files-pull.md | 5 + .changeset/great-bulldogs-wonder.md | 5 + .../phases/2-analyze/css/css-analyze.js | 47 ++++---- .../phases/2-analyze/css/css-prune.js | 101 +++++++++--------- .../compiler/phases/2-analyze/css/css-warn.js | 7 +- .../compiler/phases/2-analyze/css/utils.js | 84 ++++++++++++++- .../compiler/phases/3-transform/css/index.js | 14 +-- packages/svelte/src/compiler/types/css.d.ts | 4 +- .../svelte/tests/css/samples/has/_config.js | 42 ++++++++ .../svelte/tests/css/samples/has/expected.css | 19 +++- .../svelte/tests/css/samples/has/input.svelte | 15 +++ .../svelte/tests/css/samples/is/_config.js | 40 +++---- .../svelte/tests/css/samples/is/expected.css | 15 +++ .../svelte/tests/css/samples/is/input.svelte | 15 +++ .../samples/not-selector-global/expected.css | 9 ++ .../samples/not-selector-global/input.svelte | 9 ++ .../svelte/tests/css/samples/root/_config.js | 75 ++++++++++++- .../tests/css/samples/root/expected.css | 47 +++++++- .../tests/css/samples/root/expected.html | 2 +- .../tests/css/samples/root/input.svelte | 46 +++++++- 20 files changed, 484 insertions(+), 117 deletions(-) create mode 100644 .changeset/beige-files-pull.md create mode 100644 .changeset/great-bulldogs-wonder.md diff --git a/.changeset/beige-files-pull.md b/.changeset/beige-files-pull.md new file mode 100644 index 0000000000..2cd98a2819 --- /dev/null +++ b/.changeset/beige-files-pull.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: account for `:has(...)` as part of `:root` diff --git a/.changeset/great-bulldogs-wonder.md b/.changeset/great-bulldogs-wonder.md new file mode 100644 index 0000000000..b6cafa4585 --- /dev/null +++ b/.changeset/great-bulldogs-wonder.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent nested pseudo class from being marked as unused diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js index ec36e1ce64..38551f328f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js @@ -4,6 +4,7 @@ import { walk } from 'zimmerframe'; import * as e from '../../../errors.js'; import { is_keyframes_node } from '../../css.js'; +import { is_global, is_unscoped_pseudo_class } from './utils.js'; /** * @typedef {Visitors< @@ -15,27 +16,6 @@ import { is_keyframes_node } from '../../css.js'; * >} CssVisitors */ -/** - * True if is `:global(...)` or `:global` - * @param {Css.RelativeSelector} relative_selector - * @returns {relative_selector is Css.RelativeSelector & { selectors: [Css.PseudoClassSelector, ...Array] }} - */ -function is_global(relative_selector) { - const first = relative_selector.selectors[0]; - - return ( - first.type === 'PseudoClassSelector' && - first.name === 'global' && - (first.args === null || - // Only these two selector types keep the whole selector global, because e.g. - // :global(button).x means that the selector is still scoped because of the .x - relative_selector.selectors.every( - (selector) => - selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector' - )) - ); -} - /** * True if is `:global` * @param {Css.SimpleSelector} simple_selector @@ -119,11 +99,14 @@ const css_visitors = { node.metadata.rule?.metadata.parent_rule && node.children[0]?.selectors[0]?.type === 'NestingSelector' ) { + const first = node.children[0]?.selectors[1]; + const no_nesting_scope = + first?.type !== 'PseudoClassSelector' || is_unscoped_pseudo_class(first); const parent_is_global = node.metadata.rule.metadata.parent_rule.prelude.children.some( (child) => child.children.length === 1 && child.children[0].metadata.is_global ); // mark `&:hover` in `:global(.foo) { &:hover { color: green }}` as used - if (parent_is_global) { + if (no_nesting_scope && parent_is_global) { node.metadata.used = true; } } @@ -156,9 +139,23 @@ const css_visitors = { ].includes(first.name)); } - node.metadata.is_global_like ||= !!node.selectors.find( - (child) => child.type === 'PseudoClassSelector' && child.name === 'root' - ); + node.metadata.is_global_like ||= + node.selectors.some( + (child) => child.type === 'PseudoClassSelector' && child.name === 'root' + ) && + // :root.y:has(.x) is not a global selector because while .y is unscoped, .x inside `:has(...)` should be scoped + !node.selectors.some((child) => child.type === 'PseudoClassSelector' && child.name === 'has'); + + if (node.metadata.is_global_like || node.metadata.is_global) { + // So that nested selectors like `:root:not(.x)` are not marked as unused + for (const child of node.selectors) { + walk(/** @type {Css.Node} */ (child), null, { + ComplexSelector(node) { + node.metadata.used = true; + } + }); + } + } context.next(); }, 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 a9ea9fe913..bf0fd27566 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,7 +1,7 @@ /** @import { Visitors } from 'zimmerframe' */ /** @import * as Compiler from '#compiler' */ import { walk } from 'zimmerframe'; -import { get_possible_values } from './utils.js'; +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'; @@ -172,7 +172,7 @@ function get_relative_selectors(node) { } /** - * Discard trailing `:global(...)` selectors without a `:has/is/where/not(...)` modifier, these are unused for scoping purposes + * Discard trailing `:global(...)` selectors, these are unused for scoping purposes * @param {Compiler.Css.ComplexSelector} node */ function truncate(node) { @@ -182,21 +182,22 @@ function truncate(node) { // not after a :global selector !metadata.is_global_like && !(first.type === 'PseudoClassSelector' && first.name === 'global' && first.args === null) && - // not a :global(...) without a :has/is/where/not(...) modifier - (!metadata.is_global || - selectors.some( - (selector) => - selector.type === 'PseudoClassSelector' && - selector.args !== null && - (selector.name === 'has' || - selector.name === 'is' || - selector.name === 'where' || - selector.name === 'not') - )) + // not a :global(...) without a :has/is/where(...) modifier that is scoped + !metadata.is_global ); }); - return node.children.slice(0, i + 1); + return node.children.slice(0, i + 1).map((child) => { + // In case of `:root.y:has(...)`, `y` is unscoped, but everything in `:has(...)` should be scoped (if not global). + // To properly accomplish that, we gotta filter out all selector types except `:has`. + const root = child.selectors.find((s) => s.type === 'PseudoClassSelector' && s.name === 'root'); + if (!root || child.metadata.is_global_like) return child; + + return { + ...child, + selectors: child.selectors.filter((s) => s.type === 'PseudoClassSelector' && s.name === 'has') + }; + }); } /** @@ -334,7 +335,9 @@ function apply_combinator(combinator, relative_selector, parent_selectors, rule, * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element */ function mark(relative_selector, element) { - relative_selector.metadata.scoped = true; + if (!is_outer_global(relative_selector)) { + relative_selector.metadata.scoped = true; + } element.metadata.scoped = true; } @@ -415,6 +418,21 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, /** @type {Array} */ let sibling_elements; // do them lazy because it's rarely used and expensive to calculate + // 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 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) => + c.children.some((r) => + r.selectors.some((s) => s.type === 'PseudoClassSelector' && s.name === 'root') + ) + ); + if (include_self) { + child_elements.push(element); + descendant_elements.push(element); + } + walk( /** @type {Compiler.SvelteNode} */ (element.fragment), { is_child: true }, @@ -460,7 +478,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, const descendants = left_most_combinator.name === '+' || left_most_combinator.name === '~' - ? (sibling_elements ??= get_following_sibling_elements(element)) + ? (sibling_elements ??= get_following_sibling_elements(element, include_self)) : left_most_combinator.name === '>' ? child_elements : descendant_elements; @@ -481,20 +499,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, } if (!matched) { - if (relative_selector.metadata.is_global && !relative_selector.metadata.is_global_like) { - // Edge case: `:global(.x):has(.y)` where `.x` is global but `.y` doesn't match. - // Since `used` is set to `true` for `:global(.x)` in css-analyze beforehand, and - // we have no way of knowing if it's safe to set it back to `false`, we'll mark - // the inner selector as used and scoped to prevent it from being pruned, which could - // result in a invalid CSS output (e.g. `.x:has(/* unused .y */)`). The result - // can't match a real element, so the only drawback is the missing prune. - // TODO clean this up some day - complex_selectors[0].metadata.used = true; - complex_selectors[0].children.forEach((selector) => { - selector.metadata.scoped = true; - }); - } - return false; } } @@ -507,9 +511,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, switch (selector.type) { case 'PseudoClassSelector': { - if (name === 'host' || name === 'root') { - return false; - } + if (name === 'host' || name === 'root') return false; if ( name === 'global' && @@ -578,23 +580,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, } if (!matched) { - if ( - relative_selector.metadata.is_global && - !relative_selector.metadata.is_global_like - ) { - // Edge case: `:global(.x):is(.y)` where `.x` is global but `.y` doesn't match. - // Since `used` is set to `true` for `:global(.x)` in css-analyze beforehand, and - // we have no way of knowing if it's safe to set it back to `false`, we'll mark - // the inner selector as used and scoped to prevent it from being pruned, which could - // result in a invalid CSS output (e.g. `.x:is(/* unused .y */)`). The result - // can't match a real element, so the only drawback is the missing prune. - // TODO clean this up some day - selector.args.children[0].metadata.used = true; - selector.args.children[0].children.forEach((selector) => { - selector.metadata.scoped = true; - }); - } - return false; } } @@ -662,7 +647,10 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, const parent = /** @type {Compiler.Css.Rule} */ (rule.metadata.parent_rule); for (const complex_selector of parent.prelude.children) { - if (apply_selector(get_relative_selectors(complex_selector), parent, element, state)) { + if ( + apply_selector(get_relative_selectors(complex_selector), parent, element, state) || + complex_selector.children.every((s) => is_global(s, parent)) + ) { complex_selector.metadata.used = true; matched = true; } @@ -681,8 +669,11 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, return true; } -/** @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element */ -function get_following_sibling_elements(element) { +/** + * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element + * @param {boolean} include_self + */ +function get_following_sibling_elements(element, include_self) { /** @type {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.Root | null} */ let parent = get_element_parent(element); @@ -723,6 +714,10 @@ function get_following_sibling_elements(element) { } } + if (include_self) { + sibling_elements.push(element); + } + return sibling_elements; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js index 482278b795..eab67327e2 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js @@ -24,7 +24,12 @@ const visitors = { } }, ComplexSelector(node, context) { - if (!node.metadata.used) { + if ( + !node.metadata.used && + // prevent double-marking of `.unused:is(.unused)` + (context.path.at(-2)?.type !== 'PseudoClassSelector' || + /** @type {Css.ComplexSelector} */ (context.path.at(-4))?.metadata.used) + ) { const content = context.state.stylesheet.content; const text = content.styles.substring(node.start - content.start, node.end - content.start); w.css_unused_selector(node, text); 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 99603fad29..0226a150c9 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/utils.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/utils.js @@ -1,4 +1,4 @@ -/** @import { AST } from '#compiler' */ +/** @import { AST, Css } from '#compiler' */ /** @import { Node } from 'estree' */ const UNKNOWN = {}; @@ -33,3 +33,85 @@ export function get_possible_values(chunk) { if (values.has(UNKNOWN)) return null; return values; } + +/** + * Returns all parent rules; root is last + * @param {Css.Rule | null} rule + */ +export function get_parent_rules(rule) { + const parents = []; + + let parent = rule?.metadata.parent_rule; + while (parent) { + parents.push(parent); + parent = parent.metadata.parent_rule; + } + + return parents; +} + +/** + * True if is `:global(...)` or `:global` and no pseudo class that is scoped. + * @param {Css.RelativeSelector} relative_selector + * @returns {relative_selector is Css.RelativeSelector & { selectors: [Css.PseudoClassSelector, ...Array] }} + */ +export function is_global(relative_selector) { + const first = relative_selector.selectors[0]; + + return ( + first.type === 'PseudoClassSelector' && + first.name === 'global' && + (first.args === null || + // Only these two selector types keep the whole selector global, because e.g. + // :global(button).x means that the selector is still scoped because of the .x + relative_selector.selectors.every( + (selector) => + is_unscoped_pseudo_class(selector) || selector.type === 'PseudoElementSelector' + )) + ); +} + +/** + * `true` if is a pseudo class that cannot be or is not scoped + * @param {Css.SimpleSelector} selector + */ +export function is_unscoped_pseudo_class(selector) { + return ( + selector.type === 'PseudoClassSelector' && + // These make the selector scoped + ((selector.name !== 'has' && + selector.name !== 'is' && + selector.name !== 'where' && + // Not is special because we want to scope as specific as possible, but because :not + // inverses the result, we want to leave the unscoped, too. The exception is more than + // one selector in the :not (.e.g :not(.x .y)), then .x and .y should be scoped + (selector.name !== 'not' || + selector.args === null || + selector.args.children.every((c) => c.children.length === 1))) || + // selectors with has/is/where/not can also be global if all their children are global + selector.args === null || + selector.args.children.every((c) => c.children.every((r) => is_global(r)))) + ); +} + +/** + * True if is `:global(...)` or `:global`, irrespective of whether or not there are any pseudo classes that are scoped. + * Difference to `is_global`: `:global(x):has(y)` is `true` for `is_outer_global` but `false` for `is_global`. + * @param {Css.RelativeSelector} relative_selector + * @returns {relative_selector is Css.RelativeSelector & { selectors: [Css.PseudoClassSelector, ...Array] }} + */ +export function is_outer_global(relative_selector) { + const first = relative_selector.selectors[0]; + + return ( + first.type === 'PseudoClassSelector' && + first.name === 'global' && + (first.args === null || + // Only these two selector types can keep the whole selector global, because e.g. + // :global(button).x means that the selector is still scoped because of the .x + relative_selector.selectors.every( + (selector) => + selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector' + )) + ); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/css/index.js b/packages/svelte/src/compiler/phases/3-transform/css/index.js index 350eca35d8..d1f3d3baa6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/css/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/css/index.js @@ -292,6 +292,13 @@ const visitors = { context.state.code.prependRight(global.start, '&'); } continue; + } else { + // for any :global() or :global at the middle of compound selector + for (const selector of relative_selector.selectors) { + if (selector.type === 'PseudoClassSelector' && selector.name === 'global') { + remove_global_pseudo_class(selector, null, context.state); + } + } } if (relative_selector.metadata.scoped) { @@ -306,13 +313,6 @@ const visitors = { } } - // for any :global() or :global at the middle of compound selector - for (const selector of relative_selector.selectors) { - if (selector.type === 'PseudoClassSelector' && selector.name === 'global') { - remove_global_pseudo_class(selector, null, context.state); - } - } - if (relative_selector.selectors.some((s) => s.type === 'NestingSelector')) { continue; } diff --git a/packages/svelte/src/compiler/types/css.d.ts b/packages/svelte/src/compiler/types/css.d.ts index 97ac7fc0d3..ba4079ecd0 100644 --- a/packages/svelte/src/compiler/types/css.d.ts +++ b/packages/svelte/src/compiler/types/css.d.ts @@ -87,8 +87,8 @@ export namespace Css { /** * `true` if the whole selector is unscoped, e.g. `:global(...)` or `:global` or `:global.x`. * Selectors like `:global(...).x` are not considered global, because they still need scoping. - * Selectors like `:global(...):is/where/not/has(...)` are considered global even if they aren't - * strictly speaking (we should consolidate the logic around this at some point). + * Selectors like `:global(...):is/where/not/has(...)` are only considered global if all their + * children are global. */ is_global: boolean; /** `:root`, `:host`, `::view-transition`, or selectors after a `:global` */ diff --git a/packages/svelte/tests/css/samples/has/_config.js b/packages/svelte/tests/css/samples/has/_config.js index 8b8e7760f5..e5dc5f3459 100644 --- a/packages/svelte/tests/css/samples/has/_config.js +++ b/packages/svelte/tests/css/samples/has/_config.js @@ -44,6 +44,20 @@ export default test({ character: 401 } }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ":global(.foo):has(.unused)"', + start: { + line: 40, + column: 1, + character: 422 + }, + end: { + line: 40, + column: 27, + character: 448 + } + }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(y):has(.unused)"', @@ -141,6 +155,34 @@ export default test({ column: 11, character: 1336 } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ":has(.unused)"', + start: { + line: 129, + column: 2, + character: 1409 + }, + end: { + line: 129, + column: 15, + character: 1422 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector "&:has(.unused)"', + start: { + line: 135, + column: 2, + character: 1480 + }, + end: { + line: 135, + column: 16, + character: 1494 + } } ] }); diff --git a/packages/svelte/tests/css/samples/has/expected.css b/packages/svelte/tests/css/samples/has/expected.css index 4cb8d76bf6..8eda676f93 100644 --- a/packages/svelte/tests/css/samples/has/expected.css +++ b/packages/svelte/tests/css/samples/has/expected.css @@ -27,9 +27,9 @@ /* (unused) x:has(.unused) { color: red; }*/ - .foo:has(.unused.svelte-xyz) { + /* (unused) :global(.foo):has(.unused) { color: red; - } + }*/ x.svelte-xyz:has(y:where(.svelte-xyz) /* (unused) .unused*/) { color: green; @@ -111,3 +111,18 @@ /* (unused) x:has(~ y) { color: red; }*/ + + .foo { + .svelte-xyz:has(x:where(.svelte-xyz)) { + color: green; + } + /* (unused) :has(.unused) { + color: red; + }*/ + &:has(x.svelte-xyz) { + color: green; + } + /* (unused) &:has(.unused) { + color: red; + }*/ + } diff --git a/packages/svelte/tests/css/samples/has/input.svelte b/packages/svelte/tests/css/samples/has/input.svelte index 8eb6296f05..3487b64e8c 100644 --- a/packages/svelte/tests/css/samples/has/input.svelte +++ b/packages/svelte/tests/css/samples/has/input.svelte @@ -121,4 +121,19 @@ x:has(~ y) { color: red; } + + :global(.foo) { + :has(x) { + color: green; + } + :has(.unused) { + color: red; + } + &:has(x) { + color: green; + } + &:has(.unused) { + color: red; + } + } diff --git a/packages/svelte/tests/css/samples/is/_config.js b/packages/svelte/tests/css/samples/is/_config.js index e4c24eb756..eee617e7e4 100644 --- a/packages/svelte/tests/css/samples/is/_config.js +++ b/packages/svelte/tests/css/samples/is/_config.js @@ -30,20 +30,6 @@ export default test({ character: 125 } }, - { - code: 'css_unused_selector', - message: 'Unused CSS selector ".unused"', - start: { - line: 14, - column: 7, - character: 117 - }, - end: { - line: 14, - column: 14, - character: 124 - } - }, { code: 'css_unused_selector', message: 'Unused CSS selector ":global(.foo) :is(.unused)"', @@ -60,16 +46,30 @@ export default test({ }, { code: 'css_unused_selector', - message: 'Unused CSS selector ".unused"', + message: 'Unused CSS selector ":global(.foo):is(.unused)"', start: { - line: 28, - column: 19, - character: 292 + line: 34, + column: 1, + character: 363 }, end: { - line: 28, + line: 34, column: 26, - character: 299 + character: 388 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ":is(.unused)"', + start: { + line: 52, + column: 2, + character: 636 + }, + end: { + line: 52, + column: 14, + character: 648 } } ] diff --git a/packages/svelte/tests/css/samples/is/expected.css b/packages/svelte/tests/css/samples/is/expected.css index be8deeff28..639f0d5392 100644 --- a/packages/svelte/tests/css/samples/is/expected.css +++ b/packages/svelte/tests/css/samples/is/expected.css @@ -22,6 +22,12 @@ /* (unused) :global(.foo) :is(.unused) { color: red; }*/ + .foo:is(x.svelte-xyz) { + color: green; + } + /* (unused) :global(.foo):is(.unused) { + color: red; + }*/ x.svelte-xyz :is(html *) { color: green; @@ -32,3 +38,12 @@ y.svelte-xyz :is(x:where(.svelte-xyz) :where(.svelte-xyz)) { color: green; /* matches z */ } + + .foo { + :is(x.svelte-xyz) { + color: green; + } + /* (unused) :is(.unused) { + color: red; + }*/ + } diff --git a/packages/svelte/tests/css/samples/is/input.svelte b/packages/svelte/tests/css/samples/is/input.svelte index 06aa0669b9..b5ae663087 100644 --- a/packages/svelte/tests/css/samples/is/input.svelte +++ b/packages/svelte/tests/css/samples/is/input.svelte @@ -28,6 +28,12 @@ :global(.foo) :is(.unused) { color: red; } + :global(.foo):is(x) { + color: green; + } + :global(.foo):is(.unused) { + color: red; + } x :is(:global(html *)) { color: green; @@ -38,4 +44,13 @@ y :is(x *) { color: green; /* matches z */ } + + :global(.foo) { + :is(x) { + color: green; + } + :is(.unused) { + color: red; + } + } diff --git a/packages/svelte/tests/css/samples/not-selector-global/expected.css b/packages/svelte/tests/css/samples/not-selector-global/expected.css index ea9ff39486..b815dcf9aa 100644 --- a/packages/svelte/tests/css/samples/not-selector-global/expected.css +++ b/packages/svelte/tests/css/samples/not-selector-global/expected.css @@ -27,3 +27,12 @@ span:not(p span) { color: green; } + + .x { + .svelte-xyz:not(.foo) { + color: green; + } + &:not(.foo) { + color: green; + } + } diff --git a/packages/svelte/tests/css/samples/not-selector-global/input.svelte b/packages/svelte/tests/css/samples/not-selector-global/input.svelte index 1f0e6cd6db..1d2f7a9bca 100644 --- a/packages/svelte/tests/css/samples/not-selector-global/input.svelte +++ b/packages/svelte/tests/css/samples/not-selector-global/input.svelte @@ -34,4 +34,13 @@ :global(span:not(p span)) { color: green; } + + :global(.x) { + :not(.foo) { + color: green; + } + &:not(.foo) { + color: green; + } + } \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/root/_config.js b/packages/svelte/tests/css/samples/root/_config.js index f47bee71df..fad7fc536d 100644 --- a/packages/svelte/tests/css/samples/root/_config.js +++ b/packages/svelte/tests/css/samples/root/_config.js @@ -1,3 +1,76 @@ import { test } from '../../test'; -export default test({}); +export default test({ + warnings: [ + { + code: 'css_unused_selector', + message: 'Unused CSS selector ":root .unused"', + start: { + line: 18, + column: 2, + character: 190 + }, + end: { + line: 18, + column: 15, + character: 203 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ":root:has(.unused)"', + start: { + line: 25, + column: 2, + character: 269 + }, + end: { + line: 25, + column: 20, + character: 287 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ".unused"', + start: { + line: 37, + column: 4, + character: 401 + }, + end: { + line: 37, + column: 11, + character: 408 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ":has(.unused)"', + start: { + line: 43, + column: 4, + character: 480 + }, + end: { + line: 43, + column: 17, + character: 493 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector "&:has(.unused)"', + start: { + line: 49, + column: 4, + character: 566 + }, + end: { + line: 49, + column: 18, + character: 580 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/root/expected.css b/packages/svelte/tests/css/samples/root/expected.css index a107696250..b4c82c258e 100644 --- a/packages/svelte/tests/css/samples/root/expected.css +++ b/packages/svelte/tests/css/samples/root/expected.css @@ -1,9 +1,52 @@ + :root { - color: red; + color: green; } .foo:root { - color: blue; + color: green; } :root.foo { color: green; } + :root.unknown { + color: green; + } + + :root h1.svelte-xyz { + color: green; + } + /* (unused) :root .unused { + color: red; + }*/ + + :root:has(h1:where(.svelte-xyz)) { + color: green; + } + /* (unused) :root:has(.unused) { + color: red; + }*/ + + :root:not(.x) { + color: green; + } + + :root { + h1.svelte-xyz { + color: green; + } + /* (unused) .unused { + color: red; + }*/ + .svelte-xyz:has(h1:where(.svelte-xyz)) { + color: green; + } + /* (unused) :has(.unused) { + color: red; + }*/ + &:has(h1.svelte-xyz) { + color: green; + } + /* (unused) &:has(.unused) { + color: red; + }*/ + } diff --git a/packages/svelte/tests/css/samples/root/expected.html b/packages/svelte/tests/css/samples/root/expected.html index 1d90ab5df7..5c30de1c29 100644 --- a/packages/svelte/tests/css/samples/root/expected.html +++ b/packages/svelte/tests/css/samples/root/expected.html @@ -1 +1 @@ -

Hello!

\ No newline at end of file +

Hello!

\ No newline at end of file diff --git a/packages/svelte/tests/css/samples/root/input.svelte b/packages/svelte/tests/css/samples/root/input.svelte index 979c9d4f0a..e06a1a36fa 100644 --- a/packages/svelte/tests/css/samples/root/input.svelte +++ b/packages/svelte/tests/css/samples/root/input.svelte @@ -1,13 +1,55 @@

Hello!

From 320ebd24d8857570b0c180752765fb1580590367 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 14 Nov 2024 12:54:27 +0100 Subject: [PATCH 2/5] fix: bump `is-reference` dependency to fix `import.meta` bug (#14286) closes #14234 --- .changeset/shaggy-flowers-kneel.md | 5 ++++ packages/svelte/package.json | 2 +- packages/svelte/src/compiler/migrate/index.js | 3 ++ .../src/compiler/phases/2-analyze/index.js | 4 +++ .../visitors/ExportNamedDeclaration.js | 2 ++ .../2-analyze/visitors/ExportSpecifier.js | 15 +++++++--- .../2-analyze/visitors/ImportDeclaration.js | 5 ++-- packages/svelte/src/compiler/utils/ast.js | 11 +++++-- .../svelte/src/compiler/utils/builders.js | 5 ++-- pnpm-lock.yaml | 29 +++++++++++-------- 10 files changed, 57 insertions(+), 24 deletions(-) create mode 100644 .changeset/shaggy-flowers-kneel.md diff --git a/.changeset/shaggy-flowers-kneel.md b/.changeset/shaggy-flowers-kneel.md new file mode 100644 index 0000000000..fb0a041fbc --- /dev/null +++ b/.changeset/shaggy-flowers-kneel.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: bump `is-reference` dependency to fix `import.meta` bug diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 760272680e..608c8d466d 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -144,7 +144,7 @@ "axobject-query": "^4.1.0", "esm-env": "^1.0.0", "esrap": "^1.2.2", - "is-reference": "^3.0.2", + "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 9db09158a1..88f9bbf0ee 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -507,6 +507,7 @@ const instance_script = { for (let specifier of node.specifiers) { if ( specifier.type === 'ImportSpecifier' && + specifier.imported.type === 'Identifier' && ['beforeUpdate', 'afterUpdate'].includes(specifier.imported.name) ) { const references = state.scope.references.get(specifier.local.name); @@ -544,6 +545,8 @@ const instance_script = { let count_removed = 0; for (const specifier of node.specifiers) { + if (specifier.local.type !== 'Identifier') continue; + const binding = state.scope.get(specifier.local.name); if (binding?.kind === 'bindable_prop') { state.str.remove( diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 9e4f72a66d..a96652d60b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -474,6 +474,10 @@ export function analyze_component(root, source, options) { } } else { for (const specifier of node.specifiers) { + if (specifier.local.type !== 'Identifier' || specifier.exported.type !== 'Identifier') { + continue; + } + const binding = instance.scope.get(specifier.local.name); if ( diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportNamedDeclaration.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportNamedDeclaration.js index a9e8a05c96..784d2fdd88 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportNamedDeclaration.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportNamedDeclaration.js @@ -60,6 +60,8 @@ export function ExportNamedDeclaration(node, context) { if (!context.state.ast_type /* .svelte.js module */ || context.state.ast_type === 'module') { for (const specified of node.specifiers) { + if (specified.local.type !== 'Identifier') continue; + const binding = context.state.scope.get(specified.local.name); if (!binding) continue; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportSpecifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportSpecifier.js index 27eb39acbf..d0d1ccf932 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportSpecifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportSpecifier.js @@ -9,18 +9,25 @@ import * as e from '../../../errors.js'; * @param {Context} context */ export function ExportSpecifier(node, context) { + const local_name = + node.local.type === 'Identifier' ? node.local.name : /** @type {string} */ (node.local.value); + const exported_name = + node.exported.type === 'Identifier' + ? node.exported.name + : /** @type {string} */ (node.exported.value); + if (context.state.ast_type === 'instance') { if (context.state.analysis.runes) { context.state.analysis.exports.push({ - name: node.local.name, - alias: node.exported.name + name: local_name, + alias: exported_name }); - const binding = context.state.scope.get(node.local.name); + const binding = context.state.scope.get(local_name); if (binding) binding.reassigned = binding.updated = true; } } else { - validate_export(node, context.state.scope, node.local.name); + validate_export(node, context.state.scope, local_name); } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ImportDeclaration.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ImportDeclaration.js index eb57fee264..d46218ac64 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ImportDeclaration.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ImportDeclaration.js @@ -18,8 +18,9 @@ export function ImportDeclaration(node, context) { for (const specifier of node.specifiers) { if (specifier.type === 'ImportSpecifier') { if ( - specifier.imported.name === 'beforeUpdate' || - specifier.imported.name === 'afterUpdate' + specifier.imported.type === 'Identifier' && + (specifier.imported.name === 'beforeUpdate' || + specifier.imported.name === 'afterUpdate') ) { e.runes_mode_invalid_import(specifier, specifier.imported.name); } diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js index 9aff7546be..cc8339f16b 100644 --- a/packages/svelte/src/compiler/utils/ast.js +++ b/packages/svelte/src/compiler/utils/ast.js @@ -433,7 +433,11 @@ export function is_simple_expression(node) { } if (node.type === 'BinaryExpression' || node.type === 'LogicalExpression') { - return is_simple_expression(node.left) && is_simple_expression(node.right); + return ( + node.left.type !== 'PrivateIdentifier' && + is_simple_expression(node.left) && + is_simple_expression(node.right) + ); } return false; @@ -475,7 +479,10 @@ export function is_expression_async(expression) { case 'AssignmentExpression': case 'BinaryExpression': case 'LogicalExpression': { - return is_expression_async(expression.left) || is_expression_async(expression.right); + return ( + (expression.left.type !== 'PrivateIdentifier' && is_expression_async(expression.left)) || + is_expression_async(expression.right) + ); } case 'CallExpression': case 'NewExpression': { diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index fc1cdf4adb..ecb595d74d 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -350,7 +350,7 @@ export function prop(kind, key, value, computed = false) { * @returns {ESTree.PropertyDefinition} */ export function prop_def(key, value, computed = false, is_static = false) { - return { type: 'PropertyDefinition', key, value, computed, static: is_static, decorators: [] }; + return { type: 'PropertyDefinition', key, value, computed, static: is_static }; } /** @@ -551,8 +551,7 @@ export function method(kind, key, params, body, computed = false, is_static = fa kind, value: function_builder(null, params, block(body)), computed, - static: is_static, - decorators: [] + static: is_static }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f33a207fc..83e4522b2f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,7 +67,7 @@ importers: version: 1.5.0 '@types/estree': specifier: ^1.0.5 - version: 1.0.5 + version: 1.0.6 acorn: specifier: ^8.12.1 version: 8.12.1 @@ -87,8 +87,8 @@ importers: specifier: ^1.2.2 version: 1.2.2 is-reference: - specifier: ^3.0.2 - version: 3.0.2 + specifier: ^3.0.3 + version: 3.0.3 locate-character: specifier: ^3.0.0 version: 3.0.0 @@ -916,6 +916,9 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -1775,8 +1778,8 @@ packages: is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} - is-reference@3.0.2: - resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} is-stream@3.0.0: resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} @@ -3468,7 +3471,7 @@ snapshots: '@rollup/pluginutils@5.1.0(rollup@4.22.4)': dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 estree-walker: 2.0.2 picomatch: 2.3.1 optionalDependencies: @@ -3611,11 +3614,13 @@ snapshots: '@types/eslint@8.56.12': dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 '@types/json-schema': 7.0.15 '@types/estree@1.0.5': {} + '@types/estree@1.0.6': {} + '@types/json-schema@7.0.15': {} '@types/node@12.20.55': {} @@ -4243,7 +4248,7 @@ snapshots: esrap@1.2.2: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 esrecurse@4.3.0: dependencies: @@ -4255,7 +4260,7 @@ snapshots: estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 esutils@2.0.3: {} @@ -4556,11 +4561,11 @@ snapshots: is-reference@1.2.1: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 - is-reference@3.0.2: + is-reference@3.0.3: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 is-stream@3.0.0: {} From 6a7146bee7b98d4745b8d13fb5a7d0c3c4720a34 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 14 Nov 2024 12:47:14 -0500 Subject: [PATCH 3/5] Version Packages (#14287) Co-authored-by: github-actions[bot] --- .changeset/beige-files-pull.md | 5 ----- .changeset/great-bulldogs-wonder.md | 5 ----- .changeset/hot-frogs-melt.md | 5 ----- .changeset/shaggy-flowers-kneel.md | 5 ----- packages/svelte/CHANGELOG.md | 12 ++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 7 files changed, 14 insertions(+), 22 deletions(-) delete mode 100644 .changeset/beige-files-pull.md delete mode 100644 .changeset/great-bulldogs-wonder.md delete mode 100644 .changeset/hot-frogs-melt.md delete mode 100644 .changeset/shaggy-flowers-kneel.md diff --git a/.changeset/beige-files-pull.md b/.changeset/beige-files-pull.md deleted file mode 100644 index 2cd98a2819..0000000000 --- a/.changeset/beige-files-pull.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: account for `:has(...)` as part of `:root` diff --git a/.changeset/great-bulldogs-wonder.md b/.changeset/great-bulldogs-wonder.md deleted file mode 100644 index b6cafa4585..0000000000 --- a/.changeset/great-bulldogs-wonder.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: prevent nested pseudo class from being marked as unused diff --git a/.changeset/hot-frogs-melt.md b/.changeset/hot-frogs-melt.md deleted file mode 100644 index 95b3e0b4b4..0000000000 --- a/.changeset/hot-frogs-melt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: use strict equality for key block comparisons in runes mode diff --git a/.changeset/shaggy-flowers-kneel.md b/.changeset/shaggy-flowers-kneel.md deleted file mode 100644 index fb0a041fbc..0000000000 --- a/.changeset/shaggy-flowers-kneel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: bump `is-reference` dependency to fix `import.meta` bug diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 879fdb2058..8040c4ecab 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.1.17 + +### Patch Changes + +- fix: account for `:has(...)` as part of `:root` ([#14229](https://github.com/sveltejs/svelte/pull/14229)) + +- fix: prevent nested pseudo class from being marked as unused ([#14229](https://github.com/sveltejs/svelte/pull/14229)) + +- fix: use strict equality for key block comparisons in runes mode ([#14285](https://github.com/sveltejs/svelte/pull/14285)) + +- fix: bump `is-reference` dependency to fix `import.meta` bug ([#14286](https://github.com/sveltejs/svelte/pull/14286)) + ## 5.1.16 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 608c8d466d..ffe19223c2 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.1.16", + "version": "5.1.17", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index c6802cecf0..7c0d0680ea 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.1.16'; +export const VERSION = '5.1.17'; export const PUBLIC_VERSION = '5'; From 8a8e6f70e8955cb3d7aa256710a0ac35ae27f18d Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Thu, 14 Nov 2024 18:48:11 +0100 Subject: [PATCH 4/5] fix: avoid marking subtree as dynamic for inlined attributes (#14269) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: avoid marking subtree as dynamic for inlined attributes * fix: i'm a silly goose 🪿 * chore: refactor `is_inlinable_expression` to accept the attribute * feat: inline dom expression too * fix: special case literals with `"` in it and fix standalone case * chore: simpler check first Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> * typo * add more stuff to snapshot test * simplify/speedup by doing the work once, during analysis * simplify * simplify - no reason these cases should prevent inlining * return template * name is incorrect * name is incorrect * fix escaping * no longer necessary * remove obsolete description * better concatenation * fix test * do the work at runtime * fix another thing * tidy * tidy up * simplify * simplify * fix * note to self * another * simplify * more accurate name * simplify * simplify * explain what is happening * tidy up * simplify * better inlining * update test * colocate some code * better inlining * use attribute metadata * Update packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> * Apply suggestions from code review --------- Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> Co-authored-by: Rich Harris --- .changeset/spotty-sheep-fetch.md | 5 + .../phases/2-analyze/visitors/Attribute.js | 14 +- .../2-analyze/visitors/CallExpression.js | 1 + .../2-analyze/visitors/ExpressionTag.js | 5 - .../phases/2-analyze/visitors/Identifier.js | 22 +++- .../2-analyze/visitors/MemberExpression.js | 1 + .../visitors/TaggedTemplateExpression.js | 1 + .../phases/3-transform/client/utils.js | 52 +------- .../3-transform/client/visitors/Fragment.js | 38 ++++-- .../client/visitors/RegularElement.js | 106 +++++++++------ .../client/visitors/TitleElement.js | 4 +- .../client/visitors/shared/element.js | 4 +- .../client/visitors/shared/fragment.js | 74 ++++++++--- .../client/visitors/shared/utils.js | 124 +++++++++--------- packages/svelte/src/compiler/phases/nodes.js | 3 +- packages/svelte/src/compiler/types/index.d.ts | 2 + packages/svelte/src/internal/client/index.js | 2 + packages/svelte/src/internal/server/index.js | 30 +---- .../svelte/src/internal/shared/attributes.js | 28 ++++ .../main.svelte | 2 +- .../_expected/client/index.svelte.js | 11 +- .../_expected/server/index.svelte.js | 4 +- .../samples/inline-module-vars/index.svelte | 7 +- 23 files changed, 313 insertions(+), 227 deletions(-) create mode 100644 .changeset/spotty-sheep-fetch.md create mode 100644 packages/svelte/src/internal/shared/attributes.js diff --git a/.changeset/spotty-sheep-fetch.md b/.changeset/spotty-sheep-fetch.md new file mode 100644 index 0000000000..8a23f40e3a --- /dev/null +++ b/.changeset/spotty-sheep-fetch.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: better inlining of static attributes diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 2a281a1aa3..6931f873fb 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -1,7 +1,7 @@ /** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */ /** @import { AST, DelegatedEvent, SvelteNode } from '#compiler' */ /** @import { Context } from '../types' */ -import { is_capture_event, is_delegated } from '../../../../utils.js'; +import { is_boolean_attribute, is_capture_event, is_delegated } from '../../../../utils.js'; import { get_attribute_chunks, get_attribute_expression, @@ -16,14 +16,23 @@ import { mark_subtree_dynamic } from './shared/fragment.js'; export function Attribute(node, context) { context.next(); + const parent = /** @type {SvelteNode} */ (context.path.at(-1)); + // special case if (node.name === 'value') { - const parent = /** @type {SvelteNode} */ (context.path.at(-1)); if (parent.type === 'RegularElement' && parent.name === 'option') { mark_subtree_dynamic(context.path); } } + if (node.name.startsWith('on')) { + mark_subtree_dynamic(context.path); + } + + if (parent.type === 'RegularElement' && is_boolean_attribute(node.name.toLowerCase())) { + node.metadata.expression.can_inline = false; + } + if (node.value !== true) { for (const chunk of get_attribute_chunks(node.value)) { if (chunk.type !== 'ExpressionTag') continue; @@ -37,6 +46,7 @@ export function Attribute(node, context) { node.metadata.expression.has_state ||= chunk.metadata.expression.has_state; node.metadata.expression.has_call ||= chunk.metadata.expression.has_call; + node.metadata.expression.can_inline &&= chunk.metadata.expression.can_inline; } if (is_event_attribute(node)) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 957b27ae9b..2ae32e80e1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -178,6 +178,7 @@ export function CallExpression(node, context) { if (!is_pure(node.callee, context) || context.state.expression.dependencies.size > 0) { context.state.expression.has_call = true; context.state.expression.has_state = true; + context.state.expression.can_inline = false; } } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExpressionTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExpressionTag.js index 32c8d2ca36..f59b7fc569 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExpressionTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExpressionTag.js @@ -2,7 +2,6 @@ /** @import { Context } from '../types' */ import { is_tag_valid_with_parent } from '../../../../html-tree-validation.js'; import * as e from '../../../errors.js'; -import { mark_subtree_dynamic } from './shared/fragment.js'; /** * @param {AST.ExpressionTag} node @@ -15,9 +14,5 @@ export function ExpressionTag(node, context) { } } - // TODO ideally we wouldn't do this here, we'd just do it on encountering - // an `Identifier` within the tag. But we currently need to handle `{42}` etc - mark_subtree_dynamic(context.path); - context.next({ ...context.state, expression: node.metadata.expression }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js index 79dccd5a7c..635f939c75 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -1,5 +1,4 @@ /** @import { Expression, Identifier } from 'estree' */ -/** @import { EachBlock } from '#compiler' */ /** @import { Context } from '../types' */ import is_reference from 'is-reference'; import { should_proxy } from '../../3-transform/client/utils.js'; @@ -20,8 +19,6 @@ export function Identifier(node, context) { return; } - mark_subtree_dynamic(context.path); - // If we are using arguments outside of a function, then throw an error if ( node.name === 'arguments' && @@ -87,6 +84,12 @@ export function Identifier(node, context) { } } + // no binding means global, and we can't inline e.g. `{location}` + // because it could change between component renders. if there _is_ a + // binding and it is outside module scope, the expression cannot + // be inlined (TODO allow inlining in more cases - e.g. primitive consts) + let can_inline = !!binding && !binding.scope.parent && binding.kind === 'normal'; + if (binding) { if (context.state.expression) { context.state.expression.dependencies.add(binding); @@ -122,4 +125,17 @@ export function Identifier(node, context) { w.reactive_declaration_module_script_dependency(node); } } + + if (!can_inline && context.state.expression) { + context.state.expression.can_inline = false; + } + + /** + * if the identifier is part of an expression tag of an attribute we want to check if it's inlinable + * before marking the subtree as dynamic. This is because if it's inlinable it will be inlined in the template + * directly making the whole thing actually static. + */ + if (!can_inline || !context.path.find((node) => node.type === 'Attribute')) { + mark_subtree_dynamic(context.path); + } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js index 171a1106a8..adcc2da422 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js @@ -19,6 +19,7 @@ export function MemberExpression(node, context) { if (context.state.expression && !is_pure(node, context)) { context.state.expression.has_state = true; + context.state.expression.can_inline = false; } if (!is_safe_identifier(node, context.state.scope)) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js index eacb8a342a..724b9af311 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js @@ -10,6 +10,7 @@ export function TaggedTemplateExpression(node, context) { if (context.state.expression && !is_pure(node.tag, context)) { context.state.expression.has_call = true; context.state.expression.has_state = true; + context.state.expression.can_inline = false; } if (node.tag.type === 'Identifier') { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index c460905977..910f173f79 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -1,18 +1,18 @@ /** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, Pattern, PrivateIdentifier, Statement } from 'estree' */ -/** @import { AST, Binding, SvelteNode } from '#compiler' */ +/** @import { Binding, SvelteNode } from '#compiler' */ /** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */ /** @import { Analysis } from '../../types.js' */ /** @import { Scope } from '../../scope.js' */ -import * as b from '../../../utils/builders.js'; -import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js'; import { - PROPS_IS_LAZY_INITIAL, + PROPS_IS_BINDABLE, PROPS_IS_IMMUTABLE, + PROPS_IS_LAZY_INITIAL, PROPS_IS_RUNES, - PROPS_IS_UPDATED, - PROPS_IS_BINDABLE + PROPS_IS_UPDATED } from '../../../../constants.js'; import { dev } from '../../../state.js'; +import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js'; +import * as b from '../../../utils/builders.js'; import { get_value } from './visitors/shared/declarations.js'; /** @@ -311,43 +311,3 @@ export function create_derived_block_argument(node, context) { export function create_derived(state, arg) { return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg); } - -/** - * Whether a variable can be referenced directly from template string. - * @param {import('#compiler').Binding | undefined} binding - * @returns {boolean} - */ -export function can_inline_variable(binding) { - return ( - !!binding && - // in a ` - production test + production test + +

{a} + {b} = {a + b}

From 312dd51acc44d92ab855f7eaf15d2d9150c0a6f3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 14 Nov 2024 12:56:16 -0500 Subject: [PATCH 5/5] Version Packages (#14302) Co-authored-by: github-actions[bot] --- .changeset/spotty-sheep-fetch.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/spotty-sheep-fetch.md diff --git a/.changeset/spotty-sheep-fetch.md b/.changeset/spotty-sheep-fetch.md deleted file mode 100644 index 8a23f40e3a..0000000000 --- a/.changeset/spotty-sheep-fetch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: better inlining of static attributes diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 8040c4ecab..f7e17f14f9 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.2.0 + +### Minor Changes + +- feat: better inlining of static attributes ([#14269](https://github.com/sveltejs/svelte/pull/14269)) + ## 5.1.17 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index ffe19223c2..b23d031a17 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.1.17", + "version": "5.2.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 7c0d0680ea..e3d34edc4e 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.1.17'; +export const VERSION = '5.2.0'; export const PUBLIC_VERSION = '5';