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/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); 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 6a2b565aea..ae1a2146c9 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 @@ -252,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/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 0beb3cb9a9..901c49822c 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -122,38 +122,37 @@ 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 ``` 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/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/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 6461df1d25..e6b5de7e5f 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,53 @@ # 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 + +- feat: migrate reassigned deriveds to `$derived` ([#15581](https://github.com/sveltejs/svelte/pull/15581)) + +## 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 + +- feat: make deriveds writable ([#15570](https://github.com/sveltejs/svelte/pull/15570)) + +## 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 + +- 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/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index ab4d1519c1..572930843e 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -80,34 +80,35 @@ 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` -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/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/package.json b/packages/svelte/package.json index d005eca0b9..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.23.2", + "version": "5.25.3", "type": "module", "types": "./types/index.d.ts", "engines": { 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/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index 1bb7a69a20..9d79d88b23 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -952,7 +952,7 @@ const instance_script = { const reassigned_bindings = bindings.filter((b) => b?.reassigned); if ( - reassigned_bindings.length === 0 && + node.body.expression.right.type !== 'Literal' && !bindings.some((b) => b?.kind === 'store_sub') && node.body.expression.left.type !== 'MemberExpression' ) { @@ -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 jsdoc_comment = /@type {.+} (?:\w+|\[.*?\]) - (.+)/.exec(comment_node.value); + if (jsdoc_comment) { + cleaned_comment += jsdoc_comment[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/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/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/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 0481621ce1..da0a89fb2f 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 @@ -220,7 +220,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/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/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/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/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 018d0529a1..767d1e3f78 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -106,7 +106,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, @@ -118,7 +118,7 @@ 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, 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..fab271c916 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, @@ -10,21 +10,23 @@ 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'; 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) { @@ -327,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; 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 92508945c9..e4834902fe 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -11,11 +11,12 @@ import { untrack, increment_write_version, update_effect, - derived_sources, - set_derived_sources, + reaction_sources, + 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 { @@ -27,7 +28,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 +53,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 = { @@ -73,9 +76,14 @@ export function source(v, stack) { /** * @template V * @param {V} v + * @param {Error | null} [stack] */ -export function state(v) { - return push_derived_source(source(v)); +export function state(v, stack) { + const s = source(v, stack); + + push_reaction_value(s); + + return s; } /** @@ -100,33 +108,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 +134,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..a5d26412a4 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,28 @@ 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; +} + +/** @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); + } + } } /** @@ -367,6 +379,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 +406,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 +419,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 +495,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,24 +886,23 @@ 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; - // 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/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 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/src/version.js b/packages/svelte/src/version.js index 191b52ecef..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.23.2'; +export const VERSION = '5.25.3'; export const PUBLIC_VERSION = '5'; 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/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 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 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 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/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}!
+ + 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/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/signals/test.ts b/packages/svelte/tests/signals/test.ts index ef4cf16d3b..3977caae36 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -487,6 +487,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 +978,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(); }; }); 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 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