From 425fba33fed0572e952d364d1c59f7af01aedd07 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Fri, 20 Mar 2026 22:23:57 +0100 Subject: [PATCH 01/13] fix: hydration comments during hmr (#17975) Closes #17972 Claude found this fix I had a look and I think it makes sense (we are injecting a new comment which messes up the marching during hydration). I'm slightly confused why this only applied to `css_props` but I guess that's because there's an "hidden" element there which makes it special. I don't super-like the `if` situation, but I guess it is what it is. Also the test was using `hmr` without `dev` and this brought to my attention that we were just assuming components return something while it's not the case...I guess it doesn't really matter unless you are using `hmr` in prod which is not a thing but fixing this is simple so we might just as well doing it --- .changeset/calm-mice-allow.md | 5 ++++ .../svelte/src/internal/client/dev/hmr.js | 25 +++++++++++++------ .../samples/css-props-hmr/Component.svelte | 7 ++++++ .../samples/css-props-hmr/_config.js | 7 ++++++ .../samples/css-props-hmr/main.svelte | 5 ++++ 5 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 .changeset/calm-mice-allow.md create mode 100644 packages/svelte/tests/hydration/samples/css-props-hmr/Component.svelte create mode 100644 packages/svelte/tests/hydration/samples/css-props-hmr/_config.js create mode 100644 packages/svelte/tests/hydration/samples/css-props-hmr/main.svelte diff --git a/.changeset/calm-mice-allow.md b/.changeset/calm-mice-allow.md new file mode 100644 index 0000000000..eff71e1082 --- /dev/null +++ b/.changeset/calm-mice-allow.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: hydration comments during hmr diff --git a/packages/svelte/src/internal/client/dev/hmr.js b/packages/svelte/src/internal/client/dev/hmr.js index 071901aafc..4687708c90 100644 --- a/packages/svelte/src/internal/client/dev/hmr.js +++ b/packages/svelte/src/internal/client/dev/hmr.js @@ -34,7 +34,13 @@ export function hmr(fn) { let start = create_comment(); let end = create_comment(); - anchor.before(start); + // During hydration, inserting the start comment before the anchor could + // corrupt the DOM tree that the hydration walker is navigating (e.g. when + // a component is inside a CSS props wrapper gh-issue#17972). We defer the insertion until + // after the component has hydrated. + if (!hydrating) { + anchor.before(start); + } block(() => { if (component === (component = get(current))) { @@ -52,13 +58,13 @@ export function hmr(fn) { if (ran) set_should_intro(false); // preserve getters/setters - Object.defineProperties( - instance, - Object.getOwnPropertyDescriptors( - // @ts-expect-error - new.target ? new component(anchor, props) : component(anchor, props) - ) - ); + var result = + // @ts-expect-error + new.target ? new component(anchor, props) : component(anchor, props); + // a component is not guaranteed to return something and we can't invoke getOwnPropertyDescriptors on undefined + if (result) { + Object.defineProperties(instance, Object.getOwnPropertyDescriptors(result)); + } if (ran) set_should_intro(true); }); @@ -67,6 +73,9 @@ export function hmr(fn) { ran = true; if (hydrating) { + // Insert start comment now that hydration is done, so it doesn't + // corrupt the hydration walk + anchor.before(start); anchor = hydrate_node; } diff --git a/packages/svelte/tests/hydration/samples/css-props-hmr/Component.svelte b/packages/svelte/tests/hydration/samples/css-props-hmr/Component.svelte new file mode 100644 index 0000000000..ac73501cca --- /dev/null +++ b/packages/svelte/tests/hydration/samples/css-props-hmr/Component.svelte @@ -0,0 +1,7 @@ +

Hello

+ + \ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/css-props-hmr/_config.js b/packages/svelte/tests/hydration/samples/css-props-hmr/_config.js new file mode 100644 index 0000000000..2cd696d584 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/css-props-hmr/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + hmr: true + } +}); diff --git a/packages/svelte/tests/hydration/samples/css-props-hmr/main.svelte b/packages/svelte/tests/hydration/samples/css-props-hmr/main.svelte new file mode 100644 index 0000000000..eed9b57caf --- /dev/null +++ b/packages/svelte/tests/hydration/samples/css-props-hmr/main.svelte @@ -0,0 +1,5 @@ + + + \ No newline at end of file From 6b33dd2a1e8aa48dc88c9ce6e19c4a49a2eac51a Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Sat, 21 Mar 2026 13:29:39 +0100 Subject: [PATCH 02/13] fix: group sync statements (#17977) We were just putting each statement into its own promise. Besides this being bad for perf, it also introduces subtle timing issues - the execution order of the code could change in bad ways. Fixes #17940 --- .changeset/puny-masks-run.md | 5 + .../src/compiler/phases/2-analyze/index.js | 36 ++++- .../3-transform/shared/transform-async.js | 124 ++++++++++-------- .../svelte/src/compiler/phases/types.d.ts | 2 +- .../_expected/client/index.svelte.js | 16 ++- .../_expected/server/index.svelte.js | 16 ++- .../async-top-level-group-sync-run/_config.js | 3 + .../_expected/client/index.svelte.js | 26 ++++ .../_expected/server/index.svelte.js | 21 +++ .../index.svelte | 9 ++ 10 files changed, 184 insertions(+), 74 deletions(-) create mode 100644 .changeset/puny-masks-run.md create mode 100644 packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/_config.js create mode 100644 packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/index.svelte diff --git a/.changeset/puny-masks-run.md b/.changeset/puny-masks-run.md new file mode 100644 index 0000000000..162abe4ae7 --- /dev/null +++ b/.changeset/puny-masks-run.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: group sync statements diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index cadd159b3e..dec0081aa9 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -1074,6 +1074,9 @@ function calculate_blockers(instance, analysis) { let awaited = false; + /** @type {Array} */ + let sync_group = []; + // TODO this should probably be attached to the scope? const promises = b.id('$$promises'); @@ -1088,6 +1091,13 @@ function calculate_blockers(instance, analysis) { binding.blocker = blocker; } + function flush_sync_group() { + if (sync_group.length === 0) return; + + analysis.instance_body.async.push({ nodes: sync_group, has_await: false }); + sync_group = []; + } + /** * Analysis of blockers for functions is deferred until we know which statements are async/blockers * @type {Array} @@ -1149,6 +1159,9 @@ function calculate_blockers(instance, analysis) { trace_references(declarator, reads, writes, instance.scope); + // Needs to happen before blocker computation + if (has_await) flush_sync_group(); + const blocker = /** @type {NonNullable} */ ( b.member(promises, b.literal(analysis.instance_body.async.length), true) ); @@ -1161,11 +1174,12 @@ function calculate_blockers(instance, analysis) { push_declaration(id, blocker); } - // one declarator per declaration, makes things simpler - analysis.instance_body.async.push({ - node: declarator, - has_await - }); + if (has_await) { + // one declarator per declaration, makes things simpler + analysis.instance_body.async.push({ nodes: [declarator], has_await: true }); + } else { + sync_group.push(declarator); + } } } } else if (awaited) { @@ -1177,6 +1191,9 @@ function calculate_blockers(instance, analysis) { trace_references(node, reads, writes, instance.scope); + // Needs to happen before blocker computation + if (has_await) flush_sync_group(); + const blocker = /** @type {NonNullable} */ ( b.member(promises, b.literal(analysis.instance_body.async.length), true) ); @@ -1187,15 +1204,20 @@ function calculate_blockers(instance, analysis) { if (node.type === 'ClassDeclaration') { push_declaration(node.id, blocker); - analysis.instance_body.async.push({ node, has_await }); + } + + if (has_await) { + analysis.instance_body.async.push({ nodes: [node], has_await: true }); } else { - analysis.instance_body.async.push({ node, has_await }); + sync_group.push(node); } } else { analysis.instance_body.sync.push(node); } } + flush_sync_group(); + for (const fn of functions) { /** @type {Set} */ const reads_writes = new Set(); 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 5c8f901d7c..ac90f1db4b 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 @@ -47,64 +47,24 @@ export function transform_body(instance_body, runner, transform) { // Thunks for the await expressions if (instance_body.async.length > 0) { - const thunks = instance_body.async.map((s) => { - if (s.node.type === 'VariableDeclarator') { - const visited = /** @type {ESTree.VariableDeclaration | ESTree.EmptyStatement} */ ( - transform(b.var(s.node.id, s.node.init)) - ); - - const statements = - visited.type === 'VariableDeclaration' - ? visited.declarations.map((node) => { - if ( - node.id.type === 'Identifier' && - (node.id.name.startsWith('$$d') || node.id.name.startsWith('$$array')) - ) { - // this is an intermediate declaration created in VariableDeclaration.js; - // subsequent statements depend on it - return b.var(node.id, node.init); - } - - return b.stmt(b.assignment('=', node.id, node.init ?? b.void0)); - }) - : []; - - if (statements.length === 1) { - const statement = /** @type {ESTree.ExpressionStatement} */ (statements[0]); - return b.thunk(statement.expression, s.has_await); - } - - return b.thunk(b.block(statements), s.has_await); - } + const thunks = instance_body.async.map((entry) => { + /** @type {ESTree.Statement[]} */ + const entry_statements = []; - if (s.node.type === 'ClassDeclaration') { - return b.thunk( - b.assignment( - '=', - s.node.id, - /** @type {ESTree.ClassExpression} */ ({ ...s.node, type: 'ClassExpression' }) - ), - s.has_await - ); + for (const node of entry.nodes) { + entry_statements.push(...transform_async_node(node, transform)); } - if (s.node.type === 'ExpressionStatement') { - // the expression may be a $inspect call, which will be transformed into an empty statement - const expression = /** @type {ESTree.Expression | ESTree.EmptyStatement} */ ( - transform(s.node.expression) - ); - - if (expression.type === 'EmptyStatement') { - // Keep indices stable for async sequencing while avoiding array holes in run([...]). - return b.thunk(b.void0, false); - } + if (entry_statements.length === 0) { + // Keep indices stable for async sequencing while avoiding array holes in run([...]). + return b.thunk(b.void0, false); + } - return expression.type === 'AwaitExpression' - ? b.thunk(expression, true) - : b.thunk(b.unary('void', expression), s.has_await); + if (entry_statements.length === 1 && entry_statements[0].type === 'ExpressionStatement') { + return b.thunk(entry_statements[0].expression, entry.has_await); } - return b.thunk(b.block([/** @type {ESTree.Statement} */ (transform(s.node))]), s.has_await); + return b.thunk(b.block(entry_statements), entry.has_await); }); // TODO get the `$$promises` ID from scope @@ -113,3 +73,63 @@ export function transform_body(instance_body, runner, transform) { return statements; } + +/** + * @param {ESTree.Statement | ESTree.VariableDeclarator} node + * @param {(node: ESTree.Node) => ESTree.Node} transform + * @returns {ESTree.Statement[]} + */ +function transform_async_node(node, transform) { + if (node.type === 'VariableDeclarator') { + const visited = /** @type {ESTree.VariableDeclaration | ESTree.EmptyStatement} */ ( + transform(b.var(node.id, node.init)) + ); + + return visited.type === 'VariableDeclaration' + ? visited.declarations.map((node) => { + if ( + node.id.type === 'Identifier' && + (node.id.name.startsWith('$$d') || node.id.name.startsWith('$$array')) + ) { + // This intermediate declaration is created in VariableDeclaration.js; + // subsequent statements may depend on it. + return b.var(node.id, node.init); + } + + return b.stmt(b.assignment('=', node.id, node.init ?? b.void0)); + }) + : []; + } + + if (node.type === 'ClassDeclaration') { + return [ + b.stmt( + b.assignment( + '=', + node.id, + /** @type {ESTree.ClassExpression} */ ({ ...node, type: 'ClassExpression' }) + ) + ) + ]; + } + + if (node.type === 'ExpressionStatement') { + // The expression may be a $inspect call, which will be transformed into an empty statement. + const expression = /** @type {ESTree.Expression | ESTree.EmptyStatement} */ ( + transform(node.expression) + ); + + if (expression.type === 'EmptyStatement') { + return []; + } + + if (expression.type === 'AwaitExpression') { + return [b.stmt(expression)]; + } + + return [b.stmt(b.unary('void', expression))]; + } + + const statement = /** @type {ESTree.Statement | ESTree.EmptyStatement} */ (transform(node)); + return statement.type === 'EmptyStatement' ? [] : [statement]; +} diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index 5397ea45f9..a1a85ce145 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -131,7 +131,7 @@ export interface ComponentAnalysis extends Analysis { instance_body: { hoisted: Array; sync: Array; - async: Array<{ node: Statement | VariableDeclarator; has_await: boolean }>; + async: Array<{ nodes: Array; has_await: boolean }>; declarations: Array; }; } 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 a8b1a43495..4f06d9ddbf 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 @@ -10,13 +10,15 @@ export default function Async_in_derived($$anchor, $$props) { var $$promises = $.run([ async () => yes1 = await $.async_derived(() => 1), async () => yes2 = await $.async_derived(async () => foo(await 1)), - () => no1 = $.derived(async () => { - return await 1; - }), - - () => no2 = $.derived(() => async () => { - return await 1; - }) + () => { + no1 = $.derived(async () => { + return await 1; + }); + + no2 = $.derived(() => async () => { + return await 1; + }); + } ]); var fragment = $.comment(); 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 1697e3adc6..3a53475944 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 @@ -8,13 +8,15 @@ export default function Async_in_derived($$renderer, $$props) { var $$promises = $$renderer.run([ async () => yes1 = await $.async_derived(() => 1), async () => yes2 = await $.async_derived(async () => foo(await 1)), - () => no1 = $.derived(async () => { - return await 1; - }), - - () => no2 = $.derived(() => async () => { - return await 1; - }) + () => { + no1 = $.derived(async () => { + return await 1; + }); + + no2 = $.derived(() => async () => { + return await 1; + }); + } ]); if (true) { diff --git a/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/_config.js b/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/_config.js new file mode 100644 index 0000000000..2e30bbeb16 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/_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-top-level-group-sync-run/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/_expected/client/index.svelte.js new file mode 100644 index 0000000000..8fb09fadd2 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/_expected/client/index.svelte.js @@ -0,0 +1,26 @@ +import 'svelte/internal/disclose-version'; +import 'svelte/internal/flags/async'; +import * as $ from 'svelte/internal/client'; + +export default function Async_top_level_group_sync_run($$anchor) { + var a, + // these should be grouped into one, having an async tick inbetween + // would change how the code runs and could introduce subtle timing bugs + b, + c; + + var $$promises = $.run([ + async () => a = await Promise.resolve(1), + () => { + b = a + 1; + c = b + 1; + } + ]); + + $.next(); + + var text = $.text(); + + $.template_effect(() => $.set_text(text, c), void 0, void 0, [$$promises[1]]); + $.append($$anchor, text); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/_expected/server/index.svelte.js new file mode 100644 index 0000000000..43db129746 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/_expected/server/index.svelte.js @@ -0,0 +1,21 @@ +import 'svelte/internal/flags/async'; +import * as $ from 'svelte/internal/server'; + +export default function Async_top_level_group_sync_run($$renderer) { + var a, + // these should be grouped into one, having an async tick inbetween + // would change how the code runs and could introduce subtle timing bugs + b, + c; + + var $$promises = $$renderer.run([ + async () => a = await Promise.resolve(1), + () => { + b = a + 1; + c = b + 1; + } + ]); + + $$renderer.push(``); + $$renderer.async([$$promises[1]], ($$renderer) => $$renderer.push(() => $.escape(c))); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/index.svelte b/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/index.svelte new file mode 100644 index 0000000000..98f602312e --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/async-top-level-group-sync-run/index.svelte @@ -0,0 +1,9 @@ + + +{c} From 8e4de9b14500e195ec7f084c8c32b1782b31c5c9 Mon Sep 17 00:00:00 2001 From: Nitin Sahu Date: Sat, 21 Mar 2026 18:03:45 +0530 Subject: [PATCH 03/13] fix: null out effect.b in destroy_effect to prevent memory leak (#17980) ## Fix: #17881 ### Root Cause When a dynamic component switches (e.g. ``), branch effects are destroyed via `destroy_effect()`. However, the `effect.b` (Boundary reference) field was not cleared alongside other references (`next`, `prev`, `ctx`, `deps`, `fn`, `nodes`, `ac`). As a result, destroyed effects retained a reference to the `Boundary` instance, which holds references to component state and child effects. This prevented destroyed component subtrees from being garbage collected, causing memory usage to grow during repeated component switching. ### Solution Clear the boundary reference during effect destruction. ```diff effect.next = effect.prev = effect.teardown = effect.ctx = effect.deps = effect.fn = effect.nodes = effect.ac = + effect.b = null; ``` ### Test Plan * All existing tests pass: * runtime-runes: 2,459 * runtime-legacy: 3,294 * signals: 96 * Total: 5,849 tests * Added a dynamic component switching test that toggles components repeatedly and verifies correct DOM output. ### Validation Reproduced the issue using the example from #17881. After applying the fix: * Memory usage stabilizes during repeated component switching * Destroyed components are properly reclaimed by the garbage collector * No behavioral regressions observed ### Impact * Fixes memory leak in dynamic component switching * Minimal, safe change * No API or behavior changes --------- Co-authored-by: Rich Harris --- .changeset/fix-memory-leak-boundary.md | 5 +++++ packages/svelte/src/internal/client/reactivity/effects.js | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/fix-memory-leak-boundary.md diff --git a/.changeset/fix-memory-leak-boundary.md b/.changeset/fix-memory-leak-boundary.md new file mode 100644 index 0000000000..8a3b084981 --- /dev/null +++ b/.changeset/fix-memory-leak-boundary.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: null out `effect.b` in `destroy_effect` diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index aeffeedddd..54c8a17d79 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -559,6 +559,7 @@ export function destroy_effect(effect, remove_dom = true) { effect.fn = effect.nodes = effect.ac = + effect.b = null; } From 7123bf3a131a53b1ba9414f0d44150a94d0132e3 Mon Sep 17 00:00:00 2001 From: Abishek Raj R R Date: Sat, 21 Mar 2026 18:04:31 +0530 Subject: [PATCH 04/13] fix: remove trailing semicolon from {@const} tag printer (#17962) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #17720 ## Problem The `{@const}` tag was being printed with a trailing semicolon, producing invalid Svelte syntax like: {@const a = 1;} This happened because the `ConstTag` visitor in the printer was delegating to esrap's `VariableDeclaration` handler, which always appends a semicolon (correct for JS, but wrong for Svelte template syntax). ## Solution Instead of delegating to `VariableDeclaration`, the `ConstTag` visitor now manually prints the tag by: - Writing `{@const ` directly - Iterating through declarators and visiting each one - Separating multiple declarations with commas - Closing with `}` — no trailing semicolon ## Before {@const a = 1;} {@const a = 1, b = 2;} ## After {@const a = 1} {@const a = 1, b = 2} ## Changes - `packages/svelte/src/compiler/print/index.js` — fixed `ConstTag` visitor - `packages/svelte/tests/print/samples/const-tag/output.svelte` — updated expected test output --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: Rich Harris Co-authored-by: Rich Harris --- .changeset/two-onions-end.md | 5 +++++ packages/svelte/src/compiler/print/index.js | 9 +++++++-- .../svelte/tests/print/samples/const-tag/output.svelte | 2 +- 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 .changeset/two-onions-end.md diff --git a/.changeset/two-onions-end.md b/.changeset/two-onions-end.md new file mode 100644 index 0000000000..9e6a56bbf9 --- /dev/null +++ b/.changeset/two-onions-end.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: remove trailing semicolon from {@const} tag printer diff --git a/packages/svelte/src/compiler/print/index.js b/packages/svelte/src/compiler/print/index.js index 160aeb4345..26dc1b88e8 100644 --- a/packages/svelte/src/compiler/print/index.js +++ b/packages/svelte/src/compiler/print/index.js @@ -592,8 +592,13 @@ const svelte_visitors = (comments) => ({ }, ConstTag(node, context) { - context.write('{@'); - context.visit(node.declaration); + context.write('{@const '); + const declarators = node.declaration.declarations; + for (let i = 0; i < declarators.length; i++) { + if (i > 0) context.write(', '); + context.visit(declarators[i]); + } + context.write('}'); }, diff --git a/packages/svelte/tests/print/samples/const-tag/output.svelte b/packages/svelte/tests/print/samples/const-tag/output.svelte index d9d8addcbb..dded998a84 100644 --- a/packages/svelte/tests/print/samples/const-tag/output.svelte +++ b/packages/svelte/tests/print/samples/const-tag/output.svelte @@ -3,6 +3,6 @@ {#each boxes as box} - {@const area = box.width * box.height;} + {@const area = box.width * box.height} {box.width} * {box.height} = {area} {/each} From db69b7e345090edd4aa7949c52478adc05a61d61 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 21 Mar 2026 09:29:56 -0400 Subject: [PATCH 05/13] Version Packages (#17965) This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## svelte@5.54.1 ### Patch Changes - fix: hydration comments during hmr ([#17975](https://github.com/sveltejs/svelte/pull/17975)) - fix: null out `effect.b` in `destroy_effect` ([#17980](https://github.com/sveltejs/svelte/pull/17980)) - fix: group sync statements ([#17977](https://github.com/sveltejs/svelte/pull/17977)) - fix: defer batch resolution until earlier intersecting batches have committed ([#17162](https://github.com/sveltejs/svelte/pull/17162)) - fix: properly invoke `iterator.return()` during reactivity loss check ([#17966](https://github.com/sveltejs/svelte/pull/17966)) - fix: remove trailing semicolon from {@const} tag printer ([#17962](https://github.com/sveltejs/svelte/pull/17962)) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/calm-mice-allow.md | 5 ----- .changeset/fix-memory-leak-boundary.md | 5 ----- .changeset/puny-masks-run.md | 5 ----- .changeset/spicy-teeth-tan.md | 5 ----- .changeset/tasty-carrots-tie.md | 5 ----- .changeset/two-onions-end.md | 5 ----- packages/svelte/CHANGELOG.md | 16 ++++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 9 files changed, 18 insertions(+), 32 deletions(-) delete mode 100644 .changeset/calm-mice-allow.md delete mode 100644 .changeset/fix-memory-leak-boundary.md delete mode 100644 .changeset/puny-masks-run.md delete mode 100644 .changeset/spicy-teeth-tan.md delete mode 100644 .changeset/tasty-carrots-tie.md delete mode 100644 .changeset/two-onions-end.md diff --git a/.changeset/calm-mice-allow.md b/.changeset/calm-mice-allow.md deleted file mode 100644 index eff71e1082..0000000000 --- a/.changeset/calm-mice-allow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: hydration comments during hmr diff --git a/.changeset/fix-memory-leak-boundary.md b/.changeset/fix-memory-leak-boundary.md deleted file mode 100644 index 8a3b084981..0000000000 --- a/.changeset/fix-memory-leak-boundary.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: null out `effect.b` in `destroy_effect` diff --git a/.changeset/puny-masks-run.md b/.changeset/puny-masks-run.md deleted file mode 100644 index 162abe4ae7..0000000000 --- a/.changeset/puny-masks-run.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: group sync statements diff --git a/.changeset/spicy-teeth-tan.md b/.changeset/spicy-teeth-tan.md deleted file mode 100644 index c7efa65130..0000000000 --- a/.changeset/spicy-teeth-tan.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: defer batch resolution until earlier intersecting batches have committed diff --git a/.changeset/tasty-carrots-tie.md b/.changeset/tasty-carrots-tie.md deleted file mode 100644 index 5d6cb8025d..0000000000 --- a/.changeset/tasty-carrots-tie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: properly invoke `iterator.return()` during reactivity loss check diff --git a/.changeset/two-onions-end.md b/.changeset/two-onions-end.md deleted file mode 100644 index 9e6a56bbf9..0000000000 --- a/.changeset/two-onions-end.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"svelte": patch ---- - -fix: remove trailing semicolon from {@const} tag printer diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 7c1311ae84..16cbf92ea4 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,21 @@ # svelte +## 5.54.1 + +### Patch Changes + +- fix: hydration comments during hmr ([#17975](https://github.com/sveltejs/svelte/pull/17975)) + +- fix: null out `effect.b` in `destroy_effect` ([#17980](https://github.com/sveltejs/svelte/pull/17980)) + +- fix: group sync statements ([#17977](https://github.com/sveltejs/svelte/pull/17977)) + +- fix: defer batch resolution until earlier intersecting batches have committed ([#17162](https://github.com/sveltejs/svelte/pull/17162)) + +- fix: properly invoke `iterator.return()` during reactivity loss check ([#17966](https://github.com/sveltejs/svelte/pull/17966)) + +- fix: remove trailing semicolon from {@const} tag printer ([#17962](https://github.com/sveltejs/svelte/pull/17962)) + ## 5.54.0 ### Minor Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 270d02d621..afbdfa956c 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.54.0", + "version": "5.54.1", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 191d53016a..7de1242e62 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.54.0'; +export const VERSION = '5.54.1'; export const PUBLIC_VERSION = '5'; From 0c669d9bc98f7a7dfc5da666deaf4f672e07c847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=9D=A8=E5=B8=86?= <39647285+leno23@users.noreply.github.com> Date: Mon, 23 Mar 2026 01:09:14 +0800 Subject: [PATCH 06/13] docs: recommend createContext as the primary method for context (#17959) ### Before submitting the PR, please make sure you do the following - [ ] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [ ] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`. - [ ] This message body should clearly illustrate what problems it solves. - [ ] Ideally, include a test that fails without this PR but passes with it. - [ ] If this PR changes code within `packages/svelte/src`, add a changeset (`npx changeset`). ### Tests and linting - [ ] Run the tests with `pnpm test` and lint the project with `pnpm lint` --------- Co-authored-by: Rich Harris --- documentation/docs/06-runtime/02-context.md | 124 +++++++++++++++----- 1 file changed, 96 insertions(+), 28 deletions(-) diff --git a/documentation/docs/06-runtime/02-context.md b/documentation/docs/06-runtime/02-context.md index 61b203f93e..86009c9a20 100644 --- a/documentation/docs/06-runtime/02-context.md +++ b/documentation/docs/06-runtime/02-context.md @@ -2,7 +2,66 @@ title: Context --- -Context allows components to access values owned by parent components without passing them down as props (potentially through many layers of intermediate components, known as 'prop-drilling'). The parent component sets context with `setContext(key, value)`... +Context allows components to access values owned by parent components without passing them down as props (potentially through many layers of intermediate components, known as 'prop-drilling'). + +By creating a `[get, set]` pair of functions with `createContext`, you can set the context in a parent component and get it in a child component: + + +```svelte + + + + + + +``` + +```svelte + + + +{@render children()} +``` + +```svelte + + + +

hello {user.name}, inside Child.svelte

+``` + +```ts +/// file: context.ts +import { createContext } from 'svelte'; + +interface User { + name: string; +} + +export const [getUserContext, setUserContext] = createContext(); +``` + + +> [!NOTE] `createContext` was added in version 5.40. If you are using an earlier version of Svelte, you must use `setContext` and `getContext` instead. + +This is particularly useful when `Parent.svelte` is not directly aware of `Child.svelte`, but instead renders it as part of a `children` [snippet](snippet) as shown above. + +## `setContext` and `getContext` + +As an alternative to `createContext`, you can use `setContext` and `getContext` directly. The parent component sets context with `setContext(key, value)`... ```svelte @@ -26,32 +85,28 @@ Context allows components to access values owned by parent components without pa

{message}, inside Child.svelte

``` -This is particularly useful when `Parent.svelte` is not directly aware of `Child.svelte`, but instead renders it as part of a `children` [snippet](snippet) ([demo](/playground/untitled#H4sIAAAAAAAAE42Q3W6DMAyFX8WyJgESK-oto6hTX2D3YxcM3IIUQpR40yqUd58CrCXsp7tL7HNsf2dAWXaEKR56yfTBGOOxFWQwfR6Qz8q1XAHjL-GjUhvzToJd7bU09FO9ctMkG0wxM5VuFeeFLLjtVK8ZnkpNkuGo-w6CTTJ9Z3PwsBAemlbUF934W8iy5DpaZtOUcU02-ZLcaS51jHEkTFm_kY1_wfOO8QnXrb8hBzDEc6pgZ4gFoyz4KgiD7nxfTe8ghqAhIfrJ46cTzVZBbkPlODVJsLCDO6V7ZcJoncyw1yRr0hd1GNn_ZbEM3I9i1bmVxOlWElUvDUNHxpQngt3C4CXzjS1rtvkw22wMrTRtTbC8Lkuabe7jvthPPe3DofYCAAA=)): - -```svelte - - - -``` - The key (`'my-context'`, in the example above) and the context itself can be any JavaScript value. +> [!NOTE] `createContext` is preferred since it provides better type safety and makes it unnecessary to use keys. + In addition to [`setContext`](svelte#setContext) and [`getContext`](svelte#getContext), Svelte exposes [`hasContext`](svelte#hasContext) and [`getAllContexts`](svelte#getAllContexts) functions. ## Using context with state -You can store reactive state in context ([demo](/playground/untitled#H4sIAAAAAAAAE41R0W6DMAz8FSuaBNUQdK8MkKZ-wh7HHihzu6hgosRMm1D-fUpSVNq12x4iEvvOx_kmQU2PIhfP3DCCJGgHYvxkkYid7NCI_GUS_KUcxhVEMjOelErNB3bsatvG4LW6n0ZsRC4K02qpuKqpZtmrQTNMYJA3QRAs7PTQQxS40eMCt3mX3duxnWb-lS5h7nTI0A4jMWoo4c44P_Hku-zrOazdy64chWo-ScfRkRgl8wgHKrLTH1OxHZkHgoHaTraHcopXUFYzPPVfuC_hwQaD1GrskdiNCdQwJljJqlvXfyqVsA5CGg0uRUQifHw56xFtciO75QrP07vo_JXf_tf8yK2ezDKY_ZWt_1y2qqYzv7bI1IW1V_sN19m-07wCAAA=))... +You can store reactive state in context... + ```svelte + +``` + +```svelte + + + +

{counter.count}

+``` + +```ts +/// file: context.ts +import { createContext } from 'svelte'; + +interface Counter { + count: number; +} + +export const [getCounter, setCounter] = createContext(); ``` + ...though note that if you _reassign_ `counter` instead of updating it, you will 'break the link' — in other words instead of this... ```svelte - ``` @@ -81,21 +163,7 @@ You can store reactive state in context ([demo](/playground/untitled#H4sIAAAAAAA Svelte will warn you if you get it wrong. -## Type-safe context - -As an alternative to using `setContext` and `getContext` directly, you can use them via `createContext`. This gives you type safety and makes it unnecessary to use a key: - -```ts -/// file: context.ts -// @filename: ambient.d.ts -interface User {} - -// @filename: index.ts -// ---cut--- -import { createContext } from 'svelte'; - -export const [getUserContext, setUserContext] = createContext(); -``` +## Component testing When writing [component tests](testing#Unit-and-component-tests-with-Vitest-Component-testing), it can be useful to create a wrapper component that sets the context in order to check the behaviour of a component that uses it. As of version 5.49, you can do this sort of thing: From a94924b67721e8b3151ebf1622691b1902c9bf85 Mon Sep 17 00:00:00 2001 From: moaaz <145723871+moaaz-ae@users.noreply.github.com> Date: Sun, 22 Mar 2026 19:29:52 +0200 Subject: [PATCH 07/13] fix: export TweenOptions, SpringOptions, SpringUpdateOptions and Updater from svelte/motion (#17967) Exports `TweenedOptions`, `SpringOpts`, `SpringUpdateOpts`, and `Updater` from `svelte/motion`. These types are required for the public method signatures of `spring` and `tweened` (e.g., as parameters for `.set()` and `.update()`). This PR makes them accessible to TypeScript users, following the established pattern in modules like `svelte/store` and `svelte/transition`. Internal implementation details like `TickContext` remain private as they do not appear in any public-facing signatures. Fixes #16151 ### Before submitting the PR, please make sure you do the following - [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`. - [x] This message body should clearly illustrate what problems it solves. - [x] Ideally, include a test that fails without this PR but passes with it. - [x] If this PR changes code within `packages/svelte/src`, add a changeset (`npx changeset`). ### Tests and linting - [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` --------- Co-authored-by: Rich Harris Co-authored-by: Rich Harris --- .changeset/angry-knives-bow.md | 5 ++ packages/svelte/src/motion/private.d.ts | 34 --------- packages/svelte/src/motion/public.d.ts | 49 ++++++++++--- packages/svelte/src/motion/spring.js | 14 ++-- packages/svelte/src/motion/tweened.js | 15 ++-- packages/svelte/tests/types/motion.ts | 22 ++++++ packages/svelte/types/index.d.ts | 91 +++++++++++++------------ 7 files changed, 128 insertions(+), 102 deletions(-) create mode 100644 .changeset/angry-knives-bow.md create mode 100644 packages/svelte/tests/types/motion.ts diff --git a/.changeset/angry-knives-bow.md b/.changeset/angry-knives-bow.md new file mode 100644 index 0000000000..c96b397886 --- /dev/null +++ b/.changeset/angry-knives-bow.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: export TweenOptions, SpringOptions, SpringUpdateOptions and Updater from svelte/motion diff --git a/packages/svelte/src/motion/private.d.ts b/packages/svelte/src/motion/private.d.ts index 22b8cc4af3..a6e0f95284 100644 --- a/packages/svelte/src/motion/private.d.ts +++ b/packages/svelte/src/motion/private.d.ts @@ -8,37 +8,3 @@ export interface TickContext { }; settled: boolean; } - -export interface SpringOpts { - stiffness?: number; - damping?: number; - precision?: number; -} - -export interface SpringUpdateOpts { - /** - * @deprecated Only use this for the spring store; does nothing when set on the Spring class - */ - hard?: any; - /** - * @deprecated Only use this for the spring store; does nothing when set on the Spring class - */ - soft?: string | number | boolean; - /** - * Only use this for the Spring class; does nothing when set on the spring store - */ - instant?: boolean; - /** - * Only use this for the Spring class; does nothing when set on the spring store - */ - preserveMomentum?: number; -} - -export type Updater = (target_value: T, value: T) => T; - -export interface TweenedOptions { - delay?: number; - duration?: number | ((from: T, to: T) => number); - easing?: (t: number) => number; - interpolate?: (a: T, b: T) => (t: number) => T; -} diff --git a/packages/svelte/src/motion/public.d.ts b/packages/svelte/src/motion/public.d.ts index 4e74d4b76f..89145d0383 100644 --- a/packages/svelte/src/motion/public.d.ts +++ b/packages/svelte/src/motion/public.d.ts @@ -1,16 +1,49 @@ import { Readable, type Unsubscriber } from '../store/public.js'; -import { SpringUpdateOpts, TweenedOptions, Updater, SpringOpts } from './private.js'; + +export interface SpringOptions { + stiffness?: number; + damping?: number; + precision?: number; +} + +export interface SpringUpdateOptions { + /** + * @deprecated Only use this for the spring store; does nothing when set on the Spring class + */ + hard?: any; + /** + * @deprecated Only use this for the spring store; does nothing when set on the Spring class + */ + soft?: string | number | boolean; + /** + * Only use this for the Spring class; does nothing when set on the spring store + */ + instant?: boolean; + /** + * Only use this for the Spring class; does nothing when set on the spring store + */ + preserveMomentum?: number; +} + +export type Updater = (target_value: T, value: T) => T; + +export interface TweenOptions { + delay?: number; + duration?: number | ((from: T, to: T) => number); + easing?: (t: number) => number; + interpolate?: (a: T, b: T) => (t: number) => T; +} // TODO we do declaration merging here in order to not have a breaking change (renaming the Spring interface) // this means both the Spring class and the Spring interface are merged into one with some things only // existing on one side. In Svelte 6, remove the type definition and move the jsdoc onto the class in spring.js export interface Spring extends Readable { - set(new_value: T, opts?: SpringUpdateOpts): Promise; + set(new_value: T, opts?: SpringUpdateOptions): Promise; /** * @deprecated Only exists on the legacy `spring` store, not the `Spring` class */ - update: (fn: Updater, opts?: SpringUpdateOpts) => Promise; + update: (fn: Updater, opts?: SpringUpdateOptions) => Promise; /** * @deprecated Only exists on the legacy `spring` store, not the `Spring` class */ @@ -37,7 +70,7 @@ export interface Spring extends Readable { * @since 5.8.0 */ export class Spring { - constructor(value: T, options?: SpringOpts); + constructor(value: T, options?: SpringOptions); /** * Create a spring whose value is bound to the return value of `fn`. This must be called @@ -53,7 +86,7 @@ export class Spring { * * ``` */ - static of(fn: () => U, options?: SpringOpts): Spring; + static of(fn: () => U, options?: SpringOptions): Spring; /** * Sets `spring.target` to `value` and returns a `Promise` that resolves if and when `spring.current` catches up to it. @@ -63,7 +96,7 @@ export class Spring { * If `options.preserveMomentum` is provided, the spring will continue on its current trajectory for * the specified number of milliseconds. This is useful for things like 'fling' gestures. */ - set(value: T, options?: SpringUpdateOpts): Promise; + set(value: T, options?: SpringUpdateOptions): Promise; damping: number; precision: number; @@ -81,8 +114,8 @@ export class Spring { } export interface Tweened extends Readable { - set(value: T, opts?: TweenedOptions): Promise; - update(updater: Updater, opts?: TweenedOptions): Promise; + set(value: T, opts?: TweenOptions): Promise; + update(updater: Updater, opts?: TweenOptions): Promise; } export { prefersReducedMotion, spring, tweened, Tween } from './index.js'; diff --git a/packages/svelte/src/motion/spring.js b/packages/svelte/src/motion/spring.js index 44be1a501b..7198d60127 100644 --- a/packages/svelte/src/motion/spring.js +++ b/packages/svelte/src/motion/spring.js @@ -1,6 +1,6 @@ /** @import { Task } from '#client' */ -/** @import { SpringOpts, SpringUpdateOpts, TickContext } from './private.js' */ -/** @import { Spring as SpringStore } from './public.js' */ +/** @import { TickContext } from './private.js' */ +/** @import { Spring as SpringStore, SpringOptions, SpringUpdateOptions } from './public.js' */ import { writable } from '../store/shared/index.js'; import { loop } from '../internal/client/loop.js'; import { raf } from '../internal/client/timing.js'; @@ -62,7 +62,7 @@ function tick_spring(ctx, last_value, current_value, target_value) { * @deprecated Use [`Spring`](https://svelte.dev/docs/svelte/svelte-motion#Spring) instead * @template [T=any] * @param {T} [value] - * @param {SpringOpts} [opts] + * @param {SpringOptions} [opts] * @returns {SpringStore} */ export function spring(value, opts = {}) { @@ -83,7 +83,7 @@ export function spring(value, opts = {}) { let cancel_task = false; /** * @param {T} new_value - * @param {SpringUpdateOpts} opts + * @param {SpringUpdateOptions} opts * @returns {Promise} */ function set(new_value, opts = {}) { @@ -191,7 +191,7 @@ export class Spring { /** * @param {T} value - * @param {SpringOpts} [options] + * @param {SpringOptions} [options] */ constructor(value, options = {}) { this.#current = DEV ? tag(state(value), 'Spring.current') : state(value); @@ -225,7 +225,7 @@ export class Spring { * ``` * @template U * @param {() => U} fn - * @param {SpringOpts} [options] + * @param {SpringOptions} [options] */ static of(fn, options) { const spring = new Spring(fn(), options); @@ -293,7 +293,7 @@ export class Spring { * the specified number of milliseconds. This is useful for things like 'fling' gestures. * * @param {T} value - * @param {SpringUpdateOpts} [options] + * @param {SpringUpdateOptions} [options] */ set(value, options) { this.#deferred?.reject(new Error('Aborted')); diff --git a/packages/svelte/src/motion/tweened.js b/packages/svelte/src/motion/tweened.js index 437c22ec3b..a24148d075 100644 --- a/packages/svelte/src/motion/tweened.js +++ b/packages/svelte/src/motion/tweened.js @@ -1,6 +1,5 @@ /** @import { Task } from '../internal/client/types' */ -/** @import { Tweened } from './public' */ -/** @import { TweenedOptions } from './private' */ +/** @import { Tweened, TweenOptions } from './public' */ import { writable } from '../store/shared/index.js'; import { raf } from '../internal/client/timing.js'; import { loop } from '../internal/client/loop.js'; @@ -84,7 +83,7 @@ function get_interpolator(a, b) { * @deprecated Use [`Tween`](https://svelte.dev/docs/svelte/svelte-motion#Tween) instead * @template T * @param {T} [value] - * @param {TweenedOptions} [defaults] + * @param {TweenOptions} [defaults] * @returns {Tweened} */ export function tweened(value, defaults = {}) { @@ -94,7 +93,7 @@ export function tweened(value, defaults = {}) { let target_value = value; /** * @param {T} new_value - * @param {TweenedOptions} [opts] + * @param {TweenOptions} [opts] */ function set(new_value, opts) { target_value = new_value; @@ -180,7 +179,7 @@ export class Tween { #current; #target; - /** @type {TweenedOptions} */ + /** @type {TweenOptions} */ #defaults; /** @type {import('../internal/client/types').Task | null} */ @@ -188,7 +187,7 @@ export class Tween { /** * @param {T} value - * @param {TweenedOptions} options + * @param {TweenOptions} options */ constructor(value, options = {}) { this.#current = state(value); @@ -216,7 +215,7 @@ export class Tween { * ``` * @template U * @param {() => U} fn - * @param {TweenedOptions} [options] + * @param {TweenOptions} [options] */ static of(fn, options) { const tween = new Tween(fn(), options); @@ -233,7 +232,7 @@ export class Tween { * * If `options` are provided, they will override the tween's defaults. * @param {T} value - * @param {TweenedOptions} [options] + * @param {TweenOptions} [options] * @returns */ set(value, options) { diff --git a/packages/svelte/tests/types/motion.ts b/packages/svelte/tests/types/motion.ts new file mode 100644 index 0000000000..0ba5d1d9c6 --- /dev/null +++ b/packages/svelte/tests/types/motion.ts @@ -0,0 +1,22 @@ +import { + type TweenOptions, + type SpringOptions, + type SpringUpdateOptions, + type Updater +} from 'svelte/motion'; + +let tweenOptions: TweenOptions = { + delay: 100, + duration: 400 +}; + +let springOptions: SpringOptions = { + stiffness: 0.1, + damping: 0.5 +}; + +let springUpdateOptions: SpringUpdateOptions = { + instant: true +}; + +let updater: Updater = (target, value) => target + value; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 371a823225..019baf45dd 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2005,16 +2005,50 @@ declare module 'svelte/legacy' { declare module 'svelte/motion' { import type { MediaQuery } from 'svelte/reactivity'; + export interface SpringOptions { + stiffness?: number; + damping?: number; + precision?: number; + } + + export interface SpringUpdateOptions { + /** + * @deprecated Only use this for the spring store; does nothing when set on the Spring class + */ + hard?: any; + /** + * @deprecated Only use this for the spring store; does nothing when set on the Spring class + */ + soft?: string | number | boolean; + /** + * Only use this for the Spring class; does nothing when set on the spring store + */ + instant?: boolean; + /** + * Only use this for the Spring class; does nothing when set on the spring store + */ + preserveMomentum?: number; + } + + export type Updater = (target_value: T, value: T) => T; + + export interface TweenOptions { + delay?: number; + duration?: number | ((from: T, to: T) => number); + easing?: (t: number) => number; + interpolate?: (a: T, b: T) => (t: number) => T; + } + // TODO we do declaration merging here in order to not have a breaking change (renaming the Spring interface) // this means both the Spring class and the Spring interface are merged into one with some things only // existing on one side. In Svelte 6, remove the type definition and move the jsdoc onto the class in spring.js export interface Spring extends Readable { - set(new_value: T, opts?: SpringUpdateOpts): Promise; + set(new_value: T, opts?: SpringUpdateOptions): Promise; /** * @deprecated Only exists on the legacy `spring` store, not the `Spring` class */ - update: (fn: Updater, opts?: SpringUpdateOpts) => Promise; + update: (fn: Updater, opts?: SpringUpdateOptions) => Promise; /** * @deprecated Only exists on the legacy `spring` store, not the `Spring` class */ @@ -2041,7 +2075,7 @@ declare module 'svelte/motion' { * @since 5.8.0 */ export class Spring { - constructor(value: T, options?: SpringOpts); + constructor(value: T, options?: SpringOptions); /** * Create a spring whose value is bound to the return value of `fn`. This must be called @@ -2057,7 +2091,7 @@ declare module 'svelte/motion' { * * ``` */ - static of(fn: () => U, options?: SpringOpts): Spring; + static of(fn: () => U, options?: SpringOptions): Spring; /** * Sets `spring.target` to `value` and returns a `Promise` that resolves if and when `spring.current` catches up to it. @@ -2067,7 +2101,7 @@ declare module 'svelte/motion' { * If `options.preserveMomentum` is provided, the spring will continue on its current trajectory for * the specified number of milliseconds. This is useful for things like 'fling' gestures. */ - set(value: T, options?: SpringUpdateOpts): Promise; + set(value: T, options?: SpringUpdateOptions): Promise; damping: number; precision: number; @@ -2085,8 +2119,8 @@ declare module 'svelte/motion' { } export interface Tweened extends Readable { - set(value: T, opts?: TweenedOptions): Promise; - update(updater: Updater, opts?: TweenedOptions): Promise; + set(value: T, opts?: TweenOptions): Promise; + update(updater: Updater, opts?: TweenOptions): Promise; } /** Callback to inform of a value updates. */ type Subscriber = (value: T) => void; @@ -2103,39 +2137,6 @@ declare module 'svelte/motion' { */ subscribe(this: void, run: Subscriber, invalidate?: () => void): Unsubscriber; } - interface SpringOpts { - stiffness?: number; - damping?: number; - precision?: number; - } - - interface SpringUpdateOpts { - /** - * @deprecated Only use this for the spring store; does nothing when set on the Spring class - */ - hard?: any; - /** - * @deprecated Only use this for the spring store; does nothing when set on the Spring class - */ - soft?: string | number | boolean; - /** - * Only use this for the Spring class; does nothing when set on the spring store - */ - instant?: boolean; - /** - * Only use this for the Spring class; does nothing when set on the spring store - */ - preserveMomentum?: number; - } - - type Updater = (target_value: T, value: T) => T; - - interface TweenedOptions { - delay?: number; - duration?: number | ((from: T, to: T) => number); - easing?: (t: number) => number; - interpolate?: (a: T, b: T) => (t: number) => T; - } /** * A [media query](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery) that matches if the user [prefers reduced motion](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion). * @@ -2165,13 +2166,13 @@ declare module 'svelte/motion' { * * @deprecated Use [`Spring`](https://svelte.dev/docs/svelte/svelte-motion#Spring) instead * */ - export function spring(value?: T | undefined, opts?: SpringOpts | undefined): Spring; + export function spring(value?: T | undefined, opts?: SpringOptions | undefined): Spring; /** * A tweened store in Svelte is a special type of store that provides smooth transitions between state values over time. * * @deprecated Use [`Tween`](https://svelte.dev/docs/svelte/svelte-motion#Tween) instead * */ - export function tweened(value?: T | undefined, defaults?: TweenedOptions | undefined): Tweened; + export function tweened(value?: T | undefined, defaults?: TweenOptions | undefined): Tweened; /** * A wrapper for a value that tweens smoothly to its target value. Changes to `tween.target` will cause `tween.current` to * move towards it over time, taking account of the `delay`, `duration` and `easing` options. @@ -2204,15 +2205,15 @@ declare module 'svelte/motion' { * ``` * */ - static of(fn: () => U, options?: TweenedOptions | undefined): Tween; + static of(fn: () => U, options?: TweenOptions | undefined): Tween; - constructor(value: T, options?: TweenedOptions); + constructor(value: T, options?: TweenOptions); /** * Sets `tween.target` to `value` and returns a `Promise` that resolves if and when `tween.current` catches up to it. * * If `options` are provided, they will override the tween's defaults. * */ - set(value: T, options?: TweenedOptions | undefined): Promise; + set(value: T, options?: TweenOptions | undefined): Promise; get current(): T; set target(v: T); get target(): T; From b90a5dfb61545a547f7c5891fc23052c5ff251ea Mon Sep 17 00:00:00 2001 From: Razin Shafayet Date: Mon, 23 Mar 2026 00:29:08 +0600 Subject: [PATCH 08/13] docs: clarify `$derived` `await` behavior and dependency tracking (#17696) Closes #17660 ### Before submitting the PR, please make sure you do the following - [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`. - [x] This message body should clearly illustrate what problems it solves. - [ ] Ideally, include a test that fails without this PR but passes with it. - [ ] If this PR changes code within `packages/svelte/src`, add a changeset (`npx changeset`). ### Tests and linting - [ ] Run the tests with `pnpm test` and lint the project with `pnpm lint` --------- Co-authored-by: Rich Harris --- documentation/docs/02-runes/03-$derived.md | 11 ++++++++++ .../19-await-expressions.md | 21 ++++++++++++------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/documentation/docs/02-runes/03-$derived.md b/documentation/docs/02-runes/03-$derived.md index 35cb6c1912..f85ba90baa 100644 --- a/documentation/docs/02-runes/03-$derived.md +++ b/documentation/docs/02-runes/03-$derived.md @@ -51,6 +51,17 @@ In essence, `$derived(expression)` is equivalent to `$derived.by(() => expressio Anything read synchronously inside the `$derived` expression (or `$derived.by` function body) is considered a _dependency_ of the derived state. When the state changes, the derived will be marked as _dirty_ and recalculated when it is next read. +In addition, if an expression contains an [`await`](await-expressions), Svelte transforms it such that any state _after_ the `await` is also tracked — in other words, in a case like this... + +```js +let a = Promise.resolve(1); +let b = 2; +// ---cut--- +let total = $derived(await a + b); +``` + +...both `a` and `b` are tracked, even though `b` is only read once `a` has resolved, after the initial execution. (This does not apply to `await` in functions that are called by the expression, only the expression itself.) + To exempt a piece of state from being treated as a dependency, use [`untrack`](svelte#untrack). ## Overriding derived values diff --git a/documentation/docs/03-template-syntax/19-await-expressions.md b/documentation/docs/03-template-syntax/19-await-expressions.md index 2f73f6a47c..aa3f907b14 100644 --- a/documentation/docs/03-template-syntax/19-await-expressions.md +++ b/documentation/docs/03-template-syntax/19-await-expressions.md @@ -59,8 +59,8 @@ Updates can overlap — a fast update will be reflected in the UI while an earli Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup... ```svelte -

{await one()}

-

{await two()}

+

{await one(x)}

+

{await two(y)}

``` ...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential. @@ -68,13 +68,18 @@ Svelte will do as much asynchronous work as it can in parallel. For example if y This does not apply to sequential `await` expressions inside your ` + +a {await delay(a)} | b {await delay(b)} | c {c} | d {d} + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-2/_config.js new file mode 100644 index 0000000000..f4aff83aad --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-2/_config.js @@ -0,0 +1,82 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971 + async test({ assert, target }) { + await tick(); + const [a_b, a_c, b_d, shift, pop] = target.querySelectorAll('button'); + + a_b.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + a_c.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + b_d.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + pop.click(); // second b resolved, blocked on first batch because a still pending + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + for (let i = 0; i < 3; i++) { + pop.click(); // second a resolved, first a/b now obsolete; empty queue + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 2 | b 2 | c 1 | d 1 + + + + + + ` + ); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-2/main.svelte new file mode 100644 index 0000000000..25dbe586ca --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-2/main.svelte @@ -0,0 +1,26 @@ + + +a {await delay(a)} | b {await delay(b)} | c {c} | d {d} + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-3/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-3/_config.js new file mode 100644 index 0000000000..c9e7513b22 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-3/_config.js @@ -0,0 +1,108 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971 + async test({ assert, target }) { + await tick(); + const [a_b, a_c, b_d, shift, pop] = target.querySelectorAll('button'); + + a_b.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + a_c.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + b_d.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + shift.click(); // first a resolved, still pending: [b, a, b] + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + pop.click(); // second b resolved, still pending: [b, a] + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + shift.click(); // first b resolved, first + last batch settled, still pending: [a] + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 1 | b 2 | c 0 | d 1 + + + + + + ` + ); + + shift.click(); // all resolved + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 2 | b 2 | c 1 | d 1 + + + + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-3/main.svelte new file mode 100644 index 0000000000..25dbe586ca --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-3/main.svelte @@ -0,0 +1,26 @@ + + +a {await delay(a)} | b {await delay(b)} | c {c} | d {d} + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-4/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-4/_config.js new file mode 100644 index 0000000000..472a3ebcaf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-4/_config.js @@ -0,0 +1,110 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971 + async test({ assert, target }) { + await tick(); + const [a_b, a_c, b_d, shift, pop] = target.querySelectorAll('button'); + + a_b.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + a_c.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + b_d.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + shift.click(); // first a resolved, still pending: [b, a, b] + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + pop.click(); // second b resolved, still pending: [b, a] + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + pop.click(); // second a resolved, first a/b now obsolete + // TODO would be nice to show final result here already, right now it doesn't because + // we have no handle on the already resolved first a anymore + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + + ` + ); + + shift.click(); // queue empty + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 2 | b 2 | c 1 | d 1 + + + + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-4/main.svelte new file mode 100644 index 0000000000..25dbe586ca --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-4/main.svelte @@ -0,0 +1,26 @@ + + +a {await delay(a)} | b {await delay(b)} | c {c} | d {d} + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-5/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-5/_config.js new file mode 100644 index 0000000000..d03f3cfbb8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-5/_config.js @@ -0,0 +1,118 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [a, c, shift, pop] = target.querySelectorAll('button'); + + a.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + c.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + // how it's on main + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 1 | d 1 + + + + + ` + ); + + shift.click(); + await tick(); + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 1 | b 2 | c 1 | d 3 + + + + + ` + ); + + // how it's on https://github.com/sveltejs/svelte/pull/17971 + // shift.click(); + // await tick(); + // assert.htmlEqual( + // target.innerHTML, + // ` + // a 0 | b 0 | c 0 | d 0 + // + // + // + // + // ` + // ); + + // shift.click(); + // await tick(); + // assert.htmlEqual( + // target.innerHTML, + // ` + // a 1 | b 2 | c 0 | d 2 + // + // + // + // + // ` + // ); + + // shift.click(); + // await tick(); + // assert.htmlEqual( + // target.innerHTML, + // ` + // a 1 | b 2 | c 1 | d 3 + // + // + // + // + // ` + // ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-5/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-5/main.svelte new file mode 100644 index 0000000000..cf028718c2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-5/main.svelte @@ -0,0 +1,23 @@ + + +a {a} | b {b} | c {c} | d {d} + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-6/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-6/_config.js new file mode 100644 index 0000000000..27152889a2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-6/_config.js @@ -0,0 +1,76 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [a, c, shift, pop] = target.querySelectorAll('button'); + + a.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + c.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + // Although the second batch is eventually connected to the first one, we can't see that + // at this point yet and so the second one flushes right away. + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 1 | d 1 + + + + + ` + ); + + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 1 | d 1 + + + + + ` + ); + + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 1 | b 2 | c 1 | d 3 + + + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-6/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-6/main.svelte new file mode 100644 index 0000000000..cf028718c2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-6/main.svelte @@ -0,0 +1,23 @@ + + +a {a} | b {b} | c {c} | d {d} + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-7/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-7/_config.js new file mode 100644 index 0000000000..b1bf53a2fc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-7/_config.js @@ -0,0 +1,129 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [a, c, shift, pop] = target.querySelectorAll('button'); + + a.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + c.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + shift.click(); // schedules second step of first batch and schedules rerun of second batch + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 0 | b 0 | c 0 | d 0 + + + + + ` + ); + + // how it's on main + + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 1 | b 2 | c 0 | d 2 + + + + + ` + ); + + shift.click(); // obsolete second batch promise (already rejected) + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 1 | b 2 | c 0 | d 2 + + + + + ` + ); + + shift.click(); // first batch resolves + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + a 1 | b 2 | c 1 | d 3 + + + + + ` + ); + + // how it's on https://github.com/sveltejs/svelte/pull/17971 + // pop.click(); // second batch resolves but knows it needs to wait on first batch + // await tick(); + // assert.htmlEqual( + // target.innerHTML, + // ` + // a 0 | b 0 | c 0 | d 0 + // + // + // + // + // ` + // ); + + // shift.click(); // obsolete second batch promise (already rejected) + // await tick(); + // assert.htmlEqual( + // target.innerHTML, + // ` + // a 0 | b 0 | c 0 | d 0 + // + // + // + // + // ` + // ); + + // shift.click(); // first batch resolves, with it second can now resolve as well + // await tick(); + // assert.htmlEqual( + // target.innerHTML, + // ` + // a 1 | b 2 | c 1 | d 3 + // + // + // + // + // ` + // ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-7/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-7/main.svelte new file mode 100644 index 0000000000..cf028718c2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-7/main.svelte @@ -0,0 +1,23 @@ + + +a {a} | b {b} | c {c} | d {d} + + + + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-1/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-1/_config.js new file mode 100644 index 0000000000..07ce7e340f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-1/_config.js @@ -0,0 +1,34 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [a_b_fork, a_c, shift, pop, commit] = target.querySelectorAll('button'); + const [p] = target.querySelectorAll('p'); + + a_b_fork.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0'); + + a_c.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0'); + + pop.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 0 | c 1'); + + shift.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 0 | c 1'); + + shift.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 0 | c 1'); + + commit.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 1 | c 1'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-1/main.svelte new file mode 100644 index 0000000000..60ba74fb8e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-1/main.svelte @@ -0,0 +1,27 @@ + + +

a {await delay(a)} | b {await delay(b)} | c {c}

+ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-2/_config.js new file mode 100644 index 0000000000..180a190dae --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-2/_config.js @@ -0,0 +1,42 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + await tick(); + const [a_b_fork, a_c, b_d, shift, pop, commit] = target.querySelectorAll('button'); + const [p] = target.querySelectorAll('p'); + + a_b_fork.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0 | d 0'); + + a_c.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0 | d 0'); + + b_d.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0 | d 0'); + + pop.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 1 | c 0 | d 1'); + + pop.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 1 | c 1 | d 1'); + + shift.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 1 | c 1 | d 1'); + + shift.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 1 | c 1 | d 1'); + + commit.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 1 | c 1 | d 1'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-2/main.svelte new file mode 100644 index 0000000000..6d1c4ab418 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-2/main.svelte @@ -0,0 +1,31 @@ + + +

a {await delay(a)} | b {await delay(b)} | c {c} | d {d}

+ + + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-3/_config.js b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-3/_config.js new file mode 100644 index 0000000000..61360d8dc9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-3/_config.js @@ -0,0 +1,43 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, + async test({ assert, target }) { + await tick(); + const [a_b_fork, a_c, b_d, shift, pop, commit] = target.querySelectorAll('button'); + const [p] = target.querySelectorAll('p'); + + a_b_fork.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0 | d 0'); + + a_c.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0 | d 0'); + + b_d.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0 | d 0'); + + shift.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0 | d 0'); + + shift.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 0 | b 0 | c 0 | d 0'); + + shift.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 0 | c 1 | d 0'); + + shift.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 1 | c 1 | d 1'); + + commit.click(); + await tick(); + assert.htmlEqual(p.innerHTML, 'a 1 | b 1 | c 1 | d 1'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-3/main.svelte new file mode 100644 index 0000000000..6d1c4ab418 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-overlap-multiple-fork-3/main.svelte @@ -0,0 +1,31 @@ + + +

a {await delay(a)} | b {await delay(b)} | c {c} | d {d}

+ + + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/Child.svelte new file mode 100644 index 0000000000..6b765526c8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/Child.svelte @@ -0,0 +1,7 @@ + + +{x} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/_config.js new file mode 100644 index 0000000000..0af275009c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/_config.js @@ -0,0 +1,38 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971 + async test({ assert, target, logs }) { + const [x, y, resolve] = target.querySelectorAll('button'); + + x.click(); + await tick(); + + y.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + ` // if this shows world world - that would also be ok + ); + + resolve.click(); + await tick(); + assert.deepEqual(logs, ['universe', 'universe', '$effect: universe', '$effect: universe']); + assert.htmlEqual( + target.innerHTML, + ` + + + + universe + universe + universe + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/main.svelte new file mode 100644 index 0000000000..8edc718de2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-1/main.svelte @@ -0,0 +1,28 @@ + + + + + + + + +{#if x === 'universe'} + {await delay(x)} + +{/if} + +{#if y > 0} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/Child.svelte new file mode 100644 index 0000000000..f8c01e9efd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/Child.svelte @@ -0,0 +1,11 @@ + + + +{x} +{JSON.stringify(x)} +{#if x === 'universe'}universe{:else}world{/if} +{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if} +{await Promise.resolve(x)} +{await Promise.resolve(JSON.stringify(x))} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/_config.js new file mode 100644 index 0000000000..035616dfb6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/_config.js @@ -0,0 +1,49 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971 + async test({ assert, target }) { + const [x, y, resolve] = target.querySelectorAll('button'); + + x.click(); + await tick(); + + y.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +
+ ` // if this shows world world "world" world world world "world" - then this would also be ok + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + universe + universe + "universe" + universe + universe + universe + "universe" +
+ universe + "universe" + universe + universe + universe + "universe" + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/main.svelte new file mode 100644 index 0000000000..11c4bdf5d1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-2/main.svelte @@ -0,0 +1,30 @@ + + + + + + + + +{#if x === 'universe'} + {await delay(x)} + +{/if} + +
+ +{#if y > 0} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/Child.svelte new file mode 100644 index 0000000000..f8c01e9efd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/Child.svelte @@ -0,0 +1,11 @@ + + + +{x} +{JSON.stringify(x)} +{#if x === 'universe'}universe{:else}world{/if} +{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if} +{await Promise.resolve(x)} +{await Promise.resolve(JSON.stringify(x))} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/_config.js new file mode 100644 index 0000000000..a2d615b6e5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/_config.js @@ -0,0 +1,61 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971 + async test({ assert, target }) { + const [x, y, resolve] = target.querySelectorAll('button'); + + x.click(); + await tick(); + + y.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +
+ ` + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + +
+ ` // if this shows world world "world" world world world "world" - then this would also be ok + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + universe + universe + "universe" + universe + universe + universe + "universe" +
+ universe + "universe" + universe + universe + universe + "universe" + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/main.svelte new file mode 100644 index 0000000000..b02ab20995 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-3/main.svelte @@ -0,0 +1,34 @@ + + + + + + + +{#if x === 'universe'} + {await delay(x)} + +{/if} + +
+ +{#if y > 0} + +{/if} + \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-all-combinations/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-all-combinations/Child.svelte new file mode 100644 index 0000000000..f8c01e9efd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-all-combinations/Child.svelte @@ -0,0 +1,11 @@ + + + +{x} +{JSON.stringify(x)} +{#if x === 'universe'}universe{:else}world{/if} +{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if} +{await Promise.resolve(x)} +{await Promise.resolve(JSON.stringify(x))} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-all-combinations/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-all-combinations/_config.js new file mode 100644 index 0000000000..8b7e7784de --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-all-combinations/_config.js @@ -0,0 +1,171 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO more combinations pass on https://github.com/sveltejs/svelte/pull/17971 + timeout: 20_000, + async test({ assert, target }) { + const [x, fork_x, y, fork_y, shift, pop, commit_x, commit_y, reset] = + target.querySelectorAll('button'); + + const initial = ` + + + + + + + + + +
+ `; + + const final = ` + + + + + + + + + + universe + universe + "universe" + universe + universe + universe + "universe" +
+ universe + "universe" + universe + universe + universe + "universe" + `; + + /** @param {HTMLElement} button */ + async function click(button) { + button.click(); + await tick(); + } + + /** + * Generate all permutations of an array. + * @param {HTMLElement[]} actions + * @returns {HTMLElement[][]} + */ + function permutations(actions) { + if (actions.length <= 1) return [actions]; + + /** @type {HTMLElement[][]} */ + const result = []; + + for (let i = 0; i < actions.length; i++) { + const head = actions[i]; + const rest = actions.slice(0, i).concat(actions.slice(i + 1)); + for (const tail of permutations(rest)) { + result.push([head, ...tail]); + } + } + + return result; + } + + /** + * Keep only valid orders where fork commits happen after their fork action. + * @param {HTMLElement[]} order + */ + function is_valid_order(order) { + const x_fork_index = order.indexOf(fork_x); + const commit_x_index = order.indexOf(commit_x); + if (commit_x_index !== -1 && (x_fork_index === -1 || commit_x_index < x_fork_index)) { + return false; + } + + const y_fork_index = order.indexOf(fork_y); + const commit_y_index = order.indexOf(commit_y); + if (commit_y_index !== -1 && (y_fork_index === -1 || commit_y_index < y_fork_index)) { + return false; + } + + return true; + } + + /** + * Four control scenarios: + * - x direct, y direct + * - x direct, y via fork+commit + * - x via fork+commit, y direct + * - x via fork+commit, y via fork+commit + */ + const control_scenarios = [ + [x, y], + [x, fork_y, commit_y], + [fork_x, commit_x, y], + [fork_x, commit_x, fork_y, commit_y] + ]; + + const control_orders = control_scenarios.flatMap((scenario) => + permutations(scenario).filter(is_valid_order) + ); + + /** + * All shift/pop combinations for draining async work. + * We click three times because this scenario can queue up to 3 deferred resolutions. + */ + const resolve_orders = [ + [shift, shift, shift], + [shift, pop, pop], + [pop, shift, shift], + [pop, pop, pop] + ]; + + for (const controls of control_orders) { + for (const resolves of resolve_orders) { + for (const action of controls) { + await click(action); + } + + for (const action of resolves) { + await click(action); + } + + const failure_msg = `Failed for: ${controls + .map((btn) => btn.textContent) + .concat(...resolves.map((btn) => btn.textContent)) + .join(', ')}`; + assert.htmlEqual(target.innerHTML, final, failure_msg); + + await click(reset); + assert.htmlEqual(target.innerHTML, initial, failure_msg); + } + } + + const other_scenarios = [ + [x, shift, y, shift, shift], + [x, shift, y, pop, pop], + [fork_x, shift, y, shift, commit_x, shift], + [fork_x, shift, y, pop, commit_x, pop], + [y, shift, x, shift, shift], + [y, shift, x, pop, pop], + [fork_y, shift, x, shift, commit_y, shift], + [fork_y, shift, x, pop, commit_y, pop] + ]; + + for (const scenario of other_scenarios) { + for (const action of scenario) { + await click(action); + } + + const failure_msg = `Failed for: ${scenario.map((btn) => btn.textContent).join(', ')}`; + assert.htmlEqual(target.innerHTML, final, failure_msg); + + await click(reset); + assert.htmlEqual(target.innerHTML, initial, failure_msg); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-all-combinations/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-all-combinations/main.svelte new file mode 100644 index 0000000000..bb821d9b0f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-all-combinations/main.svelte @@ -0,0 +1,41 @@ + + + + + + + + + + + + +{#if x === 'universe'} + {await delay(x)} + +{/if} + +
+ +{#if y > 0} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-1/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-1/Child.svelte new file mode 100644 index 0000000000..f8c01e9efd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-1/Child.svelte @@ -0,0 +1,11 @@ + + + +{x} +{JSON.stringify(x)} +{#if x === 'universe'}universe{:else}world{/if} +{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if} +{await Promise.resolve(x)} +{await Promise.resolve(JSON.stringify(x))} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-1/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-1/_config.js new file mode 100644 index 0000000000..737169cb91 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-1/_config.js @@ -0,0 +1,90 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971 + async test({ assert, target }) { + const [x, y, shift, pop, commit] = target.querySelectorAll('button'); + + x.click(); + await tick(); + + y.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + +
+ ` + ); + + commit.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + +
+ ` + ); + + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + + universe + universe + "universe" + universe + universe + universe + "universe" +
+ ` + ); + + shift.click(); + await tick(); + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + + universe + universe + "universe" + universe + universe + universe + "universe" +
+ universe + "universe" + universe + universe + universe + "universe" + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-1/main.svelte new file mode 100644 index 0000000000..5f8d5efa13 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-1/main.svelte @@ -0,0 +1,36 @@ + + + + + + + + +{#if x === 'universe'} + {await delay(x)} + +{/if} + +
+ +{#if y > 0} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-2/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-2/Child.svelte new file mode 100644 index 0000000000..f8c01e9efd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-2/Child.svelte @@ -0,0 +1,11 @@ + + + +{x} +{JSON.stringify(x)} +{#if x === 'universe'}universe{:else}world{/if} +{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if} +{await Promise.resolve(x)} +{await Promise.resolve(JSON.stringify(x))} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-2/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-2/_config.js new file mode 100644 index 0000000000..a712e70630 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-2/_config.js @@ -0,0 +1,71 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971 + async test({ assert, target }) { + const [x, y, shift, pop, commit] = target.querySelectorAll('button'); + + y.click(); + await tick(); + + x.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + +
+ ` + ); + + commit.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + +
+ ` + ); + + shift.click(); + await tick(); + shift.click(); + await tick(); + shift.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + + universe + universe + "universe" + universe + universe + universe + "universe" +
+ universe + "universe" + universe + universe + universe + "universe" + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-2/main.svelte new file mode 100644 index 0000000000..5f8d5efa13 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-2/main.svelte @@ -0,0 +1,36 @@ + + + + + + + + +{#if x === 'universe'} + {await delay(x)} + +{/if} + +
+ +{#if y > 0} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-3/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-3/Child.svelte new file mode 100644 index 0000000000..f8c01e9efd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-3/Child.svelte @@ -0,0 +1,11 @@ + + + +{x} +{JSON.stringify(x)} +{#if x === 'universe'}universe{:else}world{/if} +{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if} +{await Promise.resolve(x)} +{await Promise.resolve(JSON.stringify(x))} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-3/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-3/_config.js new file mode 100644 index 0000000000..905e76511b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-3/_config.js @@ -0,0 +1,70 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [x, y, shift, pop, commit] = target.querySelectorAll('button'); + + y.click(); + await tick(); + + x.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + +
+ ` + ); + + commit.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + +
+ ` + ); + + pop.click(); + await tick(); + pop.click(); + await tick(); + pop.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + + universe + universe + "universe" + universe + universe + universe + "universe" +
+ universe + "universe" + universe + universe + universe + "universe" + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-3/main.svelte new file mode 100644 index 0000000000..5f8d5efa13 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-3/main.svelte @@ -0,0 +1,36 @@ + + + + + + + + +{#if x === 'universe'} + {await delay(x)} + +{/if} + +
+ +{#if y > 0} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-4/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-4/Child.svelte new file mode 100644 index 0000000000..f8c01e9efd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-4/Child.svelte @@ -0,0 +1,11 @@ + + + +{x} +{JSON.stringify(x)} +{#if x === 'universe'}universe{:else}world{/if} +{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if} +{await Promise.resolve(x)} +{await Promise.resolve(JSON.stringify(x))} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-4/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-4/_config.js new file mode 100644 index 0000000000..6a6399a19e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-4/_config.js @@ -0,0 +1,76 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971 + async test({ assert, target }) { + const [x, y, resolve, commit] = target.querySelectorAll('button'); + + x.click(); + await tick(); + + y.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + +
+ world + "world" + world + world + world + "world" + ` + ); + + commit.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + +
+ world + "world" + world + world + world + "world" + ` + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + universe + universe + "universe" + universe + universe + universe + "universe" +
+ universe + "universe" + universe + universe + universe + "universe" + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-4/main.svelte new file mode 100644 index 0000000000..5f00fc4dec --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-4/main.svelte @@ -0,0 +1,32 @@ + + + + + + + + +{#if x === 'universe'} + {await delay(x)} + +{/if} + +
+ +{#if y > 0} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-5/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-5/Child.svelte new file mode 100644 index 0000000000..f8c01e9efd --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-5/Child.svelte @@ -0,0 +1,11 @@ + + + +{x} +{JSON.stringify(x)} +{#if x === 'universe'}universe{:else}world{/if} +{#if JSON.stringify(x) === '"universe"'}universe{:else}world{/if} +{await Promise.resolve(x)} +{await Promise.resolve(JSON.stringify(x))} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-5/_config.js b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-5/_config.js new file mode 100644 index 0000000000..9221a96c2e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-5/_config.js @@ -0,0 +1,85 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO works on https://github.com/sveltejs/svelte/pull/17971 + async test({ assert, target }) { + const [x, y, resolve, commit] = target.querySelectorAll('button'); + + x.click(); + await tick(); + + y.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + +
+ ` + ); + + commit.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + +
+ ` + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + +
+ world + "world" + world + world + world + "world" + ` + ); + + resolve.click(); + await tick(); + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + + universe + universe + "universe" + universe + universe + universe + "universe" +
+ universe + "universe" + universe + universe + universe + "universe" + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-5/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-5/main.svelte new file mode 100644 index 0000000000..5575e3cbd4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-state-new-branch-fork-5/main.svelte @@ -0,0 +1,36 @@ + + + + + + + + +{#if x === 'universe'} + {await delay(x)} + +{/if} + +
+ +{#if y > 0} + +{/if} diff --git a/packages/svelte/tests/suite.ts b/packages/svelte/tests/suite.ts index bbd252b8e1..af36f16e29 100644 --- a/packages/svelte/tests/suite.ts +++ b/packages/svelte/tests/suite.ts @@ -4,6 +4,7 @@ import { it } from 'vitest'; export interface BaseTest { skip?: boolean; solo?: boolean; + timeout?: number; } /** @@ -30,7 +31,7 @@ export function suite(fn: (config: Test, test_dir: string await for_each_dir(cwd, samples_dir, (config, dir) => { let it_fn = config.skip ? it.skip : config.solo ? it.only : it; - it_fn(dir, () => fn(config, `${cwd}/${samples_dir}/${dir}`)); + it_fn(dir, { timeout: config.timeout }, () => fn(config, `${cwd}/${samples_dir}/${dir}`)); }); } }; @@ -57,7 +58,7 @@ export function suite_with_variants { + it_fn(`${dir} (${variant})`, { timeout: config.timeout }, async () => { if (!called_common) { called_common = true; common = await common_setup(config, `${cwd}/${samples_dir}/${dir}`); From 54ba176d2c9068f2e6df9249764bb7766ec1b69d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 24 Mar 2026 11:31:17 -0400 Subject: [PATCH 12/13] docs: 'open in playground' links (#17983) This takes advantage of https://github.com/sveltejs/svelte.dev/pull/1879 and https://github.com/sveltejs/svelte.dev/pull/1880 to replace some of the hardcoded playground links in the docs with ones that are generated automatically from the code. This is a better user experience, and easier to maintain. Not every playground link is suitable for replacement, as in many cases we gloss over implementation details. For now I'm just starting with some easy ones --- documentation/docs/02-runes/04-$effect.md | 49 ++++++- documentation/docs/02-runes/05-$props.md | 25 +++- documentation/docs/02-runes/07-$inspect.md | 10 +- .../docs/03-template-syntax/03-each.md | 19 ++- .../docs/03-template-syntax/06-snippet.md | 128 ++++++++++++++++-- .../docs/03-template-syntax/12-bind.md | 22 ++- .../19-await-expressions.md | 5 +- documentation/docs/06-runtime/02-context.md | 2 +- 8 files changed, 235 insertions(+), 25 deletions(-) diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index d41c5b8e6a..a13fc7bc46 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -41,9 +41,11 @@ You can use `$effect` anywhere, not just at the top level of a component, as lon > [!NOTE] Svelte uses effects internally to represent logic and expressions in your template — this is how `

hello {name}!

` updates when `name` changes. -An effect can return a _teardown function_ which will run immediately before the effect re-runs ([demo](/playground/untitled#H4sIAAAAAAAAE42SQVODMBCF_8pOxkPRKq3HCsx49K4n64xpskjGkDDJ0tph-O8uINo6HjxB3u7HvrehE07WKDbiyZEhi1osRWksRrF57gQdm6E2CKx_dd43zU3co6VB28mIf-nKO0JH_BmRRRVMQ8XWbXkAgfKtI8jhIpIkXKySu7lSG2tNRGZ1_GlYr1ZTD3ddYFmiosUigbyAbpC2lKbwWJkIB8ZhhxBQBWRSw6FCh3sM8GrYTthL-wqqku4N44TyqEgwF3lmRHr4Op0PGXoH31c5rO8mqV-eOZ49bikgtcHBL55tmhIkEMqg_cFB2TpFxjtg703we6NRL8HQFCS07oSUCZi6Rm04lz1yytIHBKoQpo1w6Gsm4gmyS8b8Y5PydeMdX8gwS2Ok4I-ov5NZtvQde95GMsccn_1wzNKfu3RZtS66cSl9lvL7qO1aIk7knbJGvefdtIOzi73M4bYvovUHDFk6AcX_0HRESxnpBOW_jfCDxIZCi_1L_wm4xGQ60wIAAA==)). +An effect can return a _teardown function_ which will run immediately before the effect re-runs: + ```svelte + + @@ -236,6 +254,7 @@ When using [`await`](await-expressions) in components, the `$effect.pending()` r

pending promises: {$effect.pending()}

{/if} ``` + ## `$effect.root` @@ -285,9 +304,11 @@ In general, `$effect` is best considered something of an escape hatch — useful If you're using an effect because you want to be able to reassign the derived value (to build an optimistic UI, for example) note that [deriveds can be directly overridden]($derived#Overriding-derived-values) as of Svelte 5.25. -You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/playground/untitled#H4sIAAAAAAAAE5WRTWrDMBCFryKGLBJoY3fRjWIHeoiu6i6UZBwEY0VE49TB-O6VxrFTSih0qe_Ne_OjHpxpEDS8O7ZMeIAnqC1hAP3RA1990hKI_Fb55v06XJA4sZ0J-IjvT47RcYyBIuzP1vO2chVHHFjxiQ2pUr3k-SZRQlbBx_LIFoEN4zJfzQph_UMQr4hRXmBd456Xy5Uqt6pPKHmkfmzyPAZL2PCnbRpg8qWYu63I7lu4gswOSRYqrPNt3CgeqqzgbNwRK1A76w76YqjFspfcQTWmK3vJHlQm1puSTVSeqdOc_r9GaeCHfUSY26TXry6Br4RSK3C6yMEGT-aqVU3YbUZ2NF6rfP2KzXgbuYzY46czdgyazy0On_FlLH3F-UDXhgIO35UGlA1rAgAA)): +You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Instead of using effects for this... + ```svelte + + + +``` ```svelte @@ -163,6 +179,7 @@ The fallback value of a prop not declared with `$bindable` is left untouched — clicks: {object.count} ``` + In summary: don't mutate props. Either use callback props to communicate changes, or — if parent and child should share the same object — use the [`$bindable`]($bindable) rune. diff --git a/documentation/docs/02-runes/07-$inspect.md b/documentation/docs/02-runes/07-$inspect.md index f67e250b45..00857f3ef4 100644 --- a/documentation/docs/02-runes/07-$inspect.md +++ b/documentation/docs/02-runes/07-$inspect.md @@ -5,9 +5,11 @@ tags: rune-inspect > [!NOTE] `$inspect` only works during development. In a production build it becomes a noop. -The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire ([demo](/playground/untitled#H4sIAAAAAAAACkWQ0YqDQAxFfyUMhSotdZ-tCvu431AXtGOqQ2NmmMm0LOK_r7Utfby5JzeXTOpiCIPKT5PidkSVq2_n1F7Jn3uIcEMSXHSw0evHpAjaGydVzbUQCmgbWaCETZBWMPlKj29nxBDaHj_edkAiu12JhdkYDg61JGvE_s2nR8gyuBuiJZuDJTyQ7eE-IEOzog1YD80Lb0APLfdYc5F9qnFxjiKWwbImo6_llKRQVs-2u91c_bD2OCJLkT3JZasw7KLA2XCX31qKWE6vIzNk1fKE0XbmYrBTufiI8-_8D2cUWBA_AQAA)): +The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire: + ```svelte + @@ -71,6 +73,7 @@ Snippets can be declared anywhere inside your component. They can reference valu {@render hello('alice')} {@render hello('bob')} ``` + ...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings): @@ -91,9 +94,11 @@ Snippets can be declared anywhere inside your component. They can reference valu {@render x()} ``` -Snippets can reference themselves and each other ([demo](/playground/untitled#H4sIAAAAAAAAE2WPTQqDMBCFrxLiRqH1Zysi7TlqF1YnENBJSGJLCYGeo5tesUeosfYH3c2bee_jjaWMd6BpfrAU6x5oTvdS0g01V-mFPkNnYNRaDKrxGxto5FKCIaeu1kYwFkauwsoUWtZYPh_3W5FMY4U2mb3egL9kIwY0rbhgiO-sDTgjSEqSTvIDs-jiOP7i_MHuFGAL6p9BtiSbOTl0GtzCuihqE87cqtyam6WRGz_vRcsZh5bmRg3gju4Fptq_kzQBAAA=)): +Snippets can reference themselves and each other: + ```svelte + {#snippet blastoff()} 🚀 {/snippet} @@ -109,14 +114,17 @@ Snippets can reference themselves and each other ([demo](/playground/untitled#H4 {@render countdown(10)} ``` + ## Passing snippets to components ### Explicit props -Within the template, snippets are values just like any other. As such, they can be passed to components as props ([demo](/playground/untitled#H4sIAAAAAAAAE3VS247aMBD9lZGpBGwDASRegonaPvQL2qdlH5zYEKvBNvbQLbL875VzAcKyj3PmzJnLGU8UOwqSkd8KJdaCk4TsZS0cyV49wYuJuQiQpGd-N2bu_ooaI1YwJ57hpVYoFDqSEepKKw3mO7VDeTTaIvxiRS1gb_URxvO0ibrS8WanIrHUyiHs7Vmigy28RmyHHmKvDMbMmFq4cQInvGSwTsBYWYoMVhCSB2rBFFPsyl0uruTlR3JZCWvlTXl1Yy_mawiR_rbZKZrellJ-5JQ0RiBUgnFhJ9OGR7HKmwVoilXeIye8DOJGfYCgRlZ3iE876TBsZPX7hPdteO75PC4QaIo8vwNPePmANQ2fMeEFHrLD7rR1jTNkW986E8C3KwfwVr8HSHOSEBT_kGRozyIkn_zQveXDL3rIfPJHtUDwzShJd_Qk3gQCbOGLsdq4yfTRJopRuin3I7nv6kL7ARRjmLdBDG3uv1mhuLA3V2mKtqNEf_oCn8p9aN-WYqH5peP4kWBl1UwJzAEPT9U7K--0fRrrWnPTXpCm1_EVdXjpNmlA8G1hPPyM1fKgMqjFHjctXGjLhZ05w0qpDhksGrybuNEHtJnCalZWsuaTlfq6nPaaBSv_HKw-K57BjzOiVj9ZKQYKzQjZodYFqydYTRN4gPhVzTDO2xnma3HsVWjaLjT8nbfwHy7Q5f2dBAAA)): +Within the template, snippets are values just like any other. As such, they can be passed to components as props: + ```svelte + + + + {#if header} + + {@render header()} + + {/if} + + + {#each data as d} + {@render row(d)} + {/each} + +
+ + ``` + Think about it like passing content instead of data to a component. The concept is similar to slots in web components. ### Implicit props -As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component ([demo](/playground/untitled#H4sIAAAAAAAAE3VSTa_aMBD8Kyu_SkAbCA-JSzBR20N_QXt6vIMTO8SqsY29tI2s_PcqTiB8vaPHs7MzuxuIZgdBMvJLo0QlOElIJZXwJHsLBBvb_XUASc7Mb9Yu_B-hsMMK5sUzvDQahUZPMkJ96aTFfKd3KA_WOISfrFACKmcOMFmk8TWUTjY73RFLoz1C5U4SPWzhrcN2GKDrlcGEWauEnyRwxCaDdQLWyVJksII2uaMWTDPNLtzX5YX8-kgua-GcHJVXI3u5WEPb0d83O03TMZSmfRzOkG1Db7mNacOL19JagVALxoWbztq-H8U6j0SaYp2P2BGbOyQ2v8PQIFMXLKRDk177pq0zf6d8bMrzwBdd0pamyPMb-IjNEzS2f86Gz_Dwf-2F9nvNSUJQ_EOSoTuJNvngqK5v4Pas7n4-OCwlEEJcQTIMO-nSQwtb-GSdsX46e9gbRoP9yGQ11I0rEuycunu6PHx1QnPhxm3SFN15MOlYEFJZtf0dUywMbwZOeBGsrKNLYB54-1R9WNqVdki7usim6VmQphf7mnpshiQRhNAXdoOfMyX3OgMlKtz0cGEcF27uLSul3mewjPjgOOoDukxjPS9rqfh0pb-8zs6aBSt_7505aZ7B9xOi0T9YKW4UooVsr0zB1BTrWQJ3EL-oWcZ572GxFoezCk37QLe3897-B2i2U62uBAAA)): +As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component: + ```svelte - + + + {#snippet header()} @@ -169,12 +225,54 @@ As an authoring convenience, snippets declared directly _inside_ a component imp
fruit
``` +```svelte + + + + + {#if header} + + {@render header()} + + {/if} + + + {#each data as d} + {@render row(d)} + {/each} + +
+ + +``` + + ### Implicit `children` snippet -Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet ([demo](/playground/untitled#H4sIAAAAAAAAE3WOQQrCMBBFrzIMggql3ddY1Du4si5sOmIwnYRkFKX07lKqglqX8_7_w2uRDw1hjlsWI5ZqTPBoLEXMdy3K3fdZDzB5Ndfep_FKVnpWHSKNce1YiCVijirqYLwUJQOYxrsgsLmIOIZjcA1M02w4n-PpomSVvTclqyEutDX6DA2pZ7_ABIVugrmEC3XJH92P55_G39GodCmWBFrQJ2PrQAwdLGHig_NxNv9xrQa1dhWIawrv1Wzeqawa8953D-8QOmaEAQAA)): +Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet: + ```svelte + + ``` @@ -187,6 +285,7 @@ Any content inside the component tags that is _not_ a snippet declaration implic ``` + > [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name @@ -256,9 +355,21 @@ We can tighten things up further by declaring a generic, so that `data` and `row ## Exporting snippets -Snippets declared at the top level of a `.svelte` file can be exported from a ` + +{@render add(1, 2)} + +``` ```svelte + @@ -267,6 +378,7 @@ Snippets declared at the top level of a `.svelte` file can be exported from a `< {a} + {b} = {a + b} {/snippet} ``` + > [!NOTE] > This requires Svelte 5.5.0 or newer diff --git a/documentation/docs/03-template-syntax/12-bind.md b/documentation/docs/03-template-syntax/12-bind.md index be84969b87..e8164149db 100644 --- a/documentation/docs/03-template-syntax/12-bind.md +++ b/documentation/docs/03-template-syntax/12-bind.md @@ -54,9 +54,11 @@ A `bind:value` directive on an `` element binds the input's `value` prope

{message}

``` -In the case of a numeric input (`type="number"` or `type="range"`), the value will be coerced to a number ([demo](/playground/untitled#H4sIAAAAAAAAE6WPwYoCMQxAfyWEPeyiOOqx2w74Hds9pBql0IllmhGXYf5dKqwiyILsLXnwwsuI-5i4oPkaUX8yo7kCnKNQV7dNzoty4qSVBSr8jG-Poixa0KAt2z5mbb14TaxA4OCtKCm_rz4-f2m403WltrlrYhMFTtcLNkoeFGqZ8yhDF7j3CCHKzpwoDexGmqCL4jwuPUJHZ-dxVcfmyYGe5MAv-La5pbxYFf5Z9Zf_UJXb-sEMquFgJJhBmGyTW5yj8lnRaD_w9D1dAKSSj7zqAQAA)): +In the case of a numeric input (`type="number"` or `type="range"`), the value will be coerced to a number: + ```svelte + +

Customize your burrito

+ @@ -165,7 +171,17 @@ Inputs that work together can use `bind:group` ([demo](/playground/untitled#H4sI + +

Tortilla: {tortilla}

+

Fillings: {fillings.join(', ') || 'None'}

+ + ``` + > [!NOTE] `bind:group` only works if the inputs are in the same Svelte component. diff --git a/documentation/docs/03-template-syntax/19-await-expressions.md b/documentation/docs/03-template-syntax/19-await-expressions.md index aa3f907b14..04437f5ee3 100644 --- a/documentation/docs/03-template-syntax/19-await-expressions.md +++ b/documentation/docs/03-template-syntax/19-await-expressions.md @@ -25,9 +25,11 @@ The experimental flag will be removed in Svelte 6. ## Synchronized updates -When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like [this](/playground/untitled#H4sIAAAAAAAAE42QsWrDQBBEf2VZUkhYRE4gjSwJ0qVMkS6XYk9awcFpJe5Wdoy4fw-ycdykSPt2dpiZFYVGxgrf2PsJTlPwPWTcO-U-xwIH5zli9bminudNtwEsbl-v8_wYj-x1Y5Yi_8W7SZRFI1ZYxy64WVsjRj0rEDTwEJWUs6f8cKP2Tp8vVIxSPEsHwyKdukmA-j6jAmwO63Y1SidyCsIneA_T6CJn2ZBD00Jk_XAjT4tmQwEv-32eH6AsgYK6wXWOPPTs6Xy1CaxLECDYgb3kSUbq8p5aaifzorCt0RiUZbQcDIJ10ldH8gs3K6X2Xzqbro5zu1KCHaw2QQPrtclvwVSXc2sEC1T-Vqw0LJy-ClRy_uSkx2ogHzn9ADZ1CubKAQAA)... +When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like this... + ```svelte + + +
+
{@html await firstTest()}
+ {await otherTest()} +