pull/17862/head
Rich Harris 1 week ago
commit 489cc444de

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure deriveds values are correct across batches

@ -143,7 +143,7 @@ The CSS in a component's `<style>` is scoped to that component. If a parent comp
</style>
```
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
<div>

@ -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.
>

@ -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

@ -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",

@ -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))
)
)

@ -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
/**

@ -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
};

@ -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

@ -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);

@ -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?.();
};
};
});

@ -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<Value>} */
const marked = new Set();
/** @type {Map<Reaction, boolean>} */
const checked = new Map();
for (const source of sources) {
mark_effects(source, others, marked, checked);
}
/** @type {Set<Value>} */
var marked = new Set();
if (batch.#roots.length > 0) {
batch.apply();
/** @type {Map<Reaction, boolean>} */
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) {

@ -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) {

@ -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

@ -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;
}

@ -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();

@ -38,6 +38,12 @@ export type ComponentContext = {
* @deprecated remove in 6.0
*/
x: Record<string, any> | 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

@ -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';
/**

@ -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';

@ -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, `<button>go</button><p>count1: 0, count2: 0</p>`);
button.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>go</button><p>count1: 1, count2: 1</p>`);
button.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>go</button><p>count1: 2, count2: 1</p>`);
}
});

@ -0,0 +1,18 @@
<script>
let count1 = $state(0);
let count2 = $state(0);
let cache = $state({});
function go() {
count1++;
const value = cache.value ??= get_value();
}
function get_value() {
count2++;
return 42;
}
</script>
<button onclick={go}>go</button>
<p>count1: {count1}, count2: {count2}</p>

@ -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,
`<button>clicks: 0 - 0 - 0</button> <button>shift</button> <p>true - true</p>`
);
shift.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`<button>clicks: 1 - 1 - 1</button> <button>shift</button> <p>false - false</p>`
);
}
});

@ -0,0 +1,22 @@
<script>
let count = $state(0);
const delayedCount = $derived(await push(count));
const derivedCount = $derived(count);
let resolvers = [];
function push(value) {
if (!value) return value;
const { promise, resolve } = Promise.withResolvers();
resolvers.push(() => resolve(value));
return promise;
}
</script>
<button onclick={() => count += 1}>
clicks: {count} - {delayedCount} - {derivedCount}
</button>
<button onclick={() => resolvers.shift()?.()}>shift</button>
<p>{$state.eager(count) !== count} - {$state.eager(derivedCount) !== derivedCount}</p>

@ -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]);
}
});

@ -0,0 +1,21 @@
<script>
import { fork } from 'svelte';
let s = $state(1);
let d = $derived(s * 10);
</script>
<button
onclick={() => {
const f = fork(() => {
// d has not been read yet, so this write happens with an uninitialized old value
s = 2;
d = 99;
});
f.discard();
console.log(d);
}}
>
test
</button>

@ -18,6 +18,14 @@ export default test({
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<p>0</p>
`
);
increment.click();
await tick();
@ -28,5 +36,27 @@ export default test({
<p>2</p>
`
);
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<p>2</p>
`
);
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>increment</button>
<p>4</p>
`
);
}
});

@ -0,0 +1,9 @@
<script>
let { data } = $props();
const processed = $derived(data.toUpperCase());
export function getProcessed() {
return processed;
}
</script>

@ -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,
`
<button>clear</button>
<p></p>
`
);
}
});

@ -0,0 +1,19 @@
<script>
import Inner from './Inner.svelte';
let value = $state('hello');
let innerComp = $state();
// Reads Inner's derived value from outside the {#if} block, keeping it
// connected in the reactive graph even after the branch is destroyed.
const externalView = $derived(innerComp?.getProcessed() ?? '');
</script>
{#if value}
{@const result = value}
<Inner data={result} bind:this={innerComp} />
{/if}
<button onclick={() => (value = undefined)}>clear</button>
<p>{externalView}</p>

@ -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, `<button>clear</button>`);
}
});

@ -0,0 +1,10 @@
<script>
let value = $state('hello');
let elements = {};
</script>
{#if value}
<span bind:this={elements[value.toUpperCase()]}>{value}</span>
{/if}
<button onclick={() => (value = undefined)}>clear</button>

@ -0,0 +1,5 @@
<script lang="ts">
$effect(() => {
console.log('hello from child');
});
</script>

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
async test({ assert, logs }) {
assert.deepEqual(logs, ['hello from child']);
}
});

@ -0,0 +1,11 @@
<script>
import Child from './Child.svelte';
</script>
<svelte:boundary>
<Child />
{#snippet pending()}
<p>Loading...</p>
{/snippet}
</svelte:boundary>

@ -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,
`
<button>toggle</button>
<button>count: 1</button>
<p>show: false</p>
`
);
assert.throws(() => {
flushSync(() => toggle.click());
}, /NonExistent is not defined/);
flushSync(() => increment.click());
assert.htmlEqual(
target.innerHTML,
`
<button>toggle</button>
<button>count: 2</button>
<p>show: ${compileOptions.experimental?.async ? 'false' : 'true'}</p>
`
);
}
});

@ -0,0 +1,13 @@
<script>
let show = $state(false);
let count = $state(0);
</script>
<button onclick={() => show = !show}>toggle</button>
<button onclick={() => count += 1}>count: {count}</button>
<p>show: {show}</p>
{#if show}
<NonExistent />
{/if}

@ -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,
`
<button>Open</button>
<button>Close</button>
<button>0</button>
<div>open (width: 42)</div>
`
);
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>Open</button>
<button>Close</button>
<button>1</button>
<div>open (width: 42)</div>
`
);
close.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>Open</button>
<button>Close</button>
<button>1</button>
<div>closed</div>
`
);
assert.deepEqual(logs, ['effect ran']);
}
});

@ -0,0 +1,37 @@
<script module>
let active = $state(false);
let panelWidth = $state(null);
const store = {
get active() { return active; },
open() { active = true; },
close() { active = false; },
// This getter lazily writes $state on first read
get panelWidth() {
if (panelWidth === null) panelWidth = 42;
$effect(() => {
console.log('effect ran');
});
return panelWidth;
}
};
</script>
<script>
let counter = $state(0);
</script>
<button onclick={() => store.open()}>Open</button>
<button onclick={() => store.close()}>Close</button>
<button onclick={() => counter++}>{counter}</button>
<div>
{#if store.active}
open (width: {store.panelWidth})
{:else}
closed
{/if}
</div>

@ -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,
`
<button>Open</button>
<button>Close</button>
<button>0</button>
<div>open (width: 42)</div>
`
);
increment.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>Open</button>
<button>Close</button>
<button>1</button>
<div>open (width: 42)</div>
`
);
close.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>Open</button>
<button>Close</button>
<button>1</button>
<div>closed</div>
`
);
}
});

@ -0,0 +1,31 @@
<script module>
let active = $state(false);
let panelWidth = $state(null);
const store = {
get active() { return active; },
open() { active = true; },
close() { active = false; },
// This getter lazily writes $state on first read
get panelWidth() {
if (panelWidth === null) panelWidth = 42;
return panelWidth;
}
};
</script>
<script>
let counter = $state(0);
</script>
<button onclick={() => store.open()}>Open</button>
<button onclick={() => store.close()}>Close</button>
<button onclick={() => counter++}>{counter}</button>
<div>
{#if store.active}
open (width: {store.panelWidth})
{:else}
closed
{/if}
</div>

@ -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:

Loading…
Cancel
Save