From 795fa1ab258a5dd4b78b1e067b74d888590f5d19 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Oct 2025 20:08:06 -0400 Subject: [PATCH] WIP --- .../src/compiler/phases/2-analyze/index.js | 27 +++- .../3-transform/client/transform-client.js | 43 ++----- .../3-transform/client/visitors/Program.js | 116 +++++++++++++++++- .../svelte/src/compiler/phases/types.d.ts | 18 ++- packages/svelte/src/compiler/utils/ast.js | 2 +- .../svelte/src/compiler/utils/builders.js | 8 +- 6 files changed, 167 insertions(+), 47 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 2656515e3b..12066db550 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -10,7 +10,7 @@ import { extract_identifiers, has_await_expression } from '../../utils/ast.js'; import * as b from '#compiler/builders'; import { Scope, ScopeRoot, create_scopes, get_rune, set_scope } from '../scope.js'; import check_graph_for_cycles from './utils/check_graph_for_cycles.js'; -import { create_attribute, is_custom_element_node } from '../nodes.js'; +import { create_attribute, create_expression_metadata, is_custom_element_node } from '../nodes.js'; import { analyze_css } from './css/css-analyze.js'; import { prune } from './css/css-prune.js'; import { hash, is_rune } from '../../../utils.js'; @@ -131,7 +131,18 @@ const visitors = { ignore_map.set(node, structuredClone(ignore_stack)); const scope = state.scopes.get(node); - next(scope !== undefined && scope !== state.scope ? { ...state, scope } : state); + const awaited = state.analysis.awaited_statements.get(node) ?? null; + + if (awaited || (scope !== undefined && scope !== state.scope)) { + const child_state = { ...state }; + + if (awaited) child_state.expression = awaited.metadata; + if (scope !== undefined && scope !== state.scope) child_state.scope = scope; + + next(child_state); + } else { + next(); + } if (ignores.length > 0) { pop_ignore(); @@ -707,12 +718,19 @@ export function analyze_component(root, source, options) { const id = b.id(`$$${i++}`); + analysis.awaited_statements.set(declarator, { + id, + has_await, + metadata: create_expression_metadata() + }); + for (const identifier of extract_identifiers(declarator.id)) { analysis.awaited_declarations.set(identifier.name, { id, has_await, pattern: declarator.id, - updated_by: new Set() + updated_by: new Set(), + metadata: create_expression_metadata() }); } } @@ -729,7 +747,8 @@ export function analyze_component(root, source, options) { analysis.awaited_statements.set(node, { id, - has_await + has_await, + metadata: create_expression_metadata() }); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 0dd4ae03b9..6fdca199eb 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -370,41 +370,22 @@ export function client_component(analysis, options) { analysis.reactive_statements.size > 0 || component_returned_object.length > 0; - if (analysis.instance.has_await) { - if (should_inject_context && component_returned_object.length > 0) { - component_block.body.push(b.var('$$exports')); - } - const body = b.block([ - ...store_setup, - ...state.instance_level_snippets, - .../** @type {ESTree.Statement[]} */ (instance.body), - ...(should_inject_context && component_returned_object.length > 0 - ? [b.stmt(b.assignment('=', b.id('$$exports'), b.object(component_returned_object)))] - : []), - b.if(b.call('$.aborted'), b.return()), - .../** @type {ESTree.Statement[]} */ (template.body) - ]); - - component_block.body.push( - b.stmt(b.call(`$.async_body`, b.id('$$anchor'), b.arrow([b.id('$$anchor')], body, true))) - ); - } else { - component_block.body.push( - ...state.instance_level_snippets, - .../** @type {ESTree.Statement[]} */ (instance.body) - ); - if (should_inject_context && component_returned_object.length > 0) { - component_block.body.push(b.var('$$exports', b.object(component_returned_object))); - } - component_block.body.unshift(...store_setup); + component_block.body.push( + ...state.instance_level_snippets, + .../** @type {ESTree.Statement[]} */ (instance.body) + ); - if (!analysis.runes && analysis.needs_context) { - component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined))); - } + if (should_inject_context && component_returned_object.length > 0) { + component_block.body.push(b.var('$$exports', b.object(component_returned_object))); + } + component_block.body.unshift(...store_setup); - component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body)); + if (!analysis.runes && analysis.needs_context) { + component_block.body.push(b.stmt(b.call('$.init', analysis.immutable ? b.true : undefined))); } + component_block.body.push(.../** @type {ESTree.Statement[]} */ (template.body)); + if (analysis.needs_mutation_validation) { component_block.body.unshift( b.var('$$ownership_validator', b.call('$.create_ownership_validator', b.id('$$props'))) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js index 07342da314..c834f83706 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -1,14 +1,14 @@ -/** @import { Expression, ImportDeclaration, MemberExpression, Program } from 'estree' */ +/** @import { Expression, Identifier, ImportDeclaration, MemberExpression, Program, Statement } from 'estree' */ /** @import { ComponentContext } from '../types' */ import { build_getter, is_prop_source } from '../utils.js'; import * as b from '#compiler/builders'; import { add_state_transformers } from './shared/declarations.js'; /** - * @param {Program} _ + * @param {Program} node * @param {ComponentContext} context */ -export function Program(_, context) { +export function Program(node, context) { if (!context.state.analysis.runes) { context.state.transform['$$props'] = { read: (node) => ({ ...node, name: '$$sanitized_props' }) @@ -137,5 +137,115 @@ export function Program(_, context) { add_state_transformers(context); + if (context.state.is_instance) { + return { + ...node, + body: transform_body(node, context) + }; + } + context.next(); } + +/** + * @param {Program} program + * @param {ComponentContext} context + */ +function transform_body(program, context) { + /** @type {Statement[]} */ + const out = []; + + const { awaited_declarations, awaited_statements } = context.state.analysis; + + /** @type {Identifier | null} */ + let last = null; + + for (let node of program.body) { + if (node.type === 'ImportDeclaration') { + // TODO we can get rid of the visitor + context.state.hoisted.push(node); + continue; + } + + if (node.type === 'ExportDefaultDeclaration' || node.type === 'ExportAllDeclaration') { + // this can't happen, but it's useful for TypeScript to understand that + continue; + } + + if (node.type === 'ExportNamedDeclaration') { + if (node.declaration) { + // TODO ditto — no visitor needed + node = node.declaration; + } else { + continue; + } + } + + if (node.type === 'VariableDeclaration') { + for (const declarator of node.declarations) { + const awaited = awaited_statements.get(declarator); + + if (awaited) { + // TODO dependencies + out.push( + b.var( + awaited.id, + b.call( + '$.run', + b.array([]), + b.arrow([], declarator.init ?? b.block([]), awaited.has_await) + ) + ) + ); + + last = awaited.id; + } else { + out.push(b.var(declarator.id, declarator.init)); + } + } + } else if (node.type === 'ClassDeclaration' || node.type === 'FunctionDeclaration') { + // TODO + } else { + const awaited = awaited_statements.get(node); + + if (awaited) { + const ids = new Set(); + const patterns = new Set(); + + for (const binding of awaited.metadata.dependencies) { + const dep = awaited_declarations.get(binding.node.name); + if (dep && !ids.has(dep.id)) { + ids.add(dep.id); + patterns.add(dep.pattern); + } + } + + if (last) { + ids.add(last); + } + + // TODO dependencies + out.push( + b.var( + awaited.id, + b.call( + '$.run', + b.array([...ids]), + b.arrow( + [...patterns], + node.type === 'ExpressionStatement' ? node.expression : b.block([node]), + awaited.has_await + ) + ) + ) + ); + + last = awaited.id; + } else { + out.push(node); + } + } + } + + return out.map((node) => /** @type {Statement} */ (context.visit(node))); +} diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index 5d21666e9e..baaa64f0e3 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -1,4 +1,4 @@ -import type { AST, Binding, StateField } from '#compiler'; +import type { AST, Binding, ExpressionMetadata, StateField } from '#compiler'; import type { AwaitExpression, CallExpression, @@ -8,7 +8,8 @@ import type { ModuleDeclaration, Pattern, Program, - Statement + Statement, + VariableDeclarator } from 'estree'; import type { Scope, ScopeRoot } from './scope.js'; @@ -126,11 +127,20 @@ export interface ComponentAnalysis extends Analysis { */ awaited_declarations: Map< string, - { id: Identifier; has_await: boolean; pattern: Pattern; updated_by: Set } + { + id: Identifier; + has_await: boolean; + pattern: Pattern; + metadata: ExpressionMetadata; + updated_by: Set; + } >; /** * Information about top-level instance statements that need to be transformed * so that we can run the template synchronously */ - awaited_statements: Map; + awaited_statements: Map< + Statement | ModuleDeclaration | VariableDeclarator, + { id: Identifier; has_await: boolean; metadata: ExpressionMetadata } + >; } diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js index bd92dda5d9..75aadd905b 100644 --- a/packages/svelte/src/compiler/utils/ast.js +++ b/packages/svelte/src/compiler/utils/ast.js @@ -1,4 +1,4 @@ -/** @import { AST } from '#compiler' */ +/** @import { AST, Scope } from '#compiler' */ /** @import * as ESTree from 'estree' */ import { walk } from 'zimmerframe'; import * as b from '#compiler/builders'; diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index 1a2d5cab5c..8a3f04f4ad 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -184,7 +184,7 @@ export function declaration(kind, declarations) { /** * @param {ESTree.Pattern | string} pattern - * @param {ESTree.Expression} [init] + * @param {ESTree.Expression | null} [init] * @returns {ESTree.VariableDeclarator} */ export function declarator(pattern, init) { @@ -520,7 +520,7 @@ const this_instance = { /** * @param {string | ESTree.Pattern} pattern - * @param { ESTree.Expression} [init] + * @param {ESTree.Expression | null} [init] * @returns {ESTree.VariableDeclaration} */ function let_builder(pattern, init) { @@ -529,7 +529,7 @@ function let_builder(pattern, init) { /** * @param {string | ESTree.Pattern} pattern - * @param { ESTree.Expression} init + * @param {ESTree.Expression | null} init * @returns {ESTree.VariableDeclaration} */ function const_builder(pattern, init) { @@ -538,7 +538,7 @@ function const_builder(pattern, init) { /** * @param {string | ESTree.Pattern} pattern - * @param { ESTree.Expression} [init] + * @param {ESTree.Expression | null} [init] * @returns {ESTree.VariableDeclaration} */ function var_builder(pattern, init) {