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 2573b05f21..b1beb0660b 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; + boundary: AST.SvelteBoundary | 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/AwaitExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js index 68480f2c3b..6d6058f01f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitExpression.js @@ -37,9 +37,11 @@ export function AwaitExpression(node, context) { if (!context.state.analysis.runes) { e.legacy_await_invalid(node); } - - context.state.analysis.suspends = true; } + // the await will only block if there's no `pending` snippet + context.state.analysis.has_blocking_await ||= + suspend && !context.state.boundary?.metadata.pending; + context.next(); } 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 35af96ba12..d195e01f86 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/SvelteBoundary.js @@ -14,6 +14,10 @@ export function SvelteBoundary(node, context) { e.svelte_boundary_invalid_attribute(attribute); } + if (attribute.name === 'pending') { + node.metadata.pending = attribute; + } + if ( attribute.value === true || (Array.isArray(attribute.value) && @@ -23,5 +27,12 @@ export function SvelteBoundary(node, context) { } } - context.next(); + node.metadata.pending ??= + /** @type {AST.SnippetBlock | undefined} */ ( + node.fragment.nodes.find( + (node) => node.type === 'SnippetBlock' && node.expression.name === 'pending' + ) + ) ?? null; + + 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 eadcefe6b5..a3b59c74be 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 @@ -247,7 +247,7 @@ export function server_component(analysis, options) { .../** @type {Statement[]} */ (instance.body), .../** @type {Statement[]} */ (template.body) ]), - analysis.suspends + analysis.has_blocking_await ) ) ) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js index 6e814d6384..252686d2b8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteBoundary.js @@ -12,21 +12,11 @@ import { build_attribute_value } from './shared/utils.js'; export function SvelteBoundary(node, context) { context.state.template.push(b.literal(BLOCK_OPEN)); - // if this has a `pending` snippet, render it - const pending_attribute = /** @type {AST.Attribute} */ ( - node.attributes.find((node) => node.type === 'Attribute' && node.name === 'pending') - ); - - const pending_snippet = /** @type {AST.SnippetBlock} */ ( - node.fragment.nodes.find( - (node) => node.type === 'SnippetBlock' && node.expression.name === 'pending' - ) - ); - - if (pending_attribute) { - const value = build_attribute_value(pending_attribute.value, context, false, true); + const pending_snippet = node.metadata.pending; + if (pending_snippet?.type === 'Attribute') { + const value = build_attribute_value(pending_snippet.value, context, false, true); context.state.template.push(b.call(value, b.id('$$payload'))); - } else if (pending_snippet) { + } else if (pending_snippet?.type === 'SnippetBlock') { context.state.template.push( /** @type {BlockStatement} */ (context.visit(pending_snippet.body)) ); diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index 109c6d9142..a73472e44f 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -106,8 +106,8 @@ export interface ComponentAnalysis extends Analysis { * Every snippet that is declared locally */ snippets: Set; - /** Whether the component uses `await` in a context that would cause suspense. */ - suspends: boolean; + /** Whether the component uses `await` in a context that would require an `await` on the server. */ + has_blocking_await: boolean; } declare module 'estree' { diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 2fc5b7bf16..59736b0fc8 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -401,6 +401,10 @@ export namespace AST { export interface SvelteBoundary extends BaseElement { type: 'SvelteBoundary'; name: 'svelte:boundary'; + /** @internal */ + metadata: { + pending: SnippetBlock | Attribute | null; + }; } export interface SvelteHead extends BaseElement { diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index ce37205ef2..54f8d7a8cc 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -564,7 +564,9 @@ export function valueless_option(payload, children) { children(); - var body = payload.out.slice(i).join(''); + // TODO this seems really likely to break in async world; we really need to find a better way to do this + // @ts-expect-error + var body = collect_body(payload.out.slice(i)); if (body.replace(//g, '') === payload.select_value) { // replace '>' with ' selected>' (closing tag will be added later) @@ -577,3 +579,19 @@ export function valueless_option(payload, children) { payload.out.splice(i, payload.out.length - i, body); } } + +/** + * @param {(string | Payload)[]} out_fragment + * @returns {string} + */ +function collect_body(out_fragment) { + let body = ''; + for (const item of out_fragment) { + if (typeof item === 'string') { + body += item; + } else { + body += collect_body(/** @type {(string | Payload)[]} */ (item.out)); + } + } + return body; +} diff --git a/packages/svelte/src/internal/server/payload.js b/packages/svelte/src/internal/server/payload.js index fb00922f48..25f04ad8eb 100644 --- a/packages/svelte/src/internal/server/payload.js +++ b/packages/svelte/src/internal/server/payload.js @@ -46,7 +46,8 @@ class BasePayload { * @returns {void} */ child(render) { - const child = new BasePayload(this._state); + // @ts-expect-error dynamic constructor invocation for subclass instance creation + const child = new this.constructor(this._state); this.out.push(child); const result = render({ $$payload: child }); if (result instanceof Promise) { @@ -85,8 +86,10 @@ class BasePayload { */ #collect_promises(items, promises = this.promise ? [this.promise] : []) { for (const item of items) { - if (item instanceof BasePayload && item.promise) { - promises.push(item.promise); + if (item instanceof BasePayload) { + if (item.promise) { + promises.push(item.promise); + } this.#collect_promises(item.out, promises); } } @@ -204,7 +207,8 @@ export function copy_payload({ out, css, head, uid }) { uid, head: new HeadPayload({ css: new Set(head.css), - title: head.title, + // @ts-expect-error + title: head._state.title, uid: head.uid }) });