WIP fix async attributes

pull/16797/head
Rich Harris 2 days ago
parent e5a75aecf8
commit 795b86ef9e

@ -12,7 +12,8 @@ import {
process_children, process_children,
build_template, build_template,
build_attribute_value, build_attribute_value,
call_child_renderer call_child_renderer,
PromiseOptimiser
} from './shared/utils.js'; } from './shared/utils.js';
/** /**
@ -32,8 +33,10 @@ export function RegularElement(node, context) {
const node_is_void = is_void(node.name); const node_is_void = is_void(node.name);
const optimiser = new PromiseOptimiser();
context.state.template.push(b.literal(`<${node.name}`)); 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 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) { if ((node.name === 'script' || node.name === 'style') && node.fragment.nodes.length === 1) {
@ -93,13 +96,7 @@ export function RegularElement(node, context) {
select_with_value = true; select_with_value = true;
select_with_value_async ||= spread.metadata.expression.has_await; select_with_value_async ||= spread.metadata.expression.has_await;
state.template.push( const { object, has_await } = build_spread_object(
b.stmt(
b.assignment(
'=',
b.id('$$renderer.local.select_value'),
b.member(
build_spread_object(
node, node,
node.attributes.filter( node.attributes.filter(
(attribute) => (attribute) =>
@ -107,12 +104,18 @@ export function RegularElement(node, context) {
attribute.type === 'BindDirective' || attribute.type === 'BindDirective' ||
attribute.type === 'SpreadAttribute' attribute.type === 'SpreadAttribute'
), ),
context context,
), optimiser.transform
'value', );
false,
true // TODO use has_await
)
state.template.push(
b.stmt(
b.assignment(
'=',
b.id('$$renderer.local.select_value'),
b.member(object, 'value', false, true)
) )
) )
); );
@ -128,7 +131,13 @@ export function RegularElement(node, context) {
const left = b.id('$$renderer.local.select_value'); const left = b.id('$$renderer.local.select_value');
if (value.type === 'Attribute') { if (value.type === 'Attribute') {
state.template.push( 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') { } else if (value.type === 'BindDirective') {
state.template.push( state.template.push(

@ -23,7 +23,13 @@ export function SvelteBoundary(node, context) {
if (pending_attribute || pending_snippet) { if (pending_attribute || pending_snippet) {
const pending = pending_attribute const pending = pending_attribute
? b.call( ? 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') b.id('$$renderer')
) )
: /** @type {BlockStatement} */ (context.visit(pending_snippet.body)); : /** @type {BlockStatement} */ (context.visit(pending_snippet.body));

@ -6,7 +6,7 @@ import { dev, locator } from '../../../../state.js';
import * as b from '#compiler/builders'; import * as b from '#compiler/builders';
import { determine_namespace_for_children } from '../../utils.js'; import { determine_namespace_for_children } from '../../utils.js';
import { build_element_attributes } from './shared/element.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 * @param {AST.SvelteElement} node
@ -37,7 +37,10 @@ export function SvelteElement(node, context) {
init: [] init: []
}; };
build_element_attributes(node, { ...context, state }); // TODO use this
const optimiser = new PromiseOptimiser();
build_element_attributes(node, { ...context, state }, optimiser.transform);
if (dev) { if (dev) {
const location = /** @type {Location} */ (locator(node.start)); const location = /** @type {Location} */ (locator(node.start));

@ -1,5 +1,5 @@
/** @import { ArrayExpression, Expression, Literal, ObjectExpression } from 'estree' */ /** @import { ArrayExpression, Expression, Literal, ObjectExpression } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */ /** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */
import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js'; import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js';
import { binding_properties } from '../../../../bindings.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. * their output to be the child content instead. In this case, an object is returned.
* @param {AST.RegularElement | AST.SvelteElement} node * @param {AST.RegularElement | AST.SvelteElement} node
* @param {import('zimmerframe').Context<AST.SvelteNode, ComponentServerTransformState>} context * @param {import('zimmerframe').Context<AST.SvelteNode, ComponentServerTransformState>} 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<AST.Attribute | AST.SpreadAttribute>} */ /** @type {Array<AST.Attribute | AST.SpreadAttribute>} */
const attributes = []; const attributes = [];
@ -62,7 +63,11 @@ export function build_element_attributes(node, context) {
// also see related code in analysis phase // also see related code in analysis phase
attribute.value[0].data = '\n' + attribute.value[0].data; 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') { } else if (node.name !== 'select') {
// omit value attribute for select elements, it's irrelevant for the initially selected value and has no // 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) // 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 expression: is_checkbox
? b.call( ? b.call(
b.member(attribute.expression, 'includes'), b.member(attribute.expression, 'includes'),
build_attribute_value(value_attribute.value, context) build_attribute_value(value_attribute.value, context, false, false, transform)
) )
: b.binary( : b.binary(
'===', '===',
attribute.expression, attribute.expression,
build_attribute_value(value_attribute.value, context) build_attribute_value(value_attribute.value, context, false, false, transform)
), ),
metadata: { metadata: {
expression: create_expression_metadata() expression: create_expression_metadata()
@ -202,14 +207,19 @@ export function build_element_attributes(node, context) {
} }
if (has_spread) { 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') { if (node.name === 'option') {
context.state.template.push( // TODO this is all wrong, it inlines the spread twice
b.call(
'$.maybe_selected', const { object, has_await } = build_spread_object(
b.id('$$renderer'),
b.member(
build_spread_object(
node, node,
node.attributes.filter( node.attributes.filter(
(attribute) => (attribute) =>
@ -217,13 +227,14 @@ export function build_element_attributes(node, context) {
attribute.type === 'BindDirective' || attribute.type === 'BindDirective' ||
attribute.type === 'SpreadAttribute' attribute.type === 'SpreadAttribute'
), ),
context context,
), transform
'value', );
false,
true // TODO use has_await?
)
) context.state.template.push(
b.call('$.maybe_selected', b.id('$$renderer'), b.member(object, 'value', false, true))
); );
} }
} else { } else {
@ -240,7 +251,9 @@ export function build_element_attributes(node, context) {
build_attribute_value( build_attribute_value(
attribute.value, attribute.value,
context, context,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name),
false,
transform
) )
).value; ).value;
@ -276,7 +289,9 @@ export function build_element_attributes(node, context) {
const value = build_attribute_value( const value = build_attribute_value(
attribute.value, attribute.value,
context, context,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name),
false,
transform
); );
// pre-escape and inline literal attributes : // 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)}"`)); context.state.template.push(b.literal(` ${name}="${escape_html(value.value, true)}"`));
} else if (name === 'class') { } 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') { } 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 { } else {
context.state.template.push( context.state.template.push(
b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true) 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 {AST.RegularElement | AST.SvelteElement} element
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} attributes * @param {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} attributes
* @param {ComponentContext} context * @param {ComponentContext} context
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
*/ */
export function build_spread_object(element, attributes, context) { export function build_spread_object(element, attributes, context, transform) {
return b.object( let has_await = false;
const object = b.object(
attributes.map((attribute) => { attributes.map((attribute) => {
if (attribute.type === 'Attribute') { if (attribute.type === 'Attribute') {
const name = get_attribute_name(element, attribute); const name = get_attribute_name(element, attribute);
const value = build_attribute_value( const value = build_attribute_value(
attribute.value, attribute.value,
context, 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); return b.prop('init', b.key(name), value);
} else if (attribute.type === 'BindDirective') { } else if (attribute.type === 'BindDirective') {
const name = get_attribute_name(element, attribute); const name = get_attribute_name(element, attribute);
@ -346,12 +371,17 @@ export function build_spread_object(element, attributes, context) {
attribute.expression.type === 'SequenceExpression' attribute.expression.type === 'SequenceExpression'
? b.call(attribute.expression.expressions[0]) ? b.call(attribute.expression.expressions[0])
: /** @type {Expression} */ (context.visit(attribute.expression)); : /** @type {Expression} */ (context.visit(attribute.expression));
return b.prop('init', b.key(name), value); return b.prop('init', b.key(name), value);
} }
has_await ||= attribute.metadata.expression.has_await;
return b.spread(/** @type {Expression} */ (context.visit(attribute))); 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.StyleDirective[]} style_directives
* @param {AST.ClassDirective[]} class_directives * @param {AST.ClassDirective[]} class_directives
* @param {ComponentContext} context * @param {ComponentContext} context
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
*/ */
function build_element_spread_attributes( function build_element_spread_attributes(
element, element,
attributes, attributes,
style_directives, style_directives,
class_directives, class_directives,
context context,
transform
) { ) {
let classes; let classes;
let styles; let styles;
let flags = 0; let flags = 0;
let has_await = false;
if (class_directives.length) { if (class_directives.length) {
const properties = class_directives.map((directive) => const properties = class_directives.map((directive) => {
b.init( has_await ||= directive.metadata.expression.has_await;
return b.init(
directive.name, directive.name,
directive.expression.type === 'Identifier' && directive.expression.name === directive.name directive.expression.type === 'Identifier' && directive.expression.name === directive.name
? b.id(directive.name) ? b.id(directive.name)
: /** @type {Expression} */ (context.visit(directive.expression)) : /** @type {Expression} */ (context.visit(directive.expression))
)
); );
});
classes = b.object(properties); classes = b.object(properties);
} }
if (style_directives.length > 0) { if (style_directives.length > 0) {
const properties = style_directives.map((directive) => const properties = style_directives.map((directive) => {
b.init( has_await ||= directive.metadata.expression.has_await;
return b.init(
directive.name, directive.name,
directive.value === true directive.value === true
? b.id(directive.name) ? b.id(directive.name)
: build_attribute_value(directive.value, context, true) : build_attribute_value(directive.value, context, true, false, transform)
)
); );
});
styles = b.object(properties); styles = b.object(properties);
} }
@ -406,15 +445,23 @@ function build_element_spread_attributes(
flags |= ELEMENT_IS_INPUT; flags |= ELEMENT_IS_INPUT;
} }
const object = build_spread_object(element, attributes, context); const spread = build_spread_object(element, attributes, context, transform);
const css_hash = const css_hash =
element.metadata.scoped && context.state.analysis.css.hash element.metadata.scoped && context.state.analysis.css.hash
? b.literal(context.state.analysis.css.hash) ? b.literal(context.state.analysis.css.hash)
: b.null; : undefined;
const args = [object, css_hash, classes, styles, flags ? b.literal(flags) : undefined]; const args = [spread.object, css_hash, classes, styles, flags ? b.literal(flags) : undefined];
context.state.template.push(b.call('$.spread_attributes', ...args));
let call = b.call('$.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 {Expression} expression
* @param {ComponentContext} context * @param {ComponentContext} context
* @param {string | null} hash * @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} */ /** @type {ObjectExpression | undefined} */
let directives; let directives;
@ -434,7 +482,10 @@ function build_attr_class(class_directives, expression, context, hash) {
b.prop( b.prop(
'init', 'init',
b.literal(directive.name), 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 {AST.StyleDirective[]} style_directives
* @param {Expression} expression * @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} */ /** @type {ArrayExpression | ObjectExpression | undefined} */
let directives; let directives;
@ -471,7 +523,7 @@ function build_attr_style(style_directives, expression, context) {
const expression = const expression =
directive.value === true directive.value === true
? b.id(directive.name) ? b.id(directive.name)
: build_attribute_value(directive.value, context, true); : build_attribute_value(directive.value, context, true, false, transform);
let name = directive.name; let name = directive.name;
if (name[0] !== '-' || name[1] !== '-') { if (name[0] !== '-' || name[1] !== '-') {

@ -175,13 +175,7 @@ export function build_template(template) {
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform * @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
* @returns {Expression} * @returns {Expression}
*/ */
export function build_attribute_value( export function build_attribute_value(value, context, trim_whitespace, is_component, transform) {
value,
context,
trim_whitespace = false,
is_component = false,
transform = (expression) => expression
) {
if (value === true) { if (value === true) {
return b.true; return b.true;
} }

@ -108,13 +108,13 @@ export function css_props(renderer, is_html, props, component, dynamic = false)
/** /**
* @param {Record<string, unknown>} attrs * @param {Record<string, unknown>} attrs
* @param {string | null} css_hash * @param {string} [css_hash]
* @param {Record<string, boolean>} [classes] * @param {Record<string, boolean>} [classes]
* @param {Record<string, string>} [styles] * @param {Record<string, string>} [styles]
* @param {number} [flags] * @param {number} [flags]
* @returns {string} * @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) { if (styles) {
attrs.style = to_style(attrs.style, styles); attrs.style = to_style(attrs.style, styles);
} }

@ -0,0 +1,5 @@
<script>
let { thing } = $props();
</script>
<p>{thing}</p>

@ -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,
`
<p class="cool">cool</p>
<p>beans</p>
<p class="awesome">awesome</p>
<p>sauce</p>
<p class="neato">neato</p>
<p>potato</p>
`
);
}
});

@ -0,0 +1,12 @@
<script>
import Child from './Child.svelte';
</script>
<p {...await { class: 'cool'}}>cool</p>
<Child {...await { thing: 'beans' }} />
<p class={await 'awesome'}>awesome</p>
<Child thing={await 'sauce'} />
<p {...{}} class={await 'neato'}>neato</p>
<Child {...{}} thing={await 'potato'} />
Loading…
Cancel
Save