From 8691391c0892f03731bc55e84a6d78ad56065871 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Thu, 16 Oct 2025 17:02:55 +0200 Subject: [PATCH 01/58] fix: add hydration markers in `pending` branch of SSR boundary (#16965) --- .changeset/happy-numbers-stick.md | 5 +++++ .../3-transform/server/visitors/SvelteBoundary.js | 2 +- .../samples/boundary-pending-attribute/_config.js | 10 ++++++++++ .../samples/boundary-pending-attribute/_expected.html | 1 + .../samples/boundary-pending-attribute/main.svelte | 8 ++++++++ 5 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 .changeset/happy-numbers-stick.md create mode 100644 packages/svelte/tests/hydration/samples/boundary-pending-attribute/_config.js create mode 100644 packages/svelte/tests/hydration/samples/boundary-pending-attribute/_expected.html create mode 100644 packages/svelte/tests/hydration/samples/boundary-pending-attribute/main.svelte diff --git a/.changeset/happy-numbers-stick.md b/.changeset/happy-numbers-stick.md new file mode 100644 index 0000000000..12fbd68ad8 --- /dev/null +++ b/.changeset/happy-numbers-stick.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add hydration markers in `pending` branch of SSR boundary diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js index 41a18cf52d..45f1b5aad2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js @@ -49,7 +49,7 @@ export function SvelteBoundary(node, context) { context.state.template.push( b.if( callee, - b.block([b.stmt(pending)]), + b.block(build_template([block_open_else, b.stmt(pending), block_close])), b.block(build_template([block_open, statement, block_close])) ) ); diff --git a/packages/svelte/tests/hydration/samples/boundary-pending-attribute/_config.js b/packages/svelte/tests/hydration/samples/boundary-pending-attribute/_config.js new file mode 100644 index 0000000000..eec39eb84c --- /dev/null +++ b/packages/svelte/tests/hydration/samples/boundary-pending-attribute/_config.js @@ -0,0 +1,10 @@ +import { flushSync } from 'svelte'; +import { assert_ok, test } from '../../test'; + +export default test({ + compileOptions: { + experimental: { + async: true + } + } +}); diff --git a/packages/svelte/tests/hydration/samples/boundary-pending-attribute/_expected.html b/packages/svelte/tests/hydration/samples/boundary-pending-attribute/_expected.html new file mode 100644 index 0000000000..8b44c4cc91 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/boundary-pending-attribute/_expected.html @@ -0,0 +1 @@ +loading... \ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/boundary-pending-attribute/main.svelte b/packages/svelte/tests/hydration/samples/boundary-pending-attribute/main.svelte new file mode 100644 index 0000000000..15612dd27e --- /dev/null +++ b/packages/svelte/tests/hydration/samples/boundary-pending-attribute/main.svelte @@ -0,0 +1,8 @@ +{#snippet pending()} + loading... +{/snippet} + + + {@const data = await Promise.resolve("data")} + {data} + \ No newline at end of file From a54186242a584fd43ee9b58a91c5d9fef9da6837 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 21:28:31 -0400 Subject: [PATCH 02/58] Version Packages (#16967) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/happy-numbers-stick.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/happy-numbers-stick.md diff --git a/.changeset/happy-numbers-stick.md b/.changeset/happy-numbers-stick.md deleted file mode 100644 index 12fbd68ad8..0000000000 --- a/.changeset/happy-numbers-stick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: add hydration markers in `pending` branch of SSR boundary diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index a779431a29..5c8a5e5b58 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.40.2 + +### Patch Changes + +- fix: add hydration markers in `pending` branch of SSR boundary ([#16965](https://github.com/sveltejs/svelte/pull/16965)) + ## 5.40.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 0868714f6f..d99ddb502d 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.40.1", + "version": "5.40.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 565f08a9d0..021668f1e6 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.40.1'; +export const VERSION = '5.40.2'; export const PUBLIC_VERSION = '5'; From 7d977fad88c5199773fa0dc234c61440922575c3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 16 Oct 2025 21:29:06 -0400 Subject: [PATCH 03/58] chore: run boundary async effects in the context of the current batch (#16968) --- .changeset/selfish-pets-teach.md | 5 ++++ .../src/internal/client/reactivity/batch.js | 26 +++---------------- 2 files changed, 9 insertions(+), 22 deletions(-) create mode 100644 .changeset/selfish-pets-teach.md diff --git a/.changeset/selfish-pets-teach.md b/.changeset/selfish-pets-teach.md new file mode 100644 index 0000000000..d78fea8f9f --- /dev/null +++ b/.changeset/selfish-pets-teach.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: run boundary async effects in the context of the current batch diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 2956e7ed6a..fd2a6d9f5d 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -97,13 +97,6 @@ export class Batch { */ #deferred = null; - /** - * Async effects inside a newly-created `` - * — these do not prevent the batch from committing - * @type {Effect[]} - */ - #boundary_async_effects = []; - /** * Template effects and `$effect.pre` effects, which run when * a batch is committed @@ -158,8 +151,7 @@ export class Batch { this.#traverse_effect_tree(root); } - // if we didn't start any new async work, and no async work - // is outstanding from a previous flush, commit + // if there is no outstanding async work, commit if (this.#pending === 0) { // TODO we need this because we commit _then_ flush effects... // maybe there's a way we can reverse the order? @@ -193,12 +185,6 @@ export class Batch { } batch_values = null; - - for (const effect of this.#boundary_async_effects) { - update_effect(effect); - } - - this.#boundary_async_effects = []; } /** @@ -225,13 +211,9 @@ export class Batch { this.#effects.push(effect); } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) { this.#render_effects.push(effect); - } else if ((flags & CLEAN) === 0) { - if ((flags & ASYNC) !== 0 && effect.b?.is_pending()) { - this.#boundary_async_effects.push(effect); - } else if (is_dirty(effect)) { - if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect); - update_effect(effect); - } + } else if (is_dirty(effect)) { + if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect); + update_effect(effect); } var child = effect.first; From 9b5fb3f430d437ac94f752da87cff6d35dc9555c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 06:56:04 -0400 Subject: [PATCH 04/58] fix: error if `each` block has `key` but no `as` clause (#16966) --- .changeset/thirty-rules-dance.md | 5 +++++ .../docs/98-reference/.generated/compile-errors.md | 8 +++++++- packages/svelte/messages/compile-errors/template.md | 6 +++++- packages/svelte/src/compiler/errors.js | 13 +++++++++++-- .../compiler/phases/2-analyze/visitors/EachBlock.js | 4 ++++ .../_config.js | 2 +- .../_config.js | 2 +- .../samples/each-key-without-as/_config.js | 8 ++++++++ .../samples/each-key-without-as/main.svelte | 7 +++++++ 9 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 .changeset/thirty-rules-dance.md create mode 100644 packages/svelte/tests/compiler-errors/samples/each-key-without-as/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/each-key-without-as/main.svelte diff --git a/.changeset/thirty-rules-dance.md b/.changeset/thirty-rules-dance.md new file mode 100644 index 0000000000..7fcf8d63d0 --- /dev/null +++ b/.changeset/thirty-rules-dance.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: error if `each` block has `key` but no `as` clause diff --git a/documentation/docs/98-reference/.generated/compile-errors.md b/documentation/docs/98-reference/.generated/compile-errors.md index b9c44163c9..c5703c636b 100644 --- a/documentation/docs/98-reference/.generated/compile-errors.md +++ b/documentation/docs/98-reference/.generated/compile-errors.md @@ -199,7 +199,7 @@ Cyclical dependency detected: %cycle% ### const_tag_invalid_reference ``` -The `{@const %name% = ...}` declaration is not available in this snippet +The `{@const %name% = ...}` declaration is not available in this snippet ``` The following is an error: @@ -453,6 +453,12 @@ This turned out to be buggy and unpredictable, particularly when working with de {/each} ``` +### each_key_without_as + +``` +An `{#each ...}` block without an `as` clause cannot have a key +``` + ### effect_invalid_placement ``` diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md index dc26a02767..ac95bfe4a7 100644 --- a/packages/svelte/messages/compile-errors/template.md +++ b/packages/svelte/messages/compile-errors/template.md @@ -126,7 +126,7 @@ ## const_tag_invalid_reference -> The `{@const %name% = ...}` declaration is not available in this snippet +> The `{@const %name% = ...}` declaration is not available in this snippet The following is an error: @@ -179,6 +179,10 @@ The same applies to components: > `%type%` name cannot be empty +## each_key_without_as + +> An `{#each ...}` block without an `as` clause cannot have a key + ## element_invalid_closing_tag > `` attempted to close an element that was not open diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 44fc641ee5..5e3968215f 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -986,13 +986,13 @@ export function const_tag_invalid_placement(node) { } /** - * The `{@const %name% = ...}` declaration is not available in this snippet + * The `{@const %name% = ...}` declaration is not available in this snippet * @param {null | number | NodeLike} node * @param {string} name * @returns {never} */ export function const_tag_invalid_reference(node, name) { - e(node, 'const_tag_invalid_reference', `The \`{@const ${name} = ...}\` declaration is not available in this snippet \nhttps://svelte.dev/e/const_tag_invalid_reference`); + e(node, 'const_tag_invalid_reference', `The \`{@const ${name} = ...}\` declaration is not available in this snippet\nhttps://svelte.dev/e/const_tag_invalid_reference`); } /** @@ -1023,6 +1023,15 @@ export function directive_missing_name(node, type) { e(node, 'directive_missing_name', `\`${type}\` name cannot be empty\nhttps://svelte.dev/e/directive_missing_name`); } +/** + * An `{#each ...}` block without an `as` clause cannot have a key + * @param {null | number | NodeLike} node + * @returns {never} + */ +export function each_key_without_as(node) { + e(node, 'each_key_without_as', `An \`{#each ...}\` block without an \`as\` clause cannot have a key\nhttps://svelte.dev/e/each_key_without_as`); +} + /** * `` attempted to close an element that was not open * @param {null | number | NodeLike} node diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js index e6a83921b1..d3eb58053e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js @@ -28,6 +28,10 @@ export function EachBlock(node, context) { node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index; } + if (node.metadata.keyed && !node.context) { + e.each_key_without_as(node); + } + // evaluate expression in parent scope context.visit(node.expression, { ...context.state, diff --git a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/_config.js b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/_config.js index 7424278180..be9d5a483f 100644 --- a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/_config.js +++ b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-1/_config.js @@ -4,7 +4,7 @@ export default test({ async: true, error: { code: 'const_tag_invalid_reference', - message: 'The `{@const foo = ...}` declaration is not available in this snippet ', + message: 'The `{@const foo = ...}` declaration is not available in this snippet', position: [376, 379] } }); diff --git a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js index 7ff71a61f9..5132bd93b7 100644 --- a/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js +++ b/packages/svelte/tests/compiler-errors/samples/const-tag-snippet-invalid-reference-2/_config.js @@ -4,7 +4,7 @@ export default test({ async: true, error: { code: 'const_tag_invalid_reference', - message: 'The `{@const foo = ...}` declaration is not available in this snippet ', + message: 'The `{@const foo = ...}` declaration is not available in this snippet', position: [298, 301] } }); diff --git a/packages/svelte/tests/compiler-errors/samples/each-key-without-as/_config.js b/packages/svelte/tests/compiler-errors/samples/each-key-without-as/_config.js new file mode 100644 index 0000000000..923fe0c0ac --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/each-key-without-as/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'each_key_without_as', + message: 'An `{#each ...}` block without an `as` clause cannot have a key' + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/each-key-without-as/main.svelte b/packages/svelte/tests/compiler-errors/samples/each-key-without-as/main.svelte new file mode 100644 index 0000000000..794740de8f --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/each-key-without-as/main.svelte @@ -0,0 +1,7 @@ + + +{#each items, i (items[i].id)} +

{items[i].id}

+{/each} From ee093e4c86a2db0f2ce636d86856fbe0e5540070 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 13:12:31 -0400 Subject: [PATCH 05/58] fix: preserve `` state while focused diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/select.js b/packages/svelte/src/internal/client/dom/elements/bindings/select.js index e39fb865cd..46e8f524f8 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/select.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/select.js @@ -3,6 +3,7 @@ import { listen_to_event_and_reset_event } from './shared.js'; import { is } from '../../../proxy.js'; import { is_array } from '../../../../shared/utils.js'; import * as w from '../../../warnings.js'; +import { Batch, current_batch, previous_batch } from '../../../reactivity/batch.js'; /** * Selects the correct option(s) (depending on whether this is a multiple select) @@ -83,6 +84,7 @@ export function init_select(select) { * @returns {void} */ export function bind_select_value(select, get, set = get) { + var batches = new WeakSet(); var mounting = true; listen_to_event_and_reset_event(select, 'change', (is_reset) => { @@ -102,11 +104,30 @@ export function bind_select_value(select, get, set = get) { } set(value); + + if (current_batch !== null) { + batches.add(current_batch); + } }); // Needs to be an effect, not a render_effect, so that in case of each loops the logic runs after the each block has updated effect(() => { var value = get(); + + if (select === document.activeElement) { + // we need both, because in non-async mode, render effects run before previous_batch is set + var batch = /** @type {Batch} */ (previous_batch ?? current_batch); + + // Don't update the ... + //

{await find(selected)}

+ if (batches.has(batch)) { + return; + } + } + select_option(select, value, mounting); // Mounting and value undefined -> take selection from dom diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js index b0772ad3c0..76a2032c7a 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/_config.js @@ -2,8 +2,9 @@ import { tick } from 'svelte'; import { test } from '../../test'; export default test({ - async test({ assert, target, instance }) { - instance.shift(); + async test({ assert, target }) { + const [shift] = target.querySelectorAll('button'); + shift.click(); await tick(); const [input] = target.querySelectorAll('input'); @@ -13,7 +14,7 @@ export default test({ input.dispatchEvent(new InputEvent('input', { bubbles: true })); await tick(); - assert.htmlEqual(target.innerHTML, `

0

`); + assert.htmlEqual(target.innerHTML, `

0

`); assert.equal(input.value, '1'); input.focus(); @@ -21,17 +22,17 @@ export default test({ input.dispatchEvent(new InputEvent('input', { bubbles: true })); await tick(); - assert.htmlEqual(target.innerHTML, `

0

`); + assert.htmlEqual(target.innerHTML, `

0

`); assert.equal(input.value, '2'); - instance.shift(); + shift.click(); await tick(); - assert.htmlEqual(target.innerHTML, `

1

`); + assert.htmlEqual(target.innerHTML, `

1

`); assert.equal(input.value, '2'); - instance.shift(); + shift.click(); await tick(); - assert.htmlEqual(target.innerHTML, `

2

`); + assert.htmlEqual(target.innerHTML, `

2

`); assert.equal(input.value, '2'); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte index 2fc898e654..e2f01a66c8 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte @@ -1,22 +1,23 @@ + + - +

{await push(count)}

{#snippet pending()} diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/_config.js b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/_config.js new file mode 100644 index 0000000000..7fddca0d58 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/_config.js @@ -0,0 +1,82 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [shift] = target.querySelectorAll('button'); + shift.click(); + await tick(); + + const [select] = target.querySelectorAll('select'); + + select.focus(); + select.value = 'three'; + select.dispatchEvent(new InputEvent('change', { bubbles: true })); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

two

+ ` + ); + assert.equal(select.value, 'three'); + + select.focus(); + select.value = 'one'; + select.dispatchEvent(new InputEvent('change', { bubbles: true })); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

two

+ ` + ); + assert.equal(select.value, 'one'); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

three

+ ` + ); + assert.equal(select.value, 'one'); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + +

one

+ ` + ); + assert.equal(select.value, 'one'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/main.svelte new file mode 100644 index 0000000000..566ea60ec5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/main.svelte @@ -0,0 +1,31 @@ + + + + + + + +

{await push(selected)}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
From 53f2693b3b501c0bc0b8c129fa3e6a5d70903050 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 17 Oct 2025 22:59:50 -0400 Subject: [PATCH 06/58] feat: `$state.eager(value)` (#16849) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WIP implement `$effect.pending(...)` * feat: `$state.eager(value)` (#16926) * runtime-first approach * revert these * type safety, lint * fix: better input cursor restoration for `bind:value` (#16925) If cursor was at end and new input is longer, move cursor to new end No test because not possible to reproduce using our test setup. Follow-up to #14649, helps with #16577 * Version Packages (#16920) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * docs: await no longer need pending (#16900) * docs: link to custom renderer issue in Svelte Native discussion (#16896) * fix code block (#16937) Updated code block syntax from Svelte to JavaScript for clarity. * fix: unset context on stale promises (#16935) * fix: unset context on stale promises When a stale promise is rejected in `async_derived`, and the promise eventually resolves, `d.resolve` will be noop and `d.promise.then(handler, ...)` will never run. That in turns means any restored context (via `(await save(..))()`) will never be unset. We have to handle this case and unset the context to prevent errors such as false-positive state mutation errors * fix: unset context on stale promises (slightly different approach) (#16936) * slightly different approach to #16935 * move unset_context call * get rid of logs --------- Co-authored-by: Rich Harris * fix: svg `radialGradient` `fr` attribute missing in types (#16943) * fix(svg radialGradient): fr attribute missing in types * chore: add changeset * Version Packages (#16940) * Version Packages * Update packages/svelte/CHANGELOG.md --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Rich Harris * chore: simplify `batch.apply()` (#16945) * chore: simplify `batch.apply()` * belt and braces * note to self * unused * fix: don't rerun async effects unnecessarily (#16944) Since #16866, when an async effect runs multiple times, we rebase older batches and rerun those effects. This can have unintended consequences: In a case where an async effect only depends on a single source, and that single source was updated in a later batch, we know that we don't need to / should not rerun the older batch. This PR makes it so: We collect all the sources of older batches that are not part of the current batch that just committed, and then only mark those async effects as dirty which depend on one of those other sources. Fixes the bug I noticed while working on #16935 * fix: ensure map iteration order is correct (#16947) quick follow-up to #16944 Resetting a map entry does not change its position in the map when iterating. We need to make sure that reset makes that batch jump "to the front" for the "reject all stale batches" logic below. Edge case for which I can't come up with a test case but it _is_ a possibility. * feat: add `createContext` utility for type-safe context (#16948) * feat: add `createContext` utility for type-safe context * regenerate * Version Packages (#16946) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * chore: Remove annoying sync-async warning (#16949) * fix * use `$state.eager(value)` instead of `$effect.pending(value)` --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Hyunbin Seo <47051820+hyunbinseo@users.noreply.github.com> Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> Co-authored-by: Rich Harris Co-authored-by: Hannes Rüger Co-authored-by: Elliott Johnson * decouple from boundaries * use queue_micro_task * add test * fix * changeset * revert * tidy up * update docs * Update packages/svelte/src/internal/client/reactivity/batch.js Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> * minor tweak --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Hyunbin Seo <47051820+hyunbinseo@users.noreply.github.com> Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> Co-authored-by: Hannes Rüger Co-authored-by: Elliott Johnson --- .changeset/shy-boats-protect.md | 5 ++ documentation/docs/02-runes/02-$state.md | 15 +++++ packages/svelte/src/ambient.d.ts | 12 ++++ .../2-analyze/visitors/CallExpression.js | 7 ++ .../client/visitors/CallExpression.js | 6 ++ .../server/visitors/CallExpression.js | 4 ++ packages/svelte/src/internal/client/index.js | 2 +- .../src/internal/client/reactivity/batch.js | 64 ++++++++++++++++++- packages/svelte/src/utils.js | 1 + .../samples/async-state-eager/_config.js | 36 +++++++++++ .../samples/async-state-eager/main.svelte | 20 ++++++ packages/svelte/types/index.d.ts | 12 ++++ 12 files changed, 181 insertions(+), 3 deletions(-) create mode 100644 .changeset/shy-boats-protect.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-state-eager/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-state-eager/main.svelte diff --git a/.changeset/shy-boats-protect.md b/.changeset/shy-boats-protect.md new file mode 100644 index 0000000000..7efa8ebb31 --- /dev/null +++ b/.changeset/shy-boats-protect.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add `$state.eager(value)` rune diff --git a/documentation/docs/02-runes/02-$state.md b/documentation/docs/02-runes/02-$state.md index 741e24fde0..6fbf3b8895 100644 --- a/documentation/docs/02-runes/02-$state.md +++ b/documentation/docs/02-runes/02-$state.md @@ -166,6 +166,21 @@ To take a static snapshot of a deeply reactive `$state` proxy, use `$state.snaps This is handy when you want to pass some state to an external library or API that doesn't expect a proxy, such as `structuredClone`. +## `$state.eager` + +When state changes, it may not be reflected in the UI immediately if it is used by an `await` expression, because [updates are synchronized](await-expressions#Synchronized-updates). + +In some cases, you may want to update the UI as soon as the state changes. For example, you might want to update a navigation bar when the user clicks on a link, so that they get visual feedback while waiting for the new page to load. To do this, use `$state.eager(value)`: + +```svelte + +``` + +Use this feature sparingly, and only to provide feedback in response to user action — in general, allowing Svelte to coordinate updates will provide a better user experience. + ## Passing state into functions JavaScript is a _pass-by-value_ language — when you call a function, the arguments are the _values_ rather than the _variables_. In other words: diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index 1f1b0e7b5e..823dbde9a4 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -95,6 +95,18 @@ declare namespace $state { : never : never; + /** + * Returns the latest `value`, even if the rest of the UI is suspending + * while async work (such as data loading) completes. + * + * ```svelte + * + * ``` + */ + export function eager(value: T): T; /** * Declares state that is _not_ made deeply reactive — instead of mutating it, * you must reassign it. diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 53a89125a2..76d9cecd9a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -226,6 +226,13 @@ export function CallExpression(node, context) { break; } + case '$state.eager': + if (node.arguments.length !== 1) { + e.rune_invalid_arguments_length(node, rune, 'exactly one argument'); + } + + break; + case '$state.snapshot': if (node.arguments.length !== 1) { e.rune_invalid_arguments_length(node, rune, 'exactly one argument'); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index fcc385c2ba..bf9a09bb74 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -49,6 +49,12 @@ export function CallExpression(node, context) { return b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn); } + case '$state.eager': + return b.call( + '$.eager', + b.thunk(/** @type {Expression} */ (context.visit(node.arguments[0]))) + ); + case '$state.snapshot': return b.call( '$.snapshot', diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js index 41d3202ce9..d53b631aa5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js @@ -38,6 +38,10 @@ export function CallExpression(node, context) { return b.call('$.derived', rune === '$derived' ? b.thunk(fn) : fn); } + if (rune === '$state.eager') { + return node.arguments[0]; + } + if (rune === '$state.snapshot') { return b.call( '$.snapshot', diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 3c5409bcfe..471eed299d 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -103,7 +103,7 @@ export { save, track_reactivity_loss } from './reactivity/async.js'; -export { flushSync as flush } from './reactivity/batch.js'; +export { eager, flushSync as flush } from './reactivity/batch.js'; export { async_derived, user_derived as derived, diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index fd2a6d9f5d..b35e16a409 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -17,6 +17,7 @@ import { async_mode_flag } from '../../flags/index.js'; import { deferred, define_property } from '../../shared/utils.js'; import { active_effect, + get, is_dirty, is_updating_effect, set_is_updating_effect, @@ -27,8 +28,8 @@ import * as e from '../errors.js'; import { flush_tasks, queue_micro_task } from '../dom/task.js'; import { DEV } from 'esm-env'; import { invoke_error_boundary } from '../error-handling.js'; -import { old_values } from './sources.js'; -import { unlink_effect } from './effects.js'; +import { old_values, source, update } from './sources.js'; +import { inspect_effect, unlink_effect } from './effects.js'; /** @type {Set} */ const batches = new Set(); @@ -684,6 +685,65 @@ export function schedule_effect(signal) { queued_root_effects.push(effect); } +/** @type {Source[]} */ +let eager_versions = []; + +function eager_flush() { + try { + flushSync(() => { + for (const version of eager_versions) { + update(version); + } + }); + } finally { + eager_versions = []; + } +} + +/** + * Implementation of `$state.eager(fn())` + * @template T + * @param {() => T} fn + * @returns {T} + */ +export function eager(fn) { + var version = source(0); + var initial = true; + var value = /** @type {T} */ (undefined); + + get(version); + + inspect_effect(() => { + if (initial) { + // the first time this runs, we create an inspect effect + // that will run eagerly whenever the expression changes + var previous_batch_values = batch_values; + + try { + batch_values = null; + value = fn(); + } finally { + batch_values = previous_batch_values; + } + + return; + } + + // the second time this effect runs, it's to schedule a + // `version` update. since this will recreate the effect, + // we don't need to evaluate the expression here + if (eager_versions.length === 0) { + queue_micro_task(eager_flush); + } + + eager_versions.push(version); + }); + + initial = false; + + return value; +} + /** * Forcibly remove all current batches, to prevent cross-talk between tests */ diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index f8a7e8d46d..a54a421418 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -436,6 +436,7 @@ const STATE_CREATION_RUNES = /** @type {const} */ ([ const RUNES = /** @type {const} */ ([ ...STATE_CREATION_RUNES, + '$state.eager', '$state.snapshot', '$props', '$props.id', diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-eager/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-eager/_config.js new file mode 100644 index 0000000000..f84228ec14 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-eager/_config.js @@ -0,0 +1,36 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [count, shift] = target.querySelectorAll('button'); + + shift.click(); + await tick(); + assert.htmlEqual(target.innerHTML, `

0

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

0

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

0

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

0

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

1

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

2

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

3

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

{await push(count)}

+ + {#snippet pending()}{/snippet} +
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index f01edd947f..d260b738c3 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -3193,6 +3193,18 @@ declare namespace $state { : never : never; + /** + * Returns the latest `value`, even if the rest of the UI is suspending + * while async work (such as data loading) completes. + * + * ```svelte + * + * ``` + */ + export function eager(value: T): T; /** * Declares state that is _not_ made deeply reactive — instead of mutating it, * you must reassign it. From bd2d3db6d0d7a931c2e84c38a5c537e30dda1dbe Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Oct 2025 23:02:55 -0400 Subject: [PATCH 07/58] Version Packages (#16970) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/quiet-weeks-camp.md | 5 ----- .changeset/selfish-pets-teach.md | 5 ----- .changeset/shy-boats-protect.md | 5 ----- .changeset/thirty-rules-dance.md | 5 ----- packages/svelte/CHANGELOG.md | 14 ++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 7 files changed, 16 insertions(+), 22 deletions(-) delete mode 100644 .changeset/quiet-weeks-camp.md delete mode 100644 .changeset/selfish-pets-teach.md delete mode 100644 .changeset/shy-boats-protect.md delete mode 100644 .changeset/thirty-rules-dance.md diff --git a/.changeset/quiet-weeks-camp.md b/.changeset/quiet-weeks-camp.md deleted file mode 100644 index 9e04031535..0000000000 --- a/.changeset/quiet-weeks-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: preserve `` state while focused ([#16958](https://github.com/sveltejs/svelte/pull/16958)) + +- chore: run boundary async effects in the context of the current batch ([#16968](https://github.com/sveltejs/svelte/pull/16968)) + +- fix: error if `each` block has `key` but no `as` clause ([#16966](https://github.com/sveltejs/svelte/pull/16966)) + ## 5.40.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index d99ddb502d..6d8463a52a 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.40.2", + "version": "5.41.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 021668f1e6..e88bf0d0ea 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.40.2'; +export const VERSION = '5.41.0'; export const PUBLIC_VERSION = '5'; From c8ef54098584587777993a361cdb68f086433ca9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 19 Oct 2025 14:53:49 -0400 Subject: [PATCH 08/58] fix: improve `each_key_without_as` error (#16983) --- .changeset/three-wasps-work.md | 5 +++++ .../src/compiler/phases/2-analyze/visitors/EachBlock.js | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/three-wasps-work.md diff --git a/.changeset/three-wasps-work.md b/.changeset/three-wasps-work.md new file mode 100644 index 0000000000..318d8668a4 --- /dev/null +++ b/.changeset/three-wasps-work.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve `each_key_without_as` error diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js index d3eb58053e..81a9c1e2d1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js @@ -1,3 +1,4 @@ +/** @import { Expression } from 'estree' */ /** @import { AST, Binding } from '#compiler' */ /** @import { Context } from '../types' */ /** @import { Scope } from '../../scope' */ @@ -29,7 +30,7 @@ export function EachBlock(node, context) { } if (node.metadata.keyed && !node.context) { - e.each_key_without_as(node); + e.each_key_without_as(/** @type {Expression} */ (node.key)); } // evaluate expression in parent scope From 9a488d6b25d41d594ee73582e56aff123eec49e9 Mon Sep 17 00:00:00 2001 From: ComputerGuy <63362464+Ocean-OS@users.noreply.github.com> Date: Sun, 19 Oct 2025 18:35:38 -0700 Subject: [PATCH 09/58] fix: place `let:` declarations before `{@const}` declarations (#16985) * fix: place `let:` declarations before `{@const}` declarations * lint * fix --- .changeset/brown-insects-burn.md | 5 +++ .../3-transform/client/transform-client.js | 1 + .../phases/3-transform/client/types.d.ts | 2 + .../3-transform/client/visitors/Fragment.js | 3 +- .../client/visitors/LetDirective.js | 38 ++++++++++--------- .../client/visitors/RegularElement.js | 2 +- .../client/visitors/SlotElement.js | 2 +- .../client/visitors/SvelteFragment.js | 2 +- .../client/visitors/shared/component.js | 4 +- .../let-directive-and-const-tag/_config.js | 5 +++ .../component.svelte | 1 + .../let-directive-and-const-tag/main.svelte | 7 ++++ 12 files changed, 49 insertions(+), 23 deletions(-) create mode 100644 .changeset/brown-insects-burn.md create mode 100644 packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag/component.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag/main.svelte diff --git a/.changeset/brown-insects-burn.md b/.changeset/brown-insects-burn.md new file mode 100644 index 0000000000..ceccc3fd9b --- /dev/null +++ b/.changeset/brown-insects-burn.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: place `let:` declarations before `{@const}` declarations diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 2629379f63..cd3fb7a64d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -172,6 +172,7 @@ export function client_component(analysis, options) { // these are set inside the `Fragment` visitor, and cannot be used until then init: /** @type {any} */ (null), consts: /** @type {any} */ (null), + let_directives: /** @type {any} */ (null), update: /** @type {any} */ (null), after_update: /** @type {any} */ (null), template: /** @type {any} */ (null), diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 932d353671..b9a8691a6b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -54,6 +54,8 @@ export interface ComponentClientTransformState extends ClientTransformState { readonly after_update: Statement[]; /** Transformed `{@const }` declarations */ readonly consts: Statement[]; + /** Transformed `let:` directives */ + readonly let_directives: Statement[]; /** Memoized expressions */ readonly memoizer: Memoizer; /** The HTML template string */ diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index 85d8e3caff..bee4fcaab4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -63,6 +63,7 @@ export function Fragment(node, context) { ...context.state, init: [], consts: [], + let_directives: [], update: [], after_update: [], memoizer: new Memoizer(), @@ -150,7 +151,7 @@ export function Fragment(node, context) { } } - body.push(...state.consts); + body.push(...state.let_directives, ...state.consts); if (has_await) { body.push(b.if(b.call('$.aborted'), b.return())); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js index f33febeeb2..c134b4e1e7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js @@ -21,22 +21,24 @@ export function LetDirective(node, context) { }; } - return b.const( - name, - b.call( - '$.derived', - b.thunk( - b.block([ - b.let( - /** @type {Expression} */ (node.expression).type === 'ObjectExpression' - ? // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine - b.object_pattern(node.expression.properties) - : // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine - b.array_pattern(node.expression.elements), - b.member(b.id('$$slotProps'), node.name) - ), - b.return(b.object(bindings.map((binding) => b.init(binding.node.name, binding.node)))) - ]) + context.state.let_directives.push( + b.const( + name, + b.call( + '$.derived', + b.thunk( + b.block([ + b.let( + /** @type {Expression} */ (node.expression).type === 'ObjectExpression' + ? // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine + b.object_pattern(node.expression.properties) + : // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine + b.array_pattern(node.expression.elements), + b.member(b.id('$$slotProps'), node.name) + ), + b.return(b.object(bindings.map((binding) => b.init(binding.node.name, binding.node)))) + ]) + ) ) ) ); @@ -46,6 +48,8 @@ export function LetDirective(node, context) { read: (node) => b.call('$.get', node) }; - return b.const(name, create_derived(context.state, b.member(b.id('$$slotProps'), node.name))); + context.state.let_directives.push( + b.const(name, create_derived(context.state, b.member(b.id('$$slotProps'), node.name))) + ); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index e35b7cbe5a..ab119e8f80 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -106,7 +106,7 @@ export function RegularElement(node, context) { case 'LetDirective': // visit let directives before everything else, to set state - lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); + context.visit(attribute, { ...context.state, let_directives: lets }); break; case 'OnDirective': diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js index a5c0974738..b87a13253b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js @@ -49,7 +49,7 @@ export function SlotElement(node, context) { } } } else if (attribute.type === 'LetDirective') { - lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); + context.visit(attribute, { ...context.state, let_directives: lets }); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteFragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteFragment.js index 65cc170ce5..e3b46a4eef 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteFragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteFragment.js @@ -9,7 +9,7 @@ export function SvelteFragment(node, context) { for (const attribute of node.attributes) { if (attribute.type === 'LetDirective') { - context.state.init.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); + context.visit(attribute); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 5c8ce897f4..5ca941fd70 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -101,7 +101,7 @@ export function build_component(node, component_name, context) { if (slot_scope_applies_to_itself) { for (const attribute of node.attributes) { if (attribute.type === 'LetDirective') { - lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); + context.visit(attribute, { ...context.state, let_directives: lets }); } } } @@ -109,7 +109,7 @@ export function build_component(node, component_name, context) { for (const attribute of node.attributes) { if (attribute.type === 'LetDirective') { if (!slot_scope_applies_to_itself) { - lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute, states.default))); + context.visit(attribute, { ...states.default, let_directives: lets }); } } else if (attribute.type === 'OnDirective') { if (!attribute.expression) { diff --git a/packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag/_config.js b/packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag/_config.js new file mode 100644 index 0000000000..2f7a7863a7 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: 'foo' +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag/component.svelte b/packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag/component.svelte new file mode 100644 index 0000000000..44e700bdd4 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag/component.svelte @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag/main.svelte b/packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag/main.svelte new file mode 100644 index 0000000000..abca25bab2 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag/main.svelte @@ -0,0 +1,7 @@ + + + {@const thing = data} + {thing} + \ No newline at end of file From b8627e511ddfa9310e6952989c7f7de8ef035b0e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 20 Oct 2025 09:27:28 -0400 Subject: [PATCH 10/58] chore: centralise branch management (#16977) * WIP * WIP * WIP * WIP * WIP * fix hydration * simplify * all tests passing * key blocks * snippets * fix * tidy up * WIP await * tidy up * fix * neaten up * unused * tweak * elements * changeset * fix * preserve newer batches * add comment * add comment * no longer necessary apparently? * move legacy logic to key block --- .changeset/yellow-shrimps-provide.md | 5 + .../3-transform/client/transform-client.js | 4 +- .../3-transform/client/visitors/Fragment.js | 6 +- .../src/internal/client/dom/blocks/await.js | 208 ++++++------------ .../internal/client/dom/blocks/boundary.js | 26 +-- .../internal/client/dom/blocks/branches.js | 185 ++++++++++++++++ .../src/internal/client/dom/blocks/if.js | 135 +++--------- .../src/internal/client/dom/blocks/key.js | 70 +----- .../src/internal/client/dom/blocks/snippet.js | 28 +-- .../client/dom/blocks/svelte-component.js | 70 +----- .../client/dom/blocks/svelte-element.js | 79 +++---- .../src/internal/client/reactivity/async.js | 32 ++- .../src/internal/client/reactivity/effects.js | 22 +- .../samples/await-pending-destroy/_config.js | 3 +- 14 files changed, 409 insertions(+), 464 deletions(-) create mode 100644 .changeset/yellow-shrimps-provide.md create mode 100644 packages/svelte/src/internal/client/dom/blocks/branches.js diff --git a/.changeset/yellow-shrimps-provide.md b/.changeset/yellow-shrimps-provide.md new file mode 100644 index 0000000000..a29385660a --- /dev/null +++ b/.changeset/yellow-shrimps-provide.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: centralise branch management diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index cd3fb7a64d..0dd4ae03b9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -385,7 +385,9 @@ export function client_component(analysis, options) { .../** @type {ESTree.Statement[]} */ (template.body) ]); - component_block.body.push(b.stmt(b.call(`$.async_body`, b.arrow([], body, true)))); + component_block.body.push( + b.stmt(b.call(`$.async_body`, b.id('$$anchor'), b.arrow([b.id('$$anchor')], body, true))) + ); } else { component_block.body.push( ...state.instance_level_snippets, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index bee4fcaab4..8d6a2fac88 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -178,7 +178,11 @@ export function Fragment(node, context) { } if (has_await) { - return b.block([b.stmt(b.call('$.async_body', b.arrow([], b.block(body), true)))]); + return b.block([ + b.stmt( + b.call('$.async_body', b.id('$$anchor'), b.arrow([b.id('$$anchor')], b.block(body), true)) + ) + ]); } else { return b.block(body); } diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index e7917fbd9e..bac01e4c33 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -1,12 +1,9 @@ -/** @import { Effect, Source, TemplateNode } from '#client' */ -import { DEV } from 'esm-env'; +/** @import { Source, TemplateNode } from '#client' */ import { is_promise } from '../../../shared/utils.js'; -import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; +import { block } from '../../reactivity/effects.js'; import { internal_set, mutable_source, source } from '../../reactivity/sources.js'; -import { set_active_effect, set_active_reaction } from '../../runtime.js'; import { hydrate_next, - hydrate_node, hydrating, skip_nodes, set_hydrate_node, @@ -14,15 +11,10 @@ import { } from '../hydration.js'; import { queue_micro_task } from '../task.js'; import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; -import { - component_context, - dev_stack, - is_runes, - set_component_context, - set_dev_current_component_function, - set_dev_stack -} from '../../context.js'; +import { is_runes } from '../../context.js'; import { flushSync, is_flushing_sync } from '../../reactivity/batch.js'; +import { BranchManager } from './branches.js'; +import { capture, unset_context } from '../../reactivity/async.js'; const PENDING = 0; const THEN = 1; @@ -33,7 +25,7 @@ const CATCH = 2; /** * @template V * @param {TemplateNode} node - * @param {(() => Promise)} get_input + * @param {(() => any)} get_input * @param {null | ((anchor: Node) => void)} pending_fn * @param {null | ((anchor: Node, value: Source) => void)} then_fn * @param {null | ((anchor: Node, error: unknown) => void)} catch_fn @@ -44,149 +36,94 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { hydrate_next(); } - var anchor = node; var runes = is_runes(); - var active_component_context = component_context; - - /** @type {any} */ - var component_function = DEV ? component_context?.function : null; - var dev_original_stack = DEV ? dev_stack : null; - - /** @type {V | Promise | typeof UNINITIALIZED} */ - var input = UNINITIALIZED; - - /** @type {Effect | null} */ - var pending_effect; - - /** @type {Effect | null} */ - var then_effect; - - /** @type {Effect | null} */ - var catch_effect; - - var input_source = runes - ? source(/** @type {V} */ (undefined)) - : mutable_source(/** @type {V} */ (undefined), false, false); - var error_source = runes ? source(undefined) : mutable_source(undefined, false, false); - var resolved = false; - /** - * @param {AwaitState} state - * @param {boolean} restore - */ - function update(state, restore) { - resolved = true; - - if (restore) { - set_active_effect(effect); - set_active_reaction(effect); // TODO do we need both? - set_component_context(active_component_context); - if (DEV) { - set_dev_current_component_function(component_function); - set_dev_stack(dev_original_stack); - } - } - - try { - if (state === PENDING && pending_fn) { - if (pending_effect) resume_effect(pending_effect); - else pending_effect = branch(() => pending_fn(anchor)); - } - - if (state === THEN && then_fn) { - if (then_effect) resume_effect(then_effect); - else then_effect = branch(() => then_fn(anchor, input_source)); - } - - if (state === CATCH && catch_fn) { - if (catch_effect) resume_effect(catch_effect); - else catch_effect = branch(() => catch_fn(anchor, error_source)); - } - - if (state !== PENDING && pending_effect) { - pause_effect(pending_effect, () => (pending_effect = null)); - } - - if (state !== THEN && then_effect) { - pause_effect(then_effect, () => (then_effect = null)); - } - - if (state !== CATCH && catch_effect) { - pause_effect(catch_effect, () => (catch_effect = null)); - } - } finally { - if (restore) { - if (DEV) { - set_dev_current_component_function(null); - set_dev_stack(null); - } - set_component_context(null); - set_active_reaction(null); - set_active_effect(null); + var v = /** @type {V} */ (UNINITIALIZED); + var value = runes ? source(v) : mutable_source(v, false, false); + var error = runes ? source(v) : mutable_source(v, false, false); - // without this, the DOM does not update until two ticks after the promise - // resolves, which is unexpected behaviour (and somewhat irksome to test) - if (!is_flushing_sync) flushSync(); - } - } - } + var branches = new BranchManager(node); - var effect = block(() => { - if (input === (input = get_input())) return; + block(() => { + var input = get_input(); + var destroyed = false; /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ - // @ts-ignore coercing `anchor` to a `Comment` causes TypeScript and Prettier to fight - let mismatch = hydrating && is_promise(input) === (anchor.data === HYDRATION_START_ELSE); + // @ts-ignore coercing `node` to a `Comment` causes TypeScript and Prettier to fight + let mismatch = hydrating && is_promise(input) === (node.data === HYDRATION_START_ELSE); if (mismatch) { // Hydration mismatch: remove everything inside the anchor and start fresh - anchor = skip_nodes(); - - set_hydrate_node(anchor); + set_hydrate_node(skip_nodes()); set_hydrating(false); - mismatch = true; } if (is_promise(input)) { - var promise = input; + var restore = capture(); + var resolved = false; + + /** + * @param {() => void} fn + */ + const resolve = (fn) => { + if (destroyed) return; + + resolved = true; + restore(); + + if (hydrating) { + // `restore()` could set `hydrating` to `true`, which we very much + // don't want — we want to restore everything _except_ this + set_hydrating(false); + } - resolved = false; + try { + fn(); + } finally { + unset_context(); - promise.then( - (value) => { - if (promise !== input) return; - // we technically could use `set` here since it's on the next microtick - // but let's use internal_set for consistency and just to be safe - internal_set(input_source, value); - update(THEN, true); + // without this, the DOM does not update until two ticks after the promise + // resolves, which is unexpected behaviour (and somewhat irksome to test) + if (!is_flushing_sync) flushSync(); + } + }; + + input.then( + (v) => { + resolve(() => { + internal_set(value, v); + branches.ensure(THEN, then_fn && ((target) => then_fn(target, value))); + }); }, - (error) => { - if (promise !== input) return; - // we technically could use `set` here since it's on the next microtick - // but let's use internal_set for consistency and just to be safe - internal_set(error_source, error); - update(CATCH, true); - if (!catch_fn) { - // Rethrow the error if no catch block exists - throw error_source.v; - } + (e) => { + resolve(() => { + internal_set(error, e); + branches.ensure(THEN, catch_fn && ((target) => catch_fn(target, error))); + + if (!catch_fn) { + // Rethrow the error if no catch block exists + throw error.v; + } + }); } ); if (hydrating) { - if (pending_fn) { - pending_effect = branch(() => pending_fn(anchor)); - } + branches.ensure(PENDING, pending_fn); } else { // Wait a microtask before checking if we should show the pending state as - // the promise might have resolved by the next microtask. + // the promise might have resolved by then queue_micro_task(() => { - if (!resolved) update(PENDING, true); + if (!resolved) { + resolve(() => { + branches.ensure(PENDING, pending_fn); + }); + } }); } } else { - internal_set(input_source, input); - update(THEN, false); + internal_set(value, input); + branches.ensure(THEN, then_fn && ((target) => then_fn(target, value))); } if (mismatch) { @@ -194,11 +131,8 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { set_hydrating(true); } - // Set the input to something else, in order to disable the promise callbacks - return () => (input = UNINITIALIZED); + return () => { + destroyed = true; + }; }); - - if (hydrating) { - anchor = hydrate_node; - } } diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index ca6437e384..026ffb36fc 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -8,7 +8,13 @@ import { import { HYDRATION_START_ELSE } from '../../../../constants.js'; import { component_context, set_component_context } from '../../context.js'; import { handle_error, invoke_error_boundary } from '../../error-handling.js'; -import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js'; +import { + block, + branch, + destroy_effect, + move_effect, + pause_effect +} from '../../reactivity/effects.js'; import { active_effect, active_reaction, @@ -425,24 +431,6 @@ export class Boundary { } } -/** - * - * @param {Effect} effect - * @param {DocumentFragment} fragment - */ -function move_effect(effect, fragment) { - var node = effect.nodes_start; - var end = effect.nodes_end; - - while (node !== null) { - /** @type {TemplateNode | null} */ - var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node)); - - fragment.append(node); - node = next; - } -} - export function get_boundary() { return /** @type {Boundary} */ (/** @type {Effect} */ (active_effect).b); } diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js new file mode 100644 index 0000000000..827f9f44fa --- /dev/null +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -0,0 +1,185 @@ +/** @import { Effect, TemplateNode } from '#client' */ +import { is_runes } from '../../context.js'; +import { Batch, current_batch } from '../../reactivity/batch.js'; +import { + branch, + destroy_effect, + move_effect, + pause_effect, + resume_effect +} from '../../reactivity/effects.js'; +import { set_should_intro, should_intro } from '../../render.js'; +import { hydrate_node, hydrating } from '../hydration.js'; +import { create_text, should_defer_append } from '../operations.js'; + +/** + * @typedef {{ effect: Effect, fragment: DocumentFragment }} Branch + */ + +/** + * @template Key + */ +export class BranchManager { + /** @type {TemplateNode} */ + anchor; + + /** @type {Map} */ + #batches = new Map(); + + /** @type {Map} */ + #onscreen = new Map(); + + /** @type {Map} */ + #offscreen = new Map(); + + /** + * Whether to pause (i.e. outro) on change, or destroy immediately. + * This is necessary for `` + */ + #transition = true; + + /** + * @param {TemplateNode} anchor + * @param {boolean} transition + */ + constructor(anchor, transition = true) { + this.anchor = anchor; + this.#transition = transition; + } + + #commit = () => { + var batch = /** @type {Batch} */ (current_batch); + + // if this batch was made obsolete, bail + if (!this.#batches.has(batch)) return; + + var key = /** @type {Key} */ (this.#batches.get(batch)); + + var onscreen = this.#onscreen.get(key); + + if (onscreen) { + // effect is already in the DOM — abort any current outro + resume_effect(onscreen); + } else { + // effect is currently offscreen. put it in the DOM + var offscreen = this.#offscreen.get(key); + + if (offscreen) { + this.#onscreen.set(key, offscreen.effect); + this.#offscreen.delete(key); + + // remove the anchor... + /** @type {TemplateNode} */ (offscreen.fragment.lastChild).remove(); + + // ...and append the fragment + this.anchor.before(offscreen.fragment); + onscreen = offscreen.effect; + } + } + + for (const [b, k] of this.#batches) { + this.#batches.delete(b); + + if (b === batch) { + // keep values for newer batches + break; + } + + const offscreen = this.#offscreen.get(k); + + if (offscreen) { + // for older batches, destroy offscreen effects + // as they will never be committed + destroy_effect(offscreen.effect); + this.#offscreen.delete(k); + } + } + + // outro/destroy all onscreen effects... + for (const [k, effect] of this.#onscreen) { + // ...except the one that was just committed + if (k === key) continue; + + const on_destroy = () => { + const keys = Array.from(this.#batches.values()); + + if (keys.includes(k)) { + // keep the effect offscreen, as another batch will need it + var fragment = document.createDocumentFragment(); + move_effect(effect, fragment); + + fragment.append(create_text()); // TODO can we avoid this? + + this.#offscreen.set(k, { effect, fragment }); + } else { + destroy_effect(effect); + } + + this.#onscreen.delete(k); + }; + + if (this.#transition || !onscreen) { + pause_effect(effect, on_destroy, false); + } else { + on_destroy(); + } + } + }; + + /** + * + * @param {any} key + * @param {null | ((target: TemplateNode) => void)} fn + */ + ensure(key, fn) { + var batch = /** @type {Batch} */ (current_batch); + var defer = should_defer_append(); + + if (fn && !this.#onscreen.has(key) && !this.#offscreen.has(key)) { + if (defer) { + var fragment = document.createDocumentFragment(); + var target = create_text(); + + fragment.append(target); + + this.#offscreen.set(key, { + effect: branch(() => fn(target)), + fragment + }); + } else { + this.#onscreen.set( + key, + branch(() => fn(this.anchor)) + ); + } + } + + this.#batches.set(batch, key); + + if (defer) { + for (const [k, effect] of this.#onscreen) { + if (k === key) { + batch.skipped_effects.delete(effect); + } else { + batch.skipped_effects.add(effect); + } + } + + for (const [k, branch] of this.#offscreen) { + if (k === key) { + batch.skipped_effects.delete(branch.effect); + } else { + batch.skipped_effects.add(branch.effect); + } + } + + batch.add_callback(this.#commit); + } else { + if (hydrating) { + this.anchor = hydrate_node; + } + + this.#commit(); + } + } +} diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 6349ab8399..7fa5ca464d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -1,19 +1,16 @@ -/** @import { Effect, TemplateNode } from '#client' */ -/** @import { Batch } from '../../reactivity/batch.js'; */ +/** @import { TemplateNode } from '#client' */ import { EFFECT_TRANSPARENT } from '#client/constants'; import { hydrate_next, - hydrate_node, hydrating, read_hydration_instruction, skip_nodes, set_hydrate_node, set_hydrating } from '../hydration.js'; -import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; -import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; -import { create_text, should_defer_append } from '../operations.js'; -import { current_batch } from '../../reactivity/batch.js'; +import { block } from '../../reactivity/effects.js'; +import { HYDRATION_START_ELSE } from '../../../../constants.js'; +import { BranchManager } from './branches.js'; // TODO reinstate https://github.com/sveltejs/svelte/pull/15250 @@ -28,122 +25,46 @@ export function if_block(node, fn, elseif = false) { hydrate_next(); } - var anchor = node; - - /** @type {Effect | null} */ - var consequent_effect = null; - - /** @type {Effect | null} */ - var alternate_effect = null; - - /** @type {typeof UNINITIALIZED | boolean | null} */ - var condition = UNINITIALIZED; - + var branches = new BranchManager(node); var flags = elseif ? EFFECT_TRANSPARENT : 0; - var has_branch = false; - - const set_branch = (/** @type {(anchor: Node) => void} */ fn, flag = true) => { - has_branch = true; - update_branch(flag, fn); - }; - - /** @type {DocumentFragment | null} */ - var offscreen_fragment = null; - - function commit() { - if (offscreen_fragment !== null) { - // remove the anchor - /** @type {Text} */ (offscreen_fragment.lastChild).remove(); - - anchor.before(offscreen_fragment); - offscreen_fragment = null; - } - - var active = condition ? consequent_effect : alternate_effect; - var inactive = condition ? alternate_effect : consequent_effect; - - if (active) { - resume_effect(active); - } - - if (inactive) { - pause_effect(inactive, () => { - if (condition) { - alternate_effect = null; - } else { - consequent_effect = null; - } - }); - } - } - - const update_branch = ( - /** @type {boolean | null} */ new_condition, - /** @type {null | ((anchor: Node) => void)} */ fn - ) => { - if (condition === (condition = new_condition)) return; - - /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ - let mismatch = false; - + /** + * @param {boolean} condition, + * @param {null | ((anchor: Node) => void)} fn + */ + function update_branch(condition, fn) { if (hydrating) { - const is_else = read_hydration_instruction(anchor) === HYDRATION_START_ELSE; + const is_else = read_hydration_instruction(node) === HYDRATION_START_ELSE; - if (!!condition === is_else) { + if (condition === is_else) { // Hydration mismatch: remove everything inside the anchor and start fresh. // This could happen with `{#if browser}...{/if}`, for example - anchor = skip_nodes(); + var anchor = skip_nodes(); set_hydrate_node(anchor); - set_hydrating(false); - mismatch = true; - } - } + branches.anchor = anchor; - var defer = should_defer_append(); - var target = anchor; - - if (defer) { - offscreen_fragment = document.createDocumentFragment(); - offscreen_fragment.append((target = create_text())); - } + set_hydrating(false); + branches.ensure(condition, fn); + set_hydrating(true); - if (condition) { - consequent_effect ??= fn && branch(() => fn(target)); - } else { - alternate_effect ??= fn && branch(() => fn(target)); + return; + } } - if (defer) { - var batch = /** @type {Batch} */ (current_batch); - - var active = condition ? consequent_effect : alternate_effect; - var inactive = condition ? alternate_effect : consequent_effect; - - if (active) batch.skipped_effects.delete(active); - if (inactive) batch.skipped_effects.add(inactive); + branches.ensure(condition, fn); + } - batch.add_callback(commit); - } else { - commit(); - } + block(() => { + var has_branch = false; - if (mismatch) { - // continue in hydration mode - set_hydrating(true); - } - }; + fn((fn, flag = true) => { + has_branch = true; + update_branch(flag, fn); + }); - block(() => { - has_branch = false; - fn(set_branch); if (!has_branch) { - update_branch(null, null); + update_branch(false, null); } }, flags); - - if (hydrating) { - anchor = hydrate_node; - } } diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js index 5e3c42019f..849b1c2447 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -1,12 +1,8 @@ -/** @import { Effect, TemplateNode } from '#client' */ -/** @import { Batch } from '../../reactivity/batch.js'; */ -import { UNINITIALIZED } from '../../../../constants.js'; -import { block, branch, pause_effect } from '../../reactivity/effects.js'; -import { not_equal, safe_not_equal } from '../../reactivity/equality.js'; +/** @import { TemplateNode } from '#client' */ import { is_runes } from '../../context.js'; -import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; -import { create_text, should_defer_append } from '../operations.js'; -import { current_batch } from '../../reactivity/batch.js'; +import { block } from '../../reactivity/effects.js'; +import { hydrate_next, hydrating } from '../hydration.js'; +import { BranchManager } from './branches.js'; /** * @template V @@ -20,60 +16,18 @@ export function key(node, get_key, render_fn) { hydrate_next(); } - var anchor = node; + var branches = new BranchManager(node); - /** @type {V | typeof UNINITIALIZED} */ - var key = UNINITIALIZED; - - /** @type {Effect} */ - var effect; - - /** @type {Effect} */ - var pending_effect; - - /** @type {DocumentFragment | null} */ - var offscreen_fragment = null; - - var changed = is_runes() ? not_equal : safe_not_equal; - - function commit() { - if (effect) { - pause_effect(effect); - } - - if (offscreen_fragment !== null) { - // remove the anchor - /** @type {Text} */ (offscreen_fragment.lastChild).remove(); - - anchor.before(offscreen_fragment); - offscreen_fragment = null; - } - - effect = pending_effect; - } + var legacy = !is_runes(); block(() => { - if (changed(key, (key = get_key()))) { - var target = anchor; - - var defer = should_defer_append(); - - if (defer) { - offscreen_fragment = document.createDocumentFragment(); - offscreen_fragment.append((target = create_text())); - } - - pending_effect = branch(() => render_fn(target)); + var key = get_key(); - if (defer) { - /** @type {Batch} */ (current_batch).add_callback(commit); - } else { - commit(); - } + // key blocks in Svelte <5 had stupid semantics + if (legacy && key !== null && typeof key === 'object') { + key = /** @type {V} */ ({}); } - }); - if (hydrating) { - anchor = hydrate_node; - } + branches.ensure(key, render_fn); + }); } diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index 32d88d4c60..0c4948aca0 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -1,8 +1,8 @@ /** @import { Snippet } from 'svelte' */ -/** @import { Effect, TemplateNode } from '#client' */ +/** @import { TemplateNode } from '#client' */ /** @import { Getters } from '#shared' */ import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants'; -import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js'; +import { block, teardown } from '../../reactivity/effects.js'; import { dev_current_component_function, set_dev_current_component_function @@ -14,8 +14,8 @@ import * as w from '../../warnings.js'; import * as e from '../../errors.js'; import { DEV } from 'esm-env'; import { get_first_child, get_next_sibling } from '../operations.js'; -import { noop } from '../../../shared/utils.js'; import { prevent_snippet_stringification } from '../../../shared/validate.js'; +import { BranchManager } from './branches.js'; /** * @template {(node: TemplateNode, ...args: any[]) => void} SnippetFn @@ -25,33 +25,17 @@ import { prevent_snippet_stringification } from '../../../shared/validate.js'; * @returns {void} */ export function snippet(node, get_snippet, ...args) { - var anchor = node; - - /** @type {SnippetFn | null | undefined} */ - // @ts-ignore - var snippet = noop; - - /** @type {Effect | null} */ - var snippet_effect; + var branches = new BranchManager(node); block(() => { - if (snippet === (snippet = get_snippet())) return; - - if (snippet_effect) { - destroy_effect(snippet_effect); - snippet_effect = null; - } + const snippet = get_snippet() ?? null; if (DEV && snippet == null) { e.invalid_snippet(); } - snippet_effect = branch(() => /** @type {SnippetFn} */ (snippet)(anchor, ...args)); + branches.ensure(snippet, snippet && ((anchor) => snippet(anchor, ...args))); }, EFFECT_TRANSPARENT); - - if (hydrating) { - anchor = hydrate_node; - } } /** diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js index 2697722b39..134e57e627 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -1,10 +1,8 @@ -/** @import { TemplateNode, Dom, Effect } from '#client' */ -/** @import { Batch } from '../../reactivity/batch.js'; */ +/** @import { TemplateNode, Dom } from '#client' */ import { EFFECT_TRANSPARENT } from '#client/constants'; -import { block, branch, pause_effect } from '../../reactivity/effects.js'; -import { current_batch } from '../../reactivity/batch.js'; -import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; -import { create_text, should_defer_append } from '../operations.js'; +import { block } from '../../reactivity/effects.js'; +import { hydrate_next, hydrating } from '../hydration.js'; +import { BranchManager } from './branches.js'; /** * @template P @@ -19,64 +17,10 @@ export function component(node, get_component, render_fn) { hydrate_next(); } - var anchor = node; - - /** @type {C} */ - var component; - - /** @type {Effect | null} */ - var effect; - - /** @type {DocumentFragment | null} */ - var offscreen_fragment = null; - - /** @type {Effect | null} */ - var pending_effect = null; - - function commit() { - if (effect) { - pause_effect(effect); - effect = null; - } - - if (offscreen_fragment) { - // remove the anchor - /** @type {Text} */ (offscreen_fragment.lastChild).remove(); - - anchor.before(offscreen_fragment); - offscreen_fragment = null; - } - - effect = pending_effect; - pending_effect = null; - } + var branches = new BranchManager(node); block(() => { - if (component === (component = get_component())) return; - - var defer = should_defer_append(); - - if (component) { - var target = anchor; - - if (defer) { - offscreen_fragment = document.createDocumentFragment(); - offscreen_fragment.append((target = create_text())); - if (effect) { - /** @type {Batch} */ (current_batch).skipped_effects.add(effect); - } - } - pending_effect = branch(() => render_fn(target, component)); - } - - if (defer) { - /** @type {Batch} */ (current_batch).add_callback(commit); - } else { - commit(); - } + var component = get_component() ?? null; + branches.ensure(component, component && ((target) => render_fn(target, component))); }, EFFECT_TRANSPARENT); - - if (hydrating) { - anchor = hydrate_node; - } } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js index 231a3621b1..6533ff8921 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -8,13 +8,7 @@ import { set_hydrating } from '../hydration.js'; import { create_text, get_first_child } from '../operations.js'; -import { - block, - branch, - destroy_effect, - pause_effect, - resume_effect -} from '../../reactivity/effects.js'; +import { block, teardown } from '../../reactivity/effects.js'; import { set_should_intro } from '../../render.js'; import { current_each_item, set_current_each_item } from './each.js'; import { active_effect } from '../../runtime.js'; @@ -23,6 +17,7 @@ import { DEV } from 'esm-env'; import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants'; import { assign_nodes } from '../template.js'; import { is_raw_text_element } from '../../../../utils.js'; +import { BranchManager } from './branches.js'; /** * @param {Comment | Element} node @@ -42,12 +37,6 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio var filename = DEV && location && component_context?.function[FILENAME]; - /** @type {string | null} */ - var tag; - - /** @type {string | null} */ - var current_tag; - /** @type {null | Element} */ var element = null; @@ -58,9 +47,6 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio var anchor = /** @type {TemplateNode} */ (hydrating ? hydrate_node : node); - /** @type {Effect | null} */ - var effect; - /** * The keyed `{#each ...}` item block, if any, that this element is inside. * We track this so we can set it when changing the element, allowing any @@ -68,36 +54,24 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio */ var each_item_block = current_each_item; + var branches = new BranchManager(anchor, false); + block(() => { const next_tag = get_tag() || null; var ns = get_namespace ? get_namespace() : is_svg || next_tag === 'svg' ? NAMESPACE_SVG : null; - // Assumption: Noone changes the namespace but not the tag (what would that even mean?) - if (next_tag === tag) return; - - // See explanation of `each_item_block` above - var previous_each_item = current_each_item; - set_current_each_item(each_item_block); - - if (effect) { - if (next_tag === null) { - // start outro - pause_effect(effect, () => { - effect = null; - current_tag = null; - }); - } else if (next_tag === current_tag) { - // same tag as is currently rendered — abort outro - resume_effect(effect); - } else { - // tag is changing — destroy immediately, render contents without intro transitions - destroy_effect(effect); - set_should_intro(false); - } + if (next_tag === null) { + branches.ensure(null, null); + set_should_intro(true); + return; } - if (next_tag && next_tag !== current_tag) { - effect = branch(() => { + branches.ensure(next_tag, (anchor) => { + // See explanation of `each_item_block` above + var previous_each_item = current_each_item; + set_current_each_item(each_item_block); + + if (next_tag) { element = hydrating ? /** @type {Element} */ (element) : ns @@ -149,16 +123,31 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio /** @type {Effect} */ (active_effect).nodes_end = element; anchor.before(element); - }); - } + } + + set_current_each_item(previous_each_item); + + if (hydrating) { + set_hydrate_node(anchor); + } + }); - tag = next_tag; - if (tag) current_tag = tag; + // revert to the default state after the effect has been created set_should_intro(true); - set_current_each_item(previous_each_item); + return () => { + if (next_tag) { + // if we're in this callback because we're re-running the effect, + // disable intros (unless no element is currently displayed) + set_should_intro(false); + } + }; }, EFFECT_TRANSPARENT); + teardown(() => { + set_should_intro(true); + }); + if (was_hydrating) { set_hydrating(true); set_hydrate_node(anchor); diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 45c78ff926..1d408744fd 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -1,8 +1,13 @@ -/** @import { Effect, Value } from '#client' */ - +/** @import { Effect, TemplateNode, Value } from '#client' */ import { DESTROYED } from '#client/constants'; import { DEV } from 'esm-env'; -import { component_context, is_runes, set_component_context } from '../context.js'; +import { + component_context, + dev_stack, + is_runes, + set_component_context, + set_dev_stack +} from '../context.js'; import { get_boundary } from '../dom/blocks/boundary.js'; import { invoke_error_boundary } from '../error-handling.js'; import { @@ -28,6 +33,7 @@ import { set_hydrating, skip_nodes } from '../dom/hydration.js'; +import { create_text } from '../dom/operations.js'; /** * @@ -80,7 +86,7 @@ export function flatten(sync, async, fn) { * some asynchronous work has happened (so that e.g. `await a + b` * causes `b` to be registered as a dependency). */ -function capture() { +export function capture() { var previous_effect = active_effect; var previous_reaction = active_reaction; var previous_component_context = component_context; @@ -92,6 +98,10 @@ function capture() { var previous_hydrate_node = hydrate_node; } + if (DEV) { + var previous_dev_stack = dev_stack; + } + return function restore() { set_active_effect(previous_effect); set_active_reaction(previous_reaction); @@ -105,6 +115,7 @@ function capture() { if (DEV) { set_from_async_derived(null); + set_dev_stack(previous_dev_stack); } }; } @@ -193,13 +204,18 @@ export function unset_context() { set_active_effect(null); set_active_reaction(null); set_component_context(null); - if (DEV) set_from_async_derived(null); + + if (DEV) { + set_from_async_derived(null); + set_dev_stack(null); + } } /** - * @param {() => Promise} fn + * @param {TemplateNode} anchor + * @param {(target: TemplateNode) => Promise} fn */ -export async function async_body(fn) { +export async function async_body(anchor, fn) { var boundary = get_boundary(); var batch = /** @type {Batch} */ (current_batch); var pending = boundary.is_pending(); @@ -218,7 +234,7 @@ export async function async_body(fn) { } try { - var promise = fn(); + var promise = fn(anchor); } finally { if (next_hydrate_node) { set_hydrate_node(next_hydrate_node); diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 2c9e4db911..bfbb95a8db 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -553,15 +553,16 @@ export function unlink_effect(effect) { * A paused effect does not update, and the DOM subtree becomes inert. * @param {Effect} effect * @param {() => void} [callback] + * @param {boolean} [destroy] */ -export function pause_effect(effect, callback) { +export function pause_effect(effect, callback, destroy = true) { /** @type {TransitionManager[]} */ var transitions = []; pause_children(effect, transitions, true); run_out_transitions(transitions, () => { - destroy_effect(effect); + if (destroy) destroy_effect(effect); if (callback) callback(); }); } @@ -662,3 +663,20 @@ function resume_children(effect, local) { export function aborted(effect = /** @type {Effect} */ (active_effect)) { return (effect.f & DESTROYED) !== 0; } + +/** + * @param {Effect} effect + * @param {DocumentFragment} fragment + */ +export function move_effect(effect, fragment) { + var node = effect.nodes_start; + var end = effect.nodes_end; + + while (node !== null) { + /** @type {TemplateNode | null} */ + var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node)); + + fragment.append(node); + node = next; + } +} diff --git a/packages/svelte/tests/runtime-runes/samples/await-pending-destroy/_config.js b/packages/svelte/tests/runtime-runes/samples/await-pending-destroy/_config.js index 1725cd8f6f..9ef598de6c 100644 --- a/packages/svelte/tests/runtime-runes/samples/await-pending-destroy/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/await-pending-destroy/_config.js @@ -1,3 +1,4 @@ +import { tick } from 'svelte'; import { test } from '../../test'; /** @@ -77,7 +78,7 @@ export default test({ const { promise, reject } = promiseWithResolver(); component.promise = promise; // wait for rendering - await Promise.resolve(); + await tick(); // remove the promise component.promise = null; From 7d9ea8ea99cfaba36dddb5e7c9994e9c44214b8c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 20 Oct 2025 09:30:51 -0400 Subject: [PATCH 11/58] Version Packages (#16984) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/brown-insects-burn.md | 5 ----- .changeset/three-wasps-work.md | 5 ----- .changeset/yellow-shrimps-provide.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 6 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 .changeset/brown-insects-burn.md delete mode 100644 .changeset/three-wasps-work.md delete mode 100644 .changeset/yellow-shrimps-provide.md diff --git a/.changeset/brown-insects-burn.md b/.changeset/brown-insects-burn.md deleted file mode 100644 index ceccc3fd9b..0000000000 --- a/.changeset/brown-insects-burn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: place `let:` declarations before `{@const}` declarations diff --git a/.changeset/three-wasps-work.md b/.changeset/three-wasps-work.md deleted file mode 100644 index 318d8668a4..0000000000 --- a/.changeset/three-wasps-work.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: improve `each_key_without_as` error diff --git a/.changeset/yellow-shrimps-provide.md b/.changeset/yellow-shrimps-provide.md deleted file mode 100644 index a29385660a..0000000000 --- a/.changeset/yellow-shrimps-provide.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: centralise branch management diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 81529104d9..4db131114d 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.41.1 + +### Patch Changes + +- fix: place `let:` declarations before `{@const}` declarations ([#16985](https://github.com/sveltejs/svelte/pull/16985)) + +- fix: improve `each_key_without_as` error ([#16983](https://github.com/sveltejs/svelte/pull/16983)) + +- chore: centralise branch management ([#16977](https://github.com/sveltejs/svelte/pull/16977)) + ## 5.41.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 6d8463a52a..b5a20ce82a 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.41.0", + "version": "5.41.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index e88bf0d0ea..e33d22d4c4 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.41.0'; +export const VERSION = '5.41.1'; export const PUBLIC_VERSION = '5'; From f549478dd0508074ac2b8c31777e598936e6f8c9 Mon Sep 17 00:00:00 2001 From: David Date: Tue, 21 Oct 2025 01:07:51 -0700 Subject: [PATCH 12/58] fix: guard contents updated before the guard itself (#16930) Fixes #16850, fixes #16775, fixes #16795, fixes #16982 #16631 introduced a bug that results in the effects within guards being evaluated before the guards themselves. This fix makes sure to iterate the block effects in the correct order (top down) --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Simon Holthausen --- .changeset/witty-seas-learn.md | 5 +++ .../src/internal/client/reactivity/batch.js | 31 +++++++++++++++---- .../src/internal/client/reactivity/sources.js | 2 +- .../samples/guard-else-effect/_config.js | 20 ++++++++++++ .../samples/guard-else-effect/main.svelte | 18 +++++++++++ .../samples/guard-if-nested/_config.js | 13 ++++++++ .../samples/guard-if-nested/main.svelte | 18 +++++++++++ .../guard-nested-if-pre/Component.svelte | 6 ++++ .../samples/guard-nested-if-pre/_config.js | 13 ++++++++ .../samples/guard-nested-if-pre/main.svelte | 18 +++++++++++ 10 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 .changeset/witty-seas-learn.md create mode 100644 packages/svelte/tests/runtime-runes/samples/guard-else-effect/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/guard-else-effect/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/guard-if-nested/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/guard-if-nested/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/guard-nested-if-pre/Component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/guard-nested-if-pre/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/guard-nested-if-pre/main.svelte diff --git a/.changeset/witty-seas-learn.md b/.changeset/witty-seas-learn.md new file mode 100644 index 0000000000..aa94c7c35f --- /dev/null +++ b/.changeset/witty-seas-learn.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure guards (eg. if, each, key) run before their contents diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index b35e16a409..302d48d03f 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -561,7 +561,7 @@ function infinite_loop_guard() { } } -/** @type {Effect[] | null} */ +/** @type {Set | null} */ export let eager_block_effects = null; /** @@ -578,7 +578,7 @@ function flush_queued_effects(effects) { var effect = effects[i++]; if ((effect.f & (DESTROYED | INERT)) === 0 && is_dirty(effect)) { - eager_block_effects = []; + eager_block_effects = new Set(); update_effect(effect); @@ -601,15 +601,34 @@ function flush_queued_effects(effects) { // If update_effect() has a flushSync() in it, we may have flushed another flush_queued_effects(), // which already handled this logic and did set eager_block_effects to null. - if (eager_block_effects?.length > 0) { - // TODO this feels incorrect! it gets the tests passing + if (eager_block_effects?.size > 0) { old_values.clear(); for (const e of eager_block_effects) { - update_effect(e); + // Skip eager effects that have already been unmounted + if ((e.f & (DESTROYED | INERT)) !== 0) continue; + + // Run effects in order from ancestor to descendant, else we could run into nullpointers + /** @type {Effect[]} */ + const ordered_effects = [e]; + let ancestor = e.parent; + while (ancestor !== null) { + if (eager_block_effects.has(ancestor)) { + eager_block_effects.delete(ancestor); + ordered_effects.push(ancestor); + } + ancestor = ancestor.parent; + } + + for (let j = ordered_effects.length - 1; j >= 0; j--) { + const e = ordered_effects[j]; + // Skip eager effects that have already been unmounted + if ((e.f & (DESTROYED | INERT)) !== 0) continue; + update_effect(e); + } } - eager_block_effects = []; + eager_block_effects.clear(); } } } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index cd0c28016d..c5dcff9cfb 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -336,7 +336,7 @@ function mark_reactions(signal, status) { } else if (not_dirty) { if ((flags & BLOCK_EFFECT) !== 0) { if (eager_block_effects !== null) { - eager_block_effects.push(/** @type {Effect} */ (reaction)); + eager_block_effects.add(/** @type {Effect} */ (reaction)); } } diff --git a/packages/svelte/tests/runtime-runes/samples/guard-else-effect/_config.js b/packages/svelte/tests/runtime-runes/samples/guard-else-effect/_config.js new file mode 100644 index 0000000000..4e8eec8b16 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/guard-else-effect/_config.js @@ -0,0 +1,20 @@ +import { test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + mode: ['client'], + async test({ target, assert, logs }) { + const button = target.querySelector('button'); + + button?.click(); + flushSync(); + button?.click(); + flushSync(); + button?.click(); + flushSync(); + button?.click(); + flushSync(); + + assert.deepEqual(logs, ['two', 'one', 'two', 'one', 'two']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/guard-else-effect/main.svelte b/packages/svelte/tests/runtime-runes/samples/guard-else-effect/main.svelte new file mode 100644 index 0000000000..91fd0442bd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/guard-else-effect/main.svelte @@ -0,0 +1,18 @@ + + + + +{#if v === "one"} +
if1 matched! {console.log('one')}
+{:else if v === "two"} +
if2 matched! {console.log('two')}
+{:else} +
nothing matched {console.log('else')}
+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/guard-if-nested/_config.js b/packages/svelte/tests/runtime-runes/samples/guard-if-nested/_config.js new file mode 100644 index 0000000000..881c1545ee --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/guard-if-nested/_config.js @@ -0,0 +1,13 @@ +import { test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + mode: ['client'], + async test({ target, assert }) { + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + + assert.equal(target.textContent?.trim(), 'Trigger'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/guard-if-nested/main.svelte b/packages/svelte/tests/runtime-runes/samples/guard-if-nested/main.svelte new file mode 100644 index 0000000000..4514bd114e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/guard-if-nested/main.svelte @@ -0,0 +1,18 @@ + + +{#if centerRow?.nested} + {#if centerRow?.nested?.optional != undefined && centerRow.nested.optional > 0} + op: {centerRow.nested.optional}
+ {:else} + req: {centerRow.nested.required}
+ {/if} +{/if} + + diff --git a/packages/svelte/tests/runtime-runes/samples/guard-nested-if-pre/Component.svelte b/packages/svelte/tests/runtime-runes/samples/guard-nested-if-pre/Component.svelte new file mode 100644 index 0000000000..b7322e7530 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/guard-nested-if-pre/Component.svelte @@ -0,0 +1,6 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/guard-nested-if-pre/_config.js b/packages/svelte/tests/runtime-runes/samples/guard-nested-if-pre/_config.js new file mode 100644 index 0000000000..9706855fb4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/guard-nested-if-pre/_config.js @@ -0,0 +1,13 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + async test({ assert, target, logs }) { + const button = target.querySelector('button'); + + button?.click(); + flushSync(); + assert.deepEqual(logs, ['pre', 'running b', 'pre', 'pre']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/guard-nested-if-pre/main.svelte b/packages/svelte/tests/runtime-runes/samples/guard-nested-if-pre/main.svelte new file mode 100644 index 0000000000..4ebb13eca3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/guard-nested-if-pre/main.svelte @@ -0,0 +1,18 @@ + + +{#if p || !p} + {#if p} + + {/if} +{/if} + + From a57868ebce18662a3bb07d7ec71c612692272a37 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 21 Oct 2025 21:56:18 +0200 Subject: [PATCH 13/58] fix: don't preserve reactivity context across function boundaries (#17002) Fixes #16809 We gotta take into account function boundaries when determining whether or not we're inside a derived (or const). --- .changeset/short-banks-yell.md | 5 ++ .../src/compiler/phases/2-analyze/index.js | 6 +-- .../src/compiler/phases/2-analyze/types.d.ts | 4 +- .../2-analyze/visitors/AwaitExpression.js | 19 ++++--- .../2-analyze/visitors/CallExpression.js | 2 +- .../phases/2-analyze/visitors/ConstTag.js | 4 +- .../2-analyze/visitors/VariableDeclarator.js | 6 --- .../samples/async-in-derived/_config.js | 3 ++ .../_expected/client/index.svelte.js | 52 +++++++++++++++++++ .../_expected/server/index.svelte.js | 40 ++++++++++++++ .../samples/async-in-derived/index.svelte | 21 ++++++++ 11 files changed, 143 insertions(+), 19 deletions(-) create mode 100644 .changeset/short-banks-yell.md create mode 100644 packages/svelte/tests/snapshot/samples/async-in-derived/_config.js create mode 100644 packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-in-derived/index.svelte diff --git a/.changeset/short-banks-yell.md b/.changeset/short-banks-yell.md new file mode 100644 index 0000000000..34d5ba66d3 --- /dev/null +++ b/.changeset/short-banks-yell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't preserve reactivity context across function boundaries diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 47fe37c44d..52be997374 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -306,7 +306,7 @@ export function analyze_module(source, options) { fragment: null, parent_element: null, reactive_statement: null, - in_derived: false + derived_function_depth: -1 }, visitors ); @@ -703,7 +703,7 @@ export function analyze_component(root, source, options) { state_fields: new Map(), function_depth: scope.function_depth, reactive_statement: null, - in_derived: false + derived_function_depth: -1 }; walk(/** @type {AST.SvelteNode} */ (ast), state, visitors); @@ -771,7 +771,7 @@ export function analyze_component(root, source, options) { expression: null, state_fields: new Map(), function_depth: scope.function_depth, - in_derived: false + derived_function_depth: -1 }; walk(/** @type {AST.SvelteNode} */ (ast), state, visitors); diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index ae9c5911f6..bad6c7d613 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -29,9 +29,9 @@ export interface AnalysisState { reactive_statement: null | ReactiveStatement; /** - * True if we're directly inside a `$derived(...)` expression (but not `$derived.by(...)`) + * Set when we're inside a `$derived(...)` expression (but not `$derived.by(...)`) or `@const` */ - in_derived: boolean; + derived_function_depth: number; } export type Context = import('zimmerframe').Context< diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index 9018623570..14757af4a3 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -15,7 +15,10 @@ export function AwaitExpression(node, context) { // b) awaits that precede other expressions in template or `$derived(...)` if ( tla || - (is_reactive_expression(context.path, context.state.in_derived) && + (is_reactive_expression( + context.path, + context.state.derived_function_depth === context.state.function_depth + ) && !is_last_evaluated_expression(context.path, node)) ) { context.state.analysis.pickled_awaits.add(node); @@ -53,9 +56,7 @@ export function AwaitExpression(node, context) { * @param {boolean} in_derived */ export function is_reactive_expression(path, in_derived) { - if (in_derived) { - return true; - } + if (in_derived) return true; let i = path.length; @@ -67,6 +68,7 @@ export function is_reactive_expression(path, in_derived) { parent.type === 'FunctionExpression' || parent.type === 'FunctionDeclaration' ) { + // No reactive expression found between function and await return false; } @@ -83,11 +85,16 @@ export function is_reactive_expression(path, in_derived) { * @param {AST.SvelteNode[]} path * @param {Expression | SpreadElement | Property} node */ -export function is_last_evaluated_expression(path, node) { +function is_last_evaluated_expression(path, node) { let i = path.length; while (i--) { - const parent = /** @type {Expression | Property | SpreadElement} */ (path[i]); + const parent = path[i]; + + if (parent.type === 'ConstTag') { + // {@const ...} tags are treated as deriveds and its contents should all get the preserve-reactivity treatment + return false; + } // @ts-expect-error we could probably use a neater/more robust mechanism if (parent.metadata) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 76d9cecd9a..4b66abe1d1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -248,7 +248,7 @@ export function CallExpression(node, context) { context.next({ ...context.state, function_depth: context.state.function_depth + 1, - in_derived: true, + derived_function_depth: context.state.function_depth + 1, expression }); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js index 5849d828a3..77ea654905 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js @@ -38,6 +38,8 @@ export function ConstTag(node, context) { context.visit(declaration.init, { ...context.state, expression: node.metadata.expression, - in_derived: true + // We're treating this like a $derived under the hood + function_depth: context.state.function_depth + 1, + derived_function_depth: context.state.function_depth + 1 }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js index 7a85b4a93a..dfb1d54040 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/VariableDeclarator.js @@ -64,12 +64,6 @@ export function VariableDeclarator(node, context) { } } - if (rune === '$derived') { - context.visit(node.id); - context.visit(/** @type {Expression} */ (node.init), { ...context.state, in_derived: true }); - return; - } - if (rune === '$props') { if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') { e.props_invalid_identifier(node); diff --git a/packages/svelte/tests/snapshot/samples/async-in-derived/_config.js b/packages/svelte/tests/snapshot/samples/async-in-derived/_config.js new file mode 100644 index 0000000000..2e30bbeb16 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-in-derived/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({ compileOptions: { experimental: { async: true } } }); diff --git a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js new file mode 100644 index 0000000000..7a97850175 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js @@ -0,0 +1,52 @@ +import 'svelte/internal/disclose-version'; +import 'svelte/internal/flags/async'; +import * as $ from 'svelte/internal/client'; + +export default function Async_in_derived($$anchor, $$props) { + $.push($$props, true); + + $.async_body($$anchor, async ($$anchor) => { + let yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))(); + let yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))(); + + let no1 = $.derived(async () => { + return await 1; + }); + + let no2 = $.derived(() => async () => { + return await 1; + }); + + if ($.aborted()) return; + + var fragment = $.comment(); + var node = $.first_child(fragment); + + { + var consequent = ($$anchor) => { + $.async_body($$anchor, async ($$anchor) => { + const yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))(); + const yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))(); + + const no1 = $.derived(() => (async () => { + return await 1; + })()); + + const no2 = $.derived(() => (async () => { + return await 1; + })()); + + if ($.aborted()) return; + }); + }; + + $.if(node, ($$render) => { + if (true) $$render(consequent); + }); + } + + $.append($$anchor, fragment); + }); + + $.pop(); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js new file mode 100644 index 0000000000..69eca5a383 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js @@ -0,0 +1,40 @@ +import 'svelte/internal/flags/async'; +import * as $ from 'svelte/internal/server'; + +export default function Async_in_derived($$renderer, $$props) { + $$renderer.component(($$renderer) => { + $$renderer.async(async ($$renderer) => { + let yes1 = (await $.save(1))(); + let yes2 = foo((await $.save(1))()); + + let no1 = (async () => { + return await 1; + })(); + + let no2 = async () => { + return await 1; + }; + + $$renderer.async(async ($$renderer) => { + if (true) { + $$renderer.push(''); + + const yes1 = (await $.save(1))(); + const yes2 = foo((await $.save(1))()); + + const no1 = (async () => { + return await 1; + })(); + + const no2 = (async () => { + return await 1; + })(); + } else { + $$renderer.push(''); + } + }); + + $$renderer.push(``); + }); + }); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/async-in-derived/index.svelte b/packages/svelte/tests/snapshot/samples/async-in-derived/index.svelte new file mode 100644 index 0000000000..bda88fd3ae --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-in-derived/index.svelte @@ -0,0 +1,21 @@ + + +{#if true} + {@const yes1 = await 1} + {@const yes2 = foo(await 1)} + {@const no1 = (async () => { + return await 1; + })()} + {@const no2 = (async () => { + return await 1; + })()} +{/if} From 5bb28c45675f788dc057d9ce37e6cd093e402da0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 20:30:04 -0700 Subject: [PATCH 14/58] chore(deps-dev): bump vite from 7.1.5 to 7.1.11 (#16989) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.5 to 7.1.11. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.1.11/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 7.1.11 dependency-type: direct:development ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- playgrounds/sandbox/package.json | 2 +- pnpm-lock.yaml | 649 +++++++++++++++++++++++++++---- 2 files changed, 574 insertions(+), 77 deletions(-) diff --git a/playgrounds/sandbox/package.json b/playgrounds/sandbox/package.json index 84aeab586b..629cb33b5a 100644 --- a/playgrounds/sandbox/package.json +++ b/playgrounds/sandbox/package.json @@ -21,7 +21,7 @@ "polka": "^1.0.0-next.25", "svelte": "workspace:*", "tinyglobby": "^0.2.12", - "vite": "^7.1.5", + "vite": "^7.1.11", "vite-plugin-devtools-json": "^1.0.0", "vite-plugin-inspect": "^11.3.3" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f585619252..7a8075038b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -158,7 +158,7 @@ importers: devDependencies: '@sveltejs/vite-plugin-svelte': specifier: ^6.2.0 - version: 6.2.0(svelte@packages+svelte)(vite@7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + version: 6.2.0(svelte@packages+svelte)(vite@7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) '@types/node': specifier: ^24.5.2 version: 24.5.2 @@ -172,14 +172,14 @@ importers: specifier: ^0.2.12 version: 0.2.15 vite: - specifier: ^7.1.5 - version: 7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + specifier: ^7.1.11 + version: 7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) vite-plugin-devtools-json: specifier: ^1.0.0 - version: 1.0.0(vite@7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + version: 1.0.0(vite@7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) vite-plugin-inspect: specifier: ^11.3.3 - version: 11.3.3(vite@7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + version: 11.3.3(vite@7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) packages: @@ -285,6 +285,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.25.11': + resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} @@ -297,6 +303,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.25.11': + resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} @@ -309,6 +321,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.25.11': + resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} @@ -321,6 +339,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.25.11': + resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} @@ -333,6 +357,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.25.11': + resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} @@ -345,6 +375,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.25.11': + resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} @@ -357,6 +393,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.25.11': + resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} @@ -369,6 +411,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.25.11': + resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} @@ -381,6 +429,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.25.11': + resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} @@ -393,6 +447,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.25.11': + resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} @@ -405,6 +465,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.25.11': + resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} @@ -417,6 +483,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.25.11': + resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} @@ -429,6 +501,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.25.11': + resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} @@ -441,6 +519,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.25.11': + resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} @@ -453,6 +537,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.25.11': + resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} @@ -465,6 +555,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.25.11': + resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} @@ -477,12 +573,24 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.25.11': + resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.10': resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.25.11': + resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} @@ -495,12 +603,24 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.25.11': + resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.10': resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.25.11': + resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} @@ -513,12 +633,24 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.25.11': + resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.10': resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.25.11': + resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} @@ -531,6 +663,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.25.11': + resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} @@ -543,6 +681,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.25.11': + resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} @@ -555,6 +699,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.25.11': + resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} @@ -567,6 +717,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.25.11': + resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -722,51 +878,106 @@ packages: cpu: [arm] os: [android] + '@rollup/rollup-android-arm-eabi@4.52.5': + resolution: {integrity: sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==} + cpu: [arm] + os: [android] + '@rollup/rollup-android-arm64@4.50.1': resolution: {integrity: sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==} cpu: [arm64] os: [android] + '@rollup/rollup-android-arm64@4.52.5': + resolution: {integrity: sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==} + cpu: [arm64] + os: [android] + '@rollup/rollup-darwin-arm64@4.50.1': resolution: {integrity: sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==} cpu: [arm64] os: [darwin] + '@rollup/rollup-darwin-arm64@4.52.5': + resolution: {integrity: sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==} + cpu: [arm64] + os: [darwin] + '@rollup/rollup-darwin-x64@4.50.1': resolution: {integrity: sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==} cpu: [x64] os: [darwin] + '@rollup/rollup-darwin-x64@4.52.5': + resolution: {integrity: sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==} + cpu: [x64] + os: [darwin] + '@rollup/rollup-freebsd-arm64@4.50.1': resolution: {integrity: sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==} cpu: [arm64] os: [freebsd] + '@rollup/rollup-freebsd-arm64@4.52.5': + resolution: {integrity: sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==} + cpu: [arm64] + os: [freebsd] + '@rollup/rollup-freebsd-x64@4.50.1': resolution: {integrity: sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==} cpu: [x64] os: [freebsd] + '@rollup/rollup-freebsd-x64@4.52.5': + resolution: {integrity: sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==} + cpu: [x64] + os: [freebsd] + '@rollup/rollup-linux-arm-gnueabihf@4.50.1': resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==} cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-gnueabihf@4.52.5': + resolution: {integrity: sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.50.1': resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==} cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.52.5': + resolution: {integrity: sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.50.1': resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==} cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.52.5': + resolution: {integrity: sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-arm64-musl@4.50.1': resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==} cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-musl@4.52.5': + resolution: {integrity: sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.52.5': + resolution: {integrity: sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==} + cpu: [loong64] + os: [linux] + '@rollup/rollup-linux-loongarch64-gnu@4.50.1': resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==} cpu: [loong64] @@ -777,51 +988,106 @@ packages: cpu: [ppc64] os: [linux] + '@rollup/rollup-linux-ppc64-gnu@4.52.5': + resolution: {integrity: sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==} + cpu: [ppc64] + os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.50.1': resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==} cpu: [riscv64] os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.52.5': + resolution: {integrity: sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==} + cpu: [riscv64] + os: [linux] + '@rollup/rollup-linux-riscv64-musl@4.50.1': resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==} cpu: [riscv64] os: [linux] + '@rollup/rollup-linux-riscv64-musl@4.52.5': + resolution: {integrity: sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==} + cpu: [riscv64] + os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.50.1': resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==} cpu: [s390x] os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.52.5': + resolution: {integrity: sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==} + cpu: [s390x] + os: [linux] + '@rollup/rollup-linux-x64-gnu@4.50.1': resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==} cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-gnu@4.52.5': + resolution: {integrity: sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==} + cpu: [x64] + os: [linux] + '@rollup/rollup-linux-x64-musl@4.50.1': resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==} cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-musl@4.52.5': + resolution: {integrity: sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==} + cpu: [x64] + os: [linux] + '@rollup/rollup-openharmony-arm64@4.50.1': resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==} cpu: [arm64] os: [openharmony] + '@rollup/rollup-openharmony-arm64@4.52.5': + resolution: {integrity: sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==} + cpu: [arm64] + os: [openharmony] + '@rollup/rollup-win32-arm64-msvc@4.50.1': resolution: {integrity: sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==} cpu: [arm64] os: [win32] + '@rollup/rollup-win32-arm64-msvc@4.52.5': + resolution: {integrity: sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==} + cpu: [arm64] + os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.50.1': resolution: {integrity: sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==} cpu: [ia32] os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.52.5': + resolution: {integrity: sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.52.5': + resolution: {integrity: sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==} + cpu: [x64] + os: [win32] + '@rollup/rollup-win32-x64-msvc@4.50.1': resolution: {integrity: sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==} cpu: [x64] os: [win32] + '@rollup/rollup-win32-x64-msvc@4.52.5': + resolution: {integrity: sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==} + cpu: [x64] + os: [win32] + '@stylistic/eslint-plugin-js@1.8.0': resolution: {integrity: sha512-jdvnzt+pZPg8TfclZlTZPiUbbima93ylvQ+wNgHLNmup3obY6heQvgewSu9i2CfS61BnRByv+F9fxQLPoNeHag==} engines: {node: ^16.0.0 || >=18.0.0} @@ -905,8 +1171,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/project-service@8.43.0': - resolution: {integrity: sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==} + '@typescript-eslint/project-service@8.46.2': + resolution: {integrity: sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' @@ -915,12 +1181,12 @@ packages: resolution: {integrity: sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.43.0': - resolution: {integrity: sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==} + '@typescript-eslint/scope-manager@8.46.2': + resolution: {integrity: sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.43.0': - resolution: {integrity: sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==} + '@typescript-eslint/tsconfig-utils@8.46.2': + resolution: {integrity: sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' @@ -936,8 +1202,8 @@ packages: resolution: {integrity: sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.43.0': - resolution: {integrity: sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==} + '@typescript-eslint/types@8.46.2': + resolution: {integrity: sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/typescript-estree@8.26.0': @@ -946,8 +1212,8 @@ packages: peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/typescript-estree@8.43.0': - resolution: {integrity: sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==} + '@typescript-eslint/typescript-estree@8.46.2': + resolution: {integrity: sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' @@ -959,8 +1225,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.43.0': - resolution: {integrity: sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==} + '@typescript-eslint/utils@8.46.2': + resolution: {integrity: sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -970,8 +1236,8 @@ packages: resolution: {integrity: sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.43.0': - resolution: {integrity: sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==} + '@typescript-eslint/visitor-keys@8.46.2': + resolution: {integrity: sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vitest/coverage-v8@2.1.9': @@ -1197,6 +1463,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decimal.js@10.4.3: resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} @@ -1287,6 +1562,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.25.11: + resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} + engines: {node: '>=18'} + hasBin: true + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -1475,8 +1755,8 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - get-tsconfig@4.10.1: - resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + get-tsconfig@4.12.0: + resolution: {integrity: sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -2062,6 +2342,11 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rollup@4.52.5: + resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + rrweb-cssom@0.7.1: resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} @@ -2096,6 +2381,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -2196,8 +2486,8 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - tapable@2.2.3: - resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==} + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} term-size@2.2.1: @@ -2394,8 +2684,8 @@ packages: terser: optional: true - vite@7.1.5: - resolution: {integrity: sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==} + vite@7.1.11: + resolution: {integrity: sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -2731,147 +3021,225 @@ snapshots: '@esbuild/aix-ppc64@0.25.10': optional: true + '@esbuild/aix-ppc64@0.25.11': + optional: true + '@esbuild/android-arm64@0.21.5': optional: true '@esbuild/android-arm64@0.25.10': optional: true + '@esbuild/android-arm64@0.25.11': + optional: true + '@esbuild/android-arm@0.21.5': optional: true '@esbuild/android-arm@0.25.10': optional: true + '@esbuild/android-arm@0.25.11': + optional: true + '@esbuild/android-x64@0.21.5': optional: true '@esbuild/android-x64@0.25.10': optional: true + '@esbuild/android-x64@0.25.11': + optional: true + '@esbuild/darwin-arm64@0.21.5': optional: true '@esbuild/darwin-arm64@0.25.10': optional: true + '@esbuild/darwin-arm64@0.25.11': + optional: true + '@esbuild/darwin-x64@0.21.5': optional: true '@esbuild/darwin-x64@0.25.10': optional: true + '@esbuild/darwin-x64@0.25.11': + optional: true + '@esbuild/freebsd-arm64@0.21.5': optional: true '@esbuild/freebsd-arm64@0.25.10': optional: true + '@esbuild/freebsd-arm64@0.25.11': + optional: true + '@esbuild/freebsd-x64@0.21.5': optional: true '@esbuild/freebsd-x64@0.25.10': optional: true + '@esbuild/freebsd-x64@0.25.11': + optional: true + '@esbuild/linux-arm64@0.21.5': optional: true '@esbuild/linux-arm64@0.25.10': optional: true + '@esbuild/linux-arm64@0.25.11': + optional: true + '@esbuild/linux-arm@0.21.5': optional: true '@esbuild/linux-arm@0.25.10': optional: true + '@esbuild/linux-arm@0.25.11': + optional: true + '@esbuild/linux-ia32@0.21.5': optional: true '@esbuild/linux-ia32@0.25.10': optional: true + '@esbuild/linux-ia32@0.25.11': + optional: true + '@esbuild/linux-loong64@0.21.5': optional: true '@esbuild/linux-loong64@0.25.10': optional: true + '@esbuild/linux-loong64@0.25.11': + optional: true + '@esbuild/linux-mips64el@0.21.5': optional: true '@esbuild/linux-mips64el@0.25.10': optional: true + '@esbuild/linux-mips64el@0.25.11': + optional: true + '@esbuild/linux-ppc64@0.21.5': optional: true '@esbuild/linux-ppc64@0.25.10': optional: true + '@esbuild/linux-ppc64@0.25.11': + optional: true + '@esbuild/linux-riscv64@0.21.5': optional: true '@esbuild/linux-riscv64@0.25.10': optional: true + '@esbuild/linux-riscv64@0.25.11': + optional: true + '@esbuild/linux-s390x@0.21.5': optional: true '@esbuild/linux-s390x@0.25.10': optional: true + '@esbuild/linux-s390x@0.25.11': + optional: true + '@esbuild/linux-x64@0.21.5': optional: true '@esbuild/linux-x64@0.25.10': optional: true + '@esbuild/linux-x64@0.25.11': + optional: true + '@esbuild/netbsd-arm64@0.25.10': optional: true + '@esbuild/netbsd-arm64@0.25.11': + optional: true + '@esbuild/netbsd-x64@0.21.5': optional: true '@esbuild/netbsd-x64@0.25.10': optional: true + '@esbuild/netbsd-x64@0.25.11': + optional: true + '@esbuild/openbsd-arm64@0.25.10': optional: true + '@esbuild/openbsd-arm64@0.25.11': + optional: true + '@esbuild/openbsd-x64@0.21.5': optional: true '@esbuild/openbsd-x64@0.25.10': optional: true + '@esbuild/openbsd-x64@0.25.11': + optional: true + '@esbuild/openharmony-arm64@0.25.10': optional: true + '@esbuild/openharmony-arm64@0.25.11': + optional: true + '@esbuild/sunos-x64@0.21.5': optional: true '@esbuild/sunos-x64@0.25.10': optional: true + '@esbuild/sunos-x64@0.25.11': + optional: true + '@esbuild/win32-arm64@0.21.5': optional: true '@esbuild/win32-arm64@0.25.10': optional: true + '@esbuild/win32-arm64@0.25.11': + optional: true + '@esbuild/win32-ia32@0.21.5': optional: true '@esbuild/win32-ia32@0.25.10': optional: true + '@esbuild/win32-ia32@0.25.11': + optional: true + '@esbuild/win32-x64@0.21.5': optional: true '@esbuild/win32-x64@0.25.10': optional: true + '@esbuild/win32-x64@0.25.11': + optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.9.1)': dependencies: eslint: 9.9.1 @@ -3036,66 +3404,132 @@ snapshots: '@rollup/rollup-android-arm-eabi@4.50.1': optional: true + '@rollup/rollup-android-arm-eabi@4.52.5': + optional: true + '@rollup/rollup-android-arm64@4.50.1': optional: true + '@rollup/rollup-android-arm64@4.52.5': + optional: true + '@rollup/rollup-darwin-arm64@4.50.1': optional: true + '@rollup/rollup-darwin-arm64@4.52.5': + optional: true + '@rollup/rollup-darwin-x64@4.50.1': optional: true + '@rollup/rollup-darwin-x64@4.52.5': + optional: true + '@rollup/rollup-freebsd-arm64@4.50.1': optional: true + '@rollup/rollup-freebsd-arm64@4.52.5': + optional: true + '@rollup/rollup-freebsd-x64@4.50.1': optional: true + '@rollup/rollup-freebsd-x64@4.52.5': + optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.50.1': optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.52.5': + optional: true + '@rollup/rollup-linux-arm-musleabihf@4.50.1': optional: true + '@rollup/rollup-linux-arm-musleabihf@4.52.5': + optional: true + '@rollup/rollup-linux-arm64-gnu@4.50.1': optional: true + '@rollup/rollup-linux-arm64-gnu@4.52.5': + optional: true + '@rollup/rollup-linux-arm64-musl@4.50.1': optional: true + '@rollup/rollup-linux-arm64-musl@4.52.5': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.52.5': + optional: true + '@rollup/rollup-linux-loongarch64-gnu@4.50.1': optional: true '@rollup/rollup-linux-ppc64-gnu@4.50.1': optional: true + '@rollup/rollup-linux-ppc64-gnu@4.52.5': + optional: true + '@rollup/rollup-linux-riscv64-gnu@4.50.1': optional: true + '@rollup/rollup-linux-riscv64-gnu@4.52.5': + optional: true + '@rollup/rollup-linux-riscv64-musl@4.50.1': optional: true + '@rollup/rollup-linux-riscv64-musl@4.52.5': + optional: true + '@rollup/rollup-linux-s390x-gnu@4.50.1': optional: true + '@rollup/rollup-linux-s390x-gnu@4.52.5': + optional: true + '@rollup/rollup-linux-x64-gnu@4.50.1': optional: true + '@rollup/rollup-linux-x64-gnu@4.52.5': + optional: true + '@rollup/rollup-linux-x64-musl@4.50.1': optional: true + '@rollup/rollup-linux-x64-musl@4.52.5': + optional: true + '@rollup/rollup-openharmony-arm64@4.50.1': optional: true + '@rollup/rollup-openharmony-arm64@4.52.5': + optional: true + '@rollup/rollup-win32-arm64-msvc@4.50.1': optional: true + '@rollup/rollup-win32-arm64-msvc@4.52.5': + optional: true + '@rollup/rollup-win32-ia32-msvc@4.50.1': optional: true + '@rollup/rollup-win32-ia32-msvc@4.52.5': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.52.5': + optional: true + '@rollup/rollup-win32-x64-msvc@4.50.1': optional: true + '@rollup/rollup-win32-x64-msvc@4.52.5': + optional: true + '@stylistic/eslint-plugin-js@1.8.0(eslint@9.9.1)': dependencies: '@types/eslint': 8.56.12 @@ -3120,24 +3554,24 @@ snapshots: typescript: 5.5.4 typescript-eslint: 8.26.0(eslint@9.9.1)(typescript@5.5.4) - '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.0(svelte@packages+svelte)(vite@7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': + '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.0(svelte@packages+svelte)(vite@7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.0(svelte@packages+svelte)(vite@7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + '@sveltejs/vite-plugin-svelte': 6.2.0(svelte@packages+svelte)(vite@7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) debug: 4.4.1 svelte: link:packages/svelte - vite: 7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@6.2.0(svelte@packages+svelte)(vite@7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': + '@sveltejs/vite-plugin-svelte@6.2.0(svelte@packages+svelte)(vite@7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.0(svelte@packages+svelte)(vite@7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.0(svelte@packages+svelte)(vite@7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)))(svelte@packages+svelte)(vite@7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) debug: 4.4.1 deepmerge: 4.3.1 magic-string: 0.30.17 svelte: link:packages/svelte - vite: 7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) - vitefu: 1.1.1(vite@7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + vite: 7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vitefu: 1.1.1(vite@7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) transitivePeerDependencies: - supports-color @@ -3202,11 +3636,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.43.0(typescript@5.5.4)': + '@typescript-eslint/project-service@8.46.2(typescript@5.5.4)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.5.4) - '@typescript-eslint/types': 8.43.0 - debug: 4.4.1 + '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.5.4) + '@typescript-eslint/types': 8.46.2 + debug: 4.4.3 typescript: 5.5.4 transitivePeerDependencies: - supports-color @@ -3216,12 +3650,12 @@ snapshots: '@typescript-eslint/types': 8.26.0 '@typescript-eslint/visitor-keys': 8.26.0 - '@typescript-eslint/scope-manager@8.43.0': + '@typescript-eslint/scope-manager@8.46.2': dependencies: - '@typescript-eslint/types': 8.43.0 - '@typescript-eslint/visitor-keys': 8.43.0 + '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/visitor-keys': 8.46.2 - '@typescript-eslint/tsconfig-utils@8.43.0(typescript@5.5.4)': + '@typescript-eslint/tsconfig-utils@8.46.2(typescript@5.5.4)': dependencies: typescript: 5.5.4 @@ -3238,7 +3672,7 @@ snapshots: '@typescript-eslint/types@8.26.0': {} - '@typescript-eslint/types@8.43.0': {} + '@typescript-eslint/types@8.46.2': {} '@typescript-eslint/typescript-estree@8.26.0(typescript@5.5.4)': dependencies: @@ -3254,17 +3688,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.43.0(typescript@5.5.4)': + '@typescript-eslint/typescript-estree@8.46.2(typescript@5.5.4)': dependencies: - '@typescript-eslint/project-service': 8.43.0(typescript@5.5.4) - '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.5.4) - '@typescript-eslint/types': 8.43.0 - '@typescript-eslint/visitor-keys': 8.43.0 - debug: 4.4.1 + '@typescript-eslint/project-service': 8.46.2(typescript@5.5.4) + '@typescript-eslint/tsconfig-utils': 8.46.2(typescript@5.5.4) + '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/visitor-keys': 8.46.2 + debug: 4.4.3 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 - semver: 7.7.2 + semver: 7.7.3 ts-api-utils: 2.1.0(typescript@5.5.4) typescript: 5.5.4 transitivePeerDependencies: @@ -3281,12 +3715,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.43.0(eslint@9.9.1)(typescript@5.5.4)': + '@typescript-eslint/utils@8.46.2(eslint@9.9.1)(typescript@5.5.4)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.9.1) - '@typescript-eslint/scope-manager': 8.43.0 - '@typescript-eslint/types': 8.43.0 - '@typescript-eslint/typescript-estree': 8.43.0(typescript@5.5.4) + '@typescript-eslint/scope-manager': 8.46.2 + '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.5.4) eslint: 9.9.1 typescript: 5.5.4 transitivePeerDependencies: @@ -3297,9 +3731,9 @@ snapshots: '@typescript-eslint/types': 8.26.0 eslint-visitor-keys: 4.2.1 - '@typescript-eslint/visitor-keys@8.43.0': + '@typescript-eslint/visitor-keys@8.46.2': dependencies: - '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/types': 8.46.2 eslint-visitor-keys: 4.2.1 '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@20.19.17)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0))': @@ -3522,6 +3956,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.3: + dependencies: + ms: 2.1.3 + decimal.js@10.4.3: {} deep-eql@5.0.2: {} @@ -3573,7 +4011,7 @@ snapshots: enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 - tapable: 2.2.3 + tapable: 2.3.0 enquirer@2.4.1: dependencies: @@ -3641,12 +4079,41 @@ snapshots: '@esbuild/win32-ia32': 0.25.10 '@esbuild/win32-x64': 0.25.10 + esbuild@0.25.11: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.11 + '@esbuild/android-arm': 0.25.11 + '@esbuild/android-arm64': 0.25.11 + '@esbuild/android-x64': 0.25.11 + '@esbuild/darwin-arm64': 0.25.11 + '@esbuild/darwin-x64': 0.25.11 + '@esbuild/freebsd-arm64': 0.25.11 + '@esbuild/freebsd-x64': 0.25.11 + '@esbuild/linux-arm': 0.25.11 + '@esbuild/linux-arm64': 0.25.11 + '@esbuild/linux-ia32': 0.25.11 + '@esbuild/linux-loong64': 0.25.11 + '@esbuild/linux-mips64el': 0.25.11 + '@esbuild/linux-ppc64': 0.25.11 + '@esbuild/linux-riscv64': 0.25.11 + '@esbuild/linux-s390x': 0.25.11 + '@esbuild/linux-x64': 0.25.11 + '@esbuild/netbsd-arm64': 0.25.11 + '@esbuild/netbsd-x64': 0.25.11 + '@esbuild/openbsd-arm64': 0.25.11 + '@esbuild/openbsd-x64': 0.25.11 + '@esbuild/openharmony-arm64': 0.25.11 + '@esbuild/sunos-x64': 0.25.11 + '@esbuild/win32-arm64': 0.25.11 + '@esbuild/win32-ia32': 0.25.11 + '@esbuild/win32-x64': 0.25.11 + escape-string-regexp@4.0.0: {} eslint-compat-utils@0.5.1(eslint@9.9.1): dependencies: eslint: 9.9.1 - semver: 7.7.2 + semver: 7.7.3 eslint-config-prettier@9.1.0(eslint@9.9.1): dependencies: @@ -3664,15 +4131,15 @@ snapshots: eslint-plugin-n@17.16.1(eslint@9.9.1)(typescript@5.5.4): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.9.1) - '@typescript-eslint/utils': 8.43.0(eslint@9.9.1)(typescript@5.5.4) + '@typescript-eslint/utils': 8.46.2(eslint@9.9.1)(typescript@5.5.4) enhanced-resolve: 5.18.3 eslint: 9.9.1 eslint-plugin-es-x: 7.8.0(eslint@9.9.1) - get-tsconfig: 4.10.1 + get-tsconfig: 4.12.0 globals: 15.15.0 ignore: 5.3.2 minimatch: 9.0.5 - semver: 7.7.2 + semver: 7.7.3 ts-declaration-location: 1.0.7(typescript@5.5.4) transitivePeerDependencies: - supports-color @@ -3864,7 +4331,7 @@ snapshots: function-bind@1.1.2: {} - get-tsconfig@4.10.1: + get-tsconfig@4.12.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -4408,6 +4875,34 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.50.1 fsevents: 2.3.3 + rollup@4.52.5: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.52.5 + '@rollup/rollup-android-arm64': 4.52.5 + '@rollup/rollup-darwin-arm64': 4.52.5 + '@rollup/rollup-darwin-x64': 4.52.5 + '@rollup/rollup-freebsd-arm64': 4.52.5 + '@rollup/rollup-freebsd-x64': 4.52.5 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.5 + '@rollup/rollup-linux-arm-musleabihf': 4.52.5 + '@rollup/rollup-linux-arm64-gnu': 4.52.5 + '@rollup/rollup-linux-arm64-musl': 4.52.5 + '@rollup/rollup-linux-loong64-gnu': 4.52.5 + '@rollup/rollup-linux-ppc64-gnu': 4.52.5 + '@rollup/rollup-linux-riscv64-gnu': 4.52.5 + '@rollup/rollup-linux-riscv64-musl': 4.52.5 + '@rollup/rollup-linux-s390x-gnu': 4.52.5 + '@rollup/rollup-linux-x64-gnu': 4.52.5 + '@rollup/rollup-linux-x64-musl': 4.52.5 + '@rollup/rollup-openharmony-arm64': 4.52.5 + '@rollup/rollup-win32-arm64-msvc': 4.52.5 + '@rollup/rollup-win32-ia32-msvc': 4.52.5 + '@rollup/rollup-win32-x64-gnu': 4.52.5 + '@rollup/rollup-win32-x64-msvc': 4.52.5 + fsevents: 2.3.3 + rrweb-cssom@0.7.1: {} run-applescript@7.0.0: {} @@ -4437,6 +4932,8 @@ snapshots: semver@7.7.2: {} + semver@7.7.3: {} + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -4526,7 +5023,7 @@ snapshots: symbol-tree@3.2.4: {} - tapable@2.2.3: {} + tapable@2.3.0: {} term-size@2.2.1: {} @@ -4638,15 +5135,15 @@ snapshots: v8-natives@1.2.5: {} - vite-dev-rpc@1.1.0(vite@7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): + vite-dev-rpc@1.1.0(vite@7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): dependencies: birpc: 2.5.0 - vite: 7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) - vite-hot-client: 2.1.0(vite@7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + vite: 7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite-hot-client: 2.1.0(vite@7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) - vite-hot-client@2.1.0(vite@7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): + vite-hot-client@2.1.0(vite@7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): dependencies: - vite: 7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) vite-node@2.1.9(@types/node@20.19.17)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): dependencies: @@ -4666,12 +5163,12 @@ snapshots: - supports-color - terser - vite-plugin-devtools-json@1.0.0(vite@7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): + vite-plugin-devtools-json@1.0.0(vite@7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): dependencies: uuid: 11.1.0 - vite: 7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) - vite-plugin-inspect@11.3.3(vite@7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): + vite-plugin-inspect@11.3.3(vite@7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): dependencies: ansis: 4.1.0 debug: 4.4.1 @@ -4681,8 +5178,8 @@ snapshots: perfect-debounce: 2.0.0 sirv: 3.0.2 unplugin-utils: 0.3.0 - vite: 7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) - vite-dev-rpc: 1.1.0(vite@7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) + vite: 7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite-dev-rpc: 1.1.0(vite@7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)) transitivePeerDependencies: - supports-color @@ -4698,13 +5195,13 @@ snapshots: sass: 1.70.0 terser: 5.27.0 - vite@7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): + vite@7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): dependencies: - esbuild: 0.25.10 + esbuild: 0.25.11 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.50.1 + rollup: 4.52.5 tinyglobby: 0.2.15 optionalDependencies: '@types/node': 24.5.2 @@ -4713,9 +5210,9 @@ snapshots: sass: 1.70.0 terser: 5.27.0 - vitefu@1.1.1(vite@7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): + vitefu@1.1.1(vite@7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0)): optionalDependencies: - vite: 7.1.5(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) + vite: 7.1.11(@types/node@24.5.2)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0) vitest@2.1.9(@types/node@20.19.17)(jsdom@25.0.1)(lightningcss@1.23.0)(sass@1.70.0)(terser@5.27.0): dependencies: From 4b32d6d8b30fdaf07e9bf4497c39283b5eb421d9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 22 Oct 2025 03:04:35 -0700 Subject: [PATCH 15/58] fix: make `$inspect` logs come from the callsite (#17001) * fix: make `$inspect` logs come from the callsite * default to showing stack trace * reuse id * DRY out, improve server-side inspect * update docs * ugh * fix the dang tests * ugh windows is there no punch bowl you won't poop in? * argh * how about this * alright finally --- .changeset/silly-penguins-sleep.md | 5 +++ documentation/docs/02-runes/07-$inspect.md | 9 +---- .../client/visitors/CallExpression.js | 24 ++++++++++-- .../server/visitors/CallExpression.js | 14 +++++-- .../src/compiler/phases/3-transform/utils.js | 39 +++++++------------ .../svelte/src/internal/client/dev/inspect.js | 18 +++++++-- .../svelte/src/internal/client/dev/tracing.js | 11 +++++- packages/svelte/src/internal/server/index.js | 9 ----- packages/svelte/tests/helpers.js | 20 ++++++++++ .../_config.js | 2 +- .../samples/inspect-deep-array/_config.js | 3 +- .../samples/inspect-deep/_config.js | 3 +- .../samples/inspect-derived-2/_config.js | 8 ++-- .../samples/inspect-derived-3/_config.js | 12 +++--- .../samples/inspect-map-set/_config.js | 11 +++--- .../samples/inspect-multiple/_config.js | 12 +++++- .../samples/inspect-nested-effect/_config.js | 3 +- .../samples/inspect-nested-state/_config.js | 8 ++-- .../samples/inspect-new-property/_config.js | 10 ++++- .../samples/inspect-recursive-2/_config.js | 2 +- .../samples/inspect-recursive/_config.js | 9 ++++- .../runtime-runes/samples/inspect/_config.js | 3 +- 22 files changed, 152 insertions(+), 83 deletions(-) create mode 100644 .changeset/silly-penguins-sleep.md diff --git a/.changeset/silly-penguins-sleep.md b/.changeset/silly-penguins-sleep.md new file mode 100644 index 0000000000..f397f1e8ba --- /dev/null +++ b/.changeset/silly-penguins-sleep.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: make `$inspect` logs come from the callsite diff --git a/documentation/docs/02-runes/07-$inspect.md b/documentation/docs/02-runes/07-$inspect.md index 13ac8b79a3..6d47e30e27 100644 --- a/documentation/docs/02-runes/07-$inspect.md +++ b/documentation/docs/02-runes/07-$inspect.md @@ -18,6 +18,8 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t ``` +On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations). + ## $inspect(...).with `$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect` ([demo](/playground/untitled#H4sIAAAAAAAACkVQ24qDMBD9lSEUqlTqPlsj7ON-w7pQG8c2VCchmVSK-O-bKMs-DefKYRYx6BG9qL4XQd2EohKf1opC8Nsm4F84MkbsTXAqMbVXTltuWmp5RAZlAjFIOHjuGLOP_BKVqB00eYuKs82Qn2fNjyxLtcWeyUE2sCRry3qATQIpJRyD7WPVMf9TW-7xFu53dBcoSzAOrsqQNyOe2XUKr0Xi5kcMvdDB2wSYO-I9vKazplV1-T-d6ltgNgSG1KjVUy7ZtmdbdjqtzRcphxMS1-XubOITJtPrQWMvKnYB15_1F7KKadA_AQAA)): @@ -36,13 +38,6 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t ``` -A convenient way to find the origin of some change is to pass `console.trace` to `with`: - -```js -// @errors: 2304 -$inspect(stuff).with(console.trace); -``` - ## $inspect.trace(...) This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect]($effect) or a [derived]($derived), information will be printed to the console about which pieces of reactive state caused the effect to fire. diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index bf9a09bb74..ae60f3be40 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -1,10 +1,10 @@ -/** @import { CallExpression, Expression } from 'estree' */ +/** @import { CallExpression, Expression, MemberExpression } from 'estree' */ /** @import { Context } from '../types' */ import { dev, is_ignored } from '../../../../state.js'; import * as b from '#compiler/builders'; import { get_rune } from '../../../scope.js'; -import { transform_inspect_rune } from '../../utils.js'; import { should_proxy } from '../utils.js'; +import { get_inspect_args } from '../../utils.js'; /** * @param {CallExpression} node @@ -73,7 +73,7 @@ export function CallExpression(node, context) { case '$inspect': case '$inspect().with': - return transform_inspect_rune(node, context); + return transform_inspect_rune(rune, node, context); } if ( @@ -104,3 +104,21 @@ export function CallExpression(node, context) { context.next(); } + +/** + * @param {'$inspect' | '$inspect().with'} rune + * @param {CallExpression} node + * @param {Context} context + */ +function transform_inspect_rune(rune, node, context) { + if (!dev) return b.empty; + + const { args, inspector } = get_inspect_args(rune, node, context.visit); + + // by passing an arrow function, the log appears to come from the `$inspect` callsite + // rather than the `inspect.js` file containing the utility + const id = b.id('$$args'); + const fn = b.arrow([b.rest(id)], b.call(inspector, b.spread(id))); + + return b.call('$.inspect', b.thunk(b.array(args)), fn, rune === '$inspect' && b.true); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js index d53b631aa5..bba6511eec 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js @@ -1,9 +1,9 @@ -/** @import { CallExpression, Expression } from 'estree' */ +/** @import { CallExpression, Expression, MemberExpression } from 'estree' */ /** @import { Context } from '../types.js' */ -import { is_ignored } from '../../../../state.js'; +import { dev, is_ignored } from '../../../../state.js'; import * as b from '#compiler/builders'; import { get_rune } from '../../../scope.js'; -import { transform_inspect_rune } from '../../utils.js'; +import { get_inspect_args } from '../../utils.js'; /** * @param {CallExpression} node @@ -51,7 +51,13 @@ export function CallExpression(node, context) { } if (rune === '$inspect' || rune === '$inspect().with') { - return transform_inspect_rune(node, context); + if (!dev) return b.empty; + + const { args, inspector } = get_inspect_args(rune, node, context.visit); + + return rune === '$inspect' + ? b.call(inspector, b.literal('$inspect('), ...args, b.literal(')')) + : b.call(inspector, b.literal('init'), ...args); } context.next(); diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js index 1445ce3aa6..dfc2ab1de1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/utils.js @@ -1,7 +1,7 @@ /** @import { Context } from 'zimmerframe' */ /** @import { TransformState } from './types.js' */ /** @import { AST, Binding, Namespace, ValidatedCompileOptions } from '#compiler' */ -/** @import { Node, Expression, CallExpression } from 'estree' */ +/** @import { Node, Expression, CallExpression, MemberExpression } from 'estree' */ import { regex_ends_with_whitespaces, regex_not_whitespace, @@ -452,30 +452,19 @@ export function determine_namespace_for_children(node, namespace) { } /** - * @template {TransformState} T + * @param {'$inspect' | '$inspect().with'} rune * @param {CallExpression} node - * @param {Context} context + * @param {(node: AST.SvelteNode) => AST.SvelteNode} visit */ -export function transform_inspect_rune(node, context) { - const { state, visit } = context; - const as_fn = state.options.generate === 'client'; - - if (!dev) return b.empty; - - if (node.callee.type === 'MemberExpression') { - const raw_inspect_args = /** @type {CallExpression} */ (node.callee.object).arguments; - const inspect_args = - /** @type {Array} */ - (raw_inspect_args.map((arg) => visit(arg))); - const with_arg = /** @type {Expression} */ (visit(node.arguments[0])); - - return b.call( - '$.inspect', - as_fn ? b.thunk(b.array(inspect_args)) : b.array(inspect_args), - with_arg - ); - } else { - const arg = node.arguments.map((arg) => /** @type {Expression} */ (visit(arg))); - return b.call('$.inspect', as_fn ? b.thunk(b.array(arg)) : b.array(arg)); - } +export function get_inspect_args(rune, node, visit) { + const call = + rune === '$inspect' + ? node + : /** @type {CallExpression} */ (/** @type {MemberExpression} */ (node.callee).object); + + return { + args: call.arguments.map((arg) => /** @type {Expression} */ (visit(arg))), + inspector: + rune === '$inspect' ? 'console.log' : /** @type {Expression} */ (visit(node.arguments[0])) + }; } diff --git a/packages/svelte/src/internal/client/dev/inspect.js b/packages/svelte/src/internal/client/dev/inspect.js index f79cf47299..db7ab0d976 100644 --- a/packages/svelte/src/internal/client/dev/inspect.js +++ b/packages/svelte/src/internal/client/dev/inspect.js @@ -2,13 +2,14 @@ import { UNINITIALIZED } from '../../../constants.js'; import { snapshot } from '../../shared/clone.js'; import { inspect_effect, render_effect, validate_effect } from '../reactivity/effects.js'; import { untrack } from '../runtime.js'; +import { get_stack } from './tracing.js'; /** * @param {() => any[]} get_value - * @param {Function} [inspector] + * @param {Function} inspector + * @param {boolean} show_stack */ -// eslint-disable-next-line no-console -export function inspect(get_value, inspector = console.log) { +export function inspect(get_value, inspector, show_stack = false) { validate_effect('$inspect'); let initial = true; @@ -28,7 +29,16 @@ export function inspect(get_value, inspector = console.log) { var snap = snapshot(value, true, true); untrack(() => { - inspector(initial ? 'init' : 'update', ...snap); + if (show_stack) { + inspector(...snap); + + if (!initial) { + // eslint-disable-next-line no-console + console.log(get_stack('UpdatedAt')); + } + } else { + inspector(initial ? 'init' : 'update', ...snap); + } }); initial = false; diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js index 673a710fac..95baefc64a 100644 --- a/packages/svelte/src/internal/client/dev/tracing.js +++ b/packages/svelte/src/internal/client/dev/tracing.js @@ -134,7 +134,16 @@ export function trace(label, fn) { * @returns {Error & { stack: string } | null} */ export function get_stack(label) { + // @ts-ignore stackTraceLimit doesn't exist everywhere + const limit = Error.stackTraceLimit; + + // @ts-ignore + Error.stackTraceLimit = Infinity; let error = Error(); + + // @ts-ignore + Error.stackTraceLimit = limit; + const stack = error.stack; if (!stack) return null; @@ -151,7 +160,7 @@ export function get_stack(label) { if (line.includes('validate_each_keys')) { return null; } - if (line.includes('svelte/src/internal')) { + if (line.includes('svelte/src/internal') || line.includes('svelte\\src\\internal')) { continue; } new_lines.push(line); diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 50bb629c4d..74a90a8600 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -418,15 +418,6 @@ export function ensure_array_like(array_like_or_iterator) { return []; } -/** - * @param {any[]} args - * @param {Function} [inspect] - */ -// eslint-disable-next-line no-console -export function inspect(args, inspect = console.log) { - inspect('init', ...args); -} - /** * @template V * @param {() => V} get_value diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js index 7a9640636c..bf708878a3 100644 --- a/packages/svelte/tests/helpers.js +++ b/packages/svelte/tests/helpers.js @@ -197,6 +197,26 @@ export const fragments = /** @type {'html' | 'tree'} */ (process.env.FRAGMENTS) export const async_mode = process.env.SVELTE_NO_ASYNC !== 'true'; +/** + * @param {any[]} logs + */ +export function normalise_inspect_logs(logs) { + return logs.map((log) => { + if (log instanceof Error) { + const last_line = log.stack + ?.trim() + .split('\n') + .filter((line) => !line.includes('at Module.get_stack'))[1]; + + const match = last_line && /(at .+) /.exec(last_line); + + return match && match[1]; + } + + return log; + }); +} + /** * @param {any[]} logs */ diff --git a/packages/svelte/tests/runtime-runes/samples/class-private-fields-reassigned-this/_config.js b/packages/svelte/tests/runtime-runes/samples/class-private-fields-reassigned-this/_config.js index 88b806c0f0..0e6bd73220 100644 --- a/packages/svelte/tests/runtime-runes/samples/class-private-fields-reassigned-this/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/class-private-fields-reassigned-this/_config.js @@ -5,6 +5,6 @@ export default test({ dev: true }, async test({ assert, logs }) { - assert.deepEqual(logs, ['init', 1, 'init', 1]); + assert.deepEqual(logs, [1, 1]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js index 49f1b5de41..1b331f5b40 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-deep-array/_config.js @@ -1,5 +1,6 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; +import { normalise_inspect_logs } from '../../../helpers.js'; export default test({ compileOptions: { @@ -13,6 +14,6 @@ export default test({ button?.click(); }); - assert.deepEqual(logs, ['init', [1, 2, 3, 7], 'update', [2, 3, 7]]); + assert.deepEqual(normalise_inspect_logs(logs), [[1, 2, 3, 7], [2, 3, 7], 'at Object.doSplice']); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-deep/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-deep/_config.js index f7480b0e7b..89b01da499 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-deep/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-deep/_config.js @@ -1,3 +1,4 @@ +import { normalise_inspect_logs } from '../../../helpers.js'; import { test } from '../../test'; export default test({ @@ -6,6 +7,6 @@ export default test({ }, async test({ assert, logs }) { - assert.deepEqual(logs, ['init', undefined, 'update', [{}]]); + assert.deepEqual(normalise_inspect_logs(logs), [undefined, [{}], 'at $effect']); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/_config.js index 9474397f7f..3742382759 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-derived-2/_config.js @@ -1,5 +1,6 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; +import { normalise_inspect_logs } from '../../../helpers.js'; export default test({ compileOptions: { @@ -14,8 +15,7 @@ export default test({ }); assert.htmlEqual(target.innerHTML, `\n1`); - assert.deepEqual(logs, [ - 'init', + assert.deepEqual(normalise_inspect_logs(logs), [ { data: { derived: 0, @@ -23,14 +23,14 @@ export default test({ }, derived: [] }, - 'update', { data: { derived: 0, list: [1] }, derived: [1] - } + }, + 'at HTMLButtonElement.Main.button.__click' ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-derived-3/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-derived-3/_config.js index d2226f433e..017de6c0c7 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-derived-3/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-derived-3/_config.js @@ -1,5 +1,6 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; +import { normalise_inspect_logs } from '../../../helpers.js'; export default test({ compileOptions: { @@ -13,22 +14,19 @@ export default test({ button?.click(); }); - assert.deepEqual(logs, [ - 'init', + assert.deepEqual(normalise_inspect_logs(logs), [ '0', true, - 'init', '1', false, - 'init', '2', false, - 'update', '0', false, - 'update', + 'at $effect', '1', - true + true, + 'at $effect' ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-map-set/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-map-set/_config.js index 2052cb7f13..2f91e84288 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-map-set/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-map-set/_config.js @@ -1,5 +1,6 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; +import { normalise_inspect_logs } from '../../../helpers.js'; export default test({ compileOptions: { @@ -12,15 +13,13 @@ export default test({ btn2.click(); flushSync(); - assert.deepEqual(logs, [ - 'init', + assert.deepEqual(normalise_inspect_logs(logs), [ new Map(), - 'init', new Set(), - 'update', new Map([['a', 'a']]), - 'update', - new Set(['a']) + 'at SvelteMap.set', + new Set(['a']), + 'at SvelteSet.add' ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-multiple/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-multiple/_config.js index fc9a0cda9a..6886f5e53e 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-multiple/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-multiple/_config.js @@ -1,3 +1,4 @@ +import { normalise_inspect_logs } from '../../../helpers.js'; import { test } from '../../test'; export default test({ @@ -11,6 +12,15 @@ export default test({ b2.click(); await Promise.resolve(); - assert.deepEqual(logs, ['init', 0, 0, 'update', 1, 0, 'update', 1, 1]); + assert.deepEqual(normalise_inspect_logs(logs), [ + 0, + 0, + 1, + 0, + 'at HTMLButtonElement.', + 1, + 1, + 'at HTMLButtonElement.' + ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-nested-effect/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-nested-effect/_config.js index 82429e5e36..86e65d5044 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-nested-effect/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-nested-effect/_config.js @@ -1,3 +1,4 @@ +import { normalise_inspect_logs } from '../../../helpers.js'; import { test } from '../../test'; export default test({ @@ -6,6 +7,6 @@ export default test({ }, async test({ assert, logs }) { - assert.deepEqual(logs, ['init', 0, 'update', 1]); + assert.deepEqual(normalise_inspect_logs(logs), [0, 1, 'at $effect']); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-nested-state/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-nested-state/_config.js index e4d9fb5013..34cd74d780 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-nested-state/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-nested-state/_config.js @@ -1,3 +1,4 @@ +import { normalise_inspect_logs } from '../../../helpers.js'; import { test } from '../../test'; export default test({ @@ -10,13 +11,12 @@ export default test({ b1.click(); await Promise.resolve(); - assert.deepEqual(logs, [ - 'init', + assert.deepEqual(normalise_inspect_logs(logs), [ { x: { count: 0 } }, [{ count: 0 }], - 'update', { x: { count: 1 } }, - [{ count: 1 }] + [{ count: 1 }], + 'at HTMLButtonElement.' ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js index a85972a0f9..8134044b16 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js @@ -1,3 +1,4 @@ +import { normalise_inspect_logs } from '../../../helpers.js'; import { test } from '../../test'; export default test({ @@ -10,6 +11,13 @@ export default test({ btn.click(); await Promise.resolve(); - assert.deepEqual(logs, ['init', {}, 'init', [], 'update', { x: 'hello' }, 'update', ['hello']]); + assert.deepEqual(normalise_inspect_logs(logs), [ + {}, + [], + { x: 'hello' }, + 'at HTMLButtonElement.on_click', + ['hello'], + 'at HTMLButtonElement.on_click' + ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-recursive-2/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-recursive-2/_config.js index ab49697195..1bfc2dc68f 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-recursive-2/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-recursive-2/_config.js @@ -18,6 +18,6 @@ export default test({ }; b.a.b = b; - assert.deepEqual(logs, ['init', a, 'init', b]); + assert.deepEqual(logs, [a, b]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js index e35917b1f3..9d95956e7d 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js @@ -1,3 +1,4 @@ +import { normalise_inspect_logs } from '../../../helpers.js'; import { test } from '../../test'; export default test({ @@ -11,6 +12,12 @@ export default test({ btn.click(); await Promise.resolve(); - assert.deepEqual(logs, ['init', [], 'update', [{}], 'update', [{}, {}]]); + assert.deepEqual(normalise_inspect_logs(logs), [ + [], + [{}], + 'at HTMLButtonElement.on_click', + [{}, {}], + 'at HTMLButtonElement.on_click' + ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect/_config.js index 09a921abee..c05c4b15c4 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect/_config.js @@ -1,3 +1,4 @@ +import { normalise_inspect_logs } from '../../../helpers.js'; import { test } from '../../test'; export default test({ @@ -11,6 +12,6 @@ export default test({ b2.click(); await Promise.resolve(); - assert.deepEqual(logs, ['init', 0, 'update', 1]); + assert.deepEqual(normalise_inspect_logs(logs), [0, 1, 'at HTMLButtonElement.']); } }); From 7e40186a5fc08ddf0c363dc8db49aae5a652a0b0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 22 Oct 2025 03:07:07 -0700 Subject: [PATCH 16/58] fix: run batch until complete (#16971) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit at present we only call batch.increment() when something async happens if we're not inside a pending boundary, but that's incorrect — it means that a batch is committed before everything resolves. When work inside a pending boundary does resolve, the batch becomes a zombie. At the same time, we don't handle effects inside pending boundaries correctly. They should be deferred until the boundary (and all its parents) are ready. This PR attempts to fix that — during traversal, when we exit a pending boundary, any effects that were collected get deferred until the next flush. We also distinguish between batch.#pending (any ongoing async work) and batch.#blocking_pending (any async work that should prevent effects outside pending boundaries from being flushed). --- .changeset/mighty-mice-call.md | 5 + .../src/internal/client/dom/blocks/async.js | 5 + .../internal/client/dom/blocks/boundary.js | 7 - .../src/internal/client/reactivity/async.js | 11 +- .../src/internal/client/reactivity/batch.js | 172 +++++++++++------- .../internal/client/reactivity/deriveds.js | 29 ++- .../samples/async-abort-signal/_config.js | 2 +- .../main.svelte | 2 +- .../main.svelte | 2 +- .../_config.js | 4 +- .../samples/async-block-resolve/_config.js | 63 +++++++ .../samples/async-block-resolve/main.svelte | 36 ++++ .../async-effect-after-await/Child.svelte | 6 +- .../async-effect-after-await/_config.js | 3 +- .../async-effect-after-boundary/Child.svelte | 5 + .../async-effect-after-boundary/_config.js | 16 ++ .../async-effect-after-boundary/main.svelte | 22 +++ .../samples/async-resolve-stale/_config.js | 13 +- .../samples/async-resolve-stale/main.svelte | 37 ++-- 19 files changed, 326 insertions(+), 114 deletions(-) create mode 100644 .changeset/mighty-mice-call.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-block-resolve/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-block-resolve/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-after-boundary/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-after-boundary/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-effect-after-boundary/main.svelte diff --git a/.changeset/mighty-mice-call.md b/.changeset/mighty-mice-call.md new file mode 100644 index 0000000000..340b33bd4b --- /dev/null +++ b/.changeset/mighty-mice-call.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: keep batches alive until all async work is complete diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js index 6df0739918..5ee9d25bce 100644 --- a/packages/svelte/src/internal/client/dom/blocks/async.js +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -1,5 +1,6 @@ /** @import { TemplateNode, Value } from '#client' */ import { flatten } from '../../reactivity/async.js'; +import { Batch, current_batch } from '../../reactivity/batch.js'; import { get } from '../../runtime.js'; import { hydrate_next, @@ -18,8 +19,11 @@ import { get_boundary } from './boundary.js'; */ export function async(node, expressions, fn) { var boundary = get_boundary(); + var batch = /** @type {Batch} */ (current_batch); + var blocking = !boundary.is_pending(); boundary.update_pending_count(1); + batch.increment(blocking); var was_hydrating = hydrating; @@ -44,6 +48,7 @@ export function async(node, expressions, fn) { fn(node, ...values); } finally { boundary.update_pending_count(-1); + batch.decrement(blocking); } if (was_hydrating) { diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 026ffb36fc..3da9204571 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -291,13 +291,6 @@ export class Boundary { this.#anchor.before(this.#offscreen_fragment); this.#offscreen_fragment = null; } - - // TODO this feels like a little bit of a kludge, but until we - // overhaul the boundary/batch relationship it's probably - // the most pragmatic solution available to us - queue_micro_task(() => { - Batch.ensure().flush(); - }); } } diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 1d408744fd..fb836df989 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -218,10 +218,10 @@ export function unset_context() { export async function async_body(anchor, fn) { var boundary = get_boundary(); var batch = /** @type {Batch} */ (current_batch); - var pending = boundary.is_pending(); + var blocking = !boundary.is_pending(); boundary.update_pending_count(1); - if (!pending) batch.increment(); + batch.increment(blocking); var active = /** @type {Effect} */ (active_effect); @@ -254,12 +254,7 @@ export async function async_body(anchor, fn) { } boundary.update_pending_count(-1); - - if (pending) { - batch.flush(); - } else { - batch.decrement(); - } + batch.decrement(blocking); unset_context(); } diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 302d48d03f..91635bd5d2 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -11,7 +11,8 @@ import { RENDER_EFFECT, ROOT_EFFECT, MAYBE_DIRTY, - DERIVED + DERIVED, + BOUNDARY_EFFECT } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; import { deferred, define_property } from '../../shared/utils.js'; @@ -31,6 +32,16 @@ import { invoke_error_boundary } from '../error-handling.js'; import { old_values, source, update } from './sources.js'; import { inspect_effect, unlink_effect } from './effects.js'; +/** + * @typedef {{ + * parent: EffectTarget | null; + * effect: Effect | null; + * effects: Effect[]; + * render_effects: Effect[]; + * block_effects: Effect[]; + * }} EffectTarget + */ + /** @type {Set} */ const batches = new Set(); @@ -65,6 +76,8 @@ let is_flushing = false; export let is_flushing_sync = false; export class Batch { + committed = false; + /** * The current values of any sources that are updated in this batch * They keys of this map are identical to `this.#previous` @@ -91,6 +104,11 @@ export class Batch { */ #pending = 0; + /** + * The number of async effects that are currently in flight, _not_ inside a pending boundary + */ + #blocking_pending = 0; + /** * A deferred that resolves when the batch is committed, used with `settled()` * TODO replace with Promise.withResolvers once supported widely enough @@ -98,26 +116,6 @@ export class Batch { */ #deferred = null; - /** - * Template effects and `$effect.pre` effects, which run when - * a batch is committed - * @type {Effect[]} - */ - #render_effects = []; - - /** - * The same as `#render_effects`, but for `$effect` (which runs after) - * @type {Effect[]} - */ - #effects = []; - - /** - * Block effects, which may need to re-run on subsequent flushes - * in order to update internal sources (e.g. each block items) - * @type {Effect[]} - */ - #block_effects = []; - /** * Deferred effects (which run after async work has completed) that are DIRTY * @type {Effect[]} @@ -148,41 +146,37 @@ export class Batch { this.apply(); + /** @type {EffectTarget} */ + var target = { + parent: null, + effect: null, + effects: [], + render_effects: [], + block_effects: [] + }; + for (const root of root_effects) { - this.#traverse_effect_tree(root); + this.#traverse_effect_tree(root, target); } - // if there is no outstanding async work, commit - if (this.#pending === 0) { - // TODO we need this because we commit _then_ flush effects... - // maybe there's a way we can reverse the order? - var previous_batch_sources = batch_values; + this.#resolve(); - this.#commit(); - - var render_effects = this.#render_effects; - var effects = this.#effects; - - this.#render_effects = []; - this.#effects = []; - this.#block_effects = []; + if (this.#blocking_pending > 0) { + this.#defer_effects(target.effects); + this.#defer_effects(target.render_effects); + this.#defer_effects(target.block_effects); + } else { + // TODO append/detach blocks here, not in #commit // If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with // newly updated sources, which could lead to infinite loops when effects run over and over again. previous_batch = this; current_batch = null; - batch_values = previous_batch_sources; - flush_queued_effects(render_effects); - flush_queued_effects(effects); + flush_queued_effects(target.render_effects); + flush_queued_effects(target.effects); previous_batch = null; - - this.#deferred?.resolve(); - } else { - this.#defer_effects(this.#render_effects); - this.#defer_effects(this.#effects); - this.#defer_effects(this.#block_effects); } batch_values = null; @@ -192,8 +186,9 @@ export class Batch { * Traverse the effect tree, executing effects or stashing * them for later execution as appropriate * @param {Effect} root + * @param {EffectTarget} target */ - #traverse_effect_tree(root) { + #traverse_effect_tree(root, target) { root.f ^= CLEAN; var effect = root.first; @@ -205,15 +200,25 @@ export class Batch { var skip = is_skippable_branch || (flags & INERT) !== 0 || this.skipped_effects.has(effect); + if ((effect.f & BOUNDARY_EFFECT) !== 0 && effect.b?.is_pending()) { + target = { + parent: target, + effect, + effects: [], + render_effects: [], + block_effects: [] + }; + } + if (!skip && effect.fn !== null) { if (is_branch) { effect.f ^= CLEAN; } else if ((flags & EFFECT) !== 0) { - this.#effects.push(effect); + target.effects.push(effect); } else if (async_mode_flag && (flags & RENDER_EFFECT) !== 0) { - this.#render_effects.push(effect); + target.render_effects.push(effect); } else if (is_dirty(effect)) { - if ((effect.f & BLOCK_EFFECT) !== 0) this.#block_effects.push(effect); + if ((effect.f & BLOCK_EFFECT) !== 0) target.block_effects.push(effect); update_effect(effect); } @@ -229,6 +234,17 @@ export class Batch { effect = effect.next; while (effect === null && parent !== null) { + if (parent === target.effect) { + // TODO rather than traversing into pending boundaries and deferring the effects, + // could we just attach the effects _to_ the pending boundary and schedule them + // once the boundary is ready? + this.#defer_effects(target.effects); + this.#defer_effects(target.render_effects); + this.#defer_effects(target.block_effects); + + target = /** @type {EffectTarget} */ (target.parent); + } + effect = parent.next; parent = parent.parent; } @@ -246,8 +262,6 @@ export class Batch { // mark as clean so they get scheduled if they depend on pending async state set_signal_status(e, CLEAN); } - - effects.length = 0; } /** @@ -283,8 +297,8 @@ export class Batch { // this can happen if a new batch was created during `flush_effects()` return; } - } else if (this.#pending === 0) { - this.#commit(); + } else { + this.#resolve(); } this.deactivate(); @@ -300,16 +314,19 @@ export class Batch { } } - /** - * Append and remove branches to/from the DOM - */ - #commit() { - for (const fn of this.#callbacks) { - fn(); + #resolve() { + if (this.#blocking_pending === 0) { + // append/remove branches + for (const fn of this.#callbacks) fn(); + this.#callbacks.clear(); } - this.#callbacks.clear(); + if (this.#pending === 0) { + this.#commit(); + } + } + #commit() { // If there are other pending batches, they now need to be 'rebased' — // in other words, we re-run block/async effects with the newly // committed state, unless the batch in question has a more @@ -317,7 +334,17 @@ export class Batch { if (batches.size > 1) { this.#previous.clear(); - let is_earlier = true; + var previous_batch_values = batch_values; + var is_earlier = true; + + /** @type {EffectTarget} */ + var dummy_target = { + parent: null, + effect: null, + effects: [], + render_effects: [], + block_effects: [] + }; for (const batch of batches) { if (batch === this) { @@ -359,9 +386,11 @@ export class Batch { batch.apply(); for (const root of queued_root_effects) { - batch.#traverse_effect_tree(root); + batch.#traverse_effect_tree(root, dummy_target); } + // TODO do we need to do anything with `target`? defer block effects? + queued_root_effects = []; batch.deactivate(); } @@ -369,17 +398,31 @@ export class Batch { } current_batch = null; + batch_values = previous_batch_values; } + this.committed = true; batches.delete(this); + + this.#deferred?.resolve(); } - increment() { + /** + * + * @param {boolean} blocking + */ + increment(blocking) { this.#pending += 1; + if (blocking) this.#blocking_pending += 1; } - decrement() { + /** + * + * @param {boolean} blocking + */ + decrement(blocking) { this.#pending -= 1; + if (blocking) this.#blocking_pending -= 1; for (const e of this.#dirty_effects) { set_signal_status(e, DIRTY); @@ -391,6 +434,9 @@ export class Batch { schedule_effect(e); } + this.#dirty_effects = []; + this.#maybe_dirty_effects = []; + this.flush(); } diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 6aa9a1d9d9..06ae0f6d7a 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -127,7 +127,17 @@ export function async_derived(fn, location) { // If this code is changed at some point, make sure to still access the then property // of fn() to read any signals it might access, so that we track them as dependencies. // We call `unset_context` to undo any `save` calls that happen inside `fn()` - Promise.resolve(fn()).then(d.resolve, d.reject).then(unset_context); + Promise.resolve(fn()) + .then(d.resolve, d.reject) + .then(() => { + if (batch === current_batch && batch.committed) { + // if the batch was rejected as stale, we need to cleanup + // after any `$.save(...)` calls inside `fn()` + batch.deactivate(); + } + + unset_context(); + }); } catch (error) { d.reject(error); unset_context(); @@ -136,17 +146,16 @@ export function async_derived(fn, location) { if (DEV) current_async_effect = null; var batch = /** @type {Batch} */ (current_batch); - var pending = boundary.is_pending(); if (should_suspend) { + var blocking = !boundary.is_pending(); + boundary.update_pending_count(1); - if (!pending) { - batch.increment(); + batch.increment(blocking); - deferreds.get(batch)?.reject(STALE_REACTION); - deferreds.delete(batch); // delete to ensure correct order in Map iteration below - deferreds.set(batch, d); - } + deferreds.get(batch)?.reject(STALE_REACTION); + deferreds.delete(batch); // delete to ensure correct order in Map iteration below + deferreds.set(batch, d); } /** @@ -156,7 +165,7 @@ export function async_derived(fn, location) { const handler = (value, error = undefined) => { current_async_effect = null; - if (!pending) batch.activate(); + batch.activate(); if (error) { if (error !== STALE_REACTION) { @@ -193,7 +202,7 @@ export function async_derived(fn, location) { if (should_suspend) { boundary.update_pending_count(-1); - if (!pending) batch.decrement(); + batch.decrement(blocking); } }; diff --git a/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js b/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js index a947a91ab8..af49b1779c 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-abort-signal/_config.js @@ -6,7 +6,7 @@ export default test({ const [reset, resolve] = target.querySelectorAll('button'); reset.click(); - await settled(); + await tick(); assert.deepEqual(logs, ['aborted']); resolve.click(); diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte index e2f01a66c8..c0e4d862a8 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-2/main.svelte @@ -12,7 +12,7 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/main.svelte index 566ea60ec5..8f5e2862eb 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-binding-update-while-focused-3/main.svelte @@ -12,7 +12,7 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js index c5dae7fee2..ca5fd9ca89 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-block-reject-each-during-init/_config.js @@ -24,5 +24,7 @@ export default test({

1

` ); - } + }, + + expect_unhandled_rejections: true }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-resolve/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-resolve/_config.js new file mode 100644 index 0000000000..ee403290bc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-resolve/_config.js @@ -0,0 +1,63 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [increment, shift] = target.querySelectorAll('button'); + + shift.click(); + await tick(); + + shift.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + +

even

+

0

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

even

+

0

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

odd

+

loading...

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

odd

+

1

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-resolve/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-resolve/main.svelte new file mode 100644 index 0000000000..73fe83889a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-resolve/main.svelte @@ -0,0 +1,36 @@ + + + + + + + {#if await push(count) % 2 === 0} +

even

+ {:else} +

odd

+ {/if} + + {#key count} + +

{await push(count)}

+ + {#snippet pending()} +

loading...

+ {/snippet} +
+ {/key} + + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/Child.svelte index 682f7a0631..758ebc0004 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/Child.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/Child.svelte @@ -1,7 +1,11 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/_config.js index 81548a25ea..0908b6a9fe 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-after-await/_config.js @@ -3,7 +3,8 @@ import { test } from '../../test'; export default test({ async test({ assert, logs }) { + assert.deepEqual(logs, []); await tick(); - assert.deepEqual(logs, ['hello']); + assert.deepEqual(logs, ['before', 'after']); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-after-boundary/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-effect-after-boundary/Child.svelte new file mode 100644 index 0000000000..65a225431b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-after-boundary/Child.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-after-boundary/_config.js b/packages/svelte/tests/runtime-runes/samples/async-effect-after-boundary/_config.js new file mode 100644 index 0000000000..f7b6c513d4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-after-boundary/_config.js @@ -0,0 +1,16 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const [shift] = target.querySelectorAll('button'); + + await tick(); + assert.deepEqual(logs, []); + + shift.click(); + await tick(); + + assert.deepEqual(logs, ['in effect']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-effect-after-boundary/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-effect-after-boundary/main.svelte new file mode 100644 index 0000000000..edfd3c4d10 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-effect-after-boundary/main.svelte @@ -0,0 +1,22 @@ + + + + + +

{await push('hello')}

+ + + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js b/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js index 50bb414afc..7fb49c473e 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/_config.js @@ -3,23 +3,28 @@ import { test } from '../../test'; export default test({ async test({ assert, target }) { // We gotta wait a bit more in this test because of the macrotasks in App.svelte - function macrotask(t = 3) { + function sleep(t = 50) { return new Promise((r) => setTimeout(r, t)); } - await macrotask(); + await sleep(); assert.htmlEqual(target.innerHTML, ' 1 | '); const [input] = target.querySelectorAll('input'); input.value = '1'; input.dispatchEvent(new Event('input', { bubbles: true })); - await macrotask(); + await sleep(); assert.htmlEqual(target.innerHTML, ' 1 | '); input.value = '12'; input.dispatchEvent(new Event('input', { bubbles: true })); - await macrotask(6); + await sleep(); assert.htmlEqual(target.innerHTML, ' 3 | 12'); + + input.value = ''; + input.dispatchEvent(new Event('input', { bubbles: true })); + await sleep(); + assert.htmlEqual(target.innerHTML, ' 4 | '); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/main.svelte index dc4a157928..dec5a55899 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-resolve-stale/main.svelte @@ -1,26 +1,31 @@ + + + +{#if showText} + {#if show} +
+ Should not transition out +
+ {/if} +{/if} From bd697c12c6ac31c178e8002d5f11a32161733ad2 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 24 Oct 2025 00:27:17 +0200 Subject: [PATCH 23/58] fix: flush pending changes after rendering `failed` snippet (#16995) fixes #16730 --- .changeset/slimy-turtles-yell.md | 5 +++++ .../internal/client/dom/blocks/boundary.js | 2 +- .../samples/error-boundary-23/_config.js | 12 +++++++++++ .../samples/error-boundary-23/main.svelte | 20 +++++++++++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 .changeset/slimy-turtles-yell.md create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-23/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/error-boundary-23/main.svelte diff --git a/.changeset/slimy-turtles-yell.md b/.changeset/slimy-turtles-yell.md new file mode 100644 index 0000000000..e3f3a66264 --- /dev/null +++ b/.changeset/slimy-turtles-yell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: flush pending changes after rendering `failed` snippet diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 3da9204571..72e64b1a3a 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -30,7 +30,6 @@ import { skip_nodes, set_hydrate_node } from '../hydration.js'; -import { get_next_sibling } from '../operations.js'; import { queue_micro_task } from '../task.js'; import * as e from '../../errors.js'; import * as w from '../../warnings.js'; @@ -402,6 +401,7 @@ export class Boundary { if (failed) { queue_micro_task(() => { this.#failed_effect = this.#run(() => { + Batch.ensure(); this.#is_creating_fallback = true; try { diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-23/_config.js b/packages/svelte/tests/runtime-runes/samples/error-boundary-23/_config.js new file mode 100644 index 0000000000..7a6a66eb66 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-23/_config.js @@ -0,0 +1,12 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const btn = target.querySelector('button'); + btn?.click(); + await tick(); + + assert.deepEqual(logs, ['attachment']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/error-boundary-23/main.svelte b/packages/svelte/tests/runtime-runes/samples/error-boundary-23/main.svelte new file mode 100644 index 0000000000..c1fe20d931 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/error-boundary-23/main.svelte @@ -0,0 +1,20 @@ + + + + {fail ? error() : 'all good'} + + + {#snippet failed()} +
oops!
+ {/snippet} +
From 0b477871e870db59ad10c3c0c6be008752db7f2d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Oct 2025 15:49:29 -0700 Subject: [PATCH 24/58] fix: coordinate mount of snippets with await expressions --- .changeset/huge-poets-tickle.md | 5 +++ .../internal/client/dom/blocks/boundary.js | 31 +++++++++++++++++-- .../Child.svelte | 8 +++++ .../_config.js | 25 +++++++++++++++ .../main.svelte | 23 ++++++++++++++ 5 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 .changeset/huge-poets-tickle.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte diff --git a/.changeset/huge-poets-tickle.md b/.changeset/huge-poets-tickle.md new file mode 100644 index 0000000000..f2b1ba6f25 --- /dev/null +++ b/.changeset/huge-poets-tickle.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: coordinate mount of snippets with await expressions diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 3da9204571..a8932427cd 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -30,7 +30,7 @@ import { skip_nodes, set_hydrate_node } from '../hydration.js'; -import { get_next_sibling } from '../operations.js'; +import { create_text, get_next_sibling } from '../operations.js'; import { queue_micro_task } from '../task.js'; import * as e from '../../errors.js'; import * as w from '../../warnings.js'; @@ -93,6 +93,9 @@ export class Boundary { /** @type {DocumentFragment | null} */ #offscreen_fragment = null; + /** @type {TemplateNode | null} */ + #pending_anchor = null; + #local_pending_count = 0; #pending_count = 0; @@ -156,8 +159,17 @@ export class Boundary { this.#hydrate_resolved_content(); } } else { + var anchor = this.#anchor; + + if (this.#pending) { + this.#pending_anchor = create_text(); + this.#anchor.before(this.#pending_anchor); + + anchor = this.#pending_anchor; + } + try { - this.#main_effect = branch(() => children(this.#anchor)); + this.#main_effect = branch(() => children(anchor)); } catch (error) { this.error(error); } @@ -166,6 +178,7 @@ export class Boundary { this.#show_pending_snippet(); } else { this.#pending = false; + this.#pending_anchor?.remove(); } } }, flags); @@ -195,9 +208,18 @@ export class Boundary { this.#pending_effect = branch(() => pending(this.#anchor)); Batch.enqueue(() => { + var anchor = this.#anchor; + + if (this.#pending) { + this.#pending_anchor = create_text(); + this.#anchor.before(this.#pending_anchor); + + anchor = this.#pending_anchor; + } + this.#main_effect = this.#run(() => { Batch.ensure(); - return branch(() => this.#children(this.#anchor)); + return branch(() => this.#children(anchor)); }); if (this.#pending_count > 0) { @@ -208,6 +230,7 @@ export class Boundary { }); this.#pending = false; + this.#pending_anchor?.remove(); } }); } @@ -253,6 +276,7 @@ export class Boundary { if (this.#main_effect !== null) { this.#offscreen_fragment = document.createDocumentFragment(); + this.#offscreen_fragment.append(/** @type {TemplateNode} */ (this.#pending_anchor)); move_effect(this.#main_effect, this.#offscreen_fragment); } @@ -288,6 +312,7 @@ export class Boundary { } if (this.#offscreen_fragment) { + this.#pending_anchor?.remove(); this.#anchor.before(this.#offscreen_fragment); this.#offscreen_fragment = null; } diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte new file mode 100644 index 0000000000..9b708ddef4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte @@ -0,0 +1,8 @@ + + +

message: {message}

+{@render children()} diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js new file mode 100644 index 0000000000..b6ca2ae3d2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js @@ -0,0 +1,25 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [shift] = target.querySelectorAll('button'); + + shift.click(); + await tick(); + + assert.htmlEqual(target.innerHTML, `

loading...

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

message: hello from child

+

hello from parent

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte new file mode 100644 index 0000000000..86768eb59d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte @@ -0,0 +1,23 @@ + + + + + + +

{await push('hello from parent')}

+
+ + {#snippet pending()} +

loading...

+ {/snippet} +
From 523c85bd3d592ab930e81fe29aef2805a41c7bf3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Oct 2025 15:58:12 -0700 Subject: [PATCH 25/58] merge --- .changeset/huge-poets-tickle.md | 5 ---- .../internal/client/dom/blocks/boundary.js | 30 ++----------------- .../Child.svelte | 8 ----- .../_config.js | 25 ---------------- .../main.svelte | 23 -------------- 5 files changed, 2 insertions(+), 89 deletions(-) delete mode 100644 .changeset/huge-poets-tickle.md delete mode 100644 packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte delete mode 100644 packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js delete mode 100644 packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte diff --git a/.changeset/huge-poets-tickle.md b/.changeset/huge-poets-tickle.md deleted file mode 100644 index f2b1ba6f25..0000000000 --- a/.changeset/huge-poets-tickle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: coordinate mount of snippets with await expressions diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 3c7e88b9ed..72e64b1a3a 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -30,7 +30,6 @@ import { skip_nodes, set_hydrate_node } from '../hydration.js'; -import { create_text } from '../operations.js'; import { queue_micro_task } from '../task.js'; import * as e from '../../errors.js'; import * as w from '../../warnings.js'; @@ -93,9 +92,6 @@ export class Boundary { /** @type {DocumentFragment | null} */ #offscreen_fragment = null; - /** @type {TemplateNode | null} */ - #pending_anchor = null; - #local_pending_count = 0; #pending_count = 0; @@ -159,17 +155,8 @@ export class Boundary { this.#hydrate_resolved_content(); } } else { - var anchor = this.#anchor; - - if (this.#pending) { - this.#pending_anchor = create_text(); - this.#anchor.before(this.#pending_anchor); - - anchor = this.#pending_anchor; - } - try { - this.#main_effect = branch(() => children(anchor)); + this.#main_effect = branch(() => children(this.#anchor)); } catch (error) { this.error(error); } @@ -178,7 +165,6 @@ export class Boundary { this.#show_pending_snippet(); } else { this.#pending = false; - this.#pending_anchor?.remove(); } } }, flags); @@ -208,18 +194,9 @@ export class Boundary { this.#pending_effect = branch(() => pending(this.#anchor)); Batch.enqueue(() => { - var anchor = this.#anchor; - - if (this.#pending) { - this.#pending_anchor = create_text(); - this.#anchor.before(this.#pending_anchor); - - anchor = this.#pending_anchor; - } - this.#main_effect = this.#run(() => { Batch.ensure(); - return branch(() => this.#children(anchor)); + return branch(() => this.#children(this.#anchor)); }); if (this.#pending_count > 0) { @@ -230,7 +207,6 @@ export class Boundary { }); this.#pending = false; - this.#pending_anchor?.remove(); } }); } @@ -276,7 +252,6 @@ export class Boundary { if (this.#main_effect !== null) { this.#offscreen_fragment = document.createDocumentFragment(); - this.#offscreen_fragment.append(/** @type {TemplateNode} */ (this.#pending_anchor)); move_effect(this.#main_effect, this.#offscreen_fragment); } @@ -312,7 +287,6 @@ export class Boundary { } if (this.#offscreen_fragment) { - this.#pending_anchor?.remove(); this.#anchor.before(this.#offscreen_fragment); this.#offscreen_fragment = null; } diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte deleted file mode 100644 index 9b708ddef4..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - -

message: {message}

-{@render children()} diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js deleted file mode 100644 index b6ca2ae3d2..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js +++ /dev/null @@ -1,25 +0,0 @@ -import { tick } from 'svelte'; -import { test } from '../../test'; - -export default test({ - async test({ assert, target }) { - const [shift] = target.querySelectorAll('button'); - - shift.click(); - await tick(); - - assert.htmlEqual(target.innerHTML, `

loading...

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

message: hello from child

-

hello from parent

- ` - ); - } -}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte deleted file mode 100644 index 86768eb59d..0000000000 --- a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - -

{await push('hello from parent')}

-
- - {#snippet pending()} -

loading...

- {/snippet} -
From 2e1dd489f6703fa3e8d192f2cca8e78e82baacc3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Oct 2025 10:29:33 -0400 Subject: [PATCH 26/58] fix: coordinate mount of snippets with await expressions (#17021) * fix: coordinate mount of snippets with await expressions * try this * deduplicate --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/huge-poets-tickle.md | 5 ++++ .../internal/client/dom/blocks/boundary.js | 30 +++++++++++++++++-- .../Child.svelte | 7 +++++ .../_config.js | 25 ++++++++++++++++ .../main.svelte | 21 +++++++++++++ 5 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 .changeset/huge-poets-tickle.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte diff --git a/.changeset/huge-poets-tickle.md b/.changeset/huge-poets-tickle.md new file mode 100644 index 0000000000..f2b1ba6f25 --- /dev/null +++ b/.changeset/huge-poets-tickle.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: coordinate mount of snippets with await expressions diff --git a/packages/svelte/src/internal/client/dom/blocks/boundary.js b/packages/svelte/src/internal/client/dom/blocks/boundary.js index 72e64b1a3a..febbc00898 100644 --- a/packages/svelte/src/internal/client/dom/blocks/boundary.js +++ b/packages/svelte/src/internal/client/dom/blocks/boundary.js @@ -38,6 +38,7 @@ import { Batch, effect_pending_updates } from '../../reactivity/batch.js'; import { internal_set, source } from '../../reactivity/sources.js'; import { tag } from '../../dev/tracing.js'; import { createSubscriber } from '../../../../reactivity/create-subscriber.js'; +import { create_text } from '../operations.js'; /** * @typedef {{ @@ -92,6 +93,9 @@ export class Boundary { /** @type {DocumentFragment | null} */ #offscreen_fragment = null; + /** @type {TemplateNode | null} */ + #pending_anchor = null; + #local_pending_count = 0; #pending_count = 0; @@ -155,8 +159,10 @@ export class Boundary { this.#hydrate_resolved_content(); } } else { + var anchor = this.#get_anchor(); + try { - this.#main_effect = branch(() => children(this.#anchor)); + this.#main_effect = branch(() => children(anchor)); } catch (error) { this.error(error); } @@ -167,6 +173,10 @@ export class Boundary { this.#pending = false; } } + + return () => { + this.#pending_anchor?.remove(); + }; }, flags); if (hydrating) { @@ -194,9 +204,11 @@ export class Boundary { this.#pending_effect = branch(() => pending(this.#anchor)); Batch.enqueue(() => { + var anchor = this.#get_anchor(); + this.#main_effect = this.#run(() => { Batch.ensure(); - return branch(() => this.#children(this.#anchor)); + return branch(() => this.#children(anchor)); }); if (this.#pending_count > 0) { @@ -211,6 +223,19 @@ export class Boundary { }); } + #get_anchor() { + var anchor = this.#anchor; + + if (this.#pending) { + this.#pending_anchor = create_text(); + this.#anchor.before(this.#pending_anchor); + + anchor = this.#pending_anchor; + } + + return anchor; + } + /** * Returns `true` if the effect exists inside a boundary whose pending snippet is shown * @returns {boolean} @@ -252,6 +277,7 @@ export class Boundary { if (this.#main_effect !== null) { this.#offscreen_fragment = document.createDocumentFragment(); + this.#offscreen_fragment.append(/** @type {TemplateNode} */ (this.#pending_anchor)); move_effect(this.#main_effect, this.#offscreen_fragment); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte new file mode 100644 index 0000000000..7085219a5a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/Child.svelte @@ -0,0 +1,7 @@ + + +

message: {message}

+{@render children()} diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js new file mode 100644 index 0000000000..b6ca2ae3d2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/_config.js @@ -0,0 +1,25 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [shift] = target.querySelectorAll('button'); + + shift.click(); + await tick(); + + assert.htmlEqual(target.innerHTML, `

loading...

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

message: hello from child

+

hello from parent

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte new file mode 100644 index 0000000000..3ad2c9572a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-snippet-coordinated-mount/main.svelte @@ -0,0 +1,21 @@ + + + + + + +

{await push('hello from parent')}

+
+ + {#snippet pending()} +

loading...

+ {/snippet} +
From f45d9cfe2dd59804d46828b1015d8e2a8ad05459 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Oct 2025 10:29:50 -0400 Subject: [PATCH 27/58] fix: better optimization of await expressions (#17025) --- .changeset/odd-plants-lead.md | 5 +++++ .../svelte/src/compiler/phases/2-analyze/index.js | 2 +- .../3-transform/server/visitors/shared/utils.js | 4 ++-- packages/svelte/src/compiler/utils/ast.js | 12 ++++++++---- packages/svelte/src/compiler/utils/builders.js | 4 ++-- 5 files changed, 18 insertions(+), 9 deletions(-) create mode 100644 .changeset/odd-plants-lead.md diff --git a/.changeset/odd-plants-lead.md b/.changeset/odd-plants-lead.md new file mode 100644 index 0000000000..1df2236c2a --- /dev/null +++ b/.changeset/odd-plants-lead.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: better optimization of await expressions diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 52be997374..b4c704c34d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -6,7 +6,7 @@ import { walk } from 'zimmerframe'; import { parse } from '../1-parse/acorn.js'; import * as e from '../../errors.js'; import * as w from '../../warnings.js'; -import { extract_identifiers } from '../../utils/ast.js'; +import { extract_identifiers, has_await_expression } from '../../utils/ast.js'; import * as b from '#compiler/builders'; import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js'; import check_graph_for_cycles from './utils/check_graph_for_cycles.js'; diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index f5132c1cf8..92653ed73c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -12,7 +12,7 @@ import { import * as b from '#compiler/builders'; import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js'; import { regex_whitespaces_strict } from '../../../../patterns.js'; -import { has_await } from '../../../../../utils/ast.js'; +import { has_await_expression } from '../../../../../utils/ast.js'; /** Opens an if/each block, so that we can remove nodes in the case of a mismatch */ export const block_open = b.literal(BLOCK_OPEN); @@ -315,7 +315,7 @@ export class PromiseOptimiser { const promises = b.array( this.expressions.map((expression) => { - return expression.type === 'AwaitExpression' && !has_await(expression.argument) + return expression.type === 'AwaitExpression' && !has_await_expression(expression.argument) ? expression.argument : b.call(b.thunk(expression, true)); }) diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js index 541921befb..bd92dda5d9 100644 --- a/packages/svelte/src/compiler/utils/ast.js +++ b/packages/svelte/src/compiler/utils/ast.js @@ -611,16 +611,20 @@ export function build_assignment_value(operator, left, right) { } /** - * @param {ESTree.Expression} expression + * @param {ESTree.Node} node */ -export function has_await(expression) { +export function has_await_expression(node) { let has_await = false; - walk(expression, null, { + walk(node, null, { AwaitExpression(_node, context) { has_await = true; context.stop(); - } + }, + // don't traverse into these + FunctionDeclaration() {}, + FunctionExpression() {}, + ArrowFunctionExpression() {} }); return has_await; diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index 99306ce4d9..f21b0dc8b4 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -2,7 +2,7 @@ import { walk } from 'zimmerframe'; import { regex_is_valid_identifier } from '../phases/patterns.js'; import { sanitize_template_string } from './sanitize_template_string.js'; -import { has_await } from './ast.js'; +import { has_await_expression } from './ast.js'; /** * @param {Array} elements @@ -451,7 +451,7 @@ export function thunk(expression, async = false) { export function unthunk(expression) { // optimize `async () => await x()`, but not `async () => await x(await y)` if (expression.async && expression.body.type === 'AwaitExpression') { - if (!has_await(expression.body.argument)) { + if (!has_await_expression(expression.body.argument)) { return unthunk(arrow(expression.params, expression.body.argument)); } } From 4a214f786f24ffaa20982f3cc68b1c6a02ad0fd3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 24 Oct 2025 12:45:51 -0400 Subject: [PATCH 28/58] Version Packages (#17019) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/easy-paths-take.md | 5 ----- .changeset/huge-poets-tickle.md | 5 ----- .changeset/odd-plants-lead.md | 5 ----- .changeset/slimy-turtles-yell.md | 5 ----- packages/svelte/CHANGELOG.md | 12 ++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 7 files changed, 14 insertions(+), 22 deletions(-) delete mode 100644 .changeset/easy-paths-take.md delete mode 100644 .changeset/huge-poets-tickle.md delete mode 100644 .changeset/odd-plants-lead.md delete mode 100644 .changeset/slimy-turtles-yell.md diff --git a/.changeset/easy-paths-take.md b/.changeset/easy-paths-take.md deleted file mode 100644 index 1378322abe..0000000000 --- a/.changeset/easy-paths-take.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: take into account static blocks when determining transition locality diff --git a/.changeset/huge-poets-tickle.md b/.changeset/huge-poets-tickle.md deleted file mode 100644 index f2b1ba6f25..0000000000 --- a/.changeset/huge-poets-tickle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: coordinate mount of snippets with await expressions diff --git a/.changeset/odd-plants-lead.md b/.changeset/odd-plants-lead.md deleted file mode 100644 index 1df2236c2a..0000000000 --- a/.changeset/odd-plants-lead.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: better optimization of await expressions diff --git a/.changeset/slimy-turtles-yell.md b/.changeset/slimy-turtles-yell.md deleted file mode 100644 index e3f3a66264..0000000000 --- a/.changeset/slimy-turtles-yell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: flush pending changes after rendering `failed` snippet diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 7692383aed..d63548d3e1 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.41.4 + +### Patch Changes + +- fix: take into account static blocks when determining transition locality ([#17018](https://github.com/sveltejs/svelte/pull/17018)) + +- fix: coordinate mount of snippets with await expressions ([#17021](https://github.com/sveltejs/svelte/pull/17021)) + +- fix: better optimization of await expressions ([#17025](https://github.com/sveltejs/svelte/pull/17025)) + +- fix: flush pending changes after rendering `failed` snippet ([#16995](https://github.com/sveltejs/svelte/pull/16995)) + ## 5.41.3 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 2cb3bf4ab3..1d50920d4f 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.41.3", + "version": "5.41.4", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 81ead49fca..f5f47c6056 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.41.3'; +export const VERSION = '5.41.4'; export const PUBLIC_VERSION = '5'; From 7434f21ed44cc9004562164e46a8b028c563d461 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Oct 2025 13:17:19 -0400 Subject: [PATCH 29/58] chore: configurable sandbox output (#17028) --- playgrounds/sandbox/run.js | 96 +++++++++++++++++++++++--------------- 1 file changed, 58 insertions(+), 38 deletions(-) diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js index 639b755020..7ff9f7c4cd 100644 --- a/playgrounds/sandbox/run.js +++ b/playgrounds/sandbox/run.js @@ -5,6 +5,13 @@ import { parseArgs } from 'node:util'; import { globSync } from 'tinyglobby'; import { compile, compileModule, parse, migrate } from 'svelte/compiler'; +// toggle these to change what gets written to sandbox/output +const AST = false; +const MIGRATE = false; +const FROM_HTML = true; +const FROM_TREE = false; +const DEV = false; + const argv = parseArgs({ options: { runes: { type: 'boolean' } }, args: process.argv.slice(2) }); const cwd = fileURLToPath(new URL('.', import.meta.url)).slice(0, -1); @@ -51,48 +58,52 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { mkdirp(path.dirname(output_js)); if (generate === 'client') { - const ast = parse(source, { - modern: true - }); + if (AST) { + const ast = parse(source, { + modern: true + }); + + write( + `${cwd}/output/ast/${file}.json`, + JSON.stringify( + ast, + (key, value) => (typeof value === 'bigint' ? ['BigInt', value.toString()] : value), + '\t' + ) + ); + } - write( - `${cwd}/output/ast/${file}.json`, - JSON.stringify( - ast, - (key, value) => (typeof value === 'bigint' ? ['BigInt', value.toString()] : value), - '\t' - ) - ); - - try { - const migrated = migrate(source); - write(`${cwd}/output/migrated/${file}`, migrated.code); - } catch (e) { - console.warn(`Error migrating ${file}`, e); + if (MIGRATE) { + try { + const migrated = migrate(source); + write(`${cwd}/output/migrated/${file}`, migrated.code); + } catch (e) { + console.warn(`Error migrating ${file}`, e); + } } } - const compiled = compile(source, { - dev: false, - filename: input, - generate, - runes: argv.values.runes, - experimental: { - async: true - } - }); + let from_html; + let from_tree; - for (const warning of compiled.warnings) { - console.warn(warning.code); - console.warn(warning.frame); - } + if (generate === 'server' || FROM_HTML) { + from_html = compile(source, { + dev: DEV, + filename: input, + generate, + runes: argv.values.runes, + experimental: { + async: true + } + }); - write(output_js, compiled.js.code + '\n//# sourceMappingURL=' + path.basename(output_map)); - write(output_map, compiled.js.map.toString()); + write(output_js, from_html.js.code + '\n//# sourceMappingURL=' + path.basename(output_map)); + write(output_map, from_html.js.map.toString()); + } // generate with fragments: 'tree' - if (generate === 'client') { - const compiled = compile(source, { + if (generate === 'client' && FROM_TREE) { + from_tree = compile(source, { dev: false, filename: input, generate, @@ -106,12 +117,21 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { const output_js = `${cwd}/output/${generate}/${file}.tree.js`; const output_map = `${cwd}/output/${generate}/${file}.tree.js.map`; - write(output_js, compiled.js.code + '\n//# sourceMappingURL=' + path.basename(output_map)); - write(output_map, compiled.js.map.toString()); + write(output_js, from_tree.js.code + '\n//# sourceMappingURL=' + path.basename(output_map)); + write(output_map, from_tree.js.map.toString()); } - if (compiled.css) { - write(output_css, compiled.css.code); + const compiled = from_html ?? from_tree; + + if (compiled) { + for (const warning of compiled.warnings) { + console.warn(warning.code); + console.warn(warning.frame); + } + + if (compiled.css) { + write(output_css, compiled.css.code); + } } } From c08ecba1b79a0d91f2c69eab6af2fa0dfb8dffd0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Oct 2025 14:12:57 -0400 Subject: [PATCH 30/58] feat: forking (#17004) * chore: run boundary async effects in the context of the current batch * WIP * reinstate kludge * fix test * WIP * WIP * WIP * remove kludge * restore batch_values after commit * make private * tidy up * fix tests * update test * reset #dirty_effects and #maybe_dirty_effects * add test * WIP * add test, fix block resolution * bring async-effect-after-await test from defer-effects-in-pending-boundary branch * avoid reawakening committed batches * changeset * cheat * better API * regenerate * slightly better approach * lint * revert this whatever it is * add test * Update feature description for fork API * error if missing experimental flag * rename inspect effects to eager effects, run them in prod * regenerate * Apply suggestions from code review Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> * tidy up * add some minimal prose. probably don't need to go super deep here as it's not really meant for non-framework authors * bit more detail * add a fork_timing error, regenerate * unused * add note * add fork_discarded error * require users to discard forks * add docs * regenerate * tweak docs * fix leak * fix * preload on focusin as well * missed a spot * reduce nesting --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/small-geckos-camp.md | 5 + .../19-await-expressions.md | 48 +++++ .../98-reference/.generated/client-errors.md | 18 ++ .../svelte/messages/client-errors/errors.md | 12 ++ packages/svelte/src/index-client.js | 2 +- packages/svelte/src/index-server.js | 4 + packages/svelte/src/index.d.ts | 16 ++ .../svelte/src/internal/client/constants.js | 2 +- .../svelte/src/internal/client/dev/inspect.js | 4 +- .../internal/client/dom/blocks/branches.js | 21 +- .../src/internal/client/dom/blocks/each.js | 2 +- packages/svelte/src/internal/client/errors.js | 48 +++++ packages/svelte/src/internal/client/proxy.js | 8 +- .../src/internal/client/reactivity/batch.js | 182 +++++++++++++++--- .../internal/client/reactivity/deriveds.js | 8 +- .../src/internal/client/reactivity/effects.js | 8 +- .../src/internal/client/reactivity/sources.js | 34 ++-- .../samples/async-fork/_config.js | 92 +++++++++ .../samples/async-fork/main.svelte | 37 ++++ packages/svelte/types/index.d.ts | 44 ++++- 20 files changed, 531 insertions(+), 64 deletions(-) create mode 100644 .changeset/small-geckos-camp.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork/main.svelte diff --git a/.changeset/small-geckos-camp.md b/.changeset/small-geckos-camp.md new file mode 100644 index 0000000000..622cbbbfa0 --- /dev/null +++ b/.changeset/small-geckos-camp.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +feat: experimental `fork` API diff --git a/documentation/docs/03-template-syntax/19-await-expressions.md b/documentation/docs/03-template-syntax/19-await-expressions.md index 1c613af870..2f73f6a47c 100644 --- a/documentation/docs/03-template-syntax/19-await-expressions.md +++ b/documentation/docs/03-template-syntax/19-await-expressions.md @@ -135,6 +135,54 @@ If a `` with a `pending` snippet is encountered during SSR, tha > [!NOTE] In the future, we plan to add a streaming implementation that renders the content in the background. +## Forking + +The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate. + +```svelte + + + + +{#if open} + + open = false} /> +{/if} +``` + ## Caveats As an experimental feature, the details of how `await` is handled (and related APIs like `$effect.pending()`) are subject to breaking changes outside of a semver major release, though we intend to keep such changes to a bare minimum. diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 8fdb7770aa..74a0674dba 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -130,6 +130,12 @@ $effect(() => { Often when encountering this issue, the value in question shouldn't be state (for example, if you are pushing to a `logs` array in an effect, make `logs` a normal array rather than `$state([])`). In the rare cases where you really _do_ need to write to state in an effect — [which you should avoid]($effect#When-not-to-use-$effect) — you can read the state with [untrack](svelte#untrack) to avoid adding it as a dependency. +### experimental_async_fork + +``` +Cannot use `fork(...)` unless the `experimental.async` compiler option is `true` +``` + ### flush_sync_in_effect ``` @@ -140,6 +146,18 @@ The `flushSync()` function can be used to flush any pending effects synchronousl This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6. +### fork_discarded + +``` +Cannot commit a fork that was already committed or discarded +``` + +### fork_timing + +``` +Cannot create a fork inside an effect or when state changes are pending +``` + ### get_abort_signal_outside_reaction ``` diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index 57ecca0489..b5fe51539d 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -100,6 +100,10 @@ $effect(() => { Often when encountering this issue, the value in question shouldn't be state (for example, if you are pushing to a `logs` array in an effect, make `logs` a normal array rather than `$state([])`). In the rare cases where you really _do_ need to write to state in an effect — [which you should avoid]($effect#When-not-to-use-$effect) — you can read the state with [untrack](svelte#untrack) to avoid adding it as a dependency. +## experimental_async_fork + +> Cannot use `fork(...)` unless the `experimental.async` compiler option is `true` + ## flush_sync_in_effect > Cannot use `flushSync` inside an effect @@ -108,6 +112,14 @@ The `flushSync()` function can be used to flush any pending effects synchronousl This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6. +## fork_discarded + +> Cannot commit a fork that was already committed or discarded + +## fork_timing + +> Cannot create a fork inside an effect or when state changes are pending + ## get_abort_signal_outside_reaction > `getAbortSignal()` can only be called inside an effect or derived diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 337cbb500b..4fcfff980d 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -241,7 +241,7 @@ function init_update_callbacks(context) { return (l.u ??= { a: [], b: [], m: [] }); } -export { flushSync } from './internal/client/reactivity/batch.js'; +export { flushSync, fork } from './internal/client/reactivity/batch.js'; export { createContext, getContext, diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index 223ce6a4cd..61b0d98c06 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.js @@ -33,6 +33,10 @@ export function unmount() { e.lifecycle_function_unavailable('unmount'); } +export function fork() { + e.lifecycle_function_unavailable('fork'); +} + export async function tick() {} export async function settled() {} diff --git a/packages/svelte/src/index.d.ts b/packages/svelte/src/index.d.ts index 38e6086689..a1782f5b61 100644 --- a/packages/svelte/src/index.d.ts +++ b/packages/svelte/src/index.d.ts @@ -352,4 +352,20 @@ export type MountOptions = Record props: Props; }); +/** + * Represents work that is happening off-screen, such as data being preloaded + * in anticipation of the user navigating + * @since 5.42 + */ +export interface Fork { + /** + * Commit the fork. The promise will resolve once the state change has been applied + */ + commit(): Promise; + /** + * Discard the fork + */ + discard(): void; +} + export * from './index-client.js'; diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 6818fd9d30..24dc9e4fb8 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -19,7 +19,7 @@ export const EFFECT_RAN = 1 << 15; * This is on a block effect 99% of the time but may also be on a branch effect if its parent block effect was pruned */ export const EFFECT_TRANSPARENT = 1 << 16; -export const INSPECT_EFFECT = 1 << 17; +export const EAGER_EFFECT = 1 << 17; export const HEAD_EFFECT = 1 << 18; export const EFFECT_PRESERVED = 1 << 19; export const USER_EFFECT = 1 << 20; diff --git a/packages/svelte/src/internal/client/dev/inspect.js b/packages/svelte/src/internal/client/dev/inspect.js index db7ab0d976..09150d6ee4 100644 --- a/packages/svelte/src/internal/client/dev/inspect.js +++ b/packages/svelte/src/internal/client/dev/inspect.js @@ -1,6 +1,6 @@ import { UNINITIALIZED } from '../../../constants.js'; import { snapshot } from '../../shared/clone.js'; -import { inspect_effect, render_effect, validate_effect } from '../reactivity/effects.js'; +import { eager_effect, render_effect, validate_effect } from '../reactivity/effects.js'; import { untrack } from '../runtime.js'; import { get_stack } from './tracing.js'; @@ -19,7 +19,7 @@ export function inspect(get_value, inspector, show_stack = false) { // stack traces. As a consequence, reading the value might result // in an error (an `$inspect(object.property)` will run before the // `{#if object}...{/if}` that contains it) - inspect_effect(() => { + eager_effect(() => { try { var value = get_value(); } catch (e) { diff --git a/packages/svelte/src/internal/client/dom/blocks/branches.js b/packages/svelte/src/internal/client/dom/blocks/branches.js index 827f9f44fa..f1b9baf6f6 100644 --- a/packages/svelte/src/internal/client/dom/blocks/branches.js +++ b/packages/svelte/src/internal/client/dom/blocks/branches.js @@ -1,5 +1,4 @@ /** @import { Effect, TemplateNode } from '#client' */ -import { is_runes } from '../../context.js'; import { Batch, current_batch } from '../../reactivity/batch.js'; import { branch, @@ -8,7 +7,6 @@ import { pause_effect, resume_effect } from '../../reactivity/effects.js'; -import { set_should_intro, should_intro } from '../../render.js'; import { hydrate_node, hydrating } from '../hydration.js'; import { create_text, should_defer_append } from '../operations.js'; @@ -126,6 +124,22 @@ export class BranchManager { } }; + /** + * @param {Batch} batch + */ + #discard = (batch) => { + this.#batches.delete(batch); + + const keys = Array.from(this.#batches.values()); + + for (const [k, branch] of this.#offscreen) { + if (!keys.includes(k)) { + destroy_effect(branch.effect); + this.#offscreen.delete(k); + } + } + }; + /** * * @param {any} key @@ -173,7 +187,8 @@ export class BranchManager { } } - batch.add_callback(this.#commit); + batch.oncommit(this.#commit); + batch.ondiscard(this.#discard); } else { if (hydrating) { this.anchor = hydrate_node; diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index a6369a7211..a0fae37133 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -310,7 +310,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f } } - batch.add_callback(commit); + batch.oncommit(commit); } else { commit(); } diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 937971da5e..2a433ed8f9 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -229,6 +229,22 @@ export function effect_update_depth_exceeded() { } } +/** + * Cannot use `fork(...)` unless the `experimental.async` compiler option is `true` + * @returns {never} + */ +export function experimental_async_fork() { + if (DEV) { + const error = new Error(`experimental_async_fork\nCannot use \`fork(...)\` unless the \`experimental.async\` compiler option is \`true\`\nhttps://svelte.dev/e/experimental_async_fork`); + + error.name = 'Svelte error'; + + throw error; + } else { + throw new Error(`https://svelte.dev/e/experimental_async_fork`); + } +} + /** * Cannot use `flushSync` inside an effect * @returns {never} @@ -245,6 +261,38 @@ export function flush_sync_in_effect() { } } +/** + * Cannot commit a fork that was already committed or discarded + * @returns {never} + */ +export function fork_discarded() { + if (DEV) { + const error = new Error(`fork_discarded\nCannot commit a fork that was already committed or discarded\nhttps://svelte.dev/e/fork_discarded`); + + error.name = 'Svelte error'; + + throw error; + } else { + throw new Error(`https://svelte.dev/e/fork_discarded`); + } +} + +/** + * Cannot create a fork inside an effect or when state changes are pending + * @returns {never} + */ +export function fork_timing() { + if (DEV) { + const error = new Error(`fork_timing\nCannot create a fork inside an effect or when state changes are pending\nhttps://svelte.dev/e/fork_timing`); + + error.name = 'Svelte error'; + + throw error; + } else { + throw new Error(`https://svelte.dev/e/fork_timing`); + } +} + /** * `getAbortSignal()` can only be called inside an effect or derived * @returns {never} diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index dae3791eb0..9baacacd0d 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -19,8 +19,8 @@ import { state as source, set, increment, - flush_inspect_effects, - set_inspect_effects_deferred + flush_eager_effects, + set_eager_effects_deferred } from './reactivity/sources.js'; import { PROXY_PATH_SYMBOL, STATE_SYMBOL } from '#client/constants'; import { UNINITIALIZED } from '../../constants.js'; @@ -421,9 +421,9 @@ function inspectable_array(array) { * @param {any[]} args */ return function (...args) { - set_inspect_effects_deferred(); + set_eager_effects_deferred(); var result = value.apply(this, args); - flush_inspect_effects(); + flush_eager_effects(); return result; }; } diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 2cef562ac9..fdeb111a4d 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -1,3 +1,4 @@ +/** @import { Fork } from 'svelte' */ /** @import { Derived, Effect, Reaction, Source, Value } from '#client' */ import { BLOCK_EFFECT, @@ -12,25 +13,35 @@ import { ROOT_EFFECT, MAYBE_DIRTY, DERIVED, - BOUNDARY_EFFECT + BOUNDARY_EFFECT, + EAGER_EFFECT } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; import { deferred, define_property } from '../../shared/utils.js'; import { active_effect, get, + increment_write_version, is_dirty, is_updating_effect, set_is_updating_effect, set_signal_status, + tick, update_effect } from '../runtime.js'; import * as e from '../errors.js'; import { flush_tasks, queue_micro_task } from '../dom/task.js'; import { DEV } from 'esm-env'; import { invoke_error_boundary } from '../error-handling.js'; -import { old_values, source, update } from './sources.js'; -import { inspect_effect, unlink_effect } from './effects.js'; +import { + flush_eager_effects, + eager_effects, + old_values, + set_eager_effects, + source, + update +} from './sources.js'; +import { eager_effect, unlink_effect } from './effects.js'; /** * @typedef {{ @@ -90,14 +101,20 @@ export class Batch { * They keys of this map are identical to `this.#current` * @type {Map} */ - #previous = new Map(); + previous = new Map(); /** * When the batch is committed (and the DOM is updated), we need to remove old branches * and append new ones by calling the functions added inside (if/each/key/etc) blocks * @type {Set<() => void>} */ - #callbacks = new Set(); + #commit_callbacks = new Set(); + + /** + * If a fork is discarded, we need to destroy any effects that are no longer needed + * @type {Set<(batch: Batch) => void>} + */ + #discard_callbacks = new Set(); /** * The number of async effects that are currently in flight @@ -135,6 +152,8 @@ export class Batch { */ skipped_effects = new Set(); + is_fork = false; + /** * * @param {Effect[]} root_effects @@ -159,15 +178,15 @@ export class Batch { this.#traverse_effect_tree(root, target); } - this.#resolve(); + if (!this.is_fork) { + this.#resolve(); + } - if (this.#blocking_pending > 0) { + if (this.#blocking_pending > 0 || this.is_fork) { this.#defer_effects(target.effects); this.#defer_effects(target.render_effects); this.#defer_effects(target.block_effects); } else { - // TODO append/detach blocks here, not in #commit - // If sources are written to, then work needs to happen in a separate batch, else prior sources would be mixed with // newly updated sources, which could lead to infinite loops when effects run over and over again. previous_batch = this; @@ -271,8 +290,8 @@ export class Batch { * @param {any} value */ capture(source, value) { - if (!this.#previous.has(source)) { - this.#previous.set(source, value); + if (!this.previous.has(source)) { + this.previous.set(source, value); } this.current.set(source, source.v); @@ -289,16 +308,17 @@ export class Batch { } flush() { + this.activate(); + if (queued_root_effects.length > 0) { - this.activate(); flush_effects(); if (current_batch !== null && current_batch !== this) { // this can happen if a new batch was created during `flush_effects()` return; } - } else { - this.#resolve(); + } else if (this.#pending === 0) { + this.process([]); // TODO this feels awkward } this.deactivate(); @@ -314,11 +334,16 @@ export class Batch { } } + discard() { + for (const fn of this.#discard_callbacks) fn(this); + this.#discard_callbacks.clear(); + } + #resolve() { if (this.#blocking_pending === 0) { // append/remove branches - for (const fn of this.#callbacks) fn(); - this.#callbacks.clear(); + for (const fn of this.#commit_callbacks) fn(); + this.#commit_callbacks.clear(); } if (this.#pending === 0) { @@ -332,7 +357,7 @@ export class Batch { // committed state, unless the batch in question has a more // recent value for a given source if (batches.size > 1) { - this.#previous.clear(); + this.previous.clear(); var previous_batch_values = batch_values; var is_earlier = true; @@ -428,6 +453,10 @@ export class Batch { this.#pending -= 1; if (blocking) this.#blocking_pending -= 1; + this.revive(); + } + + revive() { for (const e of this.#dirty_effects) { set_signal_status(e, DIRTY); schedule_effect(e); @@ -445,8 +474,13 @@ export class Batch { } /** @param {() => void} fn */ - add_callback(fn) { - this.#callbacks.add(fn); + oncommit(fn) { + this.#commit_callbacks.add(fn); + } + + /** @param {(batch: Batch) => void} fn */ + ondiscard(fn) { + this.#discard_callbacks.add(fn); } settled() { @@ -489,7 +523,7 @@ export class Batch { for (const batch of batches) { if (batch === this) continue; - for (const [source, previous] of batch.#previous) { + for (const [source, previous] of batch.previous) { if (!batch_values.has(source)) { batch_values.set(source, previous); } @@ -717,6 +751,28 @@ function mark_effects(value, sources, marked, checked) { } } +/** + * When committing a fork, we need to trigger eager effects so that + * any `$state.eager(...)` expressions update immediately. This + * function allows us to discover them + * @param {Value} value + * @param {Set} effects + */ +function mark_eager_effects(value, effects) { + if (value.reactions === null) return; + + for (const reaction of value.reactions) { + const flags = reaction.f; + + if ((flags & DERIVED) !== 0) { + mark_eager_effects(/** @type {Derived} */ (reaction), effects); + } else if ((flags & EAGER_EFFECT) !== 0) { + set_signal_status(reaction, DIRTY); + effects.add(/** @type {Effect} */ (reaction)); + } + } +} + /** * @param {Reaction} reaction * @param {Source[]} sources @@ -798,9 +854,9 @@ export function eager(fn) { get(version); - inspect_effect(() => { + eager_effect(() => { if (initial) { - // the first time this runs, we create an inspect effect + // the first time this runs, we create an eager effect // that will run eagerly whenever the expression changes var previous_batch_values = batch_values; @@ -829,6 +885,88 @@ export function eager(fn) { return value; } +/** + * 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 + * the user is about to take some action. + * + * Frameworks like SvelteKit can use this to preload data when the user touches or + * hovers over a link, making any subsequent navigation feel instantaneous. + * + * The `fn` parameter is a synchronous function that modifies some state. The + * state changes will be reverted after the fork is initialised, then reapplied + * if and when the fork is eventually committed. + * + * When it becomes clear that a fork will _not_ be committed (e.g. because the + * user navigated elsewhere), it must be discarded to avoid leaking memory. + * + * @param {() => void} fn + * @returns {Fork} + * @since 5.42 + */ +export function fork(fn) { + if (!async_mode_flag) { + e.experimental_async_fork(); + } + + if (current_batch !== null) { + e.fork_timing(); + } + + const batch = Batch.ensure(); + batch.is_fork = true; + + const settled = batch.settled(); + + flushSync(fn); + + // revert state changes + for (const [source, value] of batch.previous) { + source.v = value; + } + + return { + commit: async () => { + if (!batches.has(batch)) { + e.fork_discarded(); + } + + batch.is_fork = false; + + // apply changes + for (const [source, value] of batch.current) { + source.v = value; + } + + // trigger any `$state.eager(...)` expressions with the new state. + // eager effects don't get scheduled like other effects, so we + // can't just encounter them during traversal, we need to + // proactively flush them + // TODO maybe there's a better implementation? + flushSync(() => { + /** @type {Set} */ + const eager_effects = new Set(); + + for (const source of batch.current.keys()) { + mark_eager_effects(source, eager_effects); + } + + set_eager_effects(eager_effects); + flush_eager_effects(); + }); + + batch.revive(); + await settled; + }, + discard: () => { + if (batches.has(batch)) { + batches.delete(batch); + batch.discard(); + } + } + }; +} + /** * Forcibly remove all current batches, to prevent cross-talk between tests */ diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 5a3dee4b7f..b6a50acc4d 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -28,7 +28,7 @@ import { equals, safe_equals } from './equality.js'; import * as e from '../errors.js'; import * as w from '../warnings.js'; import { async_effect, destroy_effect, teardown } from './effects.js'; -import { inspect_effects, internal_set, set_inspect_effects, source } from './sources.js'; +import { eager_effects, internal_set, set_eager_effects, source } from './sources.js'; import { get_stack } from '../dev/tracing.js'; import { async_mode_flag, tracing_mode_flag } from '../../flags/index.js'; import { Boundary } from '../dom/blocks/boundary.js'; @@ -318,8 +318,8 @@ export function execute_derived(derived) { set_active_effect(get_derived_parent_effect(derived)); if (DEV) { - let prev_inspect_effects = inspect_effects; - set_inspect_effects(new Set()); + let prev_eager_effects = eager_effects; + set_eager_effects(new Set()); try { if (stack.includes(derived)) { e.derived_references_self(); @@ -332,7 +332,7 @@ export function execute_derived(derived) { value = update_reaction(derived); } finally { set_active_effect(prev_active_effect); - set_inspect_effects(prev_inspect_effects); + set_eager_effects(prev_eager_effects); stack.pop(); } } else { diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 9b54598f9e..4235e9cb24 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -27,7 +27,7 @@ import { DERIVED, UNOWNED, CLEAN, - INSPECT_EFFECT, + EAGER_EFFECT, HEAD_EFFECT, MAYBE_DIRTY, EFFECT_PRESERVED, @@ -88,7 +88,7 @@ function create_effect(type, fn, sync, push = true) { if (DEV) { // Ensure the parent is never an inspect effect - while (parent !== null && (parent.f & INSPECT_EFFECT) !== 0) { + while (parent !== null && (parent.f & EAGER_EFFECT) !== 0) { parent = parent.parent; } } @@ -245,8 +245,8 @@ export function user_pre_effect(fn) { } /** @param {() => void | (() => void)} fn */ -export function inspect_effect(fn) { - return create_effect(INSPECT_EFFECT, fn, true); +export function eager_effect(fn) { + return create_effect(EAGER_EFFECT, fn, true); } /** diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 2fe8c4f75d..9534e718a5 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -22,7 +22,7 @@ import { DERIVED, DIRTY, BRANCH_EFFECT, - INSPECT_EFFECT, + EAGER_EFFECT, UNOWNED, MAYBE_DIRTY, BLOCK_EFFECT, @@ -39,7 +39,7 @@ import { proxy } from '../proxy.js'; import { execute_derived } from './deriveds.js'; /** @type {Set} */ -export let inspect_effects = new Set(); +export let eager_effects = new Set(); /** @type {Map} */ export const old_values = new Map(); @@ -47,14 +47,14 @@ export const old_values = new Map(); /** * @param {Set} v */ -export function set_inspect_effects(v) { - inspect_effects = v; +export function set_eager_effects(v) { + eager_effects = v; } -let inspect_effects_deferred = false; +let eager_effects_deferred = false; -export function set_inspect_effects_deferred() { - inspect_effects_deferred = true; +export function set_eager_effects_deferred() { + eager_effects_deferred = true; } /** @@ -146,9 +146,9 @@ export function set(source, value, should_proxy = false) { active_reaction !== null && // since we are untracking the function inside `$inspect.with` we need to add this check // to ensure we error if state is set inside an inspect effect - (!untracking || (active_reaction.f & INSPECT_EFFECT) !== 0) && + (!untracking || (active_reaction.f & EAGER_EFFECT) !== 0) && is_runes() && - (active_reaction.f & (DERIVED | BLOCK_EFFECT | ASYNC | INSPECT_EFFECT)) !== 0 && + (active_reaction.f & (DERIVED | BLOCK_EFFECT | ASYNC | EAGER_EFFECT)) !== 0 && !current_sources?.includes(source) ) { e.state_unsafe_mutation(); @@ -235,18 +235,18 @@ export function internal_set(source, value) { } } - if (DEV && inspect_effects.size > 0 && !inspect_effects_deferred) { - flush_inspect_effects(); + if (!batch.is_fork && eager_effects.size > 0 && !eager_effects_deferred) { + flush_eager_effects(); } } return value; } -export function flush_inspect_effects() { - inspect_effects_deferred = false; +export function flush_eager_effects() { + eager_effects_deferred = false; - const inspects = Array.from(inspect_effects); + const inspects = Array.from(eager_effects); for (const effect of inspects) { // Mark clean inspect-effects as maybe dirty and then check their dirtiness @@ -260,7 +260,7 @@ export function flush_inspect_effects() { } } - inspect_effects.clear(); + eager_effects.clear(); } /** @@ -320,8 +320,8 @@ function mark_reactions(signal, status) { if (!runes && reaction === active_effect) continue; // Inspect effects need to run immediately, so that the stack trace makes sense - if (DEV && (flags & INSPECT_EFFECT) !== 0) { - inspect_effects.add(reaction); + if (DEV && (flags & EAGER_EFFECT) !== 0) { + eager_effects.add(reaction); continue; } diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork/_config.js new file mode 100644 index 0000000000..35b47525a2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork/_config.js @@ -0,0 +1,92 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target, raf }) { + const [shift, increment, commit] = target.querySelectorAll('button'); + + shift.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

count: 0

+

eager: 0

+

even

+ ` + ); + + increment.click(); + await tick(); + + shift.click(); + await tick(); + + // nothing updates until commit + assert.htmlEqual( + target.innerHTML, + ` + + + +

count: 0

+

eager: 0

+

even

+ ` + ); + + commit.click(); + await tick(); + + // nothing updates until commit + assert.htmlEqual( + target.innerHTML, + ` + + + +

count: 1

+

eager: 1

+

odd

+ ` + ); + + increment.click(); + await tick(); + + commit.click(); + await tick(); + + // eager state updates on commit + assert.htmlEqual( + target.innerHTML, + ` + + + +

count: 1

+

eager: 2

+

odd

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

count: 2

+

eager: 2

+

even

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork/main.svelte new file mode 100644 index 0000000000..9b1f433f2d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork/main.svelte @@ -0,0 +1,37 @@ + + + + + + +

count: {count}

+

eager: {$state.eager(count)}

+ + + {#if await push(count) % 2 === 0} +

even

+ {:else} +

odd

+ {/if} + + {#snippet pending()} +

loading...

+ {/snippet} +
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index d260b738c3..5e3ca77eb5 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -348,6 +348,22 @@ declare module 'svelte' { */ props: Props; }); + + /** + * Represents work that is happening off-screen, such as data being preloaded + * in anticipation of the user navigating + * @since 5.42 + */ + export interface Fork { + /** + * Commit the fork. The promise will resolve once the state change has been applied + */ + commit(): Promise; + /** + * Discard the fork + */ + discard(): void; + } /** * Returns an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that aborts when the current [derived](https://svelte.dev/docs/svelte/$derived) or [effect](https://svelte.dev/docs/svelte/$effect) re-runs or is destroyed. * @@ -434,11 +450,6 @@ declare module 'svelte' { * @deprecated Use [`$effect`](https://svelte.dev/docs/svelte/$effect) instead * */ export function afterUpdate(fn: () => void): void; - /** - * Synchronously flush any pending updates. - * Returns void if no callback is provided, otherwise returns the result of calling the callback. - * */ - export function flushSync(fn?: (() => T) | undefined): T; /** * Create a snippet programmatically * */ @@ -448,6 +459,29 @@ declare module 'svelte' { }): Snippet; /** Anything except a function */ type NotFunction = T extends Function ? never : T; + /** + * Synchronously flush any pending updates. + * Returns void if no callback is provided, otherwise returns the result of calling the callback. + * */ + export function flushSync(fn?: (() => T) | undefined): T; + /** + * 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 + * the user is about to take some action. + * + * Frameworks like SvelteKit can use this to preload data when the user touches or + * hovers over a link, making any subsequent navigation feel instantaneous. + * + * The `fn` parameter is a synchronous function that modifies some state. The + * state changes will be reverted after the fork is initialised, then reapplied + * if and when the fork is eventually committed. + * + * When it becomes clear that a fork will _not_ be committed (e.g. because the + * user navigated elsewhere), it must be discarded to avoid leaking memory. + * + * @since 5.42 + */ + export function fork(fn: () => void): Fork; /** * Returns a `[get, set]` pair of functions for working with context in a type-safe way. * From 4eb432e941d4d8ebf1206ea86433b0c778b71737 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Oct 2025 17:27:04 -0400 Subject: [PATCH 31/58] chore: remove event hoisting (#17030) * chore: get rid of hoisted event handlers * remove unused stuff * simplify * wow we can delete so much more code. this makes me so happy * even more! --- .../phases/2-analyze/visitors/Attribute.js | 188 +----------------- .../2-analyze/visitors/shared/function.js | 7 - .../phases/3-transform/client/utils.js | 126 +----------- .../client/visitors/FunctionDeclaration.js | 11 - .../client/visitors/VariableDeclaration.js | 18 -- .../client/visitors/shared/events.js | 30 +-- .../client/visitors/shared/function.js | 13 -- .../src/compiler/phases/3-transform/utils.js | 19 -- packages/svelte/src/compiler/phases/nodes.js | 2 +- .../svelte/src/compiler/phases/types.d.ts | 26 --- .../svelte/src/compiler/types/template.d.ts | 11 +- .../svelte/src/compiler/utils/builders.js | 9 +- .../client/dom/elements/attributes.js | 4 +- .../internal/client/dom/elements/events.js | 9 +- packages/svelte/src/utils.js | 2 +- .../samples/inspect-new-property/_config.js | 4 +- .../samples/inspect-recursive/_config.js | 4 +- .../_expected/client/index.svelte.js | 11 +- .../_expected/client/index.svelte.js | 14 +- .../_expected/client/index.svelte.js | 3 +- .../_expected/client/index.svelte.js | 17 +- 21 files changed, 45 insertions(+), 483 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index b13f3f89b6..2b7d636606 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -1,12 +1,7 @@ -/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression } from 'estree' */ -/** @import { AST, DelegatedEvent } from '#compiler' */ +/** @import { AST } from '#compiler' */ /** @import { Context } from '../types' */ -import { cannot_be_set_statically, is_capture_event, is_delegated } from '../../../../utils.js'; -import { - get_attribute_chunks, - get_attribute_expression, - is_event_attribute -} from '../../../utils/ast.js'; +import { cannot_be_set_statically, can_delegate_event } from '../../../../utils.js'; +import { get_attribute_chunks, is_event_attribute } from '../../../utils/ast.js'; import { mark_subtree_dynamic } from './shared/fragment.js'; /** @@ -64,181 +59,8 @@ export function Attribute(node, context) { context.state.analysis.uses_event_attributes = true; } - const expression = get_attribute_expression(node); - const delegated_event = get_delegated_event(node.name.slice(2), expression, context); - - if (delegated_event !== null) { - if (delegated_event.hoisted) { - delegated_event.function.metadata.hoisted = true; - } - - node.metadata.delegated = delegated_event; - } - } - } -} - -/** @type {DelegatedEvent} */ -const unhoisted = { hoisted: false }; - -/** - * Checks if given event attribute can be delegated/hoisted and returns the corresponding info if so - * @param {string} event_name - * @param {Expression | null} handler - * @param {Context} context - * @returns {null | DelegatedEvent} - */ -function get_delegated_event(event_name, handler, context) { - // Handle delegated event handlers. Bail out if not a delegated event. - if (!handler || !is_delegated(event_name)) { - return null; - } - - // If we are not working with a RegularElement, then bail out. - const element = context.path.at(-1); - if (element?.type !== 'RegularElement') { - return null; - } - - /** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | null} */ - let target_function = null; - let binding = null; - - if (element.metadata.has_spread) { - // event attribute becomes part of the dynamic spread array - return unhoisted; - } - - if (handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') { - target_function = handler; - } else if (handler.type === 'Identifier') { - binding = context.state.scope.get(handler.name); - - if (context.state.analysis.module.scope.references.has(handler.name)) { - // If a binding with the same name is referenced in the module scope (even if not declared there), bail out - return unhoisted; - } - - if (binding != null) { - for (const { path } of binding.references) { - const parent = path.at(-1); - if (parent === undefined) return unhoisted; - - const grandparent = path.at(-2); - - /** @type {AST.RegularElement | null} */ - let element = null; - /** @type {string | null} */ - let event_name = null; - if (parent.type === 'OnDirective') { - element = /** @type {AST.RegularElement} */ (grandparent); - event_name = parent.name; - } else if ( - parent.type === 'ExpressionTag' && - grandparent?.type === 'Attribute' && - is_event_attribute(grandparent) - ) { - element = /** @type {AST.RegularElement} */ (path.at(-3)); - const attribute = /** @type {AST.Attribute} */ (grandparent); - event_name = get_attribute_event_name(attribute.name); - } - - if (element && event_name) { - if ( - element.type !== 'RegularElement' || - element.metadata.has_spread || - !is_delegated(event_name) - ) { - return unhoisted; - } - } else if (parent.type !== 'FunctionDeclaration' && parent.type !== 'VariableDeclarator') { - return unhoisted; - } - } + node.metadata.delegated = + parent?.type === 'RegularElement' && can_delegate_event(node.name.slice(2)); } - - // If the binding is exported, bail out - if (context.state.analysis.exports.find((node) => node.name === handler.name)) { - return unhoisted; - } - - if (binding?.is_function()) { - target_function = binding.initial; - } - } - - // If we can't find a function, or the function has multiple parameters, bail out - if (target_function == null || target_function.params.length > 1) { - return unhoisted; - } - - const visited_references = new Set(); - const scope = target_function.metadata.scope; - for (const [reference] of scope.references) { - // Bail out if the arguments keyword is used or $host is referenced - if (reference === 'arguments' || reference === '$host') return unhoisted; - // Bail out if references a store subscription - if (scope.get(`$${reference}`)?.kind === 'store_sub') return unhoisted; - - const binding = scope.get(reference); - const local_binding = context.state.scope.get(reference); - - // if the function access a snippet that can't be hoisted we bail out - if ( - local_binding !== null && - local_binding.initial?.type === 'SnippetBlock' && - !local_binding.initial.metadata.can_hoist - ) { - return unhoisted; - } - - // If we are referencing a binding that is shadowed in another scope then bail out (unless it's declared within the function). - if ( - local_binding !== null && - binding !== null && - local_binding.node !== binding.node && - scope.declarations.get(reference) !== binding - ) { - return unhoisted; - } - - // If we have multiple references to the same store using $ prefix, bail out. - if ( - binding !== null && - binding.kind === 'store_sub' && - visited_references.has(reference.slice(1)) - ) { - return unhoisted; - } - - // If we reference the index within an each block, then bail out. - if (binding !== null && binding.initial?.type === 'EachBlock') return unhoisted; - - if ( - binding !== null && - // Bail out if the binding is a rest param - (binding.declaration_kind === 'rest_param' || - // Bail out if we reference anything from the EachBlock (for now) that mutates in non-runes mode, - (((!context.state.analysis.runes && binding.kind === 'each') || - // or any normal not reactive bindings that are mutated. - binding.kind === 'normal') && - binding.updated)) - ) { - return unhoisted; - } - visited_references.add(reference); - } - - return { hoisted: true, function: target_function }; -} - -/** - * @param {string} event_name - */ -function get_attribute_event_name(event_name) { - event_name = event_name.slice(2); - if (is_capture_event(event_name)) { - event_name = event_name.slice(0, -7); } - return event_name; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js index 1776167850..4d93cd44e0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js @@ -6,13 +6,6 @@ * @param {Context} context */ export function visit_function(node, context) { - // TODO retire this in favour of a more general solution based on bindings - node.metadata = { - hoisted: false, - hoisted_params: [], - scope: context.state.scope - }; - if (context.state.expression) { for (const [name] of context.state.scope.references) { const binding = context.state.scope.get(name); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index 41ed277898..f21fb43fc1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -1,6 +1,6 @@ -/** @import { ArrowFunctionExpression, AssignmentExpression, BlockStatement, Expression, FunctionDeclaration, FunctionExpression, Identifier, Node, Pattern, UpdateExpression } from 'estree' */ +/** @import { BlockStatement, Expression, Identifier } from 'estree' */ /** @import { Binding } from '#compiler' */ -/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */ +/** @import { ClientTransformState, ComponentClientTransformState } from './types.js' */ /** @import { Analysis } from '../../types.js' */ /** @import { Scope } from '../../scope.js' */ import * as b from '#compiler/builders'; @@ -12,9 +12,6 @@ import { PROPS_IS_UPDATED, PROPS_IS_BINDABLE } from '../../../../constants.js'; -import { dev } from '../../../state.js'; -import { walk } from 'zimmerframe'; -import { validate_mutation } from './visitors/shared/utils.js'; /** * @param {Binding} binding @@ -46,125 +43,6 @@ export function build_getter(node, state) { return node; } -/** - * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node - * @param {ComponentContext} context - * @returns {Pattern[]} - */ -function get_hoisted_params(node, context) { - const scope = context.state.scope; - - /** @type {Identifier[]} */ - const params = []; - - /** - * We only want to push if it's not already present to avoid name clashing - * @param {Identifier} id - */ - function push_unique(id) { - if (!params.find((param) => param.name === id.name)) { - params.push(id); - } - } - - for (const [reference] of scope.references) { - let binding = scope.get(reference); - - if (binding !== null && !scope.declarations.has(reference) && binding.initial !== node) { - if (binding.kind === 'store_sub') { - // We need both the subscription for getting the value and the store for updating - push_unique(b.id(binding.node.name)); - binding = /** @type {Binding} */ (scope.get(binding.node.name.slice(1))); - } - - let expression = context.state.transform[reference]?.read(b.id(binding.node.name)); - - if ( - // If it's a destructured derived binding, then we can extract the derived signal reference and use that. - // TODO this code is bad, we need to kill it - expression != null && - typeof expression !== 'function' && - expression.type === 'MemberExpression' && - expression.object.type === 'CallExpression' && - expression.object.callee.type === 'Identifier' && - expression.object.callee.name === '$.get' && - expression.object.arguments[0].type === 'Identifier' - ) { - push_unique(b.id(expression.object.arguments[0].name)); - } else if ( - // If we are referencing a simple $$props value, then we need to reference the object property instead - (binding.kind === 'prop' || binding.kind === 'bindable_prop') && - !is_prop_source(binding, context.state) - ) { - push_unique(b.id('$$props')); - } else if ( - // imports don't need to be hoisted - binding.declaration_kind !== 'import' - ) { - // create a copy to remove start/end tags which would mess up source maps - push_unique(b.id(binding.node.name)); - // rest props are often accessed through the $$props object for optimization reasons, - // but we can't know if the delegated event handler will use it, so we need to add both as params - if (binding.kind === 'rest_prop' && context.state.analysis.runes) { - push_unique(b.id('$$props')); - } - } - } - } - - if (dev) { - // this is a little hacky, but necessary for ownership validation - // to work inside hoisted event handlers - - /** - * @param {AssignmentExpression | UpdateExpression} node - * @param {{ next: () => void, stop: () => void }} context - */ - function visit(node, { next, stop }) { - if (validate_mutation(node, /** @type {any} */ (context), node) !== node) { - params.push(b.id('$$ownership_validator')); - stop(); - } else { - next(); - } - } - - walk(/** @type {Node} */ (node), null, { - AssignmentExpression: visit, - UpdateExpression: visit - }); - } - - return params; -} - -/** - * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node - * @param {ComponentContext} context - * @returns {Pattern[]} - */ -export function build_hoisted_params(node, context) { - const hoisted_params = get_hoisted_params(node, context); - node.metadata.hoisted_params = hoisted_params; - - /** @type {Pattern[]} */ - const params = []; - - if (node.params.length === 0) { - if (hoisted_params.length > 0) { - // For the event object - params.push(b.id(context.state.scope.generate('_'))); - } - } else { - for (const param of node.params) { - params.push(/** @type {Pattern} */ (context.visit(param))); - } - } - - params.push(...hoisted_params); - return params; -} - /** * @param {Binding} binding * @param {ComponentClientTransformState} state diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js index cd299a710b..17327c21d6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js @@ -1,7 +1,5 @@ /** @import { FunctionDeclaration } from 'estree' */ /** @import { ComponentContext } from '../types' */ -import { build_hoisted_params } from '../utils.js'; -import * as b from '#compiler/builders'; /** * @param {FunctionDeclaration} node @@ -10,14 +8,5 @@ import * as b from '#compiler/builders'; export function FunctionDeclaration(node, context) { const state = { ...context.state, in_constructor: false, in_derived: false }; - if (node.metadata?.hoisted === true) { - const params = build_hoisted_params(node, context); - const body = context.visit(node.body, state); - - context.state.hoisted.push(/** @type {FunctionDeclaration} */ ({ ...node, params, body })); - - return b.empty; - } - context.next(state); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index 2fc3a8ed80..0f87baa433 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -7,7 +7,6 @@ import * as b from '#compiler/builders'; import * as assert from '../../../../utils/assert.js'; import { get_rune } from '../../../scope.js'; import { get_prop_source, is_prop_source, is_state_source, should_proxy } from '../utils.js'; -import { is_hoisted_function } from '../../utils.js'; import { get_value } from './shared/declarations.js'; /** @@ -32,13 +31,6 @@ export function VariableDeclaration(node, context) { rune === '$state.snapshot' || rune === '$host' ) { - if (init != null && is_hoisted_function(init)) { - context.state.hoisted.push( - b.const(declarator.id, /** @type {Expression} */ (context.visit(init))) - ); - - continue; - } declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator))); continue; } @@ -295,16 +287,6 @@ export function VariableDeclaration(node, context) { const has_props = bindings.some((binding) => binding.kind === 'bindable_prop'); if (!has_state && !has_props) { - const init = declarator.init; - - if (init != null && is_hoisted_function(init)) { - context.state.hoisted.push( - b.const(declarator.id, /** @type {Expression} */ (context.visit(init))) - ); - - continue; - } - declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator))); continue; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js index d252bd5474..d4d6721960 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js @@ -26,40 +26,12 @@ export function visit_event_attribute(node, context) { let handler = build_event_handler(tag.expression, tag.metadata.expression, context); if (node.metadata.delegated) { - let delegated_assignment; - if (!context.state.events.has(event_name)) { context.state.events.add(event_name); } - // Hoist function if we can, otherwise we leave the function as is - if (node.metadata.delegated.hoisted) { - if (node.metadata.delegated.function === tag.expression) { - const func_name = context.state.scope.root.unique('on_' + event_name); - context.state.hoisted.push(b.var(func_name, handler)); - handler = func_name; - } - - const hoisted_params = /** @type {Expression[]} */ ( - node.metadata.delegated.function.metadata.hoisted_params - ); - - // When we hoist a function we assign an array with the function and all - // hoisted closure params. - if (hoisted_params) { - const args = [handler, ...hoisted_params]; - delegated_assignment = b.array(args); - } else { - delegated_assignment = handler; - } - } else { - delegated_assignment = handler; - } - context.state.init.push( - b.stmt( - b.assignment('=', b.member(context.state.node, '__' + event_name), delegated_assignment) - ) + b.stmt(b.assignment('=', b.member(context.state.node, '__' + event_name), handler)) ); } else { const statement = b.stmt( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/function.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/function.js index 691ac0b01e..3677b30814 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/function.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/function.js @@ -1,14 +1,11 @@ /** @import { ArrowFunctionExpression, FunctionExpression, Node } from 'estree' */ /** @import { ComponentContext } from '../../types' */ -import { build_hoisted_params } from '../../utils.js'; /** * @param {ArrowFunctionExpression | FunctionExpression} node * @param {ComponentContext} context */ export const visit_function = (node, context) => { - const metadata = node.metadata; - let state = { ...context.state, in_constructor: false, in_derived: false }; if (node.type === 'FunctionExpression') { @@ -16,15 +13,5 @@ export const visit_function = (node, context) => { state.in_constructor = parent.type === 'MethodDefinition' && parent.kind === 'constructor'; } - if (metadata?.hoisted === true) { - const params = build_hoisted_params(node, context); - - return /** @type {FunctionExpression} */ ({ - ...node, - params, - body: context.visit(node.body, state) - }); - } - context.next(state); }; diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js index dfc2ab1de1..f61b59f3bd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/utils.js @@ -1,36 +1,17 @@ -/** @import { Context } from 'zimmerframe' */ /** @import { TransformState } from './types.js' */ /** @import { AST, Binding, Namespace, ValidatedCompileOptions } from '#compiler' */ /** @import { Node, Expression, CallExpression, MemberExpression } from 'estree' */ import { regex_ends_with_whitespaces, regex_not_whitespace, - regex_starts_with_newline, regex_starts_with_whitespaces } from '../patterns.js'; -import * as b from '#compiler/builders'; import * as e from '../../errors.js'; import { walk } from 'zimmerframe'; import { extract_identifiers } from '../../utils/ast.js'; import check_graph_for_cycles from '../2-analyze/utils/check_graph_for_cycles.js'; import is_reference from 'is-reference'; import { set_scope } from '../scope.js'; -import { dev } from '../../state.js'; - -/** - * @param {Node} node - * @returns {boolean} - */ -export function is_hoisted_function(node) { - if ( - node.type === 'ArrowFunctionExpression' || - node.type === 'FunctionExpression' || - node.type === 'FunctionDeclaration' - ) { - return node.metadata?.hoisted === true; - } - return false; -} /** * Match Svelte 4 behaviour by sorting ConstTag nodes in topological order diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index f4127db359..13188681d2 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -59,7 +59,7 @@ export function create_attribute(name, start, end, value) { name, value, metadata: { - delegated: null, + delegated: false, needs_clsx: false } }; diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index 4e287fd199..074012e03f 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -109,29 +109,3 @@ export interface ComponentAnalysis extends Analysis { */ snippets: Set; } - -declare module 'estree' { - interface ArrowFunctionExpression { - metadata: { - hoisted: boolean; - hoisted_params: Pattern[]; - scope: Scope; - }; - } - - interface FunctionExpression { - metadata: { - hoisted: boolean; - hoisted_params: Pattern[]; - scope: Scope; - }; - } - - interface FunctionDeclaration { - metadata: { - hoisted: boolean; - hoisted_params: Pattern[]; - scope: Scope; - }; - } -} diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 42048c3525..f38706d075 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -5,8 +5,6 @@ import type { VariableDeclaration, VariableDeclarator, Expression, - FunctionDeclaration, - FunctionExpression, Identifier, MemberExpression, Node, @@ -27,13 +25,6 @@ import type { _CSS } from './css'; */ export type Namespace = 'html' | 'svg' | 'mathml'; -export type DelegatedEvent = - | { - hoisted: true; - function: ArrowFunctionExpression | FunctionExpression | FunctionDeclaration; - } - | { hoisted: false }; - export namespace AST { export interface BaseNode { type: string; @@ -531,7 +522,7 @@ export namespace AST { /** @internal */ metadata: { /** May be set if this is an event attribute */ - delegated: null | DelegatedEvent; + delegated: boolean; /** May be `true` if this is a `class` attribute that needs `clsx` */ needs_clsx: boolean; }; diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index f21b0dc8b4..1a2d5cab5c 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -42,8 +42,7 @@ export function arrow(params, body, async = false) { body, expression: body.type !== 'BlockStatement', generator: false, - async, - metadata: /** @type {any} */ (null) // should not be used by codegen + async }; } @@ -237,8 +236,7 @@ export function function_declaration(id, params, body, async = false) { params, body, generator: false, - async, - metadata: /** @type {any} */ (null) // should not be used by codegen + async }; } @@ -595,8 +593,7 @@ function function_builder(id, params, body, async = false) { params, body, generator: false, - async, - metadata: /** @type {any} */ (null) // should not be used by codegen + async }; } diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index a5f63359c9..55d5d7860a 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -7,7 +7,7 @@ import { add_form_reset_listener, autofocus } from './misc.js'; import * as w from '../../warnings.js'; import { LOADING_ATTR_SYMBOL } from '#client/constants'; import { queue_micro_task } from '../task.js'; -import { is_capture_event, is_delegated, normalize_attribute } from '../../../../utils.js'; +import { is_capture_event, can_delegate_event, normalize_attribute } from '../../../../utils.js'; import { active_effect, active_reaction, @@ -378,7 +378,7 @@ function set_attributes( const opts = {}; const event_handle_key = '$$' + key; let event_name = key.slice(2); - var delegated = is_delegated(event_name); + var delegated = can_delegate_event(event_name); if (is_capture_event(event_name)) { event_name = event_name.slice(0, -7); diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index 15544d7426..4c64c8364a 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -1,5 +1,5 @@ import { teardown } from '../../reactivity/effects.js'; -import { define_property, is_array } from '../../../shared/utils.js'; +import { define_property } from '../../../shared/utils.js'; import { hydrating } from '../hydration.js'; import { queue_micro_task } from '../task.js'; import { FILENAME } from '../../../../constants.js'; @@ -258,12 +258,7 @@ export function handle_event_propagation(event) { // -> the target could not have been disabled because it emits the event in the first place event.target === current_target) ) { - if (is_array(delegated)) { - var [fn, ...data] = delegated; - fn.apply(current_target, [event, ...data]); - } else { - delegated.call(current_target, event); - } + delegated.call(current_target, event); } } catch (error) { if (throw_error) { diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index a54a421418..d63d4ff801 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -137,7 +137,7 @@ const DELEGATED_EVENTS = [ * Returns `true` if `event_name` is a delegated event * @param {string} event_name */ -export function is_delegated(event_name) { +export function can_delegate_event(event_name) { return DELEGATED_EVENTS.includes(event_name); } diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js index 8134044b16..43d217977e 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-new-property/_config.js @@ -15,9 +15,9 @@ export default test({ {}, [], { x: 'hello' }, - 'at HTMLButtonElement.on_click', + 'at HTMLButtonElement.Main.button.__click', ['hello'], - 'at HTMLButtonElement.on_click' + 'at HTMLButtonElement.Main.button.__click' ]); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js index 9d95956e7d..8bf67159f5 100644 --- a/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/inspect-recursive/_config.js @@ -15,9 +15,9 @@ export default test({ assert.deepEqual(normalise_inspect_logs(logs), [ [], [{}], - 'at HTMLButtonElement.on_click', + 'at HTMLButtonElement.Main.button.__click', [{}, {}], - 'at HTMLButtonElement.on_click' + 'at HTMLButtonElement.Main.button.__click' ]); } }); diff --git a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js index 9bb45ebf78..52820c1652 100644 --- a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client/index.svelte.js @@ -1,19 +1,20 @@ import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; -function increment(_, counter) { - counter.count += 1; -} - var root = $.from_html(` `, 1); export default function Await_block_scope($$anchor) { let counter = $.proxy({ count: 0 }); const promise = $.derived(() => Promise.resolve(counter)); + + function increment() { + counter.count += 1; + } + var fragment = root(); var button = $.first_child(fragment); - button.__click = [increment, counter]; + button.__click = increment; var text = $.child(button); diff --git a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js index 0d95d8d335..ae28419b95 100644 --- a/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/delegated-locally-declared-shadowed/_expected/client/index.svelte.js @@ -2,12 +2,6 @@ import 'svelte/internal/disclose-version'; import 'svelte/internal/flags/legacy'; import * as $ from 'svelte/internal/client'; -var on_click = (e) => { - const index = Number(e.currentTarget.dataset.index); - - console.log(index); -}; - var root_1 = $.from_html(``); export default function Delegated_locally_declared_shadowed($$anchor) { @@ -18,7 +12,13 @@ export default function Delegated_locally_declared_shadowed($$anchor) { var button = root_1(); $.set_attribute(button, 'data-index', index); - button.__click = [on_click]; + + button.__click = (e) => { + const index = Number(e.currentTarget.dataset.index); + + console.log(index); + }; + $.append($$anchor, button); }); diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js index b46acee82e..7025c788be 100644 --- a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client/index.svelte.js @@ -1,7 +1,6 @@ import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; -var on_click = (_, count) => $.update(count); var root = $.from_html(`

`, 1); export default function Nullish_coallescence_omittance($$anchor) { @@ -18,7 +17,7 @@ export default function Nullish_coallescence_omittance($$anchor) { var button = $.sibling(b, 2); - button.__click = [on_click, count]; + button.__click = () => $.update(count); var text = $.child(button); diff --git a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js index c446b3d3ef..30691231f4 100644 --- a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js @@ -1,18 +1,19 @@ import 'svelte/internal/disclose-version'; import * as $ from 'svelte/internal/client'; -function reset(_, str, tpl) { - $.set(str, ''); - $.set(str, ``); - $.set(tpl, ''); - $.set(tpl, ``); -} - var root = $.from_html(` `, 1); export default function State_proxy_literal($$anchor) { let str = $.state(''); let tpl = $.state(``); + + function reset() { + $.set(str, ''); + $.set(str, ``); + $.set(tpl, ''); + $.set(tpl, ``); + } + var fragment = root(); var input = $.first_child(fragment); @@ -24,7 +25,7 @@ export default function State_proxy_literal($$anchor) { var button = $.sibling(input_1, 2); - button.__click = [reset, str, tpl]; + button.__click = reset; $.bind_value(input, () => $.get(str), ($$value) => $.set(str, $$value)); $.bind_value(input_1, () => $.get(tpl), ($$value) => $.set(tpl, $$value)); $.append($$anchor, fragment); From b7f39b464a00eda8eeb233dbf52d03a1fe0a740b Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Sat, 25 Oct 2025 17:16:29 +0200 Subject: [PATCH 32/58] fix: always allow `setContext` before first await in component (#17031) The previous check was flawed because EFFECT_RAN would be set by the time it is checked, since a promise in a parent component will cause a delay of the inner component being instantiated. Instead we have a new field on the component context checking if the component was already popped (if se we are indeed too late). Don't love it to have a field just for this but I don't see another way to reliably check it. Fixes #16629 --- .changeset/itchy-hats-study.md | 5 +++++ packages/svelte/src/internal/client/constants.js | 1 + packages/svelte/src/internal/client/context.js | 9 ++++++++- packages/svelte/src/internal/client/error-handling.js | 4 ++-- packages/svelte/src/internal/client/types.d.ts | 4 +++- .../async-context-throws-after-await/_config.js | 11 +++++++++++ .../async-context-throws-after-await/main.svelte | 7 +++++++ .../samples/async-set-context/Inner.svelte | 7 +++++++ .../samples/async-set-context/Outer.svelte | 9 +++++++++ .../samples/async-set-context/_config.js | 11 +++++++++++ .../samples/async-set-context/main.svelte | 7 +++++++ 11 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 .changeset/itchy-hats-study.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-set-context/Inner.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-set-context/Outer.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-set-context/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-set-context/main.svelte diff --git a/.changeset/itchy-hats-study.md b/.changeset/itchy-hats-study.md new file mode 100644 index 0000000000..e92ec5affd --- /dev/null +++ b/.changeset/itchy-hats-study.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: always allow `setContext` before first await in component diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 24dc9e4fb8..c2f7861b78 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -13,6 +13,7 @@ export const INERT = 1 << 13; export const DESTROYED = 1 << 14; // Flags exclusive to effects +/** Set once an effect that should run synchronously has run */ export const EFFECT_RAN = 1 << 15; /** * 'Transparent' effects do not create a transition boundary. diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index 751a35321a..ffdb342adb 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -128,7 +128,11 @@ export function setContext(key, context) { if (async_mode_flag) { var flags = /** @type {Effect} */ (active_effect).f; - var valid = !active_reaction && (flags & BRANCH_EFFECT) !== 0 && (flags & EFFECT_RAN) === 0; + var valid = + !active_reaction && + (flags & BRANCH_EFFECT) !== 0 && + // pop() runs synchronously, so this indicates we're setting context after an await + !(/** @type {ComponentContext} */ (component_context).i); if (!valid) { e.set_context_after_init(); @@ -173,6 +177,7 @@ export function getAllContexts() { export function push(props, runes = false, fn) { component_context = { p: component_context, + i: false, c: null, e: null, s: props, @@ -208,6 +213,8 @@ export function pop(component) { context.x = component; } + context.i = true; + component_context = context.p; if (DEV) { diff --git a/packages/svelte/src/internal/client/error-handling.js b/packages/svelte/src/internal/client/error-handling.js index 6c83a453d5..dcbbf14e20 100644 --- a/packages/svelte/src/internal/client/error-handling.js +++ b/packages/svelte/src/internal/client/error-handling.js @@ -29,7 +29,7 @@ export function handle_error(error) { // if the error occurred while creating this subtree, we let it // bubble up until it hits a boundary that can handle it if ((effect.f & BOUNDARY_EFFECT) === 0) { - if (!effect.parent && error instanceof Error) { + if (DEV && !effect.parent && error instanceof Error) { apply_adjustments(error); } @@ -61,7 +61,7 @@ export function invoke_error_boundary(error, effect) { effect = effect.parent; } - if (error instanceof Error) { + if (DEV && error instanceof Error) { apply_adjustments(error); } diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index d24218c4d3..deb3e82986 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -1,6 +1,6 @@ import type { Store } from '#shared'; import { STATE_SYMBOL } from './constants.js'; -import type { Effect, Source, Value, Reaction } from './reactivity/types.js'; +import type { Effect, Source, Value } from './reactivity/types.js'; type EventCallback = (event: Event) => boolean; export type EventCallbackMap = Record; @@ -16,6 +16,8 @@ export type ComponentContext = { c: null | Map; /** deferred effects */ e: null | Array<() => void | (() => void)>; + /** True if initialized, i.e. pop() ran */ + i: boolean; /** * props — needed for legacy mode lifecycle functions, and for `createEventDispatcher` * @deprecated remove in 6.0 diff --git a/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/_config.js new file mode 100644 index 0000000000..be73968a88 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + async test() { + // else runtime_error is checked too soon + await tick(); + }, + runtime_error: 'set_context_after_init' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/main.svelte new file mode 100644 index 0000000000..8e770c214b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-context-throws-after-await/main.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-set-context/Inner.svelte new file mode 100644 index 0000000000..2c7fd5d43d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/Inner.svelte @@ -0,0 +1,7 @@ + + +

{greeting}

\ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/Outer.svelte b/packages/svelte/tests/runtime-runes/samples/async-set-context/Outer.svelte new file mode 100644 index 0000000000..9a493c5b75 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/Outer.svelte @@ -0,0 +1,9 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/_config.js b/packages/svelte/tests/runtime-runes/samples/async-set-context/_config.js new file mode 100644 index 0000000000..041f67a39e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/_config.js @@ -0,0 +1,11 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client', 'async-server'], + ssrHtml: `

hi

`, + async test({ assert, target }) { + await tick(); + assert.htmlEqual(target.innerHTML, '

hi

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-set-context/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-set-context/main.svelte new file mode 100644 index 0000000000..01b46bda93 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-set-context/main.svelte @@ -0,0 +1,7 @@ + + + From d8137b78a5ab747d0b51fd531288b113b974d830 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 11:29:10 -0400 Subject: [PATCH 33/58] fix: less confusing names for inspect errors (#17026) * fix: less confusing names for inspect errors * fix * Update packages/svelte/src/internal/client/dev/inspect.js * fix --- .changeset/swift-taxes-shake.md | 5 +++++ .../svelte/src/internal/client/dev/inspect.js | 11 ++++++++++- .../svelte/src/internal/client/dev/tracing.js | 3 +-- packages/svelte/src/internal/client/proxy.js | 2 +- .../src/internal/client/reactivity/deriveds.js | 2 +- .../src/internal/client/reactivity/sources.js | 4 ++-- packages/svelte/src/internal/client/runtime.js | 4 ++-- packages/svelte/tests/helpers.js | 18 ++++++++++++++---- .../async-reactivity-loss-for-await/_config.js | 2 +- .../samples/async-reactivity-loss/_config.js | 2 +- .../samples/effect-loop-infinite/_config.js | 2 +- 11 files changed, 39 insertions(+), 16 deletions(-) create mode 100644 .changeset/swift-taxes-shake.md diff --git a/.changeset/swift-taxes-shake.md b/.changeset/swift-taxes-shake.md new file mode 100644 index 0000000000..73b1529d87 --- /dev/null +++ b/.changeset/swift-taxes-shake.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: less confusing names for inspect errors diff --git a/packages/svelte/src/internal/client/dev/inspect.js b/packages/svelte/src/internal/client/dev/inspect.js index 09150d6ee4..34ba508984 100644 --- a/packages/svelte/src/internal/client/dev/inspect.js +++ b/packages/svelte/src/internal/client/dev/inspect.js @@ -33,8 +33,17 @@ export function inspect(get_value, inspector, show_stack = false) { inspector(...snap); if (!initial) { + const stack = get_stack('$inspect(...)'); // eslint-disable-next-line no-console - console.log(get_stack('UpdatedAt')); + + if (stack) { + // eslint-disable-next-line no-console + console.groupCollapsed('stack trace'); + // eslint-disable-next-line no-console + console.log(stack); + // eslint-disable-next-line no-console + console.groupEnd(); + } } } else { inspector(initial ? 'init' : 'update', ...snap); diff --git a/packages/svelte/src/internal/client/dev/tracing.js b/packages/svelte/src/internal/client/dev/tracing.js index 98be92d4b2..4688637f5d 100644 --- a/packages/svelte/src/internal/client/dev/tracing.js +++ b/packages/svelte/src/internal/client/dev/tracing.js @@ -179,8 +179,7 @@ export function get_stack(label) { }); define_property(error, 'name', { - // 'Error' suffix is required for stack traces to be rendered properly - value: `${label}Error` + value: label }); return /** @type {Error & { stack: string }} */ (error); diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 9baacacd0d..49cef451b3 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -53,7 +53,7 @@ export function proxy(value) { var is_proxied_array = is_array(value); var version = source(0); - var stack = DEV && tracing_mode_flag ? get_stack('CreatedAt') : null; + var stack = DEV && tracing_mode_flag ? get_stack('created at') : null; var parent_version = update_version; /** diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index b6a50acc4d..1eb640ad26 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -86,7 +86,7 @@ export function derived(fn) { }; if (DEV && tracing_mode_flag) { - signal.created = get_stack('CreatedAt'); + signal.created = get_stack('created at'); } return signal; diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 9534e718a5..b480d4155a 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -76,7 +76,7 @@ export function source(v, stack) { }; if (DEV && tracing_mode_flag) { - signal.created = stack ?? get_stack('CreatedAt'); + signal.created = stack ?? get_stack('created at'); signal.updated = null; signal.set_during_effect = false; signal.trace = null; @@ -186,7 +186,7 @@ export function internal_set(source, value) { if (DEV) { if (tracing_mode_flag || active_effect !== null) { - const error = get_stack('UpdatedAt'); + const error = get_stack('updated at'); if (error !== null) { source.updated ??= new Map(); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 2e6f05b4b1..49396d6feb 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -609,7 +609,7 @@ export function get(signal) { if (!tracking && !untracking && !was_read) { w.await_reactivity_loss(/** @type {string} */ (signal.label)); - var trace = get_stack('TracedAt'); + var trace = get_stack('traced at'); // eslint-disable-next-line no-console if (trace) console.warn(trace); } @@ -628,7 +628,7 @@ export function get(signal) { if (signal.trace) { signal.trace(); } else { - trace = get_stack('TracedAt'); + trace = get_stack('traced at'); if (trace) { var entry = tracing_expressions.entries.get(signal); diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js index bf708878a3..d0ec8b6e44 100644 --- a/packages/svelte/tests/helpers.js +++ b/packages/svelte/tests/helpers.js @@ -201,7 +201,15 @@ export const async_mode = process.env.SVELTE_NO_ASYNC !== 'true'; * @param {any[]} logs */ export function normalise_inspect_logs(logs) { - return logs.map((log) => { + /** @type {string[]} */ + const normalised = []; + + for (const log of logs) { + if (log === 'stack trace') { + // ignore `console.group('stack trace')` in default `$inspect(...)` output + continue; + } + if (log instanceof Error) { const last_line = log.stack ?.trim() @@ -210,11 +218,13 @@ export function normalise_inspect_logs(logs) { const match = last_line && /(at .+) /.exec(last_line); - return match && match[1]; + if (match) normalised.push(match[1]); + } else { + normalised.push(log); } + } - return log; - }); + return normalised; } /** diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js index bde65a499f..2bcb129b12 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js @@ -17,7 +17,7 @@ export default test({ 'Detected reactivity loss when reading `values[1]`. This happens when state is read in an async function after an earlier `await`' ); - assert.equal(warnings[1].name, 'TracedAtError'); + assert.equal(warnings[1].name, 'traced at'); assert.equal(warnings.length, 2); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js index 16318a3b44..747648e83f 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js @@ -20,7 +20,7 @@ export default test({ 'Detected reactivity loss when reading `b`. This happens when state is read in an async function after an earlier `await`' ); - assert.equal(warnings[1].name, 'TracedAtError'); + assert.equal(warnings[1].name, 'traced at'); assert.equal(warnings.length, 2); } diff --git a/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js index 400495050c..57f60c2b44 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/effect-loop-infinite/_config.js @@ -14,7 +14,7 @@ export default test({ try { flushSync(() => button.click()); } catch (e) { - assert.equal(errors.length, 1); // for whatever reason we can't get the name which should be UpdatedAtError + assert.equal(errors.length, 1); // for whatever reason we can't get the name which should be 'updated at' assert.ok(/** @type {Error} */ (e).message.startsWith('effect_update_depth_exceeded')); } } From 875a04170ec58e81234a28aea742131391233ae9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 11:35:04 -0400 Subject: [PATCH 34/58] Version Packages (#17029) * Version Packages * minor not patch * Apply suggestions from code review --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Rich Harris --- .changeset/itchy-hats-study.md | 5 ----- .changeset/small-geckos-camp.md | 5 ----- .changeset/swift-taxes-shake.md | 5 ----- packages/svelte/CHANGELOG.md | 12 ++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 6 files changed, 14 insertions(+), 17 deletions(-) delete mode 100644 .changeset/itchy-hats-study.md delete mode 100644 .changeset/small-geckos-camp.md delete mode 100644 .changeset/swift-taxes-shake.md diff --git a/.changeset/itchy-hats-study.md b/.changeset/itchy-hats-study.md deleted file mode 100644 index e92ec5affd..0000000000 --- a/.changeset/itchy-hats-study.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: always allow `setContext` before first await in component diff --git a/.changeset/small-geckos-camp.md b/.changeset/small-geckos-camp.md deleted file mode 100644 index 622cbbbfa0..0000000000 --- a/.changeset/small-geckos-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"svelte": patch ---- - -feat: experimental `fork` API diff --git a/.changeset/swift-taxes-shake.md b/.changeset/swift-taxes-shake.md deleted file mode 100644 index 73b1529d87..0000000000 --- a/.changeset/swift-taxes-shake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: less confusing names for inspect errors diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index d63548d3e1..c59d4dbc6e 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.42.0 + +### Minor Changes + +- feat: experimental `fork` API ([#17004](https://github.com/sveltejs/svelte/pull/17004)) + +### Patch Changes + +- fix: always allow `setContext` before first await in component ([#17031](https://github.com/sveltejs/svelte/pull/17031)) + +- fix: less confusing names for inspect errors ([#17026](https://github.com/sveltejs/svelte/pull/17026)) + ## 5.41.4 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 1d50920d4f..c3b7254bd5 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.41.4", + "version": "5.42.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index f5f47c6056..abdb12e088 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.41.4'; +export const VERSION = '5.42.0'; export const PUBLIC_VERSION = '5'; From 657ec89caa93d39d2cf8d8cdbe27b0041615409f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 14:12:22 -0400 Subject: [PATCH 35/58] fix: ignore fork `discard()` after `commit()` (#17034) * fix: ignore fork `discard()` after `commit()` * fix message --- .changeset/twenty-onions-attack.md | 5 +++++ .../98-reference/.generated/client-errors.md | 2 +- .../svelte/messages/client-errors/errors.md | 2 +- packages/svelte/src/internal/client/errors.js | 4 ++-- .../src/internal/client/reactivity/batch.js | 22 +++++++++++++------ 5 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 .changeset/twenty-onions-attack.md diff --git a/.changeset/twenty-onions-attack.md b/.changeset/twenty-onions-attack.md new file mode 100644 index 0000000000..c23c8b590b --- /dev/null +++ b/.changeset/twenty-onions-attack.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ignore fork `discard()` after `commit()` diff --git a/documentation/docs/98-reference/.generated/client-errors.md b/documentation/docs/98-reference/.generated/client-errors.md index 74a0674dba..3f1cb8f76b 100644 --- a/documentation/docs/98-reference/.generated/client-errors.md +++ b/documentation/docs/98-reference/.generated/client-errors.md @@ -149,7 +149,7 @@ This restriction only applies when using the `experimental.async` option, which ### fork_discarded ``` -Cannot commit a fork that was already committed or discarded +Cannot commit a fork that was already discarded ``` ### fork_timing diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index b5fe51539d..ae7d811b2e 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -114,7 +114,7 @@ This restriction only applies when using the `experimental.async` option, which ## fork_discarded -> Cannot commit a fork that was already committed or discarded +> Cannot commit a fork that was already discarded ## fork_timing diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 2a433ed8f9..8a5fde4f3b 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -262,12 +262,12 @@ export function flush_sync_in_effect() { } /** - * Cannot commit a fork that was already committed or discarded + * Cannot commit a fork that was already discarded * @returns {never} */ export function fork_discarded() { if (DEV) { - const error = new Error(`fork_discarded\nCannot commit a fork that was already committed or discarded\nhttps://svelte.dev/e/fork_discarded`); + const error = new Error(`fork_discarded\nCannot commit a fork that was already discarded\nhttps://svelte.dev/e/fork_discarded`); error.name = 'Svelte error'; diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index fdeb111a4d..ab83050cd0 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -913,28 +913,36 @@ export function fork(fn) { e.fork_timing(); } - const batch = Batch.ensure(); + var batch = Batch.ensure(); batch.is_fork = true; - const settled = batch.settled(); + var committed = false; + var settled = batch.settled(); flushSync(fn); // revert state changes - for (const [source, value] of batch.previous) { + for (var [source, value] of batch.previous) { source.v = value; } return { commit: async () => { + if (committed) { + await settled; + return; + } + if (!batches.has(batch)) { e.fork_discarded(); } + committed = true; + batch.is_fork = false; // apply changes - for (const [source, value] of batch.current) { + for (var [source, value] of batch.current) { source.v = value; } @@ -945,9 +953,9 @@ export function fork(fn) { // TODO maybe there's a better implementation? flushSync(() => { /** @type {Set} */ - const eager_effects = new Set(); + var eager_effects = new Set(); - for (const source of batch.current.keys()) { + for (var source of batch.current.keys()) { mark_eager_effects(source, eager_effects); } @@ -959,7 +967,7 @@ export function fork(fn) { await settled; }, discard: () => { - if (batches.has(batch)) { + if (!committed && batches.has(batch)) { batches.delete(batch); batch.discard(); } From e33f774877be45cf68fb96f84b4670a9deb52159 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 14:48:43 -0400 Subject: [PATCH 36/58] Version Packages (#17035) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/twenty-onions-attack.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/twenty-onions-attack.md diff --git a/.changeset/twenty-onions-attack.md b/.changeset/twenty-onions-attack.md deleted file mode 100644 index c23c8b590b..0000000000 --- a/.changeset/twenty-onions-attack.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ignore fork `discard()` after `commit()` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index c59d4dbc6e..51e6317491 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.42.1 + +### Patch Changes + +- fix: ignore fork `discard()` after `commit()` ([#17034](https://github.com/sveltejs/svelte/pull/17034)) + ## 5.42.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c3b7254bd5..1f8746e72d 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.42.0", + "version": "5.42.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index abdb12e088..8d50b983ce 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.42.0'; +export const VERSION = '5.42.1'; export const PUBLIC_VERSION = '5'; From 8368a4beb98fd9ed07fc34500ddb689916fbf8ad Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Sat, 25 Oct 2025 23:46:58 +0200 Subject: [PATCH 37/58] fix: better error message for global variable assignments (#17036) --- .changeset/slick-teeth-exist.md | 5 +++++ .../src/compiler/phases/2-analyze/visitors/shared/utils.js | 5 ++++- .../samples/global-variable-assignment/_config.js | 6 ++++++ .../samples/global-variable-assignment/foo.svelte.js | 1 + .../samples/global-variable-assignment/main.svelte | 3 +++ 5 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 .changeset/slick-teeth-exist.md create mode 100644 packages/svelte/tests/runtime-runes/samples/global-variable-assignment/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/global-variable-assignment/foo.svelte.js create mode 100644 packages/svelte/tests/runtime-runes/samples/global-variable-assignment/main.svelte diff --git a/.changeset/slick-teeth-exist.md b/.changeset/slick-teeth-exist.md new file mode 100644 index 0000000000..aeeb0f41b9 --- /dev/null +++ b/.changeset/slick-teeth-exist.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: better error message for global variable assignments diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js index d7b682da08..cc4376a0c2 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js @@ -22,7 +22,10 @@ export function validate_assignment(node, argument, context) { const binding = context.state.scope.get(argument.name); if (context.state.analysis.runes) { - if (binding?.node === context.state.analysis.props_id) { + if ( + context.state.analysis.props_id != null && + binding?.node === context.state.analysis.props_id + ) { e.constant_assignment(node, '$props.id()'); } diff --git a/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/_config.js b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/_config.js new file mode 100644 index 0000000000..37f4b2814c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/_config.js @@ -0,0 +1,6 @@ +import { test } from '../../test'; + +export default test({ + error: 'x is not defined', + async test() {} +}); diff --git a/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/foo.svelte.js b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/foo.svelte.js new file mode 100644 index 0000000000..198b8f89e7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/foo.svelte.js @@ -0,0 +1 @@ +x = 1; diff --git a/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/main.svelte b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/main.svelte new file mode 100644 index 0000000000..0ac6956b1d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/global-variable-assignment/main.svelte @@ -0,0 +1,3 @@ + From b01647455c495c4b99b84f9ecd7ce664e593b4f0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 20:36:01 -0400 Subject: [PATCH 38/58] chore: create_expression_metadata -> ExpressionMetadata (#17039) * chore: create_expression_metadata -> ExpressionMetadata * err whatever --- .../compiler/phases/1-parse/state/element.js | 17 ++++---- .../src/compiler/phases/1-parse/state/tag.js | 18 ++++---- .../src/compiler/phases/2-analyze/types.d.ts | 3 +- .../2-analyze/visitors/CallExpression.js | 4 +- .../phases/2-analyze/visitors/RenderTag.js | 4 +- .../client/visitors/shared/element.js | 3 +- .../client/visitors/shared/events.js | 3 +- .../client/visitors/shared/utils.js | 3 +- .../server/visitors/shared/element.js | 12 ++---- .../server/visitors/shared/utils.js | 5 ++- packages/svelte/src/compiler/phases/nodes.js | 41 ++++++++++++------- packages/svelte/src/compiler/phases/scope.js | 4 +- packages/svelte/src/compiler/types/index.d.ts | 17 -------- .../svelte/src/compiler/types/template.d.ts | 3 +- 14 files changed, 67 insertions(+), 70 deletions(-) diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index b0db9ce178..7b0950ae82 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -9,11 +9,10 @@ import { decode_character_references } from '../utils/html.js'; import * as e from '../../../errors.js'; import * as w from '../../../warnings.js'; import { create_fragment } from '../utils/create.js'; -import { create_attribute, create_expression_metadata, is_element_node } from '../../nodes.js'; +import { create_attribute, ExpressionMetadata, is_element_node } from '../../nodes.js'; import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js'; import { closing_tag_omitted } from '../../../../html-tree-validation.js'; import { list } from '../../../utils/string.js'; -import { regex_whitespace } from '../../patterns.js'; const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/; const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i; @@ -297,7 +296,7 @@ export default function element(parser) { element.tag = get_attribute_expression(definition); } - element.metadata.expression = create_expression_metadata(); + element.metadata.expression = new ExpressionMetadata(); } if (is_top_level_script_or_style) { @@ -508,7 +507,7 @@ function read_attribute(parser) { end: parser.index, expression, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }; @@ -528,7 +527,7 @@ function read_attribute(parser) { end: parser.index, expression, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }; @@ -568,7 +567,7 @@ function read_attribute(parser) { name }, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }; @@ -628,7 +627,7 @@ function read_attribute(parser) { modifiers: /** @type {Array<'important'>} */ (modifiers), value, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }; } @@ -658,7 +657,7 @@ function read_attribute(parser) { name: directive_name, expression, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }; @@ -824,7 +823,7 @@ function read_sequence(parser, done, location) { end: parser.index, expression, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }; diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index ba091ef7ec..4ff948e165 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -3,7 +3,7 @@ /** @import { Parser } from '../index.js' */ import { walk } from 'zimmerframe'; import * as e from '../../../errors.js'; -import { create_expression_metadata } from '../../nodes.js'; +import { ExpressionMetadata } from '../../nodes.js'; import { parse_expression_at } from '../acorn.js'; import read_pattern from '../read/context.js'; import read_expression, { get_loose_identifier } from '../read/expression.js'; @@ -42,7 +42,7 @@ export default function tag(parser) { end: parser.index, expression, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }); } @@ -65,7 +65,7 @@ function open(parser) { consequent: create_fragment(), alternate: null, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }); @@ -249,7 +249,7 @@ function open(parser) { then: null, catch: null, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }); @@ -334,7 +334,7 @@ function open(parser) { expression, fragment: create_fragment(), metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }); @@ -477,7 +477,7 @@ function next(parser) { consequent: create_fragment(), alternate: null, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }); @@ -643,7 +643,7 @@ function special(parser) { end: parser.index, expression, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }); @@ -721,7 +721,7 @@ function special(parser) { end: parser.index - 1 }, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } }); } @@ -748,7 +748,7 @@ function special(parser) { end: parser.index, expression: /** @type {AST.RenderTag['expression']} */ (expression), metadata: { - expression: create_expression_metadata(), + expression: new ExpressionMetadata(), dynamic: false, arguments: [], path: [], diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index bad6c7d613..9d24f9dbac 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -1,6 +1,7 @@ import type { Scope } from '../scope.js'; import type { ComponentAnalysis, ReactiveStatement } from '../types.js'; -import type { AST, ExpressionMetadata, StateField, ValidatedCompileOptions } from '#compiler'; +import type { AST, StateField, ValidatedCompileOptions } from '#compiler'; +import type { ExpressionMetadata } from '../nodes.js'; export interface AnalysisState { scope: Scope; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 4b66abe1d1..52eba8c735 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -7,7 +7,7 @@ import { get_parent } from '../../../utils/ast.js'; import { is_pure, is_safe_identifier } from './shared/utils.js'; import { dev, locate_node, source } from '../../../state.js'; import * as b from '#compiler/builders'; -import { create_expression_metadata } from '../../nodes.js'; +import { ExpressionMetadata } from '../../nodes.js'; /** * @param {CallExpression} node @@ -243,7 +243,7 @@ export function CallExpression(node, context) { // `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning if (rune === '$derived') { - const expression = create_expression_metadata(); + const expression = new ExpressionMetadata(); context.next({ ...context.state, diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js index 1230ef6b04..d0c994b7a4 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js @@ -5,7 +5,7 @@ import * as e from '../../../errors.js'; import { validate_opening_tag } from './shared/utils.js'; import { mark_subtree_dynamic } from './shared/fragment.js'; import { is_resolved_snippet } from './shared/snippets.js'; -import { create_expression_metadata } from '../../nodes.js'; +import { ExpressionMetadata } from '../../nodes.js'; /** * @param {AST.RenderTag} node @@ -57,7 +57,7 @@ export function RenderTag(node, context) { context.visit(callee, { ...context.state, expression: node.metadata.expression }); for (const arg of expression.arguments) { - const metadata = create_expression_metadata(); + const metadata = new ExpressionMetadata(); node.metadata.arguments.push(metadata); context.visit(arg, { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 4b32dab82a..dd390c99da 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -1,5 +1,5 @@ /** @import { Expression, Identifier, ObjectExpression } from 'estree' */ -/** @import { AST, ExpressionMetadata } from '#compiler' */ +/** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../../types' */ import { escape_html } from '../../../../../../escaping.js'; import { normalize_attribute } from '../../../../../../utils.js'; @@ -8,6 +8,7 @@ import { is_event_attribute } from '../../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js'; import { build_expression, build_template_chunk, Memoizer } from './utils.js'; +import { ExpressionMetadata } from '../../../../nodes.js'; /** * @param {Array} attributes diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js index d4d6721960..3ab1506eb3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js @@ -1,9 +1,10 @@ /** @import { Expression } from 'estree' */ -/** @import { AST, ExpressionMetadata } from '#compiler' */ +/** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../../types' */ import { is_capture_event, is_passive_event } from '../../../../../../utils.js'; import { dev, locator } from '../../../../../state.js'; import * as b from '#compiler/builders'; +import { ExpressionMetadata } from '../../../../nodes.js'; /** * @param {AST.Attribute} node diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index a42063b2e2..46d2f2b777 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -1,5 +1,5 @@ /** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, ExpressionStatement } from 'estree' */ -/** @import { AST, ExpressionMetadata } from '#compiler' */ +/** @import { AST } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */ import { walk } from 'zimmerframe'; import { object } from '../../../../../utils/ast.js'; @@ -9,6 +9,7 @@ import { regex_is_valid_identifier } from '../../../../patterns.js'; import is_reference from 'is-reference'; import { dev, is_ignored, locator, component_name } from '../../../../../state.js'; import { build_getter } from '../../utils.js'; +import { ExpressionMetadata } from '../../../../nodes.js'; /** * A utility for extracting complex expressions (such as call expressions) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js index cfb87b0ce7..b7607f3fb8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js @@ -1,13 +1,9 @@ /** @import { ArrayExpression, Expression, Literal, ObjectExpression } from 'estree' */ -/** @import { AST, ExpressionMetadata } from '#compiler' */ +/** @import { AST } from '#compiler' */ /** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */ import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js'; import { binding_properties } from '../../../../bindings.js'; -import { - create_attribute, - create_expression_metadata, - is_custom_element_node -} from '../../../../nodes.js'; +import { create_attribute, ExpressionMetadata, is_custom_element_node } from '../../../../nodes.js'; import { regex_starts_with_newline } from '../../../../patterns.js'; import * as b from '#compiler/builders'; import { @@ -160,7 +156,7 @@ export function build_element_attributes(node, context, transform) { build_attribute_value(value_attribute.value, context, transform) ), metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } } ]) @@ -174,7 +170,7 @@ export function build_element_attributes(node, context, transform) { end: -1, expression, metadata: { - expression: create_expression_metadata() + expression: new ExpressionMetadata() } } ]) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index 92653ed73c..09a854670c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -1,5 +1,5 @@ -/** @import { AssignmentOperator, Expression, Identifier, Node, Statement, BlockStatement } from 'estree' */ -/** @import { AST, ExpressionMetadata } from '#compiler' */ +/** @import { Expression, Identifier, Node, Statement, BlockStatement } from 'estree' */ +/** @import { AST } from '#compiler' */ /** @import { ComponentContext, ServerTransformState } from '../../types.js' */ import { escape_html } from '../../../../../../escaping.js'; @@ -13,6 +13,7 @@ import * as b from '#compiler/builders'; import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js'; import { regex_whitespaces_strict } from '../../../../patterns.js'; import { has_await_expression } from '../../../../../utils/ast.js'; +import { ExpressionMetadata } from '../../../../nodes.js'; /** Opens an if/each block, so that we can remove nodes in the case of a mismatch */ export const block_open = b.literal(BLOCK_OPEN); diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index 13188681d2..bca9c29a65 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -1,5 +1,5 @@ /** @import { Expression, PrivateIdentifier } from 'estree' */ -/** @import { AST, ExpressionMetadata } from '#compiler' */ +/** @import { AST, Binding } from '#compiler' */ /** * All nodes that can appear elsewhere than the top level, have attributes and can contain children @@ -64,20 +64,33 @@ export function create_attribute(name, start, end, value) { } }; } +export class ExpressionMetadata { + /** True if the expression references state directly, or _might_ (via member/call expressions) */ + has_state = false; -/** - * @returns {ExpressionMetadata} - */ -export function create_expression_metadata() { - return { - dependencies: new Set(), - references: new Set(), - has_state: false, - has_call: false, - has_member_expression: false, - has_assignment: false, - has_await: false - }; + /** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */ + has_call = false; + + /** True if the expression contains `await` */ + has_await = false; + + /** True if the expression includes a member expression */ + has_member_expression = false; + + /** True if the expression includes an assignment or an update */ + has_assignment = false; + + /** + * All the bindings that are referenced eagerly (not inside functions) in this expression + * @type {Set} + */ + dependencies = new Set(); + + /** + * True if the expression references state directly, or _might_ (via member/call expressions) + * @type {Set} + */ + references = new Set(); } /** diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index f7d3dac0f7..7dbdf47967 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -3,7 +3,7 @@ /** @import { AST, BindingKind, DeclarationKind } from '#compiler' */ import is_reference from 'is-reference'; import { walk } from 'zimmerframe'; -import { create_expression_metadata } from './nodes.js'; +import { ExpressionMetadata } from './nodes.js'; import * as b from '#compiler/builders'; import * as e from '../errors.js'; import { @@ -1201,7 +1201,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { if (node.fallback) visit(node.fallback, { scope }); node.metadata = { - expression: create_expression_metadata(), + expression: new ExpressionMetadata(), keyed: false, contains_group_binding: false, index: scope.root.unique('$$index'), diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 9bd4b91d58..fce3f62c5c 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -294,23 +294,6 @@ export type DeclarationKind = | 'using' | 'await using'; -export interface ExpressionMetadata { - /** All the bindings that are referenced eagerly (not inside functions) in this expression */ - dependencies: Set; - /** All the bindings that are referenced inside this expression, including inside functions */ - references: Set; - /** True if the expression references state directly, or _might_ (via member/call expressions) */ - has_state: boolean; - /** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */ - has_call: boolean; - /** True if the expression contains `await` */ - has_await: boolean; - /** True if the expression includes a member expression */ - has_member_expression: boolean; - /** True if the expression includes an assignment or an update */ - has_assignment: boolean; -} - export interface StateField { type: StateCreationRuneName; node: PropertyDefinition | AssignmentExpression; diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index f38706d075..fa7484e523 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -1,4 +1,4 @@ -import type { Binding, ExpressionMetadata } from '#compiler'; +import type { Binding } from '#compiler'; import type { ArrayExpression, ArrowFunctionExpression, @@ -17,6 +17,7 @@ import type { } from 'estree'; import type { Scope } from '../phases/scope'; import type { _CSS } from './css'; +import type { ExpressionMetadata } from '../phases/nodes'; /** * - `html` — the default, for e.g. `
` or `` From 9096680c2eef27b7a682c2c71f302a12e5e6991b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 20:41:13 -0400 Subject: [PATCH 39/58] chore: use ESTree namespace imports (#17040) --- .../src/compiler/phases/2-analyze/index.js | 12 +++++----- .../3-transform/server/transform-server.js | 24 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index b4c704c34d..4c05fd6148 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -1,4 +1,4 @@ -/** @import { Expression, Node, Program } from 'estree' */ +/** @import * as ESTree from 'estree' */ /** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */ /** @import { AnalysisState, Visitors } from './types' */ /** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */ @@ -206,7 +206,7 @@ const visitors = { * @returns {Js} */ function js(script, root, allow_reactive_declarations, parent) { - /** @type {Program} */ + /** @type {ESTree.Program} */ const ast = script?.content ?? { type: 'Program', sourceType: 'module', @@ -289,7 +289,7 @@ export function analyze_module(source, options) { }); walk( - /** @type {Node} */ (ast), + /** @type {ESTree.Node} */ (ast), { scope, scopes, @@ -347,7 +347,7 @@ export function analyze_component(root, source, options) { const store_name = name.slice(1); const declaration = instance.scope.get(store_name); - const init = /** @type {Node | undefined} */ (declaration?.initial); + const init = /** @type {ESTree.Node | undefined} */ (declaration?.initial); // If we're not in legacy mode through the compiler option, assume the user // is referencing a rune and not a global store. @@ -407,7 +407,7 @@ export function analyze_component(root, source, options) { /** @type {number} */ (node.start) > /** @type {number} */ (module.ast.start) && /** @type {number} */ (node.end) < /** @type {number} */ (module.ast.end) && // const state = $state(0) is valid - get_rune(/** @type {Node} */ (path.at(-1)), module.scope) === null + get_rune(/** @type {ESTree.Node} */ (path.at(-1)), module.scope) === null ) { e.store_invalid_subscription(node); } @@ -636,7 +636,7 @@ export function analyze_component(root, source, options) { // @ts-expect-error _: set_scope, Identifier(node, context) { - const parent = /** @type {Expression} */ (context.path.at(-1)); + const parent = /** @type {ESTree.Expression} */ (context.path.at(-1)); if (is_reference(node, parent)) { const binding = context.state.scope.get(node.name); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index b22b95f5aa..1c9764a759 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -1,4 +1,4 @@ -/** @import { Program, Property, Statement, VariableDeclarator } from 'estree' */ +/** @import * as ESTree from 'estree' */ /** @import { AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */ /** @import { ComponentServerTransformState, ComponentVisitors, ServerTransformState, Visitors } from './types.js' */ /** @import { Analysis, ComponentAnalysis } from '../../types.js' */ @@ -86,7 +86,7 @@ const template_visitors = { /** * @param {ComponentAnalysis} analysis * @param {ValidatedCompileOptions} options - * @returns {Program} + * @returns {ESTree.Program} */ export function server_component(analysis, options) { /** @type {ComponentServerTransformState} */ @@ -106,11 +106,11 @@ export function server_component(analysis, options) { skip_hydration_boundaries: false }; - const module = /** @type {Program} */ ( + const module = /** @type {ESTree.Program} */ ( walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, global_visitors) ); - const instance = /** @type {Program} */ ( + const instance = /** @type {ESTree.Program} */ ( walk( /** @type {AST.SvelteNode} */ (analysis.instance.ast), { ...state, scopes: analysis.instance.scopes }, @@ -131,7 +131,7 @@ export function server_component(analysis, options) { ) ); - const template = /** @type {Program} */ ( + const template = /** @type {ESTree.Program} */ ( walk( /** @type {AST.SvelteNode} */ (analysis.template.ast), { ...state, scopes: analysis.template.scopes }, @@ -140,7 +140,7 @@ export function server_component(analysis, options) { ) ); - /** @type {VariableDeclarator[]} */ + /** @type {ESTree.VariableDeclarator[]} */ const legacy_reactive_declarations = []; for (const [node] of analysis.reactive_statements) { @@ -192,7 +192,7 @@ export function server_component(analysis, options) { b.function_declaration( b.id('$$render_inner'), [b.id('$$renderer')], - b.block(/** @type {Statement[]} */ (rest)) + b.block(/** @type {ESTree.Statement[]} */ (rest)) ), b.do_while( b.unary('!', b.id('$$settled')), @@ -219,7 +219,7 @@ export function server_component(analysis, options) { // Propagate values of bound props upwards if they're undefined in the parent and have a value. // Don't do this as part of the props retrieval because people could eagerly mutate the prop in the instance script. - /** @type {Property[]} */ + /** @type {ESTree.Property[]} */ const props = []; for (const [name, binding] of analysis.instance.scope.declarations) { @@ -239,8 +239,8 @@ export function server_component(analysis, options) { } let component_block = b.block([ - .../** @type {Statement[]} */ (instance.body), - .../** @type {Statement[]} */ (template.body) + .../** @type {ESTree.Statement[]} */ (instance.body), + .../** @type {ESTree.Statement[]} */ (template.body) ]); if (analysis.instance.has_await) { @@ -395,7 +395,7 @@ export function server_component(analysis, options) { /** * @param {Analysis} analysis * @param {ValidatedModuleCompileOptions} options - * @returns {Program} + * @returns {ESTree.Program} */ export function server_module(analysis, options) { /** @type {ServerTransformState} */ @@ -411,7 +411,7 @@ export function server_module(analysis, options) { state_fields: new Map() }; - const module = /** @type {Program} */ ( + const module = /** @type {ESTree.Program} */ ( walk(/** @type {AST.SvelteNode} */ (analysis.module.ast), state, global_visitors) ); From fc181cf6110519f693536611f30c2c3cc9618fb4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 20:54:01 -0400 Subject: [PATCH 40/58] chore: move `$effect` visitors (#17041) --- .../3-transform/client/visitors/CallExpression.js | 11 +++++++++++ .../client/visitors/ExpressionStatement.js | 10 ---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index ae60f3be40..c9e5f49b96 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -62,6 +62,17 @@ export function CallExpression(node, context) { is_ignored(node, 'state_snapshot_uncloneable') && b.true ); + case '$effect': + case '$effect.pre': { + const callee = rune === '$effect' ? '$.user_effect' : '$.user_pre_effect'; + const func = /** @type {Expression} */ (context.visit(node.arguments[0])); + + const expr = b.call(callee, /** @type {Expression} */ (func)); + expr.callee.loc = node.callee.loc; // ensure correct mapping + + return expr; + } + case '$effect.root': return b.call( '$.effect_root', diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js index 859842ebc3..96a378747f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js @@ -11,16 +11,6 @@ export function ExpressionStatement(node, context) { if (node.expression.type === 'CallExpression') { const rune = get_rune(node.expression, context.state.scope); - if (rune === '$effect' || rune === '$effect.pre') { - const callee = rune === '$effect' ? '$.user_effect' : '$.user_pre_effect'; - const func = /** @type {Expression} */ (context.visit(node.expression.arguments[0])); - - const expr = b.call(callee, /** @type {Expression} */ (func)); - expr.callee.loc = node.expression.callee.loc; // ensure correct mapping - - return b.stmt(expr); - } - if (rune === '$inspect.trace') { return b.empty; } From b5e23a6d13d2dfceb2826fe64ea83bf2cd8253fb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Oct 2025 21:04:13 -0400 Subject: [PATCH 41/58] chore: tweak memoizer logic (#17042) --- .changeset/slimy-shirts-lose.md | 5 ++ .../client/visitors/RegularElement.js | 46 +++++++++++-------- .../3-transform/client/visitors/RenderTag.js | 2 +- .../client/visitors/SlotElement.js | 2 +- .../client/visitors/shared/component.js | 6 +-- .../client/visitors/shared/element.js | 14 ++---- .../client/visitors/shared/utils.js | 18 ++++++-- 7 files changed, 55 insertions(+), 38 deletions(-) create mode 100644 .changeset/slimy-shirts-lose.md diff --git a/.changeset/slimy-shirts-lose.md b/.changeset/slimy-shirts-lose.md new file mode 100644 index 0000000000..084fb07ea7 --- /dev/null +++ b/.changeset/slimy-shirts-lose.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: tweak memoizer logic diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index ab119e8f80..3998770a71 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -11,7 +11,7 @@ import { import { is_ignored } from '../../../../state.js'; import { is_event_attribute, is_text_attribute } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; -import { create_attribute, is_custom_element_node } from '../../../nodes.js'; +import { create_attribute, ExpressionMetadata, is_custom_element_node } from '../../../nodes.js'; import { clean_nodes, determine_namespace_for_children } from '../../utils.js'; import { build_getter } from '../utils.js'; import { @@ -267,10 +267,7 @@ export function RegularElement(node, context) { const { value, has_state } = build_attribute_value( attribute.value, context, - (value, metadata) => - metadata.has_call || metadata.has_await - ? context.state.memoizer.add(value, metadata.has_await) - : value + (value, metadata) => context.state.memoizer.add(value, metadata) ); const update = build_element_attribute_update(node, node_id, name, value, attributes); @@ -487,11 +484,25 @@ function setup_select_synchronization(value_binding, context) { ); } +/** + * @param {ExpressionMetadata} target + * @param {ExpressionMetadata} source + */ +function merge_metadata(target, source) { + target.has_assignment ||= source.has_assignment; + target.has_await ||= source.has_await; + target.has_call ||= source.has_call; + target.has_member_expression ||= source.has_member_expression; + target.has_state ||= source.has_state; + + for (const r of source.references) target.references.add(r); + for (const b of source.dependencies) target.dependencies.add(b); +} + /** * @param {AST.ClassDirective[]} class_directives * @param {ComponentContext} context * @param {Memoizer} memoizer - * @return {ObjectExpression | Identifier} */ export function build_class_directives_object( class_directives, @@ -499,26 +510,25 @@ export function build_class_directives_object( memoizer = context.state.memoizer ) { let properties = []; - let has_call_or_state = false; - let has_await = false; + + const metadata = new ExpressionMetadata(); for (const d of class_directives) { + merge_metadata(metadata, d.metadata.expression); + const expression = /** @type Expression */ (context.visit(d.expression)); properties.push(b.init(d.name, expression)); - has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state; - has_await ||= d.metadata.expression.has_await; } const directives = b.object(properties); - return has_call_or_state || has_await ? memoizer.add(directives, has_await) : directives; + return memoizer.add(directives, metadata); } /** * @param {AST.StyleDirective[]} style_directives * @param {ComponentContext} context * @param {Memoizer} memoizer - * @return {ObjectExpression | ArrayExpression | Identifier}} */ export function build_style_directives_object( style_directives, @@ -528,10 +538,11 @@ export function build_style_directives_object( const normal = b.object([]); const important = b.object([]); - let has_call_or_state = false; - let has_await = false; + const metadata = new ExpressionMetadata(); for (const d of style_directives) { + merge_metadata(metadata, d.metadata.expression); + const expression = d.value === true ? build_getter(b.id(d.name), context.state) @@ -539,14 +550,11 @@ export function build_style_directives_object( const object = d.modifiers.includes('important') ? important : normal; object.properties.push(b.init(d.name, expression)); - - has_call_or_state ||= d.metadata.expression.has_call || d.metadata.expression.has_state; - has_await ||= d.metadata.expression.has_await; } const directives = important.properties.length ? b.array([normal, important]) : normal; - return has_call_or_state || has_await ? memoizer.add(directives, has_await) : directives; + return memoizer.add(directives, metadata); } /** @@ -675,7 +683,7 @@ function build_element_special_value_attribute( element === 'select' && attribute.value !== true && !is_text_attribute(attribute); const { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata.has_await) : value + state.memoizer.add(value, metadata) ); const evaluated = context.state.scope.evaluate(value); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js index b7a6e65557..b3619e8669 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js @@ -26,7 +26,7 @@ export function RenderTag(node, context) { let expression = build_expression(context, arg, metadata); if (metadata.has_await || metadata.has_call) { - expression = b.call('$.get', memoizer.add(expression, metadata.has_await)); + expression = b.call('$.get', memoizer.add(expression, metadata)); } args.push(b.thunk(expression)); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js index b87a13253b..f6db21212b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js @@ -35,7 +35,7 @@ export function SlotElement(node, context) { context, (value, metadata) => metadata.has_call || metadata.has_await - ? b.call('$.get', memoizer.add(value, metadata.has_await)) + ? b.call('$.get', memoizer.add(value, metadata)) : value ); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 5ca941fd70..688191fd20 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -134,7 +134,7 @@ export function build_component(node, component_name, context) { props_and_spreads.push( b.thunk( attribute.metadata.expression.has_await || attribute.metadata.expression.has_call - ? b.call('$.get', memoizer.add(expression, attribute.metadata.expression.has_await)) + ? b.call('$.get', memoizer.add(expression, attribute.metadata.expression)) : expression ) ); @@ -149,7 +149,7 @@ export function build_component(node, component_name, context) { build_attribute_value(attribute.value, context, (value, metadata) => { // TODO put the derived in the local block return metadata.has_call || metadata.has_await - ? b.call('$.get', memoizer.add(value, metadata.has_await)) + ? b.call('$.get', memoizer.add(value, metadata)) : value; }).value ) @@ -185,7 +185,7 @@ export function build_component(node, component_name, context) { }); return should_wrap_in_derived - ? b.call('$.get', memoizer.add(value, metadata.has_await)) + ? b.call('$.get', memoizer.add(value, metadata, true)) : value; } ); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index dd390c99da..29baf2cad5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -36,7 +36,7 @@ export function build_attribute_effect( for (const attribute of attributes) { if (attribute.type === 'Attribute') { const { value } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call || metadata.has_await ? memoizer.add(value, metadata.has_await) : value + memoizer.add(value, metadata) ); if ( @@ -53,9 +53,7 @@ export function build_attribute_effect( } else { let value = /** @type {Expression} */ (context.visit(attribute)); - if (attribute.metadata.expression.has_call || attribute.metadata.expression.has_await) { - value = memoizer.add(value, attribute.metadata.expression.has_await); - } + value = memoizer.add(value, attribute.metadata.expression); values.push(b.spread(value)); } @@ -156,9 +154,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c value = b.call('$.clsx', value); } - return metadata.has_call || metadata.has_await - ? context.state.memoizer.add(value, metadata.has_await) - : value; + return context.state.memoizer.add(value, metadata); }); /** @type {Identifier | undefined} */ @@ -167,7 +163,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c /** @type {ObjectExpression | Identifier | undefined} */ let prev; - /** @type {ObjectExpression | Identifier | undefined} */ + /** @type {Expression | undefined} */ let next; if (class_directives.length) { @@ -228,7 +224,7 @@ export function build_set_class(element, node_id, attribute, class_directives, c */ export function build_set_style(node_id, attribute, style_directives, context) { let { value, has_state } = build_attribute_value(attribute.value, context, (value, metadata) => - metadata.has_call ? context.state.memoizer.add(value, metadata.has_await) : value + context.state.memoizer.add(value, metadata) ); /** @type {Identifier | undefined} */ diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index 46d2f2b777..691b78199e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -24,12 +24,21 @@ export class Memoizer { /** * @param {Expression} expression - * @param {boolean} has_await + * @param {ExpressionMetadata} metadata + * @param {boolean} memoize_if_state */ - add(expression, has_await) { + add(expression, metadata, memoize_if_state = false) { + const should_memoize = + metadata.has_call || metadata.has_await || (memoize_if_state && metadata.has_state); + + if (!should_memoize) { + // no memoization required + return expression; + } + const id = b.id('#'); // filled in later - (has_await ? this.#async : this.#sync).push({ id, expression }); + (metadata.has_await ? this.#async : this.#sync).push({ id, expression }); return id; } @@ -73,8 +82,7 @@ export function build_template_chunk( values, context, state = context.state, - memoize = (value, metadata) => - metadata.has_call || metadata.has_await ? state.memoizer.add(value, metadata.has_await) : value + memoize = (value, metadata) => state.memoizer.add(value, metadata) ) { /** @type {Expression[]} */ const expressions = []; From da00abe1162a8e56455e92b79020c4e33290e10e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 25 Oct 2025 21:13:05 -0400 Subject: [PATCH 42/58] Version Packages (#17037) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/slick-teeth-exist.md | 5 ----- .changeset/slimy-shirts-lose.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/slick-teeth-exist.md delete mode 100644 .changeset/slimy-shirts-lose.md diff --git a/.changeset/slick-teeth-exist.md b/.changeset/slick-teeth-exist.md deleted file mode 100644 index aeeb0f41b9..0000000000 --- a/.changeset/slick-teeth-exist.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: better error message for global variable assignments diff --git a/.changeset/slimy-shirts-lose.md b/.changeset/slimy-shirts-lose.md deleted file mode 100644 index 084fb07ea7..0000000000 --- a/.changeset/slimy-shirts-lose.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: tweak memoizer logic diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 51e6317491..1d58806694 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.42.2 + +### Patch Changes + +- fix: better error message for global variable assignments ([#17036](https://github.com/sveltejs/svelte/pull/17036)) + +- chore: tweak memoizer logic ([#17042](https://github.com/sveltejs/svelte/pull/17042)) + ## 5.42.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 1f8746e72d..9fb7189d07 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.42.1", + "version": "5.42.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 8d50b983ce..605e1d9cdc 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.42.1'; +export const VERSION = '5.42.2'; export const PUBLIC_VERSION = '5'; From cc0143c904ec48dcce1eac2600b5a88ca5df0d17 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Oct 2025 02:39:43 +0100 Subject: [PATCH 43/58] fix: handle `` rendered asynchronously (#17052) * fix: handle `` rendered asynchronously * fix tests --- .changeset/khaki-emus-rest.md | 5 ++++ .../3-transform/client/visitors/SvelteHead.js | 3 ++ .../3-transform/server/visitors/SvelteHead.js | 11 ++++++- .../internal/client/dom/blocks/svelte-head.js | 29 +++++++------------ packages/svelte/src/internal/client/render.js | 10 +------ packages/svelte/src/internal/server/index.js | 7 +++-- packages/svelte/tests/hydration/test.ts | 6 +++- .../runtime-runes/samples/async-head/A.svelte | 7 +++++ .../runtime-runes/samples/async-head/B.svelte | 8 +++++ .../samples/async-head/_config.js | 23 +++++++++++++++ .../samples/async-head/main.svelte | 11 +++++++ 11 files changed, 87 insertions(+), 33 deletions(-) create mode 100644 .changeset/khaki-emus-rest.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head/A.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head/B.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head/main.svelte diff --git a/.changeset/khaki-emus-rest.md b/.changeset/khaki-emus-rest.md new file mode 100644 index 0000000000..5364ff60df --- /dev/null +++ b/.changeset/khaki-emus-rest.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: handle `` rendered asynchronously diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js index 0701c37c48..3a45389dd7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js @@ -2,6 +2,8 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; +import { hash } from '../../../../../utils.js'; +import { filename } from '../../../../state.js'; /** * @param {AST.SvelteHead} node @@ -13,6 +15,7 @@ export function SvelteHead(node, context) { b.stmt( b.call( '$.head', + b.literal(hash(filename)), b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fragment))) ) ) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js index a519057cb6..177ec62416 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js @@ -2,6 +2,8 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types.js' */ import * as b from '#compiler/builders'; +import { hash } from '../../../../../utils.js'; +import { filename } from '../../../../state.js'; /** * @param {AST.SvelteHead} node @@ -11,6 +13,13 @@ export function SvelteHead(node, context) { const block = /** @type {BlockStatement} */ (context.visit(node.fragment)); context.state.template.push( - b.stmt(b.call('$.head', b.id('$$renderer'), b.arrow([b.id('$$renderer')], block))) + b.stmt( + b.call( + '$.head', + b.literal(hash(filename)), + b.id('$$renderer'), + b.arrow([b.id('$$renderer')], block) + ) + ) ); } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js index 66d3371836..13926ccc4b 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -3,22 +3,13 @@ import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hyd import { create_text, get_first_child, get_next_sibling } from '../operations.js'; import { block } from '../../reactivity/effects.js'; import { COMMENT_NODE, HEAD_EFFECT } from '#client/constants'; -import { HYDRATION_START } from '../../../../constants.js'; - -/** - * @type {Node | undefined} - */ -let head_anchor; - -export function reset_head_anchor() { - head_anchor = undefined; -} /** + * @param {string} hash * @param {(anchor: Node) => void} render_fn * @returns {void} */ -export function head(render_fn) { +export function head(hash, render_fn) { // The head function may be called after the first hydration pass and ssr comment nodes may still be present, // therefore we need to skip that when we detect that we're not in hydration mode. let previous_hydrate_node = null; @@ -30,15 +21,13 @@ export function head(render_fn) { if (hydrating) { previous_hydrate_node = hydrate_node; - // There might be multiple head blocks in our app, so we need to account for each one needing independent hydration. - if (head_anchor === undefined) { - head_anchor = /** @type {TemplateNode} */ (get_first_child(document.head)); - } + var head_anchor = /** @type {TemplateNode} */ (get_first_child(document.head)); + // There might be multiple head blocks in our app, and they could have been + // rendered in an arbitrary order — find one corresponding to this component while ( head_anchor !== null && - (head_anchor.nodeType !== COMMENT_NODE || - /** @type {Comment} */ (head_anchor).data !== HYDRATION_START) + (head_anchor.nodeType !== COMMENT_NODE || /** @type {Comment} */ (head_anchor).data !== hash) ) { head_anchor = /** @type {TemplateNode} */ (get_next_sibling(head_anchor)); } @@ -48,7 +37,10 @@ export function head(render_fn) { if (head_anchor === null) { set_hydrating(false); } else { - head_anchor = set_hydrate_node(/** @type {TemplateNode} */ (get_next_sibling(head_anchor))); + var start = /** @type {TemplateNode} */ (get_next_sibling(head_anchor)); + head_anchor.remove(); // in case this component is repeated + + set_hydrate_node(start); } } @@ -61,7 +53,6 @@ export function head(render_fn) { } finally { if (was_hydrating) { set_hydrating(true); - head_anchor = hydrate_node; // so that next head block starts from the correct node set_hydrate_node(/** @type {TemplateNode} */ (previous_hydrate_node)); } } diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index b1165a6e7a..416627a157 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -12,20 +12,13 @@ import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants import { active_effect } from './runtime.js'; import { push, pop, component_context } from './context.js'; import { component_root } from './reactivity/effects.js'; -import { - hydrate_next, - hydrate_node, - hydrating, - set_hydrate_node, - set_hydrating -} from './dom/hydration.js'; +import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from './dom/hydration.js'; import { array_from } from '../shared/utils.js'; import { all_registered_events, handle_event_propagation, root_event_handles } from './dom/elements/events.js'; -import { reset_head_anchor } from './dom/blocks/svelte-head.js'; import * as w from './warnings.js'; import * as e from './errors.js'; import { assign_nodes } from './dom/template.js'; @@ -152,7 +145,6 @@ export function hydrate(component, options) { } finally { set_hydrating(was_hydrating); set_hydrate_node(previous_hydrate_node); - reset_head_anchor(); } } diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 74a90a8600..c0dbdbda14 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -64,15 +64,16 @@ export function render(component, options = {}) { } /** + * @param {string} hash * @param {Renderer} renderer * @param {(renderer: Renderer) => Promise | void} fn * @returns {void} */ -export function head(renderer, fn) { +export function head(hash, renderer, fn) { renderer.head((renderer) => { - renderer.push(BLOCK_OPEN); + renderer.push(``); renderer.child(fn); - renderer.push(BLOCK_CLOSE); + renderer.push(EMPTY_COMMENT); }); } diff --git a/packages/svelte/tests/hydration/test.ts b/packages/svelte/tests/hydration/test.ts index 70d5c5d072..ba13d2c611 100644 --- a/packages/svelte/tests/hydration/test.ts +++ b/packages/svelte/tests/hydration/test.ts @@ -132,7 +132,11 @@ const { test, run } = suite(async (config, cwd) => { flushSync(); const normalize = (string: string) => - string.trim().replaceAll('\r\n', '\n').replaceAll('/>', '>'); + string + .trim() + .replaceAll('\r\n', '\n') + .replaceAll('/>', '>') + .replace(//g, ''); const expected = read(`${cwd}/_expected.html`) ?? rendered.html; assert.equal(normalize(target.innerHTML), normalize(expected)); diff --git a/packages/svelte/tests/runtime-runes/samples/async-head/A.svelte b/packages/svelte/tests/runtime-runes/samples/async-head/A.svelte new file mode 100644 index 0000000000..d821bb6fa0 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head/A.svelte @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-head/B.svelte b/packages/svelte/tests/runtime-runes/samples/async-head/B.svelte new file mode 100644 index 0000000000..d725d5f03b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head/B.svelte @@ -0,0 +1,8 @@ + + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-head/_config.js b/packages/svelte/tests/runtime-runes/samples/async-head/_config.js new file mode 100644 index 0000000000..6fdf41b434 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head/_config.js @@ -0,0 +1,23 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, window }) { + await tick(); + + const head = window.document.head; + + // we don't care about the order, but we want to ensure that the + // elements didn't clobber each other + for (let n of ['1', '2', '3']) { + const a = head.querySelector(`meta[name="a-${n}"]`); + assert.equal(a?.getAttribute('content'), n); + + const b1 = head.querySelector(`meta[name="b-${n}-1"]`); + assert.equal(b1?.getAttribute('content'), `${n}-1`); + + const b2 = head.querySelector(`meta[name="b-${n}-2"]`); + assert.equal(b2?.getAttribute('content'), `${n}-2`); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-head/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-head/main.svelte new file mode 100644 index 0000000000..7f23489373 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head/main.svelte @@ -0,0 +1,11 @@ + + + + + + + + From d2f453f8b099ee46eb5835fc2af2952bda0e2fe6 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 28 Oct 2025 02:40:13 +0100 Subject: [PATCH 44/58] fix: don't restore batch in `#await` (#17051) #16977 had one slight regression which might contribute to #16990: The batch from earlier was restored, but that doesn't make sense in this situations since this has nothing to do with our new async logic of batches suspending until pending work is done. As a result you could end up with a batch being created, and then the restore then instead reverting to an earlier batch that was already done, which means a ghost-batch ends up in the set of batches, subsequently triggering time traveling when it shouldn't. This may help with #16990 No test because basically impossible to do so --- .changeset/shaky-jars-cut.md | 5 +++++ packages/svelte/src/internal/client/dom/blocks/await.js | 8 ++++++-- packages/svelte/src/internal/client/reactivity/async.js | 5 ++--- 3 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 .changeset/shaky-jars-cut.md diff --git a/.changeset/shaky-jars-cut.md b/.changeset/shaky-jars-cut.md new file mode 100644 index 0000000000..b74b00fa1c --- /dev/null +++ b/.changeset/shaky-jars-cut.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't restore batch in `#await` diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index bac01e4c33..87d64df23e 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -12,7 +12,7 @@ import { import { queue_micro_task } from '../task.js'; import { HYDRATION_START_ELSE, UNINITIALIZED } from '../../../../constants.js'; import { is_runes } from '../../context.js'; -import { flushSync, is_flushing_sync } from '../../reactivity/batch.js'; +import { Batch, flushSync, is_flushing_sync } from '../../reactivity/batch.js'; import { BranchManager } from './branches.js'; import { capture, unset_context } from '../../reactivity/async.js'; @@ -69,7 +69,11 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { if (destroyed) return; resolved = true; - restore(); + // We don't want to restore the previous batch here; {#await} blocks don't follow the async logic + // we have elsewhere, instead pending/resolve/fail states are each their own batch so to speak. + restore(false); + // Make sure we have a batch, since the branch manager expects one to exist + Batch.ensure(); if (hydrating) { // `restore()` could set `hydrating` to `true`, which we very much diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index fb836df989..bdd7eed940 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -33,7 +33,6 @@ import { set_hydrating, skip_nodes } from '../dom/hydration.js'; -import { create_text } from '../dom/operations.js'; /** * @@ -102,11 +101,11 @@ export function capture() { var previous_dev_stack = dev_stack; } - return function restore() { + return function restore(activate_batch = true) { set_active_effect(previous_effect); set_active_reaction(previous_reaction); set_component_context(previous_component_context); - previous_batch?.activate(); + if (activate_batch) previous_batch?.activate(); if (was_hydrating) { set_hydrating(true); From 1b2f7b068e01f0407f4013a292ec9f3f3381233e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 02:43:59 +0100 Subject: [PATCH 45/58] Version Packages (#17053) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/khaki-emus-rest.md | 5 ----- .changeset/shaky-jars-cut.md | 5 ----- packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) delete mode 100644 .changeset/khaki-emus-rest.md delete mode 100644 .changeset/shaky-jars-cut.md diff --git a/.changeset/khaki-emus-rest.md b/.changeset/khaki-emus-rest.md deleted file mode 100644 index 5364ff60df..0000000000 --- a/.changeset/khaki-emus-rest.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: handle `` rendered asynchronously diff --git a/.changeset/shaky-jars-cut.md b/.changeset/shaky-jars-cut.md deleted file mode 100644 index b74b00fa1c..0000000000 --- a/.changeset/shaky-jars-cut.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: don't restore batch in `#await` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 1d58806694..8dabe54b33 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.42.3 + +### Patch Changes + +- fix: handle `` rendered asynchronously ([#17052](https://github.com/sveltejs/svelte/pull/17052)) + +- fix: don't restore batch in `#await` ([#17051](https://github.com/sveltejs/svelte/pull/17051)) + ## 5.42.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 9fb7189d07..1ee6c50121 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.42.2", + "version": "5.42.3", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 605e1d9cdc..999aacc998 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.42.2'; +export const VERSION = '5.42.3'; export const PUBLIC_VERSION = '5'; From 90a8a039889ffdf0566dff6b2aa1b55d6a176b98 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Oct 2025 10:48:35 +0100 Subject: [PATCH 46/58] fix: settle batch after DOM updates (#17054) --- .changeset/loud-chairs-tease.md | 5 ++++ .../src/internal/client/reactivity/batch.js | 4 +-- .../async-settled-after-dom/_config.js | 27 +++++++++++++++++++ .../async-settled-after-dom/main.svelte | 20 ++++++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 .changeset/loud-chairs-tease.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/main.svelte diff --git a/.changeset/loud-chairs-tease.md b/.changeset/loud-chairs-tease.md new file mode 100644 index 0000000000..e08d111cdd --- /dev/null +++ b/.changeset/loud-chairs-tease.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: settle batch after DOM updates diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index ab83050cd0..bbc05bb1ff 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -196,6 +196,8 @@ export class Batch { flush_queued_effects(target.effects); previous_batch = null; + + this.#deferred?.resolve(); } batch_values = null; @@ -432,8 +434,6 @@ export class Batch { this.committed = true; batches.delete(this); - - this.#deferred?.resolve(); } /** diff --git a/packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/_config.js b/packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/_config.js new file mode 100644 index 0000000000..2c9816f3ed --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/_config.js @@ -0,0 +1,27 @@ +import { settled, tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + + async test({ assert, target }) { + const [shift, update] = target.querySelectorAll('button'); + + shift.click(); + await tick(); + + assert.htmlEqual(target.innerHTML, '

hello

'); + + update.click(); + const promise = settled(); + + await tick(); + shift.click(); + await promise; + + assert.htmlEqual( + target.innerHTML, + '

goodbye

' + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/main.svelte new file mode 100644 index 0000000000..0db9f80118 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-settled-after-dom/main.svelte @@ -0,0 +1,20 @@ + + + + + + +

{await push(text)}

+ + {#snippet pending()}{/snippet} +
From 1126ef318650792dcca883eeb72dbe58633b37cc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 28 Oct 2025 17:50:29 +0100 Subject: [PATCH 47/58] feat: out of order rendering (#17038) * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * revert * note to self * unused * WIP * WIP * WIP * WIP * WIP * WIP * WIP * WIP * deprecate * update tests * lint * lint * WIP * WIP * fix * WIP * unused * deopt to ensure state is ready * fix * DRY * reduce diff * reduce diff * reduce diff * handle blocked attributes * WIP * pre-transform * tidy up * fix * WIP * WIP * fix: handle `` rendered asynchronously * fix tests * fix * delay resolve * Revert "fix" This reverts commit 2e56cd75753abc1fdfabbada9d27829d9eaa496a. * add error * simplify/fix hydration restoration * fix * use $state.eager mechanism for $effect.pending - way simpler and more robust * disable these warnings for now, too many false positives * fix * changeset was already merged * changeset * oops * lint * docs + tidy * prettier * robustify: logic inside memoizer and outside could get out of sync, introducing bugs. use equality comparison instead * oops * uncomment * use finally * use is_async --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> Co-authored-by: Simon Holthausen --- .changeset/bitter-rings-help.md | 5 + .../src/compiler/phases/2-analyze/index.js | 203 +++++++++++++++++- .../2-analyze/visitors/AwaitExpression.js | 12 +- .../2-analyze/visitors/BindDirective.js | 1 + .../phases/2-analyze/visitors/SnippetBlock.js | 7 +- .../3-transform/client/transform-client.js | 47 ++-- .../phases/3-transform/client/types.d.ts | 3 - .../client/visitors/BindDirective.js | 25 ++- .../client/visitors/CallExpression.js | 4 +- .../3-transform/client/visitors/EachBlock.js | 10 +- .../client/visitors/ExpressionStatement.js | 2 +- .../3-transform/client/visitors/HtmlTag.js | 10 +- .../3-transform/client/visitors/IfBlock.js | 10 +- .../client/visitors/ImportDeclaration.js | 16 -- .../3-transform/client/visitors/KeyBlock.js | 9 +- .../3-transform/client/visitors/Program.js | 18 +- .../3-transform/client/visitors/RenderTag.js | 10 +- .../client/visitors/SlotElement.js | 4 +- .../client/visitors/SvelteElement.js | 9 +- .../client/visitors/VariableDeclaration.js | 15 +- .../client/visitors/shared/component.js | 36 ++-- .../client/visitors/shared/element.js | 1 + .../client/visitors/shared/utils.js | 16 +- .../3-transform/server/transform-server.js | 18 +- .../3-transform/server/visitors/AwaitBlock.js | 8 +- .../server/visitors/CallExpression.js | 9 +- .../3-transform/server/visitors/EachBlock.js | 17 +- .../3-transform/server/visitors/HtmlTag.js | 8 +- .../3-transform/server/visitors/IfBlock.js | 16 +- .../3-transform/server/visitors/KeyBlock.js | 8 +- .../3-transform/server/visitors/Program.js | 25 +++ .../server/visitors/RegularElement.js | 21 +- .../3-transform/server/visitors/RenderTag.js | 38 +++- .../server/visitors/SlotElement.js | 11 +- .../server/visitors/SvelteElement.js | 28 ++- .../server/visitors/shared/component.js | 14 +- .../server/visitors/shared/utils.js | 87 +++++++- .../3-transform/shared/transform-async.js | 102 +++++++++ .../compiler/phases/3-transform/types.d.ts | 3 + packages/svelte/src/compiler/phases/nodes.js | 24 +++ packages/svelte/src/compiler/phases/scope.js | 35 ++- .../svelte/src/compiler/phases/types.d.ts | 27 ++- .../svelte/src/compiler/types/template.d.ts | 2 + packages/svelte/src/compiler/utils/ast.js | 2 +- .../svelte/src/compiler/utils/builders.js | 19 +- .../src/internal/client/dom/blocks/async.js | 13 +- .../internal/client/dom/blocks/boundary.js | 13 +- .../client/dom/elements/attributes.js | 4 +- packages/svelte/src/internal/client/index.js | 1 + .../src/internal/client/reactivity/async.js | 151 +++++++++---- .../src/internal/client/reactivity/batch.js | 13 -- .../src/internal/client/reactivity/effects.js | 5 +- .../svelte/src/internal/client/runtime.js | 23 +- .../svelte/src/internal/server/renderer.js | 59 ++++- .../svelte/tests/runtime-legacy/shared.ts | 5 + .../async-if-after-await-in-script/_config.js | 16 ++ .../main.svelte | 10 + .../_config.js | 3 + .../samples/async-reactivity-loss/_config.js | 3 + .../_expected/client/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 61 +++--- .../_expected/server/index.svelte.js | 58 ++--- 69 files changed, 1097 insertions(+), 352 deletions(-) create mode 100644 .changeset/bitter-rings-help.md delete mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/ImportDeclaration.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-if-after-await-in-script/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-if-after-await-in-script/main.svelte diff --git a/.changeset/bitter-rings-help.md b/.changeset/bitter-rings-help.md new file mode 100644 index 0000000000..f71b9f96b2 --- /dev/null +++ b/.changeset/bitter-rings-help.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: out-of-order rendering diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 4c05fd6148..ed71b898ed 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -6,7 +6,12 @@ import { walk } from 'zimmerframe'; import { parse } from '../1-parse/acorn.js'; import * as e from '../../errors.js'; import * as w from '../../warnings.js'; -import { extract_identifiers, has_await_expression } from '../../utils/ast.js'; +import { + extract_identifiers, + has_await_expression, + object, + unwrap_pattern +} from '../../utils/ast.js'; import * as b from '#compiler/builders'; import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js'; import check_graph_for_cycles from './utils/check_graph_for_cycles.js'; @@ -543,7 +548,13 @@ export function analyze_component(root, source, options) { snippet_renderers: new Map(), snippets: new Set(), async_deriveds: new Set(), - pickled_awaits: new Set() + pickled_awaits: new Set(), + instance_body: { + sync: [], + async: [], + declarations: [], + hoisted: [] + } }; if (!runes) { @@ -676,6 +687,194 @@ export function analyze_component(root, source, options) { } } + /** + * @param {ESTree.Node} expression + * @param {Scope} scope + * @param {Set} touched + * @param {Set} seen + */ + const touch = (expression, scope, touched, seen = new Set()) => { + if (seen.has(expression)) return; + seen.add(expression); + + walk( + expression, + { scope }, + { + ImportDeclaration(node) {}, + Identifier(node, context) { + const parent = /** @type {ESTree.Node} */ (context.path.at(-1)); + if (is_reference(node, parent)) { + const binding = context.state.scope.get(node.name); + if (binding) { + touched.add(binding); + + for (const assignment of binding.assignments) { + touch(assignment.value, assignment.scope, touched, seen); + } + } + } + } + } + ); + }; + + /** + * @param {ESTree.Node} node + * @param {Set} seen + * @param {Set} reads + * @param {Set} writes + */ + const trace_references = (node, reads, writes, seen = new Set()) => { + if (seen.has(node)) return; + seen.add(node); + + /** + * @param {ESTree.Pattern} node + * @param {Scope} scope + */ + function update(node, scope) { + for (const pattern of unwrap_pattern(node)) { + const node = object(pattern); + if (!node) return; + + const binding = scope.get(node.name); + if (!binding) return; + + writes.add(binding); + } + } + + walk( + node, + { scope: instance.scope }, + { + _(node, context) { + const scope = scopes.get(node); + if (scope) { + context.next({ scope }); + } else { + context.next(); + } + }, + AssignmentExpression(node, context) { + update(node.left, context.state.scope); + }, + UpdateExpression(node, context) { + update( + /** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node.argument), + context.state.scope + ); + }, + CallExpression(node, context) { + // for now, assume everything touched by the callee ends up mutating the object + // TODO optimise this better + + // special case — no need to peek inside effects as they only run once async work has completed + const rune = get_rune(node, context.state.scope); + if (rune === '$effect') return; + + /** @type {Set} */ + const touched = new Set(); + touch(node, context.state.scope, touched); + + for (const b of touched) { + writes.add(b); + } + }, + // don't look inside functions until they are called + ArrowFunctionExpression(_, context) {}, + FunctionDeclaration(_, context) {}, + FunctionExpression(_, context) {}, + Identifier(node, context) { + const parent = /** @type {ESTree.Node} */ (context.path.at(-1)); + if (is_reference(node, parent)) { + const binding = context.state.scope.get(node.name); + if (binding) { + reads.add(binding); + } + } + } + } + ); + }; + + let awaited = false; + + // TODO this should probably be attached to the scope? + var promises = b.id('$$promises'); + + /** + * @param {ESTree.Identifier} id + * @param {ESTree.Expression} blocker + */ + function push_declaration(id, blocker) { + analysis.instance_body.declarations.push(id); + + const binding = /** @type {Binding} */ (instance.scope.get(id.name)); + binding.blocker = blocker; + } + + for (let node of instance.ast.body) { + if (node.type === 'ImportDeclaration') { + analysis.instance_body.hoisted.push(node); + continue; + } + + if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') { + // these can't exist inside ` + +{#if condition} +

yep

+{:else} +

nope

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js index 2bcb129b12..ce7cd6bd49 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss-for-await/_config.js @@ -2,6 +2,9 @@ import { tick } from 'svelte'; import { test } from '../../test'; export default test({ + // TODO reinstate + skip: true, + compileOptions: { dev: true }, diff --git a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js index 747648e83f..ad333a573a 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-reactivity-loss/_config.js @@ -2,6 +2,9 @@ import { tick } from 'svelte'; import { test } from '../../test'; export default test({ + // TODO reinstate this + skip: true, + compileOptions: { dev: true }, diff --git a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js index cf667e1624..6f1c40988d 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js @@ -6,7 +6,7 @@ export default function Async_each_fallback_hoisting($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); - $.async(node, [() => Promise.resolve([])], (node, $$collection) => { + $.async(node, [], [() => Promise.resolve([])], (node, $$collection) => { $.each( node, 16, diff --git a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js index c579fda929..7249fd6e4f 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_each_fallback_hoisting($$renderer) { - $$renderer.async(async ($$renderer) => { + $$renderer.async_block([], async ($$renderer) => { const each_array = $.ensure_array_like((await $.save(Promise.resolve([])))()); if (each_array.length !== 0) { diff --git a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js index a1535d6886..4045ad4bf4 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js @@ -9,7 +9,7 @@ export default function Async_each_hoisting($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); - $.async(node, [() => Promise.resolve([first, second, third])], (node, $$collection) => { + $.async(node, [], [() => Promise.resolve([first, second, third])], (node, $$collection) => { $.each(node, 17, () => $.get($$collection), $.index, ($$anchor, item) => { $.next(); diff --git a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js index e87b50e2a4..43fe9414eb 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js @@ -8,7 +8,7 @@ export default function Async_each_hoisting($$renderer) { $$renderer.push(``); - $$renderer.async(async ($$renderer) => { + $$renderer.async_block([], async ($$renderer) => { const each_array = $.ensure_array_like((await $.save(Promise.resolve([first, second, third])))()); for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) { diff --git a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js index e385f5d234..d86001e273 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js @@ -6,7 +6,7 @@ export default function Async_if_alternate_hoisting($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); - $.async(node, [() => Promise.resolve(false)], (node, $$condition) => { + $.async(node, [], [() => Promise.resolve(false)], (node, $$condition) => { var consequent = ($$anchor) => { var text = $.text(); diff --git a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js index df4ad80899..1e7330429a 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_if_alternate_hoisting($$renderer) { - $$renderer.async(async ($$renderer) => { + $$renderer.async_block([], async ($$renderer) => { if ((await $.save(Promise.resolve(false)))()) { $$renderer.push(''); $$renderer.push(async () => $.escape(await Promise.reject('no no no'))); diff --git a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js index 356e8e9607..5cdb6978d9 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js @@ -6,7 +6,7 @@ export default function Async_if_hoisting($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); - $.async(node, [() => Promise.resolve(true)], (node, $$condition) => { + $.async(node, [], [() => Promise.resolve(true)], (node, $$condition) => { var consequent = ($$anchor) => { var text = $.text(); diff --git a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js index 1d935f9be8..1ca24cf81a 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_if_hoisting($$renderer) { - $$renderer.async(async ($$renderer) => { + $$renderer.async_block([], async ($$renderer) => { if ((await $.save(Promise.resolve(true)))()) { $$renderer.push(''); $$renderer.push(async () => $.escape(await Promise.resolve('yes yes yes'))); diff --git a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js index 7a97850175..e4df43c6c2 100644 --- a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js @@ -5,48 +5,47 @@ import * as $ from 'svelte/internal/client'; export default function Async_in_derived($$anchor, $$props) { $.push($$props, true); - $.async_body($$anchor, async ($$anchor) => { - let yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))(); - let yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))(); + var yes1, yes2, no1, no2; - let no1 = $.derived(async () => { - return await 1; - }); + var $$promises = $.run([ + async () => yes1 = await $.async_derived(() => 1), + async () => yes2 = await $.async_derived(async () => foo(await 1)), - let no2 = $.derived(() => async () => { + () => no1 = $.derived(async () => { return await 1; - }); + }), - if ($.aborted()) return; - - var fragment = $.comment(); - var node = $.first_child(fragment); + () => no2 = $.derived(() => async () => { + return await 1; + }) + ]); - { - var consequent = ($$anchor) => { - $.async_body($$anchor, async ($$anchor) => { - const yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))(); - const yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))(); + var fragment = $.comment(); + var node = $.first_child(fragment); - const no1 = $.derived(() => (async () => { - return await 1; - })()); + { + var consequent = ($$anchor) => { + $.async_body($$anchor, async ($$anchor) => { + const yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))(); + const yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))(); - const no2 = $.derived(() => (async () => { - return await 1; - })()); + const no1 = $.derived(() => (async () => { + return await 1; + })()); - if ($.aborted()) return; - }); - }; + const no2 = $.derived(() => (async () => { + return await 1; + })()); - $.if(node, ($$render) => { - if (true) $$render(consequent); + if ($.aborted()) return; }); - } + }; - $.append($$anchor, fragment); - }); + $.if(node, ($$render) => { + if (true) $$render(consequent); + }); + } + $.append($$anchor, fragment); $.pop(); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js index 69eca5a383..bece6402c6 100644 --- a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js @@ -3,38 +3,40 @@ import * as $ from 'svelte/internal/server'; export default function Async_in_derived($$renderer, $$props) { $$renderer.component(($$renderer) => { - $$renderer.async(async ($$renderer) => { - let yes1 = (await $.save(1))(); - let yes2 = foo((await $.save(1))()); + var yes1, yes2, no1, no2; - let no1 = (async () => { - return await 1; - })(); + var $$promises = $$renderer.run([ + async () => yes1 = await 1, + async () => yes2 = foo(await 1), - let no2 = async () => { + () => no1 = (async () => { return await 1; - }; - - $$renderer.async(async ($$renderer) => { - if (true) { - $$renderer.push(''); - - const yes1 = (await $.save(1))(); - const yes2 = foo((await $.save(1))()); - - const no1 = (async () => { - return await 1; - })(); + })(), - const no2 = (async () => { - return await 1; - })(); - } else { - $$renderer.push(''); - } - }); - - $$renderer.push(``); + () => no2 = async () => { + return await 1; + } + ]); + + $$renderer.async_block([], async ($$renderer) => { + if (true) { + $$renderer.push(''); + + const yes1 = (await $.save(1))(); + const yes2 = foo((await $.save(1))()); + + const no1 = (async () => { + return await 1; + })(); + + const no2 = (async () => { + return await 1; + })(); + } else { + $$renderer.push(''); + } }); + + $$renderer.push(``); }); } \ No newline at end of file From 83746adcf79c41e9b95b41225ee156dc3c08f5b7 Mon Sep 17 00:00:00 2001 From: Alejandro Torres Date: Tue, 28 Oct 2025 12:51:19 -0400 Subject: [PATCH 48/58] Fix spelling in code comments (#17059) --- packages/svelte/src/internal/client/proxy.js | 2 +- packages/svelte/src/internal/server/renderer.js | 2 +- packages/svelte/tests/runtime-browser/assert.js | 2 +- .../tests/runtime-runes/samples/mount-props-updates/_config.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 49cef451b3..5b028d8d09 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -267,7 +267,7 @@ export function proxy(value) { if (other_s !== undefined) { set(other_s, UNINITIALIZED); } else if (i in target) { - // If the item exists in the original, we need to create a uninitialized source, + // If the item exists in the original, we need to create an uninitialized source, // else a later read of the property would result in a source being created with // the value of the original item at that index. other_s = with_parent(() => source(UNINITIALIZED, stack)); diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index 5dc845e376..479175c2eb 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -441,7 +441,7 @@ export class Renderer { } /** - * Collect all of the `onDestroy` callbacks regsitered during rendering. In an async context, this is only safe to call + * Collect all of the `onDestroy` callbacks registered during rendering. In an async context, this is only safe to call * after awaiting `collect_async`. * * Child renderers are "porous" and don't affect execution order, but component body renderers diff --git a/packages/svelte/tests/runtime-browser/assert.js b/packages/svelte/tests/runtime-browser/assert.js index e331c8b677..249c5ad33d 100644 --- a/packages/svelte/tests/runtime-browser/assert.js +++ b/packages/svelte/tests/runtime-browser/assert.js @@ -166,7 +166,7 @@ export function test(args) { return args; } -// TypeScript needs the type of assertions to be directly visible, not infered, which is why +// TypeScript needs the type of assertions to be directly visible, not inferred, which is why // we can't have it on the test suite type. /** * @param {any} value diff --git a/packages/svelte/tests/runtime-runes/samples/mount-props-updates/_config.js b/packages/svelte/tests/runtime-runes/samples/mount-props-updates/_config.js index ff7af2d524..57e4d276ff 100644 --- a/packages/svelte/tests/runtime-runes/samples/mount-props-updates/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/mount-props-updates/_config.js @@ -22,7 +22,7 @@ export default test({ target.innerHTML, // bar is not set in the parent because it's a readonly property // baz is not set in the parent because while it's a bindable property, - // it wasn't set initially so it's treated as a readonly proeprty + // it wasn't set initially so it's treated as a readonly property ` foo 3
1 2 3 4
From 70d020bd9128bf0cc596780636c7f4b0081088a6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 28 Oct 2025 21:35:50 +0100 Subject: [PATCH 49/58] Version Packages (#17055) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/bitter-rings-help.md | 5 ----- .changeset/loud-chairs-tease.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) delete mode 100644 .changeset/bitter-rings-help.md delete mode 100644 .changeset/loud-chairs-tease.md diff --git a/.changeset/bitter-rings-help.md b/.changeset/bitter-rings-help.md deleted file mode 100644 index f71b9f96b2..0000000000 --- a/.changeset/bitter-rings-help.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': minor ---- - -feat: out-of-order rendering diff --git a/.changeset/loud-chairs-tease.md b/.changeset/loud-chairs-tease.md deleted file mode 100644 index e08d111cdd..0000000000 --- a/.changeset/loud-chairs-tease.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: settle batch after DOM updates diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 8dabe54b33..0a8637a321 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.43.0 + +### Minor Changes + +- feat: out-of-order rendering ([#17038](https://github.com/sveltejs/svelte/pull/17038)) + +### Patch Changes + +- fix: settle batch after DOM updates ([#17054](https://github.com/sveltejs/svelte/pull/17054)) + ## 5.42.3 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 1ee6c50121..a66afa61fe 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.42.3", + "version": "5.43.0", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 999aacc998..225f50220c 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.42.3'; +export const VERSION = '5.43.0'; export const PUBLIC_VERSION = '5'; From 9477f18b7705d731ea22bc7dc4e369baec0f31b4 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 30 Oct 2025 13:51:23 +0100 Subject: [PATCH 50/58] fix: transform `$bindable` after `await` expressions (#17066) Fixes #17064 --- .changeset/hot-crews-notice.md | 5 +++ .../3-transform/shared/transform-async.js | 2 +- .../samples/async-bindable-prop/Child.svelte | 8 +++++ .../samples/async-bindable-prop/_config.js | 35 +++++++++++++++++++ .../samples/async-bindable-prop/main.svelte | 9 +++++ 5 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 .changeset/hot-crews-notice.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-bindable-prop/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-bindable-prop/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-bindable-prop/main.svelte diff --git a/.changeset/hot-crews-notice.md b/.changeset/hot-crews-notice.md new file mode 100644 index 0000000000..5b11a82fda --- /dev/null +++ b/.changeset/hot-crews-notice.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: transform `$bindable` after `await` expressions diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js b/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js index 444b8d7d94..6ec7893452 100644 --- a/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js +++ b/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js @@ -55,7 +55,7 @@ export function transform_body(instance_body, runner, transform) { if (visited.declarations.length === 1) { return b.thunk( - b.assignment('=', s.node.id, visited.declarations[0].init ?? b.void0), + b.assignment('=', visited.declarations[0].id, visited.declarations[0].init ?? b.void0), s.has_await ); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/Child.svelte new file mode 100644 index 0000000000..4214a85f37 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/Child.svelte @@ -0,0 +1,8 @@ + + + +{value} + diff --git a/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/_config.js b/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/_config.js new file mode 100644 index 0000000000..a4ff70a8dc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/_config.js @@ -0,0 +1,35 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['async-server', 'client', 'hydrate'], + ssrHtml: ` + initial

initial

+ `, + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + initial +

initial

+ ` + ); + + const button = target.querySelector('button'); + button?.click(); + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` + + updated +

updated

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/main.svelte new file mode 100644 index 0000000000..e053fdda6a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-bindable-prop/main.svelte @@ -0,0 +1,9 @@ + + + +

{value}

+ From 8ebc3b1337bd12931f5608ab608b9668e10aa6c2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 13:56:25 +0100 Subject: [PATCH 51/58] Version Packages (#17076) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/hot-crews-notice.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/hot-crews-notice.md diff --git a/.changeset/hot-crews-notice.md b/.changeset/hot-crews-notice.md deleted file mode 100644 index 5b11a82fda..0000000000 --- a/.changeset/hot-crews-notice.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: transform `$bindable` after `await` expressions diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 0a8637a321..2a2cd28698 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.43.1 + +### Patch Changes + +- fix: transform `$bindable` after `await` expressions ([#17066](https://github.com/sveltejs/svelte/pull/17066)) + ## 5.43.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index a66afa61fe..a7e0c618bf 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.43.0", + "version": "5.43.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 225f50220c..4c0a8f05c8 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.43.0'; +export const VERSION = '5.43.1'; export const PUBLIC_VERSION = '5'; From a791e9178b229e720adbbc0156eb03aa08941698 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 30 Oct 2025 15:15:47 +0100 Subject: [PATCH 52/58] fix: treat each blocks with async dependencies as uncontrolled (#17077) --- .changeset/fair-files-cover.md | 5 +++++ .../phases/3-transform/client/visitors/shared/fragment.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/fair-files-cover.md diff --git a/.changeset/fair-files-cover.md b/.changeset/fair-files-cover.md new file mode 100644 index 0000000000..1009773e5b --- /dev/null +++ b/.changeset/fair-files-cover.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: treat each blocks with async dependencies as uncontrolled diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js index 3588f2843a..c7f843af48 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js @@ -105,7 +105,7 @@ export function process_children(nodes, initial, is_element, context) { is_element && // In case it's wrapped in async the async logic will want to skip sibling nodes up until the end, hence we cannot make this controlled // TODO switch this around and instead optimize for elements with a single block child and not require extra comments (neither for async nor normally) - !(node.body.metadata.has_await || node.metadata.expression.has_await) + !(node.body.metadata.has_await || node.metadata.expression.is_async()) ) { node.metadata.is_controlled = true; } else { From 723c421fbb1abb8e50d13505b9ef5999df884a9d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 16:14:11 +0100 Subject: [PATCH 53/58] Version Packages (#17078) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/fair-files-cover.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) delete mode 100644 .changeset/fair-files-cover.md diff --git a/.changeset/fair-files-cover.md b/.changeset/fair-files-cover.md deleted file mode 100644 index 1009773e5b..0000000000 --- a/.changeset/fair-files-cover.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: treat each blocks with async dependencies as uncontrolled diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 2a2cd28698..bcf17fe45e 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.43.2 + +### Patch Changes + +- fix: treat each blocks with async dependencies as uncontrolled ([#17077](https://github.com/sveltejs/svelte/pull/17077)) + ## 5.43.1 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index a7e0c618bf..f7a1cca616 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.43.1", + "version": "5.43.2", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 4c0a8f05c8..0a28702778 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.43.1'; +export const VERSION = '5.43.2'; export const PUBLIC_VERSION = '5'; From 0e709e3fec59a66635962b8fac3bbbacdd765e9e Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 31 Oct 2025 07:17:12 +0100 Subject: [PATCH 54/58] fix: change title only after any pending work has completed (#17061) * fix: change title only after any pending work has completed We have to use an effect - not a render effect - for updating the title, and always. That way we change the title only after any pending work has completed. Fixes #17060 * fix --- .changeset/legal-mangos-peel.md | 5 ++++ .../client/visitors/TitleElement.js | 23 ++++++++++++++---- .../src/internal/client/reactivity/batch.js | 21 +++++++--------- .../src/internal/client/reactivity/effects.js | 5 ++-- .../samples/async-head-title-1/Inner.svelte | 15 ++++++++++++ .../samples/async-head-title-1/_config.js | 24 +++++++++++++++++++ .../samples/async-head-title-1/main.svelte | 12 ++++++++++ .../samples/async-head-title-2/Inner.svelte | 13 ++++++++++ .../samples/async-head-title-2/_config.js | 23 ++++++++++++++++++ .../samples/async-head-title-2/main.svelte | 12 ++++++++++ 10 files changed, 135 insertions(+), 18 deletions(-) create mode 100644 .changeset/legal-mangos-peel.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head-title-1/Inner.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head-title-1/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head-title-1/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head-title-2/Inner.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head-title-2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-head-title-2/main.svelte diff --git a/.changeset/legal-mangos-peel.md b/.changeset/legal-mangos-peel.md new file mode 100644 index 0000000000..bddad21bff --- /dev/null +++ b/.changeset/legal-mangos-peel.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: change title only after any pending work has completed diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js index 98d7880b25..edd8835e00 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js @@ -1,16 +1,19 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; -import { build_template_chunk } from './shared/utils.js'; +import { build_template_chunk, Memoizer } from './shared/utils.js'; /** * @param {AST.TitleElement} node * @param {ComponentContext} context */ export function TitleElement(node, context) { + const memoizer = new Memoizer(); const { has_state, value } = build_template_chunk( /** @type {any} */ (node.fragment.nodes), - context + context, + context.state, + (value, metadata) => memoizer.add(value, metadata) ); const evaluated = context.state.scope.evaluate(value); @@ -26,9 +29,21 @@ export function TitleElement(node, context) { ) ); + // Always in an $effect so it only changes the title once async work is done if (has_state) { - context.state.update.push(statement); + context.state.after_update.push( + b.stmt( + b.call( + '$.template_effect', + b.arrow(memoizer.apply(), b.block([statement])), + memoizer.sync_values(), + memoizer.async_values(), + memoizer.blockers(), + b.true + ) + ) + ); } else { - context.state.init.push(statement); + context.state.after_update.push(b.stmt(b.call('$.effect', b.thunk(b.block([statement]))))); } } diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 9d61b6bbf9..27c90d7708 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -14,33 +14,25 @@ import { MAYBE_DIRTY, DERIVED, BOUNDARY_EFFECT, - EAGER_EFFECT + EAGER_EFFECT, + HEAD_EFFECT } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; import { deferred, define_property } from '../../shared/utils.js'; import { active_effect, get, - increment_write_version, is_dirty, is_updating_effect, set_is_updating_effect, set_signal_status, - tick, update_effect } from '../runtime.js'; import * as e from '../errors.js'; import { flush_tasks, queue_micro_task } from '../dom/task.js'; import { DEV } from 'esm-env'; import { invoke_error_boundary } from '../error-handling.js'; -import { - flush_eager_effects, - eager_effects, - old_values, - set_eager_effects, - source, - update -} from './sources.js'; +import { flush_eager_effects, old_values, set_eager_effects, source, update } from './sources.js'; import { eager_effect, unlink_effect } from './effects.js'; /** @@ -800,7 +792,12 @@ export function schedule_effect(signal) { // if the effect is being scheduled because a parent (each/await/etc) block // updated an internal source, bail out or we'll cause a second flush - if (is_flushing && effect === active_effect && (flags & BLOCK_EFFECT) !== 0) { + if ( + is_flushing && + effect === active_effect && + (flags & BLOCK_EFFECT) !== 0 && + (flags & HEAD_EFFECT) === 0 + ) { return; } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 4a9fce7286..8c4b84438c 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -366,10 +366,11 @@ export function render_effect(fn, flags = 0) { * @param {Array<() => any>} sync * @param {Array<() => Promise>} async * @param {Array>} blockers + * @param {boolean} defer */ -export function template_effect(fn, sync = [], async = [], blockers = []) { +export function template_effect(fn, sync = [], async = [], blockers = [], defer = false) { flatten(blockers, sync, async, (values) => { - create_effect(RENDER_EFFECT, () => fn(...values.map(get)), true); + create_effect(defer ? EFFECT : RENDER_EFFECT, () => fn(...values.map(get)), true); }); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-1/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/Inner.svelte new file mode 100644 index 0000000000..089ba43607 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/Inner.svelte @@ -0,0 +1,15 @@ + + + + title + + +

{await push()}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-1/_config.js b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/_config.js new file mode 100644 index 0000000000..39cbf5becb --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/_config.js @@ -0,0 +1,24 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [toggle, resolve] = target.querySelectorAll('button'); + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + resolve.click(); + await tick(); + await tick(); + assert.equal(window.document.title, 'title'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/main.svelte new file mode 100644 index 0000000000..3535157087 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-1/main.svelte @@ -0,0 +1,12 @@ + + + + +{#if show} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-2/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/Inner.svelte new file mode 100644 index 0000000000..b2a8656276 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/Inner.svelte @@ -0,0 +1,13 @@ + + + + {await push()} + diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/_config.js new file mode 100644 index 0000000000..b89dce62d1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/_config.js @@ -0,0 +1,23 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [toggle, resolve] = target.querySelectorAll('button'); + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + toggle.click(); + await tick(); + assert.equal(window.document.title, ''); + + resolve.click(); + await tick(); + assert.equal(window.document.title, 'title'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-head-title-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/main.svelte new file mode 100644 index 0000000000..3535157087 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-head-title-2/main.svelte @@ -0,0 +1,12 @@ + + + + +{#if show} + +{/if} From 8ebbb3c7bccfba4f509456b5b4f5b81ccf13d65c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 1 Nov 2025 23:35:20 +0100 Subject: [PATCH 55/58] fix: preserve symbols when creating derived rest properties (#17096) fixes #17094 --- .changeset/sixty-comics-bow.md | 5 +++++ packages/svelte/src/internal/client/runtime.js | 14 ++++++++++---- .../derived-rest-includes-symbol/_config.js | 5 +++++ .../derived-rest-includes-symbol/main.svelte | 8 ++++++++ 4 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 .changeset/sixty-comics-bow.md create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/main.svelte diff --git a/.changeset/sixty-comics-bow.md b/.changeset/sixty-comics-bow.md new file mode 100644 index 0000000000..2463e52430 --- /dev/null +++ b/.changeset/sixty-comics-bow.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: preserve symbols when creating derived rest properties diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 2197f34d16..6485d21ec2 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -763,12 +763,12 @@ export function set_signal_status(signal, status) { } /** - * @param {Record} obj - * @param {string[]} keys - * @returns {Record} + * @param {Record} obj + * @param {Array} keys + * @returns {Record} */ export function exclude_from_object(obj, keys) { - /** @type {Record} */ + /** @type {Record} */ var result = {}; for (var key in obj) { @@ -777,6 +777,12 @@ export function exclude_from_object(obj, keys) { } } + for (var symbol of Object.getOwnPropertySymbols(obj)) { + if (!keys.includes(symbol)) { + result[symbol] = obj[symbol]; + } + } + return result; } diff --git a/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/_config.js new file mode 100644 index 0000000000..eec60928a2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `

42

` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/main.svelte new file mode 100644 index 0000000000..ee161696e1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/main.svelte @@ -0,0 +1,8 @@ + + +

{b[symbol]}

From b7625fd42c067b7cd03fea9589ef45349f9dedbe Mon Sep 17 00:00:00 2001 From: 7nik Date: Sun, 2 Nov 2025 11:40:32 +0200 Subject: [PATCH 56/58] fix: do not spread non-enumerable symbols (#17097) --- packages/svelte/src/internal/client/runtime.js | 2 +- .../samples/derived-rest-includes-symbol/_config.js | 2 +- .../samples/derived-rest-includes-symbol/main.svelte | 9 ++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 6485d21ec2..76531d3320 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -778,7 +778,7 @@ export function exclude_from_object(obj, keys) { } for (var symbol of Object.getOwnPropertySymbols(obj)) { - if (!keys.includes(symbol)) { + if (Object.propertyIsEnumerable.call(obj, symbol) && !keys.includes(symbol)) { result[symbol] = obj[symbol]; } } diff --git a/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/_config.js index eec60928a2..d0633983d2 100644 --- a/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/_config.js @@ -1,5 +1,5 @@ import { test } from '../../test'; export default test({ - html: `

42

` + html: `

true false

` }); diff --git a/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/main.svelte index ee161696e1..2454e98ab7 100644 --- a/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/derived-rest-includes-symbol/main.svelte @@ -1,8 +1,11 @@ -

{b[symbol]}

+

{symbol1 in b} {symbol2 in b}

From eea8a18cf2ff8245f0687e8012d5923c5d508d03 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 3 Nov 2025 21:02:09 +0100 Subject: [PATCH 57/58] fix: ensure fork always accesses correct values (#17098) Not all batches will flush right after being activated, some will be activated and then `get` is called on a signal. In that case the value was wrong because we did not apply the changes of that batch. By doing `this.apply()` during `activate()` we ensure we do, which fixes (among other things, likely) a forking bug where old values where sneaking in. Fixes #17079 --- .changeset/eight-news-laugh.md | 5 +++++ .../src/internal/client/reactivity/batch.js | 13 +++++++++---- .../samples/async-fork-if/Child.svelte | 8 ++++++++ .../samples/async-fork-if/_config.js | 12 ++++++++++++ .../samples/async-fork-if/main.svelte | 17 +++++++++++++++++ 5 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 .changeset/eight-news-laugh.md create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-if/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-if/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-fork-if/main.svelte diff --git a/.changeset/eight-news-laugh.md b/.changeset/eight-news-laugh.md new file mode 100644 index 0000000000..e120b19f5e --- /dev/null +++ b/.changeset/eight-news-laugh.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure fork always accesses correct values diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 27c90d7708..57aa185a31 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -15,7 +15,8 @@ import { DERIVED, BOUNDARY_EFFECT, EAGER_EFFECT, - HEAD_EFFECT + HEAD_EFFECT, + ERROR_VALUE } from '#client/constants'; import { async_mode_flag } from '../../flags/index.js'; import { deferred, define_property } from '../../shared/utils.js'; @@ -285,12 +286,16 @@ export class Batch { this.previous.set(source, value); } - this.current.set(source, source.v); - batch_values?.set(source, source.v); + // Don't save errors in `batch_values`, or they won't be thrown in `runtime.js#get` + if ((source.f & ERROR_VALUE) === 0) { + this.current.set(source, source.v); + batch_values?.set(source, source.v); + } } activate() { current_batch = this; + this.apply(); } deactivate() { @@ -492,7 +497,7 @@ export class Batch { } apply() { - if (!async_mode_flag || batches.size === 1) return; + if (!async_mode_flag || (!this.is_fork && batches.size === 1)) return; // if there are multiple batches, we are 'time travelling' — // we need to override values with the ones in this batch... diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-if/Child.svelte new file mode 100644 index 0000000000..6ef7d03eea --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if/Child.svelte @@ -0,0 +1,8 @@ + + +{x} diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-if/_config.js new file mode 100644 index 0000000000..1bc168d9ae --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if/_config.js @@ -0,0 +1,12 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, target, logs }) { + const btn = target.querySelector('button'); + + btn?.click(); + await new Promise((r) => setTimeout(r, 2)); + assert.htmlEqual(target.innerHTML, ` universe`); + assert.deepEqual(logs, ['universe', 'universe']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-if/main.svelte new file mode 100644 index 0000000000..625040ec13 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if/main.svelte @@ -0,0 +1,17 @@ + + + + +{#if x === 'universe'} + +{/if} From 7a2435471c96c43d6f00144cdb5889772703d786 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:53:51 -0500 Subject: [PATCH 58/58] Version Packages (#17087) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/eight-news-laugh.md | 5 ----- .changeset/legal-mangos-peel.md | 5 ----- .changeset/sixty-comics-bow.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 6 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 .changeset/eight-news-laugh.md delete mode 100644 .changeset/legal-mangos-peel.md delete mode 100644 .changeset/sixty-comics-bow.md diff --git a/.changeset/eight-news-laugh.md b/.changeset/eight-news-laugh.md deleted file mode 100644 index e120b19f5e..0000000000 --- a/.changeset/eight-news-laugh.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ensure fork always accesses correct values diff --git a/.changeset/legal-mangos-peel.md b/.changeset/legal-mangos-peel.md deleted file mode 100644 index bddad21bff..0000000000 --- a/.changeset/legal-mangos-peel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: change title only after any pending work has completed diff --git a/.changeset/sixty-comics-bow.md b/.changeset/sixty-comics-bow.md deleted file mode 100644 index 2463e52430..0000000000 --- a/.changeset/sixty-comics-bow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: preserve symbols when creating derived rest properties diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index bcf17fe45e..bc2f815908 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.43.3 + +### Patch Changes + +- fix: ensure fork always accesses correct values ([#17098](https://github.com/sveltejs/svelte/pull/17098)) + +- fix: change title only after any pending work has completed ([#17061](https://github.com/sveltejs/svelte/pull/17061)) + +- fix: preserve symbols when creating derived rest properties ([#17096](https://github.com/sveltejs/svelte/pull/17096)) + ## 5.43.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index f7a1cca616..f178444593 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.43.2", + "version": "5.43.3", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 0a28702778..5ad40ddee6 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.43.2'; +export const VERSION = '5.43.3'; export const PUBLIC_VERSION = '5';