From 2c807ad88c95ddce6fdd2fba2508fee29dbacb6e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 29 Jun 2024 13:57:19 -0700 Subject: [PATCH] fix: store DOM boundaries on effects (#12215) * WIP * progress * fix * add comment * update tests * mostly fix dynamic elements * delete unused code * remove unused code * more * tidy up * fix * more * relink effects inside each block * simpler to just leave these in (without children) and ignore them * fix head stuff * tidy up * fix some type errors * simplify * use hydration marker as effect boundary where possible * all tests passing * tidy up * tidy up a bit * tidy up * beat typescript into submission * bring tests over from fix-each-dom-bug * tweaks * simplify a tad * tidy up * simplify * reduce indirection * belt and braces * tidy * revert config change * missed a spot * regenerate types * cleaner separation between EachState and EachItem - precursor to efficient relinking * HMR fix * set effects * FINALLY --- .../src/compiler/phases/1-parse/state/tag.js | 5 +- .../src/compiler/phases/2-analyze/index.js | 7 + .../compiler/phases/2-analyze/validation.js | 6 +- .../3-transform/client/visitors/template.js | 51 +++++-- .../svelte/src/compiler/types/template.d.ts | 6 + packages/svelte/src/constants.js | 1 + .../svelte/src/internal/client/constants.js | 1 + .../svelte/src/internal/client/dev/hmr.js | 2 +- .../src/internal/client/dom/blocks/await.js | 2 +- .../src/internal/client/dom/blocks/each.js | 136 +++++++++--------- .../src/internal/client/dom/blocks/html.js | 127 ++++++---------- .../src/internal/client/dom/blocks/if.js | 4 +- .../src/internal/client/dom/blocks/key.js | 2 +- .../src/internal/client/dom/blocks/snippet.js | 11 +- .../client/dom/blocks/svelte-component.js | 7 +- .../client/dom/blocks/svelte-element.js | 44 +----- .../internal/client/dom/blocks/svelte-head.js | 3 +- .../src/internal/client/dom/hydration.js | 11 ++ .../src/internal/client/dom/operations.js | 16 +-- .../src/internal/client/dom/template.js | 119 ++++++++------- .../src/internal/client/reactivity/effects.js | 85 ++++++++--- .../src/internal/client/reactivity/types.d.ts | 8 +- .../svelte/src/internal/client/runtime.js | 2 +- .../svelte/src/internal/client/types.d.ts | 4 +- .../samples/noscript-removal/_config.js | 27 ---- .../samples/noscript-removal/main.svelte | 5 - .../samples/each-updates-6/_config.js | 27 ++++ .../samples/each-updates-6/main.svelte | 26 ++++ .../samples/each-updates-7/_config.js | 27 ++++ .../samples/each-updates-7/main.svelte | 26 ++++ .../samples/each-updates-8/_config.js | 42 ++++++ .../samples/each-updates-8/main.svelte | 23 +++ .../_expected/client/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 2 +- packages/svelte/types/index.d.ts | 6 + 37 files changed, 534 insertions(+), 343 deletions(-) delete mode 100644 packages/svelte/tests/runtime-legacy/samples/noscript-removal/_config.js delete mode 100644 packages/svelte/tests/runtime-legacy/samples/noscript-removal/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/each-updates-6/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/each-updates-6/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/each-updates-7/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/each-updates-7/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/each-updates-8/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/each-updates-8/main.svelte diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 0dc7b9709e..baa86783b0 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -594,7 +594,10 @@ function special(parser) { type: 'RenderTag', start, end: parser.index, - expression: expression + expression: expression, + metadata: { + dynamic: false + } }); } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index f0f0178144..06cb49b1bc 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -1520,6 +1520,13 @@ const common_visitors = { return; } } + }, + Component(node, context) { + const binding = context.state.scope.get( + node.name.includes('.') ? node.name.slice(0, node.name.indexOf('.')) : node.name + ); + + node.metadata.dynamic = binding !== null && binding.kind !== 'normal'; } }; diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 93d12bfeda..b2097e60ce 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -633,6 +633,11 @@ const validation = { }); }, RenderTag(node, context) { + const callee = unwrap_optional(node.expression).callee; + + node.metadata.dynamic = + callee.type !== 'Identifier' || context.state.scope.get(callee.name)?.kind !== 'normal'; + context.state.analysis.uses_render_tags = true; const raw_args = unwrap_optional(node.expression).arguments; @@ -642,7 +647,6 @@ const validation = { } } - const callee = unwrap_optional(node.expression).callee; if ( callee.type === 'MemberExpression' && callee.property.type === 'Identifier' && 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 b28c73c4d8..3045814ac9 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 @@ -36,6 +36,7 @@ import { EACH_KEYED, is_capture_event, TEMPLATE_FRAGMENT, + TEMPLATE_UNSET_START, TEMPLATE_USE_IMPORT_NODE, TRANSITION_GLOBAL, TRANSITION_IN, @@ -942,6 +943,7 @@ function serialize_inline_component(node, component_name, context) { fn = (node_id) => { return b.call( '$.component', + node_id, b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.expression))), b.arrow( [b.id(component_name)], @@ -1680,14 +1682,35 @@ export const template_visitors = { process_children(trimmed, expression, false, { ...context, state }); + var first = trimmed[0]; + + /** + * If the first item in an effect is a static slot or render tag, it will clone + * a template but without creating a child effect. In these cases, we need to keep + * the current `effect.nodes.start` undefined, so that it can be populated by + * the item in question + * TODO come up with a better name than `unset` + */ + var unset = false; + + if (first.type === 'SlotElement') unset = true; + if (first.type === 'RenderTag' && !first.metadata.dynamic) unset = true; + if (first.type === 'Component' && !first.metadata.dynamic && !context.state.options.hmr) { + unset = true; + } + const use_comment_template = state.template.length === 1 && state.template[0] === ''; if (use_comment_template) { // special case — we can use `$.comment` instead of creating a unique template - body.push(b.var(id, b.call('$.comment'))); + body.push(b.var(id, b.call('$.comment', unset && b.literal(unset)))); } else { let flags = TEMPLATE_FRAGMENT; + if (unset) { + flags |= TEMPLATE_UNSET_START; + } + if (state.metadata.context.template_needs_import_node) { flags |= TEMPLATE_USE_IMPORT_NODE; } @@ -1832,27 +1855,26 @@ export const template_visitors = { context.state.template.push(''); const callee = unwrap_optional(node.expression).callee; const raw_args = unwrap_optional(node.expression).arguments; - const is_reactive = - callee.type !== 'Identifier' || context.state.scope.get(callee.name)?.kind !== 'normal'; - /** @type {import('estree').Expression[]} */ - const args = [context.state.node]; - for (const arg of raw_args) { - args.push(b.thunk(/** @type {import('estree').Expression} */ (context.visit(arg)))); - } + const args = raw_args.map((arg) => + b.thunk(/** @type {import('estree').Expression} */ (context.visit(arg))) + ); let snippet_function = /** @type {import('estree').Expression} */ (context.visit(callee)); if (context.state.options.dev) { snippet_function = b.call('$.validate_snippet', snippet_function); } - if (is_reactive) { - context.state.init.push(b.stmt(b.call('$.snippet', b.thunk(snippet_function), ...args))); + if (node.metadata.dynamic) { + context.state.init.push( + b.stmt(b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args)) + ); } else { context.state.init.push( b.stmt( (node.expression.type === 'CallExpression' ? b.call : b.maybe_call)( snippet_function, + context.state.node, ...args ) ) @@ -1915,7 +1937,7 @@ export const template_visitors = { } if (node.name === 'noscript') { - context.state.template.push(''); + context.state.template.push(''); return; } if (node.name === 'script') { @@ -2985,16 +3007,14 @@ export const template_visitors = { } }, Component(node, context) { - const binding = context.state.scope.get( - node.name.includes('.') ? node.name.slice(0, node.name.indexOf('.')) : node.name - ); - if (binding !== null && binding.kind !== 'normal') { + if (node.metadata.dynamic) { // Handle dynamic references to what seems like static inline components const component = serialize_inline_component(node, '$$component', context); context.state.init.push( b.stmt( b.call( '$.component', + context.state.node, // TODO use untrack here to not update when binding changes? // Would align with Svelte 4 behavior, but it's arguably nicer/expected to update this b.thunk( @@ -3006,6 +3026,7 @@ export const template_visitors = { ); return; } + const component = serialize_inline_component(node, node.name, context); context.state.init.push(component); }, diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 05ac91bda2..25947dcf2b 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -152,6 +152,9 @@ export interface DebugTag extends BaseNode { export interface RenderTag extends BaseNode { type: 'RenderTag'; expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression }); + metadata: { + dynamic: boolean; + }; } type Tag = ExpressionTag | HtmlTag | ConstTag | DebugTag | RenderTag; @@ -271,6 +274,9 @@ interface BaseElement extends BaseNode { export interface Component extends BaseElement { type: 'Component'; + metadata: { + dynamic: boolean; + }; } interface TitleElement extends BaseElement { diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index a8963d6854..5c22be5f5d 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -18,6 +18,7 @@ export const TRANSITION_GLOBAL = 1 << 2; export const TEMPLATE_FRAGMENT = 1; export const TEMPLATE_USE_IMPORT_NODE = 1 << 1; +export const TEMPLATE_UNSET_START = 1 << 2; export const HYDRATION_START = '['; export const HYDRATION_END = ']'; diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 4fc214d469..accdf05036 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -17,6 +17,7 @@ export const EFFECT_TRANSPARENT = 1 << 15; /** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */ export const LEGACY_DERIVED_PROP = 1 << 16; export const INSPECT_EFFECT = 1 << 17; +export const HEAD_EFFECT = 1 << 18; export const STATE_SYMBOL = Symbol('$state'); export const STATE_FROZEN_SYMBOL = Symbol('$state.frozen'); diff --git a/packages/svelte/src/internal/client/dev/hmr.js b/packages/svelte/src/internal/client/dev/hmr.js index 9ba2a69897..e9b4a60bbf 100644 --- a/packages/svelte/src/internal/client/dev/hmr.js +++ b/packages/svelte/src/internal/client/dev/hmr.js @@ -18,7 +18,7 @@ export function hmr(source) { /** @type {import("#client").Effect} */ let effect; - block(() => { + block(anchor, 0, () => { const component = get(source); if (effect) { diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index 4c57f9b564..d1681d3e00 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -105,7 +105,7 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) { } } - var effect = block(() => { + var effect = block(anchor, 0, () => { if (input === (input = get_input())) return; if (is_promise(input)) { diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index e20d4ec41f..7fdd2fc0e7 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -24,12 +24,14 @@ import { run_out_transitions, pause_children, pause_effect, - resume_effect + resume_effect, + get_first_node } from '../../reactivity/effects.js'; 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 { queue_micro_task } from '../task.js'; +import { current_effect } from '../../runtime.js'; /** * The row of a keyed each block that is currently updating. We track this @@ -54,11 +56,12 @@ export function index(_, i) { /** * Pause multiple effects simultaneously, and coordinate their * subsequent destruction. Used in each blocks + * @param {import('#client').EachState} state * @param {import('#client').EachItem[]} items * @param {null | Node} controlled_anchor * @param {Map} items_map */ -function pause_effects(items, controlled_anchor, items_map) { +function pause_effects(state, items, controlled_anchor, items_map) { /** @type {import('#client').TransitionManager[]} */ var transitions = []; var length = items.length; @@ -77,7 +80,7 @@ function pause_effects(items, controlled_anchor, items_map) { clear_text_content(parent_node); parent_node.append(/** @type {Element} */ (controlled_anchor)); items_map.clear(); - link(items[0].prev, items[length - 1].next); + link(state, items[0].prev, items[length - 1].next); } run_out_transitions(transitions, () => { @@ -85,7 +88,7 @@ function pause_effects(items, controlled_anchor, items_map) { var item = items[i]; if (!is_controlled) { items_map.delete(item.k); - link(item.prev, item.next); + link(state, item.prev, item.next); } destroy_effect(item.e, !is_controlled); } @@ -104,7 +107,7 @@ function pause_effects(items, controlled_anchor, items_map) { */ export function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn = null) { /** @type {import('#client').EachState} */ - var state = { flags, items: new Map(), next: null }; + var state = { flags, items: new Map(), first: null }; var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; @@ -121,7 +124,7 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback /** @type {import('#client').Effect | null} */ var fallback = null; - block(() => { + block(anchor, 0, () => { var collection = get_collection(); var array = is_array(collection) @@ -163,8 +166,8 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback /** @type {Node} */ var child_anchor = hydrate_start; - /** @type {import('#client').EachItem | import('#client').EachState} */ - var prev = state; + /** @type {import('#client').EachItem | null} */ + var prev = null; /** @type {import('#client').EachItem} */ var item; @@ -184,7 +187,7 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback 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_anchor, state, prev, null, value, key, i, render_fn, flags); state.items.set(key, item); child_anchor = /** @type {Comment} */ (child_anchor.nextSibling); @@ -242,14 +245,14 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) { var length = array.length; var items = state.items; - var first = state.next; + var first = state.first; var current = first; /** @type {Set} */ var seen = new Set(); - /** @type {import('#client').EachState | import('#client').EachItem} */ - var prev = state; + /** @type {import('#client').EachItem | null} */ + var prev = null; /** @type {Set} */ var to_animate = new Set(); @@ -293,7 +296,17 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) { if (item === undefined) { var child_anchor = current ? get_first_node(current.e) : anchor; - prev = create_item(child_anchor, prev, prev.next, value, key, i, render_fn, flags); + prev = create_item( + child_anchor, + state, + prev, + prev === null ? state.first : prev.next, + value, + key, + i, + render_fn, + flags + ); items.set(key, prev); @@ -336,9 +349,9 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) { seen.delete(stashed[j]); } - link(a.prev, b.next); - link(prev, a); - link(b, start); + link(state, a.prev, b.next); + link(state, prev, a); + link(state, b, start); current = start; prev = b; @@ -351,9 +364,9 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) { seen.delete(item); move(item, current, anchor); - link(item.prev, item.next); - link(item, prev.next); - link(prev, item); + link(state, item.prev, item.next); + link(state, item, prev === null ? state.first : prev.next); + link(state, prev, item); prev = item; } @@ -403,7 +416,7 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) { } } - pause_effects(to_destroy, controlled_anchor, items); + pause_effects(state, to_destroy, controlled_anchor, items); } if (is_animated) { @@ -413,6 +426,9 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) { } }); } + + /** @type {import('#client').Effect} */ (current_effect).first = state.first && state.first.e; + /** @type {import('#client').Effect} */ (current_effect).last = prev && prev.e; } /** @@ -437,7 +453,8 @@ function update_item(item, value, index, type) { /** * @template V * @param {Node} anchor - * @param {import('#client').EachItem | import('#client').EachState} prev + * @param {import('#client').EachState} state + * @param {import('#client').EachItem | null} prev * @param {import('#client').EachItem | null} next * @param {V} value * @param {unknown} key @@ -446,7 +463,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(anchor, state, prev, next, value, key, index, render_fn, flags) { var previous_each_item = current_each_item; try { @@ -468,52 +485,28 @@ function create_item(anchor, prev, next, value, key, index, render_fn, flags) { next }; - prev.next = item; - if (next !== null) next.prev = item; - current_each_item = item; - item.e = branch(() => render_fn(anchor, v, i)); + item.e = branch(() => render_fn(anchor, v, i), hydrating); - return item; - } finally { - current_each_item = previous_each_item; - } -} + item.e.prev = prev && prev.e; + item.e.next = next && next.e; -/** - * @param {import('#client').TemplateNode} dom - * @param {import("#client").Effect} effect - * @returns {import('#client').TemplateNode} - */ -function get_adjusted_first_node(dom, effect) { - if ((dom.nodeType === 3 && /** @type {Text} */ (dom).data === '') || dom.nodeType === 8) { - var adjusted = effect.first; - var next; - while (adjusted !== null) { - next = adjusted.first; - if (adjusted.dom !== null) { - break; - } else if (next === null) { - return /** @type {import('#client').TemplateNode} */ (dom.previousSibling); - } - adjusted = next; + if (prev === null) { + state.first = item; + } else { + prev.next = item; + prev.e.next = item.e; } - return get_first_node(/** @type {import("#client").Effect} */ (adjusted)); - } - return dom; -} -/** - * - * @param {import('#client').Effect} effect - * @returns {import('#client').TemplateNode} - */ -function get_first_node(effect) { - var dom = effect.dom; - if (is_array(dom)) { - return get_adjusted_first_node(dom[0], effect); + if (next !== null) { + next.prev = item; + next.e.prev = item.e; + } + + return item; + } finally { + current_each_item = previous_each_item; } - return get_adjusted_first_node(/** @type {import('#client').TemplateNode} **/ (dom), effect); } /** @@ -535,11 +528,20 @@ function move(item, next, anchor) { } /** - * - * @param {import('#client').EachItem | import('#client').EachState} prev + * @param {import('#client').EachState} state + * @param {import('#client').EachItem | null} prev * @param {import('#client').EachItem | null} next */ -function link(prev, next) { - prev.next = next; - if (next !== null) next.prev = prev; +function link(state, prev, next) { + if (prev === null) { + state.first = next; + } else { + prev.next = next; + prev.e.next = next && next.e; + } + + if (next !== null) { + next.prev = prev; + next.e.prev = prev && prev.e; + } } diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index 6873adf70c..c014da4a1d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -1,30 +1,7 @@ -import { derived } from '../../reactivity/deriveds.js'; -import { render_effect } from '../../reactivity/effects.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; - } -} +import { block, branch, destroy_effect } from '../../reactivity/effects.js'; +import { get_start, hydrate_nodes, hydrating } from '../hydration.js'; +import { create_fragment_from_html } from '../reconciler.js'; +import { assign_nodes } from '../template.js'; /** * @param {Element | Text | Comment} anchor @@ -34,72 +11,52 @@ function remove_from_parent_effect(effect, to_remove) { * @returns {void} */ export function html(anchor, get_value, svg, mathml) { - const parent_effect = anchor.parentNode !== current_effect?.dom ? current_effect : null; - let value = derived(get_value); + var value = ''; - render_effect(() => { - var dom = html_to_dom(anchor, parent_effect, get(value), svg, mathml); + /** @type {import('#client').Effect | null} */ + var effect; - if (dom) { - return () => { - if (parent_effect !== null) { - remove_from_parent_effect(parent_effect, is_array(dom) ? dom : [dom]); - } - remove(dom); - }; - } - }); -} + block(anchor, 0, () => { + if (value === (value = get_value())) return; -/** - * Creates the content for a `@html` tag from its string value, - * 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 - * @param {boolean} mathml - * @returns {Element | Comment | (Element | Comment | Text)[]} - */ -function html_to_dom(target, effect, value, svg, mathml) { - if (hydrating) return hydrate_nodes; - - var html = value + ''; - if (svg) html = `${html}`; - else if (mathml) html = `${html}`; + if (effect) { + destroy_effect(effect); + effect = null; + } - // Don't use create_fragment_with_script_from_html here because that would mean script tags are executed. - // @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons. - /** @type {DocumentFragment | Element} */ - var node = create_fragment_from_html(html); + if (value === '') return; - if (svg || mathml) { - node = /** @type {Element} */ (node.firstChild); - } + effect = branch(() => { + if (hydrating) { + assign_nodes(get_start(), hydrate_nodes[hydrate_nodes.length - 1]); + return; + } - if (node.childNodes.length === 1) { - var child = /** @type {Text | Element | Comment} */ (node.firstChild); - target.before(child); - if (effect !== null) { - push_template_node(child, effect); - } - return child; - } + var html = value + ''; + if (svg) html = `${html}`; + else if (mathml) html = `${html}`; - var nodes = /** @type {Array} */ ([...node.childNodes]); + // Don't use create_fragment_with_script_from_html here because that would mean script tags are executed. + // @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons. + /** @type {DocumentFragment | Element} */ + var node = create_fragment_from_html(html); - if (svg || mathml) { - while (node.firstChild) { - target.before(node.firstChild); - } - } else { - target.before(node); - } + if (svg || mathml) { + node = /** @type {Element} */ (node.firstChild); + } - if (effect !== null) { - push_template_node(nodes, effect); - } + assign_nodes( + /** @type {import('#client').TemplateNode} */ (node.firstChild), + /** @type {import('#client').TemplateNode} */ (node.lastChild) + ); - return nodes; + if (svg || mathml) { + while (node.firstChild) { + anchor.before(node.firstChild); + } + } else { + anchor.before(node); + } + }); + }); } diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 80ed8c09f4..9ad208fe2f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -30,7 +30,7 @@ export function if_block( var flags = elseif ? EFFECT_TRANSPARENT : 0; - block(() => { + block(anchor, flags, () => { if (condition === (condition = !!get_condition())) return; /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ @@ -78,5 +78,5 @@ export function if_block( // continue in hydration mode set_hydrating(true); } - }, flags); + }); } diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js index eb68978ffe..641fd2875e 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -16,7 +16,7 @@ export function key_block(anchor, get_key, render_fn) { /** @type {import('#client').Effect} */ let effect; - block(() => { + block(anchor, 0, () => { if (safe_not_equal(key, (key = get_key()))) { if (effect) { pause_effect(effect); diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index 611733feb1..5602f7baf2 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -2,26 +2,25 @@ import { add_snippet_symbol } from '../../../shared/validate.js'; import { EFFECT_TRANSPARENT } from '../../constants.js'; import { branch, block, destroy_effect } from '../../reactivity/effects.js'; import { - current_component_context, dev_current_component_function, set_dev_current_component_function } from '../../runtime.js'; /** * @template {(node: import('#client').TemplateNode, ...args: any[]) => import('#client').Dom} SnippetFn + * @param {import('#client').TemplateNode} anchor * @param {() => SnippetFn | null | undefined} get_snippet - * @param {import('#client').TemplateNode} node * @param {(() => any)[]} args * @returns {void} */ -export function snippet(get_snippet, node, ...args) { +export function snippet(anchor, get_snippet, ...args) { /** @type {SnippetFn | null | undefined} */ var snippet; /** @type {import('#client').Effect | null} */ var snippet_effect; - block(() => { + block(anchor, EFFECT_TRANSPARENT, () => { if (snippet === (snippet = get_snippet())) return; if (snippet_effect) { @@ -30,9 +29,9 @@ export function snippet(get_snippet, node, ...args) { } if (snippet) { - snippet_effect = branch(() => /** @type {SnippetFn} */ (snippet)(node, ...args)); + snippet_effect = branch(() => /** @type {SnippetFn} */ (snippet)(anchor, ...args)); } - }, EFFECT_TRANSPARENT); + }); } /** diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js index 8487852b35..0e04eff10b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -1,22 +1,21 @@ import { block, branch, pause_effect } from '../../reactivity/effects.js'; -// TODO seems weird that `anchor` is unused here — possible bug? - /** * @template P * @template {(props: P) => void} C + * @param {import('#client').TemplateNode} anchor * @param {() => C} get_component * @param {(component: C) => import('#client').Dom | void} render_fn * @returns {void} */ -export function component(get_component, render_fn) { +export function component(anchor, get_component, render_fn) { /** @type {C} */ let component; /** @type {import('#client').Effect | null} */ let effect; - block(() => { + block(anchor, 0, () => { if (component === (component = get_component())) return; if (effect) { 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 6e0740b937..9fc0ab1dbb 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -12,31 +12,9 @@ import { set_should_intro } from '../../render.js'; import { current_each_item, set_current_each_item } from './each.js'; import { current_component_context, current_effect } from '../../runtime.js'; import { DEV } from 'esm-env'; -import { is_array } from '../../utils.js'; -import { push_template_node } from '../template.js'; +import { assign_nodes } from '../template.js'; import { noop } from '../../../shared/utils.js'; -/** - * @param {import('#client').Effect} effect - * @param {Element} from - * @param {Element} to - * @returns {void} - */ -function swap_block_dom(effect, from, to) { - const dom = effect.dom; - - if (is_array(dom)) { - for (let i = 0; i < dom.length; i++) { - if (dom[i] === from) { - dom[i] = to; - break; - } - } - } else if (dom === from) { - effect.dom = to; - } -} - /** * @param {Comment | Element} node * @param {() => string} get_tag @@ -63,18 +41,6 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio /** @type {import('#client').Effect | null} */ let effect; - const parent_effect = /** @type {import('#client').Effect} */ (current_effect); - - // Remove the the hydrated effect dom entry for our dynamic element - if (hydrating && is_array(parent_effect.dom)) { - var remove_index = parent_effect.dom.indexOf( - /** @type {import('#client').TemplateNode} */ (element) - ); - if (remove_index !== -1) { - parent_effect.dom.splice(remove_index, 1); - } - } - /** * The keyed `{#each ...}` item block, if any, that this element is inside. * We track this so we can set it when changing the element, allowing any @@ -82,8 +48,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio */ let each_item_block = current_each_item; - block(() => { - const element_effect = /** @type {import('#client').Effect} */ (current_effect); + block(anchor, 0, () => { const next_tag = get_tag() || null; const ns = get_namespace ? get_namespace() @@ -125,6 +90,8 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio ? document.createElementNS(ns, next_tag) : document.createElement(next_tag); + assign_nodes(element, element); + if (DEV && location) { // @ts-expect-error element.__svelte_meta = { @@ -137,10 +104,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio } if (prev_element && !hydrating) { - swap_block_dom(element_effect, prev_element, element); prev_element.remove(); - } else { - push_template_node(element, element_effect); } if (render_fn) { diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js index b00a3a242b..d61c8fbe24 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -2,6 +2,7 @@ import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrate_nodes } from '../ import { empty } from '../operations.js'; import { block } from '../../reactivity/effects.js'; import { HYDRATION_END, HYDRATION_START } from '../../../../constants.js'; +import { HEAD_EFFECT } from '../../constants.js'; /** * @type {Node | undefined} @@ -47,7 +48,7 @@ export function head(render_fn) { } try { - block(() => render_fn(anchor)); + block(null, HEAD_EFFECT, () => render_fn(anchor)); } finally { if (was_hydrating) { set_hydrate_nodes(/** @type {import('#client').TemplateNode[]} */ (previous_hydrate_nodes)); diff --git a/packages/svelte/src/internal/client/dom/hydration.js b/packages/svelte/src/internal/client/dom/hydration.js index 06113ba784..02d07be057 100644 --- a/packages/svelte/src/internal/client/dom/hydration.js +++ b/packages/svelte/src/internal/client/dom/hydration.js @@ -30,6 +30,17 @@ export function set_hydrate_nodes(nodes) { hydrate_start = nodes && nodes[0]; } +/** + * When assigning nodes to an effect during hydration, we typically want the hydration boundary comment node + * immediately before `hydrate_start`. In some cases, this comment doesn't exist because we optimized it away. + * TODO it might be worth storing this value separately rather than retrieving it with `previousSibling` + */ +export function get_start() { + return /** @type {import('#client').TemplateNode} */ ( + hydrate_start.previousSibling ?? hydrate_start + ); +} + /** * This function is only called when `hydrating` is true. If passed a `` opening * hydration marker, it finds the corresponding closing marker and sets `hydrate_nodes` diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index c1fdbd0e75..78c0969088 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -85,13 +85,13 @@ export function first_child(fragment, is_text) { // text node to hydrate — we must therefore create one if (is_text && hydrate_start?.nodeType !== 3) { var text = empty(); - var dom = /** @type {import('#client').TemplateNode[]} */ ( - /** @type {import('#client').Effect} */ (current_effect).dom - ); + var effect = /** @type {import('#client').Effect} */ (current_effect); - dom.unshift(text); - hydrate_start?.before(text); + if (effect.nodes?.start === hydrate_start) { + effect.nodes.start = text; + } + hydrate_start?.before(text); return text; } @@ -122,13 +122,7 @@ export function sibling(node, is_text = false) { // text node to hydrate — we must therefore create one if (is_text && type !== 3) { var text = empty(); - var dom = /** @type {import('#client').TemplateNode[]} */ ( - /** @type {import('#client').Effect} */ (current_effect).dom - ); - - dom.unshift(text); next_sibling?.before(text); - return text; } diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 7342b71eda..1ebef63bba 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -1,35 +1,28 @@ -import { hydrate_nodes, hydrate_start, hydrating } from './hydration.js'; +import { get_start, hydrate_nodes, hydrate_start, hydrating } from './hydration.js'; import { empty } from './operations.js'; 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 { is_array } from '../utils.js'; +import { + TEMPLATE_FRAGMENT, + TEMPLATE_UNSET_START, + TEMPLATE_USE_IMPORT_NODE +} from '../../../constants.js'; import { queue_micro_task } from './task.js'; /** - * @template {import("#client").TemplateNode | import("#client").TemplateNode[]} T - * @param {T} dom - * @param {import("#client").Effect} effect + * + * @param {import('#client').TemplateNode | undefined | null} start + * @param {import('#client').TemplateNode} end + * @param {import('#client').TemplateNode | null} anchor */ -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; - } else { - if (!is_array(current_dom)) { - current_dom = effect.dom = [current_dom]; - } +export function assign_nodes(start, end, anchor = null) { + const effect = /** @type {import('#client').Effect} */ (current_effect); - if (is_array(dom)) { - current_dom.push(...dom); - } else { - current_dom.push(dom); - } + if (effect.nodes === null) { + effect.nodes = { start, anchor, end }; + } else if (effect.nodes.start === undefined) { + effect.nodes.start = start; } - return dom; } /** @@ -45,9 +38,13 @@ export function template(content, flags) { /** @type {Node} */ var node; + var has_start = !content.startsWith(''); + var unset = (flags & TEMPLATE_UNSET_START) !== 0; + return () => { if (hydrating) { - push_template_node(is_fragment ? hydrate_nodes : hydrate_start); + assign_nodes(get_start(), hydrate_nodes[hydrate_nodes.length - 1]); + return hydrate_start; } @@ -56,14 +53,20 @@ export function template(content, flags) { if (!is_fragment) node = /** @type {Node} */ (node.firstChild); } - var clone = use_import_node ? document.importNode(node, true) : node.cloneNode(true); - - push_template_node( - is_fragment - ? /** @type {import('#client').TemplateNode[]} */ ([...clone.childNodes]) - : /** @type {import('#client').TemplateNode} */ (clone) + var clone = /** @type {import('#client').TemplateNode} */ ( + use_import_node ? document.importNode(node, true) : node.cloneNode(true) ); + if (is_fragment) { + var first = /** @type {import('#client').TemplateNode} */ (clone.firstChild); + var start = has_start ? first : unset ? undefined : null; + var end = /** @type {import('#client').TemplateNode} */ (clone.lastChild); + + assign_nodes(start, end, first); + } else { + assign_nodes(clone, clone); + } + return clone; }; } @@ -101,37 +104,46 @@ export function template_with_script(content, flags) { /*#__NO_SIDE_EFFECTS__*/ export function ns_template(content, flags, ns = 'svg') { var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0; - var fn = template(`<${ns}>${content}`, 0); // we don't need to worry about using importNode for namespaced elements + var wrapped = `<${ns}>${content}`; /** @type {Element | DocumentFragment} */ var node; + var has_start = !content.startsWith(''); + var unset = (flags & TEMPLATE_UNSET_START) !== 0; + return () => { if (hydrating) { - push_template_node(is_fragment ? hydrate_nodes : hydrate_start); + assign_nodes(get_start(), hydrate_nodes[hydrate_nodes.length - 1]); + return hydrate_start; } if (!node) { - var wrapper = /** @type {Element} */ (fn()); + var fragment = /** @type {DocumentFragment} */ (create_fragment_from_html(wrapped)); + var root = /** @type {Element} */ (fragment.firstChild); - if ((flags & TEMPLATE_FRAGMENT) === 0) { - node = /** @type {Element} */ (wrapper.firstChild); - } else { + if (is_fragment) { node = document.createDocumentFragment(); - while (wrapper.firstChild) { - node.appendChild(wrapper.firstChild); + while (root.firstChild) { + node.appendChild(root.firstChild); } + } else { + node = /** @type {Element} */ (root.firstChild); } } - var clone = node.cloneNode(true); + var clone = /** @type {import('#client').TemplateNode} */ (node.cloneNode(true)); - push_template_node( - is_fragment - ? /** @type {import('#client').TemplateNode[]} */ ([...clone.childNodes]) - : /** @type {import('#client').TemplateNode} */ (clone) - ); + if (is_fragment) { + var first = /** @type {import('#client').TemplateNode} */ (clone.firstChild); + var start = has_start ? first : unset ? undefined : null; + var end = /** @type {import('#client').TemplateNode} */ (clone.lastChild); + + assign_nodes(start, end, first); + } else { + assign_nodes(clone, clone); + } return clone; }; @@ -208,7 +220,11 @@ function run_scripts(node) { */ /*#__NO_SIDE_EFFECTS__*/ export function text(anchor) { - if (!hydrating) return push_template_node(empty()); + if (!hydrating) { + var t = empty(); + assign_nodes(t, t); + return t; + } var node = hydrate_start; @@ -218,21 +234,26 @@ export function text(anchor) { anchor.before((node = empty())); } - push_template_node(node); + assign_nodes(node, node); return node; } -export function comment() { +/** + * @param {boolean} unset + */ +export function comment(unset = false) { // we're not delegating to `template` here for performance reasons if (hydrating) { - push_template_node(hydrate_nodes); + assign_nodes(get_start(), hydrate_nodes[hydrate_nodes.length - 1]); + return hydrate_start; } var frag = document.createDocumentFragment(); var anchor = empty(); frag.append(anchor); - push_template_node([anchor]); + + assign_nodes(unset ? undefined : null, anchor, anchor); return frag; } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 0de476dab0..3e2db129b9 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -31,10 +31,10 @@ import { DERIVED, UNOWNED, CLEAN, - INSPECT_EFFECT + INSPECT_EFFECT, + HEAD_EFFECT } from '../constants.js'; import { set } from './sources.js'; -import { remove } from '../dom/reconciler.js'; import * as e from '../errors.js'; import { DEV } from 'esm-env'; import { define_property } from '../utils.js'; @@ -75,16 +75,17 @@ export function push_effect(effect, parent_effect) { * @param {number} type * @param {null | (() => void | (() => void))} fn * @param {boolean} sync + * @param {boolean} push * @returns {import('#client').Effect} */ -function create_effect(type, fn, sync) { +function create_effect(type, fn, sync, push = true) { var is_root = (type & ROOT_EFFECT) !== 0; /** @type {import('#client').Effect} */ var effect = { ctx: current_component_context, deps: null, - dom: null, + nodes: null, f: type | DIRTY, first: null, fn, @@ -120,10 +121,10 @@ function create_effect(type, fn, sync) { sync && effect.deps === null && effect.first === null && - effect.dom === null && + effect.nodes === null && effect.teardown === null; - if (!inert && !is_root) { + if (!inert && !is_root && push) { if (current_effect !== null) { push_effect(effect, current_effect); } @@ -298,16 +299,22 @@ export function template_effect(fn) { } /** - * @param {(() => void)} fn + * @param {import('#client').TemplateNode | null} anchor * @param {number} flags + * @param {(() => void)} fn */ -export function block(fn, flags = 0) { - return create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true); +export function block(anchor, flags, fn) { + const effect = create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true); + if (anchor !== null) effect.nodes = { start: null, anchor: null, end: anchor }; + return effect; } -/** @param {(() => void)} fn */ -export function branch(fn) { - return create_effect(RENDER_EFFECT | BRANCH_EFFECT, fn, true); +/** + * @param {(() => void)} fn + * @param {boolean} [push] + */ +export function branch(fn, push = true) { + return create_effect(RENDER_EFFECT | BRANCH_EFFECT, fn, true, push); } /** @@ -335,13 +342,26 @@ export function execute_effect_teardown(effect) { * @returns {void} */ export function destroy_effect(effect, remove_dom = true) { - var dom = effect.dom; + var removed = false; - if (dom !== null && remove_dom) { - remove(dom); + if ((remove_dom || (effect.f & HEAD_EFFECT) !== 0) && effect.nodes !== null) { + /** @type {import('#client').TemplateNode | null} */ + var node = get_first_node(effect); + var end = effect.nodes.end; + + while (node !== null) { + /** @type {import('#client').TemplateNode | null} */ + var next = + node === end ? null : /** @type {import('#client').TemplateNode} */ (node.nextSibling); + + node.remove(); + node = next; + } + + removed = true; } - destroy_effect_children(effect, remove_dom); + destroy_effect_children(effect, remove_dom && !removed); remove_reactions(effect, 0); set_signal_status(effect, DESTROYED); @@ -365,13 +385,44 @@ export function destroy_effect(effect, remove_dom = true) { effect.prev = effect.teardown = effect.ctx = - effect.dom = effect.deps = effect.parent = effect.fn = + effect.nodes = null; } +/** + * @param {import('#client').Effect} effect + * @returns {import('#client').TemplateNode} + */ +export function get_first_node(effect) { + var nodes = /** @type {NonNullable} */ (effect.nodes); + var start = nodes.start; + + if (start === undefined) { + // edge case — a snippet or component was the first item inside the effect, + // but it didn't render any DOM. in this case, we return the item's anchor + return /** @type {import('#client').TemplateNode} */ (nodes.anchor); + } + + if (start !== null) { + return start; + } + + var child = effect.first; + while (child && (child.nodes === null || (child.f & HEAD_EFFECT) !== 0)) { + child = child.next; + } + + if (child !== null && child.nodes !== null) { + return get_first_node(child); + } + + // in the case that there's no DOM, return the first anchor + return nodes.end; +} + /** * Detach an effect from the effect tree, freeing up memory and * reducing the amount of work that happens on subsequent traversals diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 4d77a68a13..1d35446607 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -1,4 +1,4 @@ -import type { ComponentContext, Dom, Equals, TransitionManager } from '#client'; +import type { ComponentContext, Dom, Equals, TemplateNode, TransitionManager } from '#client'; export interface Signal { /** Flags bitmask */ @@ -36,7 +36,11 @@ export interface Derived extends Value, Reaction { export interface Effect extends Reaction { parent: Effect | null; - dom: Dom | null; + nodes: null | { + start: undefined | null | TemplateNode; + anchor: null | TemplateNode; + end: TemplateNode; + }; /** The associated component context */ ctx: null | ComponentContext; /** The effect function */ diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index c46acaa160..f6db179b78 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -555,7 +555,7 @@ function flush_queued_effects(effects) { // don't know if we need to keep them until they are executed. Doing the check // here (rather than in `execute_effect`) allows us to skip the work for // immediate effects. - if (effect.deps === null && effect.first === null && effect.dom === null) { + if (effect.deps === null && effect.first === null && effect.nodes === null) { if (effect.teardown === null) { // remove this effect from the graph unlink_effect(effect); diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 292c233fa6..53fd222d18 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -71,7 +71,7 @@ export type EachState = { /** a key -> item lookup */ items: Map; /** head of the linked list of items */ - next: EachItem | null; + first: EachItem | null; }; export type EachItem = { @@ -85,7 +85,7 @@ export type EachItem = { i: number | Source; /** key */ k: unknown; - prev: EachItem | EachState; + prev: EachItem | null; next: EachItem | null; }; diff --git a/packages/svelte/tests/runtime-legacy/samples/noscript-removal/_config.js b/packages/svelte/tests/runtime-legacy/samples/noscript-removal/_config.js deleted file mode 100644 index d7e1b908c0..0000000000 --- a/packages/svelte/tests/runtime-legacy/samples/noscript-removal/_config.js +++ /dev/null @@ -1,27 +0,0 @@ -import { test } from '../../test'; - -export default test({ - test({ assert, target, variant }) { - // if created on client side, should not build noscript - if (variant === 'dom') { - assert.equal(target.querySelectorAll('noscript').length, 0); - assert.htmlEqual( - target.innerHTML, - ` -
foo
-
foo
foo
- ` - ); - } else { - assert.equal(target.querySelectorAll('noscript').length, 3); - assert.htmlEqual( - target.innerHTML, - ` - -
foo
-
foo
foo
- ` - ); - } - } -}); diff --git a/packages/svelte/tests/runtime-legacy/samples/noscript-removal/main.svelte b/packages/svelte/tests/runtime-legacy/samples/noscript-removal/main.svelte deleted file mode 100644 index 0cc2155e5d..0000000000 --- a/packages/svelte/tests/runtime-legacy/samples/noscript-removal/main.svelte +++ /dev/null @@ -1,5 +0,0 @@ - - -
foo
- -
foo
foo
diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-6/_config.js b/packages/svelte/tests/runtime-runes/samples/each-updates-6/_config.js new file mode 100644 index 0000000000..8bd2d17131 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-6/_config.js @@ -0,0 +1,27 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `
  • test (1)
  • test 2 (2)
  • test 3 (3)
`, + + async test({ assert, target }) { + const [btn1] = target.querySelectorAll('button'); + + flushSync(() => { + btn1.click(); + }); + + flushSync(() => { + btn1.click(); + }); + + flushSync(() => { + btn1.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `
  • test (1)
  • test 2 (2)
  • test 3 (3)
` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-6/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-updates-6/main.svelte new file mode 100644 index 0000000000..a1b948ac0f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-6/main.svelte @@ -0,0 +1,26 @@ + + +{#snippet renderItem(item)} +
  • + {item.name} ({item.id}) + {#if item.color}{/if} +
  • +{/snippet} + +
      + {#each items as item (item.id)} + {@render renderItem(item)} + {/each} +
    + diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-7/_config.js b/packages/svelte/tests/runtime-runes/samples/each-updates-7/_config.js new file mode 100644 index 0000000000..7f1f5b6589 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-7/_config.js @@ -0,0 +1,27 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `
    • test (1)
    • test 2 (2)
    • test 3 (3)
    `, + + async test({ assert, target }) { + const [btn1] = target.querySelectorAll('button'); + + flushSync(() => { + btn1.click(); + }); + + flushSync(() => { + btn1.click(); + }); + + flushSync(() => { + btn1.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `
    • test (1)
    • test 2 (2)
    • test 3 (3)
    ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-7/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-updates-7/main.svelte new file mode 100644 index 0000000000..df8b054a42 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-7/main.svelte @@ -0,0 +1,26 @@ + + +{#snippet renderItem(item)} +
  • + {item.name} ({item.id}) +
  • + {#if item.color}{/if} +{/snippet} + +
      + {#each items as item (item.id)} + {@render renderItem(item)} + {/each} +
    + diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-8/_config.js b/packages/svelte/tests/runtime-runes/samples/each-updates-8/_config.js new file mode 100644 index 0000000000..e675dcaf67 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-8/_config.js @@ -0,0 +1,42 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

    first

    message 1

    `, + + async test({ assert, target }) { + /** + * @type {{ click: () => void; }} + */ + let btn1; + + [btn1] = target.querySelectorAll('button'); + + flushSync(() => { + btn1.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `

    first

    message 1

    message 2

    ` + ); + + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + `

    first

    message 1

    message 2

    ` + ); + + flushSync(() => { + btn1.click(); + }); + + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + `

    first

    message 1

    message 2

    message 3

    ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-8/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-updates-8/main.svelte new file mode 100644 index 0000000000..869ccdc8dd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-8/main.svelte @@ -0,0 +1,23 @@ + + + + +{#each messages as msg, i (`${msg.id}_${msg.tmpId ?? ""}`)} + {#if i === 0} +

    first

    + {/if} +

    {msg.content}

    +{/each} diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js index 0e193af12d..c38eede434 100644 --- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js @@ -3,7 +3,7 @@ import * as $ from "svelte/internal/client"; import TextInput from './Child.svelte'; var root_1 = $.template(`Something`, 1); -var root = $.template(` `, 1); +var root = $.template(` `, 5); export default function Bind_component_snippet($$anchor) { var snippet = ($$anchor) => { diff --git a/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js index c766ee0a79..c1f5a2a309 100644 --- a/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js @@ -2,7 +2,7 @@ import "svelte/internal/disclose-version"; import * as $ from "svelte/internal/client"; export default function Bind_this($$anchor) { - var fragment = $.comment(); + var fragment = $.comment(true); var node = $.first_child(fragment); $.bind_this(Foo(node, { $$legacy: true }), ($$value) => foo = $$value, () => foo); diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js index 2d25ba9fd2..2cf005abdf 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js @@ -9,7 +9,7 @@ export default function Function_prop_no_getter($$anchor) { } const plusOne = (num) => num + 1; - var fragment = $.comment(); + var fragment = $.comment(true); var node = $.first_child(fragment); Button(node, { diff --git a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js index 7cb2415bf5..0f2d6f4200 100644 --- a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js @@ -30,4 +30,4 @@ export default function State_proxy_literal($$anchor) { $.append($$anchor, fragment); } -$.delegate(["click"]); +$.delegate(["click"]); \ No newline at end of file diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index d7503bb08f..a325cab40a 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1568,6 +1568,9 @@ declare module 'svelte/compiler' { interface RenderTag extends BaseNode { type: 'RenderTag'; expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression }); + metadata: { + dynamic: boolean; + }; } type Tag = ExpressionTag | HtmlTag | ConstTag | DebugTag | RenderTag; @@ -1687,6 +1690,9 @@ declare module 'svelte/compiler' { interface Component extends BaseElement { type: 'Component'; + metadata: { + dynamic: boolean; + }; } interface TitleElement extends BaseElement {