fix: async hydration (#16797)

* WIP fix async hydration

* add renderer.async method

* update tests

* changeset

* oops

* WIP fix async attributes

* fix

* fix

* all tests passing

* unused

* unused

* remove_nodes -> skip_nodes

* hydration boundaries around slots

* reorder arguments

* add select method

* WIP simplify selects

* WIP

* simplify

* renderer.title

* delete unused compact method

* simplify

* simplify

* simplify

* simplify

* fix TODO

* remove outdated TODO

* remove outdated TODO

* rename call_child_renderer -> create_child_block

* burrito

* add a couple of unit tests
pull/16801/head
Rich Harris 21 hours ago committed by GitHub
parent 1c8456885c
commit 5c09035685
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: async hydration

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

@ -40,7 +40,11 @@ import { TitleElement } from './visitors/TitleElement.js';
import { UpdateExpression } from './visitors/UpdateExpression.js';
import { VariableDeclaration } from './visitors/VariableDeclaration.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
import { call_child_renderer, call_component_renderer } from './visitors/shared/utils.js';
import {
create_child_block,
call_component_renderer,
create_async_block
} from './visitors/shared/utils.js';
/** @type {Visitors} */
const global_visitors = {
@ -244,7 +248,7 @@ export function server_component(analysis, options) {
]);
if (analysis.instance.has_await) {
component_block = b.block([call_child_renderer(component_block, true)]);
component_block = b.block([create_child_block(component_block, true)]);
}
// trick esrap into including comments

@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
import { block_close, call_child_renderer } from './shared/utils.js';
import { block_close, create_async_block } from './shared/utils.js';
/**
* @param {AST.AwaitBlock} node
@ -26,7 +26,7 @@ export function AwaitBlock(node, context) {
);
if (node.metadata.expression.has_await) {
statement = call_child_renderer(b.block([statement]), true);
statement = create_async_block(b.block([statement]));
}
context.state.template.push(statement, block_close);

@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
import { block_close, block_open, block_open_else, call_child_renderer } from './shared/utils.js';
import { block_close, block_open, block_open_else, create_async_block } from './shared/utils.js';
/**
* @param {AST.EachBlock} node
@ -64,7 +64,7 @@ export function EachBlock(node, context) {
}
if (node.metadata.expression.has_await) {
state.template.push(call_child_renderer(block, true), block_close);
state.template.push(create_async_block(block), block_close);
} else {
state.template.push(...block.body, block_close);
}

@ -2,7 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
import { block_close, block_open, block_open_else, call_child_renderer } from './shared/utils.js';
import { block_close, block_open, block_open_else, create_async_block } from './shared/utils.js';
/**
* @param {AST.IfBlock} node
@ -24,7 +24,7 @@ export function IfBlock(node, context) {
let statement = b.if(test, consequent, alternate);
if (node.metadata.expression.has_await) {
statement = call_child_renderer(b.block([statement]), true);
statement = create_async_block(b.block([statement]));
}
context.state.template.push(statement, block_close);

@ -1,4 +1,4 @@
/** @import { Expression, Statement } from 'estree' */
/** @import { Expression } from 'estree' */
/** @import { Location } from 'locate-character' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../types.js' */
@ -12,7 +12,8 @@ import {
process_children,
build_template,
build_attribute_value,
call_child_renderer
create_child_block,
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(
create_child_block(
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;
}
@ -77,114 +95,92 @@ export function RegularElement(node, context) {
);
}
let select_with_value = false;
let select_with_value_async = false;
const template_start = state.template.length;
if (node.name === 'select') {
const value = node.attributes.find(
if (
node.name === 'select' &&
node.attributes.some(
(attribute) =>
(attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
attribute.name === 'value'
((attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
attribute.name === 'value') ||
attribute.type === 'SpreadAttribute'
)
) {
const attributes = build_spread_object(
node,
node.attributes.filter(
(attribute) =>
attribute.type === 'Attribute' ||
attribute.type === 'BindDirective' ||
attribute.type === 'SpreadAttribute'
),
context,
optimiser.transform
);
const spread = node.attributes.find((attribute) => attribute.type === 'SpreadAttribute');
if (spread) {
select_with_value = true;
select_with_value_async ||= spread.metadata.expression.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
)
)
)
const inner_state = { ...state, template: [], init: [] };
process_children(trimmed, { ...context, state: inner_state });
const fn = b.arrow(
[b.id('$$renderer')],
b.block([...state.init, ...build_template(inner_state.template)])
);
const statement = b.stmt(b.call('$$renderer.select', attributes, fn));
if (optimiser.expressions.length > 0) {
context.state.template.push(
create_child_block(b.block([optimiser.apply(), ...state.init, statement]), true)
);
} else if (value) {
select_with_value = true;
if (value.type === 'Attribute' && value.value !== true) {
select_with_value_async ||= (Array.isArray(value.value) ? value.value : [value.value]).some(
(tag) => tag.type === 'ExpressionTag' && tag.metadata.expression.has_await
);
}
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)))
);
} else if (value.type === 'BindDirective') {
state.template.push(
b.stmt(
b.assignment(
'=',
left,
value.expression.type === 'SequenceExpression'
? /** @type {Expression} */ (context.visit(b.call(value.expression.expressions[0])))
: /** @type {Expression} */ (context.visit(value.expression))
)
)
);
}
} else {
context.state.template.push(...state.init, statement);
}
return;
}
if (
node.name === 'option' &&
!node.attributes.some(
(attribute) =>
attribute.type === 'SpreadAttribute' ||
((attribute.type === 'Attribute' || attribute.type === 'BindDirective') &&
attribute.name === 'value')
)
) {
if (node.name === 'option') {
const attributes = build_spread_object(
node,
node.attributes.filter(
(attribute) =>
attribute.type === 'Attribute' ||
attribute.type === 'BindDirective' ||
attribute.type === 'SpreadAttribute'
),
context,
optimiser.transform
);
let body;
if (node.metadata.synthetic_value_node) {
state.template.push(
b.stmt(
b.call(
'$.simple_valueless_option',
b.id('$$renderer'),
b.thunk(
node.metadata.synthetic_value_node.expression,
node.metadata.synthetic_value_node.metadata.expression.has_await
)
)
)
body = optimiser.transform(
node.metadata.synthetic_value_node.expression,
node.metadata.synthetic_value_node.metadata.expression
);
} else {
const inner_state = { ...state, template: [], init: [] };
process_children(trimmed, { ...context, state: inner_state });
state.template.push(
b.stmt(
b.call(
'$.valueless_option',
b.id('$$renderer'),
b.arrow(
[b.id('$$renderer')],
b.block([...inner_state.init, ...build_template(inner_state.template)])
)
)
)
body = b.arrow(
[b.id('$$renderer')],
b.block([...state.init, ...build_template(inner_state.template)])
);
}
} else if (body !== null) {
const statement = b.stmt(b.call('$$renderer.option', attributes, body));
if (optimiser.expressions.length > 0) {
context.state.template.push(
create_child_block(b.block([optimiser.apply(), ...state.init, statement]), true)
);
} else {
context.state.template.push(...state.init, statement);
}
return;
}
if (body !== null) {
// if this is a `<textarea>` value or a contenteditable binding, we only add
// the body if the attribute/binding is falsy
const inner_state = { ...state, template: [], init: [] };
@ -209,17 +205,6 @@ export function RegularElement(node, context) {
process_children(trimmed, { ...context, state });
}
if (select_with_value) {
// we need to create a child scope so that the `select_value` only applies children of this select element
// in an async world, we could technically have two adjacent select elements with async children, in which case
// the second element's select_value would override the first element's select_value if the children of the first
// element hadn't resolved prior to hitting the second element.
const elements = state.template.splice(template_start, Infinity);
state.template.push(
call_child_renderer(b.block(build_template(elements)), select_with_value_async)
);
}
if (!node_is_void) {
state.template.push(b.literal(`</${node.name}>`));
}
@ -227,4 +212,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(
create_child_block(
b.block([optimiser.apply(), ...state.init, ...build_template(state.template)]),
true
)
);
} else {
context.state.init.push(...state.init);
context.state.template.push(...state.template);
}
}

@ -3,10 +3,11 @@
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
import {
empty_comment,
build_attribute_value,
PromiseOptimiser,
call_child_renderer
create_async_block,
block_open,
block_close
} from './shared/utils.js';
/**
@ -32,9 +33,9 @@ export function SlotElement(node, context) {
const value = build_attribute_value(
attribute.value,
context,
optimiser.transform,
false,
true,
optimiser.transform
true
);
if (attribute.name === 'name') {
@ -66,8 +67,8 @@ export function SlotElement(node, context) {
const statement =
optimiser.expressions.length > 0
? call_child_renderer(b.block([optimiser.apply(), b.stmt(slot)]), true)
? create_async_block(b.block([optimiser.apply(), b.stmt(slot)]))
: b.stmt(slot);
context.state.template.push(empty_comment, statement, empty_comment);
context.state.template.push(block_open, statement, block_close);
}

@ -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,
(expression) => expression,
false,
true
),
b.id('$$renderer')
)
: /** @type {BlockStatement} */ (context.visit(pending_snippet.body));

@ -1,12 +1,12 @@
/** @import { Location } from 'locate-character' */
/** @import { BlockStatement, Expression } from 'estree' */
/** @import { BlockStatement, Expression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
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, create_child_block, PromiseOptimiser } from './shared/utils.js';
/**
* @param {AST.SvelteElement} node
@ -37,7 +37,9 @@ export function SvelteElement(node, context) {
init: []
};
build_element_attributes(node, { ...context, state });
const optimiser = new PromiseOptimiser();
build_element_attributes(node, { ...context, state }, optimiser.transform);
if (dev) {
const location = /** @type {Location} */ (locator(node.start));
@ -57,18 +59,23 @@ export function SvelteElement(node, context) {
const attributes = b.block([...state.init, ...build_template(state.template)]);
const children = /** @type {BlockStatement} */ (context.visit(node.fragment, state));
context.state.template.push(
b.stmt(
b.call(
'$.element',
b.id('$$renderer'),
tag,
attributes.body.length > 0 && b.thunk(attributes),
children.body.length > 0 && b.thunk(children)
)
/** @type {Statement} */
let statement = b.stmt(
b.call(
'$.element',
b.id('$$renderer'),
tag,
attributes.body.length > 0 && b.thunk(attributes),
children.body.length > 0 && b.thunk(children)
)
);
if (optimiser.expressions.length > 0) {
statement = create_child_block(b.block([optimiser.apply(), statement]), true);
}
context.state.template.push(statement);
if (dev) {
context.state.template.push(b.stmt(b.call('$.pop_element')));
}

@ -14,6 +14,8 @@ export function TitleElement(node, context) {
template.push(b.literal('</title>'));
context.state.init.push(
b.stmt(b.call('$.build_title', b.id('$$renderer'), b.thunk(b.block(build_template(template)))))
b.stmt(
b.call('$$renderer.title', b.arrow([b.id('$$renderer')], b.block(build_template(template))))
)
);
}

@ -4,8 +4,9 @@
import {
empty_comment,
build_attribute_value,
call_child_renderer,
PromiseOptimiser
create_async_block,
PromiseOptimiser,
build_template
} from './utils.js';
import * as b from '#compiler/builders';
import { is_element_node } from '../../../../nodes.js';
@ -91,9 +92,9 @@ export function build_inline_component(node, expression, context) {
const value = build_attribute_value(
attribute.value,
context,
optimiser.transform,
false,
true,
optimiser.transform
true
);
if (attribute.name.startsWith('--')) {
@ -318,7 +319,7 @@ export function build_inline_component(node, expression, context) {
}
if (optimiser.expressions.length > 0) {
statement = call_child_renderer(b.block([optimiser.apply(), statement]), true);
statement = create_async_block(b.block([optimiser.apply(), statement]));
}
if (dynamic && custom_css_props.length === 0) {
@ -327,7 +328,11 @@ export function build_inline_component(node, expression, context) {
context.state.template.push(statement);
if (!context.state.skip_hydration_boundaries && custom_css_props.length === 0) {
if (
!context.state.skip_hydration_boundaries &&
custom_css_props.length === 0 &&
optimiser.expressions.length === 0
) {
context.state.template.push(empty_comment);
}
}

@ -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,8 @@ 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, 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 +152,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, transform)
)
: b.binary(
'===',
attribute.expression,
build_attribute_value(value_attribute.value, context)
build_attribute_value(value_attribute.value, context, transform)
),
metadata: {
expression: create_expression_metadata()
@ -202,30 +204,14 @@ export function build_element_attributes(node, context) {
}
if (has_spread) {
build_element_spread_attributes(node, attributes, style_directives, class_directives, context);
if (node.name === 'option') {
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
)
)
);
}
build_element_spread_attributes(
node,
attributes,
style_directives,
class_directives,
context,
transform
);
} else {
const css_hash = node.metadata.scoped ? context.state.analysis.css.hash : null;
@ -240,6 +226,7 @@ export function build_element_attributes(node, context) {
build_attribute_value(
attribute.value,
context,
transform,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
)
).value;
@ -260,22 +247,13 @@ export function build_element_attributes(node, context) {
);
}
if (node.name === 'option' && name === 'value') {
context.state.template.push(
b.call(
'$.maybe_selected',
b.id('$$renderer'),
literal_value != null ? b.literal(/** @type {any} */ (literal_value)) : b.void0
)
);
}
continue;
}
const value = build_attribute_value(
attribute.value,
context,
transform,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
);
@ -286,18 +264,16 @@ 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)
);
}
if (name === 'value' && node.name === 'option') {
context.state.template.push(b.call('$.maybe_selected', b.id('$$renderer'), value));
}
}
}
@ -328,17 +304,20 @@ 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,
transform,
WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name)
);
return b.prop('init', b.key(name), value);
} else if (attribute.type === 'BindDirective') {
const name = get_attribute_name(element, attribute);
@ -346,12 +325,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 +348,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, transform, true)
);
});
styles = b.object(properties);
}
@ -406,15 +402,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 +422,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 +434,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 +460,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 +475,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, transform, true);
let name = directive.name;
if (name[0] !== '-' || name[1] !== '-') {

@ -170,17 +170,17 @@ export function build_template(template) {
*
* @param {AST.Attribute['value']} value
* @param {ComponentContext} context
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
* @param {boolean} trim_whitespace
* @param {boolean} is_component
* @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
* @returns {Expression}
*/
export function build_attribute_value(
value,
context,
transform,
trim_whitespace = false,
is_component = false,
transform = (expression) => expression
is_component = false
) {
if (value === true) {
return b.true;
@ -262,14 +262,23 @@ export function build_getter(node, state) {
}
/**
* Creates a `$$renderer.child(...)` expression statement
* @param {BlockStatement | Expression} body
* @param {boolean} async
* @returns {Statement}
*/
export function call_child_renderer(body, async) {
export function create_child_block(body, async) {
return b.stmt(b.call('$$renderer.child', b.arrow([b.id('$$renderer')], body, async)));
}
/**
* Creates a `$$renderer.async(...)` expression statement
* @param {BlockStatement | Expression} body
*/
export function create_async_block(body) {
return b.stmt(b.call('$$renderer.async', b.arrow([b.id('$$renderer')], body, true)));
}
/**
* @param {BlockStatement | Expression} body
* @param {Identifier | false} component_fn_id

@ -1,6 +1,14 @@
/** @import { TemplateNode, Value } from '#client' */
import { flatten } from '../../reactivity/async.js';
import { get } from '../../runtime.js';
import {
hydrate_next,
hydrate_node,
hydrating,
set_hydrate_node,
set_hydrating,
skip_nodes
} from '../hydration.js';
import { get_boundary } from './boundary.js';
/**
@ -13,7 +21,22 @@ export function async(node, expressions, fn) {
boundary.update_pending_count(1);
var was_hydrating = hydrating;
if (was_hydrating) {
hydrate_next();
var previous_hydrate_node = hydrate_node;
var end = skip_nodes(false);
set_hydrate_node(end);
}
flatten([], expressions, (values) => {
if (was_hydrating) {
set_hydrating(true);
set_hydrate_node(previous_hydrate_node);
}
try {
// get values eagerly to avoid creating blocks if they reject
for (const d of values) get(d);
@ -22,5 +45,9 @@ export function async(node, expressions, fn) {
} finally {
boundary.update_pending_count(-1);
}
if (was_hydrating) {
set_hydrating(false);
}
});
}

@ -8,7 +8,7 @@ import {
hydrate_next,
hydrate_node,
hydrating,
remove_nodes,
skip_nodes,
set_hydrate_node,
set_hydrating
} from '../hydration.js';
@ -140,7 +140,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
if (mismatch) {
// Hydration mismatch: remove everything inside the anchor and start fresh
anchor = remove_nodes();
anchor = skip_nodes();
set_hydrate_node(anchor);
set_hydrating(false);

@ -21,7 +21,7 @@ import {
hydrate_node,
hydrating,
next,
remove_nodes,
skip_nodes,
set_hydrate_node
} from '../hydration.js';
import { get_next_sibling } from '../operations.js';
@ -335,7 +335,7 @@ export class Boundary {
if (hydrating) {
set_hydrate_node(/** @type {TemplateNode} */ (this.#hydrate_open));
next();
set_hydrate_node(remove_nodes());
set_hydrate_node(skip_nodes());
}
var did_reset = false;

@ -14,7 +14,7 @@ import {
hydrate_node,
hydrating,
read_hydration_instruction,
remove_nodes,
skip_nodes,
set_hydrate_node,
set_hydrating
} from '../hydration.js';
@ -209,7 +209,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
if (is_else !== (length === 0)) {
// hydration mismatch — remove the server-rendered DOM and start over
anchor = remove_nodes();
anchor = skip_nodes();
set_hydrate_node(anchor);
set_hydrating(false);
@ -259,7 +259,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
// remove excess nodes
if (length > 0) {
set_hydrate_node(remove_nodes());
set_hydrate_node(skip_nodes());
}
}

@ -6,7 +6,7 @@ import {
hydrate_node,
hydrating,
read_hydration_instruction,
remove_nodes,
skip_nodes,
set_hydrate_node,
set_hydrating
} from '../hydration.js';
@ -93,7 +93,7 @@ export function if_block(node, fn, elseif = false) {
if (!!condition === is_else) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen with `{#if browser}...{/if}`, for example
anchor = remove_nodes();
anchor = skip_nodes();
set_hydrate_node(anchor);
set_hydrating(false);

@ -81,9 +81,10 @@ export function next(count = 1) {
}
/**
* Removes all nodes starting at `hydrate_node` up until the next hydration end comment
* Skips or removes (depending on {@link remove}) all nodes starting at `hydrate_node` up until the next hydration end comment
* @param {boolean} remove
*/
export function remove_nodes() {
export function skip_nodes(remove = true) {
var depth = 0;
var node = hydrate_node;
@ -100,7 +101,7 @@ export function remove_nodes() {
}
var next = /** @type {TemplateNode} */ (get_next_sibling(node));
node.remove();
if (remove) node.remove();
node = next;
}
}

@ -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);
}
@ -493,121 +493,3 @@ export function derived(fn) {
return updated_value;
};
}
/**
*
* @param {Renderer} renderer
* @param {unknown} value
*/
export function maybe_selected(renderer, value) {
return value === renderer.local.select_value ? ' selected' : '';
}
/**
* When an `option` element has no `value` attribute, we need to treat the child
* content as its `value` to determine whether we should apply the `selected` attribute.
* This has to be done at runtime, for hopefully obvious reasons. It is also complicated,
* for sad reasons.
* @param {Renderer} renderer
* @param {((renderer: Renderer) => void | Promise<void>)} children
* @returns {void}
*/
export function valueless_option(renderer, children) {
const i = renderer.length;
// prior to children, `renderer` has some combination of string/unresolved renderer that ends in `<option ...>`
renderer.child(children);
// post-children, `renderer` has child content, possibly also with some number of hydration comments.
// we can compact this last chunk of content to see if it matches the select value...
renderer.compact({
start: i,
fn: (content) => {
if (content.body.replace(/<!---->/g, '') === renderer.local.select_value) {
// ...and if it does match the select value, we can compact the part of the renderer representing the `<option ...>`
// to add the `selected` attribute to the end.
renderer.compact({
start: i - 1,
end: i,
fn: (content) => {
return { body: content.body.slice(0, -1) + ' selected>', head: content.head };
}
});
}
return content;
}
});
}
/**
* In the special case where an `option` element has no `value` attribute but
* the children of the `option` element are a single expression, we can simplify
* by running the children and passing the resulting value, which means
* we don't have to do all of the same parsing nonsense. It also means we can avoid
* coercing everything to a string.
* @param {Renderer} renderer
* @param {(() => unknown)} child
*/
export function simple_valueless_option(renderer, child) {
const result = child();
/**
* @param {AccumulatedContent} content
* @param {unknown} child_value
* @returns {AccumulatedContent}
*/
const mark_selected = (content, child_value) => {
if (child_value === renderer.local.select_value) {
return { body: content.body.slice(0, -1) + ' selected>', head: content.head };
}
return content;
};
renderer.compact({
start: renderer.length - 1,
fn: (content) => {
if (result instanceof Promise) {
return result.then((child_value) => mark_selected(content, child_value));
}
return mark_selected(content, result);
}
});
renderer.child((child_renderer) => {
if (result instanceof Promise) {
return result.then((child_value) => {
child_renderer.push(escape_html(child_value));
});
}
child_renderer.push(escape_html(result));
});
}
/**
* Since your document can only have one `title`, we have to have some sort of algorithm for determining
* which one "wins". To do this, we perform a depth-first comparison of where the title was encountered --
* later ones "win" over earlier ones, regardless of what order the promises resolve in. To accomodate this, we:
* - Figure out where we are in the content tree (`get_path`)
* - Render the title in its own child so that it has a defined "slot" in the renderer
* - Compact that spot so that we get the entire rendered contents of the title
* - Attempt to set the global title (this is where the "wins" logic based on the path happens)
*
* TODO we could optimize this by not even rendering the title if the path wouldn't be accepted
*
* @param {Renderer} renderer
* @param {((renderer: Renderer) => void | Promise<void>)} children
*/
export function build_title(renderer, children) {
const path = renderer.get_path();
const i = renderer.length;
renderer.child(children);
renderer.compact({
start: i,
fn: ({ head }) => {
renderer.global.set_title(head, path);
// since we can only ever render the title in this chunk, and title rendering is handled specially,
// we can just ditch the results after we've saved them globally
return { head: '', body: '' };
}
});
}

@ -6,6 +6,7 @@ import { pop, push, set_ssr_context, ssr_context } from './context.js';
import * as e from './errors.js';
import * as w from './warnings.js';
import { BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js';
import { attributes } from './index.js';
/** @typedef {'head' | 'body'} RendererType */
/** @typedef {{ [key in RendererType]: string }} AccumulatedContent */
@ -55,12 +56,10 @@ export class Renderer {
#parent;
/**
* Asynchronous work associated with this renderer. `initial` is the promise from the function
* this renderer was passed to (if that function was async), and `followup` is any any additional
* work from `compact` calls that needs to complete prior to collecting this renderer's content.
* @type {{ initial: Promise<void> | undefined, followup: Promise<void>[] }}
* Asynchronous work associated with this renderer
* @type {Promise<void> | undefined}
*/
promises = { initial: undefined, followup: [] };
promise = undefined;
/**
* State which is associated with the content tree as a whole.
@ -101,6 +100,15 @@ export class Renderer {
head.child(fn);
}
/**
* @param {(renderer: Renderer) => void} fn
*/
async(fn) {
this.#out.push(BLOCK_OPEN);
this.child(fn);
this.#out.push(BLOCK_CLOSE);
}
/**
* Create a child renderer. The child renderer inherits the state from the parent,
* but has its own content.
@ -129,7 +137,7 @@ export class Renderer {
}
// just to avoid unhandled promise rejections -- we'll end up throwing in `collect_async` if something fails
result.catch(() => {});
child.promises.initial = result;
child.promise = result;
}
return child;
@ -150,29 +158,100 @@ export class Renderer {
}
/**
* @param {string | (() => Promise<string>)} content
* @param {Record<string, any>} attrs
* @param {(renderer: Renderer) => void} fn
*/
push(content) {
if (typeof content === 'function') {
this.child(async (renderer) => renderer.push(await content()));
select({ value, ...attrs }, fn) {
this.push(`<select${attributes(attrs)}>`);
this.child((renderer) => {
renderer.local.select_value = value;
fn(renderer);
});
this.push('</select>');
}
/**
* @param {Record<string, any>} attrs
* @param {string | number | boolean | ((renderer: Renderer) => void)} body
*/
option(attrs, body) {
this.#out.push(`<option${attributes(attrs)}`);
/**
* @param {Renderer} renderer
* @param {any} value
* @param {{ head?: string, body: any }} content
*/
const close = (renderer, value, { head, body }) => {
if ('value' in attrs) {
value = attrs.value;
}
if (value === this.local.select_value) {
renderer.#out.push(' selected');
}
renderer.#out.push(`>${body}</option>`);
// super edge case, but may as well handle it
if (head) {
renderer.head((child) => child.push(head));
}
};
if (typeof body === 'function') {
this.child((renderer) => {
const r = new Renderer(this.global, this);
body(r);
if (this.global.mode === 'async') {
return r.#collect_content_async().then((content) => {
close(renderer, content.body.replaceAll('<!---->', ''), content);
});
} else {
const content = r.#collect_content();
close(renderer, content.body.replaceAll('<!---->', ''), content);
}
});
} else {
this.#out.push(content);
close(this, body, { body });
}
}
/**
* Compact everything between `start` and `end` into a single renderer, then call `fn` with the result of that renderer.
* The compacted renderer will be sync if all of the children are sync and {@link fn} is sync, otherwise it will be async.
* @param {{ start: number, end?: number, fn: (content: AccumulatedContent) => AccumulatedContent | Promise<AccumulatedContent> }} args
* @param {(renderer: Renderer) => void} fn
*/
compact({ start, end = this.#out.length, fn }) {
const child = new Renderer(this.global, this);
const to_compact = this.#out.splice(start, end - start, child);
title(fn) {
const path = this.get_path();
/** @param {string} head */
const close = (head) => {
this.global.set_title(head, path);
};
if (this.global.mode === 'sync') {
Renderer.#compact(fn, child, to_compact, this.type);
this.child((renderer) => {
const r = new Renderer(renderer.global, renderer);
fn(r);
if (renderer.global.mode === 'async') {
return r.#collect_content_async().then((content) => {
close(content.head);
});
} else {
const content = r.#collect_content();
close(content.head);
}
});
}
/**
* @param {string | (() => Promise<string>)} content
*/
push(content) {
if (typeof content === 'function') {
this.child(async (renderer) => renderer.push(await content()));
} else {
this.promises.followup.push(Renderer.#compact_async(fn, child, to_compact, this.type));
this.#out.push(content);
}
}
@ -196,7 +275,7 @@ export class Renderer {
copy() {
const copy = new Renderer(this.global, this.#parent);
copy.#out = this.#out.map((item) => (item instanceof Renderer ? item.copy() : item));
copy.promises = this.promises;
copy.promise = this.promise;
return copy;
}
@ -218,7 +297,7 @@ export class Renderer {
}
return item;
});
this.promises = other.promises;
this.promise = other.promise;
this.type = other.type;
}
@ -355,7 +434,7 @@ export class Renderer {
try {
const renderer = Renderer.#open_render('sync', component, options);
const content = Renderer.#collect_content([renderer], renderer.type);
const content = renderer.#collect_content();
return Renderer.#close_render(content, renderer);
} finally {
abort();
@ -376,7 +455,7 @@ export class Renderer {
try {
const renderer = Renderer.#open_render('async', component, options);
const content = await Renderer.#collect_content_async([renderer], renderer.type);
const content = await renderer.#collect_content_async();
return Renderer.#close_render(content, renderer);
} finally {
abort();
@ -384,94 +463,41 @@ export class Renderer {
}
}
/**
* @param {(content: AccumulatedContent) => AccumulatedContent | Promise<AccumulatedContent>} fn
* @param {Renderer} child
* @param {RendererItem[]} to_compact
* @param {RendererType} type
*/
static #compact(fn, child, to_compact, type) {
const content = Renderer.#collect_content(to_compact, type);
const transformed_content = fn(content);
if (transformed_content instanceof Promise) {
throw new Error(
"invariant: Somehow you've encountered asynchronous work while rendering synchronously. If you're seeing this, there's a compiler bug. File an issue!"
);
} else {
Renderer.#push_accumulated_content(child, transformed_content);
}
}
/**
* @param {(content: AccumulatedContent) => AccumulatedContent | Promise<AccumulatedContent>} fn
* @param {Renderer} child
* @param {RendererItem[]} to_compact
* @param {RendererType} type
*/
static async #compact_async(fn, child, to_compact, type) {
const content = await Renderer.#collect_content_async(to_compact, type);
const transformed_content = await fn(content);
Renderer.#push_accumulated_content(child, transformed_content);
}
/**
* Collect all of the code from the `out` array and return it as a string, or a promise resolving to a string.
* @param {RendererItem[]} items
* @param {RendererType} current_type
* @param {AccumulatedContent} content
* @returns {AccumulatedContent}
*/
static #collect_content(items, current_type, content = { head: '', body: '' }) {
for (const item of items) {
#collect_content(content = { head: '', body: '' }) {
for (const item of this.#out) {
if (typeof item === 'string') {
content[current_type] += item;
content[this.type] += item;
} else if (item instanceof Renderer) {
Renderer.#collect_content(item.#out, item.type, content);
item.#collect_content(content);
}
}
return content;
}
/**
* Collect all of the code from the `out` array and return it as a string.
* @param {RendererItem[]} items
* @param {RendererType} current_type
* @param {AccumulatedContent} content
* @returns {Promise<AccumulatedContent>}
*/
static async #collect_content_async(items, current_type, content = { head: '', body: '' }) {
async #collect_content_async(content = { head: '', body: '' }) {
await this.promise;
// no danger to sequentially awaiting stuff in here; all of the work is already kicked off
for (const item of items) {
for (const item of this.#out) {
if (typeof item === 'string') {
content[current_type] += item;
} else {
if (item.promises.initial) {
// this represents the async function that's modifying this renderer.
// we can't do anything until it's done and we know our `out` array is complete.
await item.promises.initial;
}
for (const followup of item.promises.followup) {
// this is sequential because `compact` could synchronously queue up additional followup work
await followup;
}
await Renderer.#collect_content_async(item.#out, item.type, content);
content[this.type] += item;
} else if (item instanceof Renderer) {
await item.#collect_content_async(content);
}
}
return content;
}
/**
* @param {Renderer} tree
* @param {AccumulatedContent} accumulated_content
*/
static #push_accumulated_content(tree, accumulated_content) {
for (const [type, content] of Object.entries(accumulated_content)) {
if (!content) continue;
const child = new Renderer(tree.global, tree);
child.type = /** @type {RendererType} */ (type);
child.push(content);
tree.#out.push(child);
}
return content;
}
/**

@ -84,26 +84,6 @@ test('creating an async child in a sync context throws', () => {
expect(() => Renderer.render(component as unknown as Component).body).toThrow('await_invalid');
});
test('compact synchronously aggregates a range and can transform into head/body', () => {
const component = (renderer: Renderer) => {
const start = renderer.length;
renderer.push('a');
renderer.push('b');
renderer.push('c');
renderer.compact({
start,
end: start + 2,
fn: (content) => {
return { head: '<h>H</h>', body: content.body + 'd' };
}
});
};
const { head, body } = Renderer.render(component as unknown as Component);
expect(head).toBe('<h>H</h>');
expect(body).toBe('<!--[-->abdc<!--]-->');
});
test('local state is shallow-copied to children', () => {
const root = new Renderer(new SSRState('sync'));
root.local.select_value = 'A';
@ -132,13 +112,13 @@ test('subsume replaces tree content and state from other', () => {
b.global.css.add({ hash: 'h', code: 'c' });
b.global.set_title('Title', [1]);
b.local.select_value = 'B';
b.promises.initial = Promise.resolve();
b.promise = Promise.resolve();
a.subsume(b);
expect(a.type).toBe('body');
expect(a.local.select_value).toBe('B');
expect(a.promises).toBe(b.promises);
expect(a.promise).toBe(b.promise);
});
test('subsume refuses to switch modes', () => {
@ -156,7 +136,7 @@ test('subsume refuses to switch modes', () => {
b.global.css.add({ hash: 'h', code: 'c' });
b.global.set_title('Title', [1]);
b.local.select_value = 'B';
b.promises.initial = Promise.resolve();
b.promise = Promise.resolve();
expect(() => a.subsume(b)).toThrow(
"invariant: A renderer cannot switch modes. If you're seeing this, there's a compiler bug. File an issue!"
@ -195,6 +175,38 @@ test('SSRState title ordering favors later lexicographic paths', () => {
expect(state.get_title()).toBe('E');
});
test('selects an option with an explicit value', () => {
const component = (renderer: Renderer) => {
renderer.select({ value: 2 }, (renderer) => {
renderer.option({ value: 1 }, (renderer) => renderer.push('one'));
renderer.option({ value: 2 }, (renderer) => renderer.push('two'));
renderer.option({ value: 3 }, (renderer) => renderer.push('three'));
});
};
const { head, body } = Renderer.render(component as unknown as Component);
expect(head).toBe('');
expect(body).toBe(
'<!--[--><select><option value="1">one</option><option value="2" selected>two</option><option value="3">three</option></select><!--]-->'
);
});
test('selects an option with an implicit value', () => {
const component = (renderer: Renderer) => {
renderer.select({ value: 'two' }, (renderer) => {
renderer.option({}, (renderer) => renderer.push('one'));
renderer.option({}, (renderer) => renderer.push('two'));
renderer.option({}, (renderer) => renderer.push('three'));
});
};
const { head, body } = Renderer.render(component as unknown as Component);
expect(head).toBe('');
expect(body).toBe(
'<!--[--><select><option>one</option><option selected>two</option><option>three</option></select><!--]-->'
);
});
describe('async', () => {
beforeAll(() => {
enable_async_mode_flag();
@ -220,28 +232,6 @@ describe('async', () => {
expect(() => result.html).toThrow('html_deprecated');
});
test('compact schedules followup when compaction input is async', async () => {
const component = (renderer: Renderer) => {
renderer.push('a');
renderer.child(async ($$renderer) => {
await Promise.resolve();
$$renderer.push('X');
});
renderer.push('b');
renderer.compact({
start: 0,
fn: async (content) => ({
body: content.body.toLowerCase(),
head: await Promise.resolve('')
})
});
};
const { body, head } = await Renderer.render(component as unknown as Component);
expect(head).toBe('');
expect(body).toBe('<!--[-->axb<!--]-->');
});
test('push accepts async functions in async context', async () => {
const component = (renderer: Renderer) => {
renderer.push('a');
@ -308,25 +298,6 @@ describe('async', () => {
expect(body).toBe('<!--[-->start-async-child--end<!--]-->');
});
test('push async functions work with compact operations', async () => {
const component = (renderer: Renderer) => {
renderer.push('a');
renderer.push(async () => {
await Promise.resolve();
return 'b';
});
renderer.push('c');
renderer.compact({
start: 0,
fn: (content) => ({ head: '', body: content.body.toUpperCase() })
});
};
const { head, body } = await Renderer.render(component as unknown as Component);
expect(head).toBe('');
expect(body).toBe('<!--[-->ABC<!--]-->');
});
test('push async functions are not supported in sync context', () => {
const component = (renderer: Renderer) => {
renderer.push('a');

@ -1 +1 @@
<!--[--><!--[--><!--]--> <div><!----><!----></div> hello<!--]-->
<!--[--><!--[--><!--]--> <div><!--[--><!--]--></div> hello<!--]-->

@ -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>burrito</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 'burrito'} />

@ -0,0 +1,33 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
mode: ['hydrate'],
server_props: {
browser: false
},
ssrHtml: `
<h1>hello from the server</h1>
<h2>hello from the server</h2>
<h3>hello from the server</h3>
`,
props: {
browser: true
},
async test({ assert, target }) {
await tick();
assert.htmlEqual(
target.innerHTML,
`
<h1>hello from the browser</h1>
<h2>hello from the browser</h2>
<h3>hello from the browser</h3>
`
);
}
});

@ -0,0 +1,10 @@
<script>
let { browser = typeof window !== 'undefined' } = $props();
</script>
{#if await true}
<h1>hello from the {browser ? 'browser' : 'server'}</h1>
{/if}
<h2>hello from the {browser ? 'browser' : 'server'}</h2>
<h3>hello from the {browser ? 'browser' : 'server'}</h3>

@ -2,7 +2,7 @@ import 'svelte/internal/flags/async';
import * as $ from 'svelte/internal/server';
export default function Async_each_fallback_hoisting($$renderer) {
$$renderer.child(async ($$renderer) => {
$$renderer.async(async ($$renderer) => {
const each_array = $.ensure_array_like((await $.save(Promise.resolve([])))());
if (each_array.length !== 0) {

@ -8,7 +8,7 @@ export default function Async_each_hoisting($$renderer) {
$$renderer.push(`<!--[-->`);
$$renderer.child(async ($$renderer) => {
$$renderer.async(async ($$renderer) => {
const each_array = $.ensure_array_like((await $.save(Promise.resolve([first, second, third])))());
for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) {

@ -2,7 +2,7 @@ import 'svelte/internal/flags/async';
import * as $ from 'svelte/internal/server';
export default function Async_if_alternate_hoisting($$renderer) {
$$renderer.child(async ($$renderer) => {
$$renderer.async(async ($$renderer) => {
if ((await $.save(Promise.resolve(false)))()) {
$$renderer.push('<!--[-->');
$$renderer.push(async () => $.escape(await Promise.reject('no no no')));

@ -2,7 +2,7 @@ import 'svelte/internal/flags/async';
import * as $ from 'svelte/internal/server';
export default function Async_if_hoisting($$renderer) {
$$renderer.child(async ($$renderer) => {
$$renderer.async(async ($$renderer) => {
if ((await $.save(Promise.resolve(true)))()) {
$$renderer.push('<!--[-->');
$$renderer.push(async () => $.escape(await Promise.resolve('yes yes yes')));

@ -3,5 +3,11 @@ import * as $ from 'svelte/internal/server';
export default function Skip_static_subtree($$renderer, $$props) {
let { title, content } = $$props;
$$renderer.push(`<header><nav><a href="/">Home</a> <a href="/away">Away</a></nav></header> <main><h1>${$.escape(title)}</h1> <div class="static"><p>we don't need to traverse these nodes</p></div> <p>or</p> <p>these</p> <p>ones</p> ${$.html(content)} <p>these</p> <p>trailing</p> <p>nodes</p> <p>can</p> <p>be</p> <p>completely</p> <p>ignored</p></main> <cant-skip><custom-elements with="attributes"></custom-elements></cant-skip> <div><input autofocus/></div> <div><source muted/></div> <select><option value="a"${$.maybe_selected($$renderer, 'a')}>a</option></select> <img src="..." alt="" loading="lazy"/> <div><img src="..." alt="" loading="lazy"/></div>`);
$$renderer.push(`<header><nav><a href="/">Home</a> <a href="/away">Away</a></nav></header> <main><h1>${$.escape(title)}</h1> <div class="static"><p>we don't need to traverse these nodes</p></div> <p>or</p> <p>these</p> <p>ones</p> ${$.html(content)} <p>these</p> <p>trailing</p> <p>nodes</p> <p>can</p> <p>be</p> <p>completely</p> <p>ignored</p></main> <cant-skip><custom-elements with="attributes"></custom-elements></cant-skip> <div><input autofocus/></div> <div><source muted/></div> <select>`);
$$renderer.option({ value: 'a' }, ($$renderer) => {
$$renderer.push(`a`);
});
$$renderer.push(`</select> <img src="..." alt="" loading="lazy"/> <div><img src="..." alt="" loading="lazy"/></div>`);
}
Loading…
Cancel
Save