diff --git a/.changeset/gold-times-see.md b/.changeset/gold-times-see.md new file mode 100644 index 0000000000..f8d5da5042 --- /dev/null +++ b/.changeset/gold-times-see.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure deriveds values are correct across batches diff --git a/documentation/docs/07-misc/01-best-practices.md b/documentation/docs/07-misc/01-best-practices.md index 66f7da2613..e2cb72828f 100644 --- a/documentation/docs/07-misc/01-best-practices.md +++ b/documentation/docs/07-misc/01-best-practices.md @@ -143,7 +143,7 @@ The CSS in a component's ` ``` -If this impossible (for example, the child component comes from a library) you can use `:global` to override styles: +If this is impossible (for example, the child component comes from a library) you can use `:global` to override styles: ```svelte
diff --git a/documentation/docs/07-misc/07-v5-migration-guide.md b/documentation/docs/07-misc/07-v5-migration-guide.md index 580dbec6d4..9b1f2dec63 100644 --- a/documentation/docs/07-misc/07-v5-migration-guide.md +++ b/documentation/docs/07-misc/07-v5-migration-guide.md @@ -324,7 +324,7 @@ When spreading props, local event handlers must go _after_ the spread, or they r > > It was always possible to use component callback props, but because you had to listen to DOM events using `on:`, it made sense to use `createEventDispatcher` for component events due to syntactical consistency. Now that we have event attributes (`onclick`), it's the other way around: Callback props are now the more sensible thing to do. > -> The removal of event modifiers is arguably one of the changes that seems like a step back for those who've liked the shorthand syntax of event modifiers. Given that they are not used that frequently, we traded a smaller surface area for more explicitness. Modifiers also were inconsistent, because most of them were only useable on DOM elements. +> The removal of event modifiers is arguably one of the changes that seems like a step back for those who've liked the shorthand syntax of event modifiers. Given that they are not used that frequently, we traded a smaller surface area for more explicitness. Modifiers also were inconsistent, because most of them were only usable on DOM elements. > > Multiple listeners for the same event are also no longer possible, but it was something of an anti-pattern anyway, since it impedes readability: if there are many attributes, it becomes harder to spot that there are two handlers unless they are right next to each other. It also implies that the two handlers are independent, when in fact something like `event.stopImmediatePropagation()` inside `one` would prevent `two` from being called. > diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 3c796623a3..2ae21fb28a 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,31 @@ # svelte +## 5.53.11 + +### Patch Changes + +- fix: remove `untrack` circular dependency ([#17910](https://github.com/sveltejs/svelte/pull/17910)) + +- fix: recover from errors that leave a corrupted effect tree ([#17888](https://github.com/sveltejs/svelte/pull/17888)) + +- fix: properly lazily evaluate RHS when checking for `assignment_value_stale` ([#17906](https://github.com/sveltejs/svelte/pull/17906)) + +- fix: resolve boundary in correct batch when hydrating ([#17914](https://github.com/sveltejs/svelte/pull/17914)) + +- chore: rebase batches after process, not during ([#17900](https://github.com/sveltejs/svelte/pull/17900)) + +## 5.53.10 + +### Patch Changes + +- fix: re-process batch if new root effects were scheduled ([#17895](https://github.com/sveltejs/svelte/pull/17895)) + +## 5.53.9 + +### Patch Changes + +- fix: better `bind:this` cleanup timing ([#17885](https://github.com/sveltejs/svelte/pull/17885)) + ## 5.53.8 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index a26deef7e5..10c93b500b 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.53.8", + "version": "5.53.11", "type": "module", "types": "./types/index.d.ts", "engines": { @@ -175,7 +175,7 @@ "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", - "devalue": "^5.6.3", + "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index 1379669e77..5282f1ed64 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -179,7 +179,7 @@ function build_assignment(operator, left, right, context) { // in cases like `(object.items ??= []).push(value)`, we may need to warn // if the value gets proxified, since the proxy _isn't_ the thing that // will be pushed to. we do this by transforming it to something like - // `$.assign_nullish(object, 'items', [])` + // `$.assign_nullish(object, 'items', () => [])` let should_transform = dev && path.at(-1) !== 'ExpressionStatement' && @@ -236,7 +236,7 @@ function build_assignment(operator, left, right, context) { ? left.property : b.literal(/** @type {Identifier} */ (left.property).name) ), - right, + b.arrow([], right), b.literal(locate_node(left)) ) ) diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 3525539f1d..df96f4899b 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -29,6 +29,8 @@ export const INERT = 1 << 13; export const DESTROYED = 1 << 14; /** Set once a reaction has run for the first time */ export const REACTION_RAN = 1 << 15; +/** Effect is in the process of getting destroyed. Can be observed in child teardown functions */ +export const DESTROYING = 1 << 25; // Flags exclusive to effects /** diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 36c735272c..0baef5c63e 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -5,7 +5,7 @@ import { active_effect, active_reaction } from './runtime.js'; import { create_user_effect } from './reactivity/effects.js'; import { async_mode_flag, legacy_mode_flag } from '../flags/index.js'; import { FILENAME } from '../../constants.js'; -import { BRANCH_EFFECT, REACTION_RAN } from './constants.js'; +import { BRANCH_EFFECT } from './constants.js'; /** @type {ComponentContext | null} */ export let component_context = null; @@ -182,6 +182,7 @@ export function push(props, runes = false, fn) { e: null, s: props, x: null, + r: /** @type {Effect} */ (active_effect), l: legacy_mode_flag && !runes ? { s: null, u: null, $: [] } : null }; diff --git a/packages/svelte/src/internal/client/dev/assign.js b/packages/svelte/src/internal/client/dev/assign.js index 3b48e736b5..1cda7044b5 100644 --- a/packages/svelte/src/internal/client/dev/assign.js +++ b/packages/svelte/src/internal/client/dev/assign.js @@ -21,12 +21,12 @@ function compare(a, b, property, location) { /** * @param {any} object * @param {string} property - * @param {any} value + * @param {() => any} rhs_getter * @param {string} location */ -export function assign(object, property, value, location) { +export function assign(object, property, rhs_getter, location) { return compare( - (object[property] = value), + (object[property] = rhs_getter()), untrack(() => object[property]), property, location @@ -36,12 +36,12 @@ export function assign(object, property, value, location) { /** * @param {any} object * @param {string} property - * @param {any} value + * @param {() => any} rhs_getter * @param {string} location */ -export function assign_and(object, property, value, location) { +export function assign_and(object, property, rhs_getter, location) { return compare( - (object[property] &&= value), + (object[property] &&= rhs_getter()), untrack(() => object[property]), property, location @@ -51,12 +51,12 @@ export function assign_and(object, property, value, location) { /** * @param {any} object * @param {string} property - * @param {any} value + * @param {() => any} rhs_getter * @param {string} location */ -export function assign_or(object, property, value, location) { +export function assign_or(object, property, rhs_getter, location) { return compare( - (object[property] ||= value), + (object[property] ||= rhs_getter()), untrack(() => object[property]), property, location @@ -66,12 +66,12 @@ export function assign_or(object, property, value, location) { /** * @param {any} object * @param {string} property - * @param {any} value + * @param {() => any} rhs_getter * @param {string} location */ -export function assign_nullish(object, property, value, location) { +export function assign_nullish(object, property, rhs_getter, location) { return compare( - (object[property] ??= value), + (object[property] ??= rhs_getter()), untrack(() => object[property]), property, location diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index b38a3131ca..8046f1e222 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -218,8 +218,6 @@ export class Boundary { this.is_pending = true; this.#pending_effect = branch(() => pending(this.#anchor)); - var batch = /** @type {Batch} */ (current_batch); - queue_micro_task(() => { var fragment = (this.#offscreen_fragment = document.createDocumentFragment()); var anchor = create_text(); @@ -238,14 +236,12 @@ export class Boundary { this.#pending_effect = null; }); - this.#resolve(batch); + this.#resolve(/** @type {Batch} */ (current_batch)); } }); } #render() { - var batch = /** @type {Batch} */ (current_batch); - try { this.is_pending = this.has_pending_snippet(); this.#pending_count = 0; @@ -262,7 +258,7 @@ export class Boundary { const pending = /** @type {(anchor: Node) => void} */ (this.#props.pending); this.#pending_effect = branch(() => pending(this.#anchor)); } else { - this.#resolve(batch); + this.#resolve(/** @type {Batch} */ (current_batch)); } } catch (error) { this.error(error); 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 f2e715113f..c39ca34062 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/this.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/this.js @@ -1,7 +1,8 @@ -import { STATE_SYMBOL } from '#client/constants'; +/** @import { ComponentContext, Effect } from '#client' */ +import { DESTROYING, STATE_SYMBOL } from '#client/constants'; +import { component_context } from '../../../context.js'; import { effect, render_effect } from '../../../reactivity/effects.js'; -import { untrack } from '../../../runtime.js'; -import { queue_micro_task } from '../../task.js'; +import { active_effect, untrack } from '../../../runtime.js'; /** * @param {any} bound_value @@ -23,6 +24,9 @@ function is_bound_this(bound_value, element_or_component) { * @returns {void} */ export function bind_this(element_or_component = {}, update, get_value, get_parts) { + var component_effect = /** @type {ComponentContext} */ (component_context).r; + var parent = /** @type {Effect} */ (active_effect); + effect(() => { /** @type {unknown[]} */ var old_parts; @@ -48,12 +52,25 @@ export function bind_this(element_or_component = {}, update, get_value, get_part }); return () => { - // We cannot use effects in the teardown phase, we we use a microtask instead. - queue_micro_task(() => { + // When the bind:this effect is destroyed, we go up the effect parent chain until we find the last parent effect that is destroyed, + // or the effect containing the component bind:this is in (whichever comes first). That way we can time the nulling of the binding + // as close to user/developer expectation as possible. + // TODO Svelte 6: Decide if we want to keep this logic or just always null the binding in the component effect's teardown + // (which would be simpler, but less intuitive in some cases, and breaks the `ondestroy-before-cleanup` test) + let p = parent; + while (p !== component_effect && p.parent !== null && p.parent.f & DESTROYING) { + p = p.parent; + } + const teardown = () => { if (parts && is_bound_this(get_value(...parts), element_or_component)) { update(null, ...parts); } - }); + }; + const original_teardown = p.teardown; + p.teardown = () => { + teardown(); + original_teardown?.(); + }; }; }); diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index fc4c70c6b8..ebaed93e9c 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -225,7 +225,12 @@ export class Batch { var updates = (legacy_updates = []); for (const root of roots) { - this.#traverse(root, effects, render_effects); + try { + this.#traverse(root, effects, render_effects); + } catch (e) { + reset_all(root); + throw e; + } } // any writes should take effect in a subsequent batch @@ -249,6 +254,10 @@ export class Batch { reset_branch(e, t); } } else { + if (this.#pending === 0) { + batches.delete(this); + } + // clear effects. Those that are still needed will be rescheduled through unskipping the skipped branches. this.#dirty_effects.clear(); this.#maybe_dirty_effects.clear(); @@ -262,15 +271,19 @@ export class Batch { flush_queued_effects(effects); previous_batch = null; - if (this.#pending === 0) { - this.#commit(); - } - this.#deferred?.resolve(); } 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, + // causing an effect and therefore a root to be scheduled again. We need to traverse the current batch + // once more in that case - most of the time this will just clean up dirty branches. + if (this.#roots.length > 0) { + const batch = (next_batch ??= this); + batch.#roots.push(...this.#roots.filter((r) => !batch.#roots.includes(r))); + } + if (next_batch !== null) { batches.add(next_batch); @@ -282,6 +295,10 @@ export class Batch { next_batch.#process(); } + + if (!batches.has(this)) { + this.#commit(); + } } /** @@ -349,11 +366,11 @@ export class Batch { * Associate a change to a given source with the current * batch, noting its previous and current values * @param {Source} source - * @param {any} value + * @param {any} old_value */ - capture(source, value) { - if (value !== UNINITIALIZED && !this.previous.has(source)) { - this.previous.set(source, value); + capture(source, old_value) { + if (old_value !== UNINITIALIZED && !this.previous.has(source)) { + this.previous.set(source, old_value); } // Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get` @@ -425,74 +442,59 @@ export class Batch { // in other words, we re-run block/async effects with the newly // committed state, unless the batch in question has a more // recent value for a given source - if (batches.size > 1) { - this.previous.clear(); - - var previous_batch = current_batch; - var previous_batch_values = batch_values; - var is_earlier = true; - - for (const batch of batches) { - if (batch === this) { - is_earlier = false; - continue; + for (const batch of batches) { + var is_earlier = batch.id < this.id; + + /** @type {Source[]} */ + var sources = []; + + for (const [source, value] of this.current) { + if (batch.current.has(source)) { + if (is_earlier && value !== batch.current.get(source)) { + // bring the value up to date + batch.current.set(source, value); + } else { + // same value or later batch has more recent value, + // no need to re-run these effects + continue; + } } - /** @type {Source[]} */ - const sources = []; - - for (const [source, value] of this.current) { - if (batch.current.has(source)) { - if (is_earlier && value !== batch.current.get(source)) { - // bring the value up to date - batch.current.set(source, value); - } else { - // same value or later batch has more recent value, - // no need to re-run these effects - continue; - } - } + sources.push(source); + } - sources.push(source); - } + if (sources.length === 0) { + continue; + } - if (sources.length === 0) { - continue; - } + // Re-run async/block effects that depend on distinct values changed in both batches + var others = [...batch.current.keys()].filter((s) => !this.current.has(s)); + if (others.length > 0) { + batch.activate(); - // Re-run async/block effects that depend on distinct values changed in both batches - const others = [...batch.current.keys()].filter((s) => !this.current.has(s)); - if (others.length > 0) { - batch.activate(); - - /** @type {Set} */ - const marked = new Set(); - /** @type {Map} */ - const checked = new Map(); - for (const source of sources) { - mark_effects(source, others, marked, checked); - } + /** @type {Set} */ + var marked = new Set(); - if (batch.#roots.length > 0) { - batch.apply(); + /** @type {Map} */ + var checked = new Map(); - for (const root of batch.#roots) { - batch.#traverse(root, [], []); - } + for (var source of sources) { + mark_effects(source, others, marked, checked); + } - // TODO do we need to do anything with the dummy effect arrays? + if (batch.#roots.length > 0) { + batch.apply(); + + for (var root of batch.#roots) { + batch.#traverse(root, [], []); } - batch.deactivate(); + // TODO do we need to do anything with the dummy effect arrays? } - } - current_batch = previous_batch; - batch_values = previous_batch_values; + batch.deactivate(); + } } - - this.#skipped_branches.clear(); - batches.delete(this); } /** @@ -559,7 +561,10 @@ export class Batch { } apply() { - if (!async_mode_flag || (!this.is_fork && batches.size === 1)) return; + if (!async_mode_flag || (!this.is_fork && batches.size === 1)) { + batch_values = null; + return; + } // if there are multiple batches, we are 'time travelling' — // we need to override values with the ones in this batch... @@ -567,7 +572,7 @@ export class Batch { // ...and undo changes belonging to other batches for (const batch of batches) { - if (batch === this) continue; + if (batch === this || batch.is_fork) continue; for (const [source, previous] of batch.previous) { if (!batch_values.has(source)) { @@ -959,6 +964,20 @@ function reset_branch(effect, tracked) { } } +/** + * Mark an entire effect tree clean following an error + * @param {Effect} effect + */ +function reset_all(effect) { + set_signal_status(effect, CLEAN); + + var e = effect.first; + while (e !== null) { + reset_all(e); + e = e.next; + } +} + /** * Creates a 'fork', in which state changes are evaluated but not applied to the DOM. * This is useful for speculatively loading data (for example) when you suspect that @@ -1001,13 +1020,6 @@ export function fork(fn) { source.v = value; } - // make writable deriveds dirty, so they recalculate correctly - for (source of batch.current.keys()) { - if ((source.f & DERIVED) !== 0) { - set_signal_status(source, DIRTY); - } - } - return { commit: async () => { if (committed) { diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index fc3444bc30..f9d73b43e9 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -411,6 +411,7 @@ export function update_derived(derived) { } } + var old_value = derived.v; var value = execute_derived(derived); if (!derived.equals(value)) { @@ -422,6 +423,7 @@ export function update_derived(derived) { // change, `derived.equals` may incorrectly return `true` if (!current_batch?.is_fork || derived.deps === null) { derived.v = value; + current_batch?.capture(derived, old_value); // deriveds without dependencies should never be recomputed if (derived.deps === null) { diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 49fe68d90b..aeffeedddd 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -34,7 +34,8 @@ import { USER_EFFECT, ASYNC, CONNECTED, - MANAGED_EFFECT + MANAGED_EFFECT, + DESTROYING } from '#client/constants'; import * as e from '../errors.js'; import { DEV } from 'esm-env'; @@ -520,9 +521,9 @@ export function destroy_effect(effect, remove_dom = true) { removed = true; } + set_signal_status(effect, DESTROYING); destroy_effect_children(effect, remove_dom && !removed); remove_reactions(effect, 0); - set_signal_status(effect, DESTROYED); var transitions = effect.nodes && effect.nodes.t; @@ -534,6 +535,9 @@ export function destroy_effect(effect, remove_dom = true) { execute_effect_teardown(effect); + effect.f ^= DESTROYING; + effect.f |= DESTROYED; + var parent = effect.parent; // If the parent doesn't have any children, then skip this work altogether diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index f63d4daedd..e208d3b6f6 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -419,9 +419,7 @@ export function prop(props, key, flags, fallback) { // special case — avoid recalculating the derived if we're in a // teardown function and the prop was overridden locally, or the - // component was already destroyed (this latter part is necessary - // because `bind:this` can read props after the component has - // been destroyed. TODO simplify `bind:this` + // component was already destroyed (people could access props in a timeout) if ((is_destroying_effect && overridden) || (parent_effect.f & DESTROYED) !== 0) { return d.v; } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index f4ae92659c..3ccde0f211 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -231,7 +231,11 @@ export function internal_set(source, value, updated_during_traversal = null) { execute_derived(derived); } - update_derived_status(derived); + // During time traveling we don't want to reset the status so that + // traversal of the graph in the other batches still happens + if (batch_values === null) { + update_derived_status(derived); + } } source.wv = increment_write_version(); diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index f4fc81170d..2320e7b510 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -38,6 +38,12 @@ export type ComponentContext = { * @deprecated remove in 6.0 */ x: Record | null; + /** + * The parent effect of this component + * TODO 6.0 this is used to control `bind:this` timing that might change, + * in which case we can remove this property + */ + r: Effect; /** * legacy stuff * @deprecated remove in 6.0 diff --git a/packages/svelte/src/store/utils.js b/packages/svelte/src/store/utils.js index db2a62c68c..2d36d64d2d 100644 --- a/packages/svelte/src/store/utils.js +++ b/packages/svelte/src/store/utils.js @@ -1,5 +1,5 @@ /** @import { Readable } from './public' */ -import { untrack } from '../index-client.js'; +import { untrack } from '../internal/client/runtime.js'; import { noop } from '../internal/shared/utils.js'; /** diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index a6f9f0f907..64b2ee5c2d 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.53.8'; +export const VERSION = '5.53.11'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/runtime-runes/samples/assignment-value-stale-lazy-rhs/_config.js b/packages/svelte/tests/runtime-runes/samples/assignment-value-stale-lazy-rhs/_config.js new file mode 100644 index 0000000000..729eb1813a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/assignment-value-stale-lazy-rhs/_config.js @@ -0,0 +1,19 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + }, + async test({ assert, target }) { + const button = /** @type {HTMLElement} */ (target.querySelector('button')); + await tick(); + assert.htmlEqual(target.innerHTML, `

count1: 0, count2: 0

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

count1: 1, count2: 1

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

count1: 2, count2: 1

`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/assignment-value-stale-lazy-rhs/main.svelte b/packages/svelte/tests/runtime-runes/samples/assignment-value-stale-lazy-rhs/main.svelte new file mode 100644 index 0000000000..9937dc9713 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/assignment-value-stale-lazy-rhs/main.svelte @@ -0,0 +1,18 @@ + + + +

count1: {count1}, count2: {count2}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-eager-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-eager-derived/_config.js new file mode 100644 index 0000000000..043f1610fb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/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-runes/samples/async-eager-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-eager-derived/main.svelte new file mode 100644 index 0000000000..d1d979126d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/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-fork-discard-derived-writable-uninitialized/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-discard-derived-writable-uninitialized/_config.js new file mode 100644 index 0000000000..98440b6922 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-discard-derived-writable-uninitialized/_config.js @@ -0,0 +1,16 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const [btn] = target.querySelectorAll('button'); + + btn.click(); + await tick(); + assert.deepEqual(logs, [10]); + + btn.click(); + await tick(); + assert.deepEqual(logs, [10, 10]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-discard-derived-writable-uninitialized/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-discard-derived-writable-uninitialized/main.svelte new file mode 100644 index 0000000000..16c1668480 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-discard-derived-writable-uninitialized/main.svelte @@ -0,0 +1,21 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/_config.js b/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/_config.js index e9ccbba2b6..c6f65c33be 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-unresolved-promise/_config.js @@ -18,6 +18,14 @@ export default test({ increment.click(); await tick(); + assert.htmlEqual( + target.innerHTML, + ` + +

0

+ ` + ); + increment.click(); await tick(); @@ -28,5 +36,27 @@ export default test({

2

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

2

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

4

+ ` + ); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-destroy-timing/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/bind-this-destroy-timing/Inner.svelte new file mode 100644 index 0000000000..e9e6e0d18d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-destroy-timing/Inner.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-destroy-timing/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-this-destroy-timing/_config.js new file mode 100644 index 0000000000..e0bf4e3af0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-destroy-timing/_config.js @@ -0,0 +1,18 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [btn] = target.querySelectorAll('button'); + + btn.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + +

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-destroy-timing/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-this-destroy-timing/main.svelte new file mode 100644 index 0000000000..11e75b8b6d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-destroy-timing/main.svelte @@ -0,0 +1,19 @@ + + +{#if value} + {@const result = value} + +{/if} + + +

{externalView}

diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-destroy-timing2/_config.js b/packages/svelte/tests/runtime-runes/samples/bind-this-destroy-timing2/_config.js new file mode 100644 index 0000000000..b48efa0fec --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-destroy-timing2/_config.js @@ -0,0 +1,12 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [btn] = target.querySelectorAll('button'); + + btn.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-this-destroy-timing2/main.svelte b/packages/svelte/tests/runtime-runes/samples/bind-this-destroy-timing2/main.svelte new file mode 100644 index 0000000000..8b60d693eb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/bind-this-destroy-timing2/main.svelte @@ -0,0 +1,10 @@ + + +{#if value} + {value} +{/if} + + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-in-pending-boundary/Child.svelte b/packages/svelte/tests/runtime-runes/samples/effect-in-pending-boundary/Child.svelte new file mode 100644 index 0000000000..0f18e43e56 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-in-pending-boundary/Child.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/effect-in-pending-boundary/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-in-pending-boundary/_config.js new file mode 100644 index 0000000000..21575231ee --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-in-pending-boundary/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, logs }) { + assert.deepEqual(logs, ['hello from child']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-in-pending-boundary/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-in-pending-boundary/main.svelte new file mode 100644 index 0000000000..c4c0ef23ab --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-in-pending-boundary/main.svelte @@ -0,0 +1,11 @@ + + + + + + {#snippet pending()} +

Loading...

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

show: false

+ ` + ); + + assert.throws(() => { + flushSync(() => toggle.click()); + }, /NonExistent is not defined/); + + flushSync(() => increment.click()); + assert.htmlEqual( + target.innerHTML, + ` + + +

show: ${compileOptions.experimental?.async ? 'false' : 'true'}

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-recovery/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-recovery/main.svelte new file mode 100644 index 0000000000..03bfae2596 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-recovery/main.svelte @@ -0,0 +1,13 @@ + + + + + +

show: {show}

+ +{#if show} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/new-branch-reschedule-2/_config.js b/packages/svelte/tests/runtime-runes/samples/new-branch-reschedule-2/_config.js new file mode 100644 index 0000000000..0d319b7274 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/new-branch-reschedule-2/_config.js @@ -0,0 +1,46 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const [open, close, increment] = target.querySelectorAll('button'); + + open.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +
open (width: 42)
+ ` + ); + + increment.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +
open (width: 42)
+ ` + ); + + close.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +
closed
+ ` + ); + + assert.deepEqual(logs, ['effect ran']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/new-branch-reschedule-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/new-branch-reschedule-2/main.svelte new file mode 100644 index 0000000000..38a88a0bca --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/new-branch-reschedule-2/main.svelte @@ -0,0 +1,37 @@ + + + + + + + + +
+ {#if store.active} + open (width: {store.panelWidth}) + {:else} + closed + {/if} +
+ \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/new-branch-reschedule/_config.js b/packages/svelte/tests/runtime-runes/samples/new-branch-reschedule/_config.js new file mode 100644 index 0000000000..5ebc278f0b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/new-branch-reschedule/_config.js @@ -0,0 +1,44 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [open, close, increment] = target.querySelectorAll('button'); + + open.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +
open (width: 42)
+ ` + ); + + increment.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +
open (width: 42)
+ ` + ); + + close.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +
closed
+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/new-branch-reschedule/main.svelte b/packages/svelte/tests/runtime-runes/samples/new-branch-reschedule/main.svelte new file mode 100644 index 0000000000..55b5baf62c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/new-branch-reschedule/main.svelte @@ -0,0 +1,31 @@ + + + + + + + + +
+ {#if store.active} + open (width: {store.panelWidth}) + {:else} + closed + {/if} +
\ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8693c466c1..aa423e4923 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,8 +96,8 @@ importers: specifier: ^2.1.1 version: 2.1.1 devalue: - specifier: ^5.6.3 - version: 5.6.3 + specifier: ^5.6.4 + version: 5.6.4 esm-env: specifier: ^1.2.1 version: 1.2.1 @@ -1267,8 +1267,8 @@ packages: engines: {node: '>=0.10'} hasBin: true - devalue@5.6.3: - resolution: {integrity: sha512-nc7XjUU/2Lb+SvEFVGcWLiKkzfw8+qHI7zn8WYXKkLMgfGSHbgCEaR6bJpev8Cm6Rmrb19Gfd/tZvGqx9is3wg==} + devalue@5.6.4: + resolution: {integrity: sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==} dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} @@ -3563,7 +3563,7 @@ snapshots: detect-libc@1.0.3: optional: true - devalue@5.6.3: {} + devalue@5.6.4: {} dir-glob@3.0.1: dependencies: