From 73220b86676d59322fb6ff0e6cf13ea72a666e04 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 11 Feb 2025 15:11:40 -0500 Subject: [PATCH 1/9] chore: simplify process_effects (#15270) * chore: simplify process_effects * return effects --- .../svelte/src/internal/client/runtime.js | 68 ++++++++----------- 1 file changed, 29 insertions(+), 39 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 8b0f84268c5a..d5d01c9b6db8 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -679,10 +679,7 @@ function flush_queued_root_effects(root_effects) { effect.f ^= CLEAN; } - /** @type {Effect[]} */ - var collected_effects = []; - - process_effects(effect, collected_effects); + var collected_effects = process_effects(effect); flush_queued_effects(collected_effects); } } finally { @@ -783,13 +780,14 @@ export function schedule_effect(signal) { * effects to be flushed. * * @param {Effect} effect - * @param {Effect[]} collected_effects - * @returns {void} + * @returns {Effect[]} */ -function process_effects(effect, collected_effects) { - var current_effect = effect.first; +function process_effects(effect) { + /** @type {Effect[]} */ var effects = []; + var current_effect = effect.first; + main_loop: while (current_effect !== null) { var flags = current_effect.f; var is_branch = (flags & BRANCH_EFFECT) !== 0; @@ -797,34 +795,32 @@ function process_effects(effect, collected_effects) { var sibling = current_effect.next; if (!is_skippable_branch && (flags & INERT) === 0) { - if ((flags & RENDER_EFFECT) !== 0) { - if (is_branch) { - current_effect.f ^= CLEAN; - } else { - // Ensure we set the effect to be the active reaction - // to ensure that unowned deriveds are correctly tracked - // because we're flushing the current effect - var previous_active_reaction = active_reaction; - try { - active_reaction = current_effect; - if (check_dirtiness(current_effect)) { - update_effect(current_effect); - } - } catch (error) { - handle_error(error, current_effect, null, current_effect.ctx); - } finally { - active_reaction = previous_active_reaction; + if ((flags & EFFECT) !== 0) { + effects.push(current_effect); + } else if (is_branch) { + current_effect.f ^= CLEAN; + } else { + // Ensure we set the effect to be the active reaction + // to ensure that unowned deriveds are correctly tracked + // because we're flushing the current effect + var previous_active_reaction = active_reaction; + try { + active_reaction = current_effect; + if (check_dirtiness(current_effect)) { + update_effect(current_effect); } + } catch (error) { + handle_error(error, current_effect, null, current_effect.ctx); + } finally { + active_reaction = previous_active_reaction; } + } - var child = current_effect.first; + var child = current_effect.first; - if (child !== null) { - current_effect = child; - continue; - } - } else if ((flags & EFFECT) !== 0) { - effects.push(current_effect); + if (child !== null) { + current_effect = child; + continue; } } @@ -847,13 +843,7 @@ function process_effects(effect, collected_effects) { current_effect = sibling; } - // We might be dealing with many effects here, far more than can be spread into - // an array push call (callstack overflow). So let's deal with each effect in a loop. - for (var i = 0; i < effects.length; i++) { - child = effects[i]; - collected_effects.push(child); - process_effects(child, collected_effects); - } + return effects; } /** From 85f83ec435ef0baa797f195f5b016395fc70be13 Mon Sep 17 00:00:00 2001 From: adiGuba Date: Tue, 11 Feb 2025 21:12:58 +0100 Subject: [PATCH 2/9] feat: $props.id(), a SSR-safe ID generation (#15185) * first impl of $$uid * fix * $props.id() * fix errors * rename $.create_uid() into $.props_id() * fix message * relax const requirement, validate assignments instead * oops * simplify * non-constants should be lowercased * ditto * start at 1 * add docs * changeset * add test * add docs * doc : add code example * fix type reported by bennymi --------- Co-authored-by: Rich Harris --- .changeset/hip-singers-vanish.md | 5 ++ documentation/docs/02-runes/05-$props.md | 21 +++++++ .../98-reference/.generated/compile-errors.md | 8 ++- .../svelte/messages/compile-errors/script.md | 6 +- packages/svelte/src/ambient.d.ts | 9 +++ packages/svelte/src/compiler/errors.js | 16 ++++- .../src/compiler/phases/2-analyze/index.js | 1 + .../2-analyze/visitors/CallExpression.js | 28 ++++++++- .../phases/2-analyze/visitors/shared/utils.js | 4 ++ .../3-transform/client/transform-client.js | 5 ++ .../client/visitors/VariableDeclaration.js | 5 ++ .../client/visitors/shared/utils.js | 6 ++ .../3-transform/server/transform-server.js | 7 +++ .../server/visitors/VariableDeclaration.js | 9 +++ .../svelte/src/compiler/phases/types.d.ts | 2 + .../src/internal/client/dom/template.js | 20 ++++++ packages/svelte/src/internal/client/index.js | 3 +- packages/svelte/src/internal/server/index.js | 31 ++++++++-- .../svelte/src/internal/server/types.d.ts | 2 + packages/svelte/src/utils.js | 1 + .../samples/props-id/Child.svelte | 5 ++ .../runtime-runes/samples/props-id/_config.js | 61 +++++++++++++++++++ .../samples/props-id/main.svelte | 19 ++++++ packages/svelte/types/index.d.ts | 9 +++ 24 files changed, 272 insertions(+), 11 deletions(-) create mode 100644 .changeset/hip-singers-vanish.md create mode 100644 packages/svelte/tests/runtime-runes/samples/props-id/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/props-id/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/props-id/main.svelte diff --git a/.changeset/hip-singers-vanish.md b/.changeset/hip-singers-vanish.md new file mode 100644 index 000000000000..9dce4d98a8db --- /dev/null +++ b/.changeset/hip-singers-vanish.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: SSR-safe ID generation with `$props.id()` diff --git a/documentation/docs/02-runes/05-$props.md b/documentation/docs/02-runes/05-$props.md index 4b1775bf5a61..f300fb239d77 100644 --- a/documentation/docs/02-runes/05-$props.md +++ b/documentation/docs/02-runes/05-$props.md @@ -199,3 +199,24 @@ You can, of course, separate the type declaration from the annotation: > [!NOTE] Interfaces for native DOM elements are provided in the `svelte/elements` module (see [Typing wrapper components](typescript#Typing-wrapper-components)) Adding types is recommended, as it ensures that people using your component can easily discover which props they should provide. + + +## `$props.id()` + +This rune, added in version 5.20.0, generates an ID that is unique to the current component instance. When hydrating a server-rendered component, the value will be consistent between server and client. + +This is useful for linking elements via attributes like `for` and `aria-labelledby`. + +```svelte + + +
+ + + + + +
+``` \ No newline at end of file diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 2fef3bd45d50..a4ecbb31d569 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -573,7 +573,13 @@ Unrecognised compiler option %keypath% ### props_duplicate ``` -Cannot use `$props()` more than once +Cannot use `%rune%()` more than once +``` + +### props_id_invalid_placement + +``` +`$props.id()` can only be used at the top level of components as a variable declaration initializer ``` ### props_illegal_name diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md index 0aa6fbed90d8..795c0b007dca 100644 --- a/packages/svelte/messages/compile-errors/script.md +++ b/packages/svelte/messages/compile-errors/script.md @@ -120,7 +120,11 @@ This turned out to be buggy and unpredictable, particularly when working with de ## props_duplicate -> Cannot use `$props()` more than once +> Cannot use `%rune%()` more than once + +## props_id_invalid_placement + +> `$props.id()` can only be used at the top level of components as a variable declaration initializer ## props_illegal_name diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index fbcecba8e47c..a1484718cc77 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -339,6 +339,15 @@ declare namespace $effect { declare function $props(): any; declare namespace $props { + /** + * Generates an ID that is unique to the current component instance. When hydrating a server-rendered component, + * the value will be consistent between server and client. + * + * This is useful for linking elements via attributes like `for` and `aria-labelledby`. + * @since 5.20.0 + */ + export function id(): string; + // prevent intellisense from being unhelpful /** @deprecated */ export const apply: never; diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 53a6ac6849ec..93eeee539cc3 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -279,12 +279,22 @@ export function module_illegal_default_export(node) { } /** - * Cannot use `$props()` more than once + * Cannot use `%rune%()` more than once + * @param {null | number | NodeLike} node + * @param {string} rune + * @returns {never} + */ +export function props_duplicate(node, rune) { + e(node, 'props_duplicate', `Cannot use \`${rune}()\` more than once\nhttps://svelte.dev/e/props_duplicate`); +} + +/** + * `$props.id()` can only be used at the top level of components as a variable declaration initializer * @param {null | number | NodeLike} node * @returns {never} */ -export function props_duplicate(node) { - e(node, 'props_duplicate', `Cannot use \`$props()\` more than once\nhttps://svelte.dev/e/props_duplicate`); +export function props_id_invalid_placement(node) { + e(node, 'props_id_invalid_placement', `\`$props.id()\` can only be used at the top level of components as a variable declaration initializer\nhttps://svelte.dev/e/props_id_invalid_placement`); } /** diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index ad9db24e1e65..846abcf7dfc9 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -416,6 +416,7 @@ export function analyze_component(root, source, options) { immutable: runes || options.immutable, exports: [], uses_props: false, + props_id: null, uses_rest_props: false, uses_slots: false, uses_component_bindings: false, diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 0a6b3f3ee520..ce520cc98055 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -55,7 +55,7 @@ export function CallExpression(node, context) { case '$props': if (context.state.has_props_rune) { - e.props_duplicate(node); + e.props_duplicate(node, rune); } context.state.has_props_rune = true; @@ -74,6 +74,32 @@ export function CallExpression(node, context) { break; + case '$props.id': { + const grand_parent = get_parent(context.path, -2); + + if (context.state.analysis.props_id) { + e.props_duplicate(node, rune); + } + + if ( + parent.type !== 'VariableDeclarator' || + parent.id.type !== 'Identifier' || + context.state.ast_type !== 'instance' || + context.state.scope !== context.state.analysis.instance.scope || + grand_parent.type !== 'VariableDeclaration' + ) { + e.props_id_invalid_placement(node); + } + + if (node.arguments.length > 0) { + e.rune_invalid_arguments(node, rune); + } + + context.state.analysis.props_id = parent.id; + + break; + } + case '$state': case '$state.raw': case '$derived': diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js index 5fe2a8f24ecc..2d90c85364cf 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js @@ -25,6 +25,10 @@ export function validate_assignment(node, argument, state) { e.constant_assignment(node, 'derived state'); } + if (binding?.node === state.analysis.props_id) { + e.constant_assignment(node, '$props.id()'); + } + if (binding?.kind === 'each') { e.each_item_invalid_assignment(node); } 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 f4a6c9a4147b..2e6307a4b7a6 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 @@ -562,6 +562,11 @@ export function client_component(analysis, options) { component_block.body.unshift(b.stmt(b.call('$.check_target', b.id('new.target')))); } + if (analysis.props_id) { + // need to be placed on first line of the component for hydration + component_block.body.unshift(b.const(analysis.props_id, b.call('$.props_id'))); + } + if (state.events.size > 0) { body.push( b.stmt(b.call('$.delegate', b.array(Array.from(state.events).map((name) => b.literal(name))))) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index afb90bbec7f9..31e712cdcc4d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -42,6 +42,11 @@ export function VariableDeclaration(node, context) { continue; } + if (rune === '$props.id') { + // skip + continue; + } + if (rune === '$props') { /** @type {string[]} */ const seen = ['$$slots', '$$events', '$$legacy']; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 00634f229eeb..9214a13c94ca 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -129,6 +129,12 @@ export function build_template_chunk( if (value.right.value === null) { value = { ...value, right: b.literal('') }; } + } else if ( + state.analysis.props_id && + value.type === 'Identifier' && + value.name === state.analysis.props_id.name + ) { + // do nothing ($props.id() is never null/undefined) } else { value = b.logical('??', value, b.literal('')); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 982b75e12f53..df3d831d3cc3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -244,6 +244,13 @@ export function server_component(analysis, options) { .../** @type {Statement[]} */ (template.body) ]); + if (analysis.props_id) { + // need to be placed on first line of the component for hydration + component_block.body.unshift( + b.const(analysis.props_id, b.call('$.props_id', b.id('$$payload'))) + ); + } + let should_inject_context = dev || analysis.needs_context; if (should_inject_context) { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js index 31de811ac76f..c4c31d7eb304 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js @@ -24,6 +24,11 @@ export function VariableDeclaration(node, context) { continue; } + if (rune === '$props.id') { + // skip + continue; + } + if (rune === '$props') { let has_rest = false; // remove $bindable() from props declaration @@ -156,6 +161,10 @@ export function VariableDeclaration(node, context) { } } + if (declarations.length === 0) { + return b.empty; + } + return { ...node, declarations diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index fe32dbba3e4a..abe2b115de02 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -44,6 +44,8 @@ export interface ComponentAnalysis extends Analysis { exports: Array<{ name: string; alias: string | null }>; /** Whether the component uses `$$props` */ uses_props: boolean; + /** The component ID variable name, if any */ + props_id: Identifier | null; /** Whether the component uses `$$restProps` */ uses_rest_props: boolean; /** Whether the component uses `$$slots` */ diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index bcbae393ecff..3e4f45aba862 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -249,3 +249,23 @@ export function append(anchor, dom) { anchor.before(/** @type {Node} */ (dom)); } + +let uid = 1; + +/** + * Create (or hydrate) an unique UID for the component instance. + */ +export function props_id() { + if ( + hydrating && + hydrate_node && + hydrate_node.nodeType === 8 && + hydrate_node.textContent?.startsWith('#s') + ) { + const id = hydrate_node.textContent.substring(1); + hydrate_next(); + return id; + } + + return 'c' + uid++; +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 3f3b29b029dd..8eaa5d66e1cb 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -96,7 +96,8 @@ export { mathml_template, template, template_with_script, - text + text, + props_id } from './dom/template.js'; export { derived, derived_safe_equal } from './reactivity/deriveds.js'; export { diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 89b3c33df887..c4e5d318dcd0 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -28,14 +28,15 @@ const INVALID_ATTR_NAME_CHAR_REGEX = * @param {Payload} to_copy * @returns {Payload} */ -export function copy_payload({ out, css, head }) { +export function copy_payload({ out, css, head, uid }) { return { out, css: new Set(css), head: { title: head.title, out: head.out - } + }, + uid }; } @@ -48,6 +49,7 @@ export function copy_payload({ out, css, head }) { export function assign_payload(p1, p2) { p1.out = p2.out; p1.head = p2.head; + p1.uid = p2.uid; } /** @@ -83,17 +85,27 @@ export function element(payload, tag, attributes_fn = noop, children_fn = noop) */ export let on_destroy = []; +function props_id_generator() { + let uid = 1; + return () => 's' + uid++; +} + /** * Only available on the server and when compiling with the `server` option. * Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app. * @template {Record} Props * @param {import('svelte').Component | ComponentType>} component - * @param {{ props?: Omit; context?: Map }} [options] + * @param {{ props?: Omit; context?: Map, uid?: () => string }} [options] * @returns {RenderOutput} */ export function render(component, options = {}) { /** @type {Payload} */ - const payload = { out: '', css: new Set(), head: { title: '', out: '' } }; + const payload = { + out: '', + css: new Set(), + head: { title: '', out: '' }, + uid: options.uid ?? props_id_generator() + }; const prev_on_destroy = on_destroy; on_destroy = []; @@ -526,6 +538,17 @@ export function once(get_value) { }; } +/** + * Create an unique ID + * @param {Payload} payload + * @returns {string} + */ +export function props_id(payload) { + const uid = payload.uid(); + payload.out += ''; + return uid; +} + export { attr, clsx }; export { html } from './blocks/html.js'; diff --git a/packages/svelte/src/internal/server/types.d.ts b/packages/svelte/src/internal/server/types.d.ts index e6c235147b5f..8a241deecd18 100644 --- a/packages/svelte/src/internal/server/types.d.ts +++ b/packages/svelte/src/internal/server/types.d.ts @@ -18,6 +18,8 @@ export interface Payload { title: string; out: string; }; + /** Function that generates a unique ID */ + uid: () => string; } export interface RenderOutput { diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index e8e1bc224ce4..d4d106d56deb 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -433,6 +433,7 @@ const RUNES = /** @type {const} */ ([ '$state.raw', '$state.snapshot', '$props', + '$props.id', '$bindable', '$derived', '$derived.by', diff --git a/packages/svelte/tests/runtime-runes/samples/props-id/Child.svelte b/packages/svelte/tests/runtime-runes/samples/props-id/Child.svelte new file mode 100644 index 000000000000..ad8bbd6f01ff --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-id/Child.svelte @@ -0,0 +1,5 @@ + + +

{id}

diff --git a/packages/svelte/tests/runtime-runes/samples/props-id/_config.js b/packages/svelte/tests/runtime-runes/samples/props-id/_config.js new file mode 100644 index 000000000000..9d91b98e0fa6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-id/_config.js @@ -0,0 +1,61 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target, variant }) { + if (variant === 'dom') { + assert.htmlEqual( + target.innerHTML, + ` + +

c1

+

c2

+

c3

+

c4

+ ` + ); + } else { + assert.htmlEqual( + target.innerHTML, + ` + +

s1

+

s2

+

s3

+

s4

+ ` + ); + } + + let button = target.querySelector('button'); + flushSync(() => button?.click()); + + if (variant === 'dom') { + assert.htmlEqual( + target.innerHTML, + ` + +

c1

+

c2

+

c3

+

c4

+

c5

+ ` + ); + } else { + // `c6` because this runs after the `dom` tests + // (slightly brittle but good enough for now) + assert.htmlEqual( + target.innerHTML, + ` + +

s1

+

s2

+

s3

+

s4

+

c6

+ ` + ); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/props-id/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-id/main.svelte new file mode 100644 index 000000000000..646bb2ebdefe --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-id/main.svelte @@ -0,0 +1,19 @@ + + + + +

{id}

+ + + + + +{#if show} + +{/if} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index eb3e93e4b5df..77d78477ee93 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2995,6 +2995,15 @@ declare namespace $effect { declare function $props(): any; declare namespace $props { + /** + * Generates an ID that is unique to the current component instance. When hydrating a server-rendered component, + * the value will be consistent between server and client. + * + * This is useful for linking elements via attributes like `for` and `aria-labelledby`. + * @since 5.20.0 + */ + export function id(): string; + // prevent intellisense from being unhelpful /** @deprecated */ export const apply: never; From afae274587b30bb505508e1302ae149fd9c8593c Mon Sep 17 00:00:00 2001 From: adiGuba Date: Tue, 11 Feb 2025 21:36:38 +0100 Subject: [PATCH 3/9] fix: value/checked not correctly set using spread (#15239) * set value/checked by JS * test * changeset * fix test form-default-value-spread --- .changeset/fuzzy-zoos-repeat.md | 5 +++ .../client/dom/elements/attributes.js | 13 +++++--- .../samples/attribute-spread-input/_config.js | 33 +++++++++++++++++++ .../attribute-spread-input/main.svelte | 22 +++++++++++++ 4 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 .changeset/fuzzy-zoos-repeat.md create mode 100644 packages/svelte/tests/runtime-runes/samples/attribute-spread-input/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/attribute-spread-input/main.svelte diff --git a/.changeset/fuzzy-zoos-repeat.md b/.changeset/fuzzy-zoos-repeat.md new file mode 100644 index 000000000000..3fb3f0502e57 --- /dev/null +++ b/.changeset/fuzzy-zoos-repeat.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: value/checked not correctly set using spread diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 4a0f0cea0e00..987d1f2086e3 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -399,15 +399,18 @@ export function set_attributes( if (name === 'value' || name === 'checked') { // removing value/checked also removes defaultValue/defaultChecked — preserve let input = /** @type {HTMLInputElement} */ (element); - + const use_default = prev === undefined; if (name === 'value') { - let prev = input.defaultValue; + let previous = input.defaultValue; input.removeAttribute(name); - input.defaultValue = prev; + input.defaultValue = previous; + // @ts-ignore + input.value = input.__value = use_default ? previous : null; } else { - let prev = input.defaultChecked; + let previous = input.defaultChecked; input.removeAttribute(name); - input.defaultChecked = prev; + input.defaultChecked = previous; + input.checked = use_default ? previous : false; } } else { element.removeAttribute(key); diff --git a/packages/svelte/tests/runtime-runes/samples/attribute-spread-input/_config.js b/packages/svelte/tests/runtime-runes/samples/attribute-spread-input/_config.js new file mode 100644 index 000000000000..ab941255037e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attribute-spread-input/_config.js @@ -0,0 +1,33 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ target, assert }) { + // Test for https://github.com/sveltejs/svelte/issues/15237 + const [setValues, clearValue] = target.querySelectorAll('button'); + const [text1, text2, check1, check2] = target.querySelectorAll('input'); + + assert.equal(text1.value, ''); + assert.equal(text2.value, ''); + assert.equal(check1.checked, false); + assert.equal(check2.checked, false); + + flushSync(() => { + setValues.click(); + }); + + assert.equal(text1.value, 'message'); + assert.equal(text2.value, 'message'); + assert.equal(check1.checked, true); + assert.equal(check2.checked, true); + + flushSync(() => { + clearValue.click(); + }); + + assert.equal(text1.value, ''); + assert.equal(text2.value, ''); + assert.equal(check1.checked, false); + assert.equal(check2.checked, false); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attribute-spread-input/main.svelte b/packages/svelte/tests/runtime-runes/samples/attribute-spread-input/main.svelte new file mode 100644 index 000000000000..4bb4365ee270 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attribute-spread-input/main.svelte @@ -0,0 +1,22 @@ + + + + + + + + + + From a3e49b611094559a2c4f3830dec2cc75680140ab Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 11 Feb 2025 23:07:17 +0100 Subject: [PATCH 4/9] fix: recurse into `$derived` for ownership validation (#15166) - `$derived` can contain `$state` declarations so we cannot ignore them, so this reverts #14533 - instead, we add equality checks to not do this expensive work unnecessarily - this also adds a render effect similar to the class ownership addition when it detects a getter on a POJO during ownership addition fixes #15164 --- .changeset/thick-carrots-arrive.md | 5 ++ .../3-transform/client/visitors/ClassBody.js | 29 ++++++------ .../client/visitors/shared/component.js | 41 +++++------------ .../src/internal/client/dev/ownership.js | 42 +++++++++++++++-- packages/svelte/src/internal/client/index.js | 1 + .../CounterBinding.svelte | 7 +++ .../CounterContext.svelte | 13 ++++++ .../_config.js | 34 ++++++++++++++ .../main.svelte | 46 +++++++++++++++++++ 9 files changed, 168 insertions(+), 50 deletions(-) create mode 100644 .changeset/thick-carrots-arrive.md create mode 100644 packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterBinding.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterContext.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/main.svelte diff --git a/.changeset/thick-carrots-arrive.md b/.changeset/thick-carrots-arrive.md new file mode 100644 index 000000000000..582cf5e6e15b --- /dev/null +++ b/.changeset/thick-carrots-arrive.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: recurse into `$derived` for ownership validation diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js index 7b3a9a4d0e29..ed800e5226ce 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js @@ -190,22 +190,21 @@ export function ClassBody(node, context) { 'method', b.id('$.ADD_OWNER'), [b.id('owner')], - Array.from(public_state) - // Only run ownership addition on $state fields. - // Theoretically someone could create a `$state` while creating `$state.raw` or inside a `$derived.by`, - // but that feels so much of an edge case that it doesn't warrant a perf hit for the common case. - .filter(([_, { kind }]) => kind === 'state') - .map(([name]) => - b.stmt( - b.call( - '$.add_owner', - b.call('$.get', b.member(b.this, b.private_id(name))), - b.id('owner'), - b.literal(false), - is_ignored(node, 'ownership_invalid_binding') && b.true - ) + [ + b.stmt( + b.call( + '$.add_owner_to_class', + b.this, + b.id('owner'), + b.array( + Array.from(public_state).map(([name]) => + b.thunk(b.call('$.get', b.member(b.this, b.private_id(name)))) + ) + ), + is_ignored(node, 'ownership_invalid_binding') && b.true ) - ), + ) + ], true ) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 15e4f68e9e49..2bae4486dc58 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -180,37 +180,18 @@ export function build_component(node, component_name, context, anchor = context. const expression = /** @type {Expression} */ (context.visit(attribute.expression)); if (dev && attribute.name !== 'this') { - let should_add_owner = true; - - if (attribute.expression.type !== 'SequenceExpression') { - const left = object(attribute.expression); - - if (left?.type === 'Identifier') { - const binding = context.state.scope.get(left.name); - - // Only run ownership addition on $state fields. - // Theoretically someone could create a `$state` while creating `$state.raw` or inside a `$derived.by`, - // but that feels so much of an edge case that it doesn't warrant a perf hit for the common case. - if (binding?.kind === 'derived' || binding?.kind === 'raw_state') { - should_add_owner = false; - } - } - } - - if (should_add_owner) { - binding_initializers.push( - b.stmt( - b.call( - b.id('$.add_owner_effect'), - expression.type === 'SequenceExpression' - ? expression.expressions[0] - : b.thunk(expression), - b.id(component_name), - is_ignored(node, 'ownership_invalid_binding') && b.true - ) + binding_initializers.push( + b.stmt( + b.call( + b.id('$.add_owner_effect'), + expression.type === 'SequenceExpression' + ? expression.expressions[0] + : b.thunk(expression), + b.id(component_name), + is_ignored(node, 'ownership_invalid_binding') && b.true ) - ); - } + ) + ); } if (expression.type === 'SequenceExpression') { diff --git a/packages/svelte/src/internal/client/dev/ownership.js b/packages/svelte/src/internal/client/dev/ownership.js index 2a2527803af9..62119b36dbd6 100644 --- a/packages/svelte/src/internal/client/dev/ownership.js +++ b/packages/svelte/src/internal/client/dev/ownership.js @@ -6,7 +6,7 @@ import { render_effect, user_pre_effect } from '../reactivity/effects.js'; import { dev_current_component_function } from '../context.js'; import { get_prototype_of } from '../../shared/utils.js'; import * as w from '../warnings.js'; -import { FILENAME } from '../../../constants.js'; +import { FILENAME, UNINITIALIZED } from '../../../constants.js'; /** @type {Record>} */ const boundaries = {}; @@ -140,6 +140,25 @@ export function add_owner_effect(get_object, Component, skip_warning = false) { }); } +/** + * @param {any} _this + * @param {Function} owner + * @param {Array<() => any>} getters + * @param {boolean} skip_warning + */ +export function add_owner_to_class(_this, owner, getters, skip_warning) { + _this[ADD_OWNER].current ||= getters.map(() => UNINITIALIZED); + + for (let i = 0; i < getters.length; i += 1) { + const current = getters[i](); + // For performance reasons we only re-add the owner if the state has changed + if (current !== _this[ADD_OWNER][i]) { + _this[ADD_OWNER].current[i] = current; + add_owner(current, owner, false, skip_warning); + } + } +} + /** * @param {ProxyMetadata | null} from * @param {ProxyMetadata} to @@ -196,7 +215,19 @@ function add_owner_to_object(object, owner, seen) { if (proto === Object.prototype) { // recurse until we find a state proxy for (const key in object) { - add_owner_to_object(object[key], owner, seen); + if (Object.getOwnPropertyDescriptor(object, key)?.get) { + // Similar to the class case; the getter could update with a new state + let current = UNINITIALIZED; + render_effect(() => { + const next = object[key]; + if (current !== next) { + current = next; + add_owner_to_object(next, owner, seen); + } + }); + } else { + add_owner_to_object(object[key], owner, seen); + } } } else if (proto === Array.prototype) { // recurse until we find a state proxy @@ -221,9 +252,10 @@ function has_owner(metadata, component) { return ( metadata.owners.has(component) || // This helps avoid false positives when using HMR, where the component function is replaced - [...metadata.owners].some( - (owner) => /** @type {any} */ (owner)[FILENAME] === /** @type {any} */ (component)?.[FILENAME] - ) || + (FILENAME in component && + [...metadata.owners].some( + (owner) => /** @type {any} */ (owner)[FILENAME] === component[FILENAME] + )) || (metadata.parent !== null && has_owner(metadata.parent, component)) ); } diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 8eaa5d66e1cb..0d64b60496c6 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -10,6 +10,7 @@ export { mark_module_start, mark_module_end, add_owner_effect, + add_owner_to_class, skip_ownership_validation } from './dev/ownership.js'; export { check_target, legacy_api } from './dev/legacy.js'; diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterBinding.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterBinding.svelte new file mode 100644 index 000000000000..d6da559fb176 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterBinding.svelte @@ -0,0 +1,7 @@ + + +

Binding

+ + diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterContext.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterContext.svelte new file mode 100644 index 000000000000..b935f0a472dc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterContext.svelte @@ -0,0 +1,13 @@ + + +

Context

+ + diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/_config.js new file mode 100644 index 000000000000..d6d12d01cd09 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/_config.js @@ -0,0 +1,34 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +// Tests that ownership is widened with $derived (on class or on its own) that contains $state +export default test({ + compileOptions: { + dev: true + }, + + test({ assert, target, warnings }) { + const [root, counter_context1, counter_context2, counter_binding1, counter_binding2] = + target.querySelectorAll('button'); + + counter_context1.click(); + counter_context2.click(); + counter_binding1.click(); + counter_binding2.click(); + flushSync(); + + assert.equal(warnings.length, 0); + + root.click(); + flushSync(); + counter_context1.click(); + counter_context2.click(); + counter_binding1.click(); + counter_binding2.click(); + flushSync(); + + assert.equal(warnings.length, 0); + }, + + warnings: [] +}); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/main.svelte new file mode 100644 index 000000000000..aaade26e162c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/main.svelte @@ -0,0 +1,46 @@ + + +

Parent

+ + + + From 5fe027286801043050b795169082ed5251e31dd1 Mon Sep 17 00:00:00 2001 From: Kid <44045911+kidonng@users.noreply.github.com> Date: Wed, 12 Feb 2025 17:02:40 +0800 Subject: [PATCH 5/9] docs: remove duplicate `onDestroy` description (#15274) --- documentation/docs/06-runtime/03-lifecycle-hooks.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/documentation/docs/06-runtime/03-lifecycle-hooks.md b/documentation/docs/06-runtime/03-lifecycle-hooks.md index a3dbe04b0029..2b97ca796fed 100644 --- a/documentation/docs/06-runtime/03-lifecycle-hooks.md +++ b/documentation/docs/06-runtime/03-lifecycle-hooks.md @@ -45,8 +45,6 @@ If a function is returned from `onMount`, it will be called when the component i ## `onDestroy` -> EXPORT_SNIPPET: svelte#onDestroy - Schedules a callback to run immediately before the component is unmounted. Out of `onMount`, `beforeUpdate`, `afterUpdate` and `onDestroy`, this is the only one that runs inside a server-side component. From 18481386f3667a13007b5dc40cb40a95382a696b Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Wed, 12 Feb 2025 14:05:25 +0100 Subject: [PATCH 6/9] fix: use `importNode` to clone templates for Firefox (#15272) * fix: use `importNode` to clone templates for Firefox * fix: move `is_firefox` check to line 28 * fix: revert using `is_firefox` too soon --- .changeset/slow-meals-wait.md | 5 ++++ .../phases/2-analyze/visitors/Attribute.js | 5 ---- .../client/visitors/RegularElement.js | 5 ---- .../client/dom/elements/attributes.js | 25 ------------------- .../src/internal/client/dom/operations.js | 4 +++ .../src/internal/client/dom/template.js | 4 +-- packages/svelte/src/internal/client/index.js | 1 - .../svelte/src/internal/client/runtime.js | 3 ++- .../_expected/client/index.svelte.js | 6 +---- 9 files changed, 14 insertions(+), 44 deletions(-) create mode 100644 .changeset/slow-meals-wait.md diff --git a/.changeset/slow-meals-wait.md b/.changeset/slow-meals-wait.md new file mode 100644 index 000000000000..e1408e384929 --- /dev/null +++ b/.changeset/slow-meals-wait.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: use `importNode` to clone templates for Firefox diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 41144fc74c5c..42e449896928 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -23,11 +23,6 @@ export function Attribute(node, context) { if (node.name === 'value' && parent.name === 'option') { mark_subtree_dynamic(context.path); } - - // special case - if (node.name === 'loading' && parent.name === 'img') { - mark_subtree_dynamic(context.path); - } } if (is_event_attribute(node)) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index c7e218d52143..98036aa9b609 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -300,11 +300,6 @@ export function RegularElement(node, context) { build_class_directives(class_directives, node_id, context, is_attributes_reactive); build_style_directives(style_directives, node_id, context, is_attributes_reactive); - // Apply the src and loading attributes for elements after the element is appended to the document - if (node.name === 'img' && (has_spread || lookup.has('loading'))) { - context.state.after_update.push(b.stmt(b.call('$.handle_lazy_img', node_id))); - } - if ( is_load_error_element(node.name) && (has_spread || has_use || lookup.has('onload') || lookup.has('onerror')) diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 987d1f2086e3..2dba2d797a4a 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -523,28 +523,3 @@ function srcset_url_equal(element, srcset) { ) ); } - -/** - * @param {HTMLImageElement} element - * @returns {void} - */ -export function handle_lazy_img(element) { - // If we're using an image that has a lazy loading attribute, we need to apply - // the loading and src after the img element has been appended to the document. - // Otherwise the lazy behaviour will not work due to our cloneNode heuristic for - // templates. - if (!hydrating && element.loading === 'lazy') { - var src = element.src; - // @ts-expect-error - element[LOADING_ATTR_SYMBOL] = null; - element.loading = 'eager'; - element.removeAttribute('src'); - requestAnimationFrame(() => { - // @ts-expect-error - if (element[LOADING_ATTR_SYMBOL] !== 'eager') { - element.loading = 'lazy'; - } - element.src = src; - }); - } -} diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index 627bf917eee1..83565d17ae68 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -11,6 +11,9 @@ export var $window; /** @type {Document} */ export var $document; +/** @type {boolean} */ +export var is_firefox; + /** @type {() => Node | null} */ var first_child_getter; /** @type {() => Node | null} */ @@ -27,6 +30,7 @@ export function init_operations() { $window = window; $document = document; + is_firefox = /Firefox/.test(navigator.userAgent); var element_prototype = Element.prototype; var node_prototype = Node.prototype; diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 3e4f45aba862..6ff3b0fa19a0 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -1,6 +1,6 @@ /** @import { Effect, TemplateNode } from '#client' */ import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js'; -import { create_text, get_first_child } from './operations.js'; +import { create_text, get_first_child, is_firefox } from './operations.js'; import { create_fragment_from_html } from './reconciler.js'; import { active_effect } from '../runtime.js'; import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js'; @@ -48,7 +48,7 @@ export function template(content, flags) { } var clone = /** @type {TemplateNode} */ ( - use_import_node ? document.importNode(node, true) : node.cloneNode(true) + use_import_node || is_firefox ? document.importNode(node, true) : node.cloneNode(true) ); if (is_fragment) { diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 0d64b60496c6..d78f6d452e84 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -35,7 +35,6 @@ export { set_attributes, set_custom_element_data, set_xlink_attribute, - handle_lazy_img, set_value, set_checked, set_selected, diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index d5d01c9b6db8..75d45d9db9fd 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -39,6 +39,7 @@ import { set_component_context, set_dev_current_component_function } from './context.js'; +import { is_firefox } from './dom/operations.js'; const FLUSH_MICROTASK = 0; const FLUSH_SYNC = 1; @@ -333,7 +334,7 @@ export function handle_error(error, effect, previous_effect, component_context) current_context = current_context.p; } - const indent = /Firefox/.test(navigator.userAgent) ? ' ' : '\t'; + const indent = is_firefox ? ' ' : '\t'; define_property(error, 'message', { value: error.message + `\n${component_stack.map((name) => `\n${indent}in ${name}`).join('')}\n` }); diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js index 9b203b97e82d..46d376aca2f9 100644 --- a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client/index.svelte.js @@ -42,12 +42,8 @@ export default function Skip_static_subtree($$anchor, $$props) { $.reset(select); var img = $.sibling(select, 2); - var div_2 = $.sibling(img, 2); - var img_1 = $.child(div_2); - $.reset(div_2); + $.next(2); $.template_effect(() => $.set_text(text, $$props.title)); - $.handle_lazy_img(img); - $.handle_lazy_img(img_1); $.append($$anchor, fragment); } \ No newline at end of file From 5e52825d608cd99e1bd1b7b278202ee3b8d65740 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Wed, 12 Feb 2025 17:53:15 +0100 Subject: [PATCH 7/9] fix: take private and public into account for `constant_assignment` of derived state (#15276) Fixes #15273 --- .changeset/five-apes-develop.md | 5 +++++ .../src/compiler/phases/2-analyze/types.d.ts | 2 +- .../phases/2-analyze/visitors/ClassBody.js | 7 +++++-- .../phases/2-analyze/visitors/shared/utils.js | 14 ++++++++++---- .../errors.json | 1 + .../input.svelte | 13 +++++++++++++ 6 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 .changeset/five-apes-develop.md create mode 100644 packages/svelte/tests/validator/samples/reassign-derived-private-public-field/errors.json create mode 100644 packages/svelte/tests/validator/samples/reassign-derived-private-public-field/input.svelte diff --git a/.changeset/five-apes-develop.md b/.changeset/five-apes-develop.md new file mode 100644 index 000000000000..68e13c94a9e9 --- /dev/null +++ b/.changeset/five-apes-develop.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: take private and public into account for `constant_assignment` of derived state diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index 14b14f9c84c1..70796a0d59b5 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -19,7 +19,7 @@ export interface AnalysisState { component_slots: Set; /** Information about the current expression/directive/block value */ expression: ExpressionMetadata | null; - derived_state: string[]; + derived_state: { name: string; private: boolean }[]; function_depth: number; // legacy stuff diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js index ed397258f804..0463e4da8563 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js @@ -7,7 +7,7 @@ import { get_rune } from '../../scope.js'; * @param {Context} context */ export function ClassBody(node, context) { - /** @type {string[]} */ + /** @type {{name: string, private: boolean}[]} */ const derived_state = []; for (const definition of node.body) { @@ -18,7 +18,10 @@ export function ClassBody(node, context) { ) { const rune = get_rune(definition.value, context.state.scope); if (rune === '$derived' || rune === '$derived.by') { - derived_state.push(definition.key.name); + derived_state.push({ + name: definition.key.name, + private: definition.key.type === 'PrivateIdentifier' + }); } } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js index 2d90c85364cf..1507123e1342 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js @@ -1,4 +1,4 @@ -/** @import { AssignmentExpression, Expression, Literal, Node, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */ +/** @import { AssignmentExpression, Expression, Identifier, Literal, Node, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */ /** @import { AST, Binding } from '#compiler' */ /** @import { AnalysisState, Context } from '../../types' */ /** @import { Scope } from '../../../scope' */ @@ -38,16 +38,22 @@ export function validate_assignment(node, argument, state) { e.snippet_parameter_assignment(node); } } - if ( argument.type === 'MemberExpression' && argument.object.type === 'ThisExpression' && (((argument.property.type === 'PrivateIdentifier' || argument.property.type === 'Identifier') && - state.derived_state.includes(argument.property.name)) || + state.derived_state.some( + (derived) => + derived.name === /** @type {PrivateIdentifier | Identifier} */ (argument.property).name && + derived.private === (argument.property.type === 'PrivateIdentifier') + )) || (argument.property.type === 'Literal' && argument.property.value && typeof argument.property.value === 'string' && - state.derived_state.includes(argument.property.value))) + state.derived_state.some( + (derived) => + derived.name === /** @type {Literal} */ (argument.property).value && !derived.private + ))) ) { e.constant_assignment(node, 'derived state'); } diff --git a/packages/svelte/tests/validator/samples/reassign-derived-private-public-field/errors.json b/packages/svelte/tests/validator/samples/reassign-derived-private-public-field/errors.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/packages/svelte/tests/validator/samples/reassign-derived-private-public-field/errors.json @@ -0,0 +1 @@ +[] diff --git a/packages/svelte/tests/validator/samples/reassign-derived-private-public-field/input.svelte b/packages/svelte/tests/validator/samples/reassign-derived-private-public-field/input.svelte new file mode 100644 index 000000000000..92f356492042 --- /dev/null +++ b/packages/svelte/tests/validator/samples/reassign-derived-private-public-field/input.svelte @@ -0,0 +1,13 @@ + \ No newline at end of file From f747c412f49a59be0873c6f875cf90faced737d3 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 12 Feb 2025 16:55:04 +0000 Subject: [PATCH 8/9] chore: tweak effect self invalidation logic (#15275) Also make sure events dispatched via transition/animation logic runs outside event context Related to #15262 --- .changeset/rare-hounds-wave.md | 5 +++++ .../internal/client/dom/elements/transitions.js | 5 ++++- packages/svelte/src/internal/client/runtime.js | 14 ++++++-------- 3 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 .changeset/rare-hounds-wave.md diff --git a/.changeset/rare-hounds-wave.md b/.changeset/rare-hounds-wave.md new file mode 100644 index 000000000000..11b368b038ae --- /dev/null +++ b/.changeset/rare-hounds-wave.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: tweak effect self invalidation logic, run transition dispatches without reactive context diff --git a/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js index b3c16cdd080f..fbc1da95df95 100644 --- a/packages/svelte/src/internal/client/dom/elements/transitions.js +++ b/packages/svelte/src/internal/client/dom/elements/transitions.js @@ -14,6 +14,7 @@ import { current_each_item } from '../blocks/each.js'; import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js'; import { BLOCK_EFFECT, EFFECT_RAN, EFFECT_TRANSPARENT } from '../../constants.js'; import { queue_micro_task } from '../task.js'; +import { without_reactive_context } from './bindings/shared.js'; /** * @param {Element} element @@ -21,7 +22,9 @@ import { queue_micro_task } from '../task.js'; * @returns {void} */ function dispatch_event(element, type) { - element.dispatchEvent(new CustomEvent(type)); + without_reactive_context(() => { + element.dispatchEvent(new CustomEvent(type)); + }); } /** diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 75d45d9db9fd..8a9ca9065b47 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -370,22 +370,18 @@ export function handle_error(error, effect, previous_effect, component_context) /** * @param {Value} signal * @param {Effect} effect - * @param {number} [depth] + * @param {boolean} [root] */ -function schedule_possible_effect_self_invalidation(signal, effect, depth = 0) { +function schedule_possible_effect_self_invalidation(signal, effect, root = true) { var reactions = signal.reactions; if (reactions === null) return; for (var i = 0; i < reactions.length; i++) { var reaction = reactions[i]; if ((reaction.f & DERIVED) !== 0) { - schedule_possible_effect_self_invalidation( - /** @type {Derived} */ (reaction), - effect, - depth + 1 - ); + schedule_possible_effect_self_invalidation(/** @type {Derived} */ (reaction), effect, false); } else if (effect === reaction) { - if (depth === 0) { + if (root) { set_signal_status(reaction, DIRTY); } else if ((reaction.f & CLEAN) !== 0) { set_signal_status(reaction, MAYBE_DIRTY); @@ -458,6 +454,8 @@ export function update_reaction(reaction) { if ( is_runes() && untracked_writes !== null && + !untracking && + deps !== null && (reaction.f & (DERIVED | MAYBE_DIRTY | DIRTY)) === 0 ) { for (i = 0; i < /** @type {Source[]} */ (untracked_writes).length; i++) { From 23ecc364da255444208d5b08a56c44f7c3f2dcc1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 12 Feb 2025 17:56:15 +0100 Subject: [PATCH 9/9] Version Packages (#15271) Co-authored-by: github-actions[bot] --- .changeset/five-apes-develop.md | 5 ----- .changeset/fuzzy-zoos-repeat.md | 5 ----- .changeset/hip-singers-vanish.md | 5 ----- .changeset/rare-hounds-wave.md | 5 ----- .changeset/slow-meals-wait.md | 5 ----- .changeset/thick-carrots-arrive.md | 5 ----- packages/svelte/CHANGELOG.md | 18 ++++++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 9 files changed, 20 insertions(+), 32 deletions(-) delete mode 100644 .changeset/five-apes-develop.md delete mode 100644 .changeset/fuzzy-zoos-repeat.md delete mode 100644 .changeset/hip-singers-vanish.md delete mode 100644 .changeset/rare-hounds-wave.md delete mode 100644 .changeset/slow-meals-wait.md delete mode 100644 .changeset/thick-carrots-arrive.md diff --git a/.changeset/five-apes-develop.md b/.changeset/five-apes-develop.md deleted file mode 100644 index 68e13c94a9e9..000000000000 --- a/.changeset/five-apes-develop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: take private and public into account for `constant_assignment` of derived state diff --git a/.changeset/fuzzy-zoos-repeat.md b/.changeset/fuzzy-zoos-repeat.md deleted file mode 100644 index 3fb3f0502e57..000000000000 --- a/.changeset/fuzzy-zoos-repeat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: value/checked not correctly set using spread diff --git a/.changeset/hip-singers-vanish.md b/.changeset/hip-singers-vanish.md deleted file mode 100644 index 9dce4d98a8db..000000000000 --- a/.changeset/hip-singers-vanish.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: SSR-safe ID generation with `$props.id()` diff --git a/.changeset/rare-hounds-wave.md b/.changeset/rare-hounds-wave.md deleted file mode 100644 index 11b368b038ae..000000000000 --- a/.changeset/rare-hounds-wave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: tweak effect self invalidation logic, run transition dispatches without reactive context diff --git a/.changeset/slow-meals-wait.md b/.changeset/slow-meals-wait.md deleted file mode 100644 index e1408e384929..000000000000 --- a/.changeset/slow-meals-wait.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: use `importNode` to clone templates for Firefox diff --git a/.changeset/thick-carrots-arrive.md b/.changeset/thick-carrots-arrive.md deleted file mode 100644 index 582cf5e6e15b..000000000000 --- a/.changeset/thick-carrots-arrive.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: recurse into `$derived` for ownership validation diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index e112bf620991..ff3b08fe7a3f 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,23 @@ # svelte +## 5.20.0 + +### Minor Changes + +- feat: SSR-safe ID generation with `$props.id()` ([#15185](https://github.com/sveltejs/svelte/pull/15185)) + +### Patch Changes + +- fix: take private and public into account for `constant_assignment` of derived state ([#15276](https://github.com/sveltejs/svelte/pull/15276)) + +- fix: value/checked not correctly set using spread ([#15239](https://github.com/sveltejs/svelte/pull/15239)) + +- chore: tweak effect self invalidation logic, run transition dispatches without reactive context ([#15275](https://github.com/sveltejs/svelte/pull/15275)) + +- fix: use `importNode` to clone templates for Firefox ([#15272](https://github.com/sveltejs/svelte/pull/15272)) + +- fix: recurse into `$derived` for ownership validation ([#15166](https://github.com/sveltejs/svelte/pull/15166)) + ## 5.19.10 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index a4594b2a5c7f..bea1efd7b425 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.19.10", + "version": "5.20.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index ada6f9019d7e..b24607615685 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.19.10'; +export const VERSION = '5.20.0'; export const PUBLIC_VERSION = '5'; pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy