diff --git a/packages/svelte/src/compiler/phases/1-parse/utils/create.js b/packages/svelte/src/compiler/phases/1-parse/utils/create.js
index 0741abd7f2..fbf5304d55 100644
--- a/packages/svelte/src/compiler/phases/1-parse/utils/create.js
+++ b/packages/svelte/src/compiler/phases/1-parse/utils/create.js
@@ -13,8 +13,7 @@ export function create_fragment(transparent = false) {
transparent,
dynamic: false,
has_await: false,
- // name is added later, after we've done scope analysis
- hoisted_promises: { id: b.id('$$promises'), promises: [] }
+ is_async: false
}
};
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js
index e303a16348..243773630f 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/index.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/index.js
@@ -542,8 +542,7 @@ export function analyze_component(root, source, options) {
source,
snippet_renderers: new Map(),
snippets: new Set(),
- async_deriveds: new Set(),
- hoisted_promises: new Map()
+ async_deriveds: new Set()
};
if (!runes) {
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 71da9c2837..2725c1df92 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js
@@ -17,14 +17,6 @@ export function AwaitExpression(node, context) {
context.state.fragment.metadata.has_await = true;
}
- if (context.state.fragment) {
- // const len = context.state.fragment.metadata.hoisted_promises.promises.push(node.argument);
- // context.state.analysis.hoisted_promises.set(
- // node.argument,
- // b.member(context.state.fragment.metadata.hoisted_promises.id, b.literal(len - 1), true)
- // );
- }
-
suspend = true;
}
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js
index 54890c325a..02d780dc0d 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js
@@ -7,16 +7,4 @@
*/
export function Fragment(node, context) {
context.next({ ...context.state, fragment: node });
-
- // TODO this indicates whether the fragment contains an `await` expression (not inside
- // a child fragment), which is necessary for ensuring that a `SnippetBlock` creates an
- // async function in SSR. It feels like this is probably duplicative, but it's late
- // and it works, so for now I'm doing it like this
- node.metadata.is_async ||= node.metadata.hoisted_promises.promises.length > 0;
-
- if (node.metadata.hoisted_promises.promises.length === 1) {
- // if there's only one promise in this fragment, we don't need to de-waterfall it
- context.state.analysis.hoisted_promises.delete(node.metadata.hoisted_promises.promises[0]);
- node.metadata.hoisted_promises.promises.length = 0;
- }
}
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 6dc469cc9a..9a3758371a 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
@@ -10,7 +10,6 @@ import { dev, filename } from '../../../state.js';
import { render_stylesheet } from '../css/index.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
-import { AwaitExpression } from './visitors/AwaitExpression.js';
import { CallExpression } from './visitors/CallExpression.js';
import { ClassBody } from './visitors/ClassBody.js';
import { Component } from './visitors/Component.js';
@@ -59,7 +58,6 @@ const global_visitors = {
/** @type {ComponentVisitors} */
const template_visitors = {
- AwaitExpression,
AwaitBlock,
Component,
ConstTag,
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js
deleted file mode 100644
index 70070509d7..0000000000
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AwaitExpression.js
+++ /dev/null
@@ -1,14 +0,0 @@
-/** @import { AwaitExpression } from 'estree' */
-/** @import { ComponentContext } from '../types.js' */
-
-/**
- * This is only registered for components, currently.
- * @param {AwaitExpression} node
- * @param {ComponentContext} context
- */
-export function AwaitExpression(node, context) {
- const hoisted = context.state.analysis.hoisted_promises.get(node.argument);
- if (hoisted) {
- node.argument = hoisted;
- }
-}
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js
index d27069f831..a44c7cfddd 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js
@@ -52,12 +52,6 @@ export function Fragment(node, context) {
/** @type {Statement[]} */
const statements = [];
- if (node.metadata.hoisted_promises.promises.length > 0) {
- statements.push(
- b.const(node.metadata.hoisted_promises.id, b.array(node.metadata.hoisted_promises.promises))
- );
- }
-
statements.push(...state.init);
statements.push(...build_template(state.template));
diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js
index fee7cb6e02..2d16a07b4e 100644
--- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js
+++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SlotElement.js
@@ -2,7 +2,12 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
-import { empty_comment, build_attribute_value } from './shared/utils.js';
+import {
+ empty_comment,
+ build_attribute_value,
+ PromiseOptimiser,
+ call_child_payload
+} from './shared/utils.js';
/**
* @param {AST.SlotElement} node
@@ -15,13 +20,22 @@ export function SlotElement(node, context) {
/** @type {Expression[]} */
const spreads = [];
+ const optimiser = new PromiseOptimiser();
+
let name = b.literal('default');
for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute') {
- spreads.push(/** @type {Expression} */ (context.visit(attribute)));
+ let expression = /** @type {Expression} */ (context.visit(attribute));
+ spreads.push(optimiser.transform(expression, attribute.metadata.expression));
} else if (attribute.type === 'Attribute') {
- const value = build_attribute_value(attribute.value, context, false, true);
+ const value = build_attribute_value(
+ attribute.value,
+ context,
+ false,
+ true,
+ optimiser.transform
+ );
if (attribute.name === 'name') {
name = /** @type {Literal} */ (value);
@@ -50,5 +64,10 @@ export function SlotElement(node, context) {
fallback
);
- context.state.template.push(empty_comment, b.stmt(slot), empty_comment);
+ const statement =
+ optimiser.expressions.length > 0
+ ? call_child_payload(b.block([optimiser.apply(), b.stmt(slot)]), true)
+ : b.stmt(slot);
+
+ context.state.template.push(empty_comment, statement, empty_comment);
}
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 f335a532c3..b1e076722e 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
@@ -8,15 +8,6 @@ import { process_children, build_template, call_child_payload } from './shared/u
* @param {ComponentContext} context
*/
export function TitleElement(node, context) {
- if (node.fragment.metadata.hoisted_promises.promises.length > 0) {
- context.state.init.push(
- b.const(
- node.fragment.metadata.hoisted_promises.id,
- b.array(node.fragment.metadata.hoisted_promises.promises)
- )
- );
- }
-
// title is guaranteed to contain only text/expression tag children
const template = [b.literal('
')];
process_children(node.fragment.nodes, { ...context, state: { ...context.state, 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 3dd78a7638..3a60b1e2ee 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
@@ -1,10 +1,16 @@
/** @import { BlockStatement, Expression, Pattern, Property, SequenceExpression, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */
-import { empty_comment, build_attribute_value, call_child_payload } from './utils.js';
+import {
+ empty_comment,
+ build_attribute_value,
+ call_child_payload,
+ PromiseOptimiser
+} from './utils.js';
import * as b from '#compiler/builders';
import { is_element_node } from '../../../../nodes.js';
import { dev } from '../../../../../state.js';
+import { get_attribute_chunks } from '../../../../../utils/ast.js';
/**
* @param {AST.Component | AST.SvelteComponent | AST.SvelteSelf} node
@@ -72,16 +78,26 @@ export function build_inline_component(node, expression, context) {
}
}
+ const optimiser = new PromiseOptimiser();
+
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
if (!slot_scope_applies_to_itself) {
lets.default.push(attribute);
}
} else if (attribute.type === 'SpreadAttribute') {
- props_and_spreads.push(/** @type {Expression} */ (context.visit(attribute)));
+ let expression = /** @type {Expression} */ (context.visit(attribute));
+ props_and_spreads.push(optimiser.transform(expression, attribute.metadata.expression));
} else if (attribute.type === 'Attribute') {
+ const value = build_attribute_value(
+ attribute.value,
+ context,
+ false,
+ true,
+ optimiser.transform
+ );
+
if (attribute.name.startsWith('--')) {
- const value = build_attribute_value(attribute.value, context, false, true);
custom_css_props.push(b.init(attribute.name, value));
continue;
}
@@ -90,7 +106,6 @@ export function build_inline_component(node, expression, context) {
has_children_prop = true;
}
- const value = build_attribute_value(attribute.value, context, false, true);
push_prop(b.prop('init', b.key(attribute.name), value));
} else if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
if (attribute.expression.type === 'SequenceExpression') {
@@ -298,27 +313,29 @@ export function build_inline_component(node, expression, context) {
node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic);
if (custom_css_props.length > 0) {
- context.state.template.push(
- b.stmt(
- b.call(
- '$.css_props',
- b.id('$$payload'),
- b.literal(context.state.namespace === 'svg' ? false : true),
- b.object(custom_css_props),
- b.thunk(b.block([statement])),
- dynamic && b.true
- )
+ statement = b.stmt(
+ b.call(
+ '$.css_props',
+ b.id('$$payload'),
+ b.literal(context.state.namespace === 'svg' ? false : true),
+ b.object(custom_css_props),
+ b.thunk(b.block([statement])),
+ dynamic && b.true
)
);
- } else {
- if (dynamic) {
- context.state.template.push(empty_comment);
- }
+ }
- context.state.template.push(statement);
+ if (optimiser.expressions.length > 0) {
+ statement = call_child_payload(b.block([optimiser.apply(), statement]), true);
+ }
- if (!context.state.skip_hydration_boundaries) {
- context.state.template.push(empty_comment);
- }
+ if (dynamic && custom_css_props.length === 0) {
+ context.state.template.push(empty_comment);
+ }
+
+ context.state.template.push(statement);
+
+ if (!context.state.skip_hydration_boundaries && custom_css_props.length === 0) {
+ context.state.template.push(empty_comment);
}
}
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 f9601e7878..dd754900be 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,5 +1,5 @@
/** @import { AssignmentOperator, Expression, Identifier, Node, Statement, BlockStatement } from 'estree' */
-/** @import { AST } from '#compiler' */
+/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentContext, ServerTransformState } from '../../types.js' */
import { escape_html } from '../../../../../../escaping.js';
@@ -11,6 +11,7 @@ import {
import * as b from '#compiler/builders';
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_whitespaces_strict } from '../../../../patterns.js';
+import { has_await } from '../../../../../utils/ast.js';
/** Opens an if/each block, so that we can remove nodes in the case of a mismatch */
export const block_open = b.literal(BLOCK_OPEN);
@@ -183,13 +184,15 @@ export function build_template(template, out = b.id('$$payload'), operator = 'pu
* @param {ComponentContext} context
* @param {boolean} trim_whitespace
* @param {boolean} is_component
+ * @param {(expression: Expression, metadata: ExpressionMetadata) => Expression} transform
* @returns {Expression}
*/
export function build_attribute_value(
value,
context,
trim_whitespace = false,
- is_component = false
+ is_component = false,
+ transform = (expression) => expression
) {
if (value === true) {
return b.true;
@@ -206,7 +209,10 @@ export function build_attribute_value(
return b.literal(is_component ? data : escape_html(data, true));
}
- return /** @type {Expression} */ (context.visit(chunk.expression));
+ return transform(
+ /** @type {Expression} */ (context.visit(chunk.expression)),
+ chunk.metadata.expression
+ );
}
let quasi = b.quasi('', false);
@@ -224,7 +230,13 @@ export function build_attribute_value(
: node.data;
} else {
expressions.push(
- b.call('$.stringify', /** @type {Expression} */ (context.visit(node.expression)))
+ b.call(
+ '$.stringify',
+ transform(
+ /** @type {Expression} */ (context.visit(node.expression)),
+ node.metadata.expression
+ )
+ )
);
quasi = b.quasi('', i + 1 === value.length);
@@ -269,3 +281,41 @@ export function build_getter(node, state) {
export function call_child_payload(body, async) {
return b.stmt(b.call('$$payload.child', b.arrow([b.id('$$payload')], body, async)));
}
+
+export class PromiseOptimiser {
+ /** @type {Expression[]} */
+ expressions = [];
+
+ /**
+ *
+ * @param {Expression} expression
+ * @param {ExpressionMetadata} metadata
+ */
+ transform = (expression, metadata) => {
+ if (metadata.has_await) {
+ const length = this.expressions.push(expression);
+ return b.id(`$$${length - 1}`);
+ }
+
+ return expression;
+ };
+
+ apply() {
+ if (this.expressions.length === 1) {
+ return b.const('$$0', this.expressions[0]);
+ }
+
+ const promises = b.array(
+ this.expressions.map((expression) => {
+ return expression.type === 'AwaitExpression' && !has_await(expression.argument)
+ ? expression.argument
+ : b.call(b.thunk(expression, true));
+ })
+ );
+
+ return b.const(
+ b.array_pattern(this.expressions.map((_, i) => b.id(`$$${i}`))),
+ b.await(b.call('Promise.all', promises))
+ );
+ }
+}
diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts
index 1f6ceda35e..40e41d4a7a 100644
--- a/packages/svelte/src/compiler/phases/types.d.ts
+++ b/packages/svelte/src/compiler/phases/types.d.ts
@@ -107,7 +107,6 @@ export interface ComponentAnalysis extends Analysis {
* Every snippet that is declared locally
*/
snippets: Set;
- hoisted_promises: Map;
}
declare module 'estree' {
diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts
index 9b1a3dba8f..5c99e7d34e 100644
--- a/packages/svelte/src/compiler/types/template.d.ts
+++ b/packages/svelte/src/compiler/types/template.d.ts
@@ -59,7 +59,6 @@ export namespace AST {
has_await: boolean;
/** TODO document */
is_async: boolean;
- hoisted_promises: { id: Identifier; promises: Expression[] };
};
}
diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js
index a20b6888c7..d988cd98fb 100644
--- a/packages/svelte/src/compiler/utils/ast.js
+++ b/packages/svelte/src/compiler/utils/ast.js
@@ -609,3 +609,19 @@ export function build_assignment_value(operator, left, right) {
? b.logical(/** @type {ESTree.LogicalOperator} */ (operator.slice(0, -1)), left, right)
: b.binary(/** @type {ESTree.BinaryOperator} */ (operator.slice(0, -1)), left, right);
}
+
+/**
+ * @param {ESTree.Expression} expression
+ */
+export function has_await(expression) {
+ let has_await = false;
+
+ walk(expression, null, {
+ AwaitExpression(_node, context) {
+ has_await = true;
+ context.stop();
+ }
+ });
+
+ return has_await;
+}
diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js
index 03a946ff9c..f69beed509 100644
--- a/packages/svelte/src/compiler/utils/builders.js
+++ b/packages/svelte/src/compiler/utils/builders.js
@@ -2,6 +2,7 @@
import { walk } from 'zimmerframe';
import { regex_is_valid_identifier } from '../phases/patterns.js';
import { sanitize_template_string } from './sanitize_template_string.js';
+import { has_await } from './ast.js';
/**
* @param {Array} elements
@@ -443,16 +444,7 @@ export function thunk(expression, async = false) {
export function unthunk(expression) {
// optimize `async () => await x()`, but not `async () => await x(await y)`
if (expression.async && expression.body.type === 'AwaitExpression') {
- let has_await = false;
-
- walk(expression.body.argument, null, {
- AwaitExpression(_node, context) {
- has_await = true;
- context.stop();
- }
- });
-
- if (!has_await) {
+ if (!has_await(expression.body.argument)) {
return unthunk(arrow(expression.params, expression.body.argument));
}
}