diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 4c05fd6148..73bcdfc61d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -1,12 +1,17 @@ /** @import * as ESTree from 'estree' */ /** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */ /** @import { AnalysisState, Visitors } from './types' */ -/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */ +/** @import { Analysis, AwaitedStatement, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */ import { walk } from 'zimmerframe'; import { parse } from '../1-parse/acorn.js'; import * as e from '../../errors.js'; import * as w from '../../warnings.js'; -import { extract_identifiers, has_await_expression } from '../../utils/ast.js'; +import { + extract_identifiers, + has_await_expression, + object, + unwrap_pattern +} from '../../utils/ast.js'; import * as b from '#compiler/builders'; import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js'; import check_graph_for_cycles from './utils/check_graph_for_cycles.js'; @@ -543,7 +548,8 @@ export function analyze_component(root, source, options) { snippet_renderers: new Map(), snippets: new Set(), async_deriveds: new Set(), - pickled_awaits: new Set() + pickled_awaits: new Set(), + awaited_statements: new Map() }; if (!runes) { @@ -687,6 +693,179 @@ export function analyze_component(root, source, options) { e.legacy_rest_props_invalid(rest_props_refs[0].node); } + if (instance.has_await) { + /** + * @param {ESTree.Expression} 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 }, + { + 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 + 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); + } + } + } + } + ); + }; + + /** + * @param {ESTree.Statement | ESTree.VariableDeclarator | ESTree.FunctionDeclaration | ESTree.ClassDeclaration} node + */ + const push = (node) => { + /** @type {Set} */ + const reads = new Set(); + + /** @type {Set} */ + const writes = new Set(); + + trace_references(node, reads, writes); + + /** @type {AwaitedStatement} */ + const statement = { + node, + has_await: has_await_expression(node), + declarations: [], + reads, + writes + }; + + analysis.awaited_statements.set(node, statement); + + if (node.type === 'VariableDeclarator') { + for (const identifier of extract_identifiers(node.id)) { + const binding = /** @type {Binding} */ (instance.scope.get(identifier.name)); + statement.declarations.push(binding); + } + } else if (node.type === 'ClassDeclaration' || node.type === 'FunctionDeclaration') { + const binding = /** @type {Binding} */ (instance.scope.get(node.id.name)); + statement.declarations.push(binding); + } + }; + + for (let node of instance.ast.body) { + if ( + node.type === 'ImportDeclaration' || + node.type === 'ExportDefaultDeclaration' || + node.type === 'ExportAllDeclaration' + ) { + continue; + } + + if (node.type === 'ExportNamedDeclaration') { + if (node.declaration) { + node = node.declaration; + } else { + continue; + } + } + + if (node.type === 'VariableDeclaration') { + for (const declarator of node.declarations) { + push(declarator); + } + } else { + push(node); + } + } + } + for (const { ast, scope, scopes } of [module, instance, template]) { /** @type {AnalysisState} */ const state = { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index 14757af4a3..22a89db76e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -10,16 +10,13 @@ import * as e from '../../../errors.js'; export function AwaitExpression(node, context) { const tla = context.state.ast_type === 'instance' && context.state.function_depth === 1; - // preserve context for - // a) top-level await and - // b) awaits that precede other expressions in template or `$derived(...)` + // preserve context for awaits that precede other expressions in template or `$derived(...)` if ( - tla || - (is_reactive_expression( + is_reactive_expression( context.path, context.state.derived_function_depth === context.state.function_depth ) && - !is_last_evaluated_expression(context.path, node)) + !is_last_evaluated_expression(context.path, node) ) { context.state.analysis.pickled_awaits.add(node); } @@ -145,6 +142,9 @@ function is_last_evaluated_expression(path, node) { if (node !== parent.expressions.at(-1)) return false; break; + case 'VariableDeclarator': + return true; + default: return false; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 0dd4ae03b9..6fdca199eb 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -370,41 +370,22 @@ export function client_component(analysis, options) { analysis.reactive_statements.size > 0 || component_returned_object.length > 0; - if (analysis.instance.has_await) { - if (should_inject_context && component_returned_object.length > 0) { - component_block.body.push(b.var('$$exports')); - } - const body = b.block([ - ...store_setup, - ...state.instance_level_snippets, - .../** @type {ESTree.Statement[]} */ (instance.body), - ...(should_inject_context && component_returned_object.length > 0 - ? [b.stmt(b.assignment('=', b.id('$$exports'), b.object(component_returned_object)))] - : []), - b.if(b.call('$.aborted'), b.return()), - .../** @type {ESTree.Statement[]} */ (template.body) - ]); - - component_block.body.push( - b.stmt(b.call(`$.async_body`, b.id('$$anchor'), b.arrow([b.id('$$anchor')], body, true))) - ); - } else { - component_block.body.push( - ...state.instance_level_snippets, - .../** @type {ESTree.Statement[]} */ (instance.body) - ); - if (should_inject_context && component_returned_object.length > 0) { - component_block.body.push(b.var('$$exports', b.object(component_returned_object))); - } - component_block.body.unshift(...store_setup); + component_block.body.push( + ...state.instance_level_snippets, + .../** @type {ESTree.Statement[]} */ (instance.body) + ); - if (!analysis.runes && analysis.needs_context) { - component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined))); - } + if (should_inject_context && component_returned_object.length > 0) { + component_block.body.push(b.var('$$exports', b.object(component_returned_object))); + } + component_block.body.unshift(...store_setup); - component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body)); + if (!analysis.runes && analysis.needs_context) { + component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined))); } + component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body)); + if (analysis.needs_mutation_validation) { component_block.body.unshift( b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props'))) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index b9a8691a6b..410ad120d7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -21,9 +21,6 @@ export interface ClientTransformState extends TransformState { */ readonly in_constructor: boolean; - /** `true` if we're transforming the contents of ` + +{#if condition} +

yep

+{:else} +

nope

+{/if} diff --git a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js index cf667e1624..6f1c40988d 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/client/index.svelte.js @@ -6,7 +6,7 @@ export default function Async_each_fallback_hoisting($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); - $.async(node, [() => Promise.resolve([])], (node, $$collection) => { + $.async(node, [], [() => Promise.resolve([])], (node, $$collection) => { $.each( node, 16, diff --git a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js index c579fda929..7249fd6e4f 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_each_fallback_hoisting($$renderer) { - $$renderer.async(async ($$renderer) => { + $$renderer.async_block([], async ($$renderer) => { const each_array = $.ensure_array_like((await $.save(Promise.resolve([])))()); if (each_array.length !== 0) { diff --git a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js index a1535d6886..4045ad4bf4 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/client/index.svelte.js @@ -9,7 +9,7 @@ export default function Async_each_hoisting($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); - $.async(node, [() => Promise.resolve([first, second, third])], (node, $$collection) => { + $.async(node, [], [() => Promise.resolve([first, second, third])], (node, $$collection) => { $.each(node, 17, () => $.get($$collection), $.index, ($$anchor, item) => { $.next(); diff --git a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js index e87b50e2a4..43fe9414eb 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js @@ -8,7 +8,7 @@ export default function Async_each_hoisting($$renderer) { $$renderer.push(``); - $$renderer.async(async ($$renderer) => { + $$renderer.async_block([], async ($$renderer) => { const each_array = $.ensure_array_like((await $.save(Promise.resolve([first, second, third])))()); for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) { diff --git a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js index e385f5d234..d86001e273 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/client/index.svelte.js @@ -6,7 +6,7 @@ export default function Async_if_alternate_hoisting($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); - $.async(node, [() => Promise.resolve(false)], (node, $$condition) => { + $.async(node, [], [() => Promise.resolve(false)], (node, $$condition) => { var consequent = ($$anchor) => { var text = $.text(); diff --git a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js index df4ad80899..1e7330429a 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_if_alternate_hoisting($$renderer) { - $$renderer.async(async ($$renderer) => { + $$renderer.async_block([], async ($$renderer) => { if ((await $.save(Promise.resolve(false)))()) { $$renderer.push(''); $$renderer.push(async () => $.escape(await Promise.reject('no no no'))); diff --git a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js index 356e8e9607..5cdb6978d9 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/client/index.svelte.js @@ -6,7 +6,7 @@ export default function Async_if_hoisting($$anchor) { var fragment = $.comment(); var node = $.first_child(fragment); - $.async(node, [() => Promise.resolve(true)], (node, $$condition) => { + $.async(node, [], [() => Promise.resolve(true)], (node, $$condition) => { var consequent = ($$anchor) => { var text = $.text(); diff --git a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js index 1d935f9be8..1ca24cf81a 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js @@ -2,7 +2,7 @@ import 'svelte/internal/flags/async'; import * as $ from 'svelte/internal/server'; export default function Async_if_hoisting($$renderer) { - $$renderer.async(async ($$renderer) => { + $$renderer.async_block([], async ($$renderer) => { if ((await $.save(Promise.resolve(true)))()) { $$renderer.push(''); $$renderer.push(async () => $.escape(await Promise.resolve('yes yes yes'))); diff --git a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js index 7a97850175..e4df43c6c2 100644 --- a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/client/index.svelte.js @@ -5,48 +5,47 @@ import * as $ from 'svelte/internal/client'; export default function Async_in_derived($$anchor, $$props) { $.push($$props, true); - $.async_body($$anchor, async ($$anchor) => { - let yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))(); - let yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))(); + var yes1, yes2, no1, no2; - let no1 = $.derived(async () => { - return await 1; - }); + var $$promises = $.run([ + async () => yes1 = await $.async_derived(() => 1), + async () => yes2 = await $.async_derived(async () => foo(await 1)), - let no2 = $.derived(() => async () => { + () => no1 = $.derived(async () => { return await 1; - }); + }), - if ($.aborted()) return; - - var fragment = $.comment(); - var node = $.first_child(fragment); + () => no2 = $.derived(() => async () => { + return await 1; + }) + ]); - { - var consequent = ($$anchor) => { - $.async_body($$anchor, async ($$anchor) => { - const yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))(); - const yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))(); + var fragment = $.comment(); + var node = $.first_child(fragment); - const no1 = $.derived(() => (async () => { - return await 1; - })()); + { + var consequent = ($$anchor) => { + $.async_body($$anchor, async ($$anchor) => { + const yes1 = (await $.save($.async_derived(async () => (await $.save(1))())))(); + const yes2 = (await $.save($.async_derived(async () => foo((await $.save(1))()))))(); - const no2 = $.derived(() => (async () => { - return await 1; - })()); + const no1 = $.derived(() => (async () => { + return await 1; + })()); - if ($.aborted()) return; - }); - }; + const no2 = $.derived(() => (async () => { + return await 1; + })()); - $.if(node, ($$render) => { - if (true) $$render(consequent); + if ($.aborted()) return; }); - } + }; - $.append($$anchor, fragment); - }); + $.if(node, ($$render) => { + if (true) $$render(consequent); + }); + } + $.append($$anchor, fragment); $.pop(); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js index 69eca5a383..bece6402c6 100644 --- a/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-in-derived/_expected/server/index.svelte.js @@ -3,38 +3,40 @@ import * as $ from 'svelte/internal/server'; export default function Async_in_derived($$renderer, $$props) { $$renderer.component(($$renderer) => { - $$renderer.async(async ($$renderer) => { - let yes1 = (await $.save(1))(); - let yes2 = foo((await $.save(1))()); + var yes1, yes2, no1, no2; - let no1 = (async () => { - return await 1; - })(); + var $$promises = $$renderer.run([ + async () => yes1 = await 1, + async () => yes2 = foo(await 1), - let no2 = async () => { + () => no1 = (async () => { return await 1; - }; - - $$renderer.async(async ($$renderer) => { - if (true) { - $$renderer.push(''); - - const yes1 = (await $.save(1))(); - const yes2 = foo((await $.save(1))()); - - const no1 = (async () => { - return await 1; - })(); + })(), - const no2 = (async () => { - return await 1; - })(); - } else { - $$renderer.push(''); - } - }); - - $$renderer.push(``); + () => no2 = async () => { + return await 1; + } + ]); + + $$renderer.async_block([], async ($$renderer) => { + if (true) { + $$renderer.push(''); + + const yes1 = (await $.save(1))(); + const yes2 = foo((await $.save(1))()); + + const no1 = (async () => { + return await 1; + })(); + + const no2 = (async () => { + return await 1; + })(); + } else { + $$renderer.push(''); + } }); + + $$renderer.push(``); }); } \ No newline at end of file