From 8a28f72090b5dab66db208c4967204ad68de58fc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Jan 2025 22:45:34 -0500 Subject: [PATCH] dynamic elements --- .../compiler/phases/1-parse/state/element.js | 2 + .../2-analyze/visitors/SvelteElement.js | 14 +++++- .../client/visitors/SvelteElement.js | 45 ++++++++++++------- .../svelte/src/compiler/types/template.d.ts | 1 + .../samples/async-svelte-element/_config.js | 35 +++++++++++++++ .../samples/async-svelte-element/main.svelte | 11 +++++ 6 files changed, 92 insertions(+), 16 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/async-svelte-element/main.svelte diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index 66946a8f8d..b18e1cb25b 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -284,6 +284,8 @@ export default function element(parser) { } else { element.tag = get_attribute_expression(definition); } + + element.metadata.expression = create_expression_metadata(); } if (is_top_level_script_or_style) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteElement.js index c45859408c..5be1f91cba 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteElement.js @@ -62,5 +62,17 @@ export function SvelteElement(node, context) { mark_subtree_dynamic(context.path); - context.next({ ...context.state, parent_element: null }); + context.visit(node.tag, { + ...context.state, + expression: node.metadata.expression + }); + + for (const attribute of node.attributes) { + context.visit(attribute); + } + + context.visit(node.fragment, { + ...context.state, + parent_element: null + }); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js index ccf08dc423..37092a6306 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js @@ -33,7 +33,7 @@ export function SvelteElement(node, context) { const style_directives = []; /** @type {ExpressionStatement[]} */ - const lets = []; + const statements = []; // Create a temporary context which picks up the init/update statements. // They'll then be added to the function parameter of $.element @@ -66,7 +66,7 @@ export function SvelteElement(node, context) { } else if (attribute.type === 'StyleDirective') { style_directives.push(attribute); } else if (attribute.type === 'LetDirective') { - lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); + statements.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); } else if (attribute.type === 'OnDirective') { const handler = /** @type {Expression} */ (context.visit(attribute, inner_context.state)); inner_context.state.after_update.push(b.stmt(handler)); @@ -75,9 +75,6 @@ export function SvelteElement(node, context) { } } - // Let bindings first, they can be used on attributes - context.state.init.push(...lets); // create computeds in the outer context; the dynamic element is the single child of this slot - // Then do attributes let is_attributes_reactive = false; @@ -108,15 +105,6 @@ export function SvelteElement(node, context) { build_class_directives(class_directives, element_id, inner_context, is_attributes_reactive); build_style_directives(style_directives, element_id, inner_context, is_attributes_reactive); - const get_tag = b.thunk(/** @type {Expression} */ (context.visit(node.tag))); - - if (dev) { - if (node.fragment.nodes.length > 0) { - context.state.init.push(b.stmt(b.call('$.validate_void_dynamic_element', get_tag))); - } - context.state.init.push(b.stmt(b.call('$.validate_dynamic_element_tag', get_tag))); - } - /** @type {Statement[]} */ const inner = inner_context.state.init; if (inner_context.state.update.length > 0) { @@ -135,9 +123,21 @@ export function SvelteElement(node, context) { ).body ); + const { is_async } = node.metadata.expression; + + const expression = /** @type {Expression} */ (context.visit(node.tag)); + const get_tag = b.thunk(is_async ? b.call('$.get', b.id('$$tag')) : expression); + + if (dev) { + if (node.fragment.nodes.length > 0) { + statements.push(b.stmt(b.call('$.validate_void_dynamic_element', get_tag))); + } + statements.push(b.stmt(b.call('$.validate_dynamic_element_tag', get_tag))); + } + const location = dev && locator(node.start); - context.state.init.push( + statements.push( b.stmt( b.call( '$.element', @@ -150,4 +150,19 @@ export function SvelteElement(node, context) { ) ) ); + + if (is_async) { + context.state.init.push( + b.stmt( + b.call( + '$.async', + context.state.node, + b.array([b.thunk(expression, true)]), + b.arrow([context.state.node, b.id('$$tag')], b.block(statements)) + ) + ) + ); + } else { + context.state.init.push(statements.length === 1 ? statements[0] : b.block(statements)); + } } diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 14b9e522a4..dcdf645c4a 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -349,6 +349,7 @@ export namespace AST { tag: Expression; /** @internal */ metadata: { + expression: ExpressionMetadata; /** * `true` if this is an svg element. The boolean may not be accurate because * the tag is dynamic, but we do our best to infer it from the template. diff --git a/packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js new file mode 100644 index 0000000000..92946a539f --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/_config.js @@ -0,0 +1,35 @@ +import { flushSync, tick } from 'svelte'; +import { deferred } from '../../../../src/internal/shared/utils.js'; +import { test } from '../../test'; + +/** @type {ReturnType} */ +let d; + +export default test({ + html: `

pending

`, + + get props() { + d = deferred(); + + return { + promise: d.promise + }; + }, + + async test({ assert, target, component }) { + d.resolve('h1'); + await Promise.resolve(); + await Promise.resolve(); + await tick(); + flushSync(); + assert.htmlEqual(target.innerHTML, '

hello

'); + + component.promise = (d = deferred()).promise; + await tick(); + assert.htmlEqual(target.innerHTML, '

pending

'); + + d.resolve('h2'); + await tick(); + assert.htmlEqual(target.innerHTML, '

hello

'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-svelte-element/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/main.svelte new file mode 100644 index 0000000000..52852b549c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-svelte-element/main.svelte @@ -0,0 +1,11 @@ + + + + hello + + {#snippet pending()} +

pending

+ {/snippet} +