Merge branch 'async-attributes' into async-hydration

async-hydration
Rich Harris 19 hours ago
commit 27e36abbc7

@ -130,7 +130,7 @@ export function build_component(node, component_name, context) {
} else if (attribute.type === 'SpreadAttribute') {
const expression = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_state) {
if (attribute.metadata.expression.has_state || attribute.metadata.expression.has_await) {
props_and_spreads.push(
b.thunk(
attribute.metadata.expression.has_await || attribute.metadata.expression.has_call

@ -12,7 +12,8 @@ import {
process_children,
build_template,
build_attribute_value,
call_child_renderer
call_child_renderer,
PromiseOptimiser
} from './shared/utils.js';
/**
@ -27,21 +28,38 @@ export function RegularElement(node, context) {
...context.state,
namespace,
preserve_whitespace:
context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea'
context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea',
init: [],
template: []
};
const node_is_void = is_void(node.name);
context.state.template.push(b.literal(`<${node.name}`));
const body = build_element_attributes(node, { ...context, state });
context.state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance
const optimiser = new PromiseOptimiser();
state.template.push(b.literal(`<${node.name}`));
const body = build_element_attributes(node, { ...context, state }, optimiser.transform);
state.template.push(b.literal(node_is_void ? '/>' : '>')); // add `/>` for XHTML compliance
if ((node.name === 'script' || node.name === 'style') && node.fragment.nodes.length === 1) {
context.state.template.push(
state.template.push(
b.literal(/** @type {AST.Text} */ (node.fragment.nodes[0]).data),
b.literal(`</${node.name}>`)
);
// TODO this is a real edge case, would be good to DRY this out
if (optimiser.expressions.length > 0) {
context.state.template.push(
call_child_renderer(
b.block([optimiser.apply(), ...state.init, ...build_template(state.template)]),
true
)
);
} else {
context.state.init.push(...state.init);
context.state.template.push(...state.template);
}
return;
}
@ -93,26 +111,24 @@ export function RegularElement(node, context) {
select_with_value = true;
select_with_value_async ||= spread.metadata.expression.has_await;
const object = build_spread_object(
node,
node.attributes.filter(
(attribute) =>
attribute.type === 'Attribute' ||
attribute.type === 'BindDirective' ||
attribute.type === 'SpreadAttribute'
),
context,
optimiser.transform
);
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 +144,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(
@ -227,4 +249,16 @@ export function RegularElement(node, context) {
if (dev) {
state.template.push(b.stmt(b.call('$.pop_element')));
}
if (optimiser.expressions.length > 0) {
context.state.template.push(
call_child_renderer(
b.block([optimiser.apply(), ...state.init, ...build_template(state.template)]),
true
)
);
} else {
context.state.init.push(...state.init);
context.state.template.push(...state.template);
}
}

@ -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));

@ -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));

@ -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<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>} */
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,32 @@ 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 = build_spread_object(
node,
node.attributes.filter(
(attribute) =>
attribute.type === 'Attribute' ||
attribute.type === 'BindDirective' ||
attribute.type === 'SpreadAttribute'
),
context,
transform
);
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 +249,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 +287,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 +299,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 +343,23 @@ function get_attribute_name(element, attribute) {
* @param {AST.RegularElement | AST.SvelteElement} element
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.BindDirective>} 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) {
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 +367,20 @@ 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);
}
return b.spread(/** @type {Expression} */ (context.visit(attribute)));
return b.spread(
transform(
/** @type {Expression} */ (context.visit(attribute)),
attribute.metadata.expression
)
);
})
);
return object;
}
/**
@ -361,39 +390,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 +444,18 @@ function build_element_spread_attributes(
flags |= ELEMENT_IS_INPUT;
}
const object = build_spread_object(element, attributes, context);
const object = 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 = [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);
context.state.template.push(call);
}
/**
@ -423,8 +464,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 +476,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 +502,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 +517,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] !== '-') {

@ -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;
}

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

@ -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