diff --git a/.changeset/gentle-showers-speak.md b/.changeset/gentle-showers-speak.md new file mode 100644 index 0000000000..9cc1adee94 --- /dev/null +++ b/.changeset/gentle-showers-speak.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly handle functions when determining async blockers diff --git a/.changeset/soft-radios-make.md b/.changeset/soft-radios-make.md new file mode 100644 index 0000000000..15a9dbe35a --- /dev/null +++ b/.changeset/soft-radios-make.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: take blockers into account when creating `#await` blocks diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index ed71b898ed..34490ff9c8 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-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 @@ + + + + + +