diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js index 60ec158b53..3ae9e9ce88 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js @@ -12,7 +12,8 @@ import { process_children, build_template, build_attribute_value, - call_child_renderer + call_child_renderer, + PromiseOptimiser } from './shared/utils.js'; /** @@ -32,8 +33,10 @@ export function RegularElement(node, context) { const node_is_void = is_void(node.name); + const optimiser = new PromiseOptimiser(); + context.state.template.push(b.literal(`<${node.name}`)); - const body = build_element_attributes(node, { ...context, state }); + const body = build_element_attributes(node, { ...context, state }, optimiser.transform); context.state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance if ((node.name === 'script' || node.name === 'style') && node.fragment.nodes.length === 1) { @@ -93,26 +96,26 @@ export function RegularElement(node, context) { select_with_value = true; select_with_value_async ||= spread.metadata.expression.has_await; + const { object, has_await } = build_spread_object( + node, + node.attributes.filter( + (attribute) => + attribute.type === 'Attribute' || + attribute.type === 'BindDirective' || + attribute.type === 'SpreadAttribute' + ), + context, + optimiser.transform + ); + + // TODO use has_await + state.template.push( b.stmt( b.assignment( '=', b.id('$$renderer.local.select_value'), - b.member( - build_spread_object( - node, - node.attributes.filter( - (attribute) => - attribute.type === 'Attribute' || - attribute.type === 'BindDirective' || - attribute.type === 'SpreadAttribute' - ), - context - ), - 'value', - false, - true - ) + b.member(object, 'value', false, true) ) ) ); @@ -128,7 +131,13 @@ export function RegularElement(node, context) { const left = b.id('$$renderer.local.select_value'); if (value.type === 'Attribute') { state.template.push( - b.stmt(b.assignment('=', left, build_attribute_value(value.value, context))) + b.stmt( + b.assignment( + '=', + left, + build_attribute_value(value.value, context, false, false, optimiser.transform) + ) + ) ); } else if (value.type === 'BindDirective') { state.template.push( diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js index 2e116f57d8..582d78249f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js @@ -23,7 +23,13 @@ export function SvelteBoundary(node, context) { if (pending_attribute || pending_snippet) { const pending = pending_attribute ? b.call( - build_attribute_value(pending_attribute.value, context, false, true), + build_attribute_value( + pending_attribute.value, + context, + false, + true, + (expression) => expression + ), b.id('$$renderer') ) : /** @type {BlockStatement} */ (context.visit(pending_snippet.body)); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js index 6ca0997bb6..cc57a94afe 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js @@ -6,7 +6,7 @@ import { dev, locator } from '../../../../state.js'; import * as b from '#compiler/builders'; import { determine_namespace_for_children } from '../../utils.js'; import { build_element_attributes } from './shared/element.js'; -import { build_template } from './shared/utils.js'; +import { build_template, PromiseOptimiser } from './shared/utils.js'; /** * @param {AST.SvelteElement} node @@ -37,7 +37,10 @@ export function SvelteElement(node, context) { init: [] }; - build_element_attributes(node, { ...context, state }); + // TODO use this + const optimiser = new PromiseOptimiser(); + + build_element_attributes(node, { ...context, state }, optimiser.transform); if (dev) { const location = /** @type {Location} */ (locator(node.start)); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js index fbd3a22b1c..ae71faf60b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js @@ -1,5 +1,5 @@ /** @import { ArrayExpression, Expression, Literal, ObjectExpression } from 'estree' */ -/** @import { AST } from '#compiler' */ +/** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */ import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js'; import { binding_properties } from '../../../../bindings.js'; @@ -30,8 +30,9 @@ const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style']; * their output to be the child content instead. In this case, an object is returned. * @param {AST.RegularElement | AST.SvelteElement} node * @param {import('zimmerframe').Context} context + * @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform */ -export function build_element_attributes(node, context) { +export function build_element_attributes(node, context, transform) { /** @type {Array} */ const attributes = []; @@ -62,7 +63,11 @@ export function build_element_attributes(node, context) { // also see related code in analysis phase attribute.value[0].data = '\n' + attribute.value[0].data; } - content = b.call('$.escape', build_attribute_value(attribute.value, context)); + + content = b.call( + '$.escape', + build_attribute_value(attribute.value, context, false, false, transform) + ); } else if (node.name !== 'select') { // omit value attribute for select elements, it's irrelevant for the initially selected value and has no // effect on the selected value after the user interacts with the select element (the value _property_ does, but not the attribute) @@ -150,12 +155,12 @@ export function build_element_attributes(node, context) { expression: is_checkbox ? b.call( b.member(attribute.expression, 'includes'), - build_attribute_value(value_attribute.value, context) + build_attribute_value(value_attribute.value, context, false, false, transform) ) : b.binary( '===', attribute.expression, - build_attribute_value(value_attribute.value, context) + build_attribute_value(value_attribute.value, context, false, false, transform) ), metadata: { expression: create_expression_metadata() @@ -202,28 +207,34 @@ export function build_element_attributes(node, context) { } if (has_spread) { - build_element_spread_attributes(node, attributes, style_directives, class_directives, context); + build_element_spread_attributes( + node, + attributes, + style_directives, + class_directives, + context, + transform + ); + if (node.name === 'option') { + // TODO this is all wrong, it inlines the spread twice + + const { object, has_await } = build_spread_object( + node, + node.attributes.filter( + (attribute) => + attribute.type === 'Attribute' || + attribute.type === 'BindDirective' || + attribute.type === 'SpreadAttribute' + ), + context, + transform + ); + + // TODO use has_await? + context.state.template.push( - b.call( - '$.maybe_selected', - b.id('$$renderer'), - b.member( - build_spread_object( - node, - node.attributes.filter( - (attribute) => - attribute.type === 'Attribute' || - attribute.type === 'BindDirective' || - attribute.type === 'SpreadAttribute' - ), - context - ), - 'value', - false, - true - ) - ) + b.call('$.maybe_selected', b.id('$$renderer'), b.member(object, 'value', false, true)) ); } } else { @@ -240,7 +251,9 @@ export function build_element_attributes(node, context) { build_attribute_value( attribute.value, context, - WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) + WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name), + false, + transform ) ).value; @@ -276,7 +289,9 @@ export function build_element_attributes(node, context) { const value = build_attribute_value( attribute.value, context, - WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) + WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name), + false, + transform ); // pre-escape and inline literal attributes : @@ -286,9 +301,11 @@ export function build_element_attributes(node, context) { } context.state.template.push(b.literal(` ${name}="${escape_html(value.value, true)}"`)); } else if (name === 'class') { - context.state.template.push(build_attr_class(class_directives, value, context, css_hash)); + context.state.template.push( + build_attr_class(class_directives, value, context, css_hash, transform) + ); } else if (name === 'style') { - context.state.template.push(build_attr_style(style_directives, value, context)); + context.state.template.push(build_attr_style(style_directives, value, context, transform)); } else { context.state.template.push( b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true) @@ -328,17 +345,25 @@ function get_attribute_name(element, attribute) { * @param {AST.RegularElement | AST.SvelteElement} element * @param {Array} attributes * @param {ComponentContext} context + * @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform */ -export function build_spread_object(element, attributes, context) { - return b.object( +export function build_spread_object(element, attributes, context, transform) { + let has_await = false; + + const object = b.object( attributes.map((attribute) => { if (attribute.type === 'Attribute') { const name = get_attribute_name(element, attribute); const value = build_attribute_value( attribute.value, context, - WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) + WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name), + false, + transform ); + + // TODO check has_await + return b.prop('init', b.key(name), value); } else if (attribute.type === 'BindDirective') { const name = get_attribute_name(element, attribute); @@ -346,12 +371,17 @@ export function build_spread_object(element, attributes, context) { attribute.expression.type === 'SequenceExpression' ? b.call(attribute.expression.expressions[0]) : /** @type {Expression} */ (context.visit(attribute.expression)); + return b.prop('init', b.key(name), value); } + has_await ||= attribute.metadata.expression.has_await; + return b.spread(/** @type {Expression} */ (context.visit(attribute))); }) ); + + return { object, has_await }; } /** @@ -361,39 +391,48 @@ export function build_spread_object(element, attributes, context) { * @param {AST.StyleDirective[]} style_directives * @param {AST.ClassDirective[]} class_directives * @param {ComponentContext} context + * @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform */ function build_element_spread_attributes( element, attributes, style_directives, class_directives, - context + context, + transform ) { let classes; let styles; let flags = 0; + let has_await = false; + if (class_directives.length) { - const properties = class_directives.map((directive) => - b.init( + const properties = class_directives.map((directive) => { + has_await ||= directive.metadata.expression.has_await; + + return b.init( directive.name, directive.expression.type === 'Identifier' && directive.expression.name === directive.name ? b.id(directive.name) : /** @type {Expression} */ (context.visit(directive.expression)) - ) - ); + ); + }); + classes = b.object(properties); } if (style_directives.length > 0) { - const properties = style_directives.map((directive) => - b.init( + const properties = style_directives.map((directive) => { + has_await ||= directive.metadata.expression.has_await; + + return b.init( directive.name, directive.value === true ? b.id(directive.name) - : build_attribute_value(directive.value, context, true) - ) - ); + : build_attribute_value(directive.value, context, true, false, transform) + ); + }); styles = b.object(properties); } @@ -406,15 +445,23 @@ function build_element_spread_attributes( flags |= ELEMENT_IS_INPUT; } - const object = build_spread_object(element, attributes, context); + const spread = build_spread_object(element, attributes, context, transform); const css_hash = element.metadata.scoped && context.state.analysis.css.hash ? b.literal(context.state.analysis.css.hash) - : b.null; + : undefined; + + const args = [spread.object, css_hash, classes, styles, flags ? b.literal(flags) : undefined]; + + let call = b.call('$.attributes', ...args); - const args = [object, css_hash, classes, styles, flags ? b.literal(flags) : undefined]; - context.state.template.push(b.call('$.spread_attributes', ...args)); + if (spread.has_await) { + call = b.call('$$renderer.push', b.thunk(call, true)); + context.state.template.push(b.stmt(call)); + } else { + context.state.template.push(call); + } } /** @@ -423,8 +470,9 @@ function build_element_spread_attributes( * @param {Expression} expression * @param {ComponentContext} context * @param {string | null} hash + * @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform */ -function build_attr_class(class_directives, expression, context, hash) { +function build_attr_class(class_directives, expression, context, hash, transform) { /** @type {ObjectExpression | undefined} */ let directives; @@ -434,7 +482,10 @@ function build_attr_class(class_directives, expression, context, hash) { b.prop( 'init', b.literal(directive.name), - /** @type {Expression} */ (context.visit(directive.expression, context.state)) + transform( + /** @type {Expression} */ (context.visit(directive.expression, context.state)), + directive.metadata.expression + ) ) ) ); @@ -457,9 +508,10 @@ function build_attr_class(class_directives, expression, context, hash) { * * @param {AST.StyleDirective[]} style_directives * @param {Expression} expression - * @param {ComponentContext} context + * @param {ComponentContext} context, + * @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform */ -function build_attr_style(style_directives, expression, context) { +function build_attr_style(style_directives, expression, context, transform) { /** @type {ArrayExpression | ObjectExpression | undefined} */ let directives; @@ -471,7 +523,7 @@ function build_attr_style(style_directives, expression, context) { const expression = directive.value === true ? b.id(directive.name) - : build_attribute_value(directive.value, context, true); + : build_attribute_value(directive.value, context, true, false, transform); let name = directive.name; if (name[0] !== '-' || name[1] !== '-') { diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js index 5e2b34496f..e7a31b5f07 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/utils.js @@ -175,13 +175,7 @@ export function build_template(template) { * @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform * @returns {Expression} */ -export function build_attribute_value( - value, - context, - trim_whitespace = false, - is_component = false, - transform = (expression) => expression -) { +export function build_attribute_value(value, context, trim_whitespace, is_component, transform) { if (value === true) { return b.true; } diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 1e2d75d6dc..436be6c021 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -108,13 +108,13 @@ export function css_props(renderer, is_html, props, component, dynamic = false) /** * @param {Record} attrs - * @param {string | null} css_hash + * @param {string} [css_hash] * @param {Record} [classes] * @param {Record} [styles] * @param {number} [flags] * @returns {string} */ -export function spread_attributes(attrs, css_hash, classes, styles, flags = 0) { +export function attributes(attrs, css_hash, classes, styles, flags = 0) { if (styles) { attrs.style = to_style(attrs.style, styles); } diff --git a/packages/svelte/tests/runtime-runes/samples/async-no-pending-attributes/Child.svelte b/packages/svelte/tests/runtime-runes/samples/async-no-pending-attributes/Child.svelte new file mode 100644 index 0000000000..ab87364cb7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-no-pending-attributes/Child.svelte @@ -0,0 +1,5 @@ + + +

{thing}

diff --git a/packages/svelte/tests/runtime-runes/samples/async-no-pending-attributes/_config.js b/packages/svelte/tests/runtime-runes/samples/async-no-pending-attributes/_config.js new file mode 100644 index 0000000000..8975978e17 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-no-pending-attributes/_config.js @@ -0,0 +1,24 @@ +import { tick } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip_mode: ['async-server'], + + async test({ assert, target }) { + await tick(); + + assert.htmlEqual( + target.innerHTML, + ` +

cool

+

beans

+ +

awesome

+

sauce

+ +

neato

+

potato

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-no-pending-attributes/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-no-pending-attributes/main.svelte new file mode 100644 index 0000000000..b0ec462bb7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-no-pending-attributes/main.svelte @@ -0,0 +1,12 @@ + + +

cool

+ + +

awesome

+ + +

neato

+