diff --git a/.changeset/easy-singers-retire.md b/.changeset/easy-singers-retire.md new file mode 100644 index 0000000000..4420286e13 --- /dev/null +++ b/.changeset/easy-singers-retire.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't rebase just-created batches diff --git a/.changeset/flat-shrimps-worry.md b/.changeset/flat-shrimps-worry.md new file mode 100644 index 0000000000..a5f76a0f9d --- /dev/null +++ b/.changeset/flat-shrimps-worry.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: flush eager effects in production diff --git a/.changeset/full-waves-tease.md b/.changeset/full-waves-tease.md new file mode 100644 index 0000000000..3915334bf7 --- /dev/null +++ b/.changeset/full-waves-tease.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: account for proxified instance when updating `bind:this` diff --git a/.changeset/many-pandas-add.md b/.changeset/many-pandas-add.md new file mode 100644 index 0000000000..85de7acb35 --- /dev/null +++ b/.changeset/many-pandas-add.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure scheduled batch is flushed if not obsolete diff --git a/.changeset/small-tools-walk.md b/.changeset/small-tools-walk.md new file mode 100644 index 0000000000..2b275368b3 --- /dev/null +++ b/.changeset/small-tools-walk.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow `@debug` tags to reference awaited variables diff --git a/.changeset/smooth-poems-tap.md b/.changeset/smooth-poems-tap.md new file mode 100644 index 0000000000..ac160656cf --- /dev/null +++ b/.changeset/smooth-poems-tap.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: re-run fallback props if dependencies update diff --git a/.changeset/three-pears-build.md b/.changeset/three-pears-build.md new file mode 100644 index 0000000000..8a638149a3 --- /dev/null +++ b/.changeset/three-pears-build.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ignore comments when reading CSS values diff --git a/.changeset/twelve-cooks-speak.md b/.changeset/twelve-cooks-speak.md new file mode 100644 index 0000000000..d4fcd5c339 --- /dev/null +++ b/.changeset/twelve-cooks-speak.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ignore false-positive errors of `$inspect` dependencies diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index d763b6578f..b90c71366a 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -167,6 +167,8 @@ To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snaps This is handy when you want to pass some state to an external library or API that doesn't expect a proxy, such as `structuredClone`. +If a value has a `toJSON` method, the snapshot will clone the value returned from `toJSON` instead of the original object. + ## `$state.eager` When state changes, it may not be reflected in the UI immediately if it is used by an `await` expression, because [updates are synchronized](await-expressions#Synchronized-updates). diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index 159a568477..bbbc86c997 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -147,6 +147,8 @@ declare namespace $state { * * ``` * + * If `state` has a `toJSON` method, the snapshot will clone the value returned from `toJSON` instead of the original object. + * * @see {@link https://svelte.dev/docs/svelte/$state#$state.snapshot Documentation} * * @param state The value to snapshot diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js index 8cb1d54d54..160e5da277 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/style.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js @@ -524,6 +524,21 @@ function read_value(parser) { in_url = true; } else if ((char === ';' || char === '{' || char === '}') && !in_url && !quote_mark) { return value.trim(); + } else if ( + char === '/' && + !in_url && + !quote_mark && + parser.template[parser.index + 1] === '*' + ) { + parser.index += 2; + while (parser.index < parser.template.length) { + if (parser.template[parser.index] === '*' && parser.template[parser.index + 1] === '/') { + parser.index += 2; + break; + } + parser.index++; + } + continue; } value += char; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/DebugTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/DebugTag.js index ef9a070859..01a7e0e872 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/DebugTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/DebugTag.js @@ -8,6 +8,10 @@ import * as b from '#compiler/builders'; * @param {ComponentContext} context */ export function DebugTag(node, context) { + const blockers = node.identifiers + .map((identifier) => context.state.scope.get(identifier.name)?.blocker) + .filter((blocker) => blocker != null); + const object = b.object( node.identifiers.map((identifier) => { const visited = b.call('$.snapshot', /** @type {Expression} */ (context.visit(identifier))); @@ -20,9 +24,11 @@ export function DebugTag(node, context) { }) ); - const call = b.call('console.log', object); + const args = [b.thunk(b.block([b.stmt(b.call('console.log', object)), b.debugger]))]; - context.state.init.push( - b.stmt(b.call('$.template_effect', b.thunk(b.block([b.stmt(call), b.debugger])))) - ); + if (blockers.length > 0) { + args.push(b.array([]), b.array([]), b.array(blockers)); + } + + context.state.init.push(b.stmt(b.call('$.template_effect', ...args))); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/DebugTag.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/DebugTag.js index 31b53fd3eb..3c4af2fe04 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/DebugTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/DebugTag.js @@ -2,23 +2,34 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import * as b from '#compiler/builders'; +import { create_child_block } from './shared/utils.js'; /** * @param {AST.DebugTag} node * @param {ComponentContext} context */ export function DebugTag(node, context) { + const blockers = node.identifiers + .map((identifier) => context.state.scope.get(identifier.name)?.blocker) + .filter((blocker) => blocker != null); + context.state.template.push( - b.stmt( - b.call( - 'console.log', - b.object( - node.identifiers.map((identifier) => - b.prop('init', identifier, /** @type {Expression} */ (context.visit(identifier))) + ...create_child_block( + [ + b.stmt( + b.call( + 'console.log', + b.object( + node.identifiers.map((identifier) => + b.prop('init', identifier, /** @type {Expression} */ (context.visit(identifier))) + ) + ) ) - ) - ) - ), - b.debugger + ), + b.debugger + ], + b.array(blockers), + false + ) ); } diff --git a/packages/svelte/src/internal/client/dev/inspect.js b/packages/svelte/src/internal/client/dev/inspect.js index 75b29ce9b1..7a8fa0e963 100644 --- a/packages/svelte/src/internal/client/dev/inspect.js +++ b/packages/svelte/src/internal/client/dev/inspect.js @@ -20,6 +20,8 @@ export function inspect(get_value, inspector, show_stack = false) { // in an error (an `$inspect(object.property)` will run before the // `{#if object}...{/if}` that contains it) eager_effect(() => { + error = UNINITIALIZED; + try { var value = get_value(); } catch (e) { diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index f59eef69e1..5ed6e61992 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -621,7 +621,7 @@ function get_setters(element) { var element_proto = Element.prototype; // Stop at Element, from there on there's only unnecessary setters we're not interested in - // Do not use contructor.name here as that's unreliable in some browser environments + // Do not use constructor.name here as that's unreliable in some browser environments while (element_proto !== proto) { descriptors = get_descriptors(proto); diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/this.js b/packages/svelte/src/internal/client/dom/elements/bindings/this.js index c39ca34062..52f0c213d3 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/this.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/this.js @@ -40,7 +40,7 @@ export function bind_this(element_or_component = {}, update, get_value, get_part parts = get_parts?.() || []; untrack(() => { - if (element_or_component !== get_value(...parts)) { + if (!is_bound_this(get_value(...parts), element_or_component)) { update(element_or_component, ...parts); // If this is an effect rerun (cause: each block context changes), then nullify the binding at // the previous position if it isn't already taken over by a different effect. diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 82be1d1e8d..4239cda04b 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -342,6 +342,14 @@ export class Batch { this.#deferred?.resolve(); } + // Order matters here - we need to commit and THEN continue flushing new batches, not the other way around, + // else we could start flushing a new batch and then, if it has pending work, rebase it right afterwards, which is wrong. + // In sync mode flushSync can cause #commit to wrongfully think that there needs to be a rebase, so we only do it in async mode + // TODO fix the underlying cause, otherwise this will likely regress when non-async mode is removed + if (async_mode_flag && !batches.has(this)) { + this.#commit(); + } + var next_batch = /** @type {Batch | null} */ (/** @type {unknown} */ (current_batch)); // Edge case: During traversal new branches might create effects that run immediately and set state, @@ -363,12 +371,6 @@ export class Batch { next_batch.#process(); } - - // In sync mode flushSync can cause #commit to wrongfully think that there needs to be a rebase, so we only do it in async mode - // TODO fix the underlying cause, otherwise this will likely regress when non-async mode is removed - if (async_mode_flag && !batches.has(this)) { - this.#commit(); - } } /** @@ -575,19 +577,23 @@ export class Batch { checked = new Map(); var current_unequal = [...batch.current.keys()].filter((c) => - this.current.has(c) ? /** @type {[any, boolean]} */ (this.current.get(c))[0] !== c : true + this.current.has(c) + ? /** @type {[any, boolean]} */ (this.current.get(c))[0] !== c.v + : true ); - for (const effect of this.#new_effects) { - if ( - (effect.f & (DESTROYED | INERT | EAGER_EFFECT)) === 0 && - depends_on(effect, current_unequal, checked) - ) { - if ((effect.f & (ASYNC | BLOCK_EFFECT)) !== 0) { - set_signal_status(effect, DIRTY); - batch.schedule(effect); - } else { - batch.#dirty_effects.add(effect); + if (current_unequal.length > 0) { + for (const effect of this.#new_effects) { + if ( + (effect.f & (DESTROYED | INERT | EAGER_EFFECT)) === 0 && + depends_on(effect, current_unequal, checked) + ) { + if ((effect.f & (ASYNC | BLOCK_EFFECT)) !== 0) { + set_signal_status(effect, DIRTY); + batch.schedule(effect); + } else { + batch.#dirty_effects.add(effect); + } } } } @@ -716,7 +722,7 @@ export class Batch { if (!is_flushing_sync) { queue_micro_task(() => { - if (current_batch !== batch) { + if (!batches.has(batch) || batch.#pending.size > 0) { // a flushSync happened in the meantime return; } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 5af51449ad..4ae49fecba 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -43,7 +43,7 @@ import { get_error } from '../../shared/dev.js'; import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { component_context } from '../context.js'; import { UNINITIALIZED } from '../../../constants.js'; -import { batch_values, current_batch } from './batch.js'; +import { batch_values, current_batch, previous_batch } from './batch.js'; import { increment_pending, unset_context } from './async.js'; import { deferred, includes, noop } from '../../shared/utils.js'; import { set_signal_status, update_derived_status } from './status.js'; @@ -399,7 +399,14 @@ export function update_derived(derived) { // change, `derived.equals` may incorrectly return `true` if (!current_batch?.is_fork || derived.deps === null) { if (current_batch !== null) { + // We also write to previous_batch because if it exists, it is a sign that we're + // currently in the process of flushing effects. These updates to deriveds may belong + // to the previous batch, not the new one (which can already exist if an earlier + // effect wrote to a source). This can cause bugs when running batch.#commit() later, + // but not adding it to current_batch can, too, so we add it to both. + // See https://github.com/sveltejs/svelte/pull/18117 for more details. current_batch.capture(derived, value, true); + previous_batch?.capture(derived, value, true); } else { derived.v = value; } diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index e208d3b6f6..5626639a84 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -1,4 +1,4 @@ -/** @import { Effect, Source } from './types.js' */ +/** @import { Derived, Effect, Source } from './types.js' */ import { DEV } from 'esm-env'; import { PROPS_IS_BINDABLE, @@ -283,8 +283,14 @@ export function prop(props, key, flags, fallback) { var fallback_value = /** @type {V} */ (fallback); var fallback_dirty = true; + var fallback_signal = /** @type {Derived | undefined} */ (undefined); var get_fallback = () => { + if (lazy && runes) { + fallback_signal ??= derived(/** @type {() => V} */ (fallback)); + return get(fallback_signal); + } + if (fallback_dirty) { fallback_dirty = false; diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 1831183e6f..f374f6a26b 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -47,7 +47,7 @@ import { proxy } from '../proxy.js'; import { execute_derived } from './deriveds.js'; import { set_signal_status, update_derived_status } from './status.js'; -/** @type {Set} */ +/** @type {Set} */ export let eager_effects = new Set(); /** @type {Map} */ @@ -272,7 +272,18 @@ export function flush_eager_effects() { set_signal_status(effect, MAYBE_DIRTY); } - if (is_dirty(effect)) { + let dirty; + + try { + dirty = is_dirty(effect); + } catch { + // Dirty-checking can evaluate derived dependencies and throw in cases where + // parent effects are about to destroy this eager effect. Run the effect so + // its own error handling can deal with transient failures. + dirty = true; + } + + if (dirty) { update_effect(effect); } } @@ -338,12 +349,6 @@ function mark_reactions(signal, status, updated_during_traversal) { // In legacy mode, skip the current effect to prevent infinite loops if (!runes && reaction === active_effect) continue; - // Inspect effects need to run immediately, so that the stack trace makes sense - if (DEV && (flags & EAGER_EFFECT) !== 0) { - eager_effects.add(reaction); - continue; - } - var not_dirty = (flags & DIRTY) === 0; // don't set a DIRTY reaction to MAYBE_DIRTY @@ -351,7 +356,12 @@ function mark_reactions(signal, status, updated_during_traversal) { set_signal_status(reaction, status); } - if ((flags & DERIVED) !== 0) { + if ((flags & EAGER_EFFECT) !== 0) { + // Eager effects need to run immediately: + // - for $inspect so that the stack trace makes sense + // - for $state.eager because they might be without an effect parent + eager_effects.add(/** @type {Effect} */ (reaction)); + } else if ((flags & DERIVED) !== 0) { var derived = /** @type {Derived} */ (reaction); batch_values?.delete(derived); diff --git a/packages/svelte/tests/css/samples/comment-with-apostrophe/expected.css b/packages/svelte/tests/css/samples/comment-with-apostrophe/expected.css new file mode 100644 index 0000000000..a196d53cc8 --- /dev/null +++ b/packages/svelte/tests/css/samples/comment-with-apostrophe/expected.css @@ -0,0 +1,4 @@ + + p.svelte-xyz { + padding: 0 /* it's a comment */ 1em; + } diff --git a/packages/svelte/tests/css/samples/comment-with-apostrophe/input.svelte b/packages/svelte/tests/css/samples/comment-with-apostrophe/input.svelte new file mode 100644 index 0000000000..0f9e0d2355 --- /dev/null +++ b/packages/svelte/tests/css/samples/comment-with-apostrophe/input.svelte @@ -0,0 +1,7 @@ +

red

+ + diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js index d0ec8b6e44..52bd47dfae 100644 --- a/packages/svelte/tests/helpers.js +++ b/packages/svelte/tests/helpers.js @@ -201,7 +201,7 @@ export const async_mode = process.env.SVELTE_NO_ASYNC !== 'true'; * @param {any[]} logs */ export function normalise_inspect_logs(logs) { - /** @type {string[]} */ + /** @type {any[]} */ const normalised = []; for (const log of logs) { diff --git a/packages/svelte/tests/runtime-production/samples/async-eager-derived/_config.js b/packages/svelte/tests/runtime-production/samples/async-eager-derived/_config.js new file mode 100644 index 0000000000..043f1610fb --- /dev/null +++ b/packages/svelte/tests/runtime-production/samples/async-eager-derived/_config.js @@ -0,0 +1,23 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [increment, shift] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + `

true - true

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

false - false

` + ); + } +}); diff --git a/packages/svelte/tests/runtime-production/samples/async-eager-derived/main.svelte b/packages/svelte/tests/runtime-production/samples/async-eager-derived/main.svelte new file mode 100644 index 0000000000..d1d979126d --- /dev/null +++ b/packages/svelte/tests/runtime-production/samples/async-eager-derived/main.svelte @@ -0,0 +1,22 @@ + + + + + +

{$state.eager(count) !== count} - {$state.eager(derivedCount) !== derivedCount}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-debug-awaited-expression/_config.js b/packages/svelte/tests/runtime-runes/samples/async-debug-awaited-expression/_config.js new file mode 100644 index 0000000000..304f65cd0a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-debug-awaited-expression/_config.js @@ -0,0 +1,18 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + mode: ['client', 'async-server'], + + async test({ assert, logs }) { + await tick(); + + assert.deepEqual(logs, [{ data: 'works' }]); + }, + test_ssr({ assert, logs }) { + assert.deepEqual(logs, [{ data: 'works' }]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-debug-awaited-expression/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-debug-awaited-expression/main.svelte new file mode 100644 index 0000000000..92b69df8fb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-debug-awaited-expression/main.svelte @@ -0,0 +1,4 @@ + + {@const data = await Promise.resolve("works")} + {@debug data} + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-1/_config.js b/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-1/_config.js new file mode 100644 index 0000000000..fb6f3388c9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-1/_config.js @@ -0,0 +1,27 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +// Tests that a newly created batch during an effect flush isn't rebased right away by the previous batch.#commit(), +// rescheduling an effect on the new batch that shouldn't run. +export default test({ + async test({ assert, target, logs }) { + await tick(); + const [increment, resolve] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + assert.deepEqual(logs, []); + + // This resolve + // - shouldn't result in the derived execution capturing the new derived value on the new batch, but on the previous batch which is currently flushing + // - shouldn't result in #commit() rebasing the new batch + resolve.click(); + await tick(); + assert.deepEqual(logs, [2]); + + // As a result, this resolve shouldn't result in another execution of the effect depending on the derived + resolve.click(); + await tick(); + assert.deepEqual(logs, [2]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-1/main.svelte new file mode 100644 index 0000000000..af470363bf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-1/main.svelte @@ -0,0 +1,32 @@ + + + + + +{#if count} + + + {(() => { + $effect(() => { + count_mirror = count; + }) + })()} + + {(() => { + $effect(() => { + console.log(double); + }) + })()} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-2/_config.js new file mode 100644 index 0000000000..d8a86f77da --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-2/_config.js @@ -0,0 +1,25 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +// Tests that a newly created batch during an effect flush isn't rebased right away by the previous batch.#commit(), +// rescheduling an effect on the new batch that shouldn't run. +export default test({ + async test({ assert, target, logs }) { + await tick(); + const [increment, resolve] = target.querySelectorAll('button'); + assert.deepEqual(logs, ['delay 0']); + + increment.click(); + await tick(); + assert.deepEqual(logs, ['delay 0', 'delay 2']); + + // This resolve should trigger the async effect only once + resolve.click(); + await tick(); + assert.deepEqual(logs, ['delay 0', 'delay 2', 'effect run', 'delay 4']); + + resolve.click(); + await tick(); + assert.deepEqual(logs, ['delay 0', 'delay 2', 'effect run', 'delay 4']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-2/main.svelte new file mode 100644 index 0000000000..fc90ae2ba4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-2/main.svelte @@ -0,0 +1,29 @@ + + + + +{await delay(a + b + c)} diff --git a/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-3/_config.js b/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-3/_config.js new file mode 100644 index 0000000000..b430e408c7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-3/_config.js @@ -0,0 +1,31 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +// Tests that a newly created batch during an effect flush isn't rebased right away by the previous batch.#commit(), +// rescheduling an effect on the new batch that shouldn't run. +export default test({ + async test({ assert, target, logs }) { + await tick(); + const [increment, shift, pop] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + assert.deepEqual(logs, []); + + // Resolve the blocking await which shouldn't result in the derived execution capturing + // the new derived value on the new batch, but on the previous batch which is currently flushing + pop.click(); + await tick(); + assert.deepEqual(logs, [2]); + + // Resolve the non-blocking await which shouldn't result in #commit() rebasing the new batch + shift.click(); + await tick(); + assert.deepEqual(logs, [2]); + + // Resolve the new batch's await + shift.click(); + await tick(); + assert.deepEqual(logs, [2]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-3/main.svelte new file mode 100644 index 0000000000..9dec14cd13 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-3/main.svelte @@ -0,0 +1,37 @@ + + + + + + +{#if count} + + {await delay(count)} + {#snippet pending()}loading{/snippet} + + + + {(() => { + $effect(() => { + count_mirror = count; + }) + })()} + + {(() => { + $effect(() => { + console.log(double); + }) + })()} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-4/_config.js b/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-4/_config.js new file mode 100644 index 0000000000..804c1f53bb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-4/_config.js @@ -0,0 +1,58 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +// Tests that a newly created batch during an effect flush isn't rebased right away by the previous batch.#commit(), +// rescheduling an effect on the new batch that shouldn't run. +export default test({ + async test({ assert, target, logs }) { + await tick(); + const [increment, unrelated, resolve] = target.querySelectorAll('button'); + + increment.click(); + await tick(); + assert.deepEqual(logs, []); + + // This resolve + // - shouldn't result in the derived execution capturing the new derived value on the new batch, but on the previous batch which is currently flushing + // - shouldn't result in #commit() rebasing the new batch + resolve.click(); + await tick(); + assert.deepEqual(logs, [2]); + assert.htmlEqual( + target.innerHTML, + ` + + + + ` + ); + + // This resolve + // - shouldn't result in the derived execution capturing the new derived value on the new batch, but on the previous batch which is currently flushing + // - shouldn't result in #commit() rebasing the new batch + unrelated.click(); + await tick(); + assert.deepEqual(logs, [2]); + assert.htmlEqual( + target.innerHTML, + ` + + + + ` + ); + + // As a result, this resolve shouldn't result in another execution of the effect depending on the derived + resolve.click(); + await tick(); + assert.deepEqual(logs, [2]); + assert.htmlEqual( + target.innerHTML, + ` + + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-4/main.svelte new file mode 100644 index 0000000000..fdc2447e3e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-dont-rebase-new-batch-4/main.svelte @@ -0,0 +1,38 @@ + + + + + + +{#if count} + + + {(() => { + $effect(() => { + count_mirror = count; + untrack(() => count_mirror_d); // execute derived; should associate value with the right batch + }) + })()} + + {(() => { + $effect(() => { + console.log(double); + }) + })()} +{/if} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-updates-microtask-separated/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-updates-microtask-separated/_config.js new file mode 100644 index 0000000000..dea121c456 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-updates-microtask-separated/_config.js @@ -0,0 +1,15 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +// Ensure that microtask timing doesn't influence whether or not a scheduled batch is flushed. +// Timing can be such that the current_batch is reset before the scheduled flush runs, which +// would cause the flush to skip without the fix. +export default test({ + async test({ assert, target }) { + const [btn] = target.querySelectorAll('button'); + + btn.click(); + await tick(); + assert.htmlEqual(target.innerHTML, '1 1'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-updates-microtask-separated/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-updates-microtask-separated/main.svelte new file mode 100644 index 0000000000..ebfbf4ca4e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-updates-microtask-separated/main.svelte @@ -0,0 +1,18 @@ + + +{#if a} + {@const toShow = await a} + {toShow} + {b} +{:else} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-proxy-deep/Component.svelte b/packages/svelte/tests/runtime-runes/samples/bind-this-proxy-deep/Component.svelte new file mode 100644 index 0000000000..c43810b6bf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-proxy-deep/Component.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-proxy-deep/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-this-proxy-deep/_config.js new file mode 100644 index 0000000000..2f9e60e017 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-proxy-deep/_config.js @@ -0,0 +1,22 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const btn = target.querySelector('button'); + + flushSync(() => { + btn?.click(); + }); + + flushSync(() => { + btn?.click(); + }); + + assert.deepEqual(logs, [ + {}, + { 0: { name: 'Row 0' } }, + { 0: { name: 'Row 0' }, 1: { name: 'Row 1' } } + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-proxy-deep/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-this-proxy-deep/main.svelte new file mode 100644 index 0000000000..2e2a49b3f6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-proxy-deep/main.svelte @@ -0,0 +1,16 @@ + + + +{#each rows as row (row.id)} + +{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-derived-if-destroy/List.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-derived-if-destroy/List.svelte new file mode 100644 index 0000000000..c73ac9a99d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-derived-if-destroy/List.svelte @@ -0,0 +1,11 @@ + + +
    + {#each things as thing} +
  • thing {thing.id}
  • + {/each} +
diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-derived-if-destroy/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-derived-if-destroy/_config.js new file mode 100644 index 0000000000..c29022c9dc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-derived-if-destroy/_config.js @@ -0,0 +1,21 @@ +import { normalise_inspect_logs } from '../../../helpers'; +import { test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + compileOptions: { + dev: true + }, + + async test({ assert, target, errors, logs }) { + const button = target.querySelector('button'); + + flushSync(() => { + button?.click(); + }); + + assert.htmlEqual(target.innerHTML, ''); + assert.equal(errors.length, 0); + assert.deepEqual(normalise_inspect_logs(logs), [[{ id: 1 }, { id: 2 }]]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-derived-if-destroy/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-derived-if-destroy/main.svelte new file mode 100644 index 0000000000..89e09f417b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-derived-if-destroy/main.svelte @@ -0,0 +1,15 @@ + + +{#if data} + t)} /> +{/if} + + diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy-accessors/main.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy-accessors/main.svelte index fe2ac37bd3..f6437d6589 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy-accessors/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-lazy-accessors/main.svelte @@ -1,5 +1,5 @@ + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-reactivity/sub.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-value-reactivity/sub.svelte new file mode 100644 index 0000000000..b3cd3fae36 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-reactivity/sub.svelte @@ -0,0 +1,9 @@ + + +

greeting: {p0}

\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-value-reactivity/translations.svelte.js b/packages/svelte/tests/runtime-runes/samples/props-default-value-reactivity/translations.svelte.js new file mode 100644 index 0000000000..4aa4dc9999 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/props-default-value-reactivity/translations.svelte.js @@ -0,0 +1,9 @@ +let greeting = $state('Hello'); + +export function get_translation() { + return greeting; +} + +export function set_translation(value) { + greeting = value; +} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index d555f63489..2cd78cbc54 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -3661,6 +3661,8 @@ declare namespace $state { * * ``` * + * If `state` has a `toJSON` method, the snapshot will clone the value returned from `toJSON` instead of the original object. + * * @see {@link https://svelte.dev/docs/svelte/$state#$state.snapshot Documentation} * * @param state The value to snapshot