diff --git a/.changeset/true-cities-retire.md b/.changeset/true-cities-retire.md new file mode 100644 index 0000000000..c1846e9267 --- /dev/null +++ b/.changeset/true-cities-retire.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: robustify blocker calculation diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index ef0b35f560..969af842cc 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -690,7 +690,7 @@ export function analyze_component(root, source, options) { } } - calculate_blockers(instance, scopes, analysis); + calculate_blockers(instance, analysis); if (analysis.runes) { const props_refs = module.scope.references.get('$$props'); @@ -940,11 +940,10 @@ export function analyze_component(root, source, options) { * top level statements. This includes indirect blockers such as functions referencing async top level statements. * * @param {Js} instance - * @param {Map} scopes * @param {ComponentAnalysis} analysis * @returns {void} */ -function calculate_blockers(instance, scopes, analysis) { +function calculate_blockers(instance, analysis) { /** * @param {ESTree.Node} expression * @param {Scope} scope @@ -959,6 +958,14 @@ function calculate_blockers(instance, scopes, analysis) { expression, { scope }, { + _(node, context) { + const scope = instance.scopes.get(node); + if (scope) { + context.next({ scope }); + } else { + context.next(); + } + }, ImportDeclaration(node) {}, Identifier(node, context) { const parent = /** @type {ESTree.Node} */ (context.path.at(-1)); @@ -979,14 +986,11 @@ function calculate_blockers(instance, scopes, analysis) { /** * @param {ESTree.Node} node - * @param {Set} seen * @param {Set} reads * @param {Set} writes + * @param {Scope} scope */ - const trace_references = (node, reads, writes, seen = new Set()) => { - if (seen.has(node)) return; - seen.add(node); - + const trace_references = (node, reads, writes, scope) => { /** * @param {ESTree.Pattern} node * @param {Scope} scope @@ -1005,10 +1009,10 @@ function calculate_blockers(instance, scopes, analysis) { walk( node, - { scope: instance.scope }, + { scope }, { _(node, context) { - const scope = scopes.get(node); + const scope = instance.scopes.get(node); if (scope) { context.next({ scope }); } else { @@ -1040,10 +1044,6 @@ function calculate_blockers(instance, scopes, analysis) { 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)) { @@ -1052,7 +1052,19 @@ function calculate_blockers(instance, scopes, analysis) { reads.add(binding); } } - } + }, + ReturnStatement(node, context) { + // We have to assume that anything returned from a function, even if it's a function itself, + // might be called immediately, so we have to touch all references within it. Example: + // function foo() { return () => blocker; } foo(); // blocker is touched + if (node.argument) { + touch(node.argument, context.state.scope, reads); + } + }, + // don't look inside functions until they are called + ArrowFunctionExpression(_, context) {}, + FunctionDeclaration(_, context) {}, + FunctionExpression(_, context) {} } ); }; @@ -1132,7 +1144,7 @@ function calculate_blockers(instance, scopes, analysis) { /** @type {Set} */ const writes = new Set(); - trace_references(declarator, reads, writes); + trace_references(declarator, reads, writes, instance.scope); const blocker = /** @type {NonNullable} */ ( b.member(promises, b.literal(analysis.instance_body.async.length), true) @@ -1160,7 +1172,7 @@ function calculate_blockers(instance, scopes, analysis) { /** @type {Set} */ const writes = new Set(); - trace_references(node, reads, writes); + trace_references(node, reads, writes, instance.scope); const blocker = /** @type {NonNullable} */ ( b.member(promises, b.literal(analysis.instance_body.async.length), true) @@ -1184,12 +1196,17 @@ function calculate_blockers(instance, scopes, analysis) { for (const fn of functions) { /** @type {Set} */ const reads_writes = new Set(); - const body = + const init = fn.type === 'VariableDeclarator' - ? /** @type {ESTree.FunctionExpression | ESTree.ArrowFunctionExpression} */ (fn.init).body - : fn.body; - - trace_references(body, reads_writes, reads_writes); + ? /** @type {ESTree.FunctionExpression | ESTree.ArrowFunctionExpression} */ (fn.init) + : fn; + + trace_references( + init.body, + reads_writes, + reads_writes, + /** @type {Scope} */ (instance.scopes.get(init)) + ); const max = [...reads_writes].reduce((max, binding) => { if (binding.blocker) { diff --git a/packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/_config.js b/packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/_config.js new file mode 100644 index 0000000000..080e2d278c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/_config.js @@ -0,0 +1,14 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['async-server', 'client', 'hydrate'], + ssrHtml: 'true true true true true', + + async test({ assert, target }) { + await new Promise((resolve) => setTimeout(resolve, 10)); + await tick(); + + assert.htmlEqual(target.innerHTML, 'true true true true true'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/main.svelte new file mode 100644 index 0000000000..5f79a14830 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-bind-factory-function-remote/main.svelte @@ -0,0 +1,41 @@ + + + +{#if true} + {checkedFactory()()} +{/if} +{#if true} + {indirectCheckedFactory()()} +{/if} +{#if true} + {callFactory(checkedFactory)()} +{/if} +{#if true} + {indirectCallFactory()()} +{/if} +{#if true} + {indirectChecked2()()} +{/if}