${$.escape(title)}
we don't need to traverse these nodes
or
these
ones
${$.html(content)}these
trailing
nodes
can
be
completely
ignored
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 1a6f15aee7..0741abd7f2 100644 --- a/packages/svelte/src/compiler/phases/1-parse/utils/create.js +++ b/packages/svelte/src/compiler/phases/1-parse/utils/create.js @@ -1,4 +1,5 @@ /** @import { AST } from '#compiler' */ +import * as b from '#compiler/builders'; /** * @param {any} transparent @@ -13,7 +14,7 @@ export function create_fragment(transparent = false) { dynamic: false, has_await: false, // name is added later, after we've done scope analysis - hoisted_promises: { name: '', promises: [] } + hoisted_promises: { id: b.id('$$promises'), promises: [] } } }; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 353fb84605..e303a16348 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -131,9 +131,6 @@ const visitors = { ignore_map.set(node, structuredClone(ignore_stack)); const scope = state.scopes.get(node); - if (node.type === 'Fragment') { - node.metadata.hoisted_promises.name = state.scope.generate('promises'); - } next(scope !== undefined && scope !== state.scope ? { ...state, scope } : state); if (ignores.length > 0) { @@ -309,8 +306,7 @@ export function analyze_module(source, options) { title: null, boundary: null, parent_element: null, - reactive_statement: null, - async_hoist_boundary: null + reactive_statement: null }, visitors ); @@ -547,7 +543,6 @@ export function analyze_component(root, source, options) { snippet_renderers: new Map(), snippets: new Set(), async_deriveds: new Set(), - suspends_without_fallback: false, hoisted_promises: new Map() }; @@ -709,8 +704,7 @@ export function analyze_component(root, source, options) { expression: null, state_fields: new Map(), function_depth: scope.function_depth, - reactive_statement: null, - async_hoist_boundary: ast === template.ast ? ast : null + reactive_statement: null }; walk(/** @type {AST.SvelteNode} */ (ast), state, visitors); @@ -779,8 +773,7 @@ export function analyze_component(root, source, options) { component_slots: new Set(), expression: null, state_fields: new Map(), - function_depth: scope.function_depth, - async_hoist_boundary: ast === template.ast ? ast : null + function_depth: scope.function_depth }; walk(/** @type {AST.SvelteNode} */ (ast), state, visitors); 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 a5b734f187..934c21d10b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -11,14 +11,6 @@ export interface AnalysisState { fragment: AST.Fragment | null; title: AST.TitleElement | null; boundary: AST.SvelteBoundary | null; - /** - * The "anchor" fragment for any hoisted promises. This is the root fragment when - * walking starts and until another boundary fragment is encountered, like a - * consequent or alternate of an `#if` or `#each` block. When this fragment is emitted - * during server transformation, the promise expressions will be hoisted out of the fragment - * and placed right above it in an array. - */ - async_hoist_boundary: AST.Fragment | null; /** * Tag name of the parent element. `null` if the parent is `svelte:element`, `#snippet`, a component or the root. * Parent doesn't necessarily mean direct path predecessor because there could be `#each`, `#if` etc in-between. diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js index b26848f565..f9bbcd7031 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js @@ -44,22 +44,7 @@ export function AwaitBlock(node, context) { // this one doesn't get the new state because it still hoists to the existing scope context.visit(node.expression, { ...context.state, expression: node.metadata.expression }); - if (node.pending) { - context.visit(node.pending, { - ...context.state, - async_hoist_boundary: node.pending - }); - } - if (node.then) { - context.visit(node.then, { - ...context.state, - async_hoist_boundary: node.then - }); - } - if (node.catch) { - context.visit(node.catch, { - ...context.state, - async_hoist_boundary: node.catch - }); - } + if (node.pending) context.visit(node.pending); + if (node.then) context.visit(node.then); + if (node.catch) context.visit(node.catch); } 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 f896562431..f32aede4d2 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -17,17 +17,11 @@ export function AwaitExpression(node, context) { context.state.fragment.metadata.has_await = true; } - if (context.state.async_hoist_boundary) { - const len = context.state.async_hoist_boundary.metadata.hoisted_promises.promises.push( - node.argument - ); + 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( - b.id(context.state.async_hoist_boundary.metadata.hoisted_promises.name), - b.literal(len - 1), - true - ) + b.member(context.state.fragment.metadata.hoisted_promises.id, b.literal(len - 1), true) ); } @@ -48,8 +42,6 @@ export function AwaitExpression(node, context) { if (!context.state.analysis.runes) { e.legacy_await_invalid(node); } - - context.state.analysis.suspends_without_fallback ||= !context.state.boundary?.metadata.pending; } context.next(); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js index 0a72b72a65..215edda8ad 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js @@ -35,16 +35,10 @@ export function EachBlock(node, context) { scope: /** @type {Scope} */ (context.state.scope.parent) }); - context.visit(node.body, { - ...context.state, - async_hoist_boundary: node.body - }); + context.visit(node.body); if (node.key) context.visit(node.key); if (node.fallback) { - context.visit(node.fallback, { - ...context.state, - async_hoist_boundary: node.fallback - }); + context.visit(node.fallback); } if (!context.state.analysis.runes) { 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 02d780dc0d..252936e067 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js @@ -7,4 +7,16 @@ */ 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/2-analyze/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js index edaf3d398e..10228397e0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js @@ -22,14 +22,8 @@ export function IfBlock(node, context) { expression: node.metadata.expression }); - context.visit(node.consequent, { - ...context.state, - async_hoist_boundary: node.consequent - }); + context.visit(node.consequent); if (node.alternate) { - context.visit(node.alternate, { - ...context.state, - async_hoist_boundary: node.alternate - }); + context.visit(node.alternate); } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js index dbc2eb6611..09e604ea66 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js @@ -17,9 +17,5 @@ export function KeyBlock(node, context) { mark_subtree_dynamic(context.path); context.visit(node.expression, { ...context.state, expression: node.metadata.expression }); - - context.visit(node.fragment, { - ...context.state, - async_hoist_boundary: node.fragment - }); + context.visit(node.fragment); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js index d0554856e9..7930c2b1a7 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SnippetBlock.js @@ -23,11 +23,7 @@ export function SnippetBlock(node, context) { } } - context.next({ - ...context.state, - parent_element: null, - async_hoist_boundary: node.body - }); + context.next({ ...context.state, parent_element: null }); const can_hoist = context.path.length === 1 && diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js index 786b08bb88..d195e01f86 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js @@ -34,9 +34,5 @@ export function SvelteBoundary(node, context) { ) ) ?? null; - context.next({ - ...context.state, - boundary: node, - async_hoist_boundary: node.fragment - }); + context.next({ ...context.state, boundary: 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 84f4779ba0..6dc469cc9a 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 @@ -238,16 +238,15 @@ export function server_component(analysis, options) { template.body.push(b.stmt(b.call('$.bind_props', b.id('$$props'), b.object(props)))); } - const component_block = b.block([ - call_child_payload( - b.block([ - .../** @type {Statement[]} */ (instance.body), - .../** @type {Statement[]} */ (template.body) - ]), - analysis.suspends_without_fallback - ) + let component_block = b.block([ + .../** @type {Statement[]} */ (instance.body), + .../** @type {Statement[]} */ (template.body) ]); + if (analysis.instance.has_await) { + component_block = b.block([call_child_payload(component_block, true)]); + } + // trick esrap into including comments component_block.loc = instance.loc; 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 427a111024..d27069f831 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 @@ -1,4 +1,5 @@ /** @import { AST } from '#compiler' */ +/** @import { Statement } from 'estree' */ /** @import { ComponentContext, ComponentServerTransformState } from '../types.js' */ import { clean_nodes, infer_namespace } from '../../utils.js'; import * as b from '#compiler/builders'; @@ -47,15 +48,20 @@ export function Fragment(node, context) { process_children(trimmed, { ...context, state }); - if (node.metadata.hoisted_promises.promises.length > 0) { - return b.block([ - b.const( - node.metadata.hoisted_promises.name, - b.array(node.metadata.hoisted_promises.promises) - ), - ...state.init, - call_child_payload(b.block(build_template(state.template)), true) - ]); + if (node.metadata.is_async) { + /** @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)); + + return b.block([call_child_payload(b.block(statements), true)]); } return b.block([...state.init, ...build_template(state.template)]); 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 1c9e76edbb..fd99a61b17 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 @@ -78,6 +78,7 @@ 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') { @@ -86,8 +87,12 @@ export function RegularElement(node, context) { (attribute.type === 'Attribute' || attribute.type === 'BindDirective') && attribute.name === 'value' ); - if (node.attributes.some((attribute) => attribute.type === 'SpreadAttribute')) { + + 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( @@ -113,6 +118,13 @@ export function RegularElement(node, context) { ); } 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('$$payload.local.select_value'); if (value.type === 'Attribute') { state.template.push( @@ -164,7 +176,7 @@ export function RegularElement(node, context) { b.arrow( [b.id('$$payload')], b.block([...inner_state.init, ...build_template(inner_state.template)]), - context.state.analysis.suspends_without_fallback + node.fragment.metadata.is_async ) ) ) @@ -192,7 +204,11 @@ export function RegularElement(node, context) { ) ); } else { - process_children(trimmed, { ...context, state }); + if (node.fragment.metadata.is_async) { + state.template.push(/** @type {Statement} */ (context.visit(node.fragment))); + } else { + process_children(trimmed, { ...context, state }); + } } if (select_with_value) { @@ -208,7 +224,7 @@ export function RegularElement(node, context) { // 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.suspends_without_fallback + select_with_value_async ) ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js index 238485e665..a27c3e00a2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SnippetBlock.js @@ -12,7 +12,8 @@ export function SnippetBlock(node, context) { let fn = b.function_declaration( node.expression, [b.id('$$payload'), ...node.parameters], - /** @type {BlockStatement} */ (context.visit(node.body)) + /** @type {BlockStatement} */ (context.visit(node.body)), + node.body.metadata.is_async ); // @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js index 5ef6e71989..aa401e4555 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteHead.js @@ -15,9 +15,7 @@ export function SvelteHead(node, context) { b.call( '$.head', b.id('$$payload'), - // same thing as elsewhere; this will create more async functions than necessary but should never be _wrong_ - // because the component rendering this head block will always be async if the head block is async - b.arrow([b.id('$$payload')], block, context.state.analysis.suspends_without_fallback) + b.arrow([b.id('$$payload')], block, node.fragment.metadata.is_sync) ) ) ); 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 71e1cf4dae..531a7bf7d3 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,6 +8,15 @@ 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.name, + b.array(node.fragment.metadata.hoisted_promises.promises) + ) + ); + } + // title is guaranteed to contain only text/expression tag children const template = [b.literal('
index: ${$.escape(i)}
`); - } + for (let i = 0, $$length = each_array.length; i < $$length; i++) { + $$payload.push(`index: ${$.escape(i)}
`); + } - $$payload.push(``); - }); + $$payload.push(``); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js index 1b935b96e0..6544baad6f 100644 --- a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/server/index.svelte.js @@ -1,17 +1,15 @@ import * as $ from 'svelte/internal/server'; export default function Each_string_template($$payload) { - $$payload.child(($$payload) => { - const each_array = $.ensure_array_like(['foo', 'bar', 'baz']); + const each_array = $.ensure_array_like(['foo', 'bar', 'baz']); - $$payload.push(``); + $$payload.push(``); - for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) { - let thing = each_array[$$index]; + for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) { + let thing = each_array[$$index]; - $$payload.push(`${$.escape(thing)}, `); - } + $$payload.push(`${$.escape(thing)}, `); + } - $$payload.push(``); - }); + $$payload.push(``); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js index 49c3642811..03b3fb93d7 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js @@ -1,27 +1,25 @@ import * as $ from 'svelte/internal/server'; export default function Function_prop_no_getter($$payload) { - $$payload.child(($$payload) => { - let count = 0; + let count = 0; - function onmouseup() { - count += 2; - } + function onmouseup() { + count += 2; + } - const plusOne = (num) => num + 1; + const plusOne = (num) => num + 1; - Button($$payload, { - onmousedown: () => count += 1, - onmouseup, - onmouseenter: () => count = plusOne(count), + Button($$payload, { + onmousedown: () => count += 1, + onmouseup, + onmouseenter: () => count = plusOne(count), - children: ($$payload) => { - $$payload.child(($$payload) => { - $$payload.push(`clicks: ${$.escape(count)}`); - }); - }, + children: ($$payload) => { + $$payload.child(($$payload) => { + $$payload.push(`clicks: ${$.escape(count)}`); + }); + }, - $$slots: { default: true } - }); + $$slots: { default: true } }); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js index d0d7092dc8..e23a50cff5 100644 --- a/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/functional-templating/_expected/server/index.svelte.js @@ -1,7 +1,5 @@ import * as $ from 'svelte/internal/server'; export default function Functional_templating($$payload) { - $$payload.child(($$payload) => { - $$payload.push(`child element
another child element
child element
another child element
0
${$.escape(location.href)}
`); - Child($$payload, { prop: encodeURIComponent('hello') }); - $$payload.push(``); - }); + $$payload.push(`0
${$.escape(location.href)}
`); + Child($$payload, { prop: encodeURIComponent('hello') }); + $$payload.push(``); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js index 8e22d31463..e49480f682 100644 --- a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/server/index.svelte.js @@ -1,9 +1,7 @@ import * as $ from 'svelte/internal/server'; export default function Skip_static_subtree($$payload, $$props) { - $$payload.child(($$payload) => { - let { title, content } = $$props; + let { title, content } = $$props; - $$payload.push(`we don't need to traverse these nodes
or
these
ones
${$.html(content)}these
trailing
nodes
can
be
completely
ignored
we don't need to traverse these nodes
or
these
ones
${$.html(content)}these
trailing
nodes
can
be
completely
ignored
${$.escape(text1())}${$.escape(text2())}
`); - }); + $$payload.push(`${$.escape(text1())}${$.escape(text2())}
`); } \ No newline at end of file