diff --git a/.changeset/famous-bulldogs-tan.md b/.changeset/famous-bulldogs-tan.md new file mode 100644 index 0000000000..a5cc14b7f2 --- /dev/null +++ b/.changeset/famous-bulldogs-tan.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: do not prune selectors like `:global(.foo):has(.scoped)` diff --git a/.changeset/good-rocks-talk.md b/.changeset/good-rocks-talk.md new file mode 100644 index 0000000000..59af86c686 --- /dev/null +++ b/.changeset/good-rocks-talk.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't error on slot prop inside block inside other component diff --git a/.changeset/loud-cars-scream.md b/.changeset/loud-cars-scream.md new file mode 100644 index 0000000000..2b61bc4453 --- /dev/null +++ b/.changeset/loud-cars-scream.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure reactions are correctly attached for unowned deriveds diff --git a/.changeset/odd-rules-hear.md b/.changeset/odd-rules-hear.md new file mode 100644 index 0000000000..325b8ddf96 --- /dev/null +++ b/.changeset/odd-rules-hear.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: silence a11y attribute warnings when spread attributes present diff --git a/.changeset/ten-cougars-look.md b/.changeset/ten-cougars-look.md new file mode 100644 index 0000000000..fe20d057dd --- /dev/null +++ b/.changeset/ten-cougars-look.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent false-positive ownership validations due to hot reload diff --git a/.changeset/unlucky-gorillas-hunt.md b/.changeset/unlucky-gorillas-hunt.md new file mode 100644 index 0000000000..055fc120a6 --- /dev/null +++ b/.changeset/unlucky-gorillas-hunt.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: widen ownership when calling setContext 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 109010e88c..e719895798 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 @@ -339,13 +339,18 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) 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). + // because the global selector might be for an element that's outside the component, + // e.g. :root:has(.scoped), :global(.foo):has(.scoped), or :root { &:has(.scoped) {} } const rules = 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') + r.selectors.some( + (s) => + s.type === 'PseudoClassSelector' && + (s.name === 'root' || (s.name === 'global' && s.args)) + ) ) ); if (include_self) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js index a5ca8463a4..24a8e5122d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y.js @@ -756,7 +756,8 @@ export function check_element(node, context) { name === 'aria-activedescendant' && !is_dynamic_element && !is_interactive_element(node.name, attribute_map) && - !attribute_map.has('tabindex') + !attribute_map.has('tabindex') && + !has_spread ) { w.a11y_aria_activedescendant_has_tabindex(attribute); } @@ -810,9 +811,9 @@ export function check_element(node, context) { const role = roles_map.get(current_role); if (role) { const required_role_props = Object.keys(role.requiredProps); - const has_missing_props = required_role_props.some( - (prop) => !attributes.find((a) => a.name === prop) - ); + const has_missing_props = + !has_spread && + required_role_props.some((prop) => !attributes.find((a) => a.name === prop)); if (has_missing_props) { w.a11y_role_has_required_aria_props( attribute, @@ -828,6 +829,7 @@ export function check_element(node, context) { // interactive-supports-focus if ( + !has_spread && !has_disabled_attribute(attribute_map) && !is_hidden_from_screen_reader(node.name, attribute_map) && !is_presentation_role(current_role) && @@ -845,6 +847,7 @@ export function check_element(node, context) { // no-interactive-element-to-noninteractive-role if ( + !has_spread && is_interactive_element(node.name, attribute_map) && (is_non_interactive_roles(current_role) || is_presentation_role(current_role)) ) { @@ -853,6 +856,7 @@ export function check_element(node, context) { // no-noninteractive-element-to-interactive-role if ( + !has_spread && is_non_interactive_element(node.name, attribute_map) && is_interactive_roles(current_role) && !a11y_non_interactive_element_to_interactive_role_exceptions[node.name]?.includes( @@ -947,6 +951,7 @@ export function check_element(node, context) { // no-noninteractive-element-interactions if ( + !has_spread && !has_contenteditable_attr && !is_hidden_from_screen_reader(node.name, attribute_map) && !is_presentation_role(role_static_value) && @@ -964,6 +969,7 @@ export function check_element(node, context) { // no-static-element-interactions if ( + !has_spread && (!role || role_static_value !== null) && !is_hidden_from_screen_reader(node.name, attribute_map) && !is_presentation_role(role_static_value) && @@ -981,11 +987,11 @@ export function check_element(node, context) { } } - if (handlers.has('mouseover') && !handlers.has('focus')) { + if (!has_spread && handlers.has('mouseover') && !handlers.has('focus')) { w.a11y_mouse_events_have_key_events(node, 'mouseover', 'focus'); } - if (handlers.has('mouseout') && !handlers.has('blur')) { + if (!has_spread && handlers.has('mouseout') && !handlers.has('blur')) { w.a11y_mouse_events_have_key_events(node, 'mouseout', 'blur'); } @@ -995,7 +1001,7 @@ export function check_element(node, context) { if (node.name === 'a' || node.name === 'button') { const is_hidden = get_static_value(attribute_map.get('aria-hidden')) === 'true'; - if (!is_hidden && !is_labelled && !has_content(node)) { + if (!has_spread && !is_hidden && !is_labelled && !has_content(node)) { w.a11y_consider_explicit_label(node); } } @@ -1054,7 +1060,7 @@ export function check_element(node, context) { if (node.name === 'img') { const alt_attribute = get_static_text_value(attribute_map.get('alt')); const aria_hidden = get_static_value(attribute_map.get('aria-hidden')); - if (alt_attribute && !aria_hidden) { + if (alt_attribute && !aria_hidden && !has_spread) { if (/\b(image|picture|photo)\b/i.test(alt_attribute)) { w.a11y_img_redundant_alt(node); } @@ -1087,7 +1093,7 @@ export function check_element(node, context) { ); return has; }; - if (!attribute_map.has('for') && !has_input_child(node)) { + if (!has_spread && !attribute_map.has('for') && !has_input_child(node)) { w.a11y_label_has_associated_control(node); } } @@ -1095,7 +1101,7 @@ export function check_element(node, context) { if (node.name === 'video') { const aria_hidden_attribute = attribute_map.get('aria-hidden'); const aria_hidden_exist = aria_hidden_attribute && get_static_value(aria_hidden_attribute); - if (attribute_map.has('muted') || aria_hidden_exist === 'true') { + if (attribute_map.has('muted') || aria_hidden_exist === 'true' || has_spread) { return; } let has_caption = false; @@ -1141,6 +1147,7 @@ export function check_element(node, context) { // Check content if ( + !has_spread && !is_labelled && !has_contenteditable_binding && a11y_required_content.includes(node.name) && diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/attribute.js index 198e464ac7..19bd7b6e54 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/attribute.js @@ -80,40 +80,42 @@ export function validate_slot_attribute(context, attribute, is_component = false } if (owner) { - if (!is_text_attribute(attribute)) { - e.slot_attribute_invalid(attribute); - } - if ( owner.type === 'Component' || owner.type === 'SvelteComponent' || owner.type === 'SvelteSelf' ) { if (owner !== parent) { - e.slot_attribute_invalid_placement(attribute); - } + if (!is_component) { + e.slot_attribute_invalid_placement(attribute); + } + } else { + if (!is_text_attribute(attribute)) { + e.slot_attribute_invalid(attribute); + } - const name = attribute.value[0].data; + const name = attribute.value[0].data; - if (context.state.component_slots.has(name)) { - e.slot_attribute_duplicate(attribute, name, owner.name); - } - - context.state.component_slots.add(name); + if (context.state.component_slots.has(name)) { + e.slot_attribute_duplicate(attribute, name, owner.name); + } - if (name === 'default') { - for (const node of owner.fragment.nodes) { - if (node.type === 'Text' && regex_only_whitespaces.test(node.data)) { - continue; - } + context.state.component_slots.add(name); - if (node.type === 'RegularElement' || node.type === 'SvelteFragment') { - if (node.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot')) { + if (name === 'default') { + for (const node of owner.fragment.nodes) { + if (node.type === 'Text' && regex_only_whitespaces.test(node.data)) { continue; } - } - e.slot_default_duplicate(node); + if (node.type === 'RegularElement' || node.type === 'SvelteFragment') { + if (node.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot')) { + continue; + } + } + + e.slot_default_duplicate(node); + } } } } diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index e1088edf30..bd94d5ad8a 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -8,7 +8,8 @@ import { active_effect, active_reaction, set_active_effect, - set_active_reaction + set_active_reaction, + untrack } from './runtime.js'; import { effect } from './reactivity/effects.js'; import { legacy_mode_flag } from '../flags/index.js'; @@ -49,14 +50,6 @@ export function set_dev_current_component_function(fn) { export function getContext(key) { const context_map = get_or_init_context_map('getContext'); const result = /** @type {T} */ (context_map.get(key)); - - if (DEV) { - const fn = /** @type {ComponentContext} */ (component_context).function; - if (fn) { - add_owner(result, fn, true); - } - } - return result; } @@ -74,6 +67,15 @@ export function getContext(key) { */ export function setContext(key, context) { const context_map = get_or_init_context_map('setContext'); + + if (DEV) { + // When state is put into context, we treat as if it's global from now on. + // We do for performance reasons (it's for example very expensive to call + // getContext on a big object many times when part of a list component) + // and danger of false positives. + untrack(() => add_owner(context, null, true)); + } + context_map.set(key, context); return context; } @@ -100,16 +102,6 @@ export function hasContext(key) { */ export function getAllContexts() { const context_map = get_or_init_context_map('getAllContexts'); - - if (DEV) { - const fn = component_context?.function; - if (fn) { - for (const value of context_map.values()) { - add_owner(value, fn, true); - } - } - } - return /** @type {T} */ (context_map); } diff --git a/packages/svelte/src/internal/client/dev/ownership.js b/packages/svelte/src/internal/client/dev/ownership.js index 70cfbb47f3..2a2527803a 100644 --- a/packages/svelte/src/internal/client/dev/ownership.js +++ b/packages/svelte/src/internal/client/dev/ownership.js @@ -109,7 +109,7 @@ export function mark_module_end(component) { /** * @param {any} object - * @param {any} owner + * @param {any | null} owner * @param {boolean} [global] * @param {boolean} [skip_warning] */ @@ -120,7 +120,7 @@ export function add_owner(object, owner, global = false, skip_warning = false) { if (metadata && !has_owner(metadata, component)) { let original = get_owner(metadata); - if (owner[FILENAME] !== component[FILENAME] && !skip_warning) { + if (owner && owner[FILENAME] !== component[FILENAME] && !skip_warning) { w.ownership_invalid_binding(component[FILENAME], owner[FILENAME], original[FILENAME]); } } @@ -165,7 +165,7 @@ export function widen_ownership(from, to) { /** * @param {any} object - * @param {Function} owner + * @param {Function | null} owner If `null`, then the object is globally owned and will not be checked * @param {Set} seen */ function add_owner_to_object(object, owner, seen) { @@ -174,7 +174,11 @@ function add_owner_to_object(object, owner, seen) { if (metadata) { // this is a state proxy, add owner directly, if not globally shared if ('owners' in metadata && metadata.owners != null) { - metadata.owners.add(owner); + if (owner) { + metadata.owners.add(owner); + } else { + metadata.owners = null; + } } } else if (object && typeof object === 'object') { if (seen.has(object)) return; @@ -216,6 +220,10 @@ function has_owner(metadata, component) { return ( metadata.owners.has(component) || + // This helps avoid false positives when using HMR, where the component function is replaced + [...metadata.owners].some( + (owner) => /** @type {any} */ (owner)[FILENAME] === /** @type {any} */ (component)?.[FILENAME] + ) || (metadata.parent !== null && has_owner(metadata.parent, component)) ); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 72bd359f18..9efb42da09 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -833,7 +833,12 @@ function process_effects(effect, collected_effects) { if (is_branch) { current_effect.f ^= CLEAN; } else if (!skip_suspended) { + // Ensure we set the effect to be the active reaction + // to ensure that unowned deriveds are correctly tracked + // because we're flushing the current effect + var previous_active_reaction = active_reaction; try { + active_reaction = current_effect; if (check_dirtiness(current_effect)) { update_effect(current_effect); if ((flags & IS_ASYNC) !== 0 && !suspended) { @@ -842,6 +847,8 @@ function process_effects(effect, collected_effects) { } } catch (error) { handle_error(error, current_effect, null, current_effect.ctx); + } finally { + active_reaction = previous_active_reaction; } } @@ -1013,13 +1020,11 @@ export function get(signal) { var derived = /** @type {Derived} */ (signal); var parent = derived.parent; - if (parent !== null) { + if (parent !== null && (parent.f & UNOWNED) === 0) { // If the derived is owned by another derived then mark it as unowned // as the derived value might have been referenced in a different context // since and thus its parent might not be its true owner anymore - if ((parent.f & UNOWNED) === 0) { - derived.f ^= UNOWNED; - } + derived.f ^= UNOWNED; } } diff --git a/packages/svelte/tests/css/samples/has/_config.js b/packages/svelte/tests/css/samples/has/_config.js index 33bbe74949..8d89d98cbd 100644 --- a/packages/svelte/tests/css/samples/has/_config.js +++ b/packages/svelte/tests/css/samples/has/_config.js @@ -197,6 +197,20 @@ export default test({ column: 16, character: 1614 } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector ":global(.foo):has(.unused)"', + start: { + line: 155, + column: 1, + character: 1684 + }, + end: { + line: 155, + column: 27, + character: 1710 + } } ] }); diff --git a/packages/svelte/tests/css/samples/has/expected.css b/packages/svelte/tests/css/samples/has/expected.css index 9627bf730c..b257370d61 100644 --- a/packages/svelte/tests/css/samples/has/expected.css +++ b/packages/svelte/tests/css/samples/has/expected.css @@ -136,3 +136,10 @@ color: red; }*/ } + + .foo:has(x.svelte-xyz) { + color: green; + } + /* (unused) :global(.foo):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 946cf2df90..9b254996bf 100644 --- a/packages/svelte/tests/css/samples/has/input.svelte +++ b/packages/svelte/tests/css/samples/has/input.svelte @@ -148,4 +148,11 @@ color: red; } } + + :global(.foo):has(x) { + color: green; + } + :global(.foo):has(.unused) { + color: red; + } diff --git a/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/Child.svelte b/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/Child.svelte new file mode 100644 index 0000000000..cd215304a3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/Child.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/Child2.svelte b/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/Child2.svelte new file mode 100644 index 0000000000..a1d9f93bec --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/Child2.svelte @@ -0,0 +1,5 @@ + + +{disabled} diff --git a/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/_config.js new file mode 100644 index 0000000000..9948f91966 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/_config.js @@ -0,0 +1,16 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + let [btn1, btn2] = target.querySelectorAll('button'); + + btn1?.click(); + flushSync(); + + btn2?.click(); + flushSync(); + + assert.htmlEqual(target.innerHTML, `\nfalse`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/main.svelte new file mode 100644 index 0000000000..0219acdf7f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-unowned-11/main.svelte @@ -0,0 +1,11 @@ + + + + + + diff --git a/packages/svelte/tests/validator/samples/a11y-label-has-associated-control/input.svelte b/packages/svelte/tests/validator/samples/a11y-label-has-associated-control/input.svelte index 124888c089..f47743b33b 100644 --- a/packages/svelte/tests/validator/samples/a11y-label-has-associated-control/input.svelte +++ b/packages/svelte/tests/validator/samples/a11y-label-has-associated-control/input.svelte @@ -10,3 +10,4 @@ G + \ No newline at end of file diff --git a/packages/svelte/tests/validator/samples/a11y-mouse-events-have-key-events/input.svelte b/packages/svelte/tests/validator/samples/a11y-mouse-events-have-key-events/input.svelte index 613b80e6d9..f9fe4f15c1 100644 --- a/packages/svelte/tests/validator/samples/a11y-mouse-events-have-key-events/input.svelte +++ b/packages/svelte/tests/validator/samples/a11y-mouse-events-have-key-events/input.svelte @@ -1,21 +1,19 @@ -
void 0}>
+
{}}>
-
void 0} on:focus={() => void 0}>
+
{}} onfocus={() => {}}>
-
void 0} {...otherProps}>
+
{}} {...otherProps}>
-
void 0}>
+
{}}>
-
void 0} on:blur={() => void 0}>
+
{}} onblur={() => {}}>
-
void 0} {...otherProps}>
+
{}} {...otherProps}>
diff --git a/packages/svelte/tests/validator/samples/a11y-mouse-events-have-key-events/warnings.json b/packages/svelte/tests/validator/samples/a11y-mouse-events-have-key-events/warnings.json index 574b019e0f..3dee4e9673 100644 --- a/packages/svelte/tests/validator/samples/a11y-mouse-events-have-key-events/warnings.json +++ b/packages/svelte/tests/validator/samples/a11y-mouse-events-have-key-events/warnings.json @@ -2,49 +2,25 @@ { "code": "a11y_mouse_events_have_key_events", "end": { - "column": 39, - "line": 11 + "column": 34, + "line": 9 }, "message": "'mouseover' event must be accompanied by 'focus' event", "start": { "column": 0, - "line": 11 + "line": 9 } }, { "code": "a11y_mouse_events_have_key_events", "end": { - "column": 55, + "column": 33, "line": 15 }, - "message": "'mouseover' event must be accompanied by 'focus' event", - "start": { - "column": 0, - "line": 15 - } - }, - { - "code": "a11y_mouse_events_have_key_events", - "end": { - "column": 38, - "line": 17 - }, "message": "'mouseout' event must be accompanied by 'blur' event", "start": { "column": 0, - "line": 17 - } - }, - { - "code": "a11y_mouse_events_have_key_events", - "end": { - "column": 54, - "line": 21 - }, - "message": "'mouseout' event must be accompanied by 'blur' event", - "start": { - "column": 0, - "line": 21 + "line": 15 } } ] diff --git a/packages/svelte/tests/validator/samples/slot-attribute-component/input.svelte b/packages/svelte/tests/validator/samples/slot-attribute-component/input.svelte index 5acb14e409..5d559e614e 100644 --- a/packages/svelte/tests/validator/samples/slot-attribute-component/input.svelte +++ b/packages/svelte/tests/validator/samples/slot-attribute-component/input.svelte @@ -1,2 +1,8 @@ valid valid + + {#if true} + valid + valid + {/if} + \ No newline at end of file