From c34e44f7812b15f94990213da13d770f9214c832 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 22 Jan 2025 15:23:49 -0500 Subject: [PATCH] async props --- .../client/visitors/shared/component.js | 81 +++++++++++++++---- .../src/internal/client/dom/blocks/async.js | 17 ++++ packages/svelte/src/internal/client/index.js | 1 + .../samples/async-prop/Child.svelte | 4 +- .../samples/async-prop/_config.js | 4 +- 5 files changed, 86 insertions(+), 21 deletions(-) create mode 100644 packages/svelte/src/internal/client/dom/blocks/async.js diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 15e4f68e9e49..55f632e53054 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -1,13 +1,19 @@ /** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ -/** @import { ComponentContext } from '../../types.js' */ +/** @import { ComponentContext, MemoizedExpression } from '../../types.js' */ import { dev, is_ignored } from '../../../../../state.js'; import { get_attribute_chunks, object } from '../../../../../utils/ast.js'; import * as b from '../../../../../utils/builders.js'; -import { build_bind_this, memoize_expression, validate_binding } from '../shared/utils.js'; +import { + build_bind_this, + get_expression_id, + memoize_expression, + validate_binding +} from '../shared/utils.js'; import { build_attribute_value } from '../shared/element.js'; import { build_event_handler } from './events.js'; import { determine_slot } from '../../../../../utils/slot.js'; +import { create_derived } from '../../utils.js'; /** * @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node @@ -40,6 +46,12 @@ export function build_component(node, component_name, context, anchor = context. /** @type {Record} */ const events = {}; + /** @type {MemoizedExpression[]} */ + const expressions = []; + + /** @type {MemoizedExpression[]} */ + const async_expressions = []; + /** @type {Property[]} */ const custom_css_props = []; @@ -115,16 +127,21 @@ export function build_component(node, component_name, context, anchor = context. (events[attribute.name] ||= []).push(handler); } else if (attribute.type === 'SpreadAttribute') { const expression = /** @type {Expression} */ (context.visit(attribute)); - if (attribute.metadata.expression.has_state) { - let value = expression; - if (attribute.metadata.expression.has_call) { - const id = b.id(context.state.scope.generate('spread_element')); - context.state.init.push(b.var(id, b.call('$.derived', b.thunk(value)))); - value = b.call('$.get', id); - } - - props_and_spreads.push(b.thunk(value)); + if (attribute.metadata.expression.has_state) { + props_and_spreads.push( + b.thunk( + attribute.metadata.expression.is_async || attribute.metadata.expression.has_call + ? b.call( + '$.get', + get_expression_id( + attribute.metadata.expression.is_async ? async_expressions : expressions, + expression + ) + ) + : expression + ) + ); } else { props_and_spreads.push(expression); } @@ -133,10 +150,15 @@ export function build_component(node, component_name, context, anchor = context. custom_css_props.push( b.init( attribute.name, - build_attribute_value(attribute.value, context, (value, metadata) => + build_attribute_value(attribute.value, context, (value, metadata) => { // TODO put the derived in the local block - metadata.has_call ? memoize_expression(context.state, value) : value - ).value + return metadata.has_call || metadata.is_async + ? b.call( + '$.get', + get_expression_id(metadata.is_async ? async_expressions : expressions, value) + ) + : value; + }).value ) ); continue; @@ -154,7 +176,7 @@ export function build_component(node, component_name, context, anchor = context. attribute.value, context, (value, metadata) => { - if (!metadata.has_state) return value; + if (!metadata.has_state && !metadata.is_async) return value; // When we have a non-simple computation, anything other than an Identifier or Member expression, // then there's a good chance it needs to be memoized to avoid over-firing when read within the @@ -167,7 +189,12 @@ export function build_component(node, component_name, context, anchor = context. ); }); - return should_wrap_in_derived ? memoize_expression(context.state, value) : value; + return should_wrap_in_derived + ? b.call( + '$.get', + get_expression_id(metadata.is_async ? async_expressions : expressions, value) + ) + : value; } ); @@ -420,7 +447,12 @@ export function build_component(node, component_name, context, anchor = context. }; } - const statements = [...snippet_declarations]; + const statements = [ + ...snippet_declarations, + ...expressions.map((memo) => + b.let(memo.id, create_derived(context.state, b.thunk(memo.expression))) + ) + ]; if (node.type === 'SvelteComponent') { const prev = fn; @@ -457,5 +489,20 @@ export function build_component(node, component_name, context, anchor = context. statements.push(b.stmt(fn(anchor))); } + [...async_expressions, ...expressions].forEach((memo, i) => { + memo.id.name = `$${i}`; + }); + + if (async_expressions.length > 0) { + return b.stmt( + b.call( + '$.async', + anchor, + b.array(async_expressions.map(({ expression }) => b.thunk(expression, true))), + b.arrow([b.id('$$anchor'), ...async_expressions.map(({ id }) => id)], b.block(statements)) + ) + ); + } + return statements.length > 1 ? b.block(statements) : statements[0]; } diff --git a/packages/svelte/src/internal/client/dom/blocks/async.js b/packages/svelte/src/internal/client/dom/blocks/async.js new file mode 100644 index 000000000000..0ffeb0591b1c --- /dev/null +++ b/packages/svelte/src/internal/client/dom/blocks/async.js @@ -0,0 +1,17 @@ +/** @import { TemplateNode, Value } from '#client' */ + +import { async_derived } from '../../reactivity/deriveds.js'; +import { suspend } from './boundary.js'; + +/** + * @param {TemplateNode} node + * @param {Array<() => Promise>} expressions + * @param {(anchor: TemplateNode, ...deriveds: Value[]) => void} fn + */ +export function async(node, expressions, fn) { + // TODO handle hydration + + suspend(Promise.all(expressions.map(async_derived))).then((result) => { + fn(node, ...result.exit()); + }); +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index c9b259c4dfbb..842343a11932 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -14,6 +14,7 @@ export { export { check_target, legacy_api } from './dev/legacy.js'; export { trace } from './dev/tracing.js'; export { inspect } from './dev/inspect.js'; +export { async } from './dom/blocks/async.js'; export { await_block as await } from './dom/blocks/await.js'; export { if_block as if } from './dom/blocks/if.js'; export { key_block as key } from './dom/blocks/key.js'; diff --git a/packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte index 00f8df7c0a89..85d212b1a835 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/Child.svelte @@ -1,5 +1,5 @@ -

{num}

+

{value}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js b/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js index 91daba25a933..24882c56cd16 100644 --- a/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/async-prop/_config.js @@ -22,7 +22,7 @@ export default test({ await Promise.resolve(); await Promise.resolve(); await tick(); - assert.htmlEqual(target.innerHTML, '

hello

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

hello

'); d = deferred(); component.promise = d.promise; @@ -32,6 +32,6 @@ export default test({ d.resolve('hello again'); await Promise.resolve(); await tick(); - assert.htmlEqual(target.innerHTML, '

hello again

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

hello again

'); } });