From 8773216e01c4576b8dcda7514227bca17ae6f307 Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Fri, 12 Sep 2025 19:06:49 -0600 Subject: [PATCH] got the extra children to go away --- .../phases/2-analyze/visitors/AwaitBlock.js | 5 ++++- .../phases/2-analyze/visitors/AwaitExpression.js | 10 +++++----- .../compiler/phases/2-analyze/visitors/EachBlock.js | 5 +++++ .../compiler/phases/2-analyze/visitors/Fragment.js | 2 +- .../compiler/phases/2-analyze/visitors/IfBlock.js | 4 ++++ packages/svelte/src/internal/server/payload.js | 12 ++++++++---- .../_expected/server/index.svelte.js | 13 ++++--------- .../_expected/server/index.svelte.js | 6 ++---- .../_expected/server/index.svelte.js | 10 ++-------- .../_expected/server/index.svelte.js | 10 ++-------- 10 files changed, 37 insertions(+), 40 deletions(-) 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 f9bbcd7031..b7dcbf1de4 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js @@ -41,9 +41,12 @@ export function AwaitBlock(node, context) { mark_subtree_dynamic(context.path); - // 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.metadata.expression.has_await && context.state.fragment) { + context.state.fragment.metadata.is_async = true; + } + 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 f32aede4d2..71da9c2837 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -18,11 +18,11 @@ export function AwaitExpression(node, context) { } 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) - ); + // 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/EachBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js index 215edda8ad..1c9e7dc1b7 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js @@ -35,6 +35,11 @@ export function EachBlock(node, context) { scope: /** @type {Scope} */ (context.state.scope.parent) }); + // TODO it should be impossible to be in the template and not have a fragment... + if (node.metadata.expression.has_await && context.state.fragment) { + context.state.fragment.metadata.is_async = true; + } + context.visit(node.body); if (node.key) context.visit(node.key); if (node.fallback) { 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 252936e067..54890c325a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Fragment.js @@ -12,7 +12,7 @@ export function Fragment(node, context) { // 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; + 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 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 10228397e0..2ec87d3b6f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/IfBlock.js @@ -22,6 +22,10 @@ export function IfBlock(node, context) { expression: node.metadata.expression }); + if (node.metadata.expression.has_await && context.state.fragment) { + context.state.fragment.metadata.is_async = true; + } + context.visit(node.consequent); if (node.alternate) { context.visit(node.alternate); diff --git a/packages/svelte/src/internal/server/payload.js b/packages/svelte/src/internal/server/payload.js index 57b2eef6c3..358f02c5c9 100644 --- a/packages/svelte/src/internal/server/payload.js +++ b/packages/svelte/src/internal/server/payload.js @@ -32,7 +32,7 @@ export class Payload { type; /** @type {Payload | undefined} */ - parent; + #parent; /** * Asynchronous work associated with this payload. `initial` is the promise from the function @@ -69,7 +69,7 @@ export class Payload { constructor(global, local = { select_value: undefined }, parent, type) { this.global = global; this.local = { ...local }; - this.parent = parent; + this.#parent = parent; this.type = type ?? parent?.type ?? 'body'; } @@ -108,6 +108,10 @@ export class Payload { */ push(content) { if (typeof content === 'function') { + if (this.global.mode === 'sync') { + // TODO more-proper error + throw new Error('Encountered an asynchronous component while rendering synchronously'); + } this.#out.push(content()); } else { this.#out.push(content); @@ -148,7 +152,7 @@ export class Payload { * @returns {number[]} */ get_path() { - return this.parent ? [...this.parent.get_path(), this.parent.#out.indexOf(this)] : []; + return this.#parent ? [...this.#parent.get_path(), this.#parent.#out.indexOf(this)] : []; } /** @@ -170,7 +174,7 @@ export class Payload { } copy() { - const copy = new Payload(this.global, this.local, this.parent, this.type); + const copy = new Payload(this.global, this.local, this.#parent, this.type); copy.#out = this.#out.map((item) => (item instanceof Payload ? item.copy() : item)); copy.promises = this.promises; return copy; diff --git a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js index 80c37a8d78..08cf3148b2 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-fallback-hoisting/_expected/server/index.svelte.js @@ -10,18 +10,13 @@ export default function Async_each_fallback_hoisting($$payload) { for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) { let item = each_array[$$index]; - $$payload.child(async ($$payload) => { - $$payload.push(``); - $$payload.push(async () => $.escape(await Promise.reject('This should never be reached'))); - }); + $$payload.push(``); + $$payload.push(async () => $.escape(await Promise.reject('This should never be reached'))); } } else { $$payload.push(''); - - $$payload.child(async ($$payload) => { - $$payload.push(``); - $$payload.push(async () => $.escape(await Promise.resolve(4))); - }); + $$payload.push(``); + $$payload.push(async () => $.escape(await Promise.resolve(4))); } $$payload.push(``); diff --git a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js index 9e3e81f4f2..67c1e2dd8e 100644 --- a/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-each-hoisting/_expected/server/index.svelte.js @@ -13,10 +13,8 @@ export default function Async_each_hoisting($$payload) { for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) { let item = each_array[$$index]; - $$payload.child(async ($$payload) => { - $$payload.push(``); - $$payload.push(async () => $.escape(await item)); - }); + $$payload.push(``); + $$payload.push(async () => $.escape(await item)); } $$payload.push(``); diff --git a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js index 92941fc87c..dcbb65e3d9 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-alternate-hoisting/_expected/server/index.svelte.js @@ -4,16 +4,10 @@ export default function Async_if_alternate_hoisting($$payload) { $$payload.child(async ($$payload) => { if (await Promise.resolve(false)) { $$payload.push(''); - - $$payload.child(async ($$payload) => { - $$payload.push(async () => $.escape(await Promise.reject('no no no'))); - }); + $$payload.push(async () => $.escape(await Promise.reject('no no no'))); } else { $$payload.push(''); - - $$payload.child(async ($$payload) => { - $$payload.push(async () => $.escape(await Promise.resolve('yes yes yes'))); - }); + $$payload.push(async () => $.escape(await Promise.resolve('yes yes yes'))); } $$payload.push(``); diff --git a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js index 83da0a2711..df3af3db9d 100644 --- a/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/async-if-hoisting/_expected/server/index.svelte.js @@ -4,16 +4,10 @@ export default function Async_if_hoisting($$payload) { $$payload.child(async ($$payload) => { if (await Promise.resolve(true)) { $$payload.push(''); - - $$payload.child(async ($$payload) => { - $$payload.push(async () => $.escape(await Promise.resolve('yes yes yes'))); - }); + $$payload.push(async () => $.escape(await Promise.resolve('yes yes yes'))); } else { $$payload.push(''); - - $$payload.child(async ($$payload) => { - $$payload.push(async () => $.escape(await Promise.reject('no no no'))); - }); + $$payload.push(async () => $.escape(await Promise.reject('no no no'))); } $$payload.push(``);