From ba95b56c696aaf06840ea8e4ac5ce07e054895ba Mon Sep 17 00:00:00 2001 From: "S. Elliott Johnson" Date: Thu, 21 Aug 2025 20:47:03 -0600 Subject: [PATCH] today's hard work; few tests left to fix --- .../src/compiler/phases/2-analyze/index.js | 8 +- .../src/compiler/phases/2-analyze/types.d.ts | 1 + .../2-analyze/visitors/AwaitExpression.js | 10 +- .../phases/2-analyze/visitors/TitleElement.js | 2 +- .../3-transform/server/transform-server.js | 4 +- .../server/visitors/RegularElement.js | 26 +- .../server/visitors/TitleElement.js | 17 +- .../server/visitors/shared/component.js | 2 +- .../server/visitors/shared/utils.js | 16 +- .../svelte/src/compiler/types/template.d.ts | 4 + packages/svelte/src/internal/server/index.js | 67 ++--- .../svelte/src/internal/server/payload.js | 250 ++++++++++-------- 12 files changed, 249 insertions(+), 158 deletions(-) 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 `