diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js
index 55f7b2d64a..a601928325 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/index.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/index.js
@@ -304,6 +304,8 @@ export function analyze_module(source, options) {
options: /** @type {ValidatedCompileOptions} */ (options),
fragment: null,
snippet: null,
+ title: null,
+ boundary: null,
parent_element: null,
reactive_statement: null
},
@@ -533,7 +535,7 @@ export function analyze_component(root, source, options) {
snippet_renderers: new Map(),
snippets: new Set(),
async_deriveds: new Set(),
- suspends: false
+ has_blocking_await: false
};
state.adjust({
@@ -694,6 +696,8 @@ export function analyze_component(root, source, options) {
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
fragment: ast === template.ast ? ast : null,
snippet: null,
+ title: null,
+ boundary: null,
parent_element: null,
has_props_rune: false,
component_slots: new Set(),
@@ -761,6 +765,8 @@ export function analyze_component(root, source, options) {
options,
fragment: ast === template.ast ? ast : null,
snippet: null,
+ title: null,
+ boundary: null,
parent_element: null,
has_props_rune: false,
ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module',
diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
index b1beb0660b..f020461d73 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
+++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts
@@ -10,6 +10,7 @@ export interface AnalysisState {
ast_type: 'instance' | 'template' | 'module';
fragment: AST.Fragment | null;
snippet: AST.SnippetBlock | null;
+ title: AST.TitleElement | null;
boundary: AST.SvelteBoundary | null;
/**
* Tag name of the parent element. `null` if the parent is `svelte:element`, `#snippet`, a component or the root.
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js
index 6d6058f01f..588410af96 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js
@@ -12,11 +12,7 @@ export function AwaitExpression(node, context) {
if (context.state.expression) {
context.state.expression.has_await = true;
- if (
- context.state.fragment &&
- // TODO there's probably a better way to do this
- context.path.some((node) => node.type === 'ConstTag')
- ) {
+ if (context.state.fragment && context.path.some((node) => node.type === 'ConstTag')) {
context.state.fragment.metadata.has_await = true;
}
@@ -27,6 +23,10 @@ export function AwaitExpression(node, context) {
context.state.snippet.metadata.has_await = true;
}
+ if (context.state.title) {
+ context.state.title.metadata.has_await = true;
+ }
+
// disallow top-level `await` or `await` in template expressions
// unless a) in runes mode and b) opted into `experimental.async`
if (suspend) {
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/TitleElement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/TitleElement.js
index caa3206b0d..796a3df3ff 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/TitleElement.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/TitleElement.js
@@ -17,5 +17,5 @@ export function TitleElement(node, context) {
}
}
- context.next();
+ context.visit(node.fragment, { ...context.state, title: node });
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
index a3b59c74be..0c39f61e97 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js
@@ -195,9 +195,7 @@ export function server_component(analysis, options) {
b.unary('!', b.id('$$settled')),
b.block([
b.stmt(b.assignment('=', b.id('$$settled'), b.true)),
- b.stmt(
- b.assignment('=', b.id('$$inner_payload'), b.call('$.copy_payload', b.id('$$payload')))
- ),
+ b.stmt(b.assignment('=', b.id('$$inner_payload'), b.call('$$payload.copy'))),
b.stmt(b.call('$$render_inner', b.id('$$inner_payload')))
])
),
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 0779195e2e..57deeac3e5 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
@@ -1,4 +1,4 @@
-/** @import { Expression } from 'estree' */
+/** @import { Expression, Statement } from 'estree' */
/** @import { Location } from 'locate-character' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../types.js' */
@@ -8,7 +8,12 @@ import { dev, locator } from '../../../../state.js';
import * as b from '#compiler/builders';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { build_element_attributes, build_spread_object } from './shared/element.js';
-import { process_children, build_template, build_attribute_value } from './shared/utils.js';
+import {
+ process_children,
+ build_template,
+ build_attribute_value,
+ wrap_in_child_payload
+} from './shared/utils.js';
/**
* @param {AST.RegularElement} node
@@ -73,6 +78,7 @@ export function RegularElement(node, context) {
}
let select_with_value = false;
+ const template_start = state.template.length;
if (node.name === 'select') {
const value = node.attributes.find(
@@ -176,6 +182,22 @@ export function RegularElement(node, context) {
if (select_with_value) {
state.template.push(b.stmt(b.assignment('=', b.id('$$payload.select_value'), b.void0)));
+
+ // 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.
+ // TODO is this cast safe?
+ const elements = state.template.splice(template_start, Infinity);
+ state.template.push(
+ wrap_in_child_payload(
+ b.block(build_template(elements)),
+ // TODO this will always produce correct results (because it will produce an async function if the surrounding component is async)
+ // but it will false-positive and create unnecessary async functions (eg. when the component is async but the select element is not)
+ // we could probably optimize by checking if the select element is async. Might be worth it.
+ context.state.analysis.has_blocking_await
+ )
+ );
}
if (!node_is_void) {
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/TitleElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/TitleElement.js
index c42df4c646..f1d1e1cfc9 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/TitleElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/TitleElement.js
@@ -13,5 +13,20 @@ export function TitleElement(node, context) {
process_children(node.fragment.nodes, { ...context, state: { ...context.state, template } });
template.push(b.literal(''));
- context.state.init.push(...build_template(template, b.id('$$payload.title'), '='));
+ if (!node.metadata.has_await) {
+ context.state.init.push(...build_template(template, b.id('$$payload.title.value'), '='));
+ } else {
+ const async_template = b.thunk(
+ // TODO I'm sure there is a better way to do this
+ b.block([
+ b.let('title'),
+ ...build_template(template, b.id('title'), '='),
+ b.return(b.id('title'))
+ ]),
+ true
+ );
+ context.state.init.push(
+ b.stmt(b.assignment('=', b.id('$$payload.title.value'), b.call(async_template)))
+ );
+ }
}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js
index 73e9e267cb..afeeb12d1b 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js
@@ -240,7 +240,7 @@ export function build_inline_component(node, expression, context) {
b.arrow(
[b.object_pattern([b.init('$$payload', b.id('$$payload'))])],
b.block(block.body),
- context.state.analysis.suspends
+ context.state.analysis.has_blocking_await
)
)
)
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 8a8633dd1a..c9f85c8429 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
@@ -1,4 +1,4 @@
-/** @import { AssignmentOperator, Expression, Identifier, Node, Statement } from 'estree' */
+/** @import { AssignmentOperator, Expression, Identifier, Node, Statement, BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext, ServerTransformState } from '../../types.js' */
@@ -257,3 +257,17 @@ export function build_getter(node, state) {
return node;
}
+
+/**
+ * @param {BlockStatement | Expression} body
+ * @param {boolean} async
+ * @returns {Statement}
+ */
+export function wrap_in_child_payload(body, async) {
+ return b.stmt(
+ b.call(
+ '$$payload.child',
+ b.arrow([b.object_pattern([b.init('$$payload', b.id('$$payload'))])], body, async)
+ )
+ );
+}
diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts
index 59736b0fc8..21aa97daea 100644
--- a/packages/svelte/src/compiler/types/template.d.ts
+++ b/packages/svelte/src/compiler/types/template.d.ts
@@ -325,6 +325,10 @@ export namespace AST {
export interface TitleElement extends BaseElement {
type: 'TitleElement';
name: 'title';
+ /** @internal */
+ metadata: {
+ has_await: boolean;
+ };
}
export interface SlotElement extends BaseElement {
diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js
index 54f8d7a8cc..f027da860e 100644
--- a/packages/svelte/src/internal/server/index.js
+++ b/packages/svelte/src/internal/server/index.js
@@ -101,7 +101,14 @@ export function render(component, options = {}) {
for (const cleanup of on_destroy) cleanup();
on_destroy = prev_on_destroy;
- let head = payload.head.collect() + payload.head.title;
+ let head = payload.head.collect();
+
+ if (typeof payload.head.title.value !== 'string') {
+ throw new Error(
+ 'TODO -- should encorporate this into the collect/collect_async logic somewhere'
+ );
+ }
+ head += payload.head.title.value;
for (const { hash, code } of payload.css) {
head += ``;
@@ -125,10 +132,9 @@ export function render(component, options = {}) {
* @returns {void}
*/
export function head(payload, fn) {
- const head_payload = payload.head;
- head_payload.out.push(BLOCK_OPEN);
- fn(head_payload);
- head_payload.out.push(BLOCK_CLOSE);
+ payload.head.out.push(BLOCK_OPEN);
+ payload.head.child(({ $$payload }) => fn($$payload));
+ payload.head.out.push(BLOCK_CLOSE);
}
/**
@@ -507,7 +513,7 @@ export { push, pop } from './context.js';
export { push_element, pop_element, validate_snippet_args } from './dev.js';
-export { assign_payload, copy_payload } from './payload.js';
+export { assign_payload } from './payload.js';
export { snapshot } from '../shared/clone.js';
@@ -562,36 +568,33 @@ export function maybe_selected(payload, value) {
export function valueless_option(payload, children) {
var i = payload.out.length;
+ // prior to children, `payload` has some combination of string/unresolved payload that ends in `