From ab5aedaddd168e98713fd708d86ec6a5c0344f3c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 18 Aug 2025 12:58:36 -0400 Subject: [PATCH] use helper for async bodies (#16641) * use helper for async bodies * unused * fix * failing test + fix --------- Co-authored-by: Simon Holthausen --- .../3-transform/client/transform-client.js | 71 ++++++------------- .../svelte/src/compiler/utils/builders.js | 18 ----- packages/svelte/src/internal/client/index.js | 1 + .../src/internal/client/reactivity/async.js | 21 +++++- .../Child.svelte | 9 +++ .../_config.js | 15 ++++ .../main.svelte | 18 +++++ .../async-top-level-error-nested/Child.svelte | 2 - .../async-top-level-error-nested/_config.js | 3 +- .../async-top-level-error-nested/main.svelte | 12 ++-- 10 files changed, 90 insertions(+), 80 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/main.svelte 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 eb32f9c98b..940d6a9e00 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 @@ -359,16 +359,31 @@ export function client_component(analysis, options) { if (dev) push_args.push(b.id(analysis.name)); let component_block = b.block([ + store_init, ...store_setup, ...legacy_reactive_declarations, ...group_binding_declarations, - ...state.instance_level_snippets, - .../** @type {ESTree.Statement[]} */ (instance.body), - analysis.runes || !analysis.needs_context - ? b.empty - : b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined)) + ...state.instance_level_snippets ]); + if (analysis.instance.has_await) { + const body = b.block([ + .../** @type {ESTree.Statement[]} */ (instance.body), + b.if(b.call('$.aborted'), b.return()), + .../** @type {ESTree.Statement[]} */ (template.body) + ]); + + component_block.body.push(b.stmt(b.call(`$.async_body`, b.arrow([], body, true)))); + } else { + component_block.body.push(.../** @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))); + } + + 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'))) @@ -389,52 +404,6 @@ export function client_component(analysis, options) { analysis.uses_slots || analysis.slot_names.size > 0; - if (analysis.instance.has_await) { - const params = [b.id('$$anchor')]; - if (should_inject_props) { - params.push(b.id('$$props')); - } - if (store_setup.length > 0) { - params.push(b.id('$$stores')); - } - const body = b.function_declaration( - b.id('$$body'), - params, - b.block([ - b.var('$$unsuspend', b.call('$.suspend')), - b.var('$$active', b.id('$.active_effect')), - b.try_catch( - b.block([ - ...component_block.body, - b.if(b.call('$.aborted'), b.return()), - .../** @type {ESTree.Statement[]} */ (template.body) - ]), - b.block([ - b.if( - b.unary('!', b.call('$.aborted', b.id('$$active'))), - b.stmt(b.call('$.invoke_error_boundary', b.id('$$error'), b.id('$$active'))) - ) - ]) - ), - b.stmt(b.call('$$unsuspend')) - ]), - true - ); - - state.hoisted.push(body); - - component_block = b.block([ - b.var('fragment', b.call('$.comment')), - b.var('node', b.call('$.first_child', b.id('fragment'))), - store_init, - b.stmt(b.call(body.id, b.id('node'), ...params.slice(1))), - b.stmt(b.call('$.append', b.id('$$anchor'), b.id('fragment'))) - ]); - } else { - component_block.body.unshift(store_init); - component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body)); - } - // trick esrap into including comments component_block.loc = instance.loc; diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index c77cd7eee7..56a5f31ffe 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -659,24 +659,6 @@ export function throw_error(str) { }; } -/** - * @param {ESTree.BlockStatement} body - * @param {ESTree.BlockStatement} handler - * @returns {ESTree.TryStatement} - */ -export function try_catch(body, handler) { - return { - type: 'TryStatement', - block: body, - handler: { - type: 'CatchClause', - param: id('$$error'), - body: handler - }, - finalizer: null - }; -} - export { await_builder as await, let_builder as let, diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 4089401a7e..c5b7bb845c 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -99,6 +99,7 @@ export { with_script } from './dom/template.js'; export { + async_body, for_await_track_reactivity_loss, save, track_reactivity_loss diff --git a/packages/svelte/src/internal/client/reactivity/async.js b/packages/svelte/src/internal/client/reactivity/async.js index 2b133e5f44..1ea1bbe561 100644 --- a/packages/svelte/src/internal/client/reactivity/async.js +++ b/packages/svelte/src/internal/client/reactivity/async.js @@ -11,7 +11,7 @@ import { set_active_effect, set_active_reaction } from '../runtime.js'; -import { current_batch } from './batch.js'; +import { current_batch, suspend } from './batch.js'; import { async_derived, current_async_effect, @@ -19,6 +19,7 @@ import { derived_safe_equal, set_from_async_derived } from './deriveds.js'; +import { aborted } from './effects.js'; /** * @@ -170,3 +171,21 @@ export function unset_context() { set_component_context(null); if (DEV) set_from_async_derived(null); } + +/** + * @param {() => Promise} fn + */ +export async function async_body(fn) { + const unsuspend = suspend(); + const active = /** @type {Effect} */ (active_effect); + + try { + await fn(); + } catch (error) { + if (!aborted(active)) { + invoke_error_boundary(error, active); + } + } finally { + unsuspend(); + } +} diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/Child.svelte new file mode 100644 index 0000000000..f7ba132ace --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/Child.svelte @@ -0,0 +1,9 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/_config.js b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/_config.js new file mode 100644 index 0000000000..298e33e9a2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/_config.js @@ -0,0 +1,15 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

pending

`, + + async test({ assert, target }) { + const [reject] = target.querySelectorAll('button'); + + await tick(); + reject.click(); + await tick(); + assert.htmlEqual(target.innerHTML, '

route: other

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/main.svelte new file mode 100644 index 0000000000..2f461e96c8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested-obsolete/main.svelte @@ -0,0 +1,18 @@ + + + + + + {#if route.current === 'home'} + + {:else} +

route: {route.current}

+ {/if} + + {#snippet pending()} +

pending

+ {/snippet} +
diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/Child.svelte index f7ba132ace..11c9ebd653 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/Child.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/Child.svelte @@ -2,8 +2,6 @@ import { route } from "./main.svelte"; await new Promise(async (_, reject) => { - await Promise.resolve(); - route.current = 'other' route.reject = reject; }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/_config.js b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/_config.js index 298e33e9a2..57005b4112 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/_config.js @@ -7,9 +7,8 @@ export default test({ async test({ assert, target }) { const [reject] = target.querySelectorAll('button'); - await tick(); reject.click(); await tick(); - assert.htmlEqual(target.innerHTML, '

route: other

'); + assert.htmlEqual(target.innerHTML, '

failed

'); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/main.svelte index 2f461e96c8..2fdf4c0d2f 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-top-level-error-nested/main.svelte @@ -1,18 +1,18 @@ - {#if route.current === 'home'} - - {:else} -

route: {route.current}

- {/if} + {#snippet pending()}

pending

{/snippet} + + {#snippet failed()} +

failed

+ {/snippet}