From fbec895913ee413a80a4ae1ced7c80387d23eb64 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 18 Aug 2025 10:32:39 -0400 Subject: [PATCH 1/4] use helper for async bodies --- .../3-transform/client/transform-client.js | 72 ++++++------------- .../svelte/src/internal/client/async_body.js | 14 ++++ packages/svelte/src/internal/client/index.js | 1 + 3 files changed, 36 insertions(+), 51 deletions(-) create mode 100644 packages/svelte/src/internal/client/async_body.js 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 eb32f9c98b31..1bfd56363572 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,22 +359,38 @@ 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), + .../** @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'))) ); } + if (!analysis.runes && analysis.needs_context) { + component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined))); + } + const should_inject_context = dev || analysis.needs_context || @@ -389,52 +405,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/internal/client/async_body.js b/packages/svelte/src/internal/client/async_body.js new file mode 100644 index 000000000000..2336e25ae273 --- /dev/null +++ b/packages/svelte/src/internal/client/async_body.js @@ -0,0 +1,14 @@ +import { suspend } from './reactivity/batch.js'; + +/** + * @param {() => Promise} fn + */ +export async function async_body(fn) { + const unsuspend = suspend(); + + try { + await fn(); + } finally { + unsuspend(); + } +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 4089401a7ef2..3e030cbd2d03 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -1,5 +1,6 @@ export { createAttachmentKey as attachment } from '../../attachments/index.js'; export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js'; +export { async_body } from './async_body.js'; export { push, pop, add_svelte_meta } from './context.js'; export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js'; export { cleanup_styles } from './dev/css.js'; From fa21f8346453455ebcf17acec1f9b885a10f232c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 18 Aug 2025 10:36:37 -0400 Subject: [PATCH 2/4] unused --- packages/svelte/src/compiler/utils/builders.js | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index c77cd7eee782..56a5f31ffe82 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, From 14a4f6c09c10bd6a71252d04eed8740edb8b4750 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 18 Aug 2025 10:55:22 -0400 Subject: [PATCH 3/4] fix --- .../phases/3-transform/client/transform-client.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) 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 1bfd56363572..940d6a9e004d 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 @@ -375,10 +375,13 @@ export function client_component(analysis, options) { component_block.body.push(b.stmt(b.call(`$.async_body`, b.arrow([], body, true)))); } else { - component_block.body.push( - .../** @type {ESTree.Statement[]} */ (instance.body), - .../** @type {ESTree.Statement[]} */ (template.body) - ); + 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) { @@ -387,10 +390,6 @@ export function client_component(analysis, options) { ); } - if (!analysis.runes && analysis.needs_context) { - component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined))); - } - const should_inject_context = dev || analysis.needs_context || From b975914267311cb30998fbba65bd3daa37bf6e98 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 18 Aug 2025 17:59:50 +0200 Subject: [PATCH 4/4] failing test + fix --- .../svelte/src/internal/client/async_body.js | 14 ------------- packages/svelte/src/internal/client/index.js | 2 +- .../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 +++++------ 9 files changed, 70 insertions(+), 26 deletions(-) delete mode 100644 packages/svelte/src/internal/client/async_body.js 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/internal/client/async_body.js b/packages/svelte/src/internal/client/async_body.js deleted file mode 100644 index 2336e25ae273..000000000000 --- a/packages/svelte/src/internal/client/async_body.js +++ /dev/null @@ -1,14 +0,0 @@ -import { suspend } from './reactivity/batch.js'; - -/** - * @param {() => Promise} fn - */ -export async function async_body(fn) { - const unsuspend = suspend(); - - try { - await fn(); - } finally { - unsuspend(); - } -} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 3e030cbd2d03..c5b7bb845c4a 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -1,6 +1,5 @@ export { createAttachmentKey as attachment } from '../../attachments/index.js'; export { FILENAME, HMR, NAMESPACE_SVG } from '../../constants.js'; -export { async_body } from './async_body.js'; export { push, pop, add_svelte_meta } from './context.js'; export { assign, assign_and, assign_or, assign_nullish } from './dev/assign.js'; export { cleanup_styles } from './dev/css.js'; @@ -100,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 2b133e5f4492..1ea1bbe56160 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 000000000000..f7ba132acee1 --- /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 000000000000..298e33e9a299 --- /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 000000000000..2f461e96c856 --- /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 f7ba132acee1..11c9ebd6532a 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 298e33e9a299..57005b41120c 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 2f461e96c856..2fdf4c0d2f65 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}