diff --git a/.changeset/keep-has-check-sources.md b/.changeset/keep-has-check-sources.md new file mode 100644 index 0000000000..2ba811c4e8 --- /dev/null +++ b/.changeset/keep-has-check-sources.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: keep creating state sources for property checks during component init diff --git a/.changeset/major-papayas-run.md b/.changeset/major-papayas-run.md deleted file mode 100644 index 59909fb448..0000000000 --- a/.changeset/major-papayas-run.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: flush anything pending before invoking flushSync callback function diff --git a/.changeset/quiet-inputs-keep.md b/.changeset/quiet-inputs-keep.md deleted file mode 100644 index 0efcfcc058..0000000000 --- a/.changeset/quiet-inputs-keep.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't overwrite an unchanged spread `value`, preserving incomplete number input diff --git a/.changeset/reactive-missing-property-checks.md b/.changeset/reactive-missing-property-checks.md new file mode 100644 index 0000000000..c4b5f7105c --- /dev/null +++ b/.changeset/reactive-missing-property-checks.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: track missing state property checks in unowned deriveds diff --git a/.changeset/short-radios-sneeze.md b/.changeset/short-radios-sneeze.md deleted file mode 100644 index 847c3eef17..0000000000 --- a/.changeset/short-radios-sneeze.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: read batch-local array on each-block commit diff --git a/.changeset/tidy-pandas-hydrate.md b/.changeset/tidy-pandas-hydrate.md deleted file mode 100644 index 944797a760..0000000000 --- a/.changeset/tidy-pandas-hydrate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: prevent hydration mismatch recovery from being intercepted by error boundaries diff --git a/.changeset/tidy-trees-stay.md b/.changeset/tidy-trees-stay.md deleted file mode 100644 index 65bf7de619..0000000000 --- a/.changeset/tidy-trees-stay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: preserve dynamic element connections during hydration diff --git a/documentation/docs/05-special-elements/01-svelte-boundary.md b/documentation/docs/05-special-elements/01-svelte-boundary.md index 41ebee1564..8af7edfbd1 100644 --- a/documentation/docs/05-special-elements/01-svelte-boundary.md +++ b/documentation/docs/05-special-elements/01-svelte-boundary.md @@ -38,7 +38,7 @@ This snippet will be shown when the boundary is first created, and will remain v The `pending` snippet will _not_ be shown for subsequent async updates — for these, you can use [`$effect.pending()`]($effect#$effect.pending). -> [!NOTE] In the [playground](/playground), your app is rendered inside a boundary with an empty pending snippet, so that you can use `await` without having to create one. +> [!NOTE] During server-side rendering, a boundary with a `pending` snippet renders that snippet and ignores the rest of its content. See [Server-side rendering](await-expressions#Server-side-rendering). ### `failed` diff --git a/documentation/docs/07-misc/01-best-practices.md b/documentation/docs/07-misc/01-best-practices.md index e2cb72828f..ed7ddcbf7e 100644 --- a/documentation/docs/07-misc/01-best-practices.md +++ b/documentation/docs/07-misc/01-best-practices.md @@ -70,6 +70,10 @@ let color = type === 'danger' ? 'red' : 'green'; `$inspect.trace` is a debugging tool for reactivity. If something is not updating properly or running more than it should you can add `$inspect.trace(label)` as the first line of an `$effect` or `$derived.by` (or any function they call) to trace their dependencies and discover which one triggered an update. +## `#key` block + +While the `#key` block is useful when you want transitions/animations to run when a certain expression changes, it can be very expensive — it unmounts every element/component inside each time, so should be used sparingly. Instead of using `#key` to cause expressions in a child component's ` + + + {#snippet children()}{x}{/snippet} + diff --git a/packages/svelte/tests/compiler-errors/samples/let-directive-children-snippet/_config.js b/packages/svelte/tests/compiler-errors/samples/let-directive-children-snippet/_config.js new file mode 100644 index 0000000000..27b6a88667 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/let-directive-children-snippet/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'let_directive_snippet_conflict', + message: + 'Cannot use `let:` directives on a component that has a `children` snippet. Use snippet parameters instead (e.g. `{#snippet children({ value })}`)', + position: [359, 368] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/let-directive-children-snippet/main.svelte b/packages/svelte/tests/compiler-errors/samples/let-directive-children-snippet/main.svelte new file mode 100644 index 0000000000..bdfe3de9b5 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/let-directive-children-snippet/main.svelte @@ -0,0 +1,23 @@ + + + + + {value} + {#snippet foo()}x{/snippet} + + + {#snippet children({ value })}{value}{/snippet} + + + + {#snippet children()}{value}{/snippet} + + + + + + {#snippet children()}Value is: {value}{/snippet} + diff --git a/packages/svelte/tests/hydration/samples/boundary-nested-extra-node/_config.js b/packages/svelte/tests/hydration/samples/boundary-nested-extra-node/_config.js new file mode 100644 index 0000000000..d5b5a00c0d --- /dev/null +++ b/packages/svelte/tests/hydration/samples/boundary-nested-extra-node/_config.js @@ -0,0 +1,6 @@ +import { test } from '../../test'; + +// https://github.com/sveltejs/svelte/issues/18924 +export default test({ + expect_hydration_error: true +}); diff --git a/packages/svelte/tests/hydration/samples/boundary-nested-extra-node/_expected.html b/packages/svelte/tests/hydration/samples/boundary-nested-extra-node/_expected.html new file mode 100644 index 0000000000..0d29b99446 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/boundary-nested-extra-node/_expected.html @@ -0,0 +1 @@ +

inner

diff --git a/packages/svelte/tests/hydration/samples/boundary-nested-extra-node/_override.html b/packages/svelte/tests/hydration/samples/boundary-nested-extra-node/_override.html new file mode 100644 index 0000000000..e68995cf64 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/boundary-nested-extra-node/_override.html @@ -0,0 +1 @@ +

inner

diff --git a/packages/svelte/tests/hydration/samples/boundary-nested-extra-node/main.svelte b/packages/svelte/tests/hydration/samples/boundary-nested-extra-node/main.svelte new file mode 100644 index 0000000000..7cbf8a47fb --- /dev/null +++ b/packages/svelte/tests/hydration/samples/boundary-nested-extra-node/main.svelte @@ -0,0 +1,14 @@ + +
+ +

inner

+ {#snippet failed()} +

inner failed

+ {/snippet} +
+
+ + {#snippet failed()} +

outer failed

+ {/snippet} +
diff --git a/packages/svelte/tests/migrate/samples/labeled-assignment-without-dependencies/input.svelte b/packages/svelte/tests/migrate/samples/labeled-assignment-without-dependencies/input.svelte new file mode 100644 index 0000000000..552dc3ff55 --- /dev/null +++ b/packages/svelte/tests/migrate/samples/labeled-assignment-without-dependencies/input.svelte @@ -0,0 +1,8 @@ + + +

{count} {other} {obj.x}

diff --git a/packages/svelte/tests/migrate/samples/labeled-assignment-without-dependencies/output.svelte b/packages/svelte/tests/migrate/samples/labeled-assignment-without-dependencies/output.svelte new file mode 100644 index 0000000000..b2ec179aec --- /dev/null +++ b/packages/svelte/tests/migrate/samples/labeled-assignment-without-dependencies/output.svelte @@ -0,0 +1,14 @@ + + +

{count} {other} {obj.x}

\ No newline at end of file diff --git a/packages/svelte/tests/migrate/samples/props-on-script-tag-line/input.svelte b/packages/svelte/tests/migrate/samples/props-on-script-tag-line/input.svelte new file mode 100644 index 0000000000..dd623897bd --- /dev/null +++ b/packages/svelte/tests/migrate/samples/props-on-script-tag-line/input.svelte @@ -0,0 +1,2 @@ + +

{answer}

diff --git a/packages/svelte/tests/migrate/samples/props-on-script-tag-line/output.svelte b/packages/svelte/tests/migrate/samples/props-on-script-tag-line/output.svelte new file mode 100644 index 0000000000..add32531da --- /dev/null +++ b/packages/svelte/tests/migrate/samples/props-on-script-tag-line/output.svelte @@ -0,0 +1,3 @@ + +

{answer}

\ No newline at end of file diff --git a/packages/svelte/tests/print/samples/await-block/input.svelte b/packages/svelte/tests/print/samples/await-block/input.svelte index ba0ba8c882..029f31bf53 100644 --- a/packages/svelte/tests/print/samples/await-block/input.svelte +++ b/packages/svelte/tests/print/samples/await-block/input.svelte @@ -34,3 +34,7 @@ {:catch}

Something went wrong

{/await} + +{#await promise} +

waiting for the promise to resolve...

+{/await} diff --git a/packages/svelte/tests/print/samples/await-block/output.svelte b/packages/svelte/tests/print/samples/await-block/output.svelte index ba0ba8c882..029f31bf53 100644 --- a/packages/svelte/tests/print/samples/await-block/output.svelte +++ b/packages/svelte/tests/print/samples/await-block/output.svelte @@ -34,3 +34,7 @@ {:catch}

Something went wrong

{/await} + +{#await promise} +

waiting for the promise to resolve...

+{/await} diff --git a/packages/svelte/tests/runtime-browser/samples/bind-active-element-remove-focused/_config.js b/packages/svelte/tests/runtime-browser/samples/bind-active-element-remove-focused/_config.js new file mode 100644 index 0000000000..86a0f65551 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/bind-active-element-remove-focused/_config.js @@ -0,0 +1,27 @@ +import { test, ok } from '../../assert'; + +export default test({ + mode: ['client'], + async test({ assert, target }) { + /** @type {string[]} */ + const errors = []; + /** @param {ErrorEvent} event */ + const on_error = (event) => { + errors.push(event.message); + event.preventDefault(); + }; + window.addEventListener('error', on_error); + + const button = target.querySelector('button'); + ok(button); + + button.focus(); + button.click(); + await new Promise((r) => setTimeout(r, 0)); + + window.removeEventListener('error', on_error); + + assert.equal(errors.length, 0, errors.join('\n')); + assert.equal(target.querySelector('button'), null); + } +}); diff --git a/packages/svelte/tests/runtime-browser/samples/bind-active-element-remove-focused/main.svelte b/packages/svelte/tests/runtime-browser/samples/bind-active-element-remove-focused/main.svelte new file mode 100644 index 0000000000..93e6e8f308 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/samples/bind-active-element-remove-focused/main.svelte @@ -0,0 +1,10 @@ + + + + +{#if show} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-destroyed-before-blockers-settle/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-destroyed-before-blockers-settle/Child.svelte new file mode 100644 index 0000000000..63a16dde43 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-destroyed-before-blockers-settle/Child.svelte @@ -0,0 +1,7 @@ + + +

{await Promise.resolve(value)}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-destroyed-before-blockers-settle/_config.js b/packages/svelte/tests/runtime-runes/samples/async-destroyed-before-blockers-settle/_config.js new file mode 100644 index 0000000000..096f90e2df --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-destroyed-before-blockers-settle/_config.js @@ -0,0 +1,31 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [hide, resolve] = target.querySelectorAll('button'); + + assert.htmlEqual( + target.innerHTML, + ` + + +

loading...

+ ` + ); + + hide.click(); + await tick(); + + resolve.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-destroyed-before-blockers-settle/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-destroyed-before-blockers-settle/main.svelte new file mode 100644 index 0000000000..3bc50e5d24 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-destroyed-before-blockers-settle/main.svelte @@ -0,0 +1,20 @@ + + + + + + + {#if show} + + {/if} + + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-event-handler-blockers/_config.js b/packages/svelte/tests/runtime-runes/samples/async-event-handler-blockers/_config.js new file mode 100644 index 0000000000..f4dcbef823 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-event-handler-blockers/_config.js @@ -0,0 +1,19 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'hydrate'], + async test({ assert, target, window, logs }) { + await new Promise((r) => setTimeout(r)); + await tick(); + + const button = target.querySelector('button'); + const div = target.querySelector('div'); + + button?.click(); + div?.dispatchEvent(new window.MouseEvent('mouseenter')); + window.dispatchEvent(new window.Event('resize')); + + assert.deepEqual(logs, ['click', 'mouseenter', 'resize']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-event-handler-blockers/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-event-handler-blockers/main.svelte new file mode 100644 index 0000000000..16bbac14a8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-event-handler-blockers/main.svelte @@ -0,0 +1,19 @@ + + + + + +
hover
diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-discard-after-in-check/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-discard-after-in-check/_config.js new file mode 100644 index 0000000000..cf3cd1b1a3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-discard-after-in-check/_config.js @@ -0,0 +1,18 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + await tick(); + + const [rerun, discard] = target.querySelectorAll('button'); + + rerun.click(); + await tick(); + + discard.click(); + await tick(); + + assert.deepEqual(logs, [1, false, 1, 1, 1]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-discard-after-in-check/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-discard-after-in-check/main.svelte new file mode 100644 index 0000000000..95e0bf09f4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-discard-after-in-check/main.svelte @@ -0,0 +1,49 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/deferred-outro-restored/_config.js b/packages/svelte/tests/runtime-runes/samples/deferred-outro-restored/_config.js new file mode 100644 index 0000000000..367da37815 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/deferred-outro-restored/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [button] = target.querySelectorAll('button'); + + button.click(); + flushSync(); + + assert.htmlEqual(target.innerHTML, '

Deferred outro

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/deferred-outro-restored/main.svelte b/packages/svelte/tests/runtime-runes/samples/deferred-outro-restored/main.svelte new file mode 100644 index 0000000000..396a8b2528 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/deferred-outro-restored/main.svelte @@ -0,0 +1,24 @@ + + + + +{#if visible} +

Deferred outro

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-destroyed/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-destroyed/_config.js new file mode 100644 index 0000000000..01d57158bf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-destroyed/_config.js @@ -0,0 +1,50 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +/** @type {Array<() => void>} */ +const resolvers = []; + +export default test({ + transformError: (error) => new Promise((resolve) => resolvers.push(() => resolve(error))), + + async test({ assert, target, logs }) { + const [error, toggle, reset, destroy] = target.querySelectorAll('button'); + const paragraph = /** @type {HTMLParagraphElement} */ (target.querySelector('p')); + + error.click(); + await tick(); + resolvers.shift()?.(); + await tick(); + assert.htmlEqual(paragraph.innerHTML, 'boom'); + + // A retained reset is inert after its boundary has been destroyed + toggle.click(); + await tick(); + reset.click(); + await tick(); + assert.htmlEqual(paragraph.innerHTML, 'boom'); + + // Resolving an error transform cannot resume a destroyed boundary + toggle.click(); + await tick(); + error.click(); + await tick(); + toggle.click(); + await tick(); + resolvers.shift()?.(); + await tick(); + assert.htmlEqual(paragraph.innerHTML, 'boom'); + + // A failed snippet's reset is also inert while the boundary is being destroyed + toggle.click(); + await tick(); + error.click(); + await tick(); + resolvers.shift()?.(); + await tick(); + destroy.click(); + await tick(); + assert.htmlEqual(paragraph.innerHTML, 'boom,boom'); + assert.deepEqual(logs, ['render', 'render', 'render']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-destroyed/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-destroyed/main.svelte new file mode 100644 index 0000000000..e8a1db83d8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-reset-destroyed/main.svelte @@ -0,0 +1,47 @@ + + + + + + +

{errors.join(',')}

+ +{#if show} + { errors.push(error.message); reset = fn; }}> + {track_render()} + + {must_throw ? throw_error() : ''} + + {#snippet failed(_, failed_reset)} + + {/snippet} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/hydration-custom-element-error-boundary/_config.js b/packages/svelte/tests/runtime-runes/samples/hydration-custom-element-error-boundary/_config.js new file mode 100644 index 0000000000..dd20992c26 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/hydration-custom-element-error-boundary/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['hydrate'], + + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

failed: setter error

after

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/hydration-custom-element-error-boundary/main.svelte b/packages/svelte/tests/runtime-runes/samples/hydration-custom-element-error-boundary/main.svelte new file mode 100644 index 0000000000..a47217d7a9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/hydration-custom-element-error-boundary/main.svelte @@ -0,0 +1,22 @@ + + + + + + {#snippet failed(error)} +

failed: {error.message}

+ {/snippet} +
+ +

after

diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 285dc1b3ce..b83e75a767 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -15,7 +15,7 @@ import { proxy } from '../../src/internal/client/proxy'; import { derived } from '../../src/internal/client/reactivity/deriveds'; import { snapshot } from '../../src/internal/shared/clone.js'; import { SvelteSet } from '../../src/reactivity/set'; -import { DESTROYED } from '../../src/internal/client/constants'; +import { CONNECTED, DESTROYED } from '../../src/internal/client/constants'; import { noop } from 'svelte/internal/client'; import { disable_async_mode_flag, enable_async_mode_flag } from '../../src/internal/flags'; @@ -153,6 +153,46 @@ describe('signals', () => { }; }); + test('unowned deriveds track missing property checks on proxies', () => { + const value = proxy>({}); + + return () => { + const has_x = derived(() => 'x' in value); + const owns_y = derived(() => Object.hasOwn(value, 'y')); + + assert.isFalse($.get(has_x)); + assert.isFalse($.get(owns_y)); + + value.x = true; + value.y = true; + + assert.isTrue($.get(has_x)); + assert.isTrue($.get(owns_y)); + + delete value.x; + delete value.y; + + assert.isFalse($.get(has_x)); + assert.isFalse($.get(owns_y)); + }; + }); + + test('unowned deriveds track missing array indexes', () => { + const value = proxy([]); + + return () => { + const has_first = derived(() => 0 in value); + + assert.isFalse($.get(has_first)); + + value.push(true); + assert.isTrue($.get(has_first)); + + value.length = 0; + assert.isFalse($.get(has_first)); + }; + }); + test('derived from state', () => { const log: number[] = []; @@ -1516,12 +1556,137 @@ describe('signals', () => { destroy(); - // a was spuriously added to s.reactions via is_updating_effect + // a was spuriously added to s.reactions // even though the entire derived chain was read in an untracked context assert.equal(s.reactions, null); }; }); + test('untracked derived reads inside effects do not reconnect disconnected dependencies', () => { + return () => { + const source = state({ n: 1, items: [1] }); + const data = derived(() => $.get(source)); + const items = derived(() => $.get(data).items); + const count = derived(() => Math.max(1, $.get(items).length)); + const snapshot = derived(() => ({ n: $.get(data).n, count: $.get(count) })); + const show = state(true); + const trigger = state(0); + let rendered = -1; + let seen: { n: number; count: number } | undefined; + + const destroy = effect_root(() => { + render_effect(() => { + if ($.get(show)) { + render_effect(() => { + rendered = $.get(snapshot).count; + }); + } + }); + + render_effect(() => { + $.get(trigger); + seen = $.untrack(() => $.get(snapshot)); + }); + }); + + flushSync(); + assert.equal(rendered, 1); + + flushSync(() => set(show, false)); + assert.equal(source.reactions, null); + + flushSync(() => set(source, { n: 2, items: [1, 2] })); + flushSync(() => set(trigger, 1)); + + assert.deepEqual(seen, { n: 2, count: 2 }); + assert.equal(source.reactions, null); + assert.equal(items.reactions, null); + assert.equal(count.reactions, null); + assert.equal(items.f & CONNECTED, 0); + assert.equal(count.f & CONNECTED, 0); + + flushSync(() => set(show, true)); + assert.equal(rendered, 2); + assert.equal(source.reactions?.length, 1); + + flushSync(() => set(source, { n: 3, items: [1] })); + assert.equal(rendered, 1); + + destroy(); + flushSync(); + assert.equal(source.reactions, null); + }; + }); + + test('reconnecting deriveds does not duplicate subscriptions', () => { + const enabled = state(false); + const shared = state(1); + let value!: Derived; + + const destroy_owner = effect_root(() => { + value = derived(() => ($.get(enabled) ? $.get(shared) : 0)); + $.untrack(() => $.get(value)); + }); + + const nested_enabled = state(false); + const nested_shared = state(1); + let inner!: Derived; + let outer!: Derived; + + const destroy_nested_owner = effect_root(() => { + inner = derived(() => ($.get(nested_enabled) ? $.get(nested_shared) : 0)); + outer = derived(() => $.get(inner)); + $.untrack(() => $.get(outer)); + }); + + return () => { + flushSync(() => set(enabled, true)); + const destroy_reader = effect_root(() => { + render_effect(() => { + $.get(value); + }); + }); + + const enabled_subscriptions = enabled.reactions?.length; + const shared_subscriptions = shared.reactions?.length; + const enabled_reaction = enabled.reactions?.[0]; + const shared_reaction = shared.reactions?.[0]; + + destroy_reader(); + destroy_owner(); + + assert.equal(enabled_subscriptions, 1); + assert.equal(shared_subscriptions, 1); + assert.equal(enabled_reaction, shared_reaction); + assert.equal(enabled.reactions, null); + assert.equal(shared.reactions, null); + + flushSync(() => set(nested_enabled, true)); + const destroy_nested_reader = effect_root(() => { + render_effect(() => { + $.get(outer); + }); + }); + + const nested_enabled_subscriptions = nested_enabled.reactions?.length; + const nested_shared_subscriptions = nested_shared.reactions?.length; + const nested_enabled_reaction = nested_enabled.reactions?.[0] as Derived | undefined; + const nested_shared_reaction = nested_shared.reactions?.[0]; + const inner_subscriptions = nested_enabled_reaction?.reactions?.length; + + destroy_nested_reader(); + destroy_nested_owner(); + + assert.equal(nested_enabled_subscriptions, 1); + assert.equal(nested_shared_subscriptions, 1); + assert.equal(inner_subscriptions, 1); + assert.equal(nested_enabled_reaction, nested_shared_reaction); + assert.equal(nested_enabled.reactions, null); + assert.equal(nested_shared.reactions, null); + assert.equal(nested_enabled_reaction?.reactions, null); + }; + }); + // https://github.com/sveltejs/svelte/issues/18414 test('a reaction that throws after first-reading a fresh derived does not leak it', () => { const src = state(0); diff --git a/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/input.svelte b/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/input.svelte index 9e06ebe420..68965fb5f4 100644 --- a/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/input.svelte +++ b/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/input.svelte @@ -43,6 +43,11 @@
+ +foo + + + diff --git a/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/warnings.json b/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/warnings.json index e041037d71..8e94fef00a 100644 --- a/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/warnings.json +++ b/packages/svelte/tests/validator/samples/a11y-no-redundant-roles/warnings.json @@ -454,5 +454,17 @@ "column": 8, "line": 44 } + }, + { + "code": "a11y_no_redundant_roles", + "end": { + "column": 26, + "line": 47 + }, + "message": "Redundant role 'link'", + "start": { + "column": 15, + "line": 47 + } } ] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-snippet-parameter/errors.json b/packages/svelte/tests/validator/samples/declaration-tag-snippet-parameter/errors.json new file mode 100644 index 0000000000..45e51d5f01 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-snippet-parameter/errors.json @@ -0,0 +1,14 @@ +[ + { + "code": "declaration_duplicate", + "message": "`item` has already been declared", + "start": { + "line": 13, + "column": 8 + }, + "end": { + "line": 13, + "column": 12 + } + } +] diff --git a/packages/svelte/tests/validator/samples/declaration-tag-snippet-parameter/input.svelte b/packages/svelte/tests/validator/samples/declaration-tag-snippet-parameter/input.svelte new file mode 100644 index 0000000000..b59496dda1 --- /dev/null +++ b/packages/svelte/tests/validator/samples/declaration-tag-snippet-parameter/input.svelte @@ -0,0 +1,17 @@ + +{#snippet row1(item)} +{#if item} +{const item = 1} +

{item}

+{/if} +{/snippet} + +{@render row1(5)} + + +{#snippet row2(item)} + {const item = 1} +

{item}

+{/snippet} + +{@render row2(5)}