From 63cbe2108ac35204ffc99d16e5a520502ec7ade7 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 19 Jul 2025 11:35:43 -0700 Subject: [PATCH 01/90] fix: disallow `export { foo as default }` in ` \ No newline at end of file From ad0b58ee9606dd89038b055536e09c2b01112a0a Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 19 Jul 2025 14:21:25 -0700 Subject: [PATCH 02/90] fix: move ownership validation into async component body (#16449) * fix: move ownership validation into async component body * add test --------- Co-authored-by: Rich Harris --- .changeset/healthy-carpets-deny.md | 5 +++++ .../3-transform/client/transform-client.js | 12 +++++----- .../async-ownership-validation/Child.svelte | 7 ++++++ .../async-ownership-validation/_config.js | 22 +++++++++++++++++++ .../async-ownership-validation/main.svelte | 13 +++++++++++ 5 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 .changeset/healthy-carpets-deny.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-ownership-validation/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-ownership-validation/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-ownership-validation/main.svelte diff --git a/.changeset/healthy-carpets-deny.md b/.changeset/healthy-carpets-deny.md new file mode 100644 index 0000000000..94ee865fe7 --- /dev/null +++ b/.changeset/healthy-carpets-deny.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: move ownership validation into async component body 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 c42d1b95d8..7f25e6c0d2 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 @@ -364,6 +364,12 @@ export function client_component(analysis, options) { : b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)) ]); + if (analysis.needs_mutation_validation) { + component_block.body.unshift( + b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props'))) + ); + } + const should_inject_context = dev || analysis.needs_context || @@ -434,12 +440,6 @@ export function client_component(analysis, options) { ); } - if (analysis.needs_mutation_validation) { - component_block.body.unshift( - b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props'))) - ); - } - // we want the cleanup function for the stores to run as the very last thing // so that it can effectively clean up the store subscription even after the user effects runs if (should_inject_context) { diff --git a/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/Child.svelte new file mode 100644 index 0000000000..d4d5cf7554 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/Child.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/_config.js b/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/_config.js new file mode 100644 index 0000000000..167eee8488 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/_config.js @@ -0,0 +1,22 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + + async test({ assert, target, warnings }) { + await tick(); + + const [button] = target.querySelectorAll('button'); + + button.click(); + await tick(); + + assert.htmlEqual(target.innerHTML, ''); + assert.deepEqual(warnings, [ + 'Mutating unbound props (`object`, at Child.svelte:7:23) is strongly discouraged. Consider using `bind:object={...}` in main.svelte (or using a callback) instead' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/main.svelte new file mode 100644 index 0000000000..ae6b43cbb1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-ownership-validation/main.svelte @@ -0,0 +1,13 @@ + + + + + + {#snippet pending()} +

loading...

+ {/snippet} +
From 307ec228ffc70502b6c13c56c8e62f1abebeb73b Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 19 Jul 2025 14:24:47 -0700 Subject: [PATCH 03/90] fix: move store setup/cleanup outside of async component body (#16443) --- .changeset/new-fireants-bake.md | 5 +++++ .../3-transform/client/transform-client.js | 21 ++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 .changeset/new-fireants-bake.md diff --git a/.changeset/new-fireants-bake.md b/.changeset/new-fireants-bake.md new file mode 100644 index 0000000000..81bd596e43 --- /dev/null +++ b/.changeset/new-fireants-bake.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: move store setup/cleanup outside of async component body 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 7f25e6c0d2..124438a9da 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 @@ -209,7 +209,8 @@ export function client_component(analysis, options) { /** @type {ESTree.Statement[]} */ const store_setup = []; - + /** @type {ESTree.Statement} */ + let store_init = b.empty; /** @type {ESTree.VariableDeclaration[]} */ const legacy_reactive_declarations = []; @@ -227,8 +228,9 @@ export function client_component(analysis, options) { if (binding.kind === 'store_sub') { if (store_setup.length === 0) { needs_store_cleanup = true; - store_setup.push( - b.const(b.array_pattern([b.id('$$stores'), b.id('$$cleanup')]), b.call('$.setup_stores')) + store_init = b.const( + b.array_pattern([b.id('$$stores'), b.id('$$cleanup')]), + b.call('$.setup_stores') ); } @@ -385,9 +387,16 @@ export function client_component(analysis, options) { analysis.slot_names.size > 0; if (analysis.instance.has_await) { + const params = [b.id('$$anchor')]; + if (should_inject_props) { + params.push(b.id('$$props')); + } + if (store_setup.length > 0) { + params.push(b.id('$$stores')); + } const body = b.function_declaration( b.id('$$body'), - should_inject_props ? [b.id('$$anchor'), b.id('$$props')] : [b.id('$$anchor')], + params, b.block([ b.var('$$unsuspend', b.call('$.suspend')), ...component_block.body, @@ -403,10 +412,12 @@ export function client_component(analysis, options) { component_block = b.block([ b.var('fragment', b.call('$.comment')), b.var('node', b.call('$.first_child', b.id('fragment'))), - b.stmt(b.call(body.id, b.id('node'), should_inject_props && b.id('$$props'))), + store_init, + b.stmt(b.call(body.id, b.id('node'), ...params.slice(1))), b.stmt(b.call('$.append', b.id('$$anchor'), b.id('fragment'))) ]); } else { + component_block.body.unshift(store_init); component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body)); } From e1e7a75d719ab6826238ddaef4571ac6f6d22732 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sat, 19 Jul 2025 14:26:31 -0700 Subject: [PATCH 04/90] fix: allow async destructured deriveds (#16444) * fix: allow async destructured deriveds * add test * tweak * tweak --------- Co-authored-by: Rich Harris --- .changeset/mighty-balloons-rush.md | 5 ++ .../client/visitors/VariableDeclaration.js | 55 ++++++++++++++----- .../async-derived-destructured/Child.svelte | 13 +++++ .../async-derived-destructured/_config.js | 31 +++++++++++ .../async-derived-destructured/main.svelte | 11 ++++ svelte.config.js | 8 +++ 6 files changed, 108 insertions(+), 15 deletions(-) create mode 100644 .changeset/mighty-balloons-rush.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-destructured/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-derived-destructured/main.svelte create mode 100644 svelte.config.js diff --git a/.changeset/mighty-balloons-rush.md b/.changeset/mighty-balloons-rush.md new file mode 100644 index 0000000000..ce8c09ca55 --- /dev/null +++ b/.changeset/mighty-balloons-rush.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow async destructured deriveds 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 19a7de5715..0998dc4778 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 @@ -171,11 +171,14 @@ export function VariableDeclaration(node, context) { context.state.transform[id.name] = { read: get_value }; const expression = /** @type {Expression} */ (context.visit(b.thunk(value))); - const call = b.call('$.derived', expression); - return b.declarator( - id, - dev ? b.call('$.tag', call, b.literal('[$state iterable]')) : call - ); + let call = b.call('$.derived', expression); + + if (dev) { + const label = `[$state ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`; + call = b.call('$.tag', call, b.literal(label)); + } + + return b.declarator(id, call); }), ...paths.map((path) => { const value = /** @type {Expression} */ (context.visit(path.expression)); @@ -228,19 +231,37 @@ export function VariableDeclaration(node, context) { } } else { const init = /** @type {CallExpression} */ (declarator.init); + let expression = /** @type {Expression} */ ( + context.visit(value, { + ...context.state, + in_derived: rune === '$derived' + }) + ); let rhs = value; if (rune !== '$derived' || init.arguments[0].type !== 'Identifier') { const id = b.id(context.state.scope.generate('$$d')); + let call = b.call('$.derived', rune === '$derived' ? b.thunk(expression) : expression); + rhs = b.call('$.get', id); - let expression = /** @type {Expression} */ (context.visit(value)); - if (rune === '$derived') expression = b.thunk(expression); - const call = b.call('$.derived', expression); - declarations.push( - b.declarator(id, dev ? b.call('$.tag', call, b.literal('[$derived iterable]')) : call) - ); + if (is_async) { + const location = dev && !is_ignored(init, 'await_waterfall') && locate_node(init); + call = b.call( + '$.async_derived', + b.thunk(expression, true), + location ? b.literal(location) : undefined + ); + call = b.call(b.await(b.call('$.save', call))); + } + + if (dev) { + const label = `[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`; + call = b.call('$.tag', call, b.literal(label)); + } + + declarations.push(b.declarator(id, call)); } const { inserts, paths } = extract_paths(declarator.id, rhs); @@ -250,10 +271,14 @@ export function VariableDeclaration(node, context) { context.state.transform[id.name] = { read: get_value }; const expression = /** @type {Expression} */ (context.visit(b.thunk(value))); - const call = b.call('$.derived', expression); - declarations.push( - b.declarator(id, dev ? b.call('$.tag', call, b.literal('[$derived iterable]')) : call) - ); + let call = b.call('$.derived', expression); + + if (dev) { + const label = `[$derived ${declarator.id.type === 'ArrayPattern' ? 'iterable' : 'object'}]`; + call = b.call('$.tag', call, b.literal(label)); + } + + declarations.push(b.declarator(id, call)); } for (const path of paths) { diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte new file mode 100644 index 0000000000..39112b12a7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/Child.svelte @@ -0,0 +1,13 @@ + + + + +

{count} ** 2 = {squared}

+

{count} ** 3 = {cubed}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/_config.js b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/_config.js new file mode 100644 index 0000000000..d444e8e1d9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/_config.js @@ -0,0 +1,31 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + + const [increment] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + +

1 ** 2 = 1

+

1 ** 3 = 1

+ ` + ); + + increment.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + +

2 ** 2 = 4

+

2 ** 3 = 8

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/main.svelte new file mode 100644 index 0000000000..c5d8a12a78 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-derived-destructured/main.svelte @@ -0,0 +1,11 @@ + + + + + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000000..442cd7892c --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,8 @@ +// we need this so the VS Code extension doesn't yell at us +export default { + compilerOptions: { + experimental: { + async: true + } + } +}; From cc05cbcf5ccd1b19a42417dd374471c4cecb7776 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 19 Jul 2025 14:30:38 -0700 Subject: [PATCH 05/90] Version Packages (#16445) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/clever-toys-decide.md | 5 ----- .changeset/curly-buttons-burn.md | 5 ----- .changeset/healthy-carpets-deny.md | 5 ----- .changeset/mighty-balloons-rush.md | 5 ----- .changeset/new-fireants-bake.md | 5 ----- packages/svelte/CHANGELOG.md | 14 ++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 8 files changed, 16 insertions(+), 27 deletions(-) delete mode 100644 .changeset/clever-toys-decide.md delete mode 100644 .changeset/curly-buttons-burn.md delete mode 100644 .changeset/healthy-carpets-deny.md delete mode 100644 .changeset/mighty-balloons-rush.md delete mode 100644 .changeset/new-fireants-bake.md diff --git a/.changeset/clever-toys-decide.md b/.changeset/clever-toys-decide.md deleted file mode 100644 index 57eb2b0058..0000000000 --- a/.changeset/clever-toys-decide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't reexecute derived with no dependencies on teardown diff --git a/.changeset/curly-buttons-burn.md b/.changeset/curly-buttons-burn.md deleted file mode 100644 index f5d45256fa..0000000000 --- a/.changeset/curly-buttons-burn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: disallow `export { foo as default }` in ` + + From 0a2a2e213ab183dc5b038e4fa4eb4913dd7761a4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Jul 2025 22:17:14 -0400 Subject: [PATCH 09/90] fix: add labels to `@const` tags and props (#16454) * fix: add labels to `@const` tags and props * changeset --- .changeset/four-spiders-type.md | 5 +++++ .../3-transform/client/visitors/ConstTag.js | 16 ++++++++++++++-- .../src/internal/client/reactivity/props.js | 4 ++++ 3 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 .changeset/four-spiders-type.md diff --git a/.changeset/four-spiders-type.md b/.changeset/four-spiders-type.md new file mode 100644 index 0000000000..9a4056c50a --- /dev/null +++ b/.changeset/four-spiders-type.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add labels to `@const` tags and props diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js index c1be1e3220..34acdd6bb9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js @@ -17,7 +17,13 @@ export function ConstTag(node, context) { // TODO we can almost certainly share some code with $derived(...) if (declaration.id.type === 'Identifier') { const init = build_expression(context, declaration.init, node.metadata.expression); - context.state.init.push(b.const(declaration.id, create_derived(context.state, b.thunk(init)))); + let expression = create_derived(context.state, b.thunk(init)); + + if (dev) { + expression = b.call('$.tag', expression, b.literal(declaration.id.name)); + } + + context.state.init.push(b.const(declaration.id, expression)); context.state.transform[declaration.id.name] = { read: get_value }; @@ -55,7 +61,13 @@ export function ConstTag(node, context) { ]) ); - context.state.init.push(b.const(tmp, create_derived(context.state, fn))); + let expression = create_derived(context.state, fn); + + if (dev) { + expression = b.call('$.tag', expression, b.literal('[@const]')); + } + + context.state.init.push(b.const(tmp, expression)); // we need to eagerly evaluate the expression in order to hit any // 'Cannot access x before initialization' errors diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index f39d45bb04..28bfa88d96 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -393,6 +393,10 @@ export function prop(props, key, flags, fallback) { return getter(); }); + if (DEV) { + d.label = key; + } + // Capture the initial value if it's bindable if (bindable) get(d); From 53fdd0f93abee0dc89418c7338363e9b298d0900 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 19 Jul 2025 22:18:50 -0400 Subject: [PATCH 10/90] chore: remove some unused code (#16455) --- packages/svelte/src/internal/client/reactivity/props.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 28bfa88d96..05b747a1c4 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -1,13 +1,11 @@ -/** @import { ComponentContext } from '#client' */ -/** @import { Derived, Effect, Source } from './types.js' */ +/** @import { Effect, Source } from './types.js' */ import { DEV } from 'esm-env'; import { PROPS_IS_BINDABLE, PROPS_IS_IMMUTABLE, PROPS_IS_LAZY_INITIAL, PROPS_IS_RUNES, - PROPS_IS_UPDATED, - UNINITIALIZED + PROPS_IS_UPDATED } from '../../../constants.js'; import { get_descriptor, is_function } from '../../shared/utils.js'; import { set, source, update } from './sources.js'; From d3a01bd5a7d09384e29a599834d9b554a7a973ee Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 20 Jul 2025 17:19:56 -0400 Subject: [PATCH 11/90] fix: always mark reactions of deriveds (#16457) * tweak * fix * test * changeset --- .changeset/angry-hornets-hug.md | 5 ++ .../internal/client/reactivity/deriveds.js | 4 +- .../src/internal/client/reactivity/sources.js | 19 +++---- .../async-time-travelling-derived/_config.js | 56 +++++++++++++++++++ .../async-time-travelling-derived/main.svelte | 26 +++++++++ 5 files changed, 98 insertions(+), 12 deletions(-) create mode 100644 .changeset/angry-hornets-hug.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/main.svelte diff --git a/.changeset/angry-hornets-hug.md b/.changeset/angry-hornets-hug.md new file mode 100644 index 0000000000..ffe59db100 --- /dev/null +++ b/.changeset/angry-hornets-hug.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: always mark reactions of deriveds diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index fa6a9e02a1..7f730e365e 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -337,7 +337,9 @@ export function update_derived(derived) { // don't mark derived clean if we're reading it inside a // cleanup function, or it will cache a stale value - if (is_destroying_effect) return; + if (is_destroying_effect) { + return; + } if (batch_deriveds !== null) { batch_deriveds.set(derived, derived.v); diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 9b534d2d71..f6b14f3360 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -314,9 +314,6 @@ function mark_reactions(signal, status) { var reaction = reactions[i]; var flags = reaction.f; - // Skip any effects that are already dirty - if ((flags & DIRTY) !== 0) continue; - // In legacy mode, skip the current effect to prevent infinite loops if (!runes && reaction === active_effect) continue; @@ -326,15 +323,15 @@ function mark_reactions(signal, status) { continue; } - set_signal_status(reaction, status); + // don't set a DIRTY reaction to MAYBE_DIRTY + if ((flags & DIRTY) === 0) { + set_signal_status(reaction, status); + } - // If the signal a) was previously clean or b) is an unowned derived, then mark it - if ((flags & (CLEAN | UNOWNED)) !== 0) { - if ((flags & DERIVED) !== 0) { - mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); - } else { - schedule_effect(/** @type {Effect} */ (reaction)); - } + if ((flags & DERIVED) !== 0) { + mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); + } else if ((flags & DIRTY) === 0) { + schedule_effect(/** @type {Effect} */ (reaction)); } } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/_config.js new file mode 100644 index 0000000000..06437d2e31 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/_config.js @@ -0,0 +1,56 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + + const [a, b, update] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

a

+ ` + ); + + b.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

b

+ ` + ); + + update.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

b

+ ` + ); + + a.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +

a

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/main.svelte new file mode 100644 index 0000000000..5fca286a78 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-time-travelling-derived/main.svelte @@ -0,0 +1,26 @@ + + + + + + + + + {#if condition} +

a

+ {:else} +

b

+ {/if} + + {#snippet pending()}{/snippet} +
+ From 1f99914a16e03209cb7c0aac8ce9d3d72bb32f15 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 20 Jul 2025 17:22:01 -0400 Subject: [PATCH 12/90] chore: move `capture_signals` to legacy module (#16456) --- .changeset/swift-cherries-know.md | 5 ++ .../svelte/src/internal/client/dev/tracing.js | 2 +- packages/svelte/src/internal/client/index.js | 2 +- packages/svelte/src/internal/client/legacy.js | 46 ++++++++++++++++ .../svelte/src/internal/client/runtime.js | 54 ++----------------- 5 files changed, 56 insertions(+), 53 deletions(-) create mode 100644 .changeset/swift-cherries-know.md create mode 100644 packages/svelte/src/internal/client/legacy.js diff --git a/.changeset/swift-cherries-know.md b/.changeset/swift-cherries-know.md new file mode 100644 index 0000000000..d8bbb1256a --- /dev/null +++ b/.changeset/swift-cherries-know.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: move `capture_signals` to legacy module diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js index 1b26c702fb..673a710fac 100644 --- a/packages/svelte/src/internal/client/dev/tracing.js +++ b/packages/svelte/src/internal/client/dev/tracing.js @@ -4,7 +4,7 @@ import { snapshot } from '../../shared/clone.js'; import { define_property } from '../../shared/utils.js'; import { DERIVED, ASYNC, PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; import { effect_tracking } from '../reactivity/effects.js'; -import { active_reaction, captured_signals, set_captured_signals, untrack } from '../runtime.js'; +import { active_reaction, untrack } from '../runtime.js'; /** * @typedef {{ diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index cddb432a98..90f0f9baac 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -138,11 +138,11 @@ export { mark_store_binding } from './reactivity/store.js'; export { boundary, pending } from './dom/blocks/boundary.js'; +export { invalidate_inner_signals } from './legacy.js'; export { set_text } from './render.js'; export { get, safe_get, - invalidate_inner_signals, tick, untrack, exclude_from_object, diff --git a/packages/svelte/src/internal/client/legacy.js b/packages/svelte/src/internal/client/legacy.js new file mode 100644 index 0000000000..97ad7244c0 --- /dev/null +++ b/packages/svelte/src/internal/client/legacy.js @@ -0,0 +1,46 @@ +/** @import { Value } from '#client' */ +import { internal_set } from './reactivity/sources.js'; +import { untrack } from './runtime.js'; + +/** + * @type {Set | null} + * @deprecated + */ +export let captured_signals = null; + +/** + * Capture an array of all the signals that are read when `fn` is called + * @template T + * @param {() => T} fn + */ +function capture_signals(fn) { + var previous_captured_signals = captured_signals; + + try { + captured_signals = new Set(); + + untrack(fn); + + if (previous_captured_signals !== null) { + for (var signal of captured_signals) { + previous_captured_signals.add(signal); + } + } + + return captured_signals; + } finally { + captured_signals = previous_captured_signals; + } +} + +/** + * Invokes a function and captures all signals that are read during the invocation, + * then invalidates them. + * @param {() => any} fn + * @deprecated + */ +export function invalidate_inner_signals(fn) { + for (var signal of capture_signals(fn)) { + internal_set(signal, signal.v); + } +} diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 3d3c89975c..e86866af2a 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -22,7 +22,7 @@ import { STALE_REACTION, ERROR_VALUE } from './constants.js'; -import { internal_set, old_values } from './reactivity/sources.js'; +import { old_values } from './reactivity/sources.js'; import { destroy_derived_effects, execute_derived, @@ -45,6 +45,7 @@ import * as w from './warnings.js'; import { Batch, batch_deriveds, flushSync, schedule_effect } from './reactivity/batch.js'; import { handle_error } from './error-handling.js'; import { UNINITIALIZED } from '../../constants.js'; +import { captured_signals } from './legacy.js'; export let is_updating_effect = false; @@ -137,14 +138,6 @@ export function set_update_version(value) { // If we are working with a get() chain that has no active container, // to prevent memory leaks, we skip adding the reaction. export let skip_reaction = false; -// Handle collecting all signals which are read during a specific time frame -/** @type {Set | null} */ -export let captured_signals = null; - -/** @param {Set | null} value */ -export function set_captured_signals(value) { - captured_signals = value; -} export function increment_write_version() { return ++write_version; @@ -531,9 +524,7 @@ export function get(signal) { var flags = signal.f; var is_derived = (flags & DERIVED) !== 0; - if (captured_signals !== null) { - captured_signals.add(signal); - } + captured_signals?.add(signal); // Register the dependency on the current reaction signal. if (active_reaction !== null && !untracking) { @@ -713,45 +704,6 @@ export function safe_get(signal) { return signal && get(signal); } -/** - * Capture an array of all the signals that are read when `fn` is called - * @template T - * @param {() => T} fn - */ -function capture_signals(fn) { - var previous_captured_signals = captured_signals; - captured_signals = new Set(); - - var captured = captured_signals; - var signal; - - try { - untrack(fn); - if (previous_captured_signals !== null) { - for (signal of captured_signals) { - previous_captured_signals.add(signal); - } - } - } finally { - captured_signals = previous_captured_signals; - } - - return captured; -} - -/** - * Invokes a function and captures all signals that are read during the invocation, - * then invalidates them. - * @param {() => any} fn - */ -export function invalidate_inner_signals(fn) { - var captured = capture_signals(() => untrack(fn)); - - for (var signal of captured) { - internal_set(signal, signal.v); - } -} - /** * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect), * any state read inside `fn` will not be treated as a dependency. From 8afe5ec0c5a2e38b9b3a55b0dec94b99aa10447c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 20 Jul 2025 17:23:10 -0400 Subject: [PATCH 13/90] Version Packages (#16453) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/angry-hornets-hug.md | 5 ----- .changeset/four-spiders-type.md | 5 ----- .changeset/silent-rockets-tease.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 6 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 .changeset/angry-hornets-hug.md delete mode 100644 .changeset/four-spiders-type.md delete mode 100644 .changeset/silent-rockets-tease.md diff --git a/.changeset/angry-hornets-hug.md b/.changeset/angry-hornets-hug.md deleted file mode 100644 index ffe59db100..0000000000 --- a/.changeset/angry-hornets-hug.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: always mark reactions of deriveds diff --git a/.changeset/four-spiders-type.md b/.changeset/four-spiders-type.md deleted file mode 100644 index 9a4056c50a..0000000000 --- a/.changeset/four-spiders-type.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: add labels to `@const` tags and props diff --git a/.changeset/silent-rockets-tease.md b/.changeset/silent-rockets-tease.md deleted file mode 100644 index 1a708c1d69..0000000000 --- a/.changeset/silent-rockets-tease.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: tag stores for `$inspect.trace()` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index c20618af11..472a2b4a98 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.36.11 + +### Patch Changes + +- fix: always mark reactions of deriveds ([#16457](https://github.com/sveltejs/svelte/pull/16457)) + +- fix: add labels to `@const` tags and props ([#16454](https://github.com/sveltejs/svelte/pull/16454)) + +- fix: tag stores for `$inspect.trace()` ([#16452](https://github.com/sveltejs/svelte/pull/16452)) + ## 5.36.10 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 28ccb2884a..4b22911e6e 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.36.10", + "version": "5.36.11", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 8c59098e0c..07cf3cdf4d 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.36.10'; +export const VERSION = '5.36.11'; export const PUBLIC_VERSION = '5'; From ce4a99ed6d0b4b53c7abb7a8763e8e4bc4de5431 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 20 Jul 2025 18:11:26 -0400 Subject: [PATCH 14/90] Version Packages (#16459) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/swift-cherries-know.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/swift-cherries-know.md diff --git a/.changeset/swift-cherries-know.md b/.changeset/swift-cherries-know.md deleted file mode 100644 index d8bbb1256a..0000000000 --- a/.changeset/swift-cherries-know.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: move `capture_signals` to legacy module diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 472a2b4a98..1234efa0d5 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.36.12 + +### Patch Changes + +- chore: move `capture_signals` to legacy module ([#16456](https://github.com/sveltejs/svelte/pull/16456)) + ## 5.36.11 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 4b22911e6e..629ec99af8 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.36.11", + "version": "5.36.12", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 07cf3cdf4d..465bd73f0f 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.36.11'; +export const VERSION = '5.36.12'; export const PUBLIC_VERSION = '5'; From aabd333d89a4c9241bc3ed57752cb2616dd66b7c Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:22:13 +0200 Subject: [PATCH 15/90] fix: ensure subscriptions are picked up correctly by deriveds (#16466) Increment the version to ensure any dependent deriveds are marked dirty when the subscription is picked up again later. If we didn't do this then the comparison of write versions would determine that the derived has a later version than the subscriber, and it would not be re-run. Fixes #16311 Fixes #15888 --- .changeset/fresh-penguins-impress.md | 5 +++ .../src/reactivity/create-subscriber.js | 4 ++ .../samples/store-inside-derived/_config.js | 45 +++++++++++++++++++ .../samples/store-inside-derived/main.svelte | 36 +++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 .changeset/fresh-penguins-impress.md create mode 100644 packages/svelte/tests/runtime-runes/samples/store-inside-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/store-inside-derived/main.svelte diff --git a/.changeset/fresh-penguins-impress.md b/.changeset/fresh-penguins-impress.md new file mode 100644 index 0000000000..35ff4f0aaa --- /dev/null +++ b/.changeset/fresh-penguins-impress.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure subscriptions are picked up correctly by deriveds diff --git a/packages/svelte/src/reactivity/create-subscriber.js b/packages/svelte/src/reactivity/create-subscriber.js index 4dcac4e6f6..6f9313eb0a 100644 --- a/packages/svelte/src/reactivity/create-subscriber.js +++ b/packages/svelte/src/reactivity/create-subscriber.js @@ -82,6 +82,10 @@ export function createSubscriber(start) { if (subscribers === 0) { stop?.(); stop = undefined; + // Increment the version to ensure any dependent deriveds are marked dirty when the subscription is picked up again later. + // If we didn't do this then the comparison of write versions would determine that the derived has a later version than + // the subscriber, and it would not be re-run. + increment(version); } }); }; diff --git a/packages/svelte/tests/runtime-runes/samples/store-inside-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/store-inside-derived/_config.js new file mode 100644 index 0000000000..de078b1e75 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/store-inside-derived/_config.js @@ -0,0 +1,45 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test: ({ assert, target }) => { + const [loading, increment] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` +
$value: 0
+
valueFromStore.current: 0
+
valueDerivedCurrent: 0
+ + + ` + ); + + loading.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` +
$value: Loading...
+
valueFromStore.current: Loading...
+
valueDerivedCurrent: Loading...
+ + + ` + ); + + increment.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` +
$value: 1
+
valueFromStore.current: 1
+
valueDerivedCurrent: 1
+ + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/store-inside-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/store-inside-derived/main.svelte new file mode 100644 index 0000000000..06d0a0d4b4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/store-inside-derived/main.svelte @@ -0,0 +1,36 @@ + + +
+ $value: {isLoading ? 'Loading...' : $value} +
+ +
+ valueFromStore.current: {isLoading ? 'Loading...' : valueFromStore.current} +
+ +
+ valueDerivedCurrent: {isLoading ? 'Loading...' : valueDerivedCurrent} +
+ + + + From 28403beaeb360fd7b4096fe7c8ef98e43d0676c2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 21 Jul 2025 13:32:47 -0400 Subject: [PATCH 16/90] Version Packages (#16467) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/fresh-penguins-impress.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/fresh-penguins-impress.md diff --git a/.changeset/fresh-penguins-impress.md b/.changeset/fresh-penguins-impress.md deleted file mode 100644 index 35ff4f0aaa..0000000000 --- a/.changeset/fresh-penguins-impress.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ensure subscriptions are picked up correctly by deriveds diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 1234efa0d5..5a5e532a08 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.36.13 + +### Patch Changes + +- fix: ensure subscriptions are picked up correctly by deriveds ([#16466](https://github.com/sveltejs/svelte/pull/16466)) + ## 5.36.12 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 629ec99af8..4bf9a5df22 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.36.12", + "version": "5.36.13", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 465bd73f0f..7d47fbc5f1 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.36.12'; +export const VERSION = '5.36.13'; export const PUBLIC_VERSION = '5'; From 9412c5861c365610d7c6c0e4ecd3572ac3fe5860 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 21 Jul 2025 22:17:25 -0400 Subject: [PATCH 17/90] chore: log effect functions in log_effect_tree (#16468) --- packages/svelte/src/internal/client/dev/debug.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/svelte/src/internal/client/dev/debug.js b/packages/svelte/src/internal/client/dev/debug.js index c47080ed2f..2714a3af1f 100644 --- a/packages/svelte/src/internal/client/dev/debug.js +++ b/packages/svelte/src/internal/client/dev/debug.js @@ -63,6 +63,13 @@ export function log_effect_tree(effect, depth = 0) { // eslint-disable-next-line no-console console.log(callsite); + } else { + // eslint-disable-next-line no-console + console.groupCollapsed(`%cfn`, `font-weight: normal`); + // eslint-disable-next-line no-console + console.log(effect.fn); + // eslint-disable-next-line no-console + console.groupEnd(); } if (effect.deps !== null) { From 1deb31082a383fb5f4f9ae86f1ff12657823bcd7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 22 Jul 2025 12:56:32 -0400 Subject: [PATCH 18/90] fix: abort and reschedule `$effect.pre` when necessary (#16335) * unskip failing test * fix * tidy up * skip_no_async * add comment --- .../runtime-runes/samples/effect-order-7/_config.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/svelte/tests/runtime-runes/samples/effect-order-7/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-order-7/_config.js index 29c33c7b18..f0a9c2e867 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-order-7/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/effect-order-7/_config.js @@ -2,14 +2,18 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ - skip: true, + // For this to work in non-async mode, we would need to abort + // inside `#traverse_effect_tree`, which would be very + // complicated and annoying. Since this hasn't been + // a real issue (AFAICT), we ignore it + skip_no_async: true, - async test({ assert, target, logs }) { + async test({ target }) { const [open, close] = target.querySelectorAll('button'); flushSync(() => open.click()); - flushSync(() => close.click()); - assert.deepEqual(logs, [true]); + // if the effect queue isn't aborted after the state change, this will throw + flushSync(() => close.click()); } }); From 68372460e999e48ac09b1c2bd4935ceaea8c33e9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 22 Jul 2025 13:29:15 -0400 Subject: [PATCH 19/90] chore: small tidy up (#16476) --- .../src/internal/client/reactivity/batch.js | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index ec082bb595..c452211894 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -193,6 +193,8 @@ export class Batch { // if we didn't start any new async work, and no async work // is outstanding from a previous flush, commit if (this.#async_effects.length === 0 && this.#pending === 0) { + this.#commit(); + var render_effects = this.#render_effects; var effects = this.#effects; @@ -200,8 +202,6 @@ export class Batch { this.#effects = []; this.#block_effects = []; - this.#commit(); - flush_queued_effects(render_effects); flush_queued_effects(effects); @@ -539,43 +539,43 @@ function flush_queued_effects(effects) { var length = effects.length; if (length === 0) return; - for (var i = 0; i < length; i++) { - var effect = effects[i]; - - if ((effect.f & (DESTROYED | INERT)) === 0) { - if (is_dirty(effect)) { - var wv = write_version; - - update_effect(effect); - - // Effects with no dependencies or teardown do not get added to the effect tree. - // Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we - // don't know if we need to keep them until they are executed. Doing the check - // here (rather than in `update_effect`) allows us to skip the work for - // immediate effects. - if (effect.deps === null && effect.first === null && effect.nodes_start === null) { - // if there's no teardown or abort controller we completely unlink - // the effect from the graph - if (effect.teardown === null && effect.ac === null) { - // remove this effect from the graph - unlink_effect(effect); - } else { - // keep the effect in the graph, but free up some memory - effect.fn = null; - } - } + var i = 0; - // if state is written in a user effect, abort and re-schedule, lest we run - // effects that should be removed as a result of the state change - if (write_version > wv && (effect.f & USER_EFFECT) !== 0) { - break; + while (i < length) { + var effect = effects[i++]; + + if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) { + var wv = write_version; + + update_effect(effect); + + // Effects with no dependencies or teardown do not get added to the effect tree. + // Deferred effects (e.g. `$effect(...)`) _are_ added to the tree because we + // don't know if we need to keep them until they are executed. Doing the check + // here (rather than in `update_effect`) allows us to skip the work for + // immediate effects. + if (effect.deps === null && effect.first === null && effect.nodes_start === null) { + // if there's no teardown or abort controller we completely unlink + // the effect from the graph + if (effect.teardown === null && effect.ac === null) { + // remove this effect from the graph + unlink_effect(effect); + } else { + // keep the effect in the graph, but free up some memory + effect.fn = null; } } + + // if state is written in a user effect, abort and re-schedule, lest we run + // effects that should be removed as a result of the state change + if (write_version > wv && (effect.f & USER_EFFECT) !== 0) { + break; + } } } - for (; i < length; i += 1) { - schedule_effect(effects[i]); + while (i < length) { + schedule_effect(effects[i++]); } } From 8e2f4b51c50a2e10bc482fac6d900be921dd8b74 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 23 Jul 2025 07:55:51 -0400 Subject: [PATCH 20/90] fix: unset batch before flushing queued effects (#16482) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * - add state changes resulting from an $effect to a separate new batch - schedule rerunning effects based on the sources that are dirty, not just rerunning them all blindly (excempting async effects which will have run by that time already) * test * better fix * tests * this fixes the last test somehow * fix #16477 * typo * copy over changeset from #16477 * copy over changeset from #16464 * changeset * dedupe * move flushing_sync check inside Batch.ensure * unused * flushing_sync -> is_flushing_sync * remove flush_effects method * dedupe declaration * tweak * tweak * update comment — it _does_ feel slightly wrong, but no wronger than the rest of this cursed function --------- Co-authored-by: Simon Holthausen --- .changeset/grumpy-boats-beg.md | 5 + .changeset/shiny-walls-fix.md | 5 + .changeset/thick-mice-kick.md | 5 + .../src/internal/client/reactivity/batch.js | 190 ++++++++++-------- .../src/internal/client/reactivity/sources.js | 11 +- .../Component.svelte | 7 + .../1000-reading-derived-effects/_config.js | 5 + .../1000-reading-derived-effects/main.svelte | 8 + .../_config.js | 26 +++ .../main.svelte | 27 +++ .../async-effect-triggers-await/_config.js | 32 +++ .../async-effect-triggers-await/main.svelte | 24 +++ .../binding-update-while-focused-2/_config.js | 24 +++ .../main.svelte | 21 ++ 14 files changed, 298 insertions(+), 92 deletions(-) create mode 100644 .changeset/grumpy-boats-beg.md create mode 100644 .changeset/shiny-walls-fix.md create mode 100644 .changeset/thick-mice-kick.md create mode 100644 packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/Component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-triggers-await/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-triggers-await/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/main.svelte diff --git a/.changeset/grumpy-boats-beg.md b/.changeset/grumpy-boats-beg.md new file mode 100644 index 0000000000..f677743def --- /dev/null +++ b/.changeset/grumpy-boats-beg.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: keep input in sync when binding updated via effect diff --git a/.changeset/shiny-walls-fix.md b/.changeset/shiny-walls-fix.md new file mode 100644 index 0000000000..91ed548728 --- /dev/null +++ b/.changeset/shiny-walls-fix.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent infinite async loop diff --git a/.changeset/thick-mice-kick.md b/.changeset/thick-mice-kick.md new file mode 100644 index 0000000000..eec55b77ee --- /dev/null +++ b/.changeset/thick-mice-kick.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: exclude derived writes from effect abort and rescheduling diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index c452211894..ce413fa1e1 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -21,14 +21,13 @@ import { is_updating_effect, set_is_updating_effect, set_signal_status, - update_effect, - write_version + update_effect } from '../runtime.js'; import * as e from '../errors.js'; import { flush_tasks } from '../dom/task.js'; import { DEV } from 'esm-env'; import { invoke_error_boundary } from '../error-handling.js'; -import { old_values } from './sources.js'; +import { mark_reactions, old_values } from './sources.js'; import { unlink_effect } from './effects.js'; import { unset_context } from './async.js'; @@ -70,13 +69,15 @@ let last_scheduled_effect = null; let is_flushing = false; +let is_flushing_sync = false; + export class Batch { /** * The current values of any sources that are updated in this batch * They keys of this map are identical to `this.#previous` * @type {Map} */ - #current = new Map(); + current = new Map(); /** * The values of any sources that are updated in this batch _before_ those updates took place. @@ -156,7 +157,7 @@ export class Batch { * * @param {Effect[]} root_effects */ - #process(root_effects) { + process(root_effects) { queued_root_effects = []; /** @type {Map | null} */ @@ -169,7 +170,7 @@ export class Batch { current_values = new Map(); batch_deriveds = new Map(); - for (const [source, current] of this.#current) { + for (const [source, current] of this.current) { current_values.set(source, { v: source.v, wv: source.wv }); source.v = current; } @@ -202,9 +203,22 @@ export class Batch { this.#effects = []; this.#block_effects = []; + // If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with + // newly updated sources, which could lead to infinite loops when effects run over and over again. + current_batch = null; + flush_queued_effects(render_effects); flush_queued_effects(effects); + // Reinstate the current batch if there was no new one created, as `process()` runs in a loop in `flush_effects()`. + // That method expects `current_batch` to be set, and could run the loop again if effects result in new effects + // being scheduled but without writes happening in which case no new batch is created. + if (current_batch === null) { + current_batch = this; + } else { + batches.delete(this); + } + this.#deferred?.resolve(); } else { // otherwise mark effects clean so they get scheduled on the next run @@ -300,7 +314,7 @@ export class Batch { this.#previous.set(source, value); } - this.#current.set(source, source.v); + this.current.set(source, source.v); } activate() { @@ -327,13 +341,13 @@ export class Batch { flush() { if (queued_root_effects.length > 0) { - this.flush_effects(); + flush_effects(); } else { this.#commit(); } if (current_batch !== this) { - // this can happen if a `flushSync` occurred during `this.flush_effects()`, + // this can happen if a `flushSync` occurred during `flush_effects()`, // which is permitted in legacy mode despite being a terrible idea return; } @@ -345,52 +359,6 @@ export class Batch { this.deactivate(); } - flush_effects() { - var was_updating_effect = is_updating_effect; - is_flushing = true; - - try { - var flush_count = 0; - set_is_updating_effect(true); - - while (queued_root_effects.length > 0) { - if (flush_count++ > 1000) { - if (DEV) { - var updates = new Map(); - - for (const source of this.#current.keys()) { - for (const [stack, update] of source.updated ?? []) { - var entry = updates.get(stack); - - if (!entry) { - entry = { error: update.error, count: 0 }; - updates.set(stack, entry); - } - - entry.count += update.count; - } - } - - for (const update of updates.values()) { - // eslint-disable-next-line no-console - console.error(update.error); - } - } - - infinite_loop_guard(); - } - - this.#process(queued_root_effects); - old_values.clear(); - } - } finally { - is_flushing = false; - set_is_updating_effect(was_updating_effect); - - last_scheduled_effect = null; - } - } - /** * Append and remove branches to/from the DOM */ @@ -412,19 +380,8 @@ export class Batch { this.#pending -= 1; if (this.#pending === 0) { - for (const e of this.#render_effects) { - set_signal_status(e, DIRTY); - schedule_effect(e); - } - - for (const e of this.#effects) { - set_signal_status(e, DIRTY); - schedule_effect(e); - } - - for (const e of this.#block_effects) { - set_signal_status(e, DIRTY); - schedule_effect(e); + for (const source of this.current.keys()) { + mark_reactions(source, DIRTY, false); } this.#render_effects = []; @@ -445,12 +402,12 @@ export class Batch { return (this.#deferred ??= deferred()).promise; } - static ensure(autoflush = true) { + static ensure() { if (current_batch === null) { const batch = (current_batch = new Batch()); batches.add(current_batch); - if (autoflush) { + if (!is_flushing_sync) { Batch.enqueue(() => { if (current_batch !== batch) { // a flushSync happened in the meantime @@ -487,32 +444,85 @@ export function flushSync(fn) { e.flush_sync_in_effect(); } - var result; + var was_flushing_sync = is_flushing_sync; + is_flushing_sync = true; - const batch = Batch.ensure(false); + try { + var result; - if (fn) { - batch.flush_effects(); + if (fn) { + flush_effects(); + result = fn(); + } - result = fn(); - } + while (true) { + flush_tasks(); - while (true) { - flush_tasks(); + if (queued_root_effects.length === 0) { + current_batch?.flush(); - if (queued_root_effects.length === 0) { - if (batch === current_batch) { - batch.flush(); + // we need to check again, in case we just updated an `$effect.pending()` + if (queued_root_effects.length === 0) { + // this would be reset in `flush_effects()` but since we are early returning here, + // we need to reset it here as well in case the first time there's 0 queued root effects + last_scheduled_effect = null; + + return /** @type {T} */ (result); + } } - // this would be reset in `batch.flush_effects()` but since we are early returning here, - // we need to reset it here as well in case the first time there's 0 queued root effects - last_scheduled_effect = null; + flush_effects(); + } + } finally { + is_flushing_sync = was_flushing_sync; + } +} + +function flush_effects() { + var was_updating_effect = is_updating_effect; + is_flushing = true; + + try { + var flush_count = 0; + set_is_updating_effect(true); + + while (queued_root_effects.length > 0) { + var batch = Batch.ensure(); + + if (flush_count++ > 1000) { + if (DEV) { + var updates = new Map(); + + for (const source of batch.current.keys()) { + for (const [stack, update] of source.updated ?? []) { + var entry = updates.get(stack); + + if (!entry) { + entry = { error: update.error, count: 0 }; + updates.set(stack, entry); + } + + entry.count += update.count; + } + } + + for (const update of updates.values()) { + // eslint-disable-next-line no-console + console.error(update.error); + } + } + + infinite_loop_guard(); + } - return /** @type {T} */ (result); + batch.process(queued_root_effects); + old_values.clear(); } + } finally { + is_flushing = false; + set_is_updating_effect(was_updating_effect); - batch.flush_effects(); + last_scheduled_effect = null; } } @@ -545,7 +555,7 @@ function flush_queued_effects(effects) { var effect = effects[i++]; if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) { - var wv = write_version; + var n = current_batch ? current_batch.current.size : 0; update_effect(effect); @@ -568,7 +578,11 @@ function flush_queued_effects(effects) { // if state is written in a user effect, abort and re-schedule, lest we run // effects that should be removed as a result of the state change - if (write_version > wv && (effect.f & USER_EFFECT) !== 0) { + if ( + current_batch !== null && + current_batch.current.size > n && + (effect.f & USER_EFFECT) !== 0 + ) { break; } } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index f6b14f3360..3b28c8fdce 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -179,7 +179,7 @@ export function internal_set(source, value) { source.v = value; - const batch = Batch.ensure(); + var batch = Batch.ensure(); batch.capture(source, old_value); if (DEV) { @@ -301,9 +301,10 @@ export function increment(source) { /** * @param {Value} signal * @param {number} status should be DIRTY or MAYBE_DIRTY + * @param {boolean} schedule_async * @returns {void} */ -function mark_reactions(signal, status) { +export function mark_reactions(signal, status, schedule_async = true) { var reactions = signal.reactions; if (reactions === null) return; @@ -323,14 +324,16 @@ function mark_reactions(signal, status) { continue; } + var should_schedule = (flags & DIRTY) === 0 && (schedule_async || (flags & ASYNC) === 0); + // don't set a DIRTY reaction to MAYBE_DIRTY - if ((flags & DIRTY) === 0) { + if (should_schedule) { set_signal_status(reaction, status); } if ((flags & DERIVED) !== 0) { mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); - } else if ((flags & DIRTY) === 0) { + } else if (should_schedule) { schedule_effect(/** @type {Effect} */ (reaction)); } } diff --git a/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/Component.svelte b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/Component.svelte new file mode 100644 index 0000000000..7a54323cb9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/Component.svelte @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/_config.js b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/_config.js new file mode 100644 index 0000000000..2e4a27cf09 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + async test() {} +}); diff --git a/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/main.svelte b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/main.svelte new file mode 100644 index 0000000000..bd326edfb9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/1000-reading-derived-effects/main.svelte @@ -0,0 +1,8 @@ + + +{#each arr} + +{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/_config.js b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/_config.js new file mode 100644 index 0000000000..782ae945f9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/_config.js @@ -0,0 +1,26 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + + const [input] = target.querySelectorAll('input'); + + input.focus(); + input.value = '3'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + await tick(); + + assert.equal(input.value, '3'); + assert.htmlEqual(target.innerHTML, `

3

`); + + input.focus(); + input.value = '1'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + await tick(); + + assert.equal(input.value, '2'); + assert.htmlEqual(target.innerHTML, `

2

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/main.svelte new file mode 100644 index 0000000000..763ce6ebf0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused/main.svelte @@ -0,0 +1,27 @@ + + + +

{await value}

+ + + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-triggers-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-effect-triggers-await/_config.js new file mode 100644 index 0000000000..c551cc6b8c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-triggers-await/_config.js @@ -0,0 +1,32 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + + const [increment] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + +

1

+

1

+ ` + ); + + increment.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + +

2

+

2

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-triggers-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-effect-triggers-await/main.svelte new file mode 100644 index 0000000000..153fe03f0d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-triggers-await/main.svelte @@ -0,0 +1,24 @@ + + + + +

{JSON.stringify((await data), null, 2)}

+ {#if true} + +

{unrelated}

+ {/if} + + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/_config.js b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/_config.js new file mode 100644 index 0000000000..7a56c79d71 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/_config.js @@ -0,0 +1,24 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const [input] = target.querySelectorAll('input'); + + input.focus(); + input.value = '3'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + flushSync(); + + assert.equal(input.value, '3'); + assert.htmlEqual(target.innerHTML, `

3

`); + + input.focus(); + input.value = '1'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + flushSync(); + + assert.equal(input.value, '2'); + assert.htmlEqual(target.innerHTML, `

2

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/main.svelte new file mode 100644 index 0000000000..b0597c223b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-update-while-focused-2/main.svelte @@ -0,0 +1,21 @@ + + +

{value}

+ From c26365bdda1a13e98a2e75f17da655e08ff4f8bc Mon Sep 17 00:00:00 2001 From: Hyunbin Seo <47051820+hyunbinseo@users.noreply.github.com> Date: Wed, 23 Jul 2025 21:02:28 +0900 Subject: [PATCH 21/90] chore: rename form `accept-charset` attribute (#16478) * chore: rename form accept-charset attribute * chore: utf-8 to lowercase Co-authored-by: Rich Harris * Update .changeset/healthy-carpets-deny.md --------- Co-authored-by: Rich Harris --- .changeset/healthy-carpets-deny.md | 5 +++++ packages/svelte/elements.d.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/healthy-carpets-deny.md diff --git a/.changeset/healthy-carpets-deny.md b/.changeset/healthy-carpets-deny.md new file mode 100644 index 0000000000..8f8db7fa9c --- /dev/null +++ b/.changeset/healthy-carpets-deny.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: rename form accept-charset attribute diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 1492f77792..604241592a 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -996,7 +996,7 @@ export interface HTMLFieldsetAttributes extends HTMLAttributes { - acceptcharset?: string | undefined | null; + 'accept-charset'?: 'utf-8' | (string & {}) | undefined | null; action?: string | undefined | null; autocomplete?: AutoFillBase | undefined | null; enctype?: From f8820956d2591288de53927c5e173242a13f125a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 11:23:52 -0400 Subject: [PATCH 22/90] Version Packages (#16484) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/grumpy-boats-beg.md | 5 ----- .changeset/healthy-carpets-deny.md | 5 ----- .changeset/shiny-walls-fix.md | 5 ----- .changeset/thick-mice-kick.md | 5 ----- packages/svelte/CHANGELOG.md | 12 ++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 7 files changed, 14 insertions(+), 22 deletions(-) delete mode 100644 .changeset/grumpy-boats-beg.md delete mode 100644 .changeset/healthy-carpets-deny.md delete mode 100644 .changeset/shiny-walls-fix.md delete mode 100644 .changeset/thick-mice-kick.md diff --git a/.changeset/grumpy-boats-beg.md b/.changeset/grumpy-boats-beg.md deleted file mode 100644 index f677743def..0000000000 --- a/.changeset/grumpy-boats-beg.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: keep input in sync when binding updated via effect diff --git a/.changeset/healthy-carpets-deny.md b/.changeset/healthy-carpets-deny.md deleted file mode 100644 index 8f8db7fa9c..0000000000 --- a/.changeset/healthy-carpets-deny.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: rename form accept-charset attribute diff --git a/.changeset/shiny-walls-fix.md b/.changeset/shiny-walls-fix.md deleted file mode 100644 index 91ed548728..0000000000 --- a/.changeset/shiny-walls-fix.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: prevent infinite async loop diff --git a/.changeset/thick-mice-kick.md b/.changeset/thick-mice-kick.md deleted file mode 100644 index eec55b77ee..0000000000 --- a/.changeset/thick-mice-kick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: exclude derived writes from effect abort and rescheduling diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 5a5e532a08..8cd3850460 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.36.14 + +### Patch Changes + +- fix: keep input in sync when binding updated via effect ([#16482](https://github.com/sveltejs/svelte/pull/16482)) + +- fix: rename form accept-charset attribute ([#16478](https://github.com/sveltejs/svelte/pull/16478)) + +- fix: prevent infinite async loop ([#16482](https://github.com/sveltejs/svelte/pull/16482)) + +- fix: exclude derived writes from effect abort and rescheduling ([#16482](https://github.com/sveltejs/svelte/pull/16482)) + ## 5.36.13 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 4bf9a5df22..7fe1d161f2 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.36.13", + "version": "5.36.14", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 7d47fbc5f1..cd9d8b459c 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.36.13'; +export const VERSION = '5.36.14'; export const PUBLIC_VERSION = '5'; From 53417ea8f785f2ec884b095ac1bacd08aa30da86 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 24 Jul 2025 01:48:32 -0400 Subject: [PATCH 23/90] fix: preserve dirty status of deferred effects (#16487) * don't mark_reactions inside decrement, it can cause infinite loops * revert mark_reactions changes * preserve DIRTY/MAYBE_DIRTY status of deferred effects * changeset * tweak --- .changeset/cool-insects-argue.md | 5 ++ .../src/internal/client/reactivity/batch.js | 63 ++++++++++++++----- .../src/internal/client/reactivity/sources.js | 9 +-- .../async-effect-conservative/_config.js | 28 +++++++++ .../async-effect-conservative/main.svelte | 17 +++++ 5 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 .changeset/cool-insects-argue.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-conservative/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-conservative/main.svelte diff --git a/.changeset/cool-insects-argue.md b/.changeset/cool-insects-argue.md new file mode 100644 index 0000000000..ff1c520b7b --- /dev/null +++ b/.changeset/cool-insects-argue.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: preserve dirty status of deferred effects diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index ce413fa1e1..89bad947c7 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -10,7 +10,8 @@ import { INERT, RENDER_EFFECT, ROOT_EFFECT, - USER_EFFECT + USER_EFFECT, + MAYBE_DIRTY } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; import { deferred, define_property } from '../../shared/utils.js'; @@ -27,7 +28,7 @@ import * as e from '../errors.js'; import { flush_tasks } from '../dom/task.js'; import { DEV } from 'esm-env'; import { invoke_error_boundary } from '../error-handling.js'; -import { mark_reactions, old_values } from './sources.js'; +import { old_values } from './sources.js'; import { unlink_effect } from './effects.js'; import { unset_context } from './async.js'; @@ -146,6 +147,18 @@ export class Batch { */ #block_effects = []; + /** + * Deferred effects (which run after async work has completed) that are DIRTY + * @type {Effect[]} + */ + #dirty_effects = []; + + /** + * Deferred effects that are MAYBE_DIRTY + * @type {Effect[]} + */ + #maybe_dirty_effects = []; + /** * A set of branches that still exist, but will be destroyed when this batch * is committed — we skip over these during `process` @@ -221,10 +234,9 @@ export class Batch { this.#deferred?.resolve(); } else { - // otherwise mark effects clean so they get scheduled on the next run - for (const e of this.#render_effects) set_signal_status(e, CLEAN); - for (const e of this.#effects) set_signal_status(e, CLEAN); - for (const e of this.#block_effects) set_signal_status(e, CLEAN); + this.#defer_effects(this.#render_effects); + this.#defer_effects(this.#effects); + this.#defer_effects(this.#block_effects); } if (current_values) { @@ -271,15 +283,15 @@ export class Batch { if (!skip && effect.fn !== null) { if (is_branch) { effect.f ^= CLEAN; - } else if ((flags & EFFECT) !== 0) { - this.#effects.push(effect); - } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) { - this.#render_effects.push(effect); - } else if (is_dirty(effect)) { - if ((flags & ASYNC) !== 0) { + } else if ((flags & CLEAN) === 0) { + if ((flags & EFFECT) !== 0) { + this.#effects.push(effect); + } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) { + this.#render_effects.push(effect); + } else if ((flags & ASYNC) !== 0) { var effects = effect.b?.pending ? this.#boundary_async_effects : this.#async_effects; effects.push(effect); - } else { + } else if (is_dirty(effect)) { if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect); update_effect(effect); } @@ -303,6 +315,21 @@ export class Batch { } } + /** + * @param {Effect[]} effects + */ + #defer_effects(effects) { + for (const e of effects) { + const target = (e.f & DIRTY) !== 0 ? this.#dirty_effects : this.#maybe_dirty_effects; + target.push(e); + + // mark as clean so they get scheduled if they depend on pending async state + set_signal_status(e, CLEAN); + } + + effects.length = 0; + } + /** * Associate a change to a given source with the current * batch, noting its previous and current values @@ -380,8 +407,14 @@ export class Batch { this.#pending -= 1; if (this.#pending === 0) { - for (const source of this.current.keys()) { - mark_reactions(source, DIRTY, false); + for (const e of this.#dirty_effects) { + set_signal_status(e, DIRTY); + schedule_effect(e); + } + + for (const e of this.#maybe_dirty_effects) { + set_signal_status(e, MAYBE_DIRTY); + schedule_effect(e); } this.#render_effects = []; diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 3b28c8fdce..7b5198542a 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -301,10 +301,9 @@ export function increment(source) { /** * @param {Value} signal * @param {number} status should be DIRTY or MAYBE_DIRTY - * @param {boolean} schedule_async * @returns {void} */ -export function mark_reactions(signal, status, schedule_async = true) { +function mark_reactions(signal, status) { var reactions = signal.reactions; if (reactions === null) return; @@ -324,16 +323,14 @@ export function mark_reactions(signal, status, schedule_async = true) { continue; } - var should_schedule = (flags & DIRTY) === 0 && (schedule_async || (flags & ASYNC) === 0); - // don't set a DIRTY reaction to MAYBE_DIRTY - if (should_schedule) { + if ((flags & DIRTY) === 0) { set_signal_status(reaction, status); } if ((flags & DERIVED) !== 0) { mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); - } else if (should_schedule) { + } else { schedule_effect(/** @type {Effect} */ (reaction)); } } diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-conservative/_config.js b/packages/svelte/tests/runtime-runes/samples/async-effect-conservative/_config.js new file mode 100644 index 0000000000..bab06a203d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-conservative/_config.js @@ -0,0 +1,28 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + await tick(); + + const [increment] = target.querySelectorAll('button'); + + assert.deepEqual(logs, [false]); + assert.htmlEqual(target.innerHTML, '

0

'); + + increment.click(); + await tick(); + assert.deepEqual(logs, [false]); + assert.htmlEqual(target.innerHTML, '

1

'); + + increment.click(); + await tick(); + assert.deepEqual(logs, [false, true]); + assert.htmlEqual(target.innerHTML, '

2

'); + + increment.click(); + await tick(); + assert.deepEqual(logs, [false, true]); + assert.htmlEqual(target.innerHTML, '

3

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-conservative/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-effect-conservative/main.svelte new file mode 100644 index 0000000000..5305067a5a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-conservative/main.svelte @@ -0,0 +1,17 @@ + + + + +

{await count}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
From 46d3261ed9a7d6acb9908f52e989013795555a56 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 24 Jul 2025 11:39:54 +0200 Subject: [PATCH 24/90] chore: don't do effect scheduling unnecessarily (#16489) mini-cleanup post #16487 - we don't need to do the work of scheduling an effect that's already dirty which means it already scheduled its root effect to run --- packages/svelte/src/internal/client/reactivity/sources.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 7b5198542a..3b2087d56b 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -323,14 +323,16 @@ function mark_reactions(signal, status) { continue; } + var not_dirty = (flags & DIRTY) === 0; + // don't set a DIRTY reaction to MAYBE_DIRTY - if ((flags & DIRTY) === 0) { + if (not_dirty) { set_signal_status(reaction, status); } if ((flags & DERIVED) !== 0) { mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY); - } else { + } else if (not_dirty) { schedule_effect(/** @type {Effect} */ (reaction)); } } From 4e74cd35fedf55e65b60c957041f79ca94dfc197 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 11:43:28 +0200 Subject: [PATCH 25/90] Version Packages (#16488) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/cool-insects-argue.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/cool-insects-argue.md diff --git a/.changeset/cool-insects-argue.md b/.changeset/cool-insects-argue.md deleted file mode 100644 index ff1c520b7b..0000000000 --- a/.changeset/cool-insects-argue.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: preserve dirty status of deferred effects diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 8cd3850460..5bffa5f70e 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.36.15 + +### Patch Changes + +- fix: preserve dirty status of deferred effects ([#16487](https://github.com/sveltejs/svelte/pull/16487)) + ## 5.36.14 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 7fe1d161f2..051f82ec3a 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.36.14", + "version": "5.36.15", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index cd9d8b459c..1d469f29b0 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.36.14'; +export const VERSION = '5.36.15'; export const PUBLIC_VERSION = '5'; From 7eb11e0e247d9da4edb88dc5652101ceb55fc530 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:00:43 +0200 Subject: [PATCH 26/90] fix: don't destroy effect roots created inside of deriveds (#16492) We were wrongfully adding effect roots to `derived.effects`, too, which meant those were destroyed when the derived reran. --- .changeset/lemon-weeks-call.md | 5 ++ .../src/internal/client/reactivity/effects.js | 6 ++- .../src/internal/client/reactivity/types.d.ts | 2 +- packages/svelte/tests/signals/test.ts | 51 +++++++++++++++++++ 4 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 .changeset/lemon-weeks-call.md diff --git a/.changeset/lemon-weeks-call.md b/.changeset/lemon-weeks-call.md new file mode 100644 index 0000000000..ae62305630 --- /dev/null +++ b/.changeset/lemon-weeks-call.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't destroy effect roots created inside of deriveds diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index c4edd2bf8d..f44efa32f1 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -148,7 +148,11 @@ function create_effect(type, fn, sync, push = true) { } // if we're in a derived, add the effect there too - if (active_reaction !== null && (active_reaction.f & DERIVED) !== 0) { + if ( + active_reaction !== null && + (active_reaction.f & DERIVED) !== 0 && + (type & ROOT_EFFECT) === 0 + ) { var derived = /** @type {Derived} */ (active_reaction); (derived.effects ??= []).push(effect); } diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 72187e84a7..81f7197b80 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -54,7 +54,7 @@ export interface Reaction extends Signal { export interface Derived extends Value, Reaction { /** The derived function */ fn: () => V; - /** Effects created inside this signal */ + /** Effects created inside this signal. Used to destroy those effects when the derived reruns or is cleaned up */ effects: null | Effect[]; /** Parent effect or derived */ parent: Effect | Derived | null; diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 937324727b..eff6d6166a 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1390,4 +1390,55 @@ describe('signals', () => { destroy(); }; }); + + test('$effect.root inside deriveds stay alive independently', () => { + const log: any[] = []; + const c = state(0); + const cleanup: any[] = []; + const inner_states: any[] = []; + + const d = derived(() => { + const destroy = effect_root(() => { + const x = state(0); + inner_states.push(x); + + effect(() => { + log.push('inner ' + $.get(x)); + return () => { + log.push('inner destroyed'); + }; + }); + }); + + cleanup.push(destroy); + + return $.get(c); + }); + + return () => { + log.push($.get(d)); + flushSync(); + + assert.deepEqual(log, [0, 'inner 0']); + log.length = 0; + + set(inner_states[0], 1); + flushSync(); + + assert.deepEqual(log, ['inner destroyed', 'inner 1']); + log.length = 0; + + set(c, 1); + log.push($.get(d)); + flushSync(); + + assert.deepEqual(log, [1, 'inner 0']); + log.length = 0; + + cleanup.forEach((fn) => fn()); + flushSync(); + + assert.deepEqual(log, ['inner destroyed', 'inner destroyed']); + }; + }); }); From dc043fb2d3c87619e1c26315f600acd4fe19dfa4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 24 Jul 2025 10:16:15 -0400 Subject: [PATCH 27/90] fix: don't update a focused input with values from its own past (#16491) * fix: don't update a focused input with values from its own past * remove * fix --- .changeset/fast-mails-fail.md | 5 +++ .../client/dom/elements/bindings/input.js | 11 ++++-- .../src/internal/client/reactivity/batch.js | 12 +++++- .../_config.js | 37 +++++++++++++++++++ .../main.svelte | 25 +++++++++++++ 5 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 .changeset/fast-mails-fail.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte diff --git a/.changeset/fast-mails-fail.md b/.changeset/fast-mails-fail.md new file mode 100644 index 0000000000..027cb01548 --- /dev/null +++ b/.changeset/fast-mails-fail.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't update a focused input with values from its own past diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index 7c1fccea0f..7c73280dd6 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -8,7 +8,7 @@ import { queue_micro_task } from '../../task.js'; import { hydrating } from '../../hydration.js'; import { untrack } from '../../../runtime.js'; import { is_runes } from '../../../context.js'; -import { current_batch } from '../../../reactivity/batch.js'; +import { current_batch, previous_batch } from '../../../reactivity/batch.js'; /** * @param {HTMLInputElement} input @@ -76,13 +76,18 @@ export function bind_value(input, get, set = get) { var value = get(); - if (input === document.activeElement && batches.has(/** @type {Batch} */ (current_batch))) { + if (input === document.activeElement) { + // we need both, because in non-async mode, render effects run before previous_batch is set + var batch = /** @type {Batch} */ (previous_batch ?? current_batch); + // Never rewrite the contents of a focused input. We can get here if, for example, // an update is deferred because of async work depending on the input: // // //

{await find(query)}

- return; + if (batches.has(batch)) { + return; + } } if (is_numberlike_input(input) && value === to_number(input.value)) { diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 89bad947c7..123bc95d16 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -38,6 +38,13 @@ const batches = new Set(); /** @type {Batch | null} */ export let current_batch = null; +/** + * This is needed to avoid overwriting inputs in non-async mode + * TODO 6.0 remove this, as non-async mode will go away + * @type {Batch | null} + */ +export let previous_batch = null; + /** * When time travelling, we re-evaluate deriveds based on the temporary * values of their dependencies rather than their actual values, and cache @@ -71,7 +78,6 @@ let last_scheduled_effect = null; let is_flushing = false; let is_flushing_sync = false; - export class Batch { /** * The current values of any sources that are updated in this batch @@ -173,6 +179,8 @@ export class Batch { process(root_effects) { queued_root_effects = []; + previous_batch = null; + /** @type {Map | null} */ var current_values = null; @@ -218,6 +226,7 @@ export class Batch { // If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with // newly updated sources, which could lead to infinite loops when effects run over and over again. + previous_batch = current_batch; current_batch = null; flush_queued_effects(render_effects); @@ -350,6 +359,7 @@ export class Batch { deactivate() { current_batch = null; + previous_batch = null; for (const update of effect_pending_updates) { effect_pending_updates.delete(update); diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js new file mode 100644 index 0000000000..b0772ad3c0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js @@ -0,0 +1,37 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, instance }) { + instance.shift(); + await tick(); + + const [input] = target.querySelectorAll('input'); + + input.focus(); + input.value = '1'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + await tick(); + + assert.htmlEqual(target.innerHTML, `

0

`); + assert.equal(input.value, '1'); + + input.focus(); + input.value = '2'; + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + await tick(); + + assert.htmlEqual(target.innerHTML, `

0

`); + assert.equal(input.value, '2'); + + instance.shift(); + await tick(); + assert.htmlEqual(target.innerHTML, `

1

`); + assert.equal(input.value, '2'); + + instance.shift(); + await tick(); + assert.htmlEqual(target.innerHTML, `

2

`); + assert.equal(input.value, '2'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte new file mode 100644 index 0000000000..2fc898e654 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte @@ -0,0 +1,25 @@ + + + + +

{await push(count)}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
From 8ad02e4a8c920823034051e5e2a13df58f25ebd5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:22:30 +0200 Subject: [PATCH 28/90] Version Packages (#16493) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/fast-mails-fail.md | 5 ----- .changeset/lemon-weeks-call.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/fast-mails-fail.md delete mode 100644 .changeset/lemon-weeks-call.md diff --git a/.changeset/fast-mails-fail.md b/.changeset/fast-mails-fail.md deleted file mode 100644 index 027cb01548..0000000000 --- a/.changeset/fast-mails-fail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't update a focused input with values from its own past diff --git a/.changeset/lemon-weeks-call.md b/.changeset/lemon-weeks-call.md deleted file mode 100644 index ae62305630..0000000000 --- a/.changeset/lemon-weeks-call.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't destroy effect roots created inside of deriveds diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 5bffa5f70e..450ecde53b 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.36.16 + +### Patch Changes + +- fix: don't update a focused input with values from its own past ([#16491](https://github.com/sveltejs/svelte/pull/16491)) + +- fix: don't destroy effect roots created inside of deriveds ([#16492](https://github.com/sveltejs/svelte/pull/16492)) + ## 5.36.15 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 051f82ec3a..07954026b5 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.36.15", + "version": "5.36.16", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 1d469f29b0..5d76fc3f29 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.36.15'; +export const VERSION = '5.36.16'; export const PUBLIC_VERSION = '5'; From 0bba84cc203c7cab945527cc3928597d3612a37d Mon Sep 17 00:00:00 2001 From: Bladesheng Date: Fri, 25 Jul 2025 14:43:41 +0200 Subject: [PATCH 29/90] fix: add types for `part` attribute to svg attributes (#16499) --- .changeset/seven-colts-obey.md | 5 +++++ packages/svelte/elements.d.ts | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/seven-colts-obey.md diff --git a/.changeset/seven-colts-obey.md b/.changeset/seven-colts-obey.md new file mode 100644 index 0000000000..b41216f649 --- /dev/null +++ b/.changeset/seven-colts-obey.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add types for `part` attribute to svg attributes diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 604241592a..2e1042dfd6 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -1553,6 +1553,7 @@ export interface SVGAttributes extends AriaAttributes, DO height?: number | string | undefined | null; id?: string | undefined | null; lang?: string | undefined | null; + part?: string | undefined | null; max?: number | string | undefined | null; media?: string | undefined | null; // On the `textPath` element From b0f9ea3ae62cfb9eb4437898bf8a6b90e0531657 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 25 Jul 2025 15:57:32 -0700 Subject: [PATCH 30/90] fix: throw on duplicate class field declarations (#16502) * fix: throw on duplicate class field declarations * doh * handle assignment in constructor * fix * apply suggestion from review * fix * fix failing test --- .changeset/odd-phones-taste.md | 5 ++ .../98-reference/.generated/compile-errors.md | 6 +++ .../svelte/messages/compile-errors/script.md | 4 ++ packages/svelte/src/compiler/errors.js | 10 ++++ .../phases/2-analyze/visitors/ClassBody.js | 53 ++++++++++++++++++- .../class-state-constructor-9/errors.json | 12 ++--- 6 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 .changeset/odd-phones-taste.md diff --git a/.changeset/odd-phones-taste.md b/.changeset/odd-phones-taste.md new file mode 100644 index 0000000000..ec9534b741 --- /dev/null +++ b/.changeset/odd-phones-taste.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: throw on duplicate class field declarations diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index 20f57770d1..957a9f67c7 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -364,6 +364,12 @@ The $ name is reserved, and cannot be used for variables and imports The $ prefix is reserved, and cannot be used for variables and imports ``` +### duplicate_class_field + +``` +`%name%` has already been declared +``` + ### each_item_invalid_assignment ``` diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md index 2b0c5eafdf..5c1080aced 100644 --- a/packages/svelte/messages/compile-errors/script.md +++ b/packages/svelte/messages/compile-errors/script.md @@ -30,6 +30,10 @@ > The $ prefix is reserved, and cannot be used for variables and imports +## duplicate_class_field + +> `%name%` has already been declared + ## each_item_invalid_assignment > Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. `array[i] = value` instead of `entry = value`, or `bind:value={array[i]}` instead of `bind:value={entry}`) diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 599d3e8248..e763a6e073 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -152,6 +152,16 @@ export function dollar_prefix_invalid(node) { e(node, 'dollar_prefix_invalid', `The $ prefix is reserved, and cannot be used for variables and imports\nhttps://svelte.dev/e/dollar_prefix_invalid`); } +/** + * `%name%` has already been declared + * @param {null | number | NodeLike} node + * @param {string} name + * @returns {never} + */ +export function duplicate_class_field(node, name) { + e(node, 'duplicate_class_field', `\`${name}\` has already been declared\nhttps://svelte.dev/e/duplicate_class_field`); +} + /** * Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. `array[i] = value` instead of `entry = value`, or `bind:value={array[i]}` instead of `bind:value={entry}`) * @param {null | number | NodeLike} node 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 ffc39ac00d..2bfc1dbce3 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js @@ -33,6 +33,9 @@ export function ClassBody(node, context) { /** @type {Map} */ const state_fields = new Map(); + /** @type {Map>} */ + const fields = new Map(); + context.state.analysis.classes.set(node, state_fields); /** @type {MethodDefinition | null} */ @@ -54,6 +57,14 @@ export function ClassBody(node, context) { e.state_field_duplicate(node, name); } + const _key = (key.type === 'PrivateIdentifier' ? '#' : '') + name; + const field = fields.get(_key); + + // if there's already a method or assigned field, error + if (field && !(field.length === 1 && field[0] === 'prop')) { + e.duplicate_class_field(node, _key); + } + state_fields.set(name, { node, type: rune, @@ -67,10 +78,48 @@ export function ClassBody(node, context) { for (const child of node.body) { if (child.type === 'PropertyDefinition' && !child.computed && !child.static) { handle(child, child.key, child.value); + const key = (child.key.type === 'PrivateIdentifier' ? '#' : '') + get_name(child.key); + const field = fields.get(key); + if (!field) { + fields.set(key, [child.value ? 'assigned_prop' : 'prop']); + continue; + } + e.duplicate_class_field(child, key); } - if (child.type === 'MethodDefinition' && child.kind === 'constructor') { - constructor = child; + if (child.type === 'MethodDefinition') { + if (child.kind === 'constructor') { + constructor = child; + } else if (!child.computed) { + const key = (child.key.type === 'PrivateIdentifier' ? '#' : '') + get_name(child.key); + const field = fields.get(key); + if (!field) { + fields.set(key, [child.kind]); + continue; + } + if ( + field.includes(child.kind) || + field.includes('prop') || + field.includes('assigned_prop') + ) { + e.duplicate_class_field(child, key); + } + if (child.kind === 'get') { + if (field.length === 1 && field[0] === 'set') { + field.push('get'); + continue; + } + } else if (child.kind === 'set') { + if (field.length === 1 && field[0] === 'get') { + field.push('set'); + continue; + } + } else { + field.push(child.kind); + continue; + } + e.duplicate_class_field(child, key); + } } } diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json b/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json index b7dd4c8ed4..94b5f191c2 100644 --- a/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json +++ b/packages/svelte/tests/validator/samples/class-state-constructor-9/errors.json @@ -1,14 +1,14 @@ [ { - "code": "state_field_invalid_assignment", - "message": "Cannot assign to a state field before its declaration", + "code": "duplicate_class_field", + "message": "`count` has already been declared", "start": { - "line": 2, - "column": 1 + "line": 5, + "column": 2 }, "end": { - "line": 2, - "column": 12 + "line": 5, + "column": 24 } } ] From d0ebd42986c4d3e4db0420f1aaebd337afdb230f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:46:12 -0700 Subject: [PATCH 31/90] Version Packages (#16500) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/odd-phones-taste.md | 5 ----- .changeset/seven-colts-obey.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/odd-phones-taste.md delete mode 100644 .changeset/seven-colts-obey.md diff --git a/.changeset/odd-phones-taste.md b/.changeset/odd-phones-taste.md deleted file mode 100644 index ec9534b741..0000000000 --- a/.changeset/odd-phones-taste.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: throw on duplicate class field declarations diff --git a/.changeset/seven-colts-obey.md b/.changeset/seven-colts-obey.md deleted file mode 100644 index b41216f649..0000000000 --- a/.changeset/seven-colts-obey.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: add types for `part` attribute to svg attributes diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 450ecde53b..766c83763a 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.36.17 + +### Patch Changes + +- fix: throw on duplicate class field declarations ([#16502](https://github.com/sveltejs/svelte/pull/16502)) + +- fix: add types for `part` attribute to svg attributes ([#16499](https://github.com/sveltejs/svelte/pull/16499)) + ## 5.36.16 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 07954026b5..22bc8cc8fa 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.36.16", + "version": "5.36.17", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 5d76fc3f29..d2992a3dcd 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.36.16'; +export const VERSION = '5.36.17'; export const PUBLIC_VERSION = '5'; From c7851789d71cc3d0b5d89ce9c759a39be6caa51a Mon Sep 17 00:00:00 2001 From: 7nik Date: Sat, 26 Jul 2025 21:27:22 +0300 Subject: [PATCH 32/90] fix: always mark props as stateful (#16504) --- .changeset/gorgeous-jeans-begin.md | 5 +++++ .../phases/2-analyze/visitors/Identifier.js | 5 ++++- .../props-default-value-function/_config.js | 15 +++++++++++++++ .../props-default-value-function/inner.svelte | 4 ++++ .../props-default-value-function/main.svelte | 14 ++++++++++++++ .../props-default-value-function/wrapper.svelte | 7 +++++++ .../props-default-value-function/wrapper2.svelte | 7 +++++++ 7 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 .changeset/gorgeous-jeans-begin.md create mode 100644 packages/svelte/tests/runtime-runes/samples/props-default-value-function/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/props-default-value-function/inner.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/props-default-value-function/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper2.svelte diff --git a/.changeset/gorgeous-jeans-begin.md b/.changeset/gorgeous-jeans-begin.md new file mode 100644 index 0000000000..586c4d8a57 --- /dev/null +++ b/.changeset/gorgeous-jeans-begin.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: always mark props as stateful diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js index cced326f9b..4dfdfe5af1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -93,7 +93,10 @@ export function Identifier(node, context) { context.state.expression.references.add(binding); context.state.expression.has_state ||= binding.kind !== 'static' && - !binding.is_function() && + (binding.kind === 'prop' || + binding.kind === 'bindable_prop' || + binding.kind === 'rest_prop' || + !binding.is_function()) && !context.state.scope.evaluate(node).is_known; } diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-function/_config.js b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/_config.js new file mode 100644 index 0000000000..6b281f04f0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/_config.js @@ -0,0 +1,15 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + + test({ assert, target }) { + const btn = target.querySelector('button'); + + assert.htmlEqual(target.innerHTML, ` Inner: 0 Inner: 0`); + btn?.click(); + flushSync(); + assert.htmlEqual(target.innerHTML, ` Inner: 1 Inner: 1`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-function/inner.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/inner.svelte new file mode 100644 index 0000000000..6bde0a15a8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/inner.svelte @@ -0,0 +1,4 @@ + +Inner: {getter()} diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-function/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/main.svelte new file mode 100644 index 0000000000..2cb2f67b82 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/main.svelte @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper.svelte new file mode 100644 index 0000000000..525494ddfb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper2.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper2.svelte new file mode 100644 index 0000000000..9498f432d8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-function/wrapper2.svelte @@ -0,0 +1,7 @@ + + + From 7cc4d56263f724c4d76ab6140af4d117cbb41f1d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 26 Jul 2025 22:16:40 -0400 Subject: [PATCH 33/90] feat: ignore component options in `compileModule` (#16362) --- .changeset/violet-ways-sleep.md | 5 + .../svelte/src/compiler/validate-options.js | 172 +++++++++--------- 2 files changed, 96 insertions(+), 81 deletions(-) create mode 100644 .changeset/violet-ways-sleep.md diff --git a/.changeset/violet-ways-sleep.md b/.changeset/violet-ways-sleep.md new file mode 100644 index 0000000000..749ecb1719 --- /dev/null +++ b/.changeset/violet-ways-sleep.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: ignore component options in `compileModule` diff --git a/packages/svelte/src/compiler/validate-options.js b/packages/svelte/src/compiler/validate-options.js index ed83375d22..2b727ad093 100644 --- a/packages/svelte/src/compiler/validate-options.js +++ b/packages/svelte/src/compiler/validate-options.js @@ -8,7 +8,7 @@ import * as w from './warnings.js'; * @typedef {(input: Input, keypath: string) => Required} Validator */ -const common = { +const common_options = { filename: string('(unknown)'), // default to process.cwd() where it exists to replicate svelte4 behavior (and make Deno work with this as well) @@ -48,110 +48,120 @@ const common = { }) }; -export const validate_module_options = - /** @type {Validator} */ ( - object({ - ...common - }) - ); +const component_options = { + accessors: deprecate(w.options_deprecated_accessors, boolean(false)), -export const validate_component_options = - /** @type {Validator} */ ( - object({ - ...common, + css: validator('external', (input) => { + if (input === true || input === false) { + throw_error( + 'The boolean options have been removed from the css option. Use "external" instead of false and "injected" instead of true' + ); + } + if (input === 'none') { + throw_error( + 'css: "none" is no longer a valid option. If this was crucial for you, please open an issue on GitHub with your use case.' + ); + } - accessors: deprecate(w.options_deprecated_accessors, boolean(false)), + if (input !== 'external' && input !== 'injected') { + throw_error(`css should be either "external" (default, recommended) or "injected"`); + } - css: validator('external', (input) => { - if (input === true || input === false) { - throw_error( - 'The boolean options have been removed from the css option. Use "external" instead of false and "injected" instead of true' - ); - } - if (input === 'none') { - throw_error( - 'css: "none" is no longer a valid option. If this was crucial for you, please open an issue on GitHub with your use case.' - ); - } + return input; + }), - if (input !== 'external' && input !== 'injected') { - throw_error(`css should be either "external" (default, recommended) or "injected"`); - } + cssHash: fun(({ css, hash }) => { + return `svelte-${hash(css)}`; + }), + + // TODO this is a sourcemap option, would be good to put under a sourcemap namespace + cssOutputFilename: string(undefined), + + customElement: boolean(false), + + discloseVersion: boolean(true), - return input; - }), + immutable: deprecate(w.options_deprecated_immutable, boolean(false)), - cssHash: fun(({ css, hash }) => { - return `svelte-${hash(css)}`; - }), + legacy: removed( + 'The legacy option has been removed. If you are using this because of legacy.componentApi, use compatibility.componentApi instead' + ), + + compatibility: object({ + componentApi: list([4, 5], 5) + }), + + loopGuardTimeout: warn_removed(w.options_removed_loop_guard_timeout), + + name: string(undefined), - // TODO this is a sourcemap option, would be good to put under a sourcemap namespace - cssOutputFilename: string(undefined), + namespace: list(['html', 'mathml', 'svg']), - customElement: boolean(false), + modernAst: boolean(false), - discloseVersion: boolean(true), + outputFilename: string(undefined), - immutable: deprecate(w.options_deprecated_immutable, boolean(false)), + preserveComments: boolean(false), - legacy: removed( - 'The legacy option has been removed. If you are using this because of legacy.componentApi, use compatibility.componentApi instead' - ), + fragments: list(['html', 'tree']), - compatibility: object({ - componentApi: list([4, 5], 5) - }), + preserveWhitespace: boolean(false), - loopGuardTimeout: warn_removed(w.options_removed_loop_guard_timeout), + runes: boolean(undefined), - name: string(undefined), + hmr: boolean(false), - namespace: list(['html', 'mathml', 'svg']), + sourcemap: validator(undefined, (input) => { + // Source maps can take on a variety of values, including string, JSON, map objects from magic-string and source-map, + // so there's no good way to check type validity here + return input; + }), - modernAst: boolean(false), + enableSourcemap: warn_removed(w.options_removed_enable_sourcemap), - outputFilename: string(undefined), + hydratable: warn_removed(w.options_removed_hydratable), - preserveComments: boolean(false), + format: removed( + 'The format option has been removed in Svelte 4, the compiler only outputs ESM now. Remove "format" from your compiler options. ' + + 'If you did not set this yourself, bump the version of your bundler plugin (vite-plugin-svelte/rollup-plugin-svelte/svelte-loader)' + ), - fragments: list(['html', 'tree']), + tag: removed( + 'The tag option has been removed in Svelte 5. Use `` inside the component instead. ' + + 'If that does not solve your use case, please open an issue on GitHub with details.' + ), - preserveWhitespace: boolean(false), + sveltePath: removed( + 'The sveltePath option has been removed in Svelte 5. ' + + 'If this option was crucial for you, please open an issue on GitHub with your use case.' + ), - runes: boolean(undefined), + // These two were primarily created for svelte-preprocess (https://github.com/sveltejs/svelte/pull/6194), + // but with new TypeScript compilation modes strictly separating types it's not necessary anymore + errorMode: removed( + 'The errorMode option has been removed. If you are using this through svelte-preprocess with TypeScript, ' + + 'use the https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax setting instead' + ), - hmr: boolean(false), + varsReport: removed( + 'The vars option has been removed. If you are using this through svelte-preprocess with TypeScript, ' + + 'use the https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax setting instead' + ) +}; - sourcemap: validator(undefined, (input) => { - // Source maps can take on a variety of values, including string, JSON, map objects from magic-string and source-map, - // so there's no good way to check type validity here - return input; - }), +export const validate_module_options = + /** @type {Validator} */ ( + object({ + ...common_options, + ...Object.fromEntries(Object.keys(component_options).map((key) => [key, () => {}])) + }) + ); - enableSourcemap: warn_removed(w.options_removed_enable_sourcemap), - hydratable: warn_removed(w.options_removed_hydratable), - format: removed( - 'The format option has been removed in Svelte 4, the compiler only outputs ESM now. Remove "format" from your compiler options. ' + - 'If you did not set this yourself, bump the version of your bundler plugin (vite-plugin-svelte/rollup-plugin-svelte/svelte-loader)' - ), - tag: removed( - 'The tag option has been removed in Svelte 5. Use `` inside the component instead. ' + - 'If that does not solve your use case, please open an issue on GitHub with details.' - ), - sveltePath: removed( - 'The sveltePath option has been removed in Svelte 5. ' + - 'If this option was crucial for you, please open an issue on GitHub with your use case.' - ), - // These two were primarily created for svelte-preprocess (https://github.com/sveltejs/svelte/pull/6194), - // but with new TypeScript compilation modes strictly separating types it's not necessary anymore - errorMode: removed( - 'The errorMode option has been removed. If you are using this through svelte-preprocess with TypeScript, ' + - 'use the https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax setting instead' - ), - varsReport: removed( - 'The vars option has been removed. If you are using this through svelte-preprocess with TypeScript, ' + - 'use the https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax setting instead' - ) +export const validate_component_options = + /** @type {Validator} */ ( + object({ + ...common_options, + ...component_options }) ); From 39ee7cf4c247965ebacdd16fc365a31f00506026 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 26 Jul 2025 22:22:14 -0700 Subject: [PATCH 34/90] Version Packages (#16505) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/gorgeous-jeans-begin.md | 5 ----- .changeset/violet-ways-sleep.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 .changeset/gorgeous-jeans-begin.md delete mode 100644 .changeset/violet-ways-sleep.md diff --git a/.changeset/gorgeous-jeans-begin.md b/.changeset/gorgeous-jeans-begin.md deleted file mode 100644 index 586c4d8a57..0000000000 --- a/.changeset/gorgeous-jeans-begin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: always mark props as stateful diff --git a/.changeset/violet-ways-sleep.md b/.changeset/violet-ways-sleep.md deleted file mode 100644 index 749ecb1719..0000000000 --- a/.changeset/violet-ways-sleep.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: ignore component options in `compileModule` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 766c83763a..f939f69d28 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.37.0 + +### Minor Changes + +- feat: ignore component options in `compileModule` ([#16362](https://github.com/sveltejs/svelte/pull/16362)) + +### Patch Changes + +- fix: always mark props as stateful ([#16504](https://github.com/sveltejs/svelte/pull/16504)) + ## 5.36.17 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 22bc8cc8fa..c781eda8c8 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.36.17", + "version": "5.37.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index d2992a3dcd..e83ba6fb30 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.36.17'; +export const VERSION = '5.37.0'; export const PUBLIC_VERSION = '5'; From 1773df94845bc53d55a1c9e9eb033d08056c845a Mon Sep 17 00:00:00 2001 From: fkobi Date: Sun, 27 Jul 2025 14:17:50 +0000 Subject: [PATCH 35/90] docs: use sh instead of bash in source blocks (#16506) * use sh instead of bash in source blocks no bash-specific functionality is used * regenerate --------- Co-authored-by: Rich Harris --- CONTRIBUTING.md | 4 ++-- documentation/docs/01-introduction/02-getting-started.md | 2 +- documentation/docs/07-misc/02-testing.md | 4 ++-- .../docs/98-reference/.generated/compile-warnings.md | 2 +- packages/svelte/README.md | 2 +- packages/svelte/messages/compile-warnings/template.md | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2d3e45049..0653b08b76 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -101,13 +101,13 @@ Test samples are kept in `/test/xxx/samples` folder. 1. To run test, run `pnpm test`. 1. To run a particular test suite, use `pnpm test `, for example: - ```bash + ```sh pnpm test validator ``` 1. To filter tests _within_ a test suite, use `pnpm test -t `, for example: - ```bash + ```sh pnpm test validator -t a11y-alt-text ``` diff --git a/documentation/docs/01-introduction/02-getting-started.md b/documentation/docs/01-introduction/02-getting-started.md index c7351729ff..e97a46ad34 100644 --- a/documentation/docs/01-introduction/02-getting-started.md +++ b/documentation/docs/01-introduction/02-getting-started.md @@ -4,7 +4,7 @@ title: Getting started We recommend using [SvelteKit](../kit), which lets you [build almost anything](../kit/project-types). It's the official application framework from the Svelte team and powered by [Vite](https://vite.dev/). Create a new project with: -```bash +```sh npx sv create myapp cd myapp npm install diff --git a/documentation/docs/07-misc/02-testing.md b/documentation/docs/07-misc/02-testing.md index db99b70770..bcec4db0a3 100644 --- a/documentation/docs/07-misc/02-testing.md +++ b/documentation/docs/07-misc/02-testing.md @@ -10,7 +10,7 @@ Unit tests allow you to test small isolated parts of your code. Integration test To setup Vitest manually, first install it: -```bash +```sh npm install -D vitest ``` @@ -166,7 +166,7 @@ It is possible to test your components in isolation using Vitest. To get started, install jsdom (a library that shims DOM APIs): -```bash +```sh npm install -D jsdom ``` diff --git a/documentation/docs/98-reference/.generated/compile-warnings.md b/documentation/docs/98-reference/.generated/compile-warnings.md index 2af9021a6a..01003f30c5 100644 --- a/documentation/docs/98-reference/.generated/compile-warnings.md +++ b/documentation/docs/98-reference/.generated/compile-warnings.md @@ -683,7 +683,7 @@ Some templating languages (including Svelte) will 'fix' HTML by turning ` Date: Sun, 27 Jul 2025 19:50:22 +0200 Subject: [PATCH 36/90] fix: `append_styles` in an effect to make them available on mount (#16509) Co-authored-by: Rich Harris --- .changeset/shaggy-comics-fail.md | 5 +++++ .changeset/wise-hairs-pay.md | 5 +++++ .../src/compiler/phases/2-analyze/index.js | 16 +++++++------- .../3-transform/client/transform-client.js | 5 +++-- .../svelte/src/internal/client/dom/css.js | 4 ++-- .../host-rune-access-injected-css/_config.js | 21 +++++++++++++++++++ .../host-rune-access-injected-css/main.svelte | 16 ++++++++++++++ .../Thing.svelte | 9 ++++++++ .../custom-element-injected-styles/_config.js | 15 +++++++++++++ .../main.svelte | 5 +++++ 10 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 .changeset/shaggy-comics-fail.md create mode 100644 .changeset/wise-hairs-pay.md create mode 100644 packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune-access-injected-css/_config.js create mode 100644 packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune-access-injected-css/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/Thing.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/main.svelte diff --git a/.changeset/shaggy-comics-fail.md b/.changeset/shaggy-comics-fail.md new file mode 100644 index 0000000000..981a25c978 --- /dev/null +++ b/.changeset/shaggy-comics-fail.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: `append_styles` in an effect to make them available on mount diff --git a/.changeset/wise-hairs-pay.md b/.changeset/wise-hairs-pay.md new file mode 100644 index 0000000000..7d96c1daab --- /dev/null +++ b/.changeset/wise-hairs-pay.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: always inject styles when compiling as a custom element diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index d407b44556..cd44fd998a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -451,6 +451,8 @@ export function analyze_component(root, source, options) { } } + const is_custom_element = !!options.customElementOptions || options.customElement; + // TODO remove all the ?? stuff, we don't need it now that we're validating the config /** @type {ComponentAnalysis} */ const analysis = { @@ -500,13 +502,13 @@ export function analyze_component(root, source, options) { needs_props: false, event_directive_node: null, uses_event_attributes: false, - custom_element: options.customElementOptions ?? options.customElement, - inject_styles: options.css === 'injected' || options.customElement, - accessors: options.customElement - ? true - : (runes ? false : !!options.accessors) || - // because $set method needs accessors - options.compatibility?.componentApi === 4, + custom_element: is_custom_element, + inject_styles: options.css === 'injected' || is_custom_element, + accessors: + is_custom_element || + (runes ? false : !!options.accessors) || + // because $set method needs accessors + options.compatibility?.componentApi === 4, reactive_statements: new Map(), binding_groups: new Map(), slot_names: new Map(), 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 124438a9da..a56aca9c5f 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 @@ -621,8 +621,9 @@ export function client_component(analysis, options) { ); } - if (analysis.custom_element) { - const ce = analysis.custom_element; + const ce = options.customElementOptions ?? options.customElement; + + if (ce) { const ce_props = typeof ce === 'boolean' ? {} : ce.props || {}; /** @type {ESTree.Property[]} */ diff --git a/packages/svelte/src/internal/client/dom/css.js b/packages/svelte/src/internal/client/dom/css.js index 52be36aa1f..8e6faa0e32 100644 --- a/packages/svelte/src/internal/client/dom/css.js +++ b/packages/svelte/src/internal/client/dom/css.js @@ -1,6 +1,6 @@ import { DEV } from 'esm-env'; -import { queue_micro_task } from './task.js'; import { register_style } from '../dev/css.js'; +import { effect } from '../reactivity/effects.js'; /** * @param {Node} anchor @@ -8,7 +8,7 @@ import { register_style } from '../dev/css.js'; */ export function append_styles(anchor, css) { // Use `queue_micro_task` to ensure `anchor` is in the DOM, otherwise getRootNode() will yield wrong results - queue_micro_task(() => { + effect(() => { var root = anchor.getRootNode(); var target = /** @type {ShadowRoot} */ (root).host diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune-access-injected-css/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune-access-injected-css/_config.js new file mode 100644 index 0000000000..99a223492b --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune-access-injected-css/_config.js @@ -0,0 +1,21 @@ +import { test } from '../../assert'; +const tick = () => Promise.resolve(); + +export default test({ + async test({ assert, target }) { + target.innerHTML = ''; + /** @type {any} */ + const el = target.querySelector('custom-element'); + + /** @type {string} */ + let html = ''; + const handle_evt = (e) => (html = e.detail); + el.addEventListener('html', handle_evt); + + await tick(); + await tick(); + await tick(); + + assert.ok(html.includes(' + + + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/Thing.svelte b/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/Thing.svelte new file mode 100644 index 0000000000..0a2b139274 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/Thing.svelte @@ -0,0 +1,9 @@ + + +

hello

+ + diff --git a/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/_config.js b/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/_config.js new file mode 100644 index 0000000000..74597504bd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/_config.js @@ -0,0 +1,15 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + async test({ assert, target }) { + const thing = /** @type HTMLElement & { object: { test: true }; } */ ( + target.querySelector('my-thing') + ); + + await tick(); + + assert.include(thing.shadowRoot?.innerHTML, 'red'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/main.svelte b/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/main.svelte new file mode 100644 index 0000000000..ba5b788da9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/custom-element-injected-styles/main.svelte @@ -0,0 +1,5 @@ + + + From 51771447c6cf941da7a07733435a5b18a91ddd28 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 27 Jul 2025 16:59:29 -0400 Subject: [PATCH 37/90] chore: remove `parser.template_untrimmed` (#16511) --- .changeset/shiny-berries-call.md | 5 +++++ packages/svelte/src/compiler/phases/1-parse/index.js | 7 ------- .../svelte/src/compiler/phases/1-parse/state/element.js | 8 -------- 3 files changed, 5 insertions(+), 15 deletions(-) create mode 100644 .changeset/shiny-berries-call.md diff --git a/.changeset/shiny-berries-call.md b/.changeset/shiny-berries-call.md new file mode 100644 index 0000000000..adc62b3cd0 --- /dev/null +++ b/.changeset/shiny-berries-call.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: remove `parser.template_untrimmed` diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index 77cc2bf3fa..f5e0693b31 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -23,12 +23,6 @@ export class Parser { */ template; - /** - * @readonly - * @type {string} - */ - template_untrimmed; - /** * Whether or not we're in loose parsing mode, in which * case we try to continue parsing as much as possible @@ -67,7 +61,6 @@ export class Parser { } this.loose = loose; - this.template_untrimmed = template; this.template = template.trimEnd(); let match_lang; diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 87332f647d..ed1b047d55 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -370,14 +370,6 @@ export default function element(parser) { // ... or we're followed by whitespace, for example near the end of the template, // which we want to take in so that language tools has more room to work with parser.allow_whitespace(); - if (parser.index === parser.template.length) { - while ( - parser.index < parser.template_untrimmed.length && - regex_whitespace.test(parser.template_untrimmed[parser.index]) - ) { - parser.index++; - } - } } } } From 03f2e44757925d0ca53f4e563965c3135ef81f16 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Mon, 28 Jul 2025 11:56:41 -0700 Subject: [PATCH 38/90] chore: remove some todos (#16515) * chore: remove some todos * fix * fix * doh * convert `TemplateComment` back to `Comment` * `Evaluation.has_unknown` --- .changeset/happy-countries-dance.md | 5 +++++ packages/svelte/src/compiler/migrate/index.js | 4 ++-- packages/svelte/src/compiler/phases/1-parse/index.js | 1 - .../3-transform/client/visitors/CallExpression.js | 4 +++- .../3-transform/client/visitors/OnDirective.js | 4 ++-- packages/svelte/src/compiler/phases/scope.js | 11 +++++++++++ packages/svelte/src/compiler/types/template.d.ts | 12 +++++++++++- packages/svelte/types/index.d.ts | 12 +++++++++++- 8 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 .changeset/happy-countries-dance.md diff --git a/.changeset/happy-countries-dance.md b/.changeset/happy-countries-dance.md new file mode 100644 index 0000000000..641cf21fd8 --- /dev/null +++ b/.changeset/happy-countries-dance.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: remove some todos diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 6b2e6cda70..eb0e4eff8c 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -1707,14 +1707,14 @@ function extract_type_and_comment(declarator, state, path) { } // Ensure modifiers are applied in the same order as Svelte 4 -const modifier_order = [ +const modifier_order = /** @type {const} */ ([ 'preventDefault', 'stopPropagation', 'stopImmediatePropagation', 'self', 'trusted', 'once' -]; +]); /** * @param {AST.RegularElement | AST.SvelteElement | AST.SvelteWindow | AST.SvelteDocument | AST.SvelteBody} element diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index f5e0693b31..8f7ef76be5 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -1,5 +1,4 @@ /** @import { AST } from '#compiler' */ -/** @import { Comment } from 'estree' */ // @ts-expect-error acorn type definitions are borked in the release we use import { isIdentifierStart, isIdentifierChar } from 'acorn'; import fragment from './state/fragment.js'; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index 3e2f1414e6..c126742d3c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -82,7 +82,9 @@ export function CallExpression(node, context) { ['debug', 'dir', 'error', 'group', 'groupCollapsed', 'info', 'log', 'trace', 'warn'].includes( node.callee.property.name ) && - node.arguments.some((arg) => arg.type !== 'Literal') // TODO more cases? + node.arguments.some( + (arg) => arg.type === 'SpreadElement' || context.state.scope.evaluate(arg).has_unknown + ) ) { return b.call( node.callee, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js index 7a66a8ecbb..0ee3b0fb10 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js @@ -3,14 +3,14 @@ import * as b from '#compiler/builders'; import { build_event, build_event_handler } from './shared/events.js'; -const modifiers = [ +const modifiers = /** @type {const} */ ([ 'stopPropagation', 'stopImmediatePropagation', 'preventDefault', 'self', 'trusted', 'once' -]; +]); /** * @param {AST.OnDirective} node diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 700e098e45..eaacf5dda9 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -180,6 +180,13 @@ class Evaluation { */ is_known = true; + /** + * True if the possible values contains `UNKNOWN` + * @readonly + * @type {boolean} + */ + has_unknown = false; + /** * True if the value is known to not be null/undefined * @readonly @@ -540,6 +547,10 @@ class Evaluation { if (value == null || value === UNKNOWN) { this.is_defined = false; } + + if (value === UNKNOWN) { + this.has_unknown = true; + } } if (this.values.size > 1 || typeof this.value === 'symbol') { diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index de06a41469..058a1a8e66 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -247,7 +247,17 @@ export namespace AST { name: string; /** The 'y' in `on:x={y}` */ expression: null | Expression; - modifiers: string[]; // TODO specify + modifiers: Array< + | 'capture' + | 'nonpassive' + | 'once' + | 'passive' + | 'preventDefault' + | 'self' + | 'stopImmediatePropagation' + | 'stopPropagation' + | 'trusted' + >; /** @internal */ metadata: { expression: ExpressionMetadata; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 9ea45af7e6..64aa9e23ba 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1296,7 +1296,17 @@ declare module 'svelte/compiler' { name: string; /** The 'y' in `on:x={y}` */ expression: null | Expression; - modifiers: string[]; + modifiers: Array< + | 'capture' + | 'nonpassive' + | 'once' + | 'passive' + | 'preventDefault' + | 'self' + | 'stopImmediatePropagation' + | 'stopPropagation' + | 'trusted' + >; } /** A `style:` directive */ From 48f2fa22c0c941ac6e550151212845adc628b3de Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:04:06 -0700 Subject: [PATCH 39/90] fix: allow await expressions inside `{#await ...}` argument (#16514) * fix: allow await expressions inside `{#await ...}` argument * add test * apply suggestion from review --------- Co-authored-by: 7nik --- .changeset/long-roses-train.md | 5 +++ .../3-transform/client/visitors/AwaitBlock.js | 7 ++- .../samples/async-await/_config.js | 43 +++++++++++++++++++ .../samples/async-await/main.svelte | 22 ++++++++++ 4 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 .changeset/long-roses-train.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-await/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-await/main.svelte diff --git a/.changeset/long-roses-train.md b/.changeset/long-roses-train.md new file mode 100644 index 0000000000..10920ac5c4 --- /dev/null +++ b/.changeset/long-roses-train.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow await expressions inside `{#await ...}` argument diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js index c550c8e17b..4246091bcf 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -1,7 +1,7 @@ /** @import { BlockStatement, Pattern, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */ -import { extract_identifiers } from '../../../../utils/ast.js'; +import { extract_identifiers, is_expression_async } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; import { get_value } from './shared/declarations.js'; @@ -15,7 +15,10 @@ export function AwaitBlock(node, context) { context.state.template.push_comment(); // Visit {#await } first to ensure that scopes are in the correct order - const expression = b.thunk(build_expression(context, node.expression, node.metadata.expression)); + const expression = b.thunk( + build_expression(context, node.expression, node.metadata.expression), + node.metadata.expression.has_await + ); let then_block; let catch_block; diff --git a/packages/svelte/tests/runtime-runes/samples/async-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-await/_config.js new file mode 100644 index 0000000000..dda6a7a895 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-await/_config.js @@ -0,0 +1,43 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [reset, one, two, reject] = target.querySelectorAll('button'); + + await tick(); + assert.htmlEqual( + target.innerHTML, + ' waiting' + ); + + one.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' one_res' + ); + + reset.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' waiting' + ); + + two.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' two_res' + ); + + reset.click(); + reject.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ' reject_catch' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-await/main.svelte new file mode 100644 index 0000000000..8673e45414 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-await/main.svelte @@ -0,0 +1,22 @@ + + + + + + + + + {#await await deferred.promise + "_res"} + waiting + {:then res} + {res} + {:catch err} + {err}_catch + {/await} + + {#snippet pending()} +

pending

+ {/snippet} +
From d82edf6b1dfaac8af70462b1009f3b9da47a701a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:07:57 -0700 Subject: [PATCH 40/90] Version Packages (#16513) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/happy-countries-dance.md | 5 ----- .changeset/long-roses-train.md | 5 ----- .changeset/shaggy-comics-fail.md | 5 ----- .changeset/shiny-berries-call.md | 5 ----- .changeset/wise-hairs-pay.md | 5 ----- packages/svelte/CHANGELOG.md | 14 ++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 8 files changed, 16 insertions(+), 27 deletions(-) delete mode 100644 .changeset/happy-countries-dance.md delete mode 100644 .changeset/long-roses-train.md delete mode 100644 .changeset/shaggy-comics-fail.md delete mode 100644 .changeset/shiny-berries-call.md delete mode 100644 .changeset/wise-hairs-pay.md diff --git a/.changeset/happy-countries-dance.md b/.changeset/happy-countries-dance.md deleted file mode 100644 index 641cf21fd8..0000000000 --- a/.changeset/happy-countries-dance.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: remove some todos diff --git a/.changeset/long-roses-train.md b/.changeset/long-roses-train.md deleted file mode 100644 index 10920ac5c4..0000000000 --- a/.changeset/long-roses-train.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: allow await expressions inside `{#await ...}` argument diff --git a/.changeset/shaggy-comics-fail.md b/.changeset/shaggy-comics-fail.md deleted file mode 100644 index 981a25c978..0000000000 --- a/.changeset/shaggy-comics-fail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: `append_styles` in an effect to make them available on mount diff --git a/.changeset/shiny-berries-call.md b/.changeset/shiny-berries-call.md deleted file mode 100644 index adc62b3cd0..0000000000 --- a/.changeset/shiny-berries-call.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: remove `parser.template_untrimmed` diff --git a/.changeset/wise-hairs-pay.md b/.changeset/wise-hairs-pay.md deleted file mode 100644 index 7d96c1daab..0000000000 --- a/.changeset/wise-hairs-pay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: always inject styles when compiling as a custom element diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index f939f69d28..59c44dd4f9 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,19 @@ # svelte +## 5.37.1 + +### Patch Changes + +- chore: remove some todos ([#16515](https://github.com/sveltejs/svelte/pull/16515)) + +- fix: allow await expressions inside `{#await ...}` argument ([#16514](https://github.com/sveltejs/svelte/pull/16514)) + +- fix: `append_styles` in an effect to make them available on mount ([#16509](https://github.com/sveltejs/svelte/pull/16509)) + +- chore: remove `parser.template_untrimmed` ([#16511](https://github.com/sveltejs/svelte/pull/16511)) + +- fix: always inject styles when compiling as a custom element ([#16509](https://github.com/sveltejs/svelte/pull/16509)) + ## 5.37.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c781eda8c8..826850ea60 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.37.0", + "version": "5.37.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index e83ba6fb30..2d5083a2a5 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.37.0'; +export const VERSION = '5.37.1'; export const PUBLIC_VERSION = '5'; From a91e0154d4bc4b0a3209956647f868e518d0acba Mon Sep 17 00:00:00 2001 From: Laszlo Korte Date: Thu, 31 Jul 2025 09:45:52 +0200 Subject: [PATCH 41/90] fix: double event processing in Firefox (#16522) (#16527) * Fix double event processing (#16522) Firefox seems to garbage collect event objects during event propagation if no global reference to the event object is kept. That discards the __root marker set on the event object to early, leading to duplicate processing. * minor tweaks --------- Co-authored-by: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> --- .changeset/chilly-bananas-train.md | 5 +++++ .../svelte/src/internal/client/dom/elements/events.js | 9 +++++++++ 2 files changed, 14 insertions(+) create mode 100644 .changeset/chilly-bananas-train.md diff --git a/.changeset/chilly-bananas-train.md b/.changeset/chilly-bananas-train.md new file mode 100644 index 0000000000..b0305f9e61 --- /dev/null +++ b/.changeset/chilly-bananas-train.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: double event processing in firefox due to event object being garbage collected diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index fa3bf0b021..19bd1cfa18 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -141,6 +141,13 @@ export function delegate(events) { } } +// used to store the reference to the currently propagated event +// to prevent garbage collection between microtasks in Firefox +// If the event object is GCed too early, the expando __root property +// set on the event object is lost, causing the event delegation +// to process the event twice +let last_propagated_event = null; + /** * @this {EventTarget} * @param {Event} event @@ -153,6 +160,8 @@ export function handle_event_propagation(event) { var path = event.composedPath?.() || []; var current_target = /** @type {null | Element} */ (path[0] || event.target); + last_propagated_event = event; + // composedPath contains list of nodes the event has propagated through. // We check __root to skip all nodes below it in case this is a // parent of the __root node, which indicates that there's nested From f5950f866c51d9325a432ebfd5362bcf2647763a Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 31 Jul 2025 02:11:52 -0700 Subject: [PATCH 42/90] fix: correctly differentiate static fields before emitting `duplicate_class_field` (#16526) * fix: correctly differentiate static fields before emitting `duplicate_class_field` * remove unnecessary `#` concatenation * add test --- .changeset/nine-cups-film.md | 5 +++++ .../src/compiler/phases/2-analyze/visitors/ClassBody.js | 6 +++--- .../samples/class-state-constructor-9/input.svelte.js | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 .changeset/nine-cups-film.md diff --git a/.changeset/nine-cups-film.md b/.changeset/nine-cups-film.md new file mode 100644 index 0000000000..ba72337dac --- /dev/null +++ b/.changeset/nine-cups-film.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly differentiate static fields before emitting `duplicate_class_field` 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 2bfc1dbce3..dd21637174 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ClassBody.js @@ -57,7 +57,7 @@ export function ClassBody(node, context) { e.state_field_duplicate(node, name); } - const _key = (key.type === 'PrivateIdentifier' ? '#' : '') + name; + const _key = (node.type === 'AssignmentExpression' || !node.static ? '' : '@') + name; const field = fields.get(_key); // if there's already a method or assigned field, error @@ -78,7 +78,7 @@ export function ClassBody(node, context) { for (const child of node.body) { if (child.type === 'PropertyDefinition' && !child.computed && !child.static) { handle(child, child.key, child.value); - const key = (child.key.type === 'PrivateIdentifier' ? '#' : '') + get_name(child.key); + const key = /** @type {string} */ (get_name(child.key)); const field = fields.get(key); if (!field) { fields.set(key, [child.value ? 'assigned_prop' : 'prop']); @@ -91,7 +91,7 @@ export function ClassBody(node, context) { if (child.kind === 'constructor') { constructor = child; } else if (!child.computed) { - const key = (child.key.type === 'PrivateIdentifier' ? '#' : '') + get_name(child.key); + const key = (child.static ? '@' : '') + get_name(child.key); const field = fields.get(key); if (!field) { fields.set(key, [child.kind]); diff --git a/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js b/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js index a8469e13af..3806046f3f 100644 --- a/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js +++ b/packages/svelte/tests/validator/samples/class-state-constructor-9/input.svelte.js @@ -1,6 +1,6 @@ export class Counter { count = -1; - + static count() {} constructor() { this.count = $state(0); } From 72e46d31c075eeea232a1cc320efc272f5ef87d7 Mon Sep 17 00:00:00 2001 From: Elliot Bentley Date: Thu, 31 Jul 2025 11:34:28 +0100 Subject: [PATCH 43/90] fix: add types for SVG bindable attributes (#16525) * fix: types for SVG bind: attributes * add changeset and tweak --------- Co-authored-by: 7nik --- .changeset/cuddly-feet-doubt.md | 5 +++++ packages/svelte/elements.d.ts | 14 ++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 .changeset/cuddly-feet-doubt.md diff --git a/.changeset/cuddly-feet-doubt.md b/.changeset/cuddly-feet-doubt.md new file mode 100644 index 0000000000..8d7d955daa --- /dev/null +++ b/.changeset/cuddly-feet-doubt.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add bindable dimension attributes types to SVG and MathML elements diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 2e1042dfd6..b3d44d9ed6 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -464,6 +464,14 @@ export interface DOMAttributes { onfullscreenerror?: EventHandler | undefined | null; onfullscreenerrorcapture?: EventHandler | undefined | null; + // Dimensions + readonly 'bind:contentRect'?: DOMRectReadOnly | undefined | null; + readonly 'bind:contentBoxSize'?: Array | undefined | null; + readonly 'bind:borderBoxSize'?: Array | undefined | null; + readonly 'bind:devicePixelContentBoxSize'?: Array | undefined | null; + readonly 'bind:clientWidth'?: number | undefined | null; + readonly 'bind:clientHeight'?: number | undefined | null; + xmlns?: string | undefined | null; } @@ -839,13 +847,7 @@ export interface HTMLAttributes extends AriaAttributes, D */ 'bind:innerText'?: string | undefined | null; - readonly 'bind:contentRect'?: DOMRectReadOnly | undefined | null; - readonly 'bind:contentBoxSize'?: Array | undefined | null; - readonly 'bind:borderBoxSize'?: Array | undefined | null; - readonly 'bind:devicePixelContentBoxSize'?: Array | undefined | null; readonly 'bind:focused'?: boolean | undefined | null; - readonly 'bind:clientWidth'?: number | undefined | null; - readonly 'bind:clientHeight'?: number | undefined | null; readonly 'bind:offsetWidth'?: number | undefined | null; readonly 'bind:offsetHeight'?: number | undefined | null; From c04975d3db74e59f8612ffd0f64d3b3bb3133ef1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 31 Jul 2025 18:28:01 -0400 Subject: [PATCH 44/90] fix: prevent last_propagated_event from being DCE'd (#16538) * fix: prevent last_propagated_event from being DCE'd * changeset --- .changeset/serious-cars-hear.md | 5 +++++ packages/svelte/src/internal/client/dom/elements/events.js | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .changeset/serious-cars-hear.md diff --git a/.changeset/serious-cars-hear.md b/.changeset/serious-cars-hear.md new file mode 100644 index 0000000000..4647e89ed9 --- /dev/null +++ b/.changeset/serious-cars-hear.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent last_propagated_event from being DCE'd diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 19bd1cfa18..15544d7426 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -168,8 +168,11 @@ export function handle_event_propagation(event) { // mounted apps. In this case we don't want to trigger events multiple times. var path_idx = 0; + // the `last_propagated_event === event` check is redundant, but + // without it the variable will be DCE'd and things will + // fail mysteriously in Firefox // @ts-expect-error is added below - var handled_at = event.__root; + var handled_at = last_propagated_event === event && event.__root; if (handled_at) { var at_idx = path.indexOf(handled_at); From 9134856fece750608ba81da45c4a330b3d8ebb1e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 31 Jul 2025 19:15:45 -0400 Subject: [PATCH 45/90] Version Packages (#16529) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/chilly-bananas-train.md | 5 ----- .changeset/cuddly-feet-doubt.md | 5 ----- .changeset/nine-cups-film.md | 5 ----- .changeset/serious-cars-hear.md | 5 ----- packages/svelte/CHANGELOG.md | 12 ++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 7 files changed, 14 insertions(+), 22 deletions(-) delete mode 100644 .changeset/chilly-bananas-train.md delete mode 100644 .changeset/cuddly-feet-doubt.md delete mode 100644 .changeset/nine-cups-film.md delete mode 100644 .changeset/serious-cars-hear.md diff --git a/.changeset/chilly-bananas-train.md b/.changeset/chilly-bananas-train.md deleted file mode 100644 index b0305f9e61..0000000000 --- a/.changeset/chilly-bananas-train.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: double event processing in firefox due to event object being garbage collected diff --git a/.changeset/cuddly-feet-doubt.md b/.changeset/cuddly-feet-doubt.md deleted file mode 100644 index 8d7d955daa..0000000000 --- a/.changeset/cuddly-feet-doubt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: add bindable dimension attributes types to SVG and MathML elements diff --git a/.changeset/nine-cups-film.md b/.changeset/nine-cups-film.md deleted file mode 100644 index ba72337dac..0000000000 --- a/.changeset/nine-cups-film.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly differentiate static fields before emitting `duplicate_class_field` diff --git a/.changeset/serious-cars-hear.md b/.changeset/serious-cars-hear.md deleted file mode 100644 index 4647e89ed9..0000000000 --- a/.changeset/serious-cars-hear.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: prevent last_propagated_event from being DCE'd diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 59c44dd4f9..6e0a199ddc 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.37.2 + +### Patch Changes + +- fix: double event processing in firefox due to event object being garbage collected ([#16527](https://github.com/sveltejs/svelte/pull/16527)) + +- fix: add bindable dimension attributes types to SVG and MathML elements ([#16525](https://github.com/sveltejs/svelte/pull/16525)) + +- fix: correctly differentiate static fields before emitting `duplicate_class_field` ([#16526](https://github.com/sveltejs/svelte/pull/16526)) + +- fix: prevent last_propagated_event from being DCE'd ([#16538](https://github.com/sveltejs/svelte/pull/16538)) + ## 5.37.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 826850ea60..8536135ca8 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.37.1", + "version": "5.37.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 2d5083a2a5..b8f3bcfdd5 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.37.1'; +export const VERSION = '5.37.2'; export const PUBLIC_VERSION = '5'; From 80678411706e99c0224b8bf995959c2d4e7fd4db Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Thu, 31 Jul 2025 16:16:47 -0700 Subject: [PATCH 46/90] docs: add note about attachments to `svelte/action` docs (#16537) --- documentation/docs/98-reference/21-svelte-action.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/docs/98-reference/21-svelte-action.md b/documentation/docs/98-reference/21-svelte-action.md index 53423ec409..ef3ebfbf70 100644 --- a/documentation/docs/98-reference/21-svelte-action.md +++ b/documentation/docs/98-reference/21-svelte-action.md @@ -2,4 +2,6 @@ title: svelte/action --- +This module provides types for [actions](use), which have been superseded by [attachments](@attach). + > MODULE: svelte/action From 540f1b19ec56a69dde85f24471f25d9d465937e4 Mon Sep 17 00:00:00 2001 From: 7nik Date: Sat, 2 Aug 2025 22:29:10 +0300 Subject: [PATCH 47/90] fix: reset attribute cache after setting corresponding property (#16543) --- .changeset/afraid-carrots-study.md | 5 +++++ .../client/dom/elements/attributes.js | 4 +++- .../attribute-after-property/_config.js | 19 +++++++++++++++++++ .../attribute-after-property/main.svelte | 6 ++++++ 4 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 .changeset/afraid-carrots-study.md create mode 100644 packages/svelte/tests/runtime-legacy/samples/attribute-after-property/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/attribute-after-property/main.svelte diff --git a/.changeset/afraid-carrots-study.md b/.changeset/afraid-carrots-study.md new file mode 100644 index 0000000000..71f4232edb --- /dev/null +++ b/.changeset/afraid-carrots-study.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: reset attribute cache after setting corresponding property diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 22e532f5e4..2fa5d4541c 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -19,7 +19,7 @@ import { attach } from './attachments.js'; import { clsx } from '../../../shared/attributes.js'; import { set_class } from './class.js'; import { set_style } from './style.js'; -import { ATTACHMENT_KEY, NAMESPACE_HTML } from '../../../../constants.js'; +import { ATTACHMENT_KEY, NAMESPACE_HTML, UNINITIALIZED } from '../../../../constants.js'; import { block, branch, destroy_effect, effect } from '../../reactivity/effects.js'; import { init_select, select_option } from './bindings/select.js'; import { flatten } from '../../reactivity/async.js'; @@ -446,6 +446,8 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal ) { // @ts-ignore element[name] = value; + // remove it from attributes's cache + if (name in attributes) attributes[name] = UNINITIALIZED; } else if (typeof value !== 'function') { set_attribute(element, name, value, skip_warning); } diff --git a/packages/svelte/tests/runtime-legacy/samples/attribute-after-property/_config.js b/packages/svelte/tests/runtime-legacy/samples/attribute-after-property/_config.js new file mode 100644 index 0000000000..f6a98b1797 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/attribute-after-property/_config.js @@ -0,0 +1,19 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ target, assert }) { + const input = target.querySelector('input'); + const button = target.querySelector('button'); + + assert.equal(input?.step, 'any'); + + button?.click(); + flushSync(); + assert.equal(input?.step, '10'); + + button?.click(); + flushSync(); + assert.equal(input?.step, 'any'); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/attribute-after-property/main.svelte b/packages/svelte/tests/runtime-legacy/samples/attribute-after-property/main.svelte new file mode 100644 index 0000000000..2921e4e241 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/attribute-after-property/main.svelte @@ -0,0 +1,6 @@ + + + + \ No newline at end of file From 0cbb5becf6d62ae3334b78606a76d21a91eda32b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 2 Aug 2025 21:17:58 -0700 Subject: [PATCH 48/90] Version Packages (#16544) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/afraid-carrots-study.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/afraid-carrots-study.md diff --git a/.changeset/afraid-carrots-study.md b/.changeset/afraid-carrots-study.md deleted file mode 100644 index 71f4232edb..0000000000 --- a/.changeset/afraid-carrots-study.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: reset attribute cache after setting corresponding property diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 6e0a199ddc..9766e3f06b 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.37.3 + +### Patch Changes + +- fix: reset attribute cache after setting corresponding property ([#16543](https://github.com/sveltejs/svelte/pull/16543)) + ## 5.37.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 8536135ca8..e8927fcf56 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.37.2", + "version": "5.37.3", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index b8f3bcfdd5..127b43e205 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.37.2'; +export const VERSION = '5.37.3'; export const PUBLIC_VERSION = '5'; From a3f18351af4f70df4b9029aaa7198ac5dfeac6dc Mon Sep 17 00:00:00 2001 From: Anthony Date: Tue, 5 Aug 2025 05:38:30 +0200 Subject: [PATCH 49/90] docs: corrections (#16550) * Add missing hyphen in "server-side rendering" * Fix incorrect use of "as such" * regenerate types --------- Co-authored-by: Chew Tee Ming --- documentation/docs/02-runes/04-$effect.md | 2 +- documentation/docs/03-template-syntax/06-snippet.md | 2 +- documentation/docs/03-template-syntax/08-@html.md | 2 +- .../docs/05-special-elements/07-svelte-options.md | 2 +- documentation/docs/06-runtime/03-lifecycle-hooks.md | 2 +- documentation/docs/07-misc/07-v5-migration-guide.md | 12 ++++++------ .../docs/98-reference/.generated/compile-errors.md | 2 +- .../docs/98-reference/.generated/compile-warnings.md | 2 +- packages/svelte/messages/compile-errors/script.md | 2 +- .../svelte/messages/compile-warnings/template.md | 2 +- packages/svelte/src/ambient.d.ts | 4 ++-- packages/svelte/types/index.d.ts | 4 ++-- 12 files changed, 19 insertions(+), 19 deletions(-) diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index 5820e178a0..6c42f55795 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -135,7 +135,7 @@ An effect only reruns when the object it reads changes, not when a property insi An effect only depends on the values that it read the last time it ran. This has interesting implications for effects that have conditional code. -For instance, if `condition` is `true` in the code snippet below, the code inside the `if` block will run and `color` will be evaluated. As such, changes to either `condition` or `color` [will cause the effect to re-run](/playground/untitled#H4sIAAAAAAAAE21RQW6DMBD8ytaNBJHaJFLViwNIVZ8RcnBgXVk1xsILTYT4e20TQg89IOPZ2fHM7siMaJBx9tmaWpFqjQNlAKXEihx7YVJpdIyfRkY3G4gB8Pi97cPanRtQU8AuwuF_eNUaQuPlOMtc1SlLRWlKUo1tOwJflUikQHZtA0klzCDc64Imx0ANn8bInV1CDhtHgjClrsftcSXotluLybOUb3g4JJHhOZs5WZpuIS9gjNqkJKQP5e2ClrR4SMdZ13E4xZ8zTPOTJU2A2uE_PQ9COCI926_hTVarIU4hu_REPlBrKq2q73ycrf1N-vS4TMUsulaVg3EtR8H9rFgsg8uUsT1B2F9eshigZHBRpuaD0D3mY8Qm2BfB5N2YyRzdNEYVDy0Ja-WsFjcOUuP1HvFLWA6H3XuHTUSmmDV2--0TXonxsKbp7G9C6R__NONS-MFNvxj_d6mBAgAA). +For instance, if `condition` is `true` in the code snippet below, the code inside the `if` block will run and `color` will be evaluated. This means that changes to either `condition` or `color` [will cause the effect to re-run](/playground/untitled#H4sIAAAAAAAAE21RQW6DMBD8ytaNBJHaJFLViwNIVZ8RcnBgXVk1xsILTYT4e20TQg89IOPZ2fHM7siMaJBx9tmaWpFqjQNlAKXEihx7YVJpdIyfRkY3G4gB8Pi97cPanRtQU8AuwuF_eNUaQuPlOMtc1SlLRWlKUo1tOwJflUikQHZtA0klzCDc64Imx0ANn8bInV1CDhtHgjClrsftcSXotluLybOUb3g4JJHhOZs5WZpuIS9gjNqkJKQP5e2ClrR4SMdZ13E4xZ8zTPOTJU2A2uE_PQ9COCI926_hTVarIU4hu_REPlBrKq2q73ycrf1N-vS4TMUsulaVg3EtR8H9rFgsg8uUsT1B2F9eshigZHBRpuaD0D3mY8Qm2BfB5N2YyRzdNEYVDy0Ja-WsFjcOUuP1HvFLWA6H3XuHTUSmmDV2--0TXonxsKbp7G9C6R__NONS-MFNvxj_d6mBAgAA). Conversely, if `condition` is `false`, `color` will not be evaluated, and the effect will _only_ re-run again when `condition` changes. diff --git a/documentation/docs/03-template-syntax/06-snippet.md b/documentation/docs/03-template-syntax/06-snippet.md index ab536c6e5c..02f58e0f6c 100644 --- a/documentation/docs/03-template-syntax/06-snippet.md +++ b/documentation/docs/03-template-syntax/06-snippet.md @@ -277,4 +277,4 @@ Snippets can be created programmatically with the [`createRawSnippet`](svelte#cr ## Snippets and slots -In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and as such slots are deprecated in Svelte 5. +In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5. diff --git a/documentation/docs/03-template-syntax/08-@html.md b/documentation/docs/03-template-syntax/08-@html.md index 30456fa666..a92accd093 100644 --- a/documentation/docs/03-template-syntax/08-@html.md +++ b/documentation/docs/03-template-syntax/08-@html.md @@ -22,7 +22,7 @@ It also will not compile Svelte code. ## Styling -Content rendered this way is 'invisible' to Svelte and as such will not receive [scoped styles](scoped-styles) — in other words, this will not work, and the `a` and `img` styles will be regarded as unused: +Content rendered this way is 'invisible' to Svelte and thus will not receive [scoped styles](scoped-styles). In other words, this will not work, and the `a` and `img` styles will be regarded as unused: ```svelte diff --git a/documentation/docs/05-special-elements/07-svelte-options.md b/documentation/docs/05-special-elements/07-svelte-options.md index d29042e278..a2e47aa04f 100644 --- a/documentation/docs/05-special-elements/07-svelte-options.md +++ b/documentation/docs/05-special-elements/07-svelte-options.md @@ -12,7 +12,7 @@ The `` element provides a place to specify per-component compile - `runes={false}` — forces a component into _legacy mode_ - `namespace="..."` — the namespace where this component will be used, can be "html" (the default), "svg" or "mathml" - `customElement={...}` — the [options](custom-elements#Component-options) to use when compiling this component as a custom element. If a string is passed, it is used as the `tag` option -- `css="injected"` — the component will inject its styles inline: During server side rendering, it's injected as a `