From a77fab9d176b27cfb4752a42a4fe3027d9f393ce Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 26 Oct 2025 12:17:25 -0400 Subject: [PATCH] DRY --- .../3-transform/client/visitors/Program.js | 209 +---------------- .../3-transform/server/visitors/Program.js | 212 +----------------- .../3-transform/shared/transform-async.js | 200 +++++++++++++++++ 3 files changed, 221 insertions(+), 400 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js 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 603c7bd1a8..1bb9c0c911 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,11 +1,10 @@ -/** @import { BlockStatement, ClassDeclaration, ClassExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, Program, Statement, VariableDeclaration, VariableDeclarator } from 'estree' */ +/** @import { Expression, ImportDeclaration, MemberExpression, Node, Program } from 'estree' */ /** @import { ComponentContext } from '../types' */ -/** @import { AwaitedStatement } 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'; -import { get_rune } from '../../../scope.js'; import { runes } from '../../../../state.js'; +import { transform_body } from '../../shared/transform-async.js'; /** * @param {Program} node @@ -143,205 +142,15 @@ export function Program(node, context) { if (context.state.is_instance && runes) { return { ...node, - body: transform_body(node, context) + body: transform_body( + node, + context.state.analysis.awaited_statements, + b.id('$.run'), + (node) => /** @type {Node} */ (context.visit(node)), + (statement) => context.state.hoisted.push(statement) + ) }; } context.next(); } - -// TODO find a way to DRY out this and the corresponding server visitor -/** - * @param {Program} program - * @param {ComponentContext} context - */ -function transform_body(program, context) { - /** @type {Statement[]} */ - const out = []; - - /** @type {AwaitedStatement[]} */ - const statements = []; - - /** @type {AwaitedStatement[]} */ - const deriveds = []; - - const { awaited_statements } = context.state.analysis; - - let awaited = false; - - /** - * @param {Statement | VariableDeclarator | ClassDeclaration | FunctionDeclaration} node - */ - const push = (node) => { - const statement = awaited_statements.get(node); - - awaited ||= !!statement?.has_await; - - if (!awaited || !statement || node.type === 'FunctionDeclaration') { - if (node.type === 'VariableDeclarator') { - out.push(/** @type {VariableDeclaration} */ (context.visit(b.var(node.id, node.init)))); - } else { - out.push(/** @type {Statement} */ (context.visit(node))); - } - - return; - } - - // TODO put deriveds into a separate array, and group them immediately - // after their latest dependency. for now, to avoid having to figure - // out the intricacies of dependency tracking, just let 'em waterfall - // if (node.type === 'VariableDeclarator') { - // const rune = get_rune(node.init, context.state.scope); - - // if (rune === '$derived' || rune === '$derived.by') { - // deriveds.push(statement); - // return; - // } - // } - - statements.push(statement); - }; - - 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) { - push(declarator); - } - } else { - push(node); - } - } - - for (const derived of deriveds) { - // find the earliest point we can insert this derived - let index = -1; - - for (const binding of derived.reads) { - index = Math.max( - index, - statements.findIndex((s) => s.declarations.includes(binding)) - ); - } - - if (index === -1 && !derived.has_await) { - const node = /** @type {VariableDeclarator} */ (derived.node); - out.push(/** @type {VariableDeclaration} */ (context.visit(b.var(node.id, node.init)))); - } else { - // TODO combine deriveds with Promise.all where necessary - statements.splice(index + 1, 0, derived); - } - } - - var promises = b.id('$$promises'); // TODO if we use this technique for fragments, need to deconflict - - if (statements.length > 0) { - var declarations = statements.map((s) => s.declarations).flat(); - - if (declarations.length > 0) { - out.push( - b.declaration( - 'var', - declarations.map((d) => b.declarator(d.node)) - ) - ); - } - - const thunks = statements.map((s) => { - if (s.node.type === 'VariableDeclarator') { - const visited = /** @type {VariableDeclaration} */ ( - context.visit(b.var(s.node.id, s.node.init)) - ); - - if (visited.declarations.length === 1) { - return b.thunk( - b.assignment('=', s.node.id, visited.declarations[0].init ?? b.void0), - s.has_await - ); - } - - // if we have multiple declarations, it indicates destructuring - return b.thunk( - b.block([ - b.var(visited.declarations[0].id, visited.declarations[0].init), - ...visited.declarations - .slice(1) - .map((d) => b.stmt(b.assignment('=', d.id, d.init ?? b.void0))) - ]), - s.has_await - ); - } - - if (s.node.type === 'ClassDeclaration') { - return b.thunk( - b.assignment( - '=', - s.node.id, - /** @type {ClassExpression} */ ({ ...s.node, type: 'ClassExpression' }) - ), - s.has_await - ); - } - - if (s.node.type === 'FunctionDeclaration') { - return b.thunk( - b.assignment( - '=', - s.node.id, - /** @type {FunctionExpression} */ ({ ...s.node, type: 'FunctionExpression' }) - ), - s.has_await - ); - } - - if (s.node.type === 'ExpressionStatement') { - const expression = /** @type {Expression} */ (context.visit(s.node.expression)); - - return expression.type === 'AwaitExpression' - ? b.thunk(expression, true) - : b.thunk(b.unary('void', expression), s.has_await); - } - - return b.thunk(b.block([/** @type {Statement} */ (context.visit(s.node))]), s.has_await); - }); - - out.push(b.var(promises, b.call('$.run', b.array(thunks)))); - - for (let i = 0; i < statements.length; i += 1) { - const s = statements[i]; - - var blocker = b.member(promises, b.literal(i), true); - - for (const binding of s.declarations) { - binding.blocker = blocker; - } - - for (const binding of s.writes) { - // if a statement writes to a binding, any reads of that - // binding must wait for the statement - binding.blocker = blocker; - } - } - } - - return out; -} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js index a442939287..55fcba8c5f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Program.js @@ -1,8 +1,8 @@ -/** @import { BlockStatement, ClassDeclaration, ClassExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, Program, Statement, VariableDeclaration, VariableDeclarator } from 'estree' */ +/** @import { Node, Program } from 'estree' */ /** @import { Context, ComponentContext } from '../types' */ -/** @import { AwaitedStatement } from '../../../types' */ import * as b from '#compiler/builders'; import { runes } from '../../../../state.js'; +import { transform_body } from '../../shared/transform-async.js'; /** * @param {Program} node @@ -10,208 +10,20 @@ import { runes } from '../../../../state.js'; */ export function Program(node, context) { if (context.state.is_instance && runes) { + // @ts-ignore wtf + const c = /** @type {ComponentContext} */ (context); + return { ...node, - // @ts-ignore wtf - body: transform_body(node, /** @type {ComponentContext} */ (context)) + body: transform_body( + node, + c.state.analysis.awaited_statements, + b.id('$$renderer.run'), + (node) => /** @type {Node} */ (context.visit(node)), + (statement) => c.state.hoisted.push(statement) + ) }; } context.next(); } - -// TODO find a way to DRY out this and the corresponding server visitor -/** - * @param {Program} program - * @param {ComponentContext} context - */ -function transform_body(program, context) { - /** @type {Statement[]} */ - const out = []; - - /** @type {AwaitedStatement[]} */ - const statements = []; - - /** @type {AwaitedStatement[]} */ - const deriveds = []; - - const { awaited_statements } = context.state.analysis; - - let awaited = false; - - /** - * @param {Statement | VariableDeclarator | ClassDeclaration | FunctionDeclaration} node - */ - const push = (node) => { - const statement = awaited_statements.get(node); - - awaited ||= !!statement?.has_await; - - if (!awaited || !statement || node.type === 'FunctionDeclaration') { - if (node.type === 'VariableDeclarator') { - out.push(/** @type {VariableDeclaration} */ (context.visit(b.var(node.id, node.init)))); - } else { - out.push(/** @type {Statement} */ (context.visit(node))); - } - - return; - } - - // TODO put deriveds into a separate array, and group them immediately - // after their latest dependency. for now, to avoid having to figure - // out the intricacies of dependency tracking, just let 'em waterfall - // if (node.type === 'VariableDeclarator') { - // const rune = get_rune(node.init, context.state.scope); - - // if (rune === '$derived' || rune === '$derived.by') { - // deriveds.push(statement); - // return; - // } - // } - - statements.push(statement); - }; - - 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) { - push(declarator); - } - } else { - push(node); - } - } - - for (const derived of deriveds) { - // find the earliest point we can insert this derived - let index = -1; - - for (const binding of derived.reads) { - index = Math.max( - index, - statements.findIndex((s) => s.declarations.includes(binding)) - ); - } - - if (index === -1 && !derived.has_await) { - const node = /** @type {VariableDeclarator} */ (derived.node); - out.push(/** @type {VariableDeclaration} */ (context.visit(b.var(node.id, node.init)))); - } else { - // TODO combine deriveds with Promise.all where necessary - statements.splice(index + 1, 0, derived); - } - } - - var promises = b.id('$$promises'); // TODO if we use this technique for fragments, need to deconflict - - if (statements.length > 0) { - var declarations = statements.map((s) => s.declarations).flat(); - - if (declarations.length > 0) { - out.push( - b.declaration( - 'var', - declarations.map((d) => b.declarator(d.node)) - ) - ); - } - - const thunks = statements.map((s) => { - if (s.node.type === 'VariableDeclarator') { - const visited = /** @type {VariableDeclaration} */ ( - context.visit(b.var(s.node.id, s.node.init)) - ); - - if (visited.declarations.length === 1) { - return b.thunk( - b.assignment('=', s.node.id, visited.declarations[0].init ?? b.void0), - s.has_await - ); - } - - // if we have multiple declarations, it indicates destructuring - return b.thunk( - b.block([ - b.var(visited.declarations[0].id, visited.declarations[0].init), - ...visited.declarations - .slice(1) - .map((d) => b.stmt(b.assignment('=', d.id, d.init ?? b.void0))) - ]), - s.has_await - ); - } - - if (s.node.type === 'ClassDeclaration') { - return b.thunk( - b.assignment( - '=', - s.node.id, - /** @type {ClassExpression} */ ({ ...s.node, type: 'ClassExpression' }) - ), - s.has_await - ); - } - - if (s.node.type === 'FunctionDeclaration') { - return b.thunk( - b.assignment( - '=', - s.node.id, - /** @type {FunctionExpression} */ ({ ...s.node, type: 'FunctionExpression' }) - ), - s.has_await - ); - } - - if (s.node.type === 'ExpressionStatement') { - const expression = /** @type {Expression} */ (context.visit(s.node.expression)); - - return expression.type === 'AwaitExpression' - ? b.thunk(expression, true) - : b.thunk(b.unary('void', expression), s.has_await); - } - - return b.thunk(b.block([/** @type {Statement} */ (context.visit(s.node))]), s.has_await); - }); - - out.push(b.var(promises, b.call('$$renderer.run', b.array(thunks)))); - - for (let i = 0; i < statements.length; i += 1) { - const s = statements[i]; - - var blocker = b.member(promises, b.literal(i), true); - - for (const binding of s.declarations) { - binding.blocker = blocker; - } - - for (const binding of s.writes) { - // if a statement writes to a binding, any reads of that - // binding must wait for the statement - binding.blocker = blocker; - } - } - } - - return out; -} diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js b/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js new file mode 100644 index 0000000000..b77ecacea7 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/shared/transform-async.js @@ -0,0 +1,200 @@ +/** @import * as ESTree from 'estree' */ +/** @import { AwaitedStatement } from '../../types' */ +import * as b from '#compiler/builders'; + +// TODO find a way to DRY out this and the corresponding server visitor +/** + * @param {ESTree.Program} program + * @param {Map} awaited_statements + * @param {ESTree.Expression} runner + * @param {(node: ESTree.Node) => ESTree.Node} transform + * @param {(node: ESTree.Statement | ESTree.ModuleDeclaration) => void} hoist + */ +export function transform_body(program, awaited_statements, runner, transform, hoist) { + /** @type {ESTree.Statement[]} */ + const out = []; + + /** @type {AwaitedStatement[]} */ + const statements = []; + + /** @type {AwaitedStatement[]} */ + const deriveds = []; + + let awaited = false; + + /** + * @param {ESTree.Statement | ESTree.VariableDeclarator | ESTree.ClassDeclaration | ESTree.FunctionDeclaration} node + */ + const push = (node) => { + const statement = awaited_statements.get(node); + + awaited ||= !!statement?.has_await; + + if (!awaited || !statement || node.type === 'FunctionDeclaration') { + if (node.type === 'VariableDeclarator') { + out.push(/** @type {ESTree.VariableDeclaration} */ (transform(b.var(node.id, node.init)))); + } else { + out.push(/** @type {ESTree.Statement} */ (transform(node))); + } + + return; + } + + // TODO put deriveds into a separate array, and group them immediately + // after their latest dependency. for now, to avoid having to figure + // out the intricacies of dependency tracking, just let 'em waterfall + // if (node.type === 'VariableDeclarator') { + // const rune = get_rune(node.init, context.state.scope); + + // if (rune === '$derived' || rune === '$derived.by') { + // deriveds.push(statement); + // return; + // } + // } + + statements.push(statement); + }; + + for (let node of program.body) { + if (node.type === 'ImportDeclaration') { + // TODO we can get rid of the visitor + hoist(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) { + push(declarator); + } + } else { + push(node); + } + } + + for (const derived of deriveds) { + // find the earliest point we can insert this derived + let index = -1; + + for (const binding of derived.reads) { + index = Math.max( + index, + statements.findIndex((s) => s.declarations.includes(binding)) + ); + } + + if (index === -1 && !derived.has_await) { + const node = /** @type {ESTree.VariableDeclarator} */ (derived.node); + out.push(/** @type {ESTree.VariableDeclaration} */ (transform(b.var(node.id, node.init)))); + } else { + // TODO combine deriveds with Promise.all where necessary + statements.splice(index + 1, 0, derived); + } + } + + var promises = b.id('$$promises'); // TODO if we use this technique for fragments, need to deconflict + + if (statements.length > 0) { + var declarations = statements.map((s) => s.declarations).flat(); + + if (declarations.length > 0) { + out.push( + b.declaration( + 'var', + declarations.map((d) => b.declarator(d.node)) + ) + ); + } + + const thunks = statements.map((s) => { + if (s.node.type === 'VariableDeclarator') { + const visited = /** @type {ESTree.VariableDeclaration} */ ( + transform(b.var(s.node.id, s.node.init)) + ); + + if (visited.declarations.length === 1) { + return b.thunk( + b.assignment('=', s.node.id, visited.declarations[0].init ?? b.void0), + s.has_await + ); + } + + // if we have multiple declarations, it indicates destructuring + return b.thunk( + b.block([ + b.var(visited.declarations[0].id, visited.declarations[0].init), + ...visited.declarations + .slice(1) + .map((d) => b.stmt(b.assignment('=', d.id, d.init ?? b.void0))) + ]), + s.has_await + ); + } + + if (s.node.type === 'ClassDeclaration') { + return b.thunk( + b.assignment( + '=', + s.node.id, + /** @type {ESTree.ClassExpression} */ ({ ...s.node, type: 'ClassExpression' }) + ), + s.has_await + ); + } + + if (s.node.type === 'FunctionDeclaration') { + return b.thunk( + b.assignment( + '=', + s.node.id, + /** @type {ESTree.FunctionExpression} */ ({ ...s.node, type: 'FunctionExpression' }) + ), + s.has_await + ); + } + + if (s.node.type === 'ExpressionStatement') { + const expression = /** @type {ESTree.Expression} */ (transform(s.node.expression)); + + return expression.type === 'AwaitExpression' + ? b.thunk(expression, true) + : b.thunk(b.unary('void', expression), s.has_await); + } + + return b.thunk(b.block([/** @type {ESTree.Statement} */ (transform(s.node))]), s.has_await); + }); + + out.push(b.var(promises, b.call(runner, b.array(thunks)))); + + for (let i = 0; i < statements.length; i += 1) { + const s = statements[i]; + + var blocker = b.member(promises, b.literal(i), true); + + for (const binding of s.declarations) { + binding.blocker = blocker; + } + + for (const binding of s.writes) { + // if a statement writes to a binding, any reads of that + // binding must wait for the statement + binding.blocker = blocker; + } + } + } + + return out; +}