From 63456f1df922c70718102df0349b152b8e45251c Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 17 Apr 2024 09:23:48 +0100 Subject: [PATCH 01/13] fix: remove memory leak from bind:this (#11194) * fix: remove memory leak from bind:this * alternative approach * add error * tidy * tidy * add TODO * add TODO * alternative approach --- .changeset/itchy-eels-marry.md | 5 +++ .../client/dom/elements/bindings/this.js | 4 ++- .../svelte/src/internal/client/dom/task.js | 20 +++++------ .../src/internal/client/reactivity/effects.js | 34 ++++++++++++++++++- .../svelte/src/internal/client/runtime.js | 17 ++++++++-- 5 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 .changeset/itchy-eels-marry.md diff --git a/.changeset/itchy-eels-marry.md b/.changeset/itchy-eels-marry.md new file mode 100644 index 0000000000..59ea9d9dbe --- /dev/null +++ b/.changeset/itchy-eels-marry.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: remove memory leak from bind:this diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/this.js b/packages/svelte/src/internal/client/dom/elements/bindings/this.js index 940ac9f62f..b95d2a4239 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/this.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/this.js @@ -1,6 +1,7 @@ import { STATE_SYMBOL } from '../../../constants.js'; import { effect, render_effect } from '../../../reactivity/effects.js'; import { untrack } from '../../../runtime.js'; +import { queue_task } from '../../task.js'; /** * @param {any} bound_value @@ -47,7 +48,8 @@ export function bind_this(element_or_component, update, get_value, get_parts) { }); return () => { - effect(() => { + // We cannot use effects in the teardown phase, we we use a microtask instead. + queue_task(() => { if (parts && is_bound_this(get_value(...parts), element_or_component)) { update(null, ...parts); } diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index 89307a4e56..c1a2a7e162 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -1,12 +1,9 @@ import { run_all } from '../../shared/utils.js'; let is_task_queued = false; -let is_raf_queued = false; /** @type {Array<() => void>} */ let current_queued_tasks = []; -/** @type {Array<() => void>} */ -let current_raf_tasks = []; function process_task() { is_task_queued = false; @@ -15,11 +12,15 @@ function process_task() { run_all(tasks); } -function process_raf_task() { - is_raf_queued = false; - const tasks = current_raf_tasks.slice(); - current_raf_tasks = []; - run_all(tasks); +/** + * @param {() => void} fn + */ +export function queue_task(fn) { + if (!is_task_queued) { + is_task_queued = true; + queueMicrotask(process_task); + } + current_queued_tasks.push(fn); } /** @@ -29,7 +30,4 @@ export function flush_tasks() { if (is_task_queued) { process_task(); } - if (is_raf_queued) { - process_raf_task(); - } } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index f7cdbc13f9..8e7bd12b04 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -7,9 +7,11 @@ import { destroy_effect_children, execute_effect, get, + is_destroying_effect, is_flushing_effect, remove_reactions, schedule_effect, + set_is_destroying_effect, set_is_flushing_effect, set_signal_status, untrack @@ -109,6 +111,12 @@ export function user_effect(fn) { (DEV ? ': The Svelte $effect rune can only be used during component initialisation.' : '') ); } + if (is_destroying_effect) { + throw new Error( + 'ERR_SVELTE_EFFECT_IN_TEARDOWN' + + (DEV ? ': The Svelte $effect rune can not be used in the teardown phase of an effect.' : '') + ); + } // Non-nested `$effect(...)` in a component should be deferred // until the component is mounted @@ -140,6 +148,14 @@ export function user_pre_effect(fn) { : '') ); } + if (is_destroying_effect) { + throw new Error( + 'ERR_SVELTE_EFFECT_IN_TEARDOWN' + + (DEV + ? ': The Svelte $effect.pre rune can not be used in the teardown phase of an effect.' + : '') + ); + } return render_effect(fn); } @@ -228,6 +244,22 @@ export function branch(fn) { return create_effect(RENDER_EFFECT | BRANCH_EFFECT, fn, true); } +/** + * @param {import("#client").Effect} effect + */ +export function execute_effect_teardown(effect) { + var teardown = effect.teardown; + if (teardown !== null) { + const previously_destroying_effect = is_destroying_effect; + set_is_destroying_effect(true); + try { + teardown.call(null); + } finally { + set_is_destroying_effect(previously_destroying_effect); + } + } +} + /** * @param {import('#client').Effect} effect * @returns {void} @@ -249,7 +281,7 @@ export function destroy_effect(effect) { } } - effect.teardown?.call(null); + execute_effect_teardown(effect); var parent = effect.parent; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index e8bcc713d4..fe93eb47e4 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -8,7 +8,12 @@ import { object_prototype } from './utils.js'; import { snapshot } from './proxy.js'; -import { destroy_effect, effect, user_pre_effect } from './reactivity/effects.js'; +import { + destroy_effect, + effect, + execute_effect_teardown, + user_pre_effect +} from './reactivity/effects.js'; import { EFFECT, RENDER_EFFECT, @@ -37,12 +42,18 @@ let current_scheduler_mode = FLUSH_MICROTASK; // Used for handling scheduling let is_micro_task_queued = false; export let is_flushing_effect = false; +export let is_destroying_effect = false; /** @param {boolean} value */ export function set_is_flushing_effect(value) { is_flushing_effect = value; } +/** @param {boolean} value */ +export function set_is_destroying_effect(value) { + is_destroying_effect = value; +} + // Used for $inspect export let is_batching_effect = false; let is_inspecting_signal = false; @@ -406,7 +417,7 @@ export function execute_effect(effect) { destroy_effect_children(effect); } - effect.teardown?.call(null); + execute_effect_teardown(effect); var teardown = execute_reaction_fn(effect); effect.teardown = typeof teardown === 'function' ? teardown : null; } finally { @@ -658,11 +669,11 @@ export function flush_sync(fn, flush_previous = true) { var result = fn?.(); + flush_tasks(); if (current_queued_root_effects.length > 0 || root_effects.length > 0) { flush_sync(); } - flush_tasks(); flush_count = 0; return result; From 777527b5a3358fe0a9e05fa68fef7363150d57e8 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 17 Apr 2024 10:02:18 +0100 Subject: [PATCH 02/13] fix: remove memory leak from retaining old DOM elements (#11197) * fix: remove memory leak from retaining old DOM elements * missing logic * fix dynamic html bug --- .changeset/rich-plums-thank.md | 5 ++ .../src/internal/client/dom/blocks/html.js | 44 +++++++++- .../client/dom/blocks/svelte-element.js | 3 + .../src/internal/client/dom/template.js | 88 +++++++++++++++---- .../samples/each-dynamic-html/_config.js | 39 ++++++++ .../samples/each-dynamic-html/main.svelte | 30 +++++++ 6 files changed, 186 insertions(+), 23 deletions(-) create mode 100644 .changeset/rich-plums-thank.md create mode 100644 packages/svelte/tests/runtime-runes/samples/each-dynamic-html/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/each-dynamic-html/main.svelte diff --git a/.changeset/rich-plums-thank.md b/.changeset/rich-plums-thank.md new file mode 100644 index 0000000000..0c96191f92 --- /dev/null +++ b/.changeset/rich-plums-thank.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: remove memory leak from retaining old DOM elements diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index ff96617885..40583e09f2 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -1,8 +1,30 @@ import { derived } from '../../reactivity/deriveds.js'; import { render_effect } from '../../reactivity/effects.js'; -import { get } from '../../runtime.js'; +import { current_effect, get } from '../../runtime.js'; +import { is_array } from '../../utils.js'; import { hydrate_nodes, hydrating } from '../hydration.js'; import { create_fragment_from_html, remove } from '../reconciler.js'; +import { push_template_node } from '../template.js'; + +/** + * @param {import('#client').Effect} effect + * @param {(Element | Comment | Text)[]} to_remove + * @returns {void} + */ +function remove_from_parent_effect(effect, to_remove) { + const dom = effect.dom; + + if (is_array(dom)) { + for (let i = dom.length - 1; i >= 0; i--) { + if (to_remove.includes(dom[i])) { + dom.splice(i, 1); + break; + } + } + } else if (dom !== null && to_remove.includes(dom)) { + effect.dom = null; + } +} /** * @param {Element | Text | Comment} anchor @@ -11,13 +33,19 @@ import { create_fragment_from_html, remove } from '../reconciler.js'; * @returns {void} */ export function html(anchor, get_value, svg) { + const parent_effect = anchor.parentNode !== current_effect?.dom ? current_effect : null; let value = derived(get_value); render_effect(() => { - var dom = html_to_dom(anchor, get(value), svg); + var dom = html_to_dom(anchor, parent_effect, get(value), svg); if (dom) { - return () => remove(dom); + return () => { + if (parent_effect !== null) { + remove_from_parent_effect(parent_effect, is_array(dom) ? dom : [dom]); + } + remove(dom); + }; } }); } @@ -27,11 +55,12 @@ export function html(anchor, get_value, svg) { * inserts it before the target anchor and returns the new nodes. * @template V * @param {Element | Text | Comment} target + * @param {import('#client').Effect | null} effect * @param {V} value * @param {boolean} svg * @returns {Element | Comment | (Element | Comment | Text)[]} */ -function html_to_dom(target, value, svg) { +function html_to_dom(target, effect, value, svg) { if (hydrating) return hydrate_nodes; var html = value + ''; @@ -49,6 +78,9 @@ function html_to_dom(target, value, svg) { if (node.childNodes.length === 1) { var child = /** @type {Text | Element | Comment} */ (node.firstChild); target.before(child); + if (effect !== null) { + push_template_node(effect, child); + } return child; } @@ -62,5 +94,9 @@ function html_to_dom(target, value, svg) { target.before(node); } + if (effect !== null) { + push_template_node(effect, nodes); + } + return nodes; } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js index a0c3cc6d65..c7daff227b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -13,6 +13,7 @@ import { is_array } from '../../utils.js'; import { set_should_intro } from '../../render.js'; import { current_each_item, set_current_each_item } from './each.js'; import { current_effect } from '../../runtime.js'; +import { push_template_node } from '../template.js'; /** * @param {import('#client').Effect} effect @@ -131,6 +132,8 @@ export function element(anchor, get_tag, is_svg, render_fn) { if (prev_element) { swap_block_dom(parent_effect, prev_element, element); prev_element.remove(); + } else if (!hydrating) { + push_template_node(parent_effect, element); } }); } diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 8e1f51c8eb..5baeb2f313 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -4,6 +4,36 @@ import { create_fragment_from_html } from './reconciler.js'; import { current_effect } from '../runtime.js'; import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js'; import { effect } from '../reactivity/effects.js'; +import { is_array } from '../utils.js'; + +/** + * @param {import("#client").Effect} effect + * @param {import("#client").TemplateNode | import("#client").TemplateNode[]} dom + */ +export function push_template_node(effect, dom) { + var current_dom = effect.dom; + if (current_dom === null) { + effect.dom = dom; + } else { + if (!is_array(current_dom)) { + current_dom = effect.dom = [current_dom]; + } + var anchor; + // If we're working with an anchor, then remove it and put it at the end. + if (current_dom[0].nodeType === 8) { + anchor = current_dom.pop(); + } + if (is_array(dom)) { + current_dom.push(...dom); + } else { + current_dom.push(dom); + } + if (anchor !== undefined) { + current_dom.push(anchor); + } + } + return dom; +} /** * @param {string} content @@ -19,16 +49,31 @@ export function template(content, flags) { var node; return () => { + var effect = /** @type {import('#client').Effect} */ (current_effect); if (hydrating) { - return is_fragment ? hydrate_nodes : /** @type {Node} */ (hydrate_nodes[0]); + var hydration_content = push_template_node( + effect, + is_fragment ? hydrate_nodes : hydrate_nodes[0] + ); + return /** @type {Node} */ (hydration_content); } if (!node) { node = create_fragment_from_html(content); if (!is_fragment) node = /** @type {Node} */ (node.firstChild); } + var clone = use_import_node ? document.importNode(node, true) : clone_node(node, true); + + if (is_fragment) { + push_template_node( + effect, + /** @type {import('#client').TemplateNode[]} */ ([...clone.childNodes]) + ); + } else { + push_template_node(effect, /** @type {import('#client').TemplateNode} */ (clone)); + } - return use_import_node ? document.importNode(node, true) : clone_node(node, true); + return clone; }; } @@ -70,8 +115,13 @@ export function svg_template(content, flags) { var node; return () => { + var effect = /** @type {import('#client').Effect} */ (current_effect); if (hydrating) { - return is_fragment ? hydrate_nodes : /** @type {Node} */ (hydrate_nodes[0]); + var hydration_content = push_template_node( + effect, + is_fragment ? hydrate_nodes : hydrate_nodes[0] + ); + return /** @type {Node} */ (hydration_content); } if (!node) { @@ -87,7 +137,18 @@ export function svg_template(content, flags) { } } - return clone_node(node, true); + var clone = clone_node(node, true); + + if (is_fragment) { + push_template_node( + effect, + /** @type {import('#client').TemplateNode[]} */ ([...clone.childNodes]) + ); + } else { + push_template_node(effect, /** @type {import('#client').TemplateNode} */ (clone)); + } + + return clone; }; } @@ -152,7 +213,8 @@ function run_scripts(node) { */ /*#__NO_SIDE_EFFECTS__*/ export function text(anchor) { - if (!hydrating) return empty(); + var effect = /** @type {import('#client').Effect} */ (current_effect); + if (!hydrating) return push_template_node(effect, empty()); var node = hydrate_nodes[0]; @@ -162,7 +224,7 @@ export function text(anchor) { anchor.before((node = empty())); } - return node; + return push_template_node(effect, node); } export const comment = template('', TEMPLATE_FRAGMENT); @@ -174,19 +236,7 @@ export const comment = template('', TEMPLATE_FRAGMENT); * @param {import('#client').Dom} dom */ export function append(anchor, dom) { - var current = dom; - if (!hydrating) { - var node = /** @type {Node} */ (dom); - - if (node.nodeType === 11) { - // if hydrating, `dom` is already an array of nodes, but if not then - // we need to create an array to store it on the current effect - current = /** @type {import('#client').Dom} */ ([...node.childNodes]); - } - - anchor.before(node); + anchor.before(/** @type {Node} */ (dom)); } - - /** @type {import('#client').Effect} */ (current_effect).dom = current; } diff --git a/packages/svelte/tests/runtime-runes/samples/each-dynamic-html/_config.js b/packages/svelte/tests/runtime-runes/samples/each-dynamic-html/_config.js new file mode 100644 index 0000000000..93f3702491 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-dynamic-html/_config.js @@ -0,0 +1,39 @@ +import { flushSync } from '../../../../src/index-client'; +import { test } from '../../test'; + +export default test({ + html: ``, + + async test({ assert, target }) { + const [btn1, btn2, btn3] = target.querySelectorAll('button'); + + flushSync(() => { + btn1?.click(); + btn1?.click(); + btn1?.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `
Item 1
Item 2
Item 3
` + ); + + flushSync(() => { + btn2?.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `Item 1Item 2Item 3` + ); + + flushSync(() => { + btn3?.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `Item 3Item 2Item 1` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-dynamic-html/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-dynamic-html/main.svelte new file mode 100644 index 0000000000..350cbf0d4a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-dynamic-html/main.svelte @@ -0,0 +1,30 @@ + + + + + + +{#each items as item (item.id)} + {@html item.html} +{/each} From 1510c1311341dc35beb57bf3299218cc2bd5cb73 Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Wed, 17 Apr 2024 17:07:53 +0800 Subject: [PATCH 03/13] feat: add warning when using $bindable runes without calling it (#11181) * feat: add warning when using `$bindable` rune without calling it * --amend --- .changeset/shiny-rats-heal.md | 5 +++++ .../src/compiler/phases/2-analyze/validation.js | 9 +++++++++ packages/svelte/src/compiler/warnings.js | 4 +++- .../samples/runes-bindable-not-called/_config.js | 3 +++ .../samples/runes-bindable-not-called/input.svelte | 3 +++ .../runes-bindable-not-called/warnings.json | 14 ++++++++++++++ 6 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 .changeset/shiny-rats-heal.md create mode 100644 packages/svelte/tests/validator/samples/runes-bindable-not-called/_config.js create mode 100644 packages/svelte/tests/validator/samples/runes-bindable-not-called/input.svelte create mode 100644 packages/svelte/tests/validator/samples/runes-bindable-not-called/warnings.json diff --git a/.changeset/shiny-rats-heal.md b/.changeset/shiny-rats-heal.md new file mode 100644 index 0000000000..8c87fb572f --- /dev/null +++ b/.changeset/shiny-rats-heal.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +feat: add warning when using `$bindable` rune without calling it diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 7d77003c9d..60e0ef46e9 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -1174,6 +1174,15 @@ export const validation_runes = merge(validation, a11y_validators, { } } }, + AssignmentPattern(node, { state, path }) { + if ( + node.right.type === 'Identifier' && + node.right.name === '$bindable' && + !state.scope.get('bindable') + ) { + warn(state.analysis.warnings, node, path, 'invalid-bindable-declaration'); + } + }, // TODO this is a code smell. need to refactor this stuff ClassBody: validation_runes_js.ClassBody, ClassDeclaration: validation_runes_js.ClassDeclaration, diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js index 8f0e81d9c0..3e36df1677 100644 --- a/packages/svelte/src/compiler/warnings.js +++ b/packages/svelte/src/compiler/warnings.js @@ -39,7 +39,9 @@ const runes = { 'derived-iife': () => `Use \`$derived.by(() => {...})\` instead of \`$derived((() => {...})());\``, 'invalid-props-declaration': () => - `Component properties are declared using $props() in runes mode. Did you forget to call the function?` + `Component properties are declared using $props() in runes mode. Did you forget to call the function?`, + 'invalid-bindable-declaration': () => + `Bindable component properties are declared using $bindable() in runes mode. Did you forget to call the function?` }; /** @satisfies {Warnings} */ diff --git a/packages/svelte/tests/validator/samples/runes-bindable-not-called/_config.js b/packages/svelte/tests/validator/samples/runes-bindable-not-called/_config.js new file mode 100644 index 0000000000..f47bee71df --- /dev/null +++ b/packages/svelte/tests/validator/samples/runes-bindable-not-called/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/validator/samples/runes-bindable-not-called/input.svelte b/packages/svelte/tests/validator/samples/runes-bindable-not-called/input.svelte new file mode 100644 index 0000000000..7bf756a603 --- /dev/null +++ b/packages/svelte/tests/validator/samples/runes-bindable-not-called/input.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/validator/samples/runes-bindable-not-called/warnings.json b/packages/svelte/tests/validator/samples/runes-bindable-not-called/warnings.json new file mode 100644 index 0000000000..ebcd065f31 --- /dev/null +++ b/packages/svelte/tests/validator/samples/runes-bindable-not-called/warnings.json @@ -0,0 +1,14 @@ +[ + { + "code": "invalid-bindable-declaration", + "message": "Bindable component properties are declared using $bindable() in runes mode. Did you forget to call the function?", + "start": { + "column": 7, + "line": 2 + }, + "end": { + "column": 20, + "line": 2 + } + } +] From e7301af1e51cb20f64c7aaebbf3d0e16b39bba0d Mon Sep 17 00:00:00 2001 From: Azarattum <43073346+Azarattum@users.noreply.github.com> Date: Wed, 17 Apr 2024 16:08:20 +0700 Subject: [PATCH 04/13] fix: no data duplication in reactive Set/Map (#11200) * fix: get rid of data duplication in reactive map * fix: get rid of data duplication in reactive set --- packages/svelte/src/reactivity/map.js | 12 ++++---- packages/svelte/src/reactivity/map.test.ts | 34 ++++++++++++++++++++++ packages/svelte/src/reactivity/set.js | 9 +++--- packages/svelte/src/reactivity/set.test.ts | 9 ++++++ 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js index fb1e4e848b..b2b3ab605b 100644 --- a/packages/svelte/src/reactivity/map.js +++ b/packages/svelte/src/reactivity/map.js @@ -28,7 +28,6 @@ export class ReactiveMap extends Map { for (var [key, v] of value) { sources.set(key, source(v)); - super.set(key, v); } this.#size.v = sources.size; @@ -62,7 +61,8 @@ export class ReactiveMap extends Map { forEach(callbackfn, this_arg) { get(this.#version); - return super.forEach(callbackfn, this_arg); + var bound_callbackfn = callbackfn.bind(this_arg); + this.#sources.forEach((s, key) => bound_callbackfn(s.v, key, this)); } /** @param {K} key */ @@ -96,7 +96,7 @@ export class ReactiveMap extends Map { set(s, value); } - return super.set(key, value); + return this; } /** @param {K} key */ @@ -105,13 +105,14 @@ export class ReactiveMap extends Map { var s = sources.get(key); if (s !== undefined) { - sources.delete(key); + var removed = sources.delete(key); set(this.#size, sources.size); set(s, /** @type {V} */ (UNINITIALIZED)); this.#increment_version(); + return removed; } - return super.delete(key); + return false; } clear() { @@ -126,7 +127,6 @@ export class ReactiveMap extends Map { } sources.clear(); - super.clear(); } keys() { diff --git a/packages/svelte/src/reactivity/map.test.ts b/packages/svelte/src/reactivity/map.test.ts index 9d01d88ff5..7777392cf9 100644 --- a/packages/svelte/src/reactivity/map.test.ts +++ b/packages/svelte/src/reactivity/map.test.ts @@ -124,6 +124,40 @@ test('map.has(...)', () => { cleanup(); }); +test('map.forEach(...)', () => { + const map = new ReactiveMap([ + [1, 1], + [2, 2], + [3, 3] + ]); + + const log: any = []; + const this_arg = {}; + + map.forEach(function (this: unknown, ...args) { + log.push([...args, this]); + }, this_arg); + + assert.deepEqual(log, [ + [1, 1, map, this_arg], + [2, 2, map, this_arg], + [3, 3, map, this_arg] + ]); +}); + +test('map.delete(...)', () => { + const map = new ReactiveMap([ + [1, 1], + [2, 2], + [3, 3] + ]); + + assert.equal(map.delete(3), true); + assert.equal(map.delete(3), false); + + assert.deepEqual(Array.from(map.values()), [1, 2]); +}); + test('map handling of undefined values', () => { const map = new ReactiveMap(); diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index 8a889c32d0..1855090cdf 100644 --- a/packages/svelte/src/reactivity/set.js +++ b/packages/svelte/src/reactivity/set.js @@ -31,7 +31,6 @@ export class ReactiveSet extends Set { for (var element of value) { sources.set(element, source(true)); - super.add(element); } this.#size.v = sources.size; @@ -97,7 +96,7 @@ export class ReactiveSet extends Set { this.#increment_version(); } - return super.add(value); + return this; } /** @param {T} value */ @@ -106,13 +105,14 @@ export class ReactiveSet extends Set { var s = sources.get(value); if (s !== undefined) { - sources.delete(value); + var removed = sources.delete(value); set(this.#size, sources.size); set(s, false); this.#increment_version(); + return removed; } - return super.delete(value); + return false; } clear() { @@ -127,7 +127,6 @@ export class ReactiveSet extends Set { } sources.clear(); - super.clear(); } keys() { diff --git a/packages/svelte/src/reactivity/set.test.ts b/packages/svelte/src/reactivity/set.test.ts index a66e44afe5..a855ede6b3 100644 --- a/packages/svelte/src/reactivity/set.test.ts +++ b/packages/svelte/src/reactivity/set.test.ts @@ -77,3 +77,12 @@ test('set.has(...)', () => { cleanup(); }); + +test('set.delete(...)', () => { + const set = new ReactiveSet([1, 2, 3]); + + assert.equal(set.delete(3), true); + assert.equal(set.delete(3), false); + + assert.deepEqual(Array.from(set.values()), [1, 2]); +}); From 3d49731b1b92bfc5b059353f56a0cc0fc0abd62c Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Wed, 17 Apr 2024 17:09:53 +0800 Subject: [PATCH 05/13] fix: refine css `:global()` selector checks in a compound selector (#11142) --- .changeset/dry-fans-march.md | 5 +++ packages/svelte/src/compiler/errors.js | 2 + .../phases/2-analyze/css/css-analyze.js | 44 +++++++------------ .../compiler/phases/3-transform/css/index.js | 2 +- .../errors.json | 14 ++++++ .../input.svelte | 27 ++++++++++++ .../errors.json | 14 ++++++ .../input.svelte | 24 ++++++++++ 8 files changed, 104 insertions(+), 28 deletions(-) create mode 100644 .changeset/dry-fans-march.md create mode 100644 packages/svelte/tests/validator/samples/css-invalid-global-selector-list/errors.json create mode 100644 packages/svelte/tests/validator/samples/css-invalid-global-selector-list/input.svelte create mode 100644 packages/svelte/tests/validator/samples/css-invalid-type-selector-placement/errors.json create mode 100644 packages/svelte/tests/validator/samples/css-invalid-type-selector-placement/input.svelte diff --git a/.changeset/dry-fans-march.md b/.changeset/dry-fans-march.md new file mode 100644 index 0000000000..6d16d55d12 --- /dev/null +++ b/.changeset/dry-fans-march.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: refine css `:global()` selector checks in a compound selector diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 8281ab50d8..e342dc21b7 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -108,6 +108,8 @@ const css = { 'invalid-css-global-selector': () => `:global(...) must contain exactly one selector`, 'invalid-css-global-selector-list': () => `:global(...) must not contain type or universal selectors when used in a compound selector`, + 'invalid-css-type-selector-placement': () => + `:global(...) must not be followed with a type selector`, 'invalid-css-selector': () => `Invalid selector`, 'invalid-css-identifier': () => 'Expected a valid CSS identifier', 'invalid-nesting-selector': () => `Nesting selectors can only be used inside a rule`, 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 ed2aff2769..7b6cbff0ae 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 @@ -99,41 +99,31 @@ const validation_visitors = { } } - // ensure `:global(...)`contains a single selector - // (standalone :global() with multiple selectors is OK) - if (node.children.length > 1 || node.children[0].selectors.length > 1) { - for (const relative_selector of node.children) { - for (const selector of relative_selector.selectors) { - if ( - selector.type === 'PseudoClassSelector' && - selector.name === 'global' && - selector.args !== null && - selector.args.children.length > 1 - ) { - error(selector, 'invalid-css-global-selector'); - } - } - } - } - - // ensure `:global(...)` is not part of a larger compound selector + // ensure `:global(...)` do not lead to invalid css after `:global()` is removed for (const relative_selector of node.children) { for (let i = 0; i < relative_selector.selectors.length; i++) { const selector = relative_selector.selectors[i]; if (selector.type === 'PseudoClassSelector' && selector.name === 'global') { const child = selector.args?.children[0].children[0]; + // ensure `:global(element)` to be at the first position in a compound selector + if (child?.selectors[0].type === 'TypeSelector' && i !== 0) { + error(selector, 'invalid-css-global-selector-list'); + } + + // ensure `:global(.class)` is not followed by a type selector, eg: `:global(.class)element` + if (relative_selector.selectors[i + 1]?.type === 'TypeSelector') { + error(relative_selector.selectors[i + 1], 'invalid-css-type-selector-placement'); + } + + // ensure `:global(...)`contains a single selector + // (standalone :global() with multiple selectors is OK) if ( - child?.selectors[0].type === 'TypeSelector' && - !/[.:#]/.test(child.selectors[0].name[0]) && - (i !== 0 || - relative_selector.selectors - .slice(1) - .some( - (s) => s.type !== 'PseudoElementSelector' && s.type !== 'PseudoClassSelector' - )) + selector.args !== null && + selector.args.children.length > 1 && + (node.children.length > 1 || relative_selector.selectors.length > 1) ) { - error(selector, 'invalid-css-global-selector-list'); + error(selector, 'invalid-css-global-selector'); } } } 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 102cf8d7cc..a71d77b2aa 100644 --- a/packages/svelte/src/compiler/phases/3-transform/css/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/css/index.js @@ -230,7 +230,7 @@ const visitors = { context.state.specificity.bumped = true; - // TODO err... can this happen? + // for any :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); diff --git a/packages/svelte/tests/validator/samples/css-invalid-global-selector-list/errors.json b/packages/svelte/tests/validator/samples/css-invalid-global-selector-list/errors.json new file mode 100644 index 0000000000..25399f6369 --- /dev/null +++ b/packages/svelte/tests/validator/samples/css-invalid-global-selector-list/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "invalid-css-global-selector-list", + "message": ":global(...) must not contain type or universal selectors when used in a compound selector", + "start": { + "line": 20, + "column": 6 + }, + "end": { + "line": 20, + "column": 17 + } + } +] diff --git a/packages/svelte/tests/validator/samples/css-invalid-global-selector-list/input.svelte b/packages/svelte/tests/validator/samples/css-invalid-global-selector-list/input.svelte new file mode 100644 index 0000000000..84cbe45f1d --- /dev/null +++ b/packages/svelte/tests/validator/samples/css-invalid-global-selector-list/input.svelte @@ -0,0 +1,27 @@ + + +
+

hello world

+
\ No newline at end of file diff --git a/packages/svelte/tests/validator/samples/css-invalid-type-selector-placement/errors.json b/packages/svelte/tests/validator/samples/css-invalid-type-selector-placement/errors.json new file mode 100644 index 0000000000..f58f92d6ee --- /dev/null +++ b/packages/svelte/tests/validator/samples/css-invalid-type-selector-placement/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "invalid-css-type-selector-placement", + "message": ":global(...) must not be followed with a type selector", + "start": { + "line": 17, + "column": 14 + }, + "end": { + "line": 17, + "column": 16 + } + } +] diff --git a/packages/svelte/tests/validator/samples/css-invalid-type-selector-placement/input.svelte b/packages/svelte/tests/validator/samples/css-invalid-type-selector-placement/input.svelte new file mode 100644 index 0000000000..01f0c00f12 --- /dev/null +++ b/packages/svelte/tests/validator/samples/css-invalid-type-selector-placement/input.svelte @@ -0,0 +1,24 @@ + + +
+

hello world

+
From 4ef64541dd09eb065eca44df6ef3dd3c3aedfbfd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 17 Apr 2024 10:19:14 +0100 Subject: [PATCH 06/13] Version Packages (next) (#11201) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 4 ++++ packages/svelte/CHANGELOG.md | 12 ++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 8a25b9de9c..178102977c 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -76,6 +76,7 @@ "dry-clocks-grow", "dry-eggs-play", "dry-eggs-retire", + "dry-fans-march", "dry-pillows-exist", "dull-coins-vanish", "dull-mangos-wave", @@ -162,6 +163,7 @@ "hungry-trees-travel", "itchy-beans-melt", "itchy-bulldogs-tan", + "itchy-eels-marry", "itchy-kings-deliver", "itchy-lions-wash", "itchy-terms-guess", @@ -279,6 +281,7 @@ "rich-cobras-exist", "rich-garlics-laugh", "rich-olives-yell", + "rich-plums-thank", "rich-sheep-burn", "rich-tables-sing", "rich-waves-mix", @@ -308,6 +311,7 @@ "sharp-kids-happen", "sharp-tomatoes-learn", "shiny-baboons-play", + "shiny-rats-heal", "shiny-shrimps-march", "short-buses-camp", "short-countries-rush", diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 2e22b024d4..f3106dfa22 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.0.0-next.107 + +### Patch Changes + +- fix: refine css `:global()` selector checks in a compound selector ([#11142](https://github.com/sveltejs/svelte/pull/11142)) + +- fix: remove memory leak from bind:this ([#11194](https://github.com/sveltejs/svelte/pull/11194)) + +- fix: remove memory leak from retaining old DOM elements ([#11197](https://github.com/sveltejs/svelte/pull/11197)) + +- feat: add warning when using `$bindable` rune without calling it ([#11181](https://github.com/sveltejs/svelte/pull/11181)) + ## 5.0.0-next.106 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 82f502b32b..c02a89d8c3 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.0.0-next.106", + "version": "5.0.0-next.107", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index e2cea9e696..0e037fd009 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.0.0-next.106'; +export const VERSION = '5.0.0-next.107'; export const PUBLIC_VERSION = '5'; From d51075c154beed08ec706172bfab51cbaef5ec2e Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 17 Apr 2024 14:46:44 +0200 Subject: [PATCH 07/13] breaking: warn/error on old syntax in runes mode (#11203) * breaking: warn/error on old syntax in runes mode - warn on slots and event handlers in runes mode - error on `` + `{@render ...}` tag usage in same component closes #9416 * render tag + slot could occur in legacy mode as well, error there, too --- .changeset/four-pugs-listen.md | 5 +++ packages/svelte/src/compiler/errors.js | 4 +- .../src/compiler/phases/2-analyze/index.js | 9 ++++- .../compiler/phases/2-analyze/validation.js | 14 +++++++ .../svelte/src/compiler/phases/types.d.ts | 4 +- packages/svelte/src/compiler/warnings.js | 7 +++- .../_config.js | 10 +++++ .../main.svelte | 6 +++ .../runes-legacy-syntax-warnings/input.svelte | 13 +++++++ .../warnings.json | 38 +++++++++++++++++++ .../static-state-reference/input.svelte | 2 +- 11 files changed, 106 insertions(+), 6 deletions(-) create mode 100644 .changeset/four-pugs-listen.md create mode 100644 packages/svelte/tests/compiler-errors/samples/slot-conflicting-with-render-tag/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/slot-conflicting-with-render-tag/main.svelte create mode 100644 packages/svelte/tests/validator/samples/runes-legacy-syntax-warnings/input.svelte create mode 100644 packages/svelte/tests/validator/samples/runes-legacy-syntax-warnings/warnings.json diff --git a/.changeset/four-pugs-listen.md b/.changeset/four-pugs-listen.md new file mode 100644 index 0000000000..67e8057932 --- /dev/null +++ b/.changeset/four-pugs-listen.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +breaking: warn on slots and event handlers in runes mode, error on `` + `{@render ...}` tag usage in same component diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index e342dc21b7..ab0a17497e 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -163,7 +163,9 @@ const special_elements = { * @param {string | null} match */ 'invalid-svelte-tag': (tags, match) => - `Valid tag names are ${list(tags)}${match ? ' (did you mean ' + match + '?)' : ''}` + `Valid tag names are ${list(tags)}${match ? ' (did you mean ' + match + '?)' : ''}`, + 'conflicting-slot-usage': () => + `Cannot use syntax and {@render ...} tags in the same component. Migrate towards {@render ...} tags completely.` }; /** @satisfies {Errors} */ diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index c1ed378ca3..457e4d1afa 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -379,6 +379,7 @@ export function analyze_component(root, source, options) { uses_rest_props: false, uses_slots: false, uses_component_bindings: false, + uses_render_tags: false, custom_element: options.customElementOptions ?? options.customElement, inject_styles: options.css === 'injected' || options.customElement, accessors: options.customElement @@ -388,7 +389,7 @@ export function analyze_component(root, source, options) { !!options.legacy?.componentApi, reactive_statements: new Map(), binding_groups: new Map(), - slot_names: new Set(), + slot_names: new Map(), warnings, css: { ast: root.css, @@ -502,6 +503,10 @@ export function analyze_component(root, source, options) { analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements); } + if (analysis.uses_render_tags && (analysis.uses_slots || analysis.slot_names.size > 0)) { + error(analysis.slot_names.values().next().value, 'conflicting-slot-usage'); + } + // warn on any nonstate declarations that are a) reassigned and b) referenced in the template for (const scope of [module.scope, instance.scope]) { outer: for (const [name, binding] of scope.declarations) { @@ -1087,7 +1092,7 @@ const common_visitors = { break; } } - context.state.analysis.slot_names.add(name); + context.state.analysis.slot_names.set(name, node); }, StyleDirective(node, context) { if (node.value === true) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 60e0ef46e9..d86b1cb7fd 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -578,6 +578,8 @@ const validation = { }); }, RenderTag(node, context) { + context.state.analysis.uses_render_tags = true; + const raw_args = unwrap_optional(node.expression).arguments; for (const arg of raw_args) { if (arg.type === 'SpreadElement') { @@ -1183,6 +1185,18 @@ export const validation_runes = merge(validation, a11y_validators, { warn(state.analysis.warnings, node, path, 'invalid-bindable-declaration'); } }, + SlotElement(node, { state, path }) { + if (!state.analysis.custom_element) { + warn(state.analysis.warnings, node, path, 'deprecated-slot-element'); + } + }, + OnDirective(node, { state, path }) { + const parent_type = path.at(-1)?.type; + // Don't warn on component events; these might not be under the author's control so the warning would be unactionable + if (parent_type === 'RegularElement' || parent_type === 'SvelteElement') { + warn(state.analysis.warnings, node, path, 'deprecated-event-handler', node.name); + } + }, // TODO this is a code smell. need to refactor this stuff ClassBody: validation_runes_js.ClassBody, ClassDeclaration: validation_runes_js.ClassDeclaration, diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index 390ea49a46..91fd68ca39 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -3,6 +3,7 @@ import type { Css, Fragment, RegularElement, + SlotElement, SvelteElement, SvelteNode, SvelteOptions @@ -61,13 +62,14 @@ export interface ComponentAnalysis extends Analysis { /** Whether the component uses `$$slots` */ uses_slots: boolean; uses_component_bindings: boolean; + uses_render_tags: boolean; custom_element: boolean | SvelteOptions['customElement']; /** If `true`, should append styles through JavaScript */ inject_styles: boolean; reactive_statements: Map; /** Identifiers that make up the `bind:group` expression -> internal group binding name */ binding_groups: Map<[key: string, bindings: Array], Identifier>; - slot_names: Set; + slot_names: Map; css: { ast: Css.StyleSheet | null; hash: string; diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js index 3e36df1677..26bcf1ecdd 100644 --- a/packages/svelte/src/compiler/warnings.js +++ b/packages/svelte/src/compiler/warnings.js @@ -233,7 +233,12 @@ const legacy = { 'All dependencies of the reactive declaration are declared in a module script and will not be reactive', /** @param {string} name */ 'unused-export-let': (name) => - `Component has unused export property '${name}'. If it is for external reference only, please consider using \`export const ${name}\`` + `Component has unused export property '${name}'. If it is for external reference only, please consider using \`export const ${name}\``, + 'deprecated-slot-element': () => + `Using to render parent content is deprecated. Use {@render ...} tags instead.`, + /** @param {string} name */ + 'deprecated-event-handler': (name) => + `Using on:${name} to listen to the ${name} event is is deprecated. Use the event attribute on${name} instead.` }; const block = { diff --git a/packages/svelte/tests/compiler-errors/samples/slot-conflicting-with-render-tag/_config.js b/packages/svelte/tests/compiler-errors/samples/slot-conflicting-with-render-tag/_config.js new file mode 100644 index 0000000000..c027d4fd5b --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/slot-conflicting-with-render-tag/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'conflicting-slot-usage', + message: + 'Cannot use syntax and {@render ...} tags in the same component. Migrate towards {@render ...} tags completely.', + position: [71, 84] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/slot-conflicting-with-render-tag/main.svelte b/packages/svelte/tests/compiler-errors/samples/slot-conflicting-with-render-tag/main.svelte new file mode 100644 index 0000000000..0e1e549ab1 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/slot-conflicting-with-render-tag/main.svelte @@ -0,0 +1,6 @@ + + +{@render children()} + diff --git a/packages/svelte/tests/validator/samples/runes-legacy-syntax-warnings/input.svelte b/packages/svelte/tests/validator/samples/runes-legacy-syntax-warnings/input.svelte new file mode 100644 index 0000000000..e1d0afed49 --- /dev/null +++ b/packages/svelte/tests/validator/samples/runes-legacy-syntax-warnings/input.svelte @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/packages/svelte/tests/validator/samples/runes-legacy-syntax-warnings/warnings.json b/packages/svelte/tests/validator/samples/runes-legacy-syntax-warnings/warnings.json new file mode 100644 index 0000000000..c841de3686 --- /dev/null +++ b/packages/svelte/tests/validator/samples/runes-legacy-syntax-warnings/warnings.json @@ -0,0 +1,38 @@ +[ + { + "code": "deprecated-slot-element", + "end": { + "column": 13, + "line": 11 + }, + "message": "Using to render parent content is deprecated. Use {@render ...} tags instead.", + "start": { + "column": 0, + "line": 11 + } + }, + { + "code": "deprecated-slot-element", + "end": { + "column": 24, + "line": 12 + }, + "message": "Using to render parent content is deprecated. Use {@render ...} tags instead.", + "start": { + "column": 0, + "line": 12 + } + }, + { + "code": "deprecated-event-handler", + "end": { + "column": 22, + "line": 13 + }, + "message": "Using on:click to listen to the click event is is deprecated. Use the event attribute onclick instead.", + "start": { + "column": 8, + "line": 13 + } + } +] diff --git a/packages/svelte/tests/validator/samples/static-state-reference/input.svelte b/packages/svelte/tests/validator/samples/static-state-reference/input.svelte index 8862926857..2971960a14 100644 --- a/packages/svelte/tests/validator/samples/static-state-reference/input.svelte +++ b/packages/svelte/tests/validator/samples/static-state-reference/input.svelte @@ -8,6 +8,6 @@ console.log(doubled); - From 27d48c601ffbc9778135765a41929d3a18352871 Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Thu, 18 Apr 2024 02:29:42 +0800 Subject: [PATCH 08/13] fix: catch clause scope (#11209) --- packages/svelte/src/compiler/phases/scope.js | 2 +- .../samples/js-scope-catch-clause/input.svelte | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 packages/svelte/tests/validator/samples/js-scope-catch-clause/input.svelte diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index d1561f741b..cfc2f0bb44 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -506,7 +506,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { scopes.set(node, scope); for (const id of extract_identifiers(node.param)) { - state.scope.declare(id, 'normal', 'let'); + scope.declare(id, 'normal', 'let'); } next({ scope }); diff --git a/packages/svelte/tests/validator/samples/js-scope-catch-clause/input.svelte b/packages/svelte/tests/validator/samples/js-scope-catch-clause/input.svelte new file mode 100644 index 0000000000..499b4bd4b5 --- /dev/null +++ b/packages/svelte/tests/validator/samples/js-scope-catch-clause/input.svelte @@ -0,0 +1,16 @@ + \ No newline at end of file From c44234dc2ff9bbd9fb4d3a6e1f1352a48ae6b9dc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 17 Apr 2024 16:54:32 -0400 Subject: [PATCH 09/13] fix: take outroing elements out of the flow when animating siblings (#11208) * fix: take outroing elements out of the flow when animating siblings * changeset --- .changeset/proud-pets-hang.md | 5 ++ .../src/internal/client/dom/blocks/each.js | 15 +++-- .../client/dom/elements/transitions.js | 56 +++++++++++++++++-- .../src/internal/client/reactivity/effects.js | 7 ++- .../svelte/src/internal/client/types.d.ts | 2 +- 5 files changed, 69 insertions(+), 16 deletions(-) create mode 100644 .changeset/proud-pets-hang.md diff --git a/.changeset/proud-pets-hang.md b/.changeset/proud-pets-hang.md new file mode 100644 index 0000000000..dd67868e7c --- /dev/null +++ b/.changeset/proud-pets-hang.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: take outroing elements out of the flow when animating siblings diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index db529a38b5..c1e0dc82f3 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -24,7 +24,7 @@ import { } from '../../reactivity/effects.js'; import { source, mutable_source, set } from '../../reactivity/sources.js'; import { is_array, is_frozen } from '../../utils.js'; -import { STATE_SYMBOL } from '../../constants.js'; +import { INERT, STATE_SYMBOL } from '../../constants.js'; /** * The row of a keyed each block that is currently updating. We track this @@ -70,7 +70,7 @@ function pause_effects(items, controlled_anchor, callback) { parent_node.append(controlled_anchor); } - run_out_transitions(transitions, () => { + run_out_transitions(transitions, true, () => { for (var i = 0; i < length; i++) { destroy_effect(items[i].e); } @@ -238,8 +238,8 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) { /** @type {import('#client').EachState | import('#client').EachItem} */ var prev = state; - /** @type {import('#client').EachItem[]} */ - var to_animate = []; + /** @type {Set} */ + var to_animate = new Set(); /** @type {import('#client').EachItem[]} */ var matched = []; @@ -267,7 +267,7 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) { if (item !== undefined) { item.a?.measure(); - to_animate.push(item); + to_animate.add(item); } } } @@ -302,7 +302,10 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) { update_item(item, value, i, flags); } - resume_effect(item.e); + if ((item.e.f & INERT) !== 0) { + resume_effect(item.e); + to_animate.delete(item); + } if (item !== current) { if (seen.has(item)) { diff --git a/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js index 7ee9e33b56..31d705b279 100644 --- a/packages/svelte/src/internal/client/dom/elements/transitions.js +++ b/packages/svelte/src/internal/client/dom/elements/transitions.js @@ -106,7 +106,7 @@ export function animation(element, get_fn, get_params) { ) { const options = get_fn()(this.element, { from, to }, get_params?.()); - animation = animate(this.element, options, undefined, 1, () => { + animation = animate(this.element, options, false, undefined, 1, () => { animation?.abort(); animation = undefined; }); @@ -169,7 +169,7 @@ export function transition(flags, element, get_fn, get_params) { if (is_intro) { dispatch_event(element, 'introstart'); - intro = animate(element, get_options(), outro, 1, () => { + intro = animate(element, get_options(), false, outro, 1, () => { dispatch_event(element, 'introend'); intro = current_options = undefined; }); @@ -178,12 +178,12 @@ export function transition(flags, element, get_fn, get_params) { reset?.(); } }, - out(fn) { + out(fn, position_absolute = false) { if (is_outro) { element.inert = true; dispatch_event(element, 'outrostart'); - outro = animate(element, get_options(), intro, 0, () => { + outro = animate(element, get_options(), position_absolute, intro, 0, () => { dispatch_event(element, 'outroend'); outro = current_options = undefined; fn?.(); @@ -229,12 +229,13 @@ export function transition(flags, element, get_fn, get_params) { * Animates an element, according to the provided configuration * @param {Element} element * @param {import('#client').AnimationConfig | ((opts: { direction: 'in' | 'out' }) => import('#client').AnimationConfig)} options + * @param {boolean} position_absolute * @param {import('#client').Animation | undefined} counterpart The corresponding intro/outro to this outro/intro * @param {number} t2 The target `t` value — `1` for intro, `0` for outro * @param {(() => void) | undefined} callback * @returns {import('#client').Animation} */ -function animate(element, options, counterpart, t2, callback) { +function animate(element, options, position_absolute, counterpart, t2, callback) { if (is_function(options)) { // In the case of a deferred transition (such as `crossfade`), `option` will be // a function rather than an `AnimationConfig`. We need to call this function @@ -244,7 +245,7 @@ function animate(element, options, counterpart, t2, callback) { effect(() => { var o = untrack(() => options({ direction: t2 === 1 ? 'in' : 'out' })); - a = animate(element, o, counterpart, t2, callback); + a = animate(element, o, position_absolute, counterpart, t2, callback); }); // ...but we want to do so without using `async`/`await` everywhere, so @@ -284,6 +285,9 @@ function animate(element, options, counterpart, t2, callback) { /** @type {import('#client').Task} */ var task; + /** @type {null | { position: string, width: string, height: string }} */ + var original_styles = null; + if (css) { // WAAPI var keyframes = []; @@ -295,6 +299,37 @@ function animate(element, options, counterpart, t2, callback) { keyframes.push(css_to_keyframe(styles)); } + if (position_absolute) { + // we take the element out of the flow, so that sibling elements with an `animate:` + // directive can transform to the correct position + var computed_style = getComputedStyle(element); + + if (computed_style.position !== 'absolute' && computed_style.position !== 'fixed') { + var style = /** @type {HTMLElement | SVGElement} */ (element).style; + + original_styles = { + position: style.position, + width: style.width, + height: style.height + }; + + var rect_a = element.getBoundingClientRect(); + style.position = 'absolute'; + style.width = computed_style.width; + style.height = computed_style.height; + var rect_b = element.getBoundingClientRect(); + + if (rect_a.left !== rect_b.left || rect_a.top !== rect_b.top) { + var transform = `translate(${rect_a.left - rect_b.left}px, ${rect_a.top - rect_b.top}px)`; + for (var keyframe of keyframes) { + keyframe.transform = keyframe.transform + ? `${keyframe.transform} ${transform}` + : transform; + } + } + } + } + animation = element.animate(keyframes, { delay, duration, @@ -345,6 +380,15 @@ function animate(element, options, counterpart, t2, callback) { task?.abort(); }, deactivate: () => { + if (original_styles) { + // revert `animate:` position fixing + var style = /** @type {HTMLElement | SVGElement} */ (element).style; + + style.position = original_styles.position; + style.width = original_styles.width; + style.height = original_styles.height; + } + callback = undefined; }, reset: () => { diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 8e7bd12b04..b11921536a 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -334,7 +334,7 @@ export function pause_effect(effect, callback) { pause_children(effect, transitions, true); - run_out_transitions(transitions, () => { + run_out_transitions(transitions, false, () => { destroy_effect(effect); if (callback) callback(); }); @@ -342,14 +342,15 @@ export function pause_effect(effect, callback) { /** * @param {import('#client').TransitionManager[]} transitions + * @param {boolean} position_absolute * @param {() => void} fn */ -export function run_out_transitions(transitions, fn) { +export function run_out_transitions(transitions, position_absolute, fn) { var remaining = transitions.length; if (remaining > 0) { var check = () => --remaining || fn(); for (var transition of transitions) { - transition.out(check); + transition.out(check, position_absolute); } } else { fn(); diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index cd900b0eb1..7b1c4333c8 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -77,7 +77,7 @@ export interface TransitionManager { /** Called inside `resume_effect` */ in: () => void; /** Called inside `pause_effect` */ - out: (callback?: () => void) => void; + out: (callback?: () => void, position_absolute?: boolean) => void; /** Called inside `destroy_effect` */ stop: () => void; } From 4b59ef3c41cb5fd6ed4036265db239ecdc4cf569 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 18 Apr 2024 10:00:39 +0200 Subject: [PATCH 10/13] fix: widen ownership when sub state is assigned to new state (#11217) Ownership was not widened when assigning a sub state to a different top level state. The set of owners for the state was zero because the owner was on the original parent, but that one was reset to null because it's now the top level of a different state. That meant that there was no owner but also no parent to check for the owner, which is an invalid combination resulting in a nullpointer (and also potentially false positive warnings in other situations). fixes #11204 --- .changeset/weak-frogs-bow.md | 5 +++ .../src/internal/client/dev/ownership.js | 4 +- packages/svelte/src/internal/client/proxy.js | 3 ++ .../Child.svelte | 10 +++++ .../_config.js | 41 +++++++++++++++++++ .../main.svelte | 13 ++++++ 6 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 .changeset/weak-frogs-bow.md create mode 100644 packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/main.svelte diff --git a/.changeset/weak-frogs-bow.md b/.changeset/weak-frogs-bow.md new file mode 100644 index 0000000000..f897249948 --- /dev/null +++ b/.changeset/weak-frogs-bow.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: widen ownership when sub state is assigned to new state diff --git a/packages/svelte/src/internal/client/dev/ownership.js b/packages/svelte/src/internal/client/dev/ownership.js index bef9fb8437..30dceab486 100644 --- a/packages/svelte/src/internal/client/dev/ownership.js +++ b/packages/svelte/src/internal/client/dev/ownership.js @@ -127,8 +127,8 @@ export function add_owner(object, owner, global = false) { } /** - * @param {import('#client').ProxyMetadata | null} from - * @param {import('#client').ProxyMetadata} to + * @param {import('#client').ProxyMetadata | null} from + * @param {import('#client').ProxyMetadata} to */ export function widen_ownership(from, to) { if (to.owners === null) { diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 17f8e1b2cd..136458c99d 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -38,6 +38,9 @@ export function proxy(value, immutable = true, parent = null) { // someone copied the state symbol using `Reflect.ownKeys(...)` if (metadata.t === value || metadata.p === value) { if (DEV) { + // Since original parent relationship gets lost, we need to copy over ancestor owners + // into current metadata. The object might still exist on both, so we need to widen it. + widen_ownership(metadata, metadata); metadata.parent = parent; } diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/Child.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/Child.svelte new file mode 100644 index 0000000000..220638c07a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/Child.svelte @@ -0,0 +1,10 @@ + + +
{item?.name}
+ diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/_config.js new file mode 100644 index 0000000000..df3ca08f03 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/_config.js @@ -0,0 +1,41 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +/** @type {typeof console.warn} */ +let warn; + +/** @type {any[]} */ +let warnings = []; + +export default test({ + compileOptions: { + dev: true + }, + + before_test: () => { + warn = console.warn; + + console.warn = (...args) => { + warnings.push(...args); + }; + }, + + after_test: () => { + console.warn = warn; + warnings = []; + }, + + async test({ assert, target }) { + const [btn1, btn2] = target.querySelectorAll('button'); + + btn1.click(); + await tick(); + + assert.deepEqual(warnings.length, 0); + + btn2.click(); + await tick(); + + assert.deepEqual(warnings.length, 1); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/main.svelte new file mode 100644 index 0000000000..8e8343790b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/main.svelte @@ -0,0 +1,13 @@ + + + + From 70b47de124e779f3daa4db5f43431051b5f936d0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 18 Apr 2024 05:06:03 -0400 Subject: [PATCH 11/13] chore: follow-up to #11197 (#11213) * simplify * make each item first nodes explicit * remove a couple of var declarations --- .../src/internal/client/dom/blocks/each.js | 36 ++++++----- .../src/internal/client/dom/blocks/html.js | 4 +- .../client/dom/blocks/svelte-element.js | 2 +- .../src/internal/client/dom/template.js | 59 +++++++------------ .../svelte/src/internal/client/types.d.ts | 2 + 5 files changed, 42 insertions(+), 61 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index c1e0dc82f3..663cee91db 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -25,6 +25,7 @@ import { import { source, mutable_source, set } from '../../reactivity/sources.js'; import { is_array, is_frozen } from '../../utils.js'; import { INERT, STATE_SYMBOL } from '../../constants.js'; +import { push_template_node } from '../template.js'; /** * The row of a keyed each block that is currently updating. We track this @@ -168,10 +169,11 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback break; } + var child_open = /** @type {Comment} */ (child_anchor); child_anchor = hydrate_anchor(child_anchor); var value = array[i]; var key = get_key(value, i); - item = create_item(child_anchor, prev, null, value, key, i, render_fn, flags); + item = create_item(child_open, child_anchor, prev, null, value, key, i, render_fn, flags); state.items.set(key, item); child_anchor = /** @type {Comment} */ (child_anchor.nextSibling); @@ -278,8 +280,14 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) { item = items.get(key); if (item === undefined) { + var child_open = /** @type {Text} */ (push_template_node(empty())); + var child_anchor = current ? current.o : anchor; + + child_anchor.before(child_open); + prev = create_item( - current ? get_first_child(current) : anchor, + child_open, + child_anchor, prev, prev.next, value, @@ -312,7 +320,7 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) { if (matched.length < stashed.length) { // more efficient to move later items to the front var start = stashed[0]; - var local_anchor = get_first_child(start); + var local_anchor = start.o; var j; prev = start.prev; @@ -341,7 +349,7 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) { } else { // more efficient to move earlier items to the back seen.delete(item); - move(item, current ? get_first_child(current) : anchor); + move(item, current ? current.o : anchor); link(item.prev, item.next); link(item, prev.next); @@ -402,20 +410,6 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) { } } -/** - * @param {import('#client').EachItem} item - * @returns {Text | Element | Comment} - */ -function get_first_child(item) { - var current = item.e.dom; - - if (is_array(current)) { - return /** @type {Text | Element | Comment} */ (current[0]); - } - - return /** @type {Text | Element | Comment} */ (current); -} - /** * @param {import('#client').EachItem} item * @param {any} value @@ -437,6 +431,7 @@ function update_item(item, value, index, type) { /** * @template V + * @param {Comment | Text} open * @param {Node} anchor * @param {import('#client').EachItem | import('#client').EachState} prev * @param {import('#client').EachItem | null} next @@ -447,7 +442,7 @@ function update_item(item, value, index, type) { * @param {number} flags * @returns {import('#client').EachItem} */ -function create_item(anchor, prev, next, value, key, index, render_fn, flags) { +function create_item(open, anchor, prev, next, value, key, index, render_fn, flags) { var previous_each_item = current_each_item; try { @@ -465,6 +460,7 @@ function create_item(anchor, prev, next, value, key, index, render_fn, flags) { a: null, // @ts-expect-error e: null, + o: open, prev, next }; @@ -486,6 +482,8 @@ function create_item(anchor, prev, next, value, key, index, render_fn, flags) { * @param {Text | Element | Comment} anchor */ function move(item, anchor) { + anchor.before(item.o); + var dom = item.e.dom; if (dom !== null) { diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index 40583e09f2..ea8e011443 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -79,7 +79,7 @@ function html_to_dom(target, effect, value, svg) { var child = /** @type {Text | Element | Comment} */ (node.firstChild); target.before(child); if (effect !== null) { - push_template_node(effect, child); + push_template_node(child, effect); } return child; } @@ -95,7 +95,7 @@ function html_to_dom(target, effect, value, svg) { } if (effect !== null) { - push_template_node(effect, nodes); + push_template_node(nodes, effect); } return nodes; diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js index c7daff227b..ba447b2c1f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -133,7 +133,7 @@ export function element(anchor, get_tag, is_svg, render_fn) { swap_block_dom(parent_effect, prev_element, element); prev_element.remove(); } else if (!hydrating) { - push_template_node(parent_effect, element); + push_template_node(element, parent_effect); } }); } diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 5baeb2f313..4d37c610eb 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -7,10 +7,13 @@ import { effect } from '../reactivity/effects.js'; import { is_array } from '../utils.js'; /** - * @param {import("#client").Effect} effect * @param {import("#client").TemplateNode | import("#client").TemplateNode[]} dom + * @param {import("#client").Effect} effect */ -export function push_template_node(effect, dom) { +export function push_template_node( + dom, + effect = /** @type {import('#client').Effect} */ (current_effect) +) { var current_dom = effect.dom; if (current_dom === null) { effect.dom = dom; @@ -18,19 +21,12 @@ export function push_template_node(effect, dom) { if (!is_array(current_dom)) { current_dom = effect.dom = [current_dom]; } - var anchor; - // If we're working with an anchor, then remove it and put it at the end. - if (current_dom[0].nodeType === 8) { - anchor = current_dom.pop(); - } + if (is_array(dom)) { current_dom.push(...dom); } else { current_dom.push(dom); } - if (anchor !== undefined) { - current_dom.push(anchor); - } } return dom; } @@ -49,12 +45,8 @@ export function template(content, flags) { var node; return () => { - var effect = /** @type {import('#client').Effect} */ (current_effect); if (hydrating) { - var hydration_content = push_template_node( - effect, - is_fragment ? hydrate_nodes : hydrate_nodes[0] - ); + var hydration_content = push_template_node(is_fragment ? hydrate_nodes : hydrate_nodes[0]); return /** @type {Node} */ (hydration_content); } @@ -64,14 +56,11 @@ export function template(content, flags) { } var clone = use_import_node ? document.importNode(node, true) : clone_node(node, true); - if (is_fragment) { - push_template_node( - effect, - /** @type {import('#client').TemplateNode[]} */ ([...clone.childNodes]) - ); - } else { - push_template_node(effect, /** @type {import('#client').TemplateNode} */ (clone)); - } + push_template_node( + is_fragment + ? /** @type {import('#client').TemplateNode[]} */ ([...clone.childNodes]) + : /** @type {import('#client').TemplateNode} */ (clone) + ); return clone; }; @@ -115,12 +104,8 @@ export function svg_template(content, flags) { var node; return () => { - var effect = /** @type {import('#client').Effect} */ (current_effect); if (hydrating) { - var hydration_content = push_template_node( - effect, - is_fragment ? hydrate_nodes : hydrate_nodes[0] - ); + var hydration_content = push_template_node(is_fragment ? hydrate_nodes : hydrate_nodes[0]); return /** @type {Node} */ (hydration_content); } @@ -139,14 +124,11 @@ export function svg_template(content, flags) { var clone = clone_node(node, true); - if (is_fragment) { - push_template_node( - effect, - /** @type {import('#client').TemplateNode[]} */ ([...clone.childNodes]) - ); - } else { - push_template_node(effect, /** @type {import('#client').TemplateNode} */ (clone)); - } + push_template_node( + is_fragment + ? /** @type {import('#client').TemplateNode[]} */ ([...clone.childNodes]) + : /** @type {import('#client').TemplateNode} */ (clone) + ); return clone; }; @@ -213,8 +195,7 @@ function run_scripts(node) { */ /*#__NO_SIDE_EFFECTS__*/ export function text(anchor) { - var effect = /** @type {import('#client').Effect} */ (current_effect); - if (!hydrating) return push_template_node(effect, empty()); + if (!hydrating) return push_template_node(empty()); var node = hydrate_nodes[0]; @@ -224,7 +205,7 @@ export function text(anchor) { anchor.before((node = empty())); } - return push_template_node(effect, node); + return push_template_node(node); } export const comment = template('', TEMPLATE_FRAGMENT); diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 7b1c4333c8..7530b07f32 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -67,6 +67,8 @@ export type EachItem = { i: number | Source; /** key */ k: unknown; + /** anchor for items inserted before this */ + o: Comment | Text; prev: EachItem | EachState; next: EachItem | null; }; From 43d13e92a026b33a4e2470c637b6cf12ce785dab Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Thu, 18 Apr 2024 18:12:05 +0800 Subject: [PATCH 12/13] feat: allow $inspect reactivity map, set, date (#11164) * feat: allow $inspect reactivity map, set, date * feat: inspect map without adding new data source * feat: add inspect * feat: define inspect on dev mode only * fix: lint error --- .changeset/tiny-poems-doubt.md | 5 ++ .../svelte/src/internal/client/constants.js | 1 + .../svelte/src/internal/client/runtime.js | 23 +++++- packages/svelte/src/reactivity/date.js | 9 +++ packages/svelte/src/reactivity/map.js | 19 +++++ packages/svelte/src/reactivity/set.js | 8 ++ packages/svelte/src/reactivity/url.js | 23 ++++++ .../samples/inspect-reactivity/_config.js | 81 +++++++++++++++++++ .../samples/inspect-reactivity/log.js | 2 + .../samples/inspect-reactivity/main.svelte | 27 +++++++ packages/svelte/types/index.d.ts | 1 + 11 files changed, 197 insertions(+), 2 deletions(-) create mode 100644 .changeset/tiny-poems-doubt.md create mode 100644 packages/svelte/tests/runtime-runes/samples/inspect-reactivity/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/inspect-reactivity/log.js create mode 100644 packages/svelte/tests/runtime-runes/samples/inspect-reactivity/main.svelte diff --git a/.changeset/tiny-poems-doubt.md b/.changeset/tiny-poems-doubt.md new file mode 100644 index 0000000000..b908d95df2 --- /dev/null +++ b/.changeset/tiny-poems-doubt.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +feat: allow inspect reactivity map, set, date diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index b9953b2ec6..7a6e65c068 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -16,3 +16,4 @@ export const EFFECT_RAN = 1 << 13; export const EFFECT_TRANSPARENT = 1 << 14; export const STATE_SYMBOL = Symbol('$state'); +export const INSPECT_SYMBOL = Symbol('$inspect'); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index fe93eb47e4..aa59baf94c 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -27,7 +27,8 @@ import { BRANCH_EFFECT, STATE_SYMBOL, BLOCK_EFFECT, - ROOT_EFFECT + ROOT_EFFECT, + INSPECT_SYMBOL } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { add_owner } from './dev/ownership.js'; @@ -1117,6 +1118,24 @@ export function pop(component) { return component || /** @type {T} */ ({}); } +/** + * + * This is called from the inspect. + * Deeply traverse every item in the array with `deep_read` to register for inspect callback + * If the item implements INSPECT_SYMBOL, will use that instead + * @param {Array} value + * @returns {void} + */ +function deep_read_inpect(value) { + for (const item of value) { + if (item && typeof item[INSPECT_SYMBOL] === 'function') { + item[INSPECT_SYMBOL](); + } else { + deep_read(item); + } + } +} + /** * Possibly traverse an object and read all its properties so that they're all reactive in case this is `$state`. * Does only check first level of an object for performance reasons (heuristic should be good for 99% of all cases). @@ -1247,7 +1266,7 @@ export function inspect(get_value, inspect = console.log) { inspect_fn = fn; const value = get_value(); - deep_read(value); + deep_read_inpect(value); inspect_fn = null; const signals = inspect_captured_signals.slice(); diff --git a/packages/svelte/src/reactivity/date.js b/packages/svelte/src/reactivity/date.js index cf43dfe4ab..302f58ab17 100644 --- a/packages/svelte/src/reactivity/date.js +++ b/packages/svelte/src/reactivity/date.js @@ -1,3 +1,5 @@ +import { DEV } from 'esm-env'; +import { INSPECT_SYMBOL } from '../internal/client/constants.js'; import { source, set } from '../internal/client/reactivity/sources.js'; import { get } from '../internal/client/runtime.js'; @@ -88,6 +90,13 @@ export class ReactiveDate extends Date { return v; }; } + + if (DEV) { + // @ts-ignore + proto[INSPECT_SYMBOL] = function () { + get(this.#raw_time); + }; + } } } diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js index b2b3ab605b..873a7a1f7a 100644 --- a/packages/svelte/src/reactivity/map.js +++ b/packages/svelte/src/reactivity/map.js @@ -3,6 +3,9 @@ import { source, set } from '../internal/client/reactivity/sources.js'; import { get } from '../internal/client/runtime.js'; import { UNINITIALIZED } from '../constants.js'; import { map } from './utils.js'; +import { INSPECT_SYMBOL } from '../internal/client/constants.js'; + +var inited = false; /** * @template K @@ -20,6 +23,22 @@ export class ReactiveMap extends Map { constructor(value) { super(); + if (DEV) { + if (!inited) { + inited = true; + // @ts-ignore + ReactiveMap.prototype[INSPECT_SYMBOL] = function () { + // changes could either introduced by + // - modifying the value, or + // - add / remove entries to the map + for (const [, source] of this.#sources) { + get(source); + } + get(this.#size); + }; + } + } + // If the value is invalid then the native exception will fire here if (DEV) new Map(value); diff --git a/packages/svelte/src/reactivity/set.js b/packages/svelte/src/reactivity/set.js index 1855090cdf..d3eb1cccaf 100644 --- a/packages/svelte/src/reactivity/set.js +++ b/packages/svelte/src/reactivity/set.js @@ -2,6 +2,7 @@ import { DEV } from 'esm-env'; import { source, set } from '../internal/client/reactivity/sources.js'; import { get } from '../internal/client/runtime.js'; import { map } from './utils.js'; +import { INSPECT_SYMBOL } from '../internal/client/constants.js'; var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf']; var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'union']; @@ -65,6 +66,13 @@ export class ReactiveSet extends Set { return new ReactiveSet(set); }; } + + if (DEV) { + // @ts-ignore + proto[INSPECT_SYMBOL] = function () { + get(this.#version); + }; + } } #increment_version() { diff --git a/packages/svelte/src/reactivity/url.js b/packages/svelte/src/reactivity/url.js index 3f11a4af37..df0551fe06 100644 --- a/packages/svelte/src/reactivity/url.js +++ b/packages/svelte/src/reactivity/url.js @@ -1,7 +1,11 @@ +import { DEV } from 'esm-env'; +import { INSPECT_SYMBOL } from '../internal/client/constants.js'; import { source, set } from '../internal/client/reactivity/sources.js'; import { get } from '../internal/client/runtime.js'; const REPLACE = Symbol(); +var inited_url = false; +var inited_search_params = false; export class ReactiveURL extends URL { #protocol = source(super.protocol); @@ -21,6 +25,14 @@ export class ReactiveURL extends URL { url = new URL(url, base); super(url); this.#searchParams[REPLACE](url.searchParams); + + if (DEV && !inited_url) { + inited_url = true; + // @ts-ignore + ReactiveURL.prototype[INSPECT_SYMBOL] = function () { + this.href; + }; + } } get hash() { @@ -159,6 +171,17 @@ export class ReactiveURLSearchParams extends URLSearchParams { set(this.#version, this.#version.v + 1); } + constructor() { + super(); + if (DEV && !inited_search_params) { + inited_search_params = true; + // @ts-ignore + ReactiveURLSearchParams.prototype[INSPECT_SYMBOL] = function () { + get(this.#version); + }; + } + } + /** * @param {URLSearchParams} params */ diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-reactivity/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-reactivity/_config.js new file mode 100644 index 0000000000..89dfc1c729 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-reactivity/_config.js @@ -0,0 +1,81 @@ +import { test } from '../../test'; +import { flushSync } from 'svelte'; +import { log } from './log'; + +export default test({ + compileOptions: { + dev: true + }, + before_test() { + log.length = 0; + }, + async test({ assert, target }) { + const [in1, in2] = target.querySelectorAll('input'); + const [b1, b2, b3] = target.querySelectorAll('button'); + + assert.deepEqual(log, [ + { label: 'map', type: 'init', values: [] }, + { label: 'set', type: 'init', values: [] }, + { label: 'date', type: 'init', values: 1712966400000 } + ]); + log.length = 0; + + flushSync(() => b1.click()); // map.set('key', 'value') + + in1.value = 'name'; + in2.value = 'Svelte'; + in1.dispatchEvent(new window.Event('input', { bubbles: true })); + in2.dispatchEvent(new window.Event('input', { bubbles: true })); + flushSync(() => b1.click()); // map.set('name', 'Svelte') + + in2.value = 'World'; + in2.dispatchEvent(new window.Event('input', { bubbles: true })); + flushSync(() => b1.click()); // map.set('name', 'World') + flushSync(() => b1.click()); // map.set('name', 'World') + + assert.deepEqual(log, [ + { label: 'map', type: 'update', values: [['key', 'value']] }, + { + label: 'map', + type: 'update', + values: [ + ['key', 'value'], + ['name', 'Svelte'] + ] + }, + { + label: 'map', + type: 'update', + values: [ + ['key', 'value'], + ['name', 'World'] + ] + } + ]); + log.length = 0; + + flushSync(() => b2.click()); // set.add('name'); + + in1.value = 'Svelte'; + in1.dispatchEvent(new window.Event('input', { bubbles: true })); + flushSync(() => b2.click()); // set.add('Svelte'); + + flushSync(() => b2.click()); // set.add('Svelte'); + + assert.deepEqual(log, [ + { label: 'set', type: 'update', values: ['name'] }, + { label: 'set', type: 'update', values: ['name', 'Svelte'] } + ]); + log.length = 0; + + flushSync(() => b3.click()); // date.minutes++ + flushSync(() => b3.click()); // date.minutes++ + flushSync(() => b3.click()); // date.minutes++ + + assert.deepEqual(log, [ + { label: 'date', type: 'update', values: 1712966460000 }, + { label: 'date', type: 'update', values: 1712966520000 }, + { label: 'date', type: 'update', values: 1712966580000 } + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-reactivity/log.js b/packages/svelte/tests/runtime-runes/samples/inspect-reactivity/log.js new file mode 100644 index 0000000000..d3df521f4d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-reactivity/log.js @@ -0,0 +1,2 @@ +/** @type {any[]} */ +export const log = []; diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-reactivity/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-reactivity/main.svelte new file mode 100644 index 0000000000..3bbe39a46e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-reactivity/main.svelte @@ -0,0 +1,27 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index a47745a63a..68c992efbe 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2021,6 +2021,7 @@ declare module 'svelte/reactivity' { #private; } class ReactiveURLSearchParams extends URLSearchParams { + constructor(); [REPLACE](params: URLSearchParams): void; #private; From 27eb91bbce50c0825dc73329bf6666272e10a27a Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 18 Apr 2024 13:30:50 +0200 Subject: [PATCH 13/13] fix: dynamic namespace fixes and enhancements (#11219) * fix: fall back to component namespace when not statically determinable In #10006 we added more elaborate mechanisms to determine which namespace a given element is in. For `` we added a "can't know at compile time" case and introduced a limited heuristic into the runtime. This doesn't work for a few reasons: - we're checking the parent's namespace to determine the current namespace, but the element itself could be the one that _changes_ the namespace - as mentioned in the previous comment already, on the first render we can't do any parent analysis - it does not take into account the static component namespace The last point is the crucial one: In Svelte 4, we're falling back to the component namespace if we can't know statically - e.g. if someone added `` then `` should fall back to that namespace instead. We were not doing that up until now, which introduced a regression. Fixing this also means getting rid of the (flawed) "can't know statically" heuristic. Fixes #10858, though for a complete solution we likely need some way to tell `` the namespace at runtime through a special attribute. Maybe we can use `xmlns` for that like we do in the static case * support dynamic svelte:element namespace through xmlns attribute * fix --- .changeset/hip-pumpkins-boil.md | 5 +++ .../src/compiler/phases/2-analyze/index.js | 3 +- .../3-transform/client/transform-client.js | 2 +- .../3-transform/client/visitors/template.js | 33 +++++++++---------- .../svelte/src/compiler/types/template.d.ts | 6 ++-- .../client/dom/blocks/svelte-element.js | 23 ++++++------- .../_config.js | 9 +++++ .../main.svelte | 11 +++++++ .../svg.svelte | 1 + .../_config.js | 10 ++++++ .../main.svelte | 14 ++++++++ packages/svelte/types/index.d.ts | 6 ++-- 12 files changed, 85 insertions(+), 38 deletions(-) create mode 100644 .changeset/hip-pumpkins-boil.md create mode 100644 packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-inherit-namespace-2/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-inherit-namespace-2/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-inherit-namespace-2/svg.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/dynamic-element-dynamic-namespace/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/dynamic-element-dynamic-namespace/main.svelte diff --git a/.changeset/hip-pumpkins-boil.md b/.changeset/hip-pumpkins-boil.md new file mode 100644 index 0000000000..45966b5681 --- /dev/null +++ b/.changeset/hip-pumpkins-boil.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: fall back to component namespace when not statically determinable, add way to tell `` the namespace at runtime diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 457e4d1afa..37eb3a3c92 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -1341,7 +1341,8 @@ const common_visitors = { ancestor.type === 'SvelteFragment' || ancestor.type === 'SnippetBlock' ) { - // Inside a slot or a snippet -> this resets the namespace, so we can't determine it + // Inside a slot or a snippet -> this resets the namespace, so assume the component namespace + node.metadata.svg = context.state.options.namespace === 'svg'; return; } if (ancestor.type === 'SvelteElement' || ancestor.type === 'RegularElement') { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 172fedbe9b..f1de213a9b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -7,7 +7,7 @@ import { global_visitors } from './visitors/global.js'; import { javascript_visitors } from './visitors/javascript.js'; import { javascript_visitors_runes } from './visitors/javascript-runes.js'; import { javascript_visitors_legacy } from './visitors/javascript-legacy.js'; -import { is_state_source, serialize_get_binding } from './utils.js'; +import { serialize_get_binding } from './utils.js'; import { render_stylesheet } from '../css/index.js'; /** diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 8e675ce692..79a161c773 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -2008,6 +2008,9 @@ export const template_visitors = { /** @type {Array} */ const attributes = []; + /** @type {import('#compiler').Attribute['value'] | undefined} */ + let dynamic_namespace = undefined; + /** @type {import('#compiler').ClassDirective[]} */ const class_directives = []; @@ -2036,6 +2039,9 @@ export const template_visitors = { for (const attribute of node.attributes) { if (attribute.type === 'Attribute') { + if (attribute.name === 'xmlns' && !is_text_attribute(attribute)) { + dynamic_namespace = attribute.value; + } attributes.push(attribute); } else if (attribute.type === 'SpreadAttribute') { attributes.push(attribute); @@ -2090,23 +2096,16 @@ export const template_visitors = { } }) ); - context.state.init.push( - b.stmt( - b.call( - '$.element', - context.state.node, - get_tag, - node.metadata.svg === true - ? b.true - : node.metadata.svg === false - ? b.false - : b.literal(null), - inner.length === 0 - ? /** @type {any} */ (undefined) - : b.arrow([element_id, b.id('$$anchor')], b.block(inner)) - ) - ) - ); + + const args = [context.state.node, get_tag, node.metadata.svg ? b.true : b.false]; + if (inner.length > 0) { + args.push(b.arrow([element_id, b.id('$$anchor')], b.block(inner))); + } + if (dynamic_namespace) { + if (inner.length === 0) args.push(b.id('undefined')); + args.push(b.thunk(serialize_attribute_value(dynamic_namespace, context)[1])); + } + context.state.init.push(b.stmt(b.call('$.element', ...args))); }, EachBlock(node, context) { const each_node_meta = node.metadata; diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 695720d48e..e42a34c0aa 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -316,10 +316,10 @@ export interface SvelteElement extends BaseElement { tag: Expression; metadata: { /** - * `true`/`false` if this is definitely (not) an svg element. - * `null` means we can't know statically. + * `true` if this is an svg element. The boolean may not be accurate because + * the tag is dynamic, but we do our best to infer it from the template. */ - svg: boolean | null; + svg: boolean; scoped: boolean; }; } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js index ba447b2c1f..bb660209e1 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -39,11 +39,12 @@ function swap_block_dom(effect, from, to) { /** * @param {Comment} anchor * @param {() => string} get_tag - * @param {boolean | null} is_svg `null` == not statically known - * @param {undefined | ((element: Element, anchor: Node) => void)} render_fn + * @param {boolean} is_svg + * @param {undefined | ((element: Element, anchor: Node) => void)} render_fn, + * @param {undefined | (() => string)} get_namespace * @returns {void} */ -export function element(anchor, get_tag, is_svg, render_fn) { +export function element(anchor, get_tag, is_svg, render_fn, get_namespace) { const parent_effect = /** @type {import('#client').Effect} */ (current_effect); render_effect(() => { @@ -68,22 +69,18 @@ export function element(anchor, get_tag, is_svg, render_fn) { block(() => { const next_tag = get_tag() || null; + const ns = get_namespace + ? get_namespace() + : is_svg || next_tag === 'svg' + ? namespace_svg + : null; + // Assumption: Noone changes the namespace but not the tag (what would that even mean?) if (next_tag === tag) return; // See explanation of `each_item_block` above var previous_each_item = current_each_item; set_current_each_item(each_item_block); - // We try our best infering the namespace in case it's not possible to determine statically, - // but on the first render on the client (without hydration) the parent will be undefined, - // since the anchor is not attached to its parent / the dom yet. - const ns = - is_svg || next_tag === 'svg' - ? namespace_svg - : is_svg === false || anchor.parentElement?.tagName === 'foreignObject' - ? null - : anchor.parentElement?.namespaceURI ?? null; - if (effect) { if (next_tag === null) { // start outro diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-inherit-namespace-2/_config.js b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-inherit-namespace-2/_config.js new file mode 100644 index 0000000000..6eb8ef7e19 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-inherit-namespace-2/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const path = target.querySelector('path'); + + assert.equal(path?.namespaceURI, 'http://www.w3.org/2000/svg'); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-inherit-namespace-2/main.svelte b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-inherit-namespace-2/main.svelte new file mode 100644 index 0000000000..0a3c1954bc --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-inherit-namespace-2/main.svelte @@ -0,0 +1,11 @@ + + + + + + + diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-inherit-namespace-2/svg.svelte b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-inherit-namespace-2/svg.svelte new file mode 100644 index 0000000000..5932e7863f --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-inherit-namespace-2/svg.svelte @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-element-dynamic-namespace/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-element-dynamic-namespace/_config.js new file mode 100644 index 0000000000..df2a7d1778 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-element-dynamic-namespace/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + assert.equal(target.querySelector('path')?.namespaceURI, 'http://www.w3.org/2000/svg'); + + await target.querySelector('button')?.click(); + assert.equal(target.querySelector('div')?.namespaceURI, 'http://www.w3.org/1999/xhtml'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-element-dynamic-namespace/main.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-element-dynamic-namespace/main.svelte new file mode 100644 index 0000000000..b66641956e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-element-dynamic-namespace/main.svelte @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 68c992efbe..b5c5b946e8 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1539,10 +1539,10 @@ declare module 'svelte/compiler' { tag: Expression; metadata: { /** - * `true`/`false` if this is definitely (not) an svg element. - * `null` means we can't know statically. + * `true` if this is an svg element. The boolean may not be accurate because + * the tag is dynamic, but we do our best to infer it from the template. */ - svg: boolean | null; + svg: boolean; scoped: boolean; }; }