From 1c0e24013fff52e106870dfc7e4b38a817c25610 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 10 Mar 2025 11:22:21 -0400 Subject: [PATCH 01/75] chore: reuse is_function helper (#15467) --- .../phases/2-analyze/visitors/Attribute.js | 12 ++---------- packages/svelte/src/compiler/phases/scope.js | 18 +++++++++++------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 561a004526..9124a8822f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -162,16 +162,8 @@ function get_delegated_event(event_name, handler, context) { return unhoisted; } - if (binding !== null && binding.initial !== null && !binding.updated) { - const binding_type = binding.initial.type; - - if ( - binding_type === 'ArrowFunctionExpression' || - binding_type === 'FunctionDeclaration' || - binding_type === 'FunctionExpression' - ) { - target_function = binding.initial; - } + if (binding?.is_function()) { + target_function = binding.initial; } } diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index a5227c1b51..b6063c3234 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1,4 +1,4 @@ -/** @import { ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */ +/** @import { ArrowFunctionExpression, ClassDeclaration, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */ /** @import { Context, Visitor } from 'zimmerframe' */ /** @import { AST, BindingKind, DeclarationKind } from '#compiler' */ import is_reference from 'is-reference'; @@ -80,19 +80,23 @@ export class Binding { return this.mutated || this.reassigned; } + /** + * @returns {this is Binding & { initial: ArrowFunctionExpression | FunctionDeclaration | FunctionExpression }} + */ is_function() { - if (this.reassigned) { + if (this.updated) { // even if it's reassigned to another function, // we can't use it directly as e.g. an event handler return false; } - if (this.declaration_kind === 'function') { - return true; - } - const type = this.initial?.type; - return type === 'ArrowFunctionExpression' || type === 'FunctionExpression'; + + return ( + type === 'ArrowFunctionExpression' || + type === 'FunctionExpression' || + type === 'FunctionDeclaration' + ); } } From 81480c40a02678bd02d89e2e20d50a6ddd3ec383 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Mon, 10 Mar 2025 12:48:50 -0700 Subject: [PATCH 02/75] chore: add missing permissions for `pkg.pr.new-comment` (#15489) --- .github/workflows/pkg.pr.new-comment.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/pkg.pr.new-comment.yml b/.github/workflows/pkg.pr.new-comment.yml index b1fba0b04b..3f1fca5a0b 100644 --- a/.github/workflows/pkg.pr.new-comment.yml +++ b/.github/workflows/pkg.pr.new-comment.yml @@ -6,6 +6,9 @@ on: types: - completed +permissions: + pull-requests: write + jobs: build: name: 'Update comment' From dbd4617ac42a4bf5e97af05f2d5bc6a0517e24b2 Mon Sep 17 00:00:00 2001 From: Maple <52185471+fuuki12@users.noreply.github.com> Date: Tue, 11 Mar 2025 06:52:22 +0900 Subject: [PATCH 03/75] docs: correct toggle function in lifecycle hooks example (#15486) --- documentation/docs/06-runtime/03-lifecycle-hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/06-runtime/03-lifecycle-hooks.md b/documentation/docs/06-runtime/03-lifecycle-hooks.md index 2b97ca796f..f051c46d73 100644 --- a/documentation/docs/06-runtime/03-lifecycle-hooks.md +++ b/documentation/docs/06-runtime/03-lifecycle-hooks.md @@ -147,7 +147,7 @@ With runes, we can use `$effect.pre`, which behaves the same as `$effect` but ru } function toggle() { - toggleValue = !toggleValue; + theme = theme === 'dark' ? 'light' : 'dark'; } From 1cc5bcdc999716673d15844bd1190758f882ba05 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 11 Mar 2025 18:57:58 +0100 Subject: [PATCH 04/75] chore: clarify fuzzyset adaption (#15491) it was BSD in 2016 but has undergone some license changes since then - clarify in the comment that the adaption was from the 2016 BSD version --- .../svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js b/packages/svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js index cd72d73005..28b314cdd5 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/fuzzymatch.js @@ -12,8 +12,8 @@ export default function fuzzymatch(name, names) { return matches && matches[0][0] > 0.7 ? matches[0][1] : null; } -// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js -// BSD Licensed +// adapted from https://github.com/Glench/fuzzyset.js/blob/master/lib/fuzzyset.js in 2016 +// BSD Licensed (see https://github.com/Glench/fuzzyset.js/issues/10) const GRAM_SIZE_LOWER = 2; const GRAM_SIZE_UPPER = 3; From 110d42062fb1a98698d2a68a39919cc44962613d Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 11 Mar 2025 18:48:55 +0000 Subject: [PATCH 05/75] fix: on teardown, use the last known value for the signal before the set (#15469) * fix: on teardown, use the last known value for the signal before the se * fix: on teardown, use the last known value for the signal before the se * fix: on teardown, use the last known value for the signal before the se * fix: on teardown, use the last known value for the signal before the se * fix: on teardown, use the last known value for the signal before the se * Update packages/svelte/src/internal/client/reactivity/props.js Co-authored-by: Rich Harris * Update packages/svelte/src/internal/client/reactivity/props.js Co-authored-by: Rich Harris * Update packages/svelte/src/internal/client/reactivity/props.js Co-authored-by: Rich Harris * lint * lint * lint * Update .changeset/sharp-elephants-invite.md --------- Co-authored-by: Rich Harris --- .changeset/sharp-elephants-invite.md | 5 ++ .../2-analyze/visitors/CallExpression.js | 3 + .../svelte/src/internal/client/context.js | 11 ++- .../src/internal/client/reactivity/props.js | 43 +++++++----- .../src/internal/client/reactivity/sources.js | 11 ++- .../svelte/src/internal/client/runtime.js | 7 +- .../svelte/src/internal/client/types.d.ts | 2 + .../ondestroy-prop-access-2/Component.svelte | 11 +++ .../ondestroy-prop-access-2/_config.js | 14 ++++ .../ondestroy-prop-access-2/main.svelte | 15 ++++ .../ondestroy-prop-access-3/Component.svelte | 5 ++ .../ondestroy-prop-access-3/_config.js | 11 +++ .../ondestroy-prop-access-3/main.svelte | 16 +++++ .../ondestroy-prop-access/Component.svelte | 12 ++++ .../samples/ondestroy-prop-access/_config.js | 68 +++++++++++++++++++ .../samples/ondestroy-prop-access/main.svelte | 41 +++++++++++ .../samples/nested-effect-conflict/_config.js | 10 +-- 17 files changed, 254 insertions(+), 31 deletions(-) create mode 100644 .changeset/sharp-elephants-invite.md create mode 100644 packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/Component.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/Component.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/Component.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/main.svelte diff --git a/.changeset/sharp-elephants-invite.md b/.changeset/sharp-elephants-invite.md new file mode 100644 index 0000000000..3a106cd450 --- /dev/null +++ b/.changeset/sharp-elephants-invite.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +fix: make values consistent between effects and their cleanup functions diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 4d09d9293f..6c21717852 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -42,6 +42,9 @@ export function CallExpression(node, context) { e.bindable_invalid_location(node); } + // We need context in case the bound prop is stale + context.state.analysis.needs_context = true; + break; case '$host': diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index bd94d5ad8a..bfca9d5e6a 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -11,7 +11,7 @@ import { set_active_reaction, untrack } from './runtime.js'; -import { effect } from './reactivity/effects.js'; +import { effect, teardown } from './reactivity/effects.js'; import { legacy_mode_flag } from '../flags/index.js'; /** @type {ComponentContext | null} */ @@ -112,15 +112,16 @@ export function getAllContexts() { * @returns {void} */ export function push(props, runes = false, fn) { - component_context = { + var ctx = (component_context = { p: component_context, c: null, + d: false, e: null, m: false, s: props, x: null, l: null - }; + }); if (legacy_mode_flag && !runes) { component_context.l = { @@ -131,6 +132,10 @@ export function push(props, runes = false, fn) { }; } + teardown(() => { + /** @type {ComponentContext} */ (ctx).d = true; + }); + if (DEV) { // component function component_context.function = fn; diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 5a3b30281f..bd85b14df0 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -1,4 +1,4 @@ -/** @import { Source } from './types.js' */ +/** @import { Derived, Source } from './types.js' */ import { DEV } from 'esm-env'; import { PROPS_IS_BINDABLE, @@ -10,24 +10,10 @@ import { import { get_descriptor, is_function } from '../../shared/utils.js'; import { mutable_source, set, source, update } from './sources.js'; import { derived, derived_safe_equal } from './deriveds.js'; -import { - active_effect, - get, - captured_signals, - set_active_effect, - untrack, - active_reaction, - set_active_reaction -} from '../runtime.js'; +import { get, captured_signals, untrack } from '../runtime.js'; import { safe_equals } from './equality.js'; import * as e from '../errors.js'; -import { - BRANCH_EFFECT, - LEGACY_DERIVED_PROP, - LEGACY_PROPS, - ROOT_EFFECT, - STATE_SYMBOL -} from '../constants.js'; +import { LEGACY_DERIVED_PROP, LEGACY_PROPS, STATE_SYMBOL } from '../constants.js'; import { proxy } from '../proxy.js'; import { capture_store_binding } from './store.js'; import { legacy_mode_flag } from '../../flags/index.js'; @@ -249,6 +235,14 @@ export function spread_props(...props) { return new Proxy({ props }, spread_props_handler); } +/** + * @param {Derived} current_value + * @returns {boolean} + */ +function has_destroyed_component_ctx(current_value) { + return current_value.ctx?.d ?? false; +} + /** * This function is responsible for synchronizing a possibly bound prop with the inner component state. * It is used whenever the compiler sees that the component writes to the prop, or when it has a default prop_value. @@ -382,6 +376,11 @@ export function prop(props, key, flags, fallback) { return (inner_current_value.v = parent_value); }); + // Ensure we eagerly capture the initial value if it's bindable + if (bindable) { + get(current_value); + } + if (!immutable) current_value.equals = safe_equals; return function (/** @type {any} */ value, /** @type {boolean} */ mutation) { @@ -408,11 +407,21 @@ export function prop(props, key, flags, fallback) { if (fallback_used && fallback_value !== undefined) { fallback_value = new_value; } + + if (has_destroyed_component_ctx(current_value)) { + return value; + } + untrack(() => get(current_value)); // force a synchronisation immediately } return value; } + + if (has_destroyed_component_ctx(current_value)) { + return current_value.v; + } + return get(current_value); }; } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index f6a3fd7e33..49584e8626 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -14,7 +14,8 @@ import { derived_sources, set_derived_sources, check_dirtiness, - untracking + untracking, + is_destroying_effect } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import { @@ -34,6 +35,7 @@ import { get_stack } from '../dev/tracing.js'; import { component_context, is_runes } from '../context.js'; export let inspect_effects = new Set(); +export const old_values = new Map(); /** * @param {Set} v @@ -168,6 +170,13 @@ export function set(source, value) { export function internal_set(source, value) { if (!source.equals(value)) { var old_value = source.v; + + if (is_destroying_effect) { + old_values.set(source, value); + } else { + old_values.set(source, old_value); + } + source.v = value; source.wv = increment_write_version(); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index bbe4dc3d9b..aa0a41e71f 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -25,7 +25,7 @@ import { BOUNDARY_EFFECT } from './constants.js'; import { flush_tasks } from './dom/task.js'; -import { internal_set } from './reactivity/sources.js'; +import { internal_set, old_values } from './reactivity/sources.js'; import { destroy_derived_effects, update_derived } from './reactivity/deriveds.js'; import * as e from './errors.js'; import { FILENAME } from '../../constants.js'; @@ -673,6 +673,7 @@ function flush_queued_root_effects() { if (DEV) { dev_effect_stack = []; } + old_values.clear(); } } @@ -923,6 +924,10 @@ export function get(signal) { } } + if (is_destroying_effect && old_values.has(signal)) { + return old_values.get(signal); + } + return signal.v; } diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 7208ed7783..0c260a0a9f 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -14,6 +14,8 @@ export type ComponentContext = { p: null | ComponentContext; /** context */ c: null | Map; + /** destroyed */ + d: boolean; /** deferred effects */ e: null | Array<{ fn: () => void | (() => void); diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/Component.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/Component.svelte new file mode 100644 index 0000000000..73347c4d7f --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/Component.svelte @@ -0,0 +1,11 @@ + + +{my_prop.foo} diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/_config.js b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/_config.js new file mode 100644 index 0000000000..81005cf737 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/_config.js @@ -0,0 +1,14 @@ +import { test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + async test({ assert, target, logs }) { + const [btn1] = target.querySelectorAll('button'); + + flushSync(() => { + btn1.click(); + }); + + assert.deepEqual(logs, ['bar']); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/main.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/main.svelte new file mode 100644 index 0000000000..f38b37fb7f --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-2/main.svelte @@ -0,0 +1,15 @@ + + + + +{#if value !== undefined} + +{/if} diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/Component.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/Component.svelte new file mode 100644 index 0000000000..5bfb777128 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/Component.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/_config.js b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/_config.js new file mode 100644 index 0000000000..0eb68310cb --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + async test({ target }) { + const [btn1] = target.querySelectorAll('button'); + + btn1.click(); + flushSync(); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/main.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/main.svelte new file mode 100644 index 0000000000..9c72d2c48a --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access-3/main.svelte @@ -0,0 +1,16 @@ + + +{#if state} + {@const attributes = { title: state.title }} + +{/if} + diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/Component.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/Component.svelte new file mode 100644 index 0000000000..761f303c2e --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/Component.svelte @@ -0,0 +1,12 @@ + + +

{count}

+ + diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js new file mode 100644 index 0000000000..2ffb7e653f --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/_config.js @@ -0,0 +1,68 @@ +import { test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + async test({ assert, target, logs }) { + const [btn1, btn2, btn3] = target.querySelectorAll('button'); + let ps = [...target.querySelectorAll('p')]; + + for (const p of ps) { + assert.equal(p.innerHTML, '0'); + } + + flushSync(() => { + btn1.click(); + }); + + // prop update normally if we are not unmounting + for (const p of ps) { + assert.equal(p.innerHTML, '1'); + } + + flushSync(() => { + btn3.click(); + }); + + // binding still works and update the value correctly + for (const p of ps) { + assert.equal(p.innerHTML, '0'); + } + + flushSync(() => { + btn1.click(); + }); + + flushSync(() => { + btn1.click(); + }); + + console.warn(logs); + + // the five components guarded by `count < 2` unmount and log + assert.deepEqual(logs, [1, true, 1, true, 1, true, 1, true, 1, true]); + + flushSync(() => { + btn2.click(); + }); + + // the three components guarded by `show` unmount and log + assert.deepEqual(logs, [ + 1, + true, + 1, + true, + 1, + true, + 1, + true, + 1, + true, + 2, + true, + 2, + true, + 2, + true + ]); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/main.svelte b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/main.svelte new file mode 100644 index 0000000000..73a7501e9d --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-prop-access/main.svelte @@ -0,0 +1,41 @@ + + + + + + +{#if count < 2} + +{/if} + + +{#if count < 2} + +{/if} + + +{#if count < 2} + +{/if} + + +{#if show} + +{/if} + + + + + + + + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/nested-effect-conflict/_config.js b/packages/svelte/tests/runtime-runes/samples/nested-effect-conflict/_config.js index a8c16b7008..eb631bc9f4 100644 --- a/packages/svelte/tests/runtime-runes/samples/nested-effect-conflict/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/nested-effect-conflict/_config.js @@ -10,14 +10,6 @@ export default test({ }); await Promise.resolve(); - assert.deepEqual(logs, [ - 'top level', - 'inner', - 0, - 'destroy inner', - undefined, - 'destroy outer', - undefined - ]); + assert.deepEqual(logs, ['top level', 'inner', 0, 'destroy inner', 0, 'destroy outer', 0]); } }); From a1257c17f5ba93bdbf7c470a5720ff9a69a224dc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 11 Mar 2025 14:51:01 -0400 Subject: [PATCH 06/75] Version Packages (#15493) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/sharp-elephants-invite.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/sharp-elephants-invite.md diff --git a/.changeset/sharp-elephants-invite.md b/.changeset/sharp-elephants-invite.md deleted file mode 100644 index 3a106cd450..0000000000 --- a/.changeset/sharp-elephants-invite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -fix: make values consistent between effects and their cleanup functions diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index a05938dacc..65b3edd1fd 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.23.0 + +### Minor Changes + +- fix: make values consistent between effects and their cleanup functions ([#15469](https://github.com/sveltejs/svelte/pull/15469)) + ## 5.22.6 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 53f03e3543..b3ac0a5b51 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.22.6", + "version": "5.23.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 01e1b390a3..5f06fd0753 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.22.6'; +export const VERSION = '5.23.0'; export const PUBLIC_VERSION = '5'; From b27ca425c792d53d58346d76850add7db1cef3ad Mon Sep 17 00:00:00 2001 From: "D.V. Colomban" <17250935+dvcol@users.noreply.github.com> Date: Wed, 12 Mar 2025 23:58:40 +0100 Subject: [PATCH 07/75] fix: add files and group properties to HTMLInputAttributes (#15492) Fixes #14579 Although this isn't 100% correct because there's no `group` attribute, there's no better way to make people have components' props just be `HTMLInputAttributes` and allow them to reference `group` --- .changeset/plenty-bats-lay.md | 5 +++++ packages/svelte/elements.d.ts | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 .changeset/plenty-bats-lay.md diff --git a/.changeset/plenty-bats-lay.md b/.changeset/plenty-bats-lay.md new file mode 100644 index 0000000000..cd5ce66e42 --- /dev/null +++ b/.changeset/plenty-bats-lay.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add `files` and `group` to HTMLInputAttributes in elements.d.ts diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 08687cafaf..99d87b4c09 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -1076,6 +1076,7 @@ export interface HTMLInputAttributes extends HTMLAttributes { checked?: boolean | undefined | null; dirname?: string | undefined | null; disabled?: boolean | undefined | null; + files?: FileList | undefined | null; form?: string | undefined | null; formaction?: string | undefined | null; formenctype?: @@ -1087,6 +1088,7 @@ export interface HTMLInputAttributes extends HTMLAttributes { formmethod?: 'dialog' | 'get' | 'post' | 'DIALOG' | 'GET' | 'POST' | undefined | null; formnovalidate?: boolean | undefined | null; formtarget?: string | undefined | null; + group?: any | undefined | null; height?: number | string | undefined | null; indeterminate?: boolean | undefined | null; list?: string | undefined | null; From 5d3aa2bda4bea7af39607a4a01ce8f0eff6cd56b Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 13 Mar 2025 19:49:26 +0000 Subject: [PATCH 08/75] fix: ensure transient writes to tracked parent effects works as expected (#15506) * ix: ensure transient writes to tracked parent effects works as expected * lint * format test * tweak changeset --------- Co-authored-by: Rich Harris --- .changeset/brown-rockets-shake.md | 5 +++++ packages/svelte/src/internal/client/runtime.js | 8 ++++++++ .../samples/untracked-write-pre/_config.js | 7 +++++++ .../samples/untracked-write-pre/main.svelte | 13 +++++++++++++ 4 files changed, 33 insertions(+) create mode 100644 .changeset/brown-rockets-shake.md create mode 100644 packages/svelte/tests/runtime-runes/samples/untracked-write-pre/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/untracked-write-pre/main.svelte diff --git a/.changeset/brown-rockets-shake.md b/.changeset/brown-rockets-shake.md new file mode 100644 index 0000000000..3772a88f6e --- /dev/null +++ b/.changeset/brown-rockets-shake.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: invalidate parent effects when child effects update parent dependencies diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index aa0a41e71f..0a65c6e45a 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -460,6 +460,14 @@ export function update_reaction(reaction) { // the same version if (previous_reaction !== null) { read_version++; + + if (untracked_writes !== null) { + if (previous_untracked_writes === null) { + previous_untracked_writes = untracked_writes; + } else { + previous_untracked_writes.push(.../** @type {Source[]} */ (untracked_writes)); + } + } } return result; diff --git a/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/_config.js b/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/_config.js new file mode 100644 index 0000000000..0310ec4fbb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + test({ assert, target, logs }) { + assert.deepEqual(logs, ['Outer', 'Inner', 'Outer', 'Inner']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/main.svelte b/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/main.svelte new file mode 100644 index 0000000000..5e95dbfd41 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/untracked-write-pre/main.svelte @@ -0,0 +1,13 @@ + From 489f463d7bf80da22c92b686e507eb7c0dc41967 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Fri, 14 Mar 2025 03:26:45 -0700 Subject: [PATCH 09/75] fix: replace `undefined` with `void 0` to avoid edge case (#15511) * replace 'undefined' with 'void 0' * lint * YALF * reuse expression * oops --------- Co-authored-by: Rich Harris --- .changeset/curvy-countries-flow.md | 5 +++++ .../3-transform/client/visitors/VariableDeclaration.js | 3 +-- .../phases/3-transform/server/visitors/CallExpression.js | 4 ++-- .../3-transform/server/visitors/VariableDeclaration.js | 5 ++--- packages/svelte/src/compiler/utils/builders.js | 2 ++ 5 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 .changeset/curvy-countries-flow.md diff --git a/.changeset/curvy-countries-flow.md b/.changeset/curvy-countries-flow.md new file mode 100644 index 0000000000..6ef8545804 --- /dev/null +++ b/.changeset/curvy-countries-flow.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: replace `undefined` with `void 0` to avoid edge case 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 31e712cdcc..baffc5dec3 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 @@ -116,8 +116,7 @@ export function VariableDeclaration(node, context) { } const args = /** @type {CallExpression} */ (init).arguments; - const value = - args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (context.visit(args[0])); + const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0; if (rune === '$state' || rune === '$state.raw') { /** diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js index 386c6b6ff3..a425bc5ec4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js @@ -13,11 +13,11 @@ export function CallExpression(node, context) { const rune = get_rune(node, context.state.scope); if (rune === '$host') { - return b.id('undefined'); + return b.void0; } if (rune === '$effect.tracking') { - return b.literal(false); + return b.false; } if (rune === '$effect.root') { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js index c4c31d7eb3..a9c9777335 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js @@ -45,7 +45,7 @@ export function VariableDeclaration(node, context) { ) { const right = node.right.arguments.length ? /** @type {Expression} */ (context.visit(node.right.arguments[0])) - : b.id('undefined'); + : b.void0; return b.assignment_pattern(node.left, right); } } @@ -75,8 +75,7 @@ export function VariableDeclaration(node, context) { } const args = /** @type {CallExpression} */ (init).arguments; - const value = - args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (context.visit(args[0])); + const value = args.length > 0 ? /** @type {Expression} */ (context.visit(args[0])) : b.void0; if (rune === '$derived.by') { declarations.push( diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index ecb595d74d..4ec2930cc2 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -154,6 +154,8 @@ export function unary(operator, argument) { return { type: 'UnaryExpression', argument, operator, prefix: true }; } +export const void0 = unary('void', literal(0)); + /** * @param {ESTree.Expression} test * @param {ESTree.Expression} consequent From dab1a1b467b8c5e85964d74c378357bea00e5fe9 Mon Sep 17 00:00:00 2001 From: Scott Wu Date: Fri, 14 Mar 2025 19:11:08 +0800 Subject: [PATCH 10/75] docs: Update 99-faq.md (#15510) --- documentation/docs/07-misc/99-faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/07-misc/99-faq.md b/documentation/docs/07-misc/99-faq.md index b56c27af86..7e25cdab55 100644 --- a/documentation/docs/07-misc/99-faq.md +++ b/documentation/docs/07-misc/99-faq.md @@ -46,7 +46,7 @@ It will show up on hover. - You can use markdown here. - You can also use code blocks here. - Usage: - ```tsx + ```svelte
``` --> From 18d71fd5288c04d5574f5abe18e7c990053c5876 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 14 Mar 2025 07:12:02 -0400 Subject: [PATCH 11/75] chore: reuse expression nodes (#15513) --- .../compiler/phases/3-transform/client/transform-client.js | 2 +- .../phases/3-transform/client/visitors/AnimateDirective.js | 2 +- .../compiler/phases/3-transform/client/visitors/AwaitBlock.js | 2 +- .../phases/3-transform/client/visitors/BinaryExpression.js | 4 ++-- .../compiler/phases/3-transform/client/visitors/IfBlock.js | 4 +--- .../phases/3-transform/client/visitors/RegularElement.js | 2 +- .../phases/3-transform/client/visitors/SlotElement.js | 2 +- .../phases/3-transform/client/visitors/shared/element.js | 4 ++-- .../phases/3-transform/server/visitors/SlotElement.js | 2 +- packages/svelte/src/compiler/utils/builders.js | 4 ++-- 10 files changed, 13 insertions(+), 15 deletions(-) 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 cf5ba285cb..ac8263b916 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 @@ -596,7 +596,7 @@ export function client_component(analysis, options) { /** @type {ESTree.Property[]} */ ( [ prop_def.attribute ? b.init('attribute', b.literal(prop_def.attribute)) : undefined, - prop_def.reflect ? b.init('reflect', b.literal(true)) : undefined, + prop_def.reflect ? b.init('reflect', b.true) : undefined, prop_def.type ? b.init('type', b.literal(prop_def.type)) : undefined ].filter(Boolean) ) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js index 510f32cde5..2e051ec674 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js @@ -11,7 +11,7 @@ import { parse_directive_name } from './shared/utils.js'; export function AnimateDirective(node, context) { const expression = node.expression === null - ? b.literal(null) + ? b.null : b.thunk(/** @type {Expression} */ (context.visit(node.expression))); // in after_update to ensure it always happens after bind:this 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 e0aef2d316..7588b24280 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 @@ -60,7 +60,7 @@ export function AwaitBlock(node, context) { expression, node.pending ? b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.pending))) - : b.literal(null), + : b.null, then_block, catch_block ) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js index c8c54a5a59..c563920855 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js @@ -16,7 +16,7 @@ export function BinaryExpression(node, context) { '$.strict_equals', /** @type {Expression} */ (context.visit(node.left)), /** @type {Expression} */ (context.visit(node.right)), - operator === '!==' && b.literal(false) + operator === '!==' && b.false ); } @@ -25,7 +25,7 @@ export function BinaryExpression(node, context) { '$.equals', /** @type {Expression} */ (context.visit(node.left)), /** @type {Expression} */ (context.visit(node.right)), - operator === '!=' && b.literal(false) + operator === '!=' && b.false ); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js index 0876fa30b6..fdd21b2b7e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -40,9 +40,7 @@ export function IfBlock(node, context) { b.if( /** @type {Expression} */ (context.visit(node.test)), b.stmt(b.call(b.id('$$render'), b.id(consequent_id))), - alternate_id - ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.literal(false))) - : undefined + alternate_id ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.false)) : undefined ) ]) ) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 9b3ecc922d..45a594af1f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -689,7 +689,7 @@ function build_element_special_value_attribute(element, node_id, attribute, cont '=', b.member(node_id, 'value'), b.conditional( - b.binary('==', b.literal(null), b.assignment('=', b.member(node_id, '__value'), value)), + b.binary('==', b.null, b.assignment('=', b.member(node_id, '__value'), value)), b.literal(''), // render null/undefined values as empty string to support placeholder options value ) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js index fdd705e32e..c6f4ba1ed3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js @@ -59,7 +59,7 @@ export function SlotElement(node, context) { const fallback = node.fragment.nodes.length === 0 - ? b.literal(null) + ? b.null : b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment))); const slot = b.call( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 084c1e7c67..97cec7a729 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -95,7 +95,7 @@ export function build_set_attributes( const call = b.call( '$.set_attributes', element_id, - is_dynamic ? attributes_id : b.literal(null), + is_dynamic ? attributes_id : b.null, b.object(values), element.metadata.scoped && context.state.analysis.css.hash !== '' && @@ -120,7 +120,7 @@ export function build_set_attributes( */ export function build_attribute_value(value, context, memoize = (value) => value) { if (value === true) { - return { value: b.literal(true), has_state: false }; + return { value: b.true, has_state: false }; } if (!Array.isArray(value) || value.length === 1) { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js index 7ece04ae3d..e7925071cd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js @@ -38,7 +38,7 @@ export function SlotElement(node, context) { const fallback = node.fragment.nodes.length === 0 - ? b.literal(null) + ? b.null : b.thunk(/** @type {BlockStatement} */ (context.visit(node.fragment))); const slot = b.call( diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index 4ec2930cc2..736738d19f 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -485,7 +485,7 @@ export function do_while(test, body) { const true_instance = literal(true); const false_instance = literal(false); -const null_instane = literal(null); +const null_instance = literal(null); /** @type {ESTree.DebuggerStatement} */ const debugger_builder = { @@ -647,7 +647,7 @@ export { return_builder as return, if_builder as if, this_instance as this, - null_instane as null, + null_instance as null, debugger_builder as debugger }; From e74fbcbbacc529bbb7ff8cc7d6b8b5d75d647cfa Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Fri, 14 Mar 2025 13:19:24 -0700 Subject: [PATCH 12/75] chore: don't distribute unused types definitions (#15473) hopefully helps with #15182, also makes the package smaller --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/hungry-dancers-tap.md | 5 +++++ packages/svelte/package.json | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/hungry-dancers-tap.md diff --git a/.changeset/hungry-dancers-tap.md b/.changeset/hungry-dancers-tap.md new file mode 100644 index 0000000000..51b2f86019 --- /dev/null +++ b/.changeset/hungry-dancers-tap.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: don't distribute unused types definitions diff --git a/packages/svelte/package.json b/packages/svelte/package.json index b3ac0a5b51..c74c9d34ca 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -9,11 +9,12 @@ "node": ">=18" }, "files": [ + "*.d.ts", "src", "!src/**/*.test.*", + "!src/**/*.d.ts", "types", "compiler", - "*.d.ts", "README.md" ], "module": "src/index-client.js", From f227cfcea86b46e6a8ee389484d855c306ed66eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?E=CC=81ric=20NICOLAS?= Date: Fri, 14 Mar 2025 22:30:49 +0100 Subject: [PATCH 13/75] fix: Allow global-like pseudo-selectors refinement (#15313) For instance, specifying a tree-structural pseudo-class to `::view-transition-new` should still constitute a valid global-like selector. Fixes #15312 --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/gold-hairs-jog.md | 5 +++++ .../src/compiler/phases/2-analyze/css/css-analyze.js | 8 +++++++- .../svelte/tests/css/samples/view-transition/expected.css | 6 ++++++ .../svelte/tests/css/samples/view-transition/input.svelte | 6 ++++++ 4 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 .changeset/gold-hairs-jog.md diff --git a/.changeset/gold-hairs-jog.md b/.changeset/gold-hairs-jog.md new file mode 100644 index 0000000000..eaafced314 --- /dev/null +++ b/.changeset/gold-hairs-jog.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow global-like pseudo-selectors refinement diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js index ed22838582..362ac9dcad 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js @@ -133,7 +133,13 @@ const css_visitors = { node.metadata.is_global = node.selectors.length >= 1 && is_global(node); - if (node.selectors.length === 1) { + if ( + node.selectors.length >= 1 && + node.selectors.every( + (selector) => + selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector' + ) + ) { const first = node.selectors[0]; node.metadata.is_global_like ||= (first.type === 'PseudoClassSelector' && first.name === 'host') || diff --git a/packages/svelte/tests/css/samples/view-transition/expected.css b/packages/svelte/tests/css/samples/view-transition/expected.css index afc84d52eb..e216a4d3ad 100644 --- a/packages/svelte/tests/css/samples/view-transition/expected.css +++ b/packages/svelte/tests/css/samples/view-transition/expected.css @@ -8,9 +8,15 @@ ::view-transition-old { animation-duration: 0.5s; } + ::view-transition-old:only-child { + animation-duration: 0.5s; + } ::view-transition-new { animation-duration: 0.5s; } + ::view-transition-new:only-child { + animation-duration: 0.5s; + } ::view-transition-image-pair { animation-duration: 0.5s; } diff --git a/packages/svelte/tests/css/samples/view-transition/input.svelte b/packages/svelte/tests/css/samples/view-transition/input.svelte index ebb2b3fd88..345213ccd3 100644 --- a/packages/svelte/tests/css/samples/view-transition/input.svelte +++ b/packages/svelte/tests/css/samples/view-transition/input.svelte @@ -8,9 +8,15 @@ ::view-transition-old { animation-duration: 0.5s; } + ::view-transition-old:only-child { + animation-duration: 0.5s; + } ::view-transition-new { animation-duration: 0.5s; } + ::view-transition-new:only-child { + animation-duration: 0.5s; + } ::view-transition-image-pair { animation-duration: 0.5s; } From 8e9a21e374c3bafb79e046e64ef4d625f466749f Mon Sep 17 00:00:00 2001 From: 7nik Date: Fri, 14 Mar 2025 23:44:28 +0200 Subject: [PATCH 14/75] fix: correctly match `:has()`'s selector during css pruning (#15277) Fixes #14072 `:has()` was matching only against descendants or siblings, but not sibling's descendants. This makes the logic be able to go forward or backwards, simplifying a lot of cases along the way. --- .changeset/cuddly-chefs-refuse.md | 5 + .../phases/2-analyze/css/css-prune.js | 427 +++++++++--------- .../svelte/tests/css/samples/has/_config.js | 148 +++--- .../svelte/tests/css/samples/has/expected.css | 13 + .../svelte/tests/css/samples/has/input.svelte | 21 + .../css/samples/render-tag-loop/_config.js | 17 +- .../css/samples/render-tag-loop/expected.css | 9 +- .../css/samples/render-tag-loop/input.svelte | 10 +- 8 files changed, 353 insertions(+), 297 deletions(-) create mode 100644 .changeset/cuddly-chefs-refuse.md diff --git a/.changeset/cuddly-chefs-refuse.md b/.changeset/cuddly-chefs-refuse.md new file mode 100644 index 0000000000..6672ac4ab3 --- /dev/null +++ b/.changeset/cuddly-chefs-refuse.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly match `:has()` selector during css pruning diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index fc8108e46e..070ec7cd34 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -5,9 +5,12 @@ import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../ import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js'; /** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */ +/** @typedef {FORWARD | BACKWARD} Direction */ const NODE_PROBABLY_EXISTS = 0; const NODE_DEFINITELY_EXISTS = 1; +const FORWARD = 0; +const BACKWARD = 1; const whitelist_attribute_selector = new Map([ ['details', ['open']], @@ -43,6 +46,27 @@ const nesting_selector = { } }; +/** @type {Compiler.AST.CSS.RelativeSelector} */ +const any_selector = { + type: 'RelativeSelector', + start: -1, + end: -1, + combinator: null, + selectors: [ + { + type: 'TypeSelector', + name: '*', + start: -1, + end: -1 + } + ], + metadata: { + is_global: false, + is_global_like: false, + scoped: false + } +}; + /** * Snippets encountered already (avoids infinite loops) * @type {Set} @@ -72,7 +96,8 @@ export function prune(stylesheet, element) { apply_selector( selectors, /** @type {Compiler.AST.CSS.Rule} */ (node.metadata.rule), - element + element, + BACKWARD ) ) { node.metadata.used = true; @@ -159,16 +184,17 @@ function truncate(node) { * @param {Compiler.AST.CSS.RelativeSelector[]} relative_selectors * @param {Compiler.AST.CSS.Rule} rule * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element + * @param {Direction} direction * @returns {boolean} */ -function apply_selector(relative_selectors, rule, element) { - const parent_selectors = relative_selectors.slice(); - const relative_selector = parent_selectors.pop(); +function apply_selector(relative_selectors, rule, element, direction) { + const rest_selectors = relative_selectors.slice(); + const relative_selector = direction === FORWARD ? rest_selectors.shift() : rest_selectors.pop(); const matched = !!relative_selector && - relative_selector_might_apply_to_node(relative_selector, rule, element) && - apply_combinator(relative_selector, parent_selectors, rule, element); + relative_selector_might_apply_to_node(relative_selector, rule, element, direction) && + apply_combinator(relative_selector, rest_selectors, rule, element, direction); if (matched) { if (!is_outer_global(relative_selector)) { @@ -183,76 +209,63 @@ function apply_selector(relative_selectors, rule, element) { /** * @param {Compiler.AST.CSS.RelativeSelector} relative_selector - * @param {Compiler.AST.CSS.RelativeSelector[]} parent_selectors + * @param {Compiler.AST.CSS.RelativeSelector[]} rest_selectors * @param {Compiler.AST.CSS.Rule} rule * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node + * @param {Direction} direction * @returns {boolean} */ -function apply_combinator(relative_selector, parent_selectors, rule, node) { - if (!relative_selector.combinator) return true; +function apply_combinator(relative_selector, rest_selectors, rule, node, direction) { + const combinator = + direction == FORWARD ? rest_selectors[0]?.combinator : relative_selector.combinator; + if (!combinator) return true; - const name = relative_selector.combinator.name; - - switch (name) { + switch (combinator.name) { case ' ': case '>': { + const is_adjacent = combinator.name === '>'; + const parents = + direction === FORWARD + ? get_descendant_elements(node, is_adjacent) + : get_ancestor_elements(node, is_adjacent); let parent_matched = false; - const path = node.metadata.path; - let i = path.length; - - while (i--) { - const parent = path[i]; - - if (parent.type === 'SnippetBlock') { - if (seen.has(parent)) { - parent_matched = true; - } else { - seen.add(parent); - - for (const site of parent.metadata.sites) { - if (apply_combinator(relative_selector, parent_selectors, rule, site)) { - parent_matched = true; - } - } - } - - break; - } - - if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') { - if (apply_selector(parent_selectors, rule, parent)) { - parent_matched = true; - } - - if (name === '>') return parent_matched; + for (const parent of parents) { + if (apply_selector(rest_selectors, rule, parent, direction)) { + parent_matched = true; } } - return parent_matched || parent_selectors.every((selector) => is_global(selector, rule)); + return ( + parent_matched || + (direction === BACKWARD && + (!is_adjacent || parents.length === 0) && + rest_selectors.every((selector) => is_global(selector, rule))) + ); } case '+': case '~': { - const siblings = get_possible_element_siblings(node, name === '+'); + const siblings = get_possible_element_siblings(node, direction, combinator.name === '+'); let sibling_matched = false; for (const possible_sibling of siblings.keys()) { if (possible_sibling.type === 'RenderTag' || possible_sibling.type === 'SlotElement') { // `{@render foo()}

foo

` with `:global(.x) + p` is a match - if (parent_selectors.length === 1 && parent_selectors[0].metadata.is_global) { + if (rest_selectors.length === 1 && rest_selectors[0].metadata.is_global) { sibling_matched = true; } - } else if (apply_selector(parent_selectors, rule, possible_sibling)) { + } else if (apply_selector(rest_selectors, rule, possible_sibling, direction)) { sibling_matched = true; } } return ( sibling_matched || - (get_element_parent(node) === null && - parent_selectors.every((selector) => is_global(selector, rule))) + (direction === BACKWARD && + get_element_parent(node) === null && + rest_selectors.every((selector) => is_global(selector, rule))) ); } @@ -313,9 +326,10 @@ const regex_backslash_and_following_character = /\\(.)/g; * @param {Compiler.AST.CSS.RelativeSelector} relative_selector * @param {Compiler.AST.CSS.Rule} rule * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element + * @param {Direction} direction * @returns {boolean} */ -function relative_selector_might_apply_to_node(relative_selector, rule, element) { +function relative_selector_might_apply_to_node(relative_selector, rule, element, direction) { // Sort :has(...) selectors in one bucket and everything else into another const has_selectors = []; const other_selectors = []; @@ -331,13 +345,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) // If we're called recursively from a :has(...) selector, we're on the way of checking if the other selectors match. // In that case ignore this check (because we just came from this) to avoid an infinite loop. if (has_selectors.length > 0) { - /** @type {Array} */ - const child_elements = []; - /** @type {Array} */ - const descendant_elements = []; - /** @type {Array} */ - let sibling_elements; // do them lazy because it's rarely used and expensive to calculate - // If this is a :has inside a global selector, we gotta include the element itself, too, // because the global selector might be for an element that's outside the component, // e.g. :root:has(.scoped), :global(.foo):has(.scoped), or :root { &:has(.scoped) {} } @@ -353,46 +360,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) ) ) ); - if (include_self) { - child_elements.push(element); - descendant_elements.push(element); - } - - const seen = new Set(); - - /** - * @param {Compiler.AST.SvelteNode} node - * @param {{ is_child: boolean }} state - */ - function walk_children(node, state) { - walk(node, state, { - _(node, context) { - if (node.type === 'RegularElement' || node.type === 'SvelteElement') { - descendant_elements.push(node); - - if (context.state.is_child) { - child_elements.push(node); - context.state.is_child = false; - context.next(); - context.state.is_child = true; - } else { - context.next(); - } - } else if (node.type === 'RenderTag') { - for (const snippet of node.metadata.snippets) { - if (seen.has(snippet)) continue; - - seen.add(snippet); - walk_children(snippet.body, context.state); - } - } else { - context.next(); - } - } - }); - } - - walk_children(element.fragment, { is_child: true }); // :has(...) is special in that it means "look downwards in the CSS tree". Since our matching algorithm goes // upwards and back-to-front, we need to first check the selectors inside :has(...), then check the rest of the @@ -403,37 +370,34 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) let matched = false; for (const complex_selector of complex_selectors) { - const selectors = truncate(complex_selector); - const left_most_combinator = selectors[0]?.combinator ?? descendant_combinator; - // In .x:has(> y), we want to search for y, ignoring the left-most combinator - // (else it would try to walk further up and fail because there are no selectors left) - if (selectors.length > 0) { - selectors[0] = { - ...selectors[0], - combinator: null - }; + const [first, ...rest] = truncate(complex_selector); + // if it was just a :global(...) + if (!first) { + complex_selector.metadata.used = true; + matched = true; + continue; } - const descendants = - left_most_combinator.name === '+' || left_most_combinator.name === '~' - ? (sibling_elements ??= get_following_sibling_elements(element, include_self)) - : left_most_combinator.name === '>' - ? child_elements - : descendant_elements; - - let selector_matched = false; - - // Iterate over all descendant elements and check if the selector inside :has matches - for (const element of descendants) { - if ( - selectors.length === 0 /* is :global(...) */ || - (element.metadata.scoped && selector_matched) || - apply_selector(selectors, rule, element) - ) { + if (include_self) { + const selector_including_self = [ + first.combinator ? { ...first, combinator: null } : first, + ...rest + ]; + if (apply_selector(selector_including_self, rule, element, FORWARD)) { complex_selector.metadata.used = true; - selector_matched = matched = true; + matched = true; } } + + const selector_excluding_self = [ + any_selector, + first.combinator ? first : { ...first, combinator: descendant_combinator }, + ...rest + ]; + if (apply_selector(selector_excluding_self, rule, element, FORWARD)) { + complex_selector.metadata.used = true; + matched = true; + } } if (!matched) { @@ -458,7 +422,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) ) { const args = selector.args; const complex_selector = args.children[0]; - return apply_selector(complex_selector.children, rule, element); + return apply_selector(complex_selector.children, rule, element, BACKWARD); } // We came across a :global, everything beyond it is global and therefore a potential match @@ -507,7 +471,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) if (is_global) { complex_selector.metadata.used = true; matched = true; - } else if (apply_selector(relative, rule, element)) { + } else if (apply_selector(relative, rule, element, BACKWARD)) { complex_selector.metadata.used = true; matched = true; } else if (complex_selector.children.length > 1 && (name == 'is' || name == 'where')) { @@ -591,7 +555,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) for (const complex_selector of parent.prelude.children) { if ( - apply_selector(get_relative_selectors(complex_selector), parent, element) || + apply_selector(get_relative_selectors(complex_selector), parent, element, direction) || complex_selector.children.every((s) => is_global(s, parent)) ) { complex_selector.metadata.used = true; @@ -612,80 +576,6 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element) return true; } -/** - * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element - * @param {boolean} include_self - */ -function get_following_sibling_elements(element, include_self) { - const path = element.metadata.path; - let i = path.length; - - /** @type {Compiler.AST.SvelteNode} */ - let start = element; - let nodes = /** @type {Compiler.AST.SvelteNode[]} */ ( - /** @type {Compiler.AST.Fragment} */ (path[0]).nodes - ); - - // find the set of nodes to walk... - while (i--) { - const node = path[i]; - - if (node.type === 'RegularElement' || node.type === 'SvelteElement') { - nodes = node.fragment.nodes; - break; - } - - if (node.type !== 'Fragment') { - start = node; - } - } - - /** @type {Array} */ - const siblings = []; - - // ...then walk them, starting from the node containing the element in question - // skipping nodes that appears before the element - - const seen = new Set(); - let skip = true; - - /** @param {Compiler.AST.SvelteNode} node */ - function get_siblings(node) { - walk(node, null, { - RegularElement(node) { - if (node === element) { - skip = false; - if (include_self) siblings.push(node); - } else if (!skip) { - siblings.push(node); - } - }, - SvelteElement(node) { - if (node === element) { - skip = false; - if (include_self) siblings.push(node); - } else if (!skip) { - siblings.push(node); - } - }, - RenderTag(node) { - for (const snippet of node.metadata.snippets) { - if (seen.has(snippet)) continue; - - seen.add(snippet); - get_siblings(snippet.body); - } - } - }); - } - - for (const node of nodes.slice(nodes.indexOf(start))) { - get_siblings(node); - } - - return siblings; -} - /** * @param {any} operator * @param {any} expected_value @@ -822,6 +712,84 @@ function unquote(str) { return str; } +/** + * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node + * @param {boolean} adjacent_only + * @param {Set} seen + */ +function get_ancestor_elements(node, adjacent_only, seen = new Set()) { + /** @type {Array} */ + const ancestors = []; + + const path = node.metadata.path; + let i = path.length; + + while (i--) { + const parent = path[i]; + + if (parent.type === 'SnippetBlock') { + if (!seen.has(parent)) { + seen.add(parent); + + for (const site of parent.metadata.sites) { + ancestors.push(...get_ancestor_elements(site, adjacent_only, seen)); + } + } + + break; + } + + if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') { + ancestors.push(parent); + if (adjacent_only) { + break; + } + } + } + + return ancestors; +} + +/** + * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node + * @param {boolean} adjacent_only + * @param {Set} seen + */ +function get_descendant_elements(node, adjacent_only, seen = new Set()) { + /** @type {Array} */ + const descendants = []; + + /** + * @param {Compiler.AST.SvelteNode} node + */ + function walk_children(node) { + walk(node, null, { + _(node, context) { + if (node.type === 'RegularElement' || node.type === 'SvelteElement') { + descendants.push(node); + + if (!adjacent_only) { + context.next(); + } + } else if (node.type === 'RenderTag') { + for (const snippet of node.metadata.snippets) { + if (seen.has(snippet)) continue; + + seen.add(snippet); + walk_children(snippet.body); + } + } else { + context.next(); + } + } + }); + } + + walk_children(node.type === 'RenderTag' ? node : node.fragment); + + return descendants; +} + /** * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node * @returns {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null} @@ -843,11 +811,12 @@ function get_element_parent(node) { /** * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node + * @param {Direction} direction * @param {boolean} adjacent_only * @param {Set} seen * @returns {Map} */ -function get_possible_element_siblings(node, adjacent_only, seen = new Set()) { +function get_possible_element_siblings(node, direction, adjacent_only, seen = new Set()) { /** @type {Map} */ const result = new Map(); const path = node.metadata.path; @@ -859,9 +828,9 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) { while (i--) { const fragment = /** @type {Compiler.AST.Fragment} */ (path[i--]); - let j = fragment.nodes.indexOf(current); + let j = fragment.nodes.indexOf(current) + (direction === FORWARD ? 1 : -1); - while (j--) { + while (j >= 0 && j < fragment.nodes.length) { const node = fragment.nodes[j]; if (node.type === 'RegularElement') { @@ -876,21 +845,28 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) { return result; } } + // Special case: slots, render tags and svelte:element tags could resolve to no siblings, + // so we want to continue until we find a definite sibling even with the adjacent-only combinator } else if (is_block(node)) { if (node.type === 'SlotElement') { result.set(node, NODE_PROBABLY_EXISTS); } - const possible_last_child = get_possible_last_child(node, adjacent_only); + const possible_last_child = get_possible_nested_siblings(node, direction, adjacent_only); add_to_map(possible_last_child, result); if (adjacent_only && has_definite_elements(possible_last_child)) { return result; } - } else if (node.type === 'RenderTag' || node.type === 'SvelteElement') { + } else if (node.type === 'SvelteElement') { result.set(node, NODE_PROBABLY_EXISTS); - // Special case: slots, render tags and svelte:element tags could resolve to no siblings, - // so we want to continue until we find a definite sibling even with the adjacent-only combinator + } else if (node.type === 'RenderTag') { + result.set(node, NODE_PROBABLY_EXISTS); + for (const snippet of node.metadata.snippets) { + add_to_map(get_possible_nested_siblings(snippet, direction, adjacent_only), result); + } } + + j = direction === FORWARD ? j + 1 : j - 1; } current = path[i]; @@ -910,7 +886,7 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) { seen.add(current); for (const site of current.metadata.sites) { - const siblings = get_possible_element_siblings(site, adjacent_only, seen); + const siblings = get_possible_element_siblings(site, direction, adjacent_only, seen); add_to_map(siblings, result); if (adjacent_only && current.metadata.sites.size === 1 && has_definite_elements(siblings)) { @@ -923,7 +899,7 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) { if (current.type === 'EachBlock' && fragment === current.body) { // `{#each ...}{/each}` — `` can be previous sibling of `` - add_to_map(get_possible_last_child(current, adjacent_only), result); + add_to_map(get_possible_nested_siblings(current, direction, adjacent_only), result); } } @@ -931,11 +907,13 @@ function get_possible_element_siblings(node, adjacent_only, seen = new Set()) { } /** - * @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement} node + * @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement | Compiler.AST.SnippetBlock} node + * @param {Direction} direction * @param {boolean} adjacent_only + * @param {Set} seen * @returns {Map} */ -function get_possible_last_child(node, adjacent_only) { +function get_possible_nested_siblings(node, direction, adjacent_only, seen = new Set()) { /** @type {Array} */ let fragments = []; @@ -956,12 +934,20 @@ function get_possible_last_child(node, adjacent_only) { case 'SlotElement': fragments.push(node.fragment); break; + + case 'SnippetBlock': + if (seen.has(node)) { + return new Map(); + } + seen.add(node); + fragments.push(node.body); + break; } /** @type {Map} NodeMap */ const result = new Map(); - let exhaustive = node.type !== 'SlotElement'; + let exhaustive = node.type !== 'SlotElement' && node.type !== 'SnippetBlock'; for (const fragment of fragments) { if (fragment == null) { @@ -969,7 +955,7 @@ function get_possible_last_child(node, adjacent_only) { continue; } - const map = loop_child(fragment.nodes, adjacent_only); + const map = loop_child(fragment.nodes, direction, adjacent_only, seen); exhaustive &&= has_definite_elements(map); add_to_map(map, result); @@ -1012,27 +998,28 @@ function add_to_map(from, to) { } /** - * @param {NodeExistsValue | undefined} exist1 + * @param {NodeExistsValue} exist1 * @param {NodeExistsValue | undefined} exist2 * @returns {NodeExistsValue} */ function higher_existence(exist1, exist2) { - // @ts-expect-error TODO figure out if this is a bug - if (exist1 === undefined || exist2 === undefined) return exist1 || exist2; + if (exist2 === undefined) return exist1; return exist1 > exist2 ? exist1 : exist2; } /** * @param {Compiler.AST.SvelteNode[]} children + * @param {Direction} direction * @param {boolean} adjacent_only + * @param {Set} seen */ -function loop_child(children, adjacent_only) { +function loop_child(children, direction, adjacent_only, seen) { /** @type {Map} */ const result = new Map(); - let i = children.length; + let i = direction === FORWARD ? 0 : children.length - 1; - while (i--) { + while (i >= 0 && i < children.length) { const child = children[i]; if (child.type === 'RegularElement') { @@ -1042,13 +1029,19 @@ function loop_child(children, adjacent_only) { } } else if (child.type === 'SvelteElement') { result.set(child, NODE_PROBABLY_EXISTS); + } else if (child.type === 'RenderTag') { + for (const snippet of child.metadata.snippets) { + add_to_map(get_possible_nested_siblings(snippet, direction, adjacent_only, seen), result); + } } else if (is_block(child)) { - const child_result = get_possible_last_child(child, adjacent_only); + const child_result = get_possible_nested_siblings(child, direction, adjacent_only, seen); add_to_map(child_result, result); if (adjacent_only && has_definite_elements(child_result)) { break; } } + + i = direction === FORWARD ? i + 1 : i - 1; } return result; diff --git a/packages/svelte/tests/css/samples/has/_config.js b/packages/svelte/tests/css/samples/has/_config.js index 8d89d98cbd..5700a09b96 100644 --- a/packages/svelte/tests/css/samples/has/_config.js +++ b/packages/svelte/tests/css/samples/has/_config.js @@ -6,210 +6,238 @@ export default test({ code: 'css_unused_selector', message: 'Unused CSS selector ".unused:has(y)"', start: { - line: 33, + line: 41, column: 1, - character: 330 + character: 378 }, end: { - line: 33, + line: 41, column: 15, - character: 344 + character: 392 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".unused:has(:global(y))"', start: { - line: 36, + line: 44, column: 1, - character: 365 + character: 413 }, end: { - line: 36, + line: 44, column: 24, - character: 388 + character: 436 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(.unused)"', start: { - line: 39, + line: 47, column: 1, - character: 409 + character: 457 }, end: { - line: 39, + line: 47, column: 15, - character: 423 + character: 471 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ":global(.foo):has(.unused)"', start: { - line: 42, + line: 50, column: 1, - character: 444 + character: 492 }, end: { - line: 42, + line: 50, column: 27, - character: 470 + character: 518 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(y):has(.unused)"', start: { - line: 52, + line: 60, column: 1, - character: 578 + character: 626 }, end: { - line: 52, + line: 60, column: 22, - character: 599 + character: 647 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".unused"', start: { - line: 71, + line: 79, column: 2, - character: 804 + character: 852 }, end: { - line: 71, + line: 79, column: 9, - character: 811 + character: 859 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".unused x:has(y)"', start: { - line: 87, + line: 95, column: 1, - character: 958 + character: 1006 }, end: { - line: 87, + line: 95, column: 17, - character: 974 + character: 1022 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ".unused:has(.unused)"', start: { - line: 90, + line: 98, column: 1, - character: 995 + character: 1043 }, end: { - line: 90, + line: 98, column: 21, - character: 1015 + character: 1063 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(> z)"', start: { - line: 100, + line: 108, column: 1, - character: 1115 + character: 1163 }, end: { - line: 100, + line: 108, column: 11, - character: 1125 + character: 1173 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(> d)"', start: { - line: 103, + line: 111, column: 1, - character: 1146 + character: 1194 }, end: { - line: 103, + line: 111, column: 11, - character: 1156 + character: 1204 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "x:has(~ y)"', start: { - line: 123, + line: 131, column: 1, - character: 1348 + character: 1396 }, end: { - line: 123, + line: 131, column: 11, - character: 1358 + character: 1406 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector "d:has(+ f)"', + start: { + line: 141, + column: 1, + character: 1494 + }, + end: { + line: 141, + column: 11, + character: 1504 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "f:has(~ d)"', start: { - line: 133, + line: 144, column: 1, - character: 1446 + character: 1525 }, end: { - line: 133, + line: 144, column: 11, - character: 1456 + character: 1535 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ":has(.unused)"', start: { - line: 141, + line: 152, column: 2, - character: 1529 + character: 1608 }, end: { - line: 141, + line: 152, column: 15, - character: 1542 + character: 1621 } }, { code: 'css_unused_selector', message: 'Unused CSS selector "&:has(.unused)"', start: { - line: 147, + line: 158, column: 2, - character: 1600 + character: 1679 }, end: { - line: 147, + line: 158, column: 16, - character: 1614 + character: 1693 } }, { code: 'css_unused_selector', message: 'Unused CSS selector ":global(.foo):has(.unused)"', start: { - line: 155, + line: 166, column: 1, - character: 1684 + character: 1763 }, end: { - line: 155, + line: 166, column: 27, - character: 1710 + character: 1789 + } + }, + { + code: 'css_unused_selector', + message: 'Unused CSS selector "h:has(> h > i)"', + start: { + line: 173, + column: 1, + character: 1848 + }, + end: { + line: 173, + column: 15, + character: 1862 } } ] diff --git a/packages/svelte/tests/css/samples/has/expected.css b/packages/svelte/tests/css/samples/has/expected.css index b257370d61..2ce4d2bec5 100644 --- a/packages/svelte/tests/css/samples/has/expected.css +++ b/packages/svelte/tests/css/samples/has/expected.css @@ -118,6 +118,9 @@ d.svelte-xyz:has(~ f:where(.svelte-xyz)) { color: green; } + /* (unused) d:has(+ f) { + color: red; + }*/ /* (unused) f:has(~ d) { color: red; }*/ @@ -143,3 +146,13 @@ /* (unused) :global(.foo):has(.unused) { color: red; }*/ + + g.svelte-xyz:has(> h:where(.svelte-xyz) > i:where(.svelte-xyz)) { + color: green; + } + /* (unused) h:has(> h > i) { + color: red; + }*/ + g.svelte-xyz:has(+ j:where(.svelte-xyz) > k:where(.svelte-xyz)) { + color: green; + } \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/has/input.svelte b/packages/svelte/tests/css/samples/has/input.svelte index 9b254996bf..033471bc16 100644 --- a/packages/svelte/tests/css/samples/has/input.svelte +++ b/packages/svelte/tests/css/samples/has/input.svelte @@ -9,6 +9,14 @@ + + + + + + + + diff --git a/packages/svelte/tests/css/samples/render-tag-loop/_config.js b/packages/svelte/tests/css/samples/render-tag-loop/_config.js index f623b92cc3..292c6c49ac 100644 --- a/packages/svelte/tests/css/samples/render-tag-loop/_config.js +++ b/packages/svelte/tests/css/samples/render-tag-loop/_config.js @@ -1,20 +1,5 @@ import { test } from '../../test'; export default test({ - warnings: [ - { - code: 'css_unused_selector', - message: 'Unused CSS selector "div + div"', - start: { - line: 19, - column: 1, - character: 185 - }, - end: { - line: 19, - column: 10, - character: 194 - } - } - ] + warnings: [] }); diff --git a/packages/svelte/tests/css/samples/render-tag-loop/expected.css b/packages/svelte/tests/css/samples/render-tag-loop/expected.css index 9ced15e964..3e449286c9 100644 --- a/packages/svelte/tests/css/samples/render-tag-loop/expected.css +++ b/packages/svelte/tests/css/samples/render-tag-loop/expected.css @@ -2,9 +2,12 @@ div.svelte-xyz div:where(.svelte-xyz) { color: green; } - /* (unused) div + div { - color: red; /* this is marked as unused, but only because we've written an infinite loop - worth fixing? *\/ - }*/ + div.svelte-xyz + div:where(.svelte-xyz) { + color: green; + } div.svelte-xyz:has(div:where(.svelte-xyz)) { color: green; } + span.svelte-xyz:has(~span:where(.svelte-xyz)) { + color: green; + } diff --git a/packages/svelte/tests/css/samples/render-tag-loop/input.svelte b/packages/svelte/tests/css/samples/render-tag-loop/input.svelte index ade8df5744..3c55261f18 100644 --- a/packages/svelte/tests/css/samples/render-tag-loop/input.svelte +++ b/packages/svelte/tests/css/samples/render-tag-loop/input.svelte @@ -12,14 +12,22 @@ {/snippet} +{#snippet c()} + + {@render c()} +{/snippet} + From aaeda65f2f31585a4e48b452b73874e10fd4ebfc Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 14 Mar 2025 22:46:52 +0100 Subject: [PATCH 15/75] docs: add docs on state_unsafe_mutation error (#14932) closes #14752 --- .../98-reference/.generated/client-errors.md | 24 +++++++++++++++++++ .../svelte/messages/client-errors/errors.md | 24 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 2c2e0707ea..0beb3cb9a9 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -133,3 +133,27 @@ Reading state that was created inside the same derived is forbidden. Consider us ``` Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state` ``` + +This error is thrown in a situation like this: + +```svelte + + + +``` + +Here, the `$derived` updates `count`, which is `$state` and therefore forbidden to do. It is forbidden because the reactive graph could become unstable as a result, leading to subtle bugs, like values being stale or effects firing in the wrong order. To prevent this, Svelte errors when detecting an update to a `$state` variable. + +To fix this: +- See if it's possible to refactor your `$derived` such that the update becomes unnecessary +- Think about why you need to update `$state` inside a `$derived` in the first place. Maybe it's because you're using `bind:`, which leads you down a bad code path, and separating input and output path (by splitting it up to an attribute and an event, or by using [Function bindings](bind#Function-bindings)) makes it possible avoid the update +- If it's unavoidable, you may need to use an [`$effect`]($effect) instead. This could include splitting parts of the `$derived` into an [`$effect`]($effect) which does the updates diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index ce1f222c63..ab4d1519c1 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -87,3 +87,27 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long ## state_unsafe_mutation > Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state` + +This error is thrown in a situation like this: + +```svelte + + + +``` + +Here, the `$derived` updates `count`, which is `$state` and therefore forbidden to do. It is forbidden because the reactive graph could become unstable as a result, leading to subtle bugs, like values being stale or effects firing in the wrong order. To prevent this, Svelte errors when detecting an update to a `$state` variable. + +To fix this: +- See if it's possible to refactor your `$derived` such that the update becomes unnecessary +- Think about why you need to update `$state` inside a `$derived` in the first place. Maybe it's because you're using `bind:`, which leads you down a bad code path, and separating input and output path (by splitting it up to an attribute and an event, or by using [Function bindings](bind#Function-bindings)) makes it possible avoid the update +- If it's unavoidable, you may need to use an [`$effect`]($effect) instead. This could include splitting parts of the `$derived` into an [`$effect`]($effect) which does the updates From 32ee6c1bc252023e79acb9c6c255079964dfb665 Mon Sep 17 00:00:00 2001 From: adiGuba Date: Sat, 15 Mar 2025 18:46:44 +0100 Subject: [PATCH 16/75] rune_invalid_arguments_length (#15516) --- .changeset/two-spies-lie.md | 5 +++++ .../compiler/phases/2-analyze/visitors/CallExpression.js | 2 +- .../samples/runes-wrong-state-raw-args/_config.js | 8 ++++++++ .../samples/runes-wrong-state-raw-args/main.svelte | 3 +++ .../samples/runes-wrong-state-raw-args/main.svelte.js | 1 + 5 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 .changeset/two-spies-lie.md create mode 100644 packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte create mode 100644 packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte.js diff --git a/.changeset/two-spies-lie.md b/.changeset/two-spies-lie.md new file mode 100644 index 0000000000..2ea7fd6136 --- /dev/null +++ b/.changeset/two-spies-lie.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: throw rune_invalid_arguments_length when $state.raw() is used with more than 1 arg diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 6c21717852..6ef323725b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -117,7 +117,7 @@ export function CallExpression(node, context) { if ((rune === '$derived' || rune === '$derived.by') && node.arguments.length !== 1) { e.rune_invalid_arguments_length(node, rune, 'exactly one argument'); - } else if (rune === '$state' && node.arguments.length > 1) { + } else if (node.arguments.length > 1) { e.rune_invalid_arguments_length(node, rune, 'zero or one arguments'); } diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/_config.js new file mode 100644 index 0000000000..af226559d1 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'rune_invalid_arguments_length', + message: '`$state.raw` must be called with zero or one arguments' + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte new file mode 100644 index 0000000000..2b50b43b9a --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte.js new file mode 100644 index 0000000000..442aaad142 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-state-raw-args/main.svelte.js @@ -0,0 +1 @@ +const foo = $state.raw(1, 2, 3); From f30d75ab7e289f741379de7d715c0cb2508a3596 Mon Sep 17 00:00:00 2001 From: Garik Asplund <111464359+garikAsplund@users.noreply.github.com> Date: Sun, 16 Mar 2025 14:25:52 -0700 Subject: [PATCH 17/75] =?UTF-8?q?updated=20->=20to=20=20=E2=86=92=20in=20v?= =?UTF-8?q?5-migration-guide=20(#15526)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../docs/07-misc/07-v5-migration-guide.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/documentation/docs/07-misc/07-v5-migration-guide.md b/documentation/docs/07-misc/07-v5-migration-guide.md index 87ff40cf47..36e9776364 100644 --- a/documentation/docs/07-misc/07-v5-migration-guide.md +++ b/documentation/docs/07-misc/07-v5-migration-guide.md @@ -10,7 +10,7 @@ You don't have to migrate to the new syntax right away - Svelte 5 still supports At the heart of Svelte 5 is the new runes API. Runes are basically compiler instructions that inform Svelte about reactivity. Syntactically, runes are functions starting with a dollar-sign. -### let -> $state +### let → $state In Svelte 4, a `let` declaration at the top level of a component was implicitly reactive. In Svelte 5, things are more explicit: a variable is reactive when created using the `$state` rune. Let's migrate the counter to runes mode by wrapping the counter in `$state`: @@ -25,7 +25,7 @@ Nothing else changes. `count` is still the number itself, and you read and write > [!DETAILS] Why we did this > `let` being implicitly reactive at the top level worked great, but it meant that reactivity was constrained - a `let` declaration anywhere else was not reactive. This forced you to resort to using stores when refactoring code out of the top level of components for reuse. This meant you had to learn an entirely separate reactivity model, and the result often wasn't as nice to work with. Because reactivity is more explicit in Svelte 5, you can keep using the same API outside the top level of components. Head to [the tutorial](/tutorial) to learn more. -### $: -> $derived/$effect +### $: → $derived/$effect In Svelte 4, a `$:` statement at the top level of a component could be used to declare a derivation, i.e. state that is entirely defined through a computation of other state. In Svelte 5, this is achieved using the `$derived` rune: @@ -73,7 +73,7 @@ Note that [when `$effect` runs is different]($effect#Understanding-dependencies) > - executing dependencies as needed and therefore being immune to ordering problems > - being TypeScript-friendly -### export let -> $props +### export let → $props In Svelte 4, properties of a component were declared using `export let`. Each property was one declaration. In Svelte 5, all properties are declared through the `$props` rune, through destructuring: @@ -466,11 +466,11 @@ By now you should have a pretty good understanding of the before/after and how t We thought the same, which is why we provide a migration script to do most of the migration automatically. You can upgrade your project by using `npx sv migrate svelte-5`. This will do the following things: - bump core dependencies in your `package.json` -- migrate to runes (`let` -> `$state` etc) -- migrate to event attributes for DOM elements (`on:click` -> `onclick`) -- migrate slot creations to render tags (`` -> `{@render children()}`) -- migrate slot usages to snippets (`
...
` -> `{#snippet x()}
...
{/snippet}`) -- migrate obvious component creations (`new Component(...)` -> `mount(Component, ...)`) +- migrate to runes (`let` → `$state` etc) +- migrate to event attributes for DOM elements (`on:click` → `onclick`) +- migrate slot creations to render tags (`` → `{@render children()}`) +- migrate slot usages to snippets (`
...
` → `{#snippet x()}
...
{/snippet}`) +- migrate obvious component creations (`new Component(...)` → `mount(Component, ...)`) You can also migrate a single component in VS Code through the `Migrate Component to Svelte 5 Syntax` command, or in our Playground through the `Migrate` button. From e5881eade3e53316ee4329349cc1297c79d8522d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 16 Mar 2025 17:32:22 -0400 Subject: [PATCH 18/75] chore: tweak migration doc diff blocks (#15527) --- .../docs/07-misc/07-v5-migration-guide.md | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/documentation/docs/07-misc/07-v5-migration-guide.md b/documentation/docs/07-misc/07-v5-migration-guide.md index 36e9776364..e502b7921a 100644 --- a/documentation/docs/07-misc/07-v5-migration-guide.md +++ b/documentation/docs/07-misc/07-v5-migration-guide.md @@ -16,7 +16,7 @@ In Svelte 4, a `let` declaration at the top level of a component was implicitly ```svelte ``` @@ -31,8 +31,8 @@ In Svelte 4, a `$:` statement at the top level of a component could be used to d ```svelte ``` @@ -42,7 +42,8 @@ A `$:` statement could also be used to create side effects. In Svelte 5, this is ```svelte ``` @@ -105,8 +106,8 @@ In Svelte 5, the `$props` rune makes this straightforward without any additional ```svelte @@ -192,9 +193,9 @@ This function is deprecated in Svelte 5. Instead, components should accept _call ```svelte From 74917ae7039e512d6bf26b2b04f433eea7da8cd3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 16 Mar 2025 20:25:35 -0400 Subject: [PATCH 19/75] Version Packages (#15501) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/brown-rockets-shake.md | 5 ----- .changeset/cuddly-chefs-refuse.md | 5 ----- .changeset/curvy-countries-flow.md | 5 ----- .changeset/gold-hairs-jog.md | 5 ----- .changeset/hungry-dancers-tap.md | 5 ----- .changeset/plenty-bats-lay.md | 5 ----- .changeset/two-spies-lie.md | 5 ----- packages/svelte/CHANGELOG.md | 18 ++++++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 10 files changed, 20 insertions(+), 37 deletions(-) delete mode 100644 .changeset/brown-rockets-shake.md delete mode 100644 .changeset/cuddly-chefs-refuse.md delete mode 100644 .changeset/curvy-countries-flow.md delete mode 100644 .changeset/gold-hairs-jog.md delete mode 100644 .changeset/hungry-dancers-tap.md delete mode 100644 .changeset/plenty-bats-lay.md delete mode 100644 .changeset/two-spies-lie.md diff --git a/.changeset/brown-rockets-shake.md b/.changeset/brown-rockets-shake.md deleted file mode 100644 index 3772a88f6e..0000000000 --- a/.changeset/brown-rockets-shake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: invalidate parent effects when child effects update parent dependencies diff --git a/.changeset/cuddly-chefs-refuse.md b/.changeset/cuddly-chefs-refuse.md deleted file mode 100644 index 6672ac4ab3..0000000000 --- a/.changeset/cuddly-chefs-refuse.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly match `:has()` selector during css pruning diff --git a/.changeset/curvy-countries-flow.md b/.changeset/curvy-countries-flow.md deleted file mode 100644 index 6ef8545804..0000000000 --- a/.changeset/curvy-countries-flow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: replace `undefined` with `void 0` to avoid edge case diff --git a/.changeset/gold-hairs-jog.md b/.changeset/gold-hairs-jog.md deleted file mode 100644 index eaafced314..0000000000 --- a/.changeset/gold-hairs-jog.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: allow global-like pseudo-selectors refinement diff --git a/.changeset/hungry-dancers-tap.md b/.changeset/hungry-dancers-tap.md deleted file mode 100644 index 51b2f86019..0000000000 --- a/.changeset/hungry-dancers-tap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: don't distribute unused types definitions diff --git a/.changeset/plenty-bats-lay.md b/.changeset/plenty-bats-lay.md deleted file mode 100644 index cd5ce66e42..0000000000 --- a/.changeset/plenty-bats-lay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: add `files` and `group` to HTMLInputAttributes in elements.d.ts diff --git a/.changeset/two-spies-lie.md b/.changeset/two-spies-lie.md deleted file mode 100644 index 2ea7fd6136..0000000000 --- a/.changeset/two-spies-lie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: throw rune_invalid_arguments_length when $state.raw() is used with more than 1 arg diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 65b3edd1fd..e10a606fe4 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,23 @@ # svelte +## 5.23.1 + +### Patch Changes + +- fix: invalidate parent effects when child effects update parent dependencies ([#15506](https://github.com/sveltejs/svelte/pull/15506)) + +- fix: correctly match `:has()` selector during css pruning ([#15277](https://github.com/sveltejs/svelte/pull/15277)) + +- fix: replace `undefined` with `void 0` to avoid edge case ([#15511](https://github.com/sveltejs/svelte/pull/15511)) + +- fix: allow global-like pseudo-selectors refinement ([#15313](https://github.com/sveltejs/svelte/pull/15313)) + +- chore: don't distribute unused types definitions ([#15473](https://github.com/sveltejs/svelte/pull/15473)) + +- fix: add `files` and `group` to HTMLInputAttributes in elements.d.ts ([#15492](https://github.com/sveltejs/svelte/pull/15492)) + +- fix: throw rune_invalid_arguments_length when $state.raw() is used with more than 1 arg ([#15516](https://github.com/sveltejs/svelte/pull/15516)) + ## 5.23.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c74c9d34ca..6f10b2a9ea 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.23.0", + "version": "5.23.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 5f06fd0753..32a50f3bce 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.23.0'; +export const VERSION = '5.23.1'; export const PUBLIC_VERSION = '5'; From 5b9f0df8ee97ba43a3d3af18f99f2dd44bd86965 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Tue, 18 Mar 2025 11:51:26 +0100 Subject: [PATCH 20/75] fix: don't hoist listeners that access non hoistable snippets (#15534) * fix: don't hoist listeners that access non hoistable snippets * chore: add comment * chore: fix auto import fumble --- .changeset/thick-pans-fold.md | 5 +++++ .../compiler/phases/2-analyze/visitors/Attribute.js | 9 +++++++++ .../unhoist-function-accessing-snippet/_config.js | 12 ++++++++++++ .../unhoist-function-accessing-snippet/main.svelte | 12 ++++++++++++ 4 files changed, 38 insertions(+) create mode 100644 .changeset/thick-pans-fold.md create mode 100644 packages/svelte/tests/runtime-runes/samples/unhoist-function-accessing-snippet/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/unhoist-function-accessing-snippet/main.svelte diff --git a/.changeset/thick-pans-fold.md b/.changeset/thick-pans-fold.md new file mode 100644 index 0000000000..b5b5cee53e --- /dev/null +++ b/.changeset/thick-pans-fold.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't hoist listeners that access non hoistable snippets diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 9124a8822f..3ba81767cc 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -183,6 +183,15 @@ function get_delegated_event(event_name, handler, context) { const binding = scope.get(reference); const local_binding = context.state.scope.get(reference); + // if the function access a snippet that can't be hoisted we bail out + if ( + local_binding !== null && + local_binding.initial?.type === 'SnippetBlock' && + !local_binding.initial.metadata.can_hoist + ) { + return unhoisted; + } + // If we are referencing a binding that is shadowed in another scope then bail out. if (local_binding !== null && binding !== null && local_binding.node !== binding.node) { return unhoisted; diff --git a/packages/svelte/tests/runtime-runes/samples/unhoist-function-accessing-snippet/_config.js b/packages/svelte/tests/runtime-runes/samples/unhoist-function-accessing-snippet/_config.js new file mode 100644 index 0000000000..b1229f5a8a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/unhoist-function-accessing-snippet/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, errors }) { + const button = target.querySelector('button'); + flushSync(() => { + button?.click(); + }); + assert.deepEqual(errors, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/unhoist-function-accessing-snippet/main.svelte b/packages/svelte/tests/runtime-runes/samples/unhoist-function-accessing-snippet/main.svelte new file mode 100644 index 0000000000..e909d77fd6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/unhoist-function-accessing-snippet/main.svelte @@ -0,0 +1,12 @@ + + + + +{#snippet snip()} + snippet {x} +{/snippet} \ No newline at end of file From 0af6f20c77c209a5ea5691f2d1c15e0e359fbed6 Mon Sep 17 00:00:00 2001 From: henrykrinkle01 <162001892+henrykrinkle01@users.noreply.github.com> Date: Tue, 18 Mar 2025 17:54:32 +0700 Subject: [PATCH 21/75] Fix grammar (#15533) --- documentation/docs/06-runtime/02-context.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/06-runtime/02-context.md b/documentation/docs/06-runtime/02-context.md index 30799215b6..87b93a92b2 100644 --- a/documentation/docs/06-runtime/02-context.md +++ b/documentation/docs/06-runtime/02-context.md @@ -30,7 +30,7 @@ export const myGlobalState = $state({ This has a few drawbacks though: - it only safely works when your global state is only used client-side - for example, when you're building a single page application that does not render any of your components on the server. If your state ends up being managed and updated on the server, it could end up being shared between sessions and/or users, causing bugs -- it may give the false impression that certain state is global when in reality it should only used in a certain part of your app +- it may give the false impression that certain state is global when in reality it should only be used in a certain part of your app To solve these drawbacks, Svelte provides a few `context` primitives which alleviate these problems. From 190c0c7653435fd983d13a9594f5d70bdb4dd26f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 13:28:29 -0400 Subject: [PATCH 22/75] Version Packages (#15536) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/thick-pans-fold.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/thick-pans-fold.md diff --git a/.changeset/thick-pans-fold.md b/.changeset/thick-pans-fold.md deleted file mode 100644 index b5b5cee53e..0000000000 --- a/.changeset/thick-pans-fold.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't hoist listeners that access non hoistable snippets diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index e10a606fe4..6461df1d25 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.23.2 + +### Patch Changes + +- fix: don't hoist listeners that access non hoistable snippets ([#15534](https://github.com/sveltejs/svelte/pull/15534)) + ## 5.23.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 6f10b2a9ea..d005eca0b9 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.23.1", + "version": "5.23.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 32a50f3bce..191b52ecef 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.23.1'; +export const VERSION = '5.23.2'; export const PUBLIC_VERSION = '5'; From 8f940ee0ff12be2ae6b393b4e021507d3f3e2068 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 19 Mar 2025 10:41:13 -0400 Subject: [PATCH 23/75] docs: use function bindings in "when not to use effect" (#15544) --- documentation/docs/02-runes/04-$effect.md | 42 ++++------------------- 1 file changed, 7 insertions(+), 35 deletions(-) diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index e346bceba8..6a2b565aea 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -280,7 +280,7 @@ You might be tempted to do something convoluted with effects to link one value t ``` -Instead, use callbacks where possible ([demo](/playground/untitled#H4sIAAAAAAAACo1SMW6EMBD8imWluFMSIEUaDiKlvy5lSOHjlhOSMRZeTiDkv8deMEEJRcqdmZ1ZjzzxqpZgePo5cRw18JQA_sSVaPz0rnVk7iDRYxdhYA8vW4Wg0NnwzJRdrfGtUAVKQIYtCsly9pIkp4AZ7cQOezAoEA7JcWUkVBuCdol0dNWrEutWsV5fHfnhPQ5wZJMnCwyejxCh6G6A0V3IHk4zu_jOxzzPBxBld83PTr7xXrb3rUNw8PbiYJ3FP22oTIoLSComq5XuXTeu8LzgnVA3KDgj13wiQ8taRaJ82rzXskYM-URRlsXktejjgNLoo9e4fyf70_8EnwncySX1GuunX6kGRwnzR_BgaPNaGy3FmLJKwrCUeBM6ZUn0Cs2mOlp3vwthQJ5i14P9st9vZqQlsQIAAA==)): +Instead, use `oninput` callbacks or — better still — [function bindings](bind#Function-bindings) where possible ([demo](/playground/untitled#H4sIAAAAAAAAE51SsW6DMBT8FcvqABINdOhCIFKXTt06lg4GHpElYyz8iECIf69tcIIipo6-u3f3fPZMJWuBpvRzkBXyTpKSy5rLq6YRbbgATdOfmeKkrMgCBt9GPpQ66RsItFjJNBzhVScRJBobmumq5wovhSxQABLskAmSk7ckOXtMKyM22ItGhhAk4Z0R0OwIN-tIQzd-90HVhvy2HsGNiQFCMltBgd7XoecV2xzXNV7XaEcth7ZfRv7kujnsTX2Qd7USb5rFjwZkJlgJwpWRcakG04cpOS9oz-QVCuoeInXW-RyEJL-sG0b7Wy6kZWM-u7CFxM5tdrIl9qg72vB74H-y7T2iXROHyVb0CLanp1yNk4D1A1jQ91hzrQSbUtIIGLcir0ylJDm9Q7urz42bX4UwIk2xH2D5Xf4A7SeMcMQCAAA=)): ```svelte ``` -If you need to use bindings, for whatever reason (for example when you want some kind of "writable `$derived`"), consider using getters and setters to synchronise state ([demo](/playground/untitled#H4sIAAAAAAAACpWRwW6DMBBEf8WyekikFOihFwcq9TvqHkyyQUjGsfCCQMj_XnvBNKpy6Qn2DTOD1wu_tRocF18Lx9kCFwT4iRvVxenT2syNoDGyWjl4xi93g2AwxPDSXfrW4oc0EjUgwzsqzSr2VhTnxJwNHwf24lAhHIpjVDZNwy1KS5wlNoGMSg9wOCYksQccerMlv65p51X0p_Xpdt_4YEy9yTkmV3z4MJT579-bUqsaNB2kbI0dwlnCgirJe2UakJzVrbkKaqkWivasU1O1ULxnOVk3JU-Uxti0p_-vKO4no_enbQ_yXhnZn0aHs4b1jiJMK7q2zmo1C3bTMG3LaZQVrMjeoSPgaUtkDxePMCEX2Ie6b_8D4WyJJEwCAAA=)): - -```svelte - - - - - -``` - If you absolutely have to update `$state` within an effect and run into an infinite loop because you read and write to the same `$state`, use [untrack](svelte#untrack). From 701f085c82d11e6064433731d36b33d4894c706a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 19 Mar 2025 10:43:45 -0400 Subject: [PATCH 24/75] docs: rewrite context docs (#15541) --- documentation/docs/06-runtime/02-context.md | 164 ++++++++++---------- 1 file changed, 86 insertions(+), 78 deletions(-) diff --git a/documentation/docs/06-runtime/02-context.md b/documentation/docs/06-runtime/02-context.md index 87b93a92b2..b698323a04 100644 --- a/documentation/docs/06-runtime/02-context.md +++ b/documentation/docs/06-runtime/02-context.md @@ -2,129 +2,137 @@ title: Context --- - +Context allows components to access values owned by parent components without passing them down as props (potentially through many layers of intermediate components, known as 'prop-drilling'). The parent component sets context with `setContext(key, value)`... -Most state is component-level state that lives as long as its component lives. There's also section-wide or app-wide state however, which also needs to be handled somehow. - -The easiest way to do that is to create global state and just import that. +```svelte + + ``` +...and the child retrieves it with `getContext`: + ```svelte - + + +

{message}, inside Child.svelte

``` -This has a few drawbacks though: +This is particularly useful when `Parent.svelte` is not directly aware of `Child.svelte`, but instead renders it as part of a `children` [snippet](snippet) ([demo](/playground/untitled#H4sIAAAAAAAAE42Q3W6DMAyFX8WyJgESK-oto6hTX2D3YxcM3IIUQpR40yqUd58CrCXsp7tL7HNsf2dAWXaEKR56yfTBGOOxFWQwfR6Qz8q1XAHjL-GjUhvzToJd7bU09FO9ctMkG0wxM5VuFeeFLLjtVK8ZnkpNkuGo-w6CTTJ9Z3PwsBAemlbUF934W8iy5DpaZtOUcU02-ZLcaS51jHEkTFm_kY1_wfOO8QnXrb8hBzDEc6pgZ4gFoyz4KgiD7nxfTe8ghqAhIfrJ46cTzVZBbkPlODVJsLCDO6V7ZcJoncyw1yRr0hd1GNn_ZbEM3I9i1bmVxOlWElUvDUNHxpQngt3C4CXzjS1rtvkw22wMrTRtTbC8Lkuabe7jvthPPe3DofYCAAA=)): + +```svelte + + + +``` -- it only safely works when your global state is only used client-side - for example, when you're building a single page application that does not render any of your components on the server. If your state ends up being managed and updated on the server, it could end up being shared between sessions and/or users, causing bugs -- it may give the false impression that certain state is global when in reality it should only be used in a certain part of your app +The key (`'my-context'`, in the example above) and the context itself can be any JavaScript value. -To solve these drawbacks, Svelte provides a few `context` primitives which alleviate these problems. +In addition to [`setContext`](svelte#setContext) and [`getContext`](svelte#getContext), Svelte exposes [`hasContext`](svelte#hasContext) and [`getAllContexts`](svelte#getAllContexts) functions. -## Setting and getting context +## Using context with state -To associate an arbitrary object with the current component, use `setContext`. +You can store reactive state in context ([demo](/playground/untitled#H4sIAAAAAAAAE41R0W6DMAz8FSuaBNUQdK8MkKZ-wh7HHihzu6hgosRMm1D-fUpSVNq12x4iEvvOx_kmQU2PIhfP3DCCJGgHYvxkkYid7NCI_GUS_KUcxhVEMjOelErNB3bsatvG4LW6n0ZsRC4K02qpuKqpZtmrQTNMYJA3QRAs7PTQQxS40eMCt3mX3duxnWb-lS5h7nTI0A4jMWoo4c44P_Hku-zrOazdy64chWo-ScfRkRgl8wgHKrLTH1OxHZkHgoHaTraHcopXUFYzPPVfuC_hwQaD1GrskdiNCdQwJljJqlvXfyqVsA5CGg0uRUQifHw56xFtciO75QrP07vo_JXf_tf8yK2ezDKY_ZWt_1y2qqYzv7bI1IW1V_sN19m-07wCAAA=))... ```svelte + + + + + + ``` -The context is then available to children of the component (including slotted content) with `getContext`. +...though note that if you _reassign_ `counter` instead of updating it, you will 'break the link' — in other words instead of this... ```svelte - + ``` -`setContext` and `getContext` solve the above problems: +...you must do this: -- the state is not global, it's scoped to the component. That way it's safe to render your components on the server and not leak state -- it's clear that the state is not global but rather scoped to a specific component tree and therefore can't be used in other parts of your app +```svelte + +``` -> [!NOTE] `setContext`/`getContext` must be called during component initialisation. +Svelte will warn you if you get it wrong. -Context is not inherently reactive. If you need reactive values in context then you can pass a `$state` object into context, whose properties _will_ be reactive. +## Type-safe context -```svelte - - +```js +/// file: context.js +// @filename: ambient.d.ts +interface User {} - -``` +// @filename: index.js +// ---cut--- +import { getContext, setContext } from 'svelte'; -```svelte - - +/** @param {User} user */ +export function setUserContext(user) { + setContext(key, user); +} -

Count is {value.count}

+export function getUserContext() { + return /** @type {User} */ (getContext(key)); +} ``` -To check whether a given `key` has been set in the context of a parent component, use `hasContext`. +## Replacing global state -```svelte - + // ... +}); ``` -You can also retrieve the whole context map that belongs to the closest parent component using `getAllContexts`. This is useful, for example, if you programmatically create a component and want to pass the existing context to it. +In many cases this is perfectly fine, but there is a risk: if you mutate the state during server-side rendering (which is discouraged, but entirely possible!)... ```svelte + ``` -## Encapsulating context interactions - -The above methods are very unopinionated about how to use them. When your app grows in scale, it's worthwhile to encapsulate setting and getting the context into functions and properly type them. - -```ts -// @errors: 2304 -import { getContext, setContext } from 'svelte'; - -let userKey = Symbol('user'); - -export function setUserContext(user: User) { - setContext(userKey, user); -} - -export function getUserContext(): User { - return getContext(userKey) as User; -} -``` +...then the data may be accessible by the _next_ user. Context solves this problem because it is not shared between requests. From 99ca7a4d7f5948f94c2fa0137a481b57c4a6c17b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 19 Mar 2025 14:06:20 -0400 Subject: [PATCH 25/75] chore: create stack lazily when proxying value (#15547) --- packages/svelte/src/internal/client/proxy.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 4c262880f1..29828a7c99 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -25,11 +25,6 @@ import { tracing_mode_flag } from '../flags/index.js'; * @returns {T} */ export function proxy(value, parent = null, prev) { - /** @type {Error | null} */ - var stack = null; - if (DEV && tracing_mode_flag) { - stack = get_stack('CreatedAt'); - } // if non-proxyable, or is already a proxy, return `value` if (typeof value !== 'object' || value === null || STATE_SYMBOL in value) { return value; @@ -46,6 +41,8 @@ export function proxy(value, parent = null, prev) { var is_proxied_array = is_array(value); var version = source(0); + var stack = DEV && tracing_mode_flag ? get_stack('CreatedAt') : null; + if (is_proxied_array) { // We need to create the length source eagerly to ensure that // mutations to the array are properly synced with our proxy From c436b6cdbe01577b219ddbfd09e23c5765515004 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 19 Mar 2025 17:21:26 -0400 Subject: [PATCH 26/75] fix: simplify set calls for proxyable values (#15548) * chore: simplify set calls for proxyable values * changeset --- .changeset/nine-laws-rush.md | 5 ++ .../phases/3-transform/client/types.d.ts | 2 +- .../phases/3-transform/client/utils.js | 8 ---- .../client/visitors/AssignmentExpression.js | 46 +++++++------------ .../3-transform/client/visitors/ClassBody.js | 15 ++---- .../client/visitors/shared/declarations.js | 4 +- .../src/internal/client/reactivity/sources.js | 8 +++- .../_expected/client/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 4 +- .../_expected/client/index.svelte.js | 4 +- .../_expected/client/index.svelte.js | 2 +- 11 files changed, 40 insertions(+), 60 deletions(-) create mode 100644 .changeset/nine-laws-rush.md diff --git a/.changeset/nine-laws-rush.md b/.changeset/nine-laws-rush.md new file mode 100644 index 0000000000..e0a0fc15a0 --- /dev/null +++ b/.changeset/nine-laws-rush.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: simplify set calls for proxyable values diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 63fe3223cf..243e1c64a3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -30,7 +30,7 @@ export interface ClientTransformState extends TransformState { /** turn `foo` into e.g. `$.get(foo)` */ read: (id: Identifier) => Expression; /** turn `foo = bar` into e.g. `$.set(foo, bar)` */ - assign?: (node: Identifier, value: Expression) => Expression; + assign?: (node: Identifier, value: Expression, proxy?: boolean) => Expression; /** turn `foo.bar = baz` into e.g. `$.mutate(foo, $.get(foo).bar = baz);` */ mutate?: (node: Identifier, mutation: AssignmentExpression | UpdateExpression) => Expression; /** turn `foo++` into e.g. `$.update(foo)` */ diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index 421118cf68..28e3fabb19 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -45,14 +45,6 @@ export function build_getter(node, state) { return node; } -/** - * @param {Expression} value - * @param {Expression} previous - */ -export function build_proxy_reassignment(value, previous) { - return dev ? b.call('$.proxy', value, b.null, previous) : b.call('$.proxy', value); -} - /** * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node * @param {ComponentContext} context diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index a8c615af93..150c56e166 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -1,5 +1,4 @@ -/** @import { Location } from 'locate-character' */ -/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Literal, MemberExpression, Pattern } from 'estree' */ +/** @import { AssignmentExpression, AssignmentOperator, Expression, Identifier, Pattern } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { Context } from '../types.js' */ import * as b from '../../../../utils/builders.js'; @@ -8,8 +7,8 @@ import { get_attribute_expression, is_event_attribute } from '../../../../utils/ast.js'; -import { dev, filename, is_ignored, locate_node, locator } from '../../../../state.js'; -import { build_proxy_reassignment, should_proxy } from '../utils.js'; +import { dev, is_ignored, locate_node } from '../../../../state.js'; +import { should_proxy } from '../utils.js'; import { visit_assignment_expression } from '../../shared/assignments.js'; /** @@ -65,21 +64,12 @@ function build_assignment(operator, left, right, context) { context.visit(build_assignment_value(operator, left, right)) ); - if ( + const needs_proxy = private_state.kind === 'state' && is_non_coercive_operator(operator) && - should_proxy(value, context.state.scope) - ) { - value = build_proxy_reassignment(value, b.member(b.this, private_state.id)); - } - - if (context.state.in_constructor) { - // inside the constructor, we can assign to `this.#foo.v` rather than using `$.set`, - // since nothing is tracking the signal at this point - return b.assignment(operator, /** @type {Pattern} */ (context.visit(left)), value); - } + should_proxy(value, context.state.scope); - return b.call('$.set', left, value); + return b.call('$.set', left, value, needs_proxy && b.true); } } @@ -113,20 +103,18 @@ function build_assignment(operator, left, right, context) { context.visit(build_assignment_value(operator, left, right)) ); - if ( + return transform.assign( + object, + value, !is_primitive && - binding.kind !== 'prop' && - binding.kind !== 'bindable_prop' && - binding.kind !== 'raw_state' && - binding.kind !== 'store_sub' && - context.state.analysis.runes && - should_proxy(right, context.state.scope) && - is_non_coercive_operator(operator) - ) { - value = build_proxy_reassignment(value, object); - } - - return transform.assign(object, value); + binding.kind !== 'prop' && + binding.kind !== 'bindable_prop' && + binding.kind !== 'raw_state' && + binding.kind !== 'store_sub' && + context.state.analysis.runes && + should_proxy(right, context.state.scope) && + is_non_coercive_operator(operator) + ); } // mutation diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js index ed800e5226..5787b590a8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js @@ -5,7 +5,7 @@ import { dev, is_ignored } from '../../../../state.js'; import * as b from '../../../../utils/builders.js'; import { regex_invalid_identifier_chars } from '../../../patterns.js'; import { get_rune } from '../../../scope.js'; -import { build_proxy_reassignment, should_proxy } from '../utils.js'; +import { should_proxy } from '../utils.js'; /** * @param {ClassBody} node @@ -142,29 +142,20 @@ export function ClassBody(node, context) { // get foo() { return this.#foo; } body.push(b.method('get', definition.key, [], [b.return(b.call('$.get', member))])); - if (field.kind === 'state') { + if (field.kind === 'state' || field.kind === 'raw_state') { // set foo(value) { this.#foo = value; } const value = b.id('value'); - const prev = b.member(b.this, field.id); body.push( b.method( 'set', definition.key, [value], - [b.stmt(b.call('$.set', member, build_proxy_reassignment(value, prev)))] + [b.stmt(b.call('$.set', member, value, field.kind === 'state' && b.true))] ) ); } - if (field.kind === 'raw_state') { - // set foo(value) { this.#foo = value; } - const value = b.id('value'); - body.push( - b.method('set', definition.key, [value], [b.stmt(b.call('$.set', member, value))]) - ); - } - if (dev && (field.kind === 'derived' || field.kind === 'derived_by')) { body.push( b.method( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js index 0bd8c352f6..a13ecfed2c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js @@ -24,8 +24,8 @@ export function add_state_transformers(context) { ) { context.state.transform[name] = { read: binding.declaration_kind === 'var' ? (node) => b.call('$.safe_get', node) : get_value, - assign: (node, value) => { - let call = b.call('$.set', node, value); + assign: (node, value, proxy = false) => { + let call = b.call('$.set', node, value, proxy && b.true); if (context.state.scope.get(`$${node.name}`)?.kind === 'store_sub') { call = b.call('$.store_unsub', call, b.literal(`$${node.name}`), b.id('$$stores')); diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 49584e8626..92508945c9 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -33,6 +33,7 @@ import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { get_stack } from '../dev/tracing.js'; import { component_context, is_runes } from '../context.js'; +import { proxy } from '../proxy.js'; export let inspect_effects = new Set(); export const old_values = new Map(); @@ -143,9 +144,10 @@ export function mutate(source, value) { * @template V * @param {Source} source * @param {V} value + * @param {boolean} [should_proxy] * @returns {V} */ -export function set(source, value) { +export function set(source, value, should_proxy = false) { if ( active_reaction !== null && !untracking && @@ -158,7 +160,9 @@ export function set(source, value) { e.state_unsafe_mutation(); } - return internal_set(source, value); + let new_value = should_proxy ? proxy(value, null, source) : value; + + return internal_set(source, new_value); } /** diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js index fa990b33ee..390e86a351 100644 --- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js @@ -23,7 +23,7 @@ export default function Bind_component_snippet($$anchor) { return $.get(value); }, set value($$value) { - $.set(value, $.proxy($$value)); + $.set(value, $$value, true); } }); diff --git a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js index 2898f31a6f..2133974176 100644 --- a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client/index.svelte.js @@ -12,14 +12,14 @@ export default function Class_state_field_constructor_assignment($$anchor, $$pro } set a(value) { - $.set(this.#a, $.proxy(value)); + $.set(this.#a, value, true); } #b = $.state(); constructor() { this.a = 1; - this.#b.v = 2; + $.set(this.#b, 2); } } diff --git a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js index 9651713c52..47f297bce9 100644 --- a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js @@ -8,8 +8,8 @@ let d = 4; export function update(array) { ( - $.set(a, $.proxy(array[0])), - $.set(b, $.proxy(array[1])) + $.set(a, array[0], true), + $.set(b, array[1], true) ); [c, d] = array; diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js index c545608bca..762a23754c 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js @@ -13,7 +13,7 @@ export default function Function_prop_no_getter($$anchor) { Button($$anchor, { onmousedown: () => $.set(count, $.get(count) + 1), onmouseup, - onmouseenter: () => $.set(count, $.proxy(plusOne($.get(count)))), + onmouseenter: () => $.set(count, plusOne($.get(count)), true), children: ($$anchor, $$slotProps) => { $.next(); From c7ce9fc004325d5e5c957943f4c5e342e8304905 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 20 Mar 2025 14:19:42 -0400 Subject: [PATCH 27/75] fix benchmarks (#15560) --- benchmarking/compare/index.js | 1 - benchmarking/compare/runner.js | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/benchmarking/compare/index.js b/benchmarking/compare/index.js index a5fc6d10a9..9d8d279c35 100644 --- a/benchmarking/compare/index.js +++ b/benchmarking/compare/index.js @@ -2,7 +2,6 @@ import fs from 'node:fs'; import path from 'node:path'; import { execSync, fork } from 'node:child_process'; import { fileURLToPath } from 'node:url'; -import { benchmarks } from '../benchmarks.js'; // if (execSync('git status --porcelain').toString().trim()) { // console.error('Working directory is not clean'); diff --git a/benchmarking/compare/runner.js b/benchmarking/compare/runner.js index 6fa58e2bac..a2e8646379 100644 --- a/benchmarking/compare/runner.js +++ b/benchmarking/compare/runner.js @@ -1,7 +1,7 @@ -import { benchmarks } from '../benchmarks.js'; +import { reactivity_benchmarks } from '../benchmarks/reactivity/index.js'; const results = []; -for (const benchmark of benchmarks) { +for (const benchmark of reactivity_benchmarks) { const result = await benchmark(); console.error(result.benchmark); results.push(result); From 6915c12b583f4d62c125161be07f5d09573918c9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 20 Mar 2025 16:04:00 -0400 Subject: [PATCH 28/75] feat: allow state created in deriveds/effects to be written/read locally without self-invalidation (#15553) * move parent property onto Signal * don't self-invalidate when updating a source create inside current reaction * lazily create deep state with parent reaction * no need to push_derived_source with mutable_state, as it never coexists with $.derived * reduce indirection * remove state_unsafe_local_read error * changeset * tests * fix test * inelegant fix * remove arg * tweak * some progress * more * tidy up * parent -> p * tmp * alternative approach * tidy up * reduce diff size * more * update comment --- .changeset/dirty-pianos-sparkle.md | 5 ++ .../98-reference/.generated/client-errors.md | 6 -- .../svelte/messages/client-errors/errors.md | 4 -- .../3-transform/client/transform-client.js | 5 +- .../client/visitors/VariableDeclaration.js | 4 +- .../svelte/src/internal/client/constants.js | 1 + packages/svelte/src/internal/client/errors.js | 15 ----- packages/svelte/src/internal/client/index.js | 9 ++- packages/svelte/src/internal/client/proxy.js | 67 +++++++++++++++---- .../src/internal/client/reactivity/sources.js | 57 +++++----------- .../svelte/src/internal/client/runtime.js | 30 +++++---- packages/svelte/tests/signals/test.ts | 49 ++++++++++++-- 12 files changed, 151 insertions(+), 101 deletions(-) create mode 100644 .changeset/dirty-pianos-sparkle.md diff --git a/.changeset/dirty-pianos-sparkle.md b/.changeset/dirty-pianos-sparkle.md new file mode 100644 index 0000000000..b3e4dd1d8c --- /dev/null +++ b/.changeset/dirty-pianos-sparkle.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: allow state created in deriveds/effects to be written/read locally without self-invalidation diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 0beb3cb9a9..62d9c3302a 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -122,12 +122,6 @@ Property descriptors defined on `$state` objects must contain `value` and always Cannot set prototype of `$state` object ``` -### state_unsafe_local_read - -``` -Reading state that was created inside the same derived is forbidden. Consider using `untrack` to read locally created state -``` - ### state_unsafe_mutation ``` diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index ab4d1519c1..bc8ec36256 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -80,10 +80,6 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long > Cannot set prototype of `$state` object -## state_unsafe_local_read - -> Reading state that was created inside the same derived is forbidden. Consider using `untrack` to read locally created state - ## state_unsafe_mutation > Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state` 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 ac8263b916..0bdfbae746 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 @@ -219,7 +219,10 @@ export function client_component(analysis, options) { for (const [name, binding] of analysis.instance.scope.declarations) { if (binding.kind === 'legacy_reactive') { legacy_reactive_declarations.push( - b.const(name, b.call('$.mutable_state', undefined, analysis.immutable ? b.true : undefined)) + b.const( + name, + b.call('$.mutable_source', undefined, analysis.immutable ? b.true : undefined) + ) ); } if (binding.kind === 'store_sub') { 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 baffc5dec3..3a914fb560 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 @@ -299,7 +299,7 @@ function create_state_declarators(declarator, { scope, analysis }, value) { return [ b.declarator( declarator.id, - b.call('$.mutable_state', value, analysis.immutable ? b.true : undefined) + b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined) ) ]; } @@ -314,7 +314,7 @@ function create_state_declarators(declarator, { scope, analysis }, value) { return b.declarator( path.node, binding?.kind === 'state' - ? b.call('$.mutable_state', value, analysis.immutable ? b.true : undefined) + ? b.call('$.mutable_source', value, analysis.immutable ? b.true : undefined) : value ); }) diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index a4840ce4eb..21377c1cc8 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -20,6 +20,7 @@ export const LEGACY_DERIVED_PROP = 1 << 17; export const INSPECT_EFFECT = 1 << 18; export const HEAD_EFFECT = 1 << 19; export const EFFECT_HAS_DERIVED = 1 << 20; +export const EFFECT_IS_UPDATING = 1 << 21; export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL_METADATA = Symbol('$state metadata'); diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 682816e1d6..8a5b5033a7 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -307,21 +307,6 @@ export function state_prototype_fixed() { } } -/** - * Reading state that was created inside the same derived is forbidden. Consider using `untrack` to read locally created state - * @returns {never} - */ -export function state_unsafe_local_read() { - if (DEV) { - const error = new Error(`state_unsafe_local_read\nReading state that was created inside the same derived is forbidden. Consider using \`untrack\` to read locally created state\nhttps://svelte.dev/e/state_unsafe_local_read`); - - error.name = 'Svelte error'; - throw error; - } else { - throw new Error(`https://svelte.dev/e/state_unsafe_local_read`); - } -} - /** * Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state` * @returns {never} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 31da00dbb4..723ff57678 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -113,7 +113,14 @@ export { user_effect, user_pre_effect } from './reactivity/effects.js'; -export { mutable_state, mutate, set, state, update, update_pre } from './reactivity/sources.js'; +export { + mutable_source, + mutate, + set, + source as state, + update, + update_pre +} from './reactivity/sources.js'; export { prop, rest_props, diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 29828a7c99..9c3c0cf29f 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -1,6 +1,6 @@ /** @import { ProxyMetadata, Source } from '#client' */ import { DEV } from 'esm-env'; -import { get, active_effect } from './runtime.js'; +import { get, active_effect, active_reaction, set_active_reaction } from './runtime.js'; import { component_context } from './context.js'; import { array_prototype, @@ -17,14 +17,16 @@ import * as e from './errors.js'; import { get_stack } from './dev/tracing.js'; import { tracing_mode_flag } from '../flags/index.js'; +/** @type {ProxyMetadata | null} */ +var parent_metadata = null; + /** * @template T * @param {T} value - * @param {ProxyMetadata | null} [parent] * @param {Source} [prev] dev mode only * @returns {T} */ -export function proxy(value, parent = null, prev) { +export function proxy(value, prev) { // if non-proxyable, or is already a proxy, return `value` if (typeof value !== 'object' || value === null || STATE_SYMBOL in value) { return value; @@ -42,6 +44,31 @@ export function proxy(value, parent = null, prev) { var version = source(0); var stack = DEV && tracing_mode_flag ? get_stack('CreatedAt') : null; + var reaction = active_reaction; + + /** + * @template T + * @param {() => T} fn + */ + var with_parent = (fn) => { + var previous_reaction = active_reaction; + set_active_reaction(reaction); + + /** @type {T} */ + var result; + + if (DEV) { + var previous_metadata = parent_metadata; + parent_metadata = metadata; + result = fn(); + parent_metadata = previous_metadata; + } else { + result = fn(); + } + + set_active_reaction(previous_reaction); + return result; + }; if (is_proxied_array) { // We need to create the length source eagerly to ensure that @@ -54,7 +81,7 @@ export function proxy(value, parent = null, prev) { if (DEV) { metadata = { - parent, + parent: parent_metadata, owners: null }; @@ -66,7 +93,7 @@ export function proxy(value, parent = null, prev) { metadata.owners = prev_owners ? new Set(prev_owners) : null; } else { metadata.owners = - parent === null + parent_metadata === null ? component_context !== null ? new Set([component_context.function]) : null @@ -92,10 +119,13 @@ export function proxy(value, parent = null, prev) { var s = sources.get(prop); if (s === undefined) { - s = source(descriptor.value, stack); + s = with_parent(() => source(descriptor.value, stack)); sources.set(prop, s); } else { - set(s, proxy(descriptor.value, metadata)); + set( + s, + with_parent(() => proxy(descriptor.value)) + ); } return true; @@ -106,7 +136,10 @@ export function proxy(value, parent = null, prev) { if (s === undefined) { if (prop in target) { - sources.set(prop, source(UNINITIALIZED, stack)); + sources.set( + prop, + with_parent(() => source(UNINITIALIZED, stack)) + ); } } else { // When working with arrays, we need to also ensure we update the length when removing @@ -140,7 +173,7 @@ export function proxy(value, parent = null, prev) { // create a source, but only if it's an own property and not a prototype property if (s === undefined && (!exists || get_descriptor(target, prop)?.writable)) { - s = source(proxy(exists ? target[prop] : UNINITIALIZED, metadata), stack); + s = with_parent(() => source(proxy(exists ? target[prop] : UNINITIALIZED), stack)); sources.set(prop, s); } @@ -208,7 +241,7 @@ export function proxy(value, parent = null, prev) { (active_effect !== null && (!has || get_descriptor(target, prop)?.writable)) ) { if (s === undefined) { - s = source(has ? proxy(target[prop], metadata) : UNINITIALIZED, stack); + s = with_parent(() => source(has ? proxy(target[prop]) : UNINITIALIZED, stack)); sources.set(prop, s); } @@ -235,7 +268,7 @@ export function proxy(value, parent = null, prev) { // If the item exists in the original, we need to create a uninitialized source, // else a later read of the property would result in a source being created with // the value of the original item at that index. - other_s = source(UNINITIALIZED, stack); + other_s = with_parent(() => source(UNINITIALIZED, stack)); sources.set(i + '', other_s); } } @@ -247,13 +280,19 @@ export function proxy(value, parent = null, prev) { // object property before writing to that property. if (s === undefined) { if (!has || get_descriptor(target, prop)?.writable) { - s = source(undefined, stack); - set(s, proxy(value, metadata)); + s = with_parent(() => source(undefined, stack)); + set( + s, + with_parent(() => proxy(value)) + ); sources.set(prop, s); } } else { has = s.v !== UNINITIALIZED; - set(s, proxy(value, metadata)); + set( + s, + with_parent(() => proxy(value)) + ); } if (DEV) { diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 92508945c9..cac8431b4e 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -11,8 +11,8 @@ import { untrack, increment_write_version, update_effect, - derived_sources, - set_derived_sources, + reaction_sources, + set_reaction_sources, check_dirtiness, untracking, is_destroying_effect @@ -27,7 +27,8 @@ import { UNOWNED, MAYBE_DIRTY, BLOCK_EFFECT, - ROOT_EFFECT + ROOT_EFFECT, + EFFECT_IS_UPDATING } from '../constants.js'; import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; @@ -51,6 +52,7 @@ export function set_inspect_effects(v) { * @param {Error | null} [stack] * @returns {Source} */ +// TODO rename this to `state` throughout the codebase export function source(v, stack) { /** @type {Value} */ var signal = { @@ -62,6 +64,14 @@ export function source(v, stack) { wv: 0 }; + if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) { + if (reaction_sources === null) { + set_reaction_sources([signal]); + } else { + reaction_sources.push(signal); + } + } + if (DEV && tracing_mode_flag) { signal.created = stack ?? get_stack('CreatedAt'); signal.debug = null; @@ -70,14 +80,6 @@ export function source(v, stack) { return signal; } -/** - * @template V - * @param {V} v - */ -export function state(v) { - return push_derived_source(source(v)); -} - /** * @template V * @param {V} initial_value @@ -100,33 +102,6 @@ export function mutable_source(initial_value, immutable = false) { return s; } -/** - * @template V - * @param {V} v - * @param {boolean} [immutable] - * @returns {Source} - */ -export function mutable_state(v, immutable = false) { - return push_derived_source(mutable_source(v, immutable)); -} - -/** - * @template V - * @param {Source} source - */ -/*#__NO_SIDE_EFFECTS__*/ -function push_derived_source(source) { - if (active_reaction !== null && !untracking && (active_reaction.f & DERIVED) !== 0) { - if (derived_sources === null) { - set_derived_sources([source]); - } else { - derived_sources.push(source); - } - } - - return source; -} - /** * @template V * @param {Value} source @@ -153,14 +128,12 @@ export function set(source, value, should_proxy = false) { !untracking && is_runes() && (active_reaction.f & (DERIVED | BLOCK_EFFECT)) !== 0 && - // If the source was created locally within the current derived, then - // we allow the mutation. - (derived_sources === null || !derived_sources.includes(source)) + !reaction_sources?.includes(source) ) { e.state_unsafe_mutation(); } - let new_value = should_proxy ? proxy(value, null, source) : value; + let new_value = should_proxy ? proxy(value, source) : value; return internal_set(source, new_value); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 0a65c6e45a..74b58ee1a9 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -22,7 +22,8 @@ import { ROOT_EFFECT, LEGACY_DERIVED_PROP, DISCONNECTED, - BOUNDARY_EFFECT + BOUNDARY_EFFECT, + EFFECT_IS_UPDATING } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { internal_set, old_values } from './reactivity/sources.js'; @@ -87,17 +88,17 @@ export function set_active_effect(effect) { } /** - * When sources are created within a derived, we record them so that we can safely allow - * local mutations to these sources without the side-effect error being invoked unnecessarily. + * When sources are created within a reaction, reading and writing + * them should not cause a re-run * @type {null | Source[]} */ -export let derived_sources = null; +export let reaction_sources = null; /** * @param {Source[] | null} sources */ -export function set_derived_sources(sources) { - derived_sources = sources; +export function set_reaction_sources(sources) { + reaction_sources = sources; } /** @@ -367,6 +368,9 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true) for (var i = 0; i < reactions.length; i++) { var reaction = reactions[i]; + + if (reaction_sources?.includes(signal)) continue; + if ((reaction.f & DERIVED) !== 0) { schedule_possible_effect_self_invalidation(/** @type {Derived} */ (reaction), effect, false); } else if (effect === reaction) { @@ -391,9 +395,10 @@ export function update_reaction(reaction) { var previous_untracked_writes = untracked_writes; var previous_reaction = active_reaction; var previous_skip_reaction = skip_reaction; - var prev_derived_sources = derived_sources; + var previous_reaction_sources = reaction_sources; var previous_component_context = component_context; var previous_untracking = untracking; + var flags = reaction.f; new_deps = /** @type {null | Value[]} */ (null); @@ -403,11 +408,13 @@ export function update_reaction(reaction) { (flags & UNOWNED) !== 0 && (untracking || !is_updating_effect || active_reaction === null); active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null; - derived_sources = null; + reaction_sources = null; set_component_context(reaction.ctx); untracking = false; read_version++; + reaction.f |= EFFECT_IS_UPDATING; + try { var result = /** @type {Function} */ (0, reaction.fn)(); var deps = reaction.deps; @@ -477,9 +484,11 @@ export function update_reaction(reaction) { untracked_writes = previous_untracked_writes; active_reaction = previous_reaction; skip_reaction = previous_skip_reaction; - derived_sources = prev_derived_sources; + reaction_sources = previous_reaction_sources; set_component_context(previous_component_context); untracking = previous_untracking; + + reaction.f ^= EFFECT_IS_UPDATING; } } @@ -866,9 +875,6 @@ export function get(signal) { // Register the dependency on the current reaction signal. if (active_reaction !== null && !untracking) { - if (derived_sources !== null && derived_sources.includes(signal)) { - e.state_unsafe_local_read(); - } var deps = active_reaction.deps; if (signal.rv < read_version) { signal.rv = read_version; diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index ef4cf16d3b..72f99c90e5 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -8,7 +8,12 @@ import { render_effect, user_effect } from '../../src/internal/client/reactivity/effects'; -import { state, set, update, update_pre } from '../../src/internal/client/reactivity/sources'; +import { + source as state, + set, + update, + update_pre +} from '../../src/internal/client/reactivity/sources'; import type { Derived, Effect, Value } from '../../src/internal/client/types'; import { proxy } from '../../src/internal/client/proxy'; import { derived } from '../../src/internal/client/reactivity/deriveds'; @@ -487,6 +492,26 @@ describe('signals', () => { }; }); + test('schedules rerun when updating deeply nested value', (runes) => { + if (!runes) return () => {}; + + const value = proxy({ a: { b: { c: 0 } } }); + user_effect(() => { + value.a.b.c += 1; + }); + + return () => { + let errored = false; + try { + flushSync(); + } catch (e: any) { + assert.include(e.message, 'effect_update_depth_exceeded'); + errored = true; + } + assert.equal(errored, true); + }; + }); + test('schedules rerun when writing to signal before reading it', (runes) => { if (!runes) return () => {}; @@ -958,14 +983,30 @@ describe('signals', () => { }; }); - test('deriveds cannot depend on state they own', () => { + test('deriveds do not depend on state they own', () => { return () => { + let s; + const d = derived(() => { - const s = state(0); + s = state(0); return $.get(s); }); - assert.throws(() => $.get(d), 'state_unsafe_local_read'); + assert.equal($.get(d), 0); + + set(s!, 1); + assert.equal($.get(d), 0); + }; + }); + + test('effects do not depend on state they own', () => { + user_effect(() => { + const value = state(0); + set(value, $.get(value) + 1); + }); + + return () => { + flushSync(); }; }); From 1a5fb8fd51cdec1a72df9ec3100317bea83698aa Mon Sep 17 00:00:00 2001 From: Robert Gieseke Date: Fri, 21 Mar 2025 14:28:44 +0100 Subject: [PATCH 29/75] fix: Keep inlined JSDoc comments in property conversion of svelte-migrate (#15567) * Add failing JSDoc property svelte-migrate conversion tests * Add further test case and remove default value in JSDoc output * Look for inlined JSDoc comments after a hyphen * Add changeset --- .changeset/happy-cameras-bow.md | 5 +++++ packages/svelte/src/compiler/migrate/index.js | 7 +++++-- .../samples/jsdoc-with-comments/input.svelte | 9 +++++++++ .../samples/jsdoc-with-comments/output.svelte | 14 +++++++++++++- 4 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 .changeset/happy-cameras-bow.md diff --git a/.changeset/happy-cameras-bow.md b/.changeset/happy-cameras-bow.md new file mode 100644 index 0000000000..47188f4f6d --- /dev/null +++ b/.changeset/happy-cameras-bow.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +Keep inlined trailing JSDoc comments of properties when running svelte-migrate diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 1bb7a69a20..02bb5b1443 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -1592,7 +1592,6 @@ function extract_type_and_comment(declarator, state, path) { const comment_start = /** @type {any} */ (comment_node)?.start; const comment_end = /** @type {any} */ (comment_node)?.end; let comment = comment_node && str.original.substring(comment_start, comment_end); - if (comment_node) { str.update(comment_start, comment_end, ''); } @@ -1673,6 +1672,11 @@ function extract_type_and_comment(declarator, state, path) { state.has_type_or_fallback = true; const match = /@type {(.+)}/.exec(comment_node.value); if (match) { + // try to find JSDoc comments after a hyphen `-` + const jsdocComment = /@type {.+} (?:\w+|\[.*?\]) - (.+)/.exec(comment_node.value); + if (jsdocComment) { + cleaned_comment += jsdocComment[1]?.trim(); + } return { type: match[1], comment: cleaned_comment, @@ -1693,7 +1697,6 @@ function extract_type_and_comment(declarator, state, path) { }; } } - return { type: 'any', comment: state.uses_ts ? comment : cleaned_comment, diff --git a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte index f2efb1db80..f138c3a070 100644 --- a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte +++ b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/input.svelte @@ -21,6 +21,9 @@ */ export let type_no_comment; + /** @type {boolean} type_with_comment - One-line declaration with comment */ + export let type_with_comment; + /** * This is optional */ @@ -40,4 +43,10 @@ export let inline_multiline_trailing_comment = 'world'; /* * this is a same-line trailing multiline comment **/ + + /** @type {number} [default_value=1] */ + export let default_value = 1; + + /** @type {number} [comment_default_value=1] - This has a comment and an optional value. */ + export let comment_default_value = 1; \ No newline at end of file diff --git a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte index 19fbe38b50..32133ccd4c 100644 --- a/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte +++ b/packages/svelte/tests/migrate/samples/jsdoc-with-comments/output.svelte @@ -9,12 +9,18 @@ + + + + + + /** * @typedef {Object} Props * @property {string} comment - My wonderful comment @@ -22,11 +28,14 @@ * @property {any} one_line - one line comment * @property {any} no_comment * @property {boolean} type_no_comment + * @property {boolean} type_with_comment - One-line declaration with comment * @property {any} [optional] - This is optional * @property {any} inline_commented - this should stay a comment * @property {any} inline_commented_merged - This comment should be merged - with this inline comment * @property {string} [inline_multiline_leading_comment] - this is a same-line leading multiline comment * @property {string} [inline_multiline_trailing_comment] - this is a same-line trailing multiline comment + * @property {number} [default_value] + * @property {number} [comment_default_value] - This has a comment and an optional value. */ /** @type {Props} */ @@ -36,10 +45,13 @@ one_line, no_comment, type_no_comment, + type_with_comment, optional = {stuff: true}, inline_commented, inline_commented_merged, inline_multiline_leading_comment = 'world', - inline_multiline_trailing_comment = 'world' + inline_multiline_trailing_comment = 'world', + default_value = 1, + comment_default_value = 1 } = $props(); \ No newline at end of file From 1d10a65b7858ca4da8d7ade113ec5b6f9c1afb43 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 21 Mar 2025 13:30:17 +0000 Subject: [PATCH 30/75] fix: check if DOM prototypes are extensible (#15569) --- .changeset/dry-ducks-roll.md | 5 +++ .../src/internal/client/dom/operations.js | 35 +++++++++++-------- packages/svelte/src/internal/shared/utils.js | 1 + 3 files changed, 26 insertions(+), 15 deletions(-) create mode 100644 .changeset/dry-ducks-roll.md diff --git a/.changeset/dry-ducks-roll.md b/.changeset/dry-ducks-roll.md new file mode 100644 index 0000000000..2dea8174dd --- /dev/null +++ b/.changeset/dry-ducks-roll.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: check if DOM prototypes are extensible diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index 0ad9045b20..aae44d4b39 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -2,7 +2,7 @@ import { hydrate_node, hydrating, set_hydrate_node } from './hydration.js'; import { DEV } from 'esm-env'; import { init_array_prototype_warnings } from '../dev/equality.js'; -import { get_descriptor } from '../../shared/utils.js'; +import { get_descriptor, is_extensible } from '../../shared/utils.js'; // export these for reference in the compiled code, making global name deduplication unnecessary /** @type {Window} */ @@ -34,26 +34,31 @@ export function init_operations() { var element_prototype = Element.prototype; var node_prototype = Node.prototype; + var text_prototype = Text.prototype; // @ts-ignore first_child_getter = get_descriptor(node_prototype, 'firstChild').get; // @ts-ignore next_sibling_getter = get_descriptor(node_prototype, 'nextSibling').get; - // the following assignments improve perf of lookups on DOM nodes - // @ts-expect-error - element_prototype.__click = undefined; - // @ts-expect-error - element_prototype.__className = undefined; - // @ts-expect-error - element_prototype.__attributes = null; - // @ts-expect-error - element_prototype.__style = undefined; - // @ts-expect-error - element_prototype.__e = undefined; - - // @ts-expect-error - Text.prototype.__t = undefined; + if (is_extensible(element_prototype)) { + // the following assignments improve perf of lookups on DOM nodes + // @ts-expect-error + element_prototype.__click = undefined; + // @ts-expect-error + element_prototype.__className = undefined; + // @ts-expect-error + element_prototype.__attributes = null; + // @ts-expect-error + element_prototype.__style = undefined; + // @ts-expect-error + element_prototype.__e = undefined; + } + + if (is_extensible(text_prototype)) { + // @ts-expect-error + text_prototype.__t = undefined; + } if (DEV) { // @ts-expect-error diff --git a/packages/svelte/src/internal/shared/utils.js b/packages/svelte/src/internal/shared/utils.js index f9d52cb065..5e7f3152d8 100644 --- a/packages/svelte/src/internal/shared/utils.js +++ b/packages/svelte/src/internal/shared/utils.js @@ -10,6 +10,7 @@ export var get_descriptors = Object.getOwnPropertyDescriptors; export var object_prototype = Object.prototype; export var array_prototype = Array.prototype; export var get_prototype_of = Object.getPrototypeOf; +export var is_extensible = Object.isExtensible; /** * @param {any} thing From d2e79326c7d3810cd4ec657660d4aeec464cd689 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Mar 2025 09:31:03 -0400 Subject: [PATCH 31/75] fix: don't depend on deriveds created inside the current reaction (#15564) * WIP * WIP * add test * update test * changeset * oops * lint --- .changeset/young-poets-wait.md | 5 +++ packages/svelte/src/internal/client/index.js | 11 +---- packages/svelte/src/internal/client/proxy.js | 2 +- .../internal/client/reactivity/deriveds.js | 16 ++++++- .../src/internal/client/reactivity/sources.js | 24 +++++++---- .../svelte/src/internal/client/runtime.js | 43 ++++++++++++------- .../samples/effect-cleanup/_config.js | 2 +- .../samples/untrack-own-deriveds/_config.js | 20 +++++++++ .../samples/untrack-own-deriveds/main.svelte | 26 +++++++++++ packages/svelte/tests/signals/test.ts | 7 +-- 10 files changed, 114 insertions(+), 42 deletions(-) create mode 100644 .changeset/young-poets-wait.md create mode 100644 packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/main.svelte diff --git a/.changeset/young-poets-wait.md b/.changeset/young-poets-wait.md new file mode 100644 index 0000000000..479f5027ef --- /dev/null +++ b/.changeset/young-poets-wait.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't depend on deriveds created inside the current reaction diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 723ff57678..a5f93e8b17 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -101,7 +101,7 @@ export { text, props_id } from './dom/template.js'; -export { derived, derived_safe_equal } from './reactivity/deriveds.js'; +export { user_derived as derived, derived_safe_equal } from './reactivity/deriveds.js'; export { effect_tracking, effect_root, @@ -113,14 +113,7 @@ export { user_effect, user_pre_effect } from './reactivity/effects.js'; -export { - mutable_source, - mutate, - set, - source as state, - update, - update_pre -} from './reactivity/sources.js'; +export { mutable_source, mutate, set, state, update, update_pre } from './reactivity/sources.js'; export { prop, rest_props, diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 9c3c0cf29f..ffe63f4b77 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -10,7 +10,7 @@ import { object_prototype } from '../shared/utils.js'; import { check_ownership, widen_ownership } from './dev/ownership.js'; -import { source, set } from './reactivity/sources.js'; +import { state as source, set } from './reactivity/sources.js'; import { STATE_SYMBOL, STATE_SYMBOL_METADATA } from './constants.js'; import { UNINITIALIZED } from '../../constants.js'; import * as e from './errors.js'; diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 795417cc0f..cd7bbba02f 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -8,7 +8,8 @@ import { skip_reaction, update_reaction, increment_write_version, - set_active_effect + set_active_effect, + push_reaction_value } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import * as e from '../errors.js'; @@ -61,6 +62,19 @@ export function derived(fn) { return signal; } +/** + * @template V + * @param {() => V} fn + * @returns {Derived} + */ +export function user_derived(fn) { + const d = derived(fn); + + push_reaction_value(d); + + return d; +} + /** * @template V * @param {() => V} fn diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index cac8431b4e..e4834902fe 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -15,7 +15,8 @@ import { set_reaction_sources, check_dirtiness, untracking, - is_destroying_effect + is_destroying_effect, + push_reaction_value } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; import { @@ -64,14 +65,6 @@ export function source(v, stack) { wv: 0 }; - if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) { - if (reaction_sources === null) { - set_reaction_sources([signal]); - } else { - reaction_sources.push(signal); - } - } - if (DEV && tracing_mode_flag) { signal.created = stack ?? get_stack('CreatedAt'); signal.debug = null; @@ -80,6 +73,19 @@ export function source(v, stack) { return signal; } +/** + * @template V + * @param {V} v + * @param {Error | null} [stack] + */ +export function state(v, stack) { + const s = source(v, stack); + + push_reaction_value(s); + + return s; +} + /** * @template V * @param {V} initial_value diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 74b58ee1a9..a5d26412a4 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -101,6 +101,17 @@ export function set_reaction_sources(sources) { reaction_sources = sources; } +/** @param {Value} value */ +export function push_reaction_value(value) { + if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) { + if (reaction_sources === null) { + set_reaction_sources([value]); + } else { + reaction_sources.push(value); + } + } +} + /** * The dependencies of the reaction that is currently being executed. In many cases, * the dependencies are unchanged between runs, and so this will be `null` unless @@ -875,21 +886,23 @@ export function get(signal) { // Register the dependency on the current reaction signal. if (active_reaction !== null && !untracking) { - var deps = active_reaction.deps; - if (signal.rv < read_version) { - signal.rv = read_version; - // If the signal is accessing the same dependencies in the same - // order as it did last time, increment `skipped_deps` - // rather than updating `new_deps`, which creates GC cost - if (new_deps === null && deps !== null && deps[skipped_deps] === signal) { - skipped_deps++; - } else if (new_deps === null) { - new_deps = [signal]; - } else if (!skip_reaction || !new_deps.includes(signal)) { - // Normally we can push duplicated dependencies to `new_deps`, but if we're inside - // an unowned derived because skip_reaction is true, then we need to ensure that - // we don't have duplicates - new_deps.push(signal); + if (!reaction_sources?.includes(signal)) { + var deps = active_reaction.deps; + if (signal.rv < read_version) { + signal.rv = read_version; + // If the signal is accessing the same dependencies in the same + // order as it did last time, increment `skipped_deps` + // rather than updating `new_deps`, which creates GC cost + if (new_deps === null && deps !== null && deps[skipped_deps] === signal) { + skipped_deps++; + } else if (new_deps === null) { + new_deps = [signal]; + } else if (!skip_reaction || !new_deps.includes(signal)) { + // Normally we can push duplicated dependencies to `new_deps`, but if we're inside + // an unowned derived because skip_reaction is true, then we need to ensure that + // we don't have duplicates + new_deps.push(signal); + } } } } else if ( diff --git a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js index 6a3d9eef77..e55733c148 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js @@ -10,6 +10,6 @@ export default test({ flushSync(() => { b1.click(); }); - assert.deepEqual(logs, ['init 0', 'cleanup 2', null, 'init 2', 'cleanup 4', null, 'init 4']); + assert.deepEqual(logs, ['init 0']); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js b/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js new file mode 100644 index 0000000000..18062b86fb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js @@ -0,0 +1,20 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target, logs }) { + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + + assert.htmlEqual( + target.innerHTML, + ` + +

1/2

+ class Foo { + value = $state(0); + double = $derived(this.value * 2); + + constructor() { + console.log(this.value, this.double); + } + + increment() { + this.value++; + } + } + + let foo = $state(); + + $effect(() => { + foo = new Foo(); + }); + + + + +{#if foo} +

{foo.value}/{foo.double}

+{/if} diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 72f99c90e5..3977caae36 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -8,12 +8,7 @@ import { render_effect, user_effect } from '../../src/internal/client/reactivity/effects'; -import { - source as state, - set, - update, - update_pre -} from '../../src/internal/client/reactivity/sources'; +import { state, set, update, update_pre } from '../../src/internal/client/reactivity/sources'; import type { Derived, Effect, Value } from '../../src/internal/client/types'; import { proxy } from '../../src/internal/client/proxy'; import { derived } from '../../src/internal/client/reactivity/deriveds'; From e25c2812961f9bb74ab50f1b034d8e5a5d8ae412 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 09:40:37 -0400 Subject: [PATCH 32/75] Version Packages (#15551) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/dirty-pianos-sparkle.md | 5 ----- .changeset/dry-ducks-roll.md | 5 ----- .changeset/happy-cameras-bow.md | 5 ----- .changeset/nine-laws-rush.md | 5 ----- .changeset/young-poets-wait.md | 5 ----- packages/svelte/CHANGELOG.md | 16 ++++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 8 files changed, 18 insertions(+), 27 deletions(-) delete mode 100644 .changeset/dirty-pianos-sparkle.md delete mode 100644 .changeset/dry-ducks-roll.md delete mode 100644 .changeset/happy-cameras-bow.md delete mode 100644 .changeset/nine-laws-rush.md delete mode 100644 .changeset/young-poets-wait.md diff --git a/.changeset/dirty-pianos-sparkle.md b/.changeset/dirty-pianos-sparkle.md deleted file mode 100644 index b3e4dd1d8c..0000000000 --- a/.changeset/dirty-pianos-sparkle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: allow state created in deriveds/effects to be written/read locally without self-invalidation diff --git a/.changeset/dry-ducks-roll.md b/.changeset/dry-ducks-roll.md deleted file mode 100644 index 2dea8174dd..0000000000 --- a/.changeset/dry-ducks-roll.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: check if DOM prototypes are extensible diff --git a/.changeset/happy-cameras-bow.md b/.changeset/happy-cameras-bow.md deleted file mode 100644 index 47188f4f6d..0000000000 --- a/.changeset/happy-cameras-bow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -Keep inlined trailing JSDoc comments of properties when running svelte-migrate diff --git a/.changeset/nine-laws-rush.md b/.changeset/nine-laws-rush.md deleted file mode 100644 index e0a0fc15a0..0000000000 --- a/.changeset/nine-laws-rush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: simplify set calls for proxyable values diff --git a/.changeset/young-poets-wait.md b/.changeset/young-poets-wait.md deleted file mode 100644 index 479f5027ef..0000000000 --- a/.changeset/young-poets-wait.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't depend on deriveds created inside the current reaction diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 6461df1d25..8cb7efd4ef 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,21 @@ # svelte +## 5.24.0 + +### Minor Changes + +- feat: allow state created in deriveds/effects to be written/read locally without self-invalidation ([#15553](https://github.com/sveltejs/svelte/pull/15553)) + +### Patch Changes + +- fix: check if DOM prototypes are extensible ([#15569](https://github.com/sveltejs/svelte/pull/15569)) + +- Keep inlined trailing JSDoc comments of properties when running svelte-migrate ([#15567](https://github.com/sveltejs/svelte/pull/15567)) + +- fix: simplify set calls for proxyable values ([#15548](https://github.com/sveltejs/svelte/pull/15548)) + +- fix: don't depend on deriveds created inside the current reaction ([#15564](https://github.com/sveltejs/svelte/pull/15564)) + ## 5.23.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index d005eca0b9..0aa6b29841 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.23.2", + "version": "5.24.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 191b52ecef..7cd43e74cb 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.23.2'; +export const VERSION = '5.24.0'; export const PUBLIC_VERSION = '5'; From 6b23a7c4777a123dc1ea4db6cb87e03268fbb45b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Mar 2025 09:52:30 -0400 Subject: [PATCH 33/75] chore: camelCase -> snake_case (#15573) --- packages/svelte/src/compiler/migrate/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 02bb5b1443..7f26d0d010 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -1673,9 +1673,9 @@ function extract_type_and_comment(declarator, state, path) { const match = /@type {(.+)}/.exec(comment_node.value); if (match) { // try to find JSDoc comments after a hyphen `-` - const jsdocComment = /@type {.+} (?:\w+|\[.*?\]) - (.+)/.exec(comment_node.value); - if (jsdocComment) { - cleaned_comment += jsdocComment[1]?.trim(); + const jsdoc_comment = /@type {.+} (?:\w+|\[.*?\]) - (.+)/.exec(comment_node.value); + if (jsdoc_comment) { + cleaned_comment += jsdoc_comment[1]?.trim(); } return { type: match[1], From 83d0c5894dc26c92274f162c9f9495038cabe37d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Mar 2025 09:52:51 -0400 Subject: [PATCH 34/75] docs: add note on effect-local state (#15572) --- documentation/docs/02-runes/04-$effect.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index 6a2b565aea..75f59102f9 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -74,6 +74,8 @@ Teardown functions also run when the effect is destroyed, which happens when its `$effect` automatically picks up any reactive values (`$state`, `$derived`, `$props`) that are _synchronously_ read inside its function body (including indirectly, via function calls) and registers them as dependencies. When those dependencies change, the `$effect` schedules a re-run. +If `$state` and `$derived` are used directly inside the `$effect` (for example, during creation of a [reactive class](https://svelte.dev/docs/svelte/$state#Classes)), those values will _not_ be treated as dependencies. + Values that are read _asynchronously_ — after an `await` or inside a `setTimeout`, for example — will not be tracked. Here, the canvas will be repainted when `color` changes, but not when `size` changes ([demo](/playground/untitled#H4sIAAAAAAAAE31T246bMBD9lZF3pWSlBEirfaEQqdo_2PatVIpjBrDkGGQPJGnEv1e2IZfVal-wfHzmzJyZ4cIqqdCy9M-F0blDlnqArZjmB3f72XWRHVCRw_bc4me4aDWhJstSlllhZEfbQhekkMDKfwg5PFvihMvX5OXH_CJa1Zrb0-Kpqr5jkiwC48rieuDWQbqgZ6wqFLRcvkC-hYvnkWi1dWqa8ESQTxFRjfQWsOXiWzmr0sSLhEJu3p1YsoJkNUcdZUnN9dagrBu6FVRQHAM10sJRKgUG16bXcGxQ44AGdt7SDkTDdY02iqLHnJVU6hedlWuIp94JW6Tf8oBt_8GdTxlF0b4n0C35ZLBzXb3mmYn3ae6cOW74zj0YVzDNYXRHFt9mprNgHfZSl6mzml8CMoLvTV6wTZIUDEJv5us2iwMtiJRyAKG4tXnhl8O0yhbML0Wm-B7VNlSSSd31BG7z8oIZZ6dgIffAVY_5xdU9Qrz1Bnx8fCfwtZ7v8Qc9j3nB8PqgmMWlHIID6-bkVaPZwDySfWtKNGtquxQ23Qlsq2QJT0KIqb8dL0up6xQ2eIBkAg_c1FI_YqW0neLnFCqFpwmreedJYT7XX8FVOBfwWRhXstZrSXiwKQjUhOZeMIleb5JZfHWn2Yq5pWEpmR7Hv-N_wEqT8hEEAAA=)): ```ts From ade66c6feade92cfd932dcb4be2812305e518d2b Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Fri, 21 Mar 2025 15:21:40 +0100 Subject: [PATCH 35/75] fix: use `get` in constructor for deriveds (#15300) Co-authored-by: Rich Harris --- .changeset/new-cherries-leave.md | 5 +++++ .../client/visitors/MemberExpression.js | 4 +++- .../samples/deriveds-in-constructor/_config.js | 5 +++++ .../deriveds-in-constructor/main.svelte | 18 ++++++++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 .changeset/new-cherries-leave.md create mode 100644 packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/main.svelte diff --git a/.changeset/new-cherries-leave.md b/.changeset/new-cherries-leave.md new file mode 100644 index 0000000000..738a78b4a3 --- /dev/null +++ b/.changeset/new-cherries-leave.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: use `get` in constructor for deriveds diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js index 501ecda555..3f2aada1f5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js @@ -11,7 +11,9 @@ export function MemberExpression(node, context) { if (node.property.type === 'PrivateIdentifier') { const field = context.state.private_state.get(node.property.name); if (field) { - return context.state.in_constructor ? b.member(node, 'v') : b.call('$.get', node); + return context.state.in_constructor && (field.kind === 'raw_state' || field.kind === 'state') + ? b.member(node, 'v') + : b.call('$.get', node); } } diff --git a/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/_config.js b/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/_config.js new file mode 100644 index 0000000000..b364a989f4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `

state,derived state,derived.by derived state

` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/main.svelte b/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/main.svelte new file mode 100644 index 0000000000..bc8efba7e7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/deriveds-in-constructor/main.svelte @@ -0,0 +1,18 @@ + + +

{foo.initial}

\ No newline at end of file From 1f37c02f918d6fa4d8a14de5d6868228e61dd05a Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 21 Mar 2025 14:25:46 +0000 Subject: [PATCH 36/75] fix: ensure toStore root effect is connected to correct parent effect (#15574) * fix: ensure toStore root effect is connected to correct parent effect * prettier --------- Co-authored-by: Rich Harris --- .changeset/twelve-bananas-destroy.md | 5 +++ packages/svelte/src/store/index-client.js | 35 +++++++++++++++---- .../samples/toStore-subscribe2/_config.js | 16 +++++++++ .../samples/toStore-subscribe2/main.svelte | 11 ++++++ 4 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 .changeset/twelve-bananas-destroy.md create mode 100644 packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/main.svelte diff --git a/.changeset/twelve-bananas-destroy.md b/.changeset/twelve-bananas-destroy.md new file mode 100644 index 0000000000..873ee21877 --- /dev/null +++ b/.changeset/twelve-bananas-destroy.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure toStore root effect is connected to correct parent effect diff --git a/packages/svelte/src/store/index-client.js b/packages/svelte/src/store/index-client.js index ae6806ec76..2f0a1a831a 100644 --- a/packages/svelte/src/store/index-client.js +++ b/packages/svelte/src/store/index-client.js @@ -6,6 +6,12 @@ import { } from '../internal/client/reactivity/effects.js'; import { get, writable } from './shared/index.js'; import { createSubscriber } from '../reactivity/create-subscriber.js'; +import { + active_effect, + active_reaction, + set_active_effect, + set_active_reaction +} from '../internal/client/runtime.js'; export { derived, get, readable, readonly, writable } from './shared/index.js'; @@ -39,19 +45,34 @@ export { derived, get, readable, readonly, writable } from './shared/index.js'; * @returns {Writable | Readable} */ export function toStore(get, set) { - let init_value = get(); + var effect = active_effect; + var reaction = active_reaction; + var init_value = get(); + const store = writable(init_value, (set) => { // If the value has changed before we call subscribe, then // we need to treat the value as already having run - let ran = init_value !== get(); + var ran = init_value !== get(); // TODO do we need a different implementation on the server? - const teardown = effect_root(() => { - render_effect(() => { - const value = get(); - if (ran) set(value); + var teardown; + // Apply the reaction and effect at the time of toStore being called + var previous_reaction = active_reaction; + var previous_effect = active_effect; + set_active_reaction(reaction); + set_active_effect(effect); + + try { + teardown = effect_root(() => { + render_effect(() => { + const value = get(); + if (ran) set(value); + }); }); - }); + } finally { + set_active_reaction(previous_reaction); + set_active_effect(previous_effect); + } ran = true; diff --git a/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/_config.js b/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/_config.js new file mode 100644 index 0000000000..bc1793e7a4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/_config.js @@ -0,0 +1,16 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + let btn = target.querySelector('button'); + + btn?.click(); + flushSync(); + + assert.htmlEqual( + target.innerHTML, + `
Count 1!
Count from store 1!
` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/main.svelte b/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/main.svelte new file mode 100644 index 0000000000..82d20105b8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/toStore-subscribe2/main.svelte @@ -0,0 +1,11 @@ + + +
Count {counter}!
+
Count from store {$count}!
+ + From 842a7c6995f94f46b1839fcac91042fd541e52ca Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Mar 2025 10:26:15 -0400 Subject: [PATCH 37/75] docs: update state_unsafe_mutation message (#15539) * docs: update state_unsafe_mutation message * regenerate * fix example --- .../98-reference/.generated/client-errors.md | 35 +++++++++++-------- .../svelte/messages/client-errors/errors.md | 35 +++++++++++-------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 62d9c3302a..901c49822c 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -128,26 +128,31 @@ Cannot set prototype of `$state` object Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state` ``` -This error is thrown in a situation like this: +This error occurs when state is updated while evaluating a `$derived`. You might encounter it while trying to 'derive' two pieces of state in one go: ```svelte - + + +

{count} is even: {even}

+

{count} is odd: {odd}

``` -Here, the `$derived` updates `count`, which is `$state` and therefore forbidden to do. It is forbidden because the reactive graph could become unstable as a result, leading to subtle bugs, like values being stale or effects firing in the wrong order. To prevent this, Svelte errors when detecting an update to a `$state` variable. +This is forbidden because it introduces instability: if `

{count} is even: {even}

` is updated before `odd` is recalculated, `even` will be stale. In most cases the solution is to make everything derived: + +```js +let even = $derived(count % 2 === 0); +let odd = $derived(!even); +``` -To fix this: -- See if it's possible to refactor your `$derived` such that the update becomes unnecessary -- Think about why you need to update `$state` inside a `$derived` in the first place. Maybe it's because you're using `bind:`, which leads you down a bad code path, and separating input and output path (by splitting it up to an attribute and an event, or by using [Function bindings](bind#Function-bindings)) makes it possible avoid the update -- If it's unavoidable, you may need to use an [`$effect`]($effect) instead. This could include splitting parts of the `$derived` into an [`$effect`]($effect) which does the updates +If side-effects are unavoidable, use [`$effect`]($effect) instead. diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index bc8ec36256..572930843e 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -84,26 +84,31 @@ See the [migration guide](/docs/svelte/v5-migration-guide#Components-are-no-long > Updating state inside a derived or a template expression is forbidden. If the value should not be reactive, declare it without `$state` -This error is thrown in a situation like this: +This error occurs when state is updated while evaluating a `$derived`. You might encounter it while trying to 'derive' two pieces of state in one go: ```svelte - + + +

{count} is even: {even}

+

{count} is odd: {odd}

``` -Here, the `$derived` updates `count`, which is `$state` and therefore forbidden to do. It is forbidden because the reactive graph could become unstable as a result, leading to subtle bugs, like values being stale or effects firing in the wrong order. To prevent this, Svelte errors when detecting an update to a `$state` variable. +This is forbidden because it introduces instability: if `

{count} is even: {even}

` is updated before `odd` is recalculated, `even` will be stale. In most cases the solution is to make everything derived: + +```js +let even = $derived(count % 2 === 0); +let odd = $derived(!even); +``` -To fix this: -- See if it's possible to refactor your `$derived` such that the update becomes unnecessary -- Think about why you need to update `$state` inside a `$derived` in the first place. Maybe it's because you're using `bind:`, which leads you down a bad code path, and separating input and output path (by splitting it up to an attribute and an event, or by using [Function bindings](bind#Function-bindings)) makes it possible avoid the update -- If it's unavoidable, you may need to use an [`$effect`]($effect) instead. This could include splitting parts of the `$derived` into an [`$effect`]($effect) which does the updates +If side-effects are unavoidable, use [`$effect`]($effect) instead. From 2d3b65dfbd589416d94661bd34ed7a99896bcde2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 10:28:23 -0400 Subject: [PATCH 38/75] Version Packages (#15575) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/new-cherries-leave.md | 5 ----- .changeset/twelve-bananas-destroy.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/new-cherries-leave.md delete mode 100644 .changeset/twelve-bananas-destroy.md diff --git a/.changeset/new-cherries-leave.md b/.changeset/new-cherries-leave.md deleted file mode 100644 index 738a78b4a3..0000000000 --- a/.changeset/new-cherries-leave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: use `get` in constructor for deriveds diff --git a/.changeset/twelve-bananas-destroy.md b/.changeset/twelve-bananas-destroy.md deleted file mode 100644 index 873ee21877..0000000000 --- a/.changeset/twelve-bananas-destroy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ensure toStore root effect is connected to correct parent effect diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 8cb7efd4ef..04ddfcadbd 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.24.1 + +### Patch Changes + +- fix: use `get` in constructor for deriveds ([#15300](https://github.com/sveltejs/svelte/pull/15300)) + +- fix: ensure toStore root effect is connected to correct parent effect ([#15574](https://github.com/sveltejs/svelte/pull/15574)) + ## 5.24.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 0aa6b29841..f321571e7a 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.24.0", + "version": "5.24.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 7cd43e74cb..565c190713 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.24.0'; +export const VERSION = '5.24.1'; export const PUBLIC_VERSION = '5'; From 5a8fa69dbf46e99beed812157ed78609f8054331 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Mar 2025 12:54:53 -0400 Subject: [PATCH 39/75] feat: make deriveds writable (#15570) * feat: make deriveds writable * add optimistic UI example * add note to when-not-to-use-effect * add section on deep reactivity * root-relative URL * use hash URL * mention const * make handler async, move into script block --- .changeset/clever-terms-tell.md | 5 ++ documentation/docs/02-runes/03-$derived.md | 42 +++++++++++++++++ documentation/docs/02-runes/04-$effect.md | 2 + .../phases/2-analyze/visitors/shared/utils.js | 31 +------------ .../runes-no-derived-assignment/_config.js | 8 ---- .../runes-no-derived-assignment/main.svelte | 5 -- .../runes-no-derived-binding/_config.js | 8 ---- .../runes-no-derived-binding/main.svelte | 6 --- .../_config.js | 8 ---- .../main.svelte | 10 ---- .../_config.js | 8 ---- .../main.svelte | 10 ---- .../runes-no-derived-update/_config.js | 8 ---- .../runes-no-derived-update/main.svelte | 5 -- .../samples/writable-derived/_config.js | 46 +++++++++++++++++++ .../samples/writable-derived/main.svelte | 9 ++++ .../reassign-derived-literal/errors.json | 14 ------ .../reassign-derived-literal/input.svelte | 9 ---- .../errors.json | 14 ------ .../input.svelte | 9 ---- .../reassign-derived-public-field/errors.json | 14 ------ .../input.svelte | 9 ---- 22 files changed, 105 insertions(+), 175 deletions(-) create mode 100644 .changeset/clever-terms-tell.md delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/_config.js delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/main.svelte delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/_config.js delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/main.svelte delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/_config.js delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/main.svelte delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/_config.js delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/main.svelte delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/_config.js delete mode 100644 packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/writable-derived/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/writable-derived/main.svelte delete mode 100644 packages/svelte/tests/validator/samples/reassign-derived-literal/errors.json delete mode 100644 packages/svelte/tests/validator/samples/reassign-derived-literal/input.svelte delete mode 100644 packages/svelte/tests/validator/samples/reassign-derived-private-field/errors.json delete mode 100644 packages/svelte/tests/validator/samples/reassign-derived-private-field/input.svelte delete mode 100644 packages/svelte/tests/validator/samples/reassign-derived-public-field/errors.json delete mode 100644 packages/svelte/tests/validator/samples/reassign-derived-public-field/input.svelte diff --git a/.changeset/clever-terms-tell.md b/.changeset/clever-terms-tell.md new file mode 100644 index 0000000000..606868bce3 --- /dev/null +++ b/.changeset/clever-terms-tell.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: make deriveds writable diff --git a/documentation/docs/02-runes/03-$derived.md b/documentation/docs/02-runes/03-$derived.md index 24ab643b68..2464aa9295 100644 --- a/documentation/docs/02-runes/03-$derived.md +++ b/documentation/docs/02-runes/03-$derived.md @@ -52,6 +52,48 @@ Anything read synchronously inside the `$derived` expression (or `$derived.by` f To exempt a piece of state from being treated as a dependency, use [`untrack`](svelte#untrack). +## Overriding derived values + +Derived expressions are recalculated when their dependencies change, but you can temporarily override their values by reassigning them (unless they are declared with `const`). This can be useful for things like _optimistic UI_, where a value is derived from the 'source of truth' (such as data from your server) but you'd like to show immediate feedback to the user: + +```svelte + + + +``` + +> [!NOTE] Prior to Svelte 5.25, deriveds were read-only. + +## Deriveds and reactivity + +Unlike `$state`, which converts objects and arrays to [deeply reactive proxies]($state#Deep-state), `$derived` values are left as-is. For example, [in a case like this](/playground/untitled#H4sIAAAAAAAAE4VU22rjMBD9lUHd3aaQi9PdstS1A3t5XvpQ2Ic4D7I1iUUV2UjjNMX431eS7TRdSosxgjMzZ45mjt0yzffIYibvy0ojFJWqDKCQVBk2ZVup0LJ43TJ6rn2aBxw-FP2o67k9oCKP5dziW3hRaUJNjoYltjCyplWmM1JIIAn3FlL4ZIkTTtYez6jtj4w8WwyXv9GiIXiQxLVs9pfTMR7EuoSLIuLFbX7Z4930bZo_nBrD1bs834tlfvsBz9_SyX6PZXu9XaL4gOWn4sXjeyzftv4ZWfyxubpzxzg6LfD4MrooxELEosKCUPigQCMPKCZh0OtQE1iSxcsmdHuBvCiHZXALLXiN08EL3RRkaJ_kDVGle0HcSD5TPEeVtj67O4Nrg9aiSNtBY5oODJkrL5QsHtN2cgXp6nSJMWzpWWGasdlsGEMbzi5jPr5KFr0Ep7pdeM2-TCelCddIhDxAobi1jqF3cMaC1RKp64bAW9iFAmXGIHfd4wNXDabtOLN53w8W53VvJoZLh7xk4Rr3CoL-UNoLhWHrT1JQGcM17u96oES5K-kc2XOzkzqGCKL5De79OUTyyrg1zgwXsrEx3ESfx4Bz0M5UjVMHB24mw9SuXtXFoN13fYKOM1tyUT3FbvbWmSWCZX2Er-41u5xPoml45svRahl9Wb9aasbINJixDZwcPTbyTLZSUsAvrg_cPuCR7s782_WU8343Y72Qtlb8OYatwuOQvuN13M_hJKNfxann1v1U_B1KZ_D_mzhzhz24fw85CSz2irtN9w9HshBK7AQAAA==)... + +```svelte +let items = $state([...]); + +let index = $state(0); +let selected = $derived(items[index]); +``` + +...you can change (or `bind:` to) properties of `selected` and it will affect the underlying `items` array. If `items` was _not_ deeply reactive, mutating `selected` would have no effect. + ## Update propagation Svelte uses something called _push-pull reactivity_ — when state is updated, everything that depends on the state (whether directly or indirectly) is immediately notified of the change (the 'push'), but derived values are not re-evaluated until they are actually read (the 'pull'). diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index 75f59102f9..ae1a2146c9 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -254,6 +254,8 @@ In general, `$effect` is best considered something of an escape hatch — useful > [!NOTE] For things that are more complicated than a simple expression like `count * 2`, you can also use `$derived.by`. +If you're using an effect because you want to be able to reassign the derived value (to build an optimistic UI, for example) note that [deriveds can be directly overridden]($derived#Overriding-derived-values) as of Svelte 5.25. + You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/playground/untitled#H4sIAAAAAAAACpVRy26DMBD8FcvKgUhtoIdeHBwp31F6MGSJkBbHwksEQvx77aWQqooq9bgzOzP7mGTdIHipPiZJowOpGJAv0po2VmfnDv4OSBErjYdneHWzBJaCjcx91TWOToUtCIEE3cig0OIty44r5l1oDtjOkyFIsv3GINQ_CNYyGegd1DVUlCR7oU9iilDUcP8S8roYs9n8p2wdYNVFm4csTx872BxNCcjr5I11fdgonEkXsjP2CoUUZWMv6m6wBz2x7yxaM-iJvWeRsvSbSVeUy5i0uf8vKA78NIeJLSZWv1I8jQjLdyK4XuTSeIdmVKJGGI4LdjVOiezwDu1yG74My8PLCQaSiroe5s_5C2PHrkVGAgAA)): ```svelte diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js index 04f4347a40..d6c74eddb6 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js @@ -21,10 +21,6 @@ export function validate_assignment(node, argument, state) { const binding = state.scope.get(argument.name); if (state.analysis.runes) { - if (binding?.kind === 'derived') { - e.constant_assignment(node, 'derived state'); - } - if (binding?.node === state.analysis.props_id) { e.constant_assignment(node, '$props.id()'); } @@ -38,25 +34,6 @@ export function validate_assignment(node, argument, state) { e.snippet_parameter_assignment(node); } } - if ( - argument.type === 'MemberExpression' && - argument.object.type === 'ThisExpression' && - (((argument.property.type === 'PrivateIdentifier' || argument.property.type === 'Identifier') && - state.derived_state.some( - (derived) => - derived.name === /** @type {PrivateIdentifier | Identifier} */ (argument.property).name && - derived.private === (argument.property.type === 'PrivateIdentifier') - )) || - (argument.property.type === 'Literal' && - argument.property.value && - typeof argument.property.value === 'string' && - state.derived_state.some( - (derived) => - derived.name === /** @type {Literal} */ (argument.property).value && !derived.private - ))) - ) { - e.constant_assignment(node, 'derived state'); - } } /** @@ -81,7 +58,6 @@ export function validate_no_const_assignment(node, argument, scope, is_binding) } else if (argument.type === 'Identifier') { const binding = scope.get(argument.name); if ( - binding?.kind === 'derived' || binding?.declaration_kind === 'import' || (binding?.declaration_kind === 'const' && binding.kind !== 'each') ) { @@ -96,12 +72,7 @@ export function validate_no_const_assignment(node, argument, scope, is_binding) // ); // TODO have a more specific error message for assignments to things like `{:then foo}` - const thing = - binding.declaration_kind === 'import' - ? 'import' - : binding.kind === 'derived' - ? 'derived state' - : 'constant'; + const thing = binding.declaration_kind === 'import' ? 'import' : 'constant'; if (is_binding) { e.constant_binding(node, thing); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/_config.js deleted file mode 100644 index 94985a9939..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/_config.js +++ /dev/null @@ -1,8 +0,0 @@ -import { test } from '../../test'; - -export default test({ - error: { - code: 'constant_assignment', - message: 'Cannot assign to derived state' - } -}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/main.svelte deleted file mode 100644 index 3bf836f6c5..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-assignment/main.svelte +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/_config.js deleted file mode 100644 index 87b88d79cc..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/_config.js +++ /dev/null @@ -1,8 +0,0 @@ -import { test } from '../../test'; - -export default test({ - error: { - code: 'constant_binding', - message: 'Cannot bind to derived state' - } -}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/main.svelte deleted file mode 100644 index 6c198dc068..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-binding/main.svelte +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/_config.js deleted file mode 100644 index 94985a9939..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/_config.js +++ /dev/null @@ -1,8 +0,0 @@ -import { test } from '../../test'; - -export default test({ - error: { - code: 'constant_assignment', - message: 'Cannot assign to derived state' - } -}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/main.svelte deleted file mode 100644 index d44806757e..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-assignment/main.svelte +++ /dev/null @@ -1,10 +0,0 @@ - diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/_config.js deleted file mode 100644 index 94985a9939..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/_config.js +++ /dev/null @@ -1,8 +0,0 @@ -import { test } from '../../test'; - -export default test({ - error: { - code: 'constant_assignment', - message: 'Cannot assign to derived state' - } -}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/main.svelte deleted file mode 100644 index e4ee2e8635..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-state-field-update/main.svelte +++ /dev/null @@ -1,10 +0,0 @@ - diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/_config.js deleted file mode 100644 index 94985a9939..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/_config.js +++ /dev/null @@ -1,8 +0,0 @@ -import { test } from '../../test'; - -export default test({ - error: { - code: 'constant_assignment', - message: 'Cannot assign to derived state' - } -}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/main.svelte deleted file mode 100644 index d266c95bb8..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/runes-no-derived-update/main.svelte +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/packages/svelte/tests/runtime-runes/samples/writable-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/writable-derived/_config.js new file mode 100644 index 0000000000..b48ccbdfd0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/writable-derived/_config.js @@ -0,0 +1,46 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` +

0 * 2 = 0

+ `, + + ssrHtml: ` +

0 * 2 = 0

+ `, + + test({ assert, target, window }) { + const [input1, input2] = target.querySelectorAll('input'); + + flushSync(() => { + input1.value = '10'; + input1.dispatchEvent(new window.Event('input', { bubbles: true })); + }); + + assert.htmlEqual( + target.innerHTML, + `

10 * 2 = 20

` + ); + + flushSync(() => { + input2.value = '99'; + input2.dispatchEvent(new window.Event('input', { bubbles: true })); + }); + + assert.htmlEqual( + target.innerHTML, + `

10 * 2 = 99

` + ); + + flushSync(() => { + input1.value = '20'; + input1.dispatchEvent(new window.Event('input', { bubbles: true })); + }); + + assert.htmlEqual( + target.innerHTML, + `

20 * 2 = 40

` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/writable-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/writable-derived/main.svelte new file mode 100644 index 0000000000..ab1dde0b9b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/writable-derived/main.svelte @@ -0,0 +1,9 @@ + + + + + +

{count} * 2 = {double}

diff --git a/packages/svelte/tests/validator/samples/reassign-derived-literal/errors.json b/packages/svelte/tests/validator/samples/reassign-derived-literal/errors.json deleted file mode 100644 index 8681d84ab2..0000000000 --- a/packages/svelte/tests/validator/samples/reassign-derived-literal/errors.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "code": "constant_assignment", - "message": "Cannot assign to derived state", - "start": { - "column": 3, - "line": 6 - }, - "end": { - "column": 29, - "line": 6 - } - } -] diff --git a/packages/svelte/tests/validator/samples/reassign-derived-literal/input.svelte b/packages/svelte/tests/validator/samples/reassign-derived-literal/input.svelte deleted file mode 100644 index 8f109c9e1f..0000000000 --- a/packages/svelte/tests/validator/samples/reassign-derived-literal/input.svelte +++ /dev/null @@ -1,9 +0,0 @@ - \ No newline at end of file diff --git a/packages/svelte/tests/validator/samples/reassign-derived-private-field/errors.json b/packages/svelte/tests/validator/samples/reassign-derived-private-field/errors.json deleted file mode 100644 index c211aa4608..0000000000 --- a/packages/svelte/tests/validator/samples/reassign-derived-private-field/errors.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "code": "constant_assignment", - "message": "Cannot assign to derived state", - "start": { - "column": 3, - "line": 6 - }, - "end": { - "column": 27, - "line": 6 - } - } -] diff --git a/packages/svelte/tests/validator/samples/reassign-derived-private-field/input.svelte b/packages/svelte/tests/validator/samples/reassign-derived-private-field/input.svelte deleted file mode 100644 index 62e2317e03..0000000000 --- a/packages/svelte/tests/validator/samples/reassign-derived-private-field/input.svelte +++ /dev/null @@ -1,9 +0,0 @@ - \ No newline at end of file diff --git a/packages/svelte/tests/validator/samples/reassign-derived-public-field/errors.json b/packages/svelte/tests/validator/samples/reassign-derived-public-field/errors.json deleted file mode 100644 index 98837589ac..0000000000 --- a/packages/svelte/tests/validator/samples/reassign-derived-public-field/errors.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "code": "constant_assignment", - "message": "Cannot assign to derived state", - "start": { - "column": 3, - "line": 6 - }, - "end": { - "column": 26, - "line": 6 - } - } -] diff --git a/packages/svelte/tests/validator/samples/reassign-derived-public-field/input.svelte b/packages/svelte/tests/validator/samples/reassign-derived-public-field/input.svelte deleted file mode 100644 index e2c4693e86..0000000000 --- a/packages/svelte/tests/validator/samples/reassign-derived-public-field/input.svelte +++ /dev/null @@ -1,9 +0,0 @@ - \ No newline at end of file From 6e343b9ad7bca473947cbee0c7ea9455d9485599 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 13:01:58 -0400 Subject: [PATCH 40/75] Version Packages (#15578) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/clever-terms-tell.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/clever-terms-tell.md diff --git a/.changeset/clever-terms-tell.md b/.changeset/clever-terms-tell.md deleted file mode 100644 index 606868bce3..0000000000 --- a/.changeset/clever-terms-tell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: make deriveds writable diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 04ddfcadbd..4bac129169 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.25.0 + +### Minor Changes + +- feat: make deriveds writable ([#15570](https://github.com/sveltejs/svelte/pull/15570)) + ## 5.24.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index f321571e7a..e3824b89fb 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.24.1", + "version": "5.25.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 565c190713..a62190bb2e 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.24.1'; +export const VERSION = '5.25.0'; export const PUBLIC_VERSION = '5'; From 441108b8ff28a6c1aa8e38f5c041a2583446167e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Mar 2025 13:06:46 -0400 Subject: [PATCH 41/75] fix docs --- documentation/docs/98-reference/.generated/client-errors.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 901c49822c..fd9419176d 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -151,6 +151,8 @@ This error occurs when state is updated while evaluating a `$derived`. You might This is forbidden because it introduces instability: if `

{count} is even: {even}

` is updated before `odd` is recalculated, `even` will be stale. In most cases the solution is to make everything derived: ```js +let count = 0; +// ---cut--- let even = $derived(count % 2 === 0); let odd = $derived(!even); ``` From ef98ccae8b27dbac393623c166ea890b515d5e1d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Mar 2025 15:44:23 -0400 Subject: [PATCH 42/75] doh --- documentation/docs/98-reference/.generated/client-errors.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index fd9419176d..901c49822c 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -151,8 +151,6 @@ This error occurs when state is updated while evaluating a `$derived`. You might This is forbidden because it introduces instability: if `

{count} is even: {even}

` is updated before `odd` is recalculated, `even` will be stale. In most cases the solution is to make everything derived: ```js -let count = 0; -// ---cut--- let even = $derived(count % 2 === 0); let odd = $derived(!even); ``` From d1bd32ec9ec06e6505740a0de5f5b2281546787c Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Fri, 21 Mar 2025 14:46:20 -0500 Subject: [PATCH 43/75] fix: allow get_proxied_value to return original value when error (#15577) * fix: allow get_proxied_value to return original value when error closes #15546 * Update packages/svelte/src/internal/client/proxy.js --------- Co-authored-by: Rich Harris --- .changeset/afraid-penguins-battle.md | 5 +++++ packages/svelte/src/internal/client/proxy.js | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 .changeset/afraid-penguins-battle.md diff --git a/.changeset/afraid-penguins-battle.md b/.changeset/afraid-penguins-battle.md new file mode 100644 index 0000000000..2cc5059b9a --- /dev/null +++ b/.changeset/afraid-penguins-battle.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent dev server from throwing errors when attempting to retrieve the proxied value of an iframe's contentWindow diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index ffe63f4b77..fab271c916 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -366,8 +366,18 @@ function update_version(signal, d = 1) { * @param {any} value */ export function get_proxied_value(value) { - if (value !== null && typeof value === 'object' && STATE_SYMBOL in value) { - return value[STATE_SYMBOL]; + try { + if (value !== null && typeof value === 'object' && STATE_SYMBOL in value) { + return value[STATE_SYMBOL]; + } + } catch { + // the above if check can throw an error if the value in question + // is the contentWindow of an iframe on another domain, in which + // case we want to just return the value (because it's definitely + // not a proxied value) so we don't break any JavaScript interacting + // with that iframe (such as various payment companies client side + // JavaScript libraries interacting with their iframes on the same + // domain) } return value; From c1ae8953aaa81b9191d8d944c4bf0df7fdf4f2ee Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 21 Mar 2025 17:04:59 -0400 Subject: [PATCH 44/75] Version Packages (#15580) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/afraid-penguins-battle.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-penguins-battle.md diff --git a/.changeset/afraid-penguins-battle.md b/.changeset/afraid-penguins-battle.md deleted file mode 100644 index 2cc5059b9a..0000000000 --- a/.changeset/afraid-penguins-battle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: prevent dev server from throwing errors when attempting to retrieve the proxied value of an iframe's contentWindow diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 4bac129169..9e99e91b8e 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.25.1 + +### Patch Changes + +- fix: prevent dev server from throwing errors when attempting to retrieve the proxied value of an iframe's contentWindow ([#15577](https://github.com/sveltejs/svelte/pull/15577)) + ## 5.25.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index e3824b89fb..9d3902696d 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.25.0", + "version": "5.25.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index a62190bb2e..a4f5a15c8f 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.25.0'; +export const VERSION = '5.25.1'; export const PUBLIC_VERSION = '5'; From 33d118f8a29a376e4490f2d31b0b444bf8fa0c7c Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Fri, 21 Mar 2025 23:47:26 +0100 Subject: [PATCH 45/75] feat: migrate reassigned deriveds to `$derived` (#15581) --- .changeset/forty-snakes-lay.md | 5 +++++ packages/svelte/src/compiler/migrate/index.js | 17 ++++++++++++++++- .../samples/reassigned-deriveds/input.svelte | 10 ++++++++++ .../samples/reassigned-deriveds/output.svelte | 10 ++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 .changeset/forty-snakes-lay.md create mode 100644 packages/svelte/tests/migrate/samples/reassigned-deriveds/input.svelte create mode 100644 packages/svelte/tests/migrate/samples/reassigned-deriveds/output.svelte diff --git a/.changeset/forty-snakes-lay.md b/.changeset/forty-snakes-lay.md new file mode 100644 index 0000000000..6cb4c2d761 --- /dev/null +++ b/.changeset/forty-snakes-lay.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: migrate reassigned deriveds to `$derived` diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 7f26d0d010..b336ebb2b8 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -19,6 +19,7 @@ import { migrate_svelte_ignore } from '../utils/extract_svelte_ignore.js'; import { validate_component_options } from '../validate-options.js'; import { is_reserved, is_svg, is_void } from '../../utils.js'; import { regex_is_valid_identifier } from '../phases/patterns.js'; +import { VERSION } from 'svelte/compiler'; const regex_style_tags = /(]+>)([\S\s]*?)(<\/style>)/g; const style_placeholder = '/*$$__STYLE_CONTENT__$$*/'; @@ -113,6 +114,16 @@ function find_closing_parenthesis(start, code) { return end; } +function check_support_writable_deriveds() { + const [major, minor, patch] = VERSION.split('.'); + + if (+major < 5) return false; + if (+minor < 25) return false; + return true; +} + +const support_writable_derived = check_support_writable_deriveds(); + /** * Does a best-effort migration of Svelte code towards using runes, event attributes and render tags. * May throw an error if the code is too complex to migrate automatically. @@ -952,7 +963,11 @@ const instance_script = { const reassigned_bindings = bindings.filter((b) => b?.reassigned); if ( - reassigned_bindings.length === 0 && + // on version 5.25.0 deriveds are writable so we can use them even if + // reassigned (but if the right side is a literal we want to use `$state`) + (support_writable_derived + ? node.body.expression.right.type !== 'Literal' + : reassigned_bindings.length === 0) && !bindings.some((b) => b?.kind === 'store_sub') && node.body.expression.left.type !== 'MemberExpression' ) { diff --git a/packages/svelte/tests/migrate/samples/reassigned-deriveds/input.svelte b/packages/svelte/tests/migrate/samples/reassigned-deriveds/input.svelte new file mode 100644 index 0000000000..024f719fb9 --- /dev/null +++ b/packages/svelte/tests/migrate/samples/reassigned-deriveds/input.svelte @@ -0,0 +1,10 @@ + + + + + +{upper} \ No newline at end of file diff --git a/packages/svelte/tests/migrate/samples/reassigned-deriveds/output.svelte b/packages/svelte/tests/migrate/samples/reassigned-deriveds/output.svelte new file mode 100644 index 0000000000..0903299d95 --- /dev/null +++ b/packages/svelte/tests/migrate/samples/reassigned-deriveds/output.svelte @@ -0,0 +1,10 @@ + + + + + +{upper} \ No newline at end of file From 78d238c5a34396afcc9edf02bb767d0936440ad9 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Sat, 22 Mar 2025 00:03:38 +0100 Subject: [PATCH 46/75] chore: revert version check in migrate (#15583) --- packages/svelte/src/compiler/migrate/index.js | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index b336ebb2b8..9d79d88b23 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -19,7 +19,6 @@ import { migrate_svelte_ignore } from '../utils/extract_svelte_ignore.js'; import { validate_component_options } from '../validate-options.js'; import { is_reserved, is_svg, is_void } from '../../utils.js'; import { regex_is_valid_identifier } from '../phases/patterns.js'; -import { VERSION } from 'svelte/compiler'; const regex_style_tags = /(]+>)([\S\s]*?)(<\/style>)/g; const style_placeholder = '/*$$__STYLE_CONTENT__$$*/'; @@ -114,16 +113,6 @@ function find_closing_parenthesis(start, code) { return end; } -function check_support_writable_deriveds() { - const [major, minor, patch] = VERSION.split('.'); - - if (+major < 5) return false; - if (+minor < 25) return false; - return true; -} - -const support_writable_derived = check_support_writable_deriveds(); - /** * Does a best-effort migration of Svelte code towards using runes, event attributes and render tags. * May throw an error if the code is too complex to migrate automatically. @@ -963,11 +952,7 @@ const instance_script = { const reassigned_bindings = bindings.filter((b) => b?.reassigned); if ( - // on version 5.25.0 deriveds are writable so we can use them even if - // reassigned (but if the right side is a literal we want to use `$state`) - (support_writable_derived - ? node.body.expression.right.type !== 'Literal' - : reassigned_bindings.length === 0) && + node.body.expression.right.type !== 'Literal' && !bindings.some((b) => b?.kind === 'store_sub') && node.body.expression.left.type !== 'MemberExpression' ) { From 7fe9bf524bbf060a0acf8b91c8ca322b105423b4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 22 Mar 2025 00:04:01 +0100 Subject: [PATCH 47/75] Version Packages (#15582) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/forty-snakes-lay.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/forty-snakes-lay.md diff --git a/.changeset/forty-snakes-lay.md b/.changeset/forty-snakes-lay.md deleted file mode 100644 index 6cb4c2d761..0000000000 --- a/.changeset/forty-snakes-lay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -feat: migrate reassigned deriveds to `$derived` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 9e99e91b8e..86e1179029 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.25.2 + +### Patch Changes + +- feat: migrate reassigned deriveds to `$derived` ([#15581](https://github.com/sveltejs/svelte/pull/15581)) + ## 5.25.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 9d3902696d..56fa949391 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.25.1", + "version": "5.25.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index a4f5a15c8f..e2dc6c2c09 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.25.1'; +export const VERSION = '5.25.2'; export const PUBLIC_VERSION = '5'; From 3080c1334e8efc65756488c0cc36b6dec5fca00f Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Sat, 22 Mar 2025 01:56:31 +0100 Subject: [PATCH 48/75] fix: prevent state runes from being called with spread (#15585) * fix: prevent state runes from being called with spread * prevent spread arguments for all runes except $inspect --------- Co-authored-by: Rich Harris --- .changeset/nice-pianos-punch.md | 5 +++++ .../docs/98-reference/.generated/compile-errors.md | 6 ++++++ packages/svelte/messages/compile-errors/script.md | 4 ++++ packages/svelte/src/compiler/errors.js | 10 ++++++++++ .../phases/2-analyze/visitors/CallExpression.js | 8 ++++++++ .../rune-invalid-spread-derived-by/errors.json | 14 ++++++++++++++ .../rune-invalid-spread-derived-by/input.svelte | 4 ++++ .../rune-invalid-spread-derived/errors.json | 14 ++++++++++++++ .../rune-invalid-spread-derived/input.svelte | 4 ++++ .../rune-invalid-spread-state-raw/errors.json | 14 ++++++++++++++ .../rune-invalid-spread-state-raw/input.svelte | 4 ++++ .../samples/rune-invalid-spread-state/errors.json | 14 ++++++++++++++ .../samples/rune-invalid-spread-state/input.svelte | 4 ++++ 13 files changed, 105 insertions(+) create mode 100644 .changeset/nice-pianos-punch.md create mode 100644 packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/errors.json create mode 100644 packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/input.svelte create mode 100644 packages/svelte/tests/validator/samples/rune-invalid-spread-derived/errors.json create mode 100644 packages/svelte/tests/validator/samples/rune-invalid-spread-derived/input.svelte create mode 100644 packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/errors.json create mode 100644 packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/input.svelte create mode 100644 packages/svelte/tests/validator/samples/rune-invalid-spread-state/errors.json create mode 100644 packages/svelte/tests/validator/samples/rune-invalid-spread-state/input.svelte diff --git a/.changeset/nice-pianos-punch.md b/.changeset/nice-pianos-punch.md new file mode 100644 index 0000000000..f70af9eb04 --- /dev/null +++ b/.changeset/nice-pianos-punch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent state runes from being called with spread diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index ea116014e7..a8c39aaf97 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -660,6 +660,12 @@ Cannot access a computed property of a rune `%name%` is not a valid rune ``` +### rune_invalid_spread + +``` +`%rune%` cannot be called with a spread argument +``` + ### rune_invalid_usage ``` diff --git a/packages/svelte/messages/compile-errors/script.md b/packages/svelte/messages/compile-errors/script.md index 795c0b007d..aabcbeae48 100644 --- a/packages/svelte/messages/compile-errors/script.md +++ b/packages/svelte/messages/compile-errors/script.md @@ -162,6 +162,10 @@ This turned out to be buggy and unpredictable, particularly when working with de > `%name%` is not a valid rune +## rune_invalid_spread + +> `%rune%` cannot be called with a spread argument + ## rune_invalid_usage > Cannot use `%rune%` rune in non-runes mode diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 677b99fcff..6bf973948b 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -383,6 +383,16 @@ export function rune_invalid_name(node, name) { e(node, 'rune_invalid_name', `\`${name}\` is not a valid rune\nhttps://svelte.dev/e/rune_invalid_name`); } +/** + * `%rune%` cannot be called with a spread argument + * @param {null | number | NodeLike} node + * @param {string} rune + * @returns {never} + */ +export function rune_invalid_spread(node, rune) { + e(node, 'rune_invalid_spread', `\`${rune}\` cannot be called with a spread argument\nhttps://svelte.dev/e/rune_invalid_spread`); +} + /** * Cannot use `%rune%` rune in non-runes mode * @param {null | number | NodeLike} node diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 6ef323725b..2eac934b33 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -17,6 +17,14 @@ export function CallExpression(node, context) { const rune = get_rune(node, context.state.scope); + if (rune && rune !== '$inspect') { + for (const arg of node.arguments) { + if (arg.type === 'SpreadElement') { + e.rune_invalid_spread(node, rune); + } + } + } + switch (rune) { case null: if (!is_safe_identifier(node.callee, context.state.scope)) { diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/errors.json b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/errors.json new file mode 100644 index 0000000000..be59da95fa --- /dev/null +++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "rune_invalid_spread", + "end": { + "column": 35, + "line": 3 + }, + "message": "`$derived.by` cannot be called with a spread argument", + "start": { + "column": 15, + "line": 3 + } + } +] diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/input.svelte b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/input.svelte new file mode 100644 index 0000000000..49e8057aa5 --- /dev/null +++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived-by/input.svelte @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-derived/errors.json b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived/errors.json new file mode 100644 index 0000000000..6a333bc362 --- /dev/null +++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "rune_invalid_spread", + "end": { + "column": 32, + "line": 3 + }, + "message": "`$derived` cannot be called with a spread argument", + "start": { + "column": 15, + "line": 3 + } + } +] diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-derived/input.svelte b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived/input.svelte new file mode 100644 index 0000000000..9155493e17 --- /dev/null +++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-derived/input.svelte @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/errors.json b/packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/errors.json new file mode 100644 index 0000000000..e08b498fcb --- /dev/null +++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "rune_invalid_spread", + "end": { + "column": 34, + "line": 3 + }, + "message": "`$state.raw` cannot be called with a spread argument", + "start": { + "column": 15, + "line": 3 + } + } +] diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/input.svelte b/packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/input.svelte new file mode 100644 index 0000000000..d06fb053b3 --- /dev/null +++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-state-raw/input.svelte @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-state/errors.json b/packages/svelte/tests/validator/samples/rune-invalid-spread-state/errors.json new file mode 100644 index 0000000000..11ae2abce5 --- /dev/null +++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-state/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "rune_invalid_spread", + "end": { + "column": 30, + "line": 3 + }, + "message": "`$state` cannot be called with a spread argument", + "start": { + "column": 15, + "line": 3 + } + } +] diff --git a/packages/svelte/tests/validator/samples/rune-invalid-spread-state/input.svelte b/packages/svelte/tests/validator/samples/rune-invalid-spread-state/input.svelte new file mode 100644 index 0000000000..02feac893f --- /dev/null +++ b/packages/svelte/tests/validator/samples/rune-invalid-spread-state/input.svelte @@ -0,0 +1,4 @@ + \ No newline at end of file From f498a21063894e6e515e62d753396410624b2e0f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 23 Mar 2025 09:59:21 -0400 Subject: [PATCH 49/75] Version Packages (#15587) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/nice-pianos-punch.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/nice-pianos-punch.md diff --git a/.changeset/nice-pianos-punch.md b/.changeset/nice-pianos-punch.md deleted file mode 100644 index f70af9eb04..0000000000 --- a/.changeset/nice-pianos-punch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: prevent state runes from being called with spread diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 86e1179029..e6b5de7e5f 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.25.3 + +### Patch Changes + +- fix: prevent state runes from being called with spread ([#15585](https://github.com/sveltejs/svelte/pull/15585)) + ## 5.25.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 56fa949391..0410520634 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.25.2", + "version": "5.25.3", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index e2dc6c2c09..e4ae33a9b1 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.25.2'; +export const VERSION = '5.25.3'; export const PUBLIC_VERSION = '5'; From f49856449dbabb0815066485b23bf404c9c789ff Mon Sep 17 00:00:00 2001 From: Scott Wu Date: Fri, 28 Mar 2025 23:53:10 +0800 Subject: [PATCH 50/75] docs: add a reference to the official hash router (#15611) --- documentation/docs/07-misc/99-faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/07-misc/99-faq.md b/documentation/docs/07-misc/99-faq.md index 7e25cdab55..ed5c6277c0 100644 --- a/documentation/docs/07-misc/99-faq.md +++ b/documentation/docs/07-misc/99-faq.md @@ -96,7 +96,7 @@ However, you can use any router library. A lot of people use [page.js](https://g If you prefer a declarative HTML approach, there's the isomorphic [svelte-routing](https://github.com/EmilTholin/svelte-routing) library and a fork of it called [svelte-navigator](https://github.com/mefechoel/svelte-navigator) containing some additional functionality. -If you need hash-based routing on the client side, check out [svelte-spa-router](https://github.com/ItalyPaleAle/svelte-spa-router) or [abstract-state-router](https://github.com/TehShrike/abstract-state-router/). +If you need hash-based routing on the client side, check out the [hash option](https://svelte.dev/docs/kit/configuration#router) in SvelteKit, [svelte-spa-router](https://github.com/ItalyPaleAle/svelte-spa-router), or [abstract-state-router](https://github.com/TehShrike/abstract-state-router/). [Routify](https://routify.dev) is another filesystem-based router, similar to SvelteKit's router. Version 3 supports Svelte's native SSR. From 04257925d22d8ecef37f50330ac258c7f97dca0c Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sat, 29 Mar 2025 01:42:18 -0700 Subject: [PATCH 51/75] docs: clarify what you can build with SvelteKit (#15461) * docs: clarify what you can build with SvelteKit * try relative URLs * Update documentation/docs/01-introduction/02-getting-started.md Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- documentation/docs/01-introduction/02-getting-started.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/documentation/docs/01-introduction/02-getting-started.md b/documentation/docs/01-introduction/02-getting-started.md index e035e6d6df..c7351729ff 100644 --- a/documentation/docs/01-introduction/02-getting-started.md +++ b/documentation/docs/01-introduction/02-getting-started.md @@ -2,7 +2,7 @@ title: Getting started --- -We recommend using [SvelteKit](../kit), the official application framework from the Svelte team powered by [Vite](https://vite.dev/): +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 npx sv create myapp @@ -15,7 +15,9 @@ Don't worry if you don't know Svelte yet! You can ignore all the nice features S ## Alternatives to SvelteKit -You can also use Svelte directly with Vite by running `npm create vite@latest` and selecting the `svelte` option. With this, `npm run build` will generate HTML, JS and CSS files inside the `dist` directory using [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte). In most cases, you will probably need to [choose a routing library](faq#Is-there-a-router) as well. +You can also use Svelte directly with Vite by running `npm create vite@latest` and selecting the `svelte` option. With this, `npm run build` will generate HTML, JS, and CSS files inside the `dist` directory using [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte). In most cases, you will probably need to [choose a routing library](faq#Is-there-a-router) as well. + +>[!NOTE] Vite is often used in standalone mode to build [single page apps (SPAs)](../kit/glossary#SPA), which you can also [build with SvelteKit](../kit/single-page-apps). There are also plugins for [Rollup](https://github.com/sveltejs/rollup-plugin-svelte), [Webpack](https://github.com/sveltejs/svelte-loader) [and a few others](https://sveltesociety.dev/packages?category=build-plugins), but we recommend Vite. From 6f8068637c6f18649d17687c588b06381318d578 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 31 Mar 2025 16:48:42 +0200 Subject: [PATCH 52/75] fix: support TS type assertions (#15642) fixes #15565 --- .changeset/lovely-windows-hang.md | 5 +++++ .../src/compiler/phases/1-parse/remove_typescript_nodes.js | 3 +++ .../tests/runtime-runes/samples/typescript/main.svelte | 1 + 3 files changed, 9 insertions(+) create mode 100644 .changeset/lovely-windows-hang.md diff --git a/.changeset/lovely-windows-hang.md b/.changeset/lovely-windows-hang.md new file mode 100644 index 0000000000..406e6c4961 --- /dev/null +++ b/.changeset/lovely-windows-hang.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: support TS type assertions diff --git a/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js b/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js index 09eb0bfa68..37dc0e17a1 100644 --- a/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js +++ b/packages/svelte/src/compiler/phases/1-parse/remove_typescript_nodes.js @@ -94,6 +94,9 @@ const visitors = { TSTypeAliasDeclaration() { return b.empty; }, + TSTypeAssertion(node, context) { + return context.visit(node.expression); + }, TSEnumDeclaration(node) { e.typescript_invalid_feature(node, 'enums'); }, diff --git a/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte b/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte index e2942b21f3..d2a9da5439 100644 --- a/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte @@ -45,6 +45,7 @@ export type { Hello }; const TypedFoo = Foo; + const typeAssertion = true; `, + html: ``, compileOptions: { dev: true @@ -34,8 +34,8 @@ export default test({ btn?.click(); }); - assert.htmlEqual(target.innerHTML, ``); + assert.htmlEqual(target.innerHTML, ``); - assert.deepEqual(warnings, []); + assert.deepEqual(warnings, [], 'expected getContext to have widened ownership'); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/main.svelte new file mode 100644 index 0000000000..2dd7cab141 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/main.svelte @@ -0,0 +1,9 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/sub.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/sub.svelte similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/sub.svelte rename to packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-1/sub.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/_config.js index c07b9ce129..66f1726a2a 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/_config.js @@ -1,41 +1,24 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; -/** @type {typeof console.warn} */ -let warn; - -/** @type {any[]} */ -let warnings = []; - export default test({ - html: ``, - compileOptions: { dev: true }, - before_test: () => { - warn = console.warn; - - console.warn = (...args) => { - warnings.push(...args); - }; - }, + test({ assert, target, warnings }) { + const [btn1, btn2] = target.querySelectorAll('button'); - after_test: () => { - console.warn = warn; - warnings = []; - }, + flushSync(() => { + btn1.click(); + }); - test({ assert, target }) { - const btn = target.querySelector('button'); + assert.deepEqual(warnings.length, 0); flushSync(() => { - btn?.click(); + btn2.click(); }); - assert.htmlEqual(target.innerHTML, ``); - - assert.deepEqual(warnings, []); + assert.deepEqual(warnings.length, 1); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/main.svelte index ad450a937e..0be7e434e4 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/main.svelte @@ -1,9 +1,8 @@ - - + diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/state.svelte.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/state.svelte.js index 3e7a68cf97..2906b9bce5 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/state.svelte.js +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/state.svelte.js @@ -1 +1,14 @@ -export let global = $state({}); +export function create_my_state() { + const my_state = $state({ + a: 0 + }); + + function inc() { + my_state.a++; + } + + return { + my_state, + inc + }; +} diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/sub.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/sub.svelte similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/sub.svelte rename to packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-2/sub.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/Child.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/Child.svelte similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/Child.svelte rename to packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/Child.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/_config.js index 96b18d1854..ab7327ab8b 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/_config.js @@ -1,41 +1,24 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; -/** @type {typeof console.warn} */ -let warn; - -/** @type {any[]} */ -let warnings = []; - export default test({ - html: ``, - compileOptions: { dev: true }, - before_test: () => { - warn = console.warn; - - console.warn = (...args) => { - warnings.push(...args); - }; - }, + async test({ assert, target, warnings }) { + const [btn1, btn2] = target.querySelectorAll('button'); - after_test: () => { - console.warn = warn; - warnings = []; - }, + flushSync(() => { + btn1.click(); + }); - test({ assert, target }) { - const btn = target.querySelector('button'); + assert.deepEqual(warnings.length, 0); flushSync(() => { - btn?.click(); + btn2.click(); }); - assert.htmlEqual(target.innerHTML, ``); - - assert.deepEqual(warnings, [], 'expected getContext to have widened ownership'); + assert.deepEqual(warnings.length, 1); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/main.svelte index 2dd7cab141..8e8343790b 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-3/main.svelte @@ -1,9 +1,13 @@ - + + diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/_config.js deleted file mode 100644 index aeb3740dfe..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/_config.js +++ /dev/null @@ -1,37 +0,0 @@ -import { flushSync } from 'svelte'; -import { test } from '../../test'; - -/** @type {typeof console.warn} */ -let warn; - -/** @type {any[]} */ -let warnings = []; - -export default test({ - compileOptions: { - dev: true - }, - - before_test: () => { - warn = console.warn; - - console.warn = (...args) => { - warnings.push(...args); - }; - }, - - after_test: () => { - console.warn = warn; - warnings = []; - }, - - test({ assert, target }) { - const btn = target.querySelector('button'); - - flushSync(() => { - btn?.click(); - }); - - assert.deepEqual(warnings, []); - } -}); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/main.svelte deleted file mode 100644 index 2d40c13949..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/main.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/state.svelte.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/state.svelte.js deleted file mode 100644 index 4079059171..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/state.svelte.js +++ /dev/null @@ -1,13 +0,0 @@ -class Global { - state = $state({}); - - add_a(a) { - this.state.a = a; - } - - increment_a_b() { - this.state.a.b++; - } -} - -export const global = new Global(); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/sub.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/sub.svelte deleted file mode 100644 index 044904aa18..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-4/sub.svelte +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/_config.js deleted file mode 100644 index 66f1726a2a..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/_config.js +++ /dev/null @@ -1,24 +0,0 @@ -import { flushSync } from 'svelte'; -import { test } from '../../test'; - -export default test({ - compileOptions: { - dev: true - }, - - test({ assert, target, warnings }) { - const [btn1, btn2] = target.querySelectorAll('button'); - - flushSync(() => { - btn1.click(); - }); - - assert.deepEqual(warnings.length, 0); - - flushSync(() => { - btn2.click(); - }); - - assert.deepEqual(warnings.length, 1); - } -}); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/main.svelte deleted file mode 100644 index 0be7e434e4..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/main.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - - diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/state.svelte.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/state.svelte.js deleted file mode 100644 index 2906b9bce5..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-5/state.svelte.js +++ /dev/null @@ -1,14 +0,0 @@ -export function create_my_state() { - const my_state = $state({ - a: 0 - }); - - function inc() { - my_state.a++; - } - - return { - my_state, - inc - }; -} diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/Child.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/Child.svelte deleted file mode 100644 index aa31fd7606..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/Child.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/_config.js deleted file mode 100644 index cc9ea715f0..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/_config.js +++ /dev/null @@ -1,37 +0,0 @@ -import { flushSync } from 'svelte'; -import { test } from '../../test'; - -/** @type {typeof console.warn} */ -let warn; - -/** @type {any[]} */ -let warnings = []; - -export default test({ - compileOptions: { - dev: true - }, - - before_test: () => { - warn = console.warn; - - console.warn = (...args) => { - warnings.push(...args); - }; - }, - - after_test: () => { - console.warn = warn; - warnings = []; - }, - - async test({ assert, target }) { - const btn = target.querySelector('button'); - - flushSync(() => { - btn?.click(); - }); - - assert.deepEqual(warnings.length, 0); - } -}); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/main.svelte deleted file mode 100644 index 92d7dbd2db..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-6/main.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/_config.js deleted file mode 100644 index ab7327ab8b..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/_config.js +++ /dev/null @@ -1,24 +0,0 @@ -import { flushSync } from 'svelte'; -import { test } from '../../test'; - -export default test({ - compileOptions: { - dev: true - }, - - async test({ assert, target, warnings }) { - const [btn1, btn2] = target.querySelectorAll('button'); - - flushSync(() => { - btn1.click(); - }); - - assert.deepEqual(warnings.length, 0); - - flushSync(() => { - btn2.click(); - }); - - assert.deepEqual(warnings.length, 1); - } -}); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/main.svelte deleted file mode 100644 index 8e8343790b..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner-7/main.svelte +++ /dev/null @@ -1,13 +0,0 @@ - - - - diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/Counter.svelte deleted file mode 100644 index ffe6ef75c4..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/Counter.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/main.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/main.svelte deleted file mode 100644 index 5f1c7461f6..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/main.svelte +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/state.svelte.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/state.svelte.js deleted file mode 100644 index 6881c2faf6..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-inherited-owner/state.svelte.js +++ /dev/null @@ -1,3 +0,0 @@ -export let global = $state({ - object: { count: -1 } -}); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-2/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-2/_config.js index 87474a05cc..39fa80c55a 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-2/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-2/_config.js @@ -8,6 +8,6 @@ export default test({ }, warnings: [ - 'Intermediate.svelte passed a value to Counter.svelte with `bind:`, but the value is owned by main.svelte. Consider creating a binding between main.svelte and Intermediate.svelte' + 'Intermediate.svelte passed property `object` to Counter.svelte with `bind:`, but its parent component main.svelte did not declare `object` as a binding. Consider creating a binding between main.svelte and Intermediate.svelte (e.g. `bind:object={...}` instead of `object={...}`)' ] }); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-3/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-3/_config.js index 66e5184380..7b8cc676d5 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-3/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-3/_config.js @@ -33,7 +33,7 @@ export default test({ assert.htmlEqual(target.innerHTML, ``); assert.deepEqual(warnings, [ - 'Counter.svelte mutated a value owned by main.svelte. This is strongly discouraged. Consider passing values to child components with `bind:`, or use a callback instead' + 'Mutating unbound props (`notshared`, at Counter.svelte:10:23) is strongly discouraged. Consider using `bind:notshared={...}` in main.svelte (or using a callback) instead' ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-7/_config.js b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-7/_config.js index e766a946d0..bd2ecc28b6 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-7/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-7/_config.js @@ -1,7 +1,6 @@ import { flushSync } from 'svelte'; import { ok, test } from '../../test'; -// Tests that proxies widen ownership correctly even if not directly connected to each other export default test({ compileOptions: { dev: true diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterBinding.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterBinding.svelte deleted file mode 100644 index d6da559fb1..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding-8/CounterBinding.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - -

Binding

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

Context

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

Parent

- - - - From 7694818f9cf70cf802058c2a49571aaa69956d8a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 8 Apr 2025 17:09:22 -0400 Subject: [PATCH 73/75] Version Packages (#15705) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/clever-news-enjoy.md | 5 ----- .changeset/sweet-ants-care.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/clever-news-enjoy.md delete mode 100644 .changeset/sweet-ants-care.md diff --git a/.changeset/clever-news-enjoy.md b/.changeset/clever-news-enjoy.md deleted file mode 100644 index 2ff3dcbe56..0000000000 --- a/.changeset/clever-news-enjoy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: allow `$.state` and `$.derived` to be treeshaken diff --git a/.changeset/sweet-ants-care.md b/.changeset/sweet-ants-care.md deleted file mode 100644 index b4805626ab..0000000000 --- a/.changeset/sweet-ants-care.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: rework binding ownership validation diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 361de202b1..eb76e9a9e9 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.25.9 + +### Patch Changes + +- fix: allow `$.state` and `$.derived` to be treeshaken ([#15702](https://github.com/sveltejs/svelte/pull/15702)) + +- fix: rework binding ownership validation ([#15678](https://github.com/sveltejs/svelte/pull/15678)) + ## 5.25.8 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 8a225a798d..3fb843b3a2 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.25.8", + "version": "5.25.9", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 2c8140e365..13a69857a7 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.25.8'; +export const VERSION = '5.25.9'; export const PUBLIC_VERSION = '5'; From 708f541ad8ed2e9426cedb03e0bc1ad5d84cb787 Mon Sep 17 00:00:00 2001 From: 7nik Date: Wed, 9 Apr 2025 00:39:11 +0300 Subject: [PATCH 74/75] fix: better scope `:global()` with nesting selector `&` (#15671) Co-authored-by: 7nik --- .changeset/stupid-vans-draw.md | 5 +++++ .../phases/2-analyze/css/css-prune.js | 20 +++++++++++++++---- .../samples/global-with-nesting/expected.css | 7 ++++++- .../samples/global-with-nesting/input.svelte | 7 ++++++- 4 files changed, 33 insertions(+), 6 deletions(-) create mode 100644 .changeset/stupid-vans-draw.md diff --git a/.changeset/stupid-vans-draw.md b/.changeset/stupid-vans-draw.md new file mode 100644 index 0000000000..24892f1e8f --- /dev/null +++ b/.changeset/stupid-vans-draw.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: better scope `:global()` with nesting selector `&` diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index 0646c6341a..fbe6ca1cd3 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -1,6 +1,11 @@ /** @import * as Compiler from '#compiler' */ import { walk } from 'zimmerframe'; -import { get_parent_rules, get_possible_values, is_outer_global } from './utils.js'; +import { + get_parent_rules, + get_possible_values, + is_outer_global, + is_unscoped_pseudo_class +} from './utils.js'; import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js'; import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js'; @@ -286,20 +291,26 @@ function apply_combinator(relative_selector, rest_selectors, rule, node, directi * a global selector * @param {Compiler.AST.CSS.RelativeSelector} selector * @param {Compiler.AST.CSS.Rule} rule + * @returns {boolean} */ function is_global(selector, rule) { if (selector.metadata.is_global || selector.metadata.is_global_like) { return true; } + let explicitly_global = false; + for (const s of selector.selectors) { /** @type {Compiler.AST.CSS.SelectorList | null} */ let selector_list = null; + let can_be_global = false; let owner = rule; if (s.type === 'PseudoClassSelector') { if ((s.name === 'is' || s.name === 'where') && s.args) { selector_list = s.args; + } else { + can_be_global = is_unscoped_pseudo_class(s); } } @@ -308,18 +319,19 @@ function is_global(selector, rule) { selector_list = owner.prelude; } - const has_global_selectors = selector_list?.children.some((complex_selector) => { + const has_global_selectors = !!selector_list?.children.some((complex_selector) => { return complex_selector.children.every((relative_selector) => is_global(relative_selector, owner) ); }); + explicitly_global ||= has_global_selectors; - if (!has_global_selectors) { + if (!has_global_selectors && !can_be_global) { return false; } } - return true; + return explicitly_global || selector.selectors.length === 0; } const regex_backslash_and_following_character = /\\(.)/g; diff --git a/packages/svelte/tests/css/samples/global-with-nesting/expected.css b/packages/svelte/tests/css/samples/global-with-nesting/expected.css index dcb8a0e481..1863c57d85 100644 --- a/packages/svelte/tests/css/samples/global-with-nesting/expected.css +++ b/packages/svelte/tests/css/samples/global-with-nesting/expected.css @@ -1,5 +1,10 @@ div.svelte-xyz { &.class{ - color: red; + color: green; + } + } + * { + &:hover .class.svelte-xyz { + color: green; } } \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/global-with-nesting/input.svelte b/packages/svelte/tests/css/samples/global-with-nesting/input.svelte index 0c73ed7a78..2c1d2b5ebd 100644 --- a/packages/svelte/tests/css/samples/global-with-nesting/input.svelte +++ b/packages/svelte/tests/css/samples/global-with-nesting/input.svelte @@ -1,7 +1,12 @@ From 966ccfbe7451b7c00fc63ee43ea65bd7f286b5cf Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Tue, 8 Apr 2025 23:41:53 +0200 Subject: [PATCH 75/75] fix: set deriveds as `CLEAN` if they are assigned to (#15592) * fix: set deriveds as `CLEAN` if they are assigned to * chore: remove only * chore: remove unnecessary typecast * fix: set unowned as `MAYBE_DIRTY` instead of `CLEAN` * fix: visit the derived function when to update the dependencies even when it's reassigned * fix: use `execute_derived` instead of `update_reaction` * fix: execute deriveds eagerly when they are set if DIRTY --- .changeset/nervous-kids-shake.md | 5 +++ .../internal/client/reactivity/deriveds.js | 2 +- .../src/internal/client/reactivity/sources.js | 12 +++++-- .../svelte/src/internal/client/runtime.js | 2 +- packages/svelte/tests/signals/test.ts | 32 +++++++++++++++++++ 5 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 .changeset/nervous-kids-shake.md diff --git a/.changeset/nervous-kids-shake.md b/.changeset/nervous-kids-shake.md new file mode 100644 index 0000000000..3fc6429797 --- /dev/null +++ b/.changeset/nervous-kids-shake.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: set deriveds as `CLEAN` if they are assigned to diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 86171c2b2d..c9a8f7674a 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -131,7 +131,7 @@ function get_derived_parent_effect(derived) { * @param {Derived} derived * @returns {T} */ -function execute_derived(derived) { +export function execute_derived(derived) { var value; var prev_active_effect = active_effect; diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 2361762519..cae49c1832 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -28,14 +28,14 @@ import { UNOWNED, MAYBE_DIRTY, BLOCK_EFFECT, - ROOT_EFFECT, - EFFECT_IS_UPDATING + ROOT_EFFECT } from '../constants.js'; import * as e from '../errors.js'; import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { get_stack } from '../dev/tracing.js'; import { component_context, is_runes } from '../context.js'; import { proxy } from '../proxy.js'; +import { execute_derived } from './deriveds.js'; export let inspect_effects = new Set(); export const old_values = new Map(); @@ -172,6 +172,14 @@ export function internal_set(source, value) { } } + if ((source.f & DERIVED) !== 0) { + // if we are assigning to a dirty derived we set it to clean/maybe dirty but we also eagerly execute it to track the dependencies + if ((source.f & DIRTY) !== 0) { + execute_derived(/** @type {Derived} */ (source)); + } + set_signal_status(source, (source.f & UNOWNED) === 0 ? CLEAN : MAYBE_DIRTY); + } + mark_reactions(source, DIRTY); // It's possible that the current reaction might not have up-to-date dependencies diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index c1c91f3551..a7662be617 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -27,7 +27,7 @@ import { } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { internal_set, old_values } from './reactivity/sources.js'; -import { destroy_derived_effects, update_derived } from './reactivity/deriveds.js'; +import { destroy_derived_effects, execute_derived, update_derived } from './reactivity/deriveds.js'; import * as e from './errors.js'; import { FILENAME } from '../../constants.js'; import { tracing_mode_flag } from '../flags/index.js'; diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 3977caae36..3a427e9392 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1080,6 +1080,38 @@ describe('signals', () => { }; }); + test("deriveds set after they are DIRTY doesn't get updated on get", () => { + return () => { + const a = state(0); + const b = derived(() => $.get(a)); + + set(b, 1); + assert.equal($.get(b), 1); + + set(a, 2); + assert.equal($.get(b), 2); + set(b, 3); + + assert.equal($.get(b), 3); + }; + }); + + test("unowned deriveds set after they are DIRTY doesn't get updated on get", () => { + return () => { + const a = state(0); + const b = derived(() => $.get(a)); + const c = derived(() => $.get(b)); + + set(b, 1); + assert.equal($.get(c), 1); + + set(a, 2); + + assert.equal($.get(b), 2); + assert.equal($.get(c), 2); + }; + }); + test('deriveds containing effects work correctly when used with untrack', () => { return () => { let a = render_effect(() => {});