diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 1fd67f455d..670940f2df 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,27 @@ # svelte +## 5.43.10 + +### Patch Changes + +- fix: avoid other batches running with queued root effects of main batch ([#17145](https://github.com/sveltejs/svelte/pull/17145)) + +## 5.43.9 + +### Patch Changes + +- fix: correctly handle functions when determining async blockers ([#17137](https://github.com/sveltejs/svelte/pull/17137)) + +- fix: keep deriveds reactive after their original parent effect was destroyed ([#17171](https://github.com/sveltejs/svelte/pull/17171)) + +- fix: ensure eager effects don't break reactions chain ([#17138](https://github.com/sveltejs/svelte/pull/17138)) + +- fix: ensure async `@const` in boundary hydrates correctly ([#17165](https://github.com/sveltejs/svelte/pull/17165)) + +- fix: take blockers into account when creating `#await` blocks ([#17137](https://github.com/sveltejs/svelte/pull/17137)) + +- fix: parallelize async `@const`s in the template ([#17165](https://github.com/sveltejs/svelte/pull/17165)) + ## 5.43.8 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index dfecc8d62d..bec65bf787 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.43.8", + "version": "5.43.10", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/create.js b/packages/svelte/src/compiler/phases/1-parse/utils/create.js index 2fba918f20..6030f1bd7b 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/create.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/create.js @@ -10,8 +10,7 @@ export function create_fragment(transparent = false) { nodes: [], metadata: { transparent, - dynamic: false, - has_await: false + dynamic: false } }; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index ed71b898ed..4206f3df9a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -687,193 +687,7 @@ export function analyze_component(root, source, options) { } } - /** - * @param {ESTree.Node} expression - * @param {Scope} scope - * @param {Set} touched - * @param {Set} seen - */ - const touch = (expression, scope, touched, seen = new Set()) => { - if (seen.has(expression)) return; - seen.add(expression); - - walk( - expression, - { scope }, - { - ImportDeclaration(node) {}, - Identifier(node, context) { - const parent = /** @type {ESTree.Node} */ (context.path.at(-1)); - if (is_reference(node, parent)) { - const binding = context.state.scope.get(node.name); - if (binding) { - touched.add(binding); - - for (const assignment of binding.assignments) { - touch(assignment.value, assignment.scope, touched, seen); - } - } - } - } - } - ); - }; - - /** - * @param {ESTree.Node} node - * @param {Set} seen - * @param {Set} reads - * @param {Set} writes - */ - const trace_references = (node, reads, writes, seen = new Set()) => { - if (seen.has(node)) return; - seen.add(node); - - /** - * @param {ESTree.Pattern} node - * @param {Scope} scope - */ - function update(node, scope) { - for (const pattern of unwrap_pattern(node)) { - const node = object(pattern); - if (!node) return; - - const binding = scope.get(node.name); - if (!binding) return; - - writes.add(binding); - } - } - - walk( - node, - { scope: instance.scope }, - { - _(node, context) { - const scope = scopes.get(node); - if (scope) { - context.next({ scope }); - } else { - context.next(); - } - }, - AssignmentExpression(node, context) { - update(node.left, context.state.scope); - }, - UpdateExpression(node, context) { - update( - /** @type {ESTree.Identifier | ESTree.MemberExpression} */ (node.argument), - context.state.scope - ); - }, - CallExpression(node, context) { - // for now, assume everything touched by the callee ends up mutating the object - // TODO optimise this better - - // special case — no need to peek inside effects as they only run once async work has completed - const rune = get_rune(node, context.state.scope); - if (rune === '$effect') return; - - /** @type {Set} */ - const touched = new Set(); - touch(node, context.state.scope, touched); - - for (const b of touched) { - writes.add(b); - } - }, - // don't look inside functions until they are called - ArrowFunctionExpression(_, context) {}, - FunctionDeclaration(_, context) {}, - FunctionExpression(_, context) {}, - Identifier(node, context) { - const parent = /** @type {ESTree.Node} */ (context.path.at(-1)); - if (is_reference(node, parent)) { - const binding = context.state.scope.get(node.name); - if (binding) { - reads.add(binding); - } - } - } - } - ); - }; - - let awaited = false; - - // TODO this should probably be attached to the scope? - var promises = b.id('$$promises'); - - /** - * @param {ESTree.Identifier} id - * @param {ESTree.Expression} blocker - */ - function push_declaration(id, blocker) { - analysis.instance_body.declarations.push(id); - - const binding = /** @type {Binding} */ (instance.scope.get(id.name)); - binding.blocker = blocker; - } - - for (let node of instance.ast.body) { - if (node.type === 'ImportDeclaration') { - analysis.instance_body.hoisted.push(node); - continue; - } - - if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') { - // these can't exist inside ` + +{#await foo then x} +

{x}

+{/await} diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/A.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/A.svelte new file mode 100644 index 0000000000..7971deff5f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/A.svelte @@ -0,0 +1,15 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/B.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/B.svelte new file mode 100644 index 0000000000..7371f47a6f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/B.svelte @@ -0,0 +1 @@ +B \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/_config.js b/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/_config.js new file mode 100644 index 0000000000..789cdfaa02 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/_config.js @@ -0,0 +1,63 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [fork, commit, toggle] = target.querySelectorAll('button'); + + fork.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + ` + ); + + toggle.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + ` + ); + + toggle.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + ` + ); + + toggle.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + + ` + ); + + commit.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + + + B + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/main.svelte new file mode 100644 index 0000000000..7342a37f05 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-block-effect-queueing/main.svelte @@ -0,0 +1,24 @@ + + + + + + +{#if open} + +{:else} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-const/_config.js b/packages/svelte/tests/runtime-runes/samples/async-const/_config.js index 8aeca875f3..c3e74e886a 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-const/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-const/_config.js @@ -2,11 +2,12 @@ import { tick } from 'svelte'; import { test } from '../../test'; export default test({ - html: `

Loading...

`, + mode: ['async-server', 'client', 'hydrate'], + ssrHtml: `

Hello, world!

5 01234 5 sync 6 5 0`, async test({ assert, target }) { await tick(); - assert.htmlEqual(target.innerHTML, `

Hello, world!

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

Hello, world!

5 01234 5 sync 6 5 0`); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte index 7410ff6a6f..b7e00803c5 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-const/main.svelte @@ -3,17 +3,16 @@ + {@const sync = 'sync'} {@const number = await Promise.resolve(5)} - - {#snippet pending()} -

Loading...

- {/snippet} + {@const after_async = number + 1} + {@const { length, 0: first } = await '01234'} {#snippet greet()} {@const greeting = await `Hello, ${name}!`}

{greeting}

{number} - {#if number > 4} + {#if number > 4 && after_async && greeting} {@const length = await number} {#each { length }, index} {@const i = await index} @@ -23,4 +22,5 @@ {/snippet} {@render greet()} + {number} {sync} {after_async} {length} {first}
diff --git a/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component1.svelte b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component1.svelte new file mode 100644 index 0000000000..a3bb9d92b3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component1.svelte @@ -0,0 +1,13 @@ + + + +

{getValue()}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component2.svelte b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component2.svelte new file mode 100644 index 0000000000..8b28cf5708 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component2.svelte @@ -0,0 +1,11 @@ + + + +

{getValue()}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component3.svelte b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component3.svelte new file mode 100644 index 0000000000..f26daeb4f2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component3.svelte @@ -0,0 +1,16 @@ + + + +

{getValue()}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component4.svelte b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component4.svelte new file mode 100644 index 0000000000..564cb2660a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/Component4.svelte @@ -0,0 +1,19 @@ + + + +

{getValue()}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/_config.js b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/_config.js new file mode 100644 index 0000000000..7b7ee5b122 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/_config.js @@ -0,0 +1,27 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['async-server', 'client', 'hydrate'], + ssrHtml: + '

', + + async test({ assert, target }) { + await tick(); + + const inputs = Array.from(target.querySelectorAll('input')); + const paragraphs = Array.from(target.querySelectorAll('p')); + + for (let i = 0; i < 4; i++) { + assert.equal(inputs[i].value, ''); + assert.htmlEqual(paragraphs[i].innerHTML, ''); + + inputs[i].value = 'hello'; + inputs[i].dispatchEvent(new InputEvent('input', { bubbles: true })); + await tick(); + + assert.equal(inputs[i].value, 'hello'); + assert.htmlEqual(paragraphs[i].innerHTML, 'hello'); + } + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/main.svelte new file mode 100644 index 0000000000..c30111fd2b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-indirect-blockers/main.svelte @@ -0,0 +1,11 @@ + + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-derived-4/_config.js b/packages/svelte/tests/runtime-runes/samples/inspect-derived-4/_config.js new file mode 100644 index 0000000000..3a3bca7221 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-derived-4/_config.js @@ -0,0 +1,35 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; +import { normalise_inspect_logs } from '../../../helpers'; + +export default test({ + compileOptions: { + dev: true + }, + + async test({ assert, target, logs }) { + const [b] = target.querySelectorAll('button'); + + b.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ``); + + b.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ``); + + b.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ``); + + assert.deepEqual(normalise_inspect_logs(logs), [ + [0, 1, 2], + [1, 2], + 'at SvelteSet.add', + [2], + 'at SvelteSet.add', + [], + 'at SvelteSet.add' + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/inspect-derived-4/main.svelte b/packages/svelte/tests/runtime-runes/samples/inspect-derived-4/main.svelte new file mode 100644 index 0000000000..eb4ea891db --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/inspect-derived-4/main.svelte @@ -0,0 +1,14 @@ + + + diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index eff6d6166a..13430609a8 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1391,6 +1391,33 @@ describe('signals', () => { }; }); + test('derived whose original parent effect has been destroyed keeps updating', () => { + return () => { + let count: Source; + let double: Derived; + const destroy = effect_root(() => { + render_effect(() => { + count = state(0); + double = derived(() => $.get(count) * 2); + }); + }); + + flushSync(); + assert.equal($.get(double!), 0); + + destroy(); + flushSync(); + + set(count!, 1); + flushSync(); + assert.equal($.get(double!), 2); + + set(count!, 2); + flushSync(); + assert.equal($.get(double!), 4); + }; + }); + test('$effect.root inside deriveds stay alive independently', () => { const log: any[] = []; const c = state(0); 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 e4df43c6c2..7d1fe4ec67 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 @@ -25,20 +25,23 @@ export default function Async_in_derived($$anchor, $$props) { { var consequent = ($$anchor) => { - $.async_body($$anchor, async ($$anchor) => { - const yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))(); - const yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))(); + let yes1; + let yes2; + let no1; + let no2; - const no1 = $.derived(() => (async () => { - return await 1; - })()); + var promises = $.run([ + async () => yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))(), + async () => yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))(), - const no2 = $.derived(() => (async () => { + () => no1 = $.derived(() => (async () => { return await 1; - })()); + })()), - if ($.aborted()) return; - }); + () => no2 = $.derived(() => (async () => { + return await 1; + })()) + ]); }; $.if(node, ($$render) => { 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 bece6402c6..1fd184fa79 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 @@ -18,24 +18,38 @@ export default function Async_in_derived($$renderer, $$props) { } ]); - $$renderer.async_block([], async ($$renderer) => { - if (true) { - $$renderer.push(''); - - const yes1 = (await $.save(1))(); - const yes2 = foo((await $.save(1))()); - - const no1 = (async () => { - return await 1; - })(); - - const no2 = (async () => { - return await 1; - })(); - } else { - $$renderer.push(''); - } - }); + if (true) { + $$renderer.push(''); + + let yes1; + let yes2; + let no1; + let no2; + + var promises = $$renderer.run([ + async () => { + yes1 = (await $.save(1))(); + }, + + async () => { + yes2 = foo((await $.save(1))()); + }, + + () => { + no1 = (async () => { + return await 1; + })(); + }, + + () => { + no2 = (async () => { + return await 1; + })(); + } + ]); + } else { + $$renderer.push(''); + } $$renderer.push(``); });