From 038754bfc61d2836c2181d83a57c7707b1d95525 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 1 Aug 2024 15:27:32 -0400 Subject: [PATCH] chore: client transform visitors refactor (#12683) * start refactoring client transform visitor code * more * more * more * more * more * more * more * more * more * more * more * more * more * more * more * tweak * painful * more * simplify * more * more * more * more * more * tidy up * changeset --- .changeset/twelve-scissors-kneel.md | 5 + .../3-transform/client/transform-client.js | 199 +- .../phases/3-transform/client/types.d.ts | 3 +- .../phases/3-transform/client/utils.js | 31 - .../client/visitors/AnimateDirective.js | 28 + .../visitors/ArrowFunctionExpression.js | 11 + .../client/visitors/AssignmentExpression.js | 11 + .../3-transform/client/visitors/Attribute.js | 14 + .../3-transform/client/visitors/AwaitBlock.js | 67 + .../client/visitors/BinaryExpression.js | 34 + .../client/visitors/BindDirective.js | 250 ++ .../client/visitors/BreakStatement.js | 20 + .../client/visitors/CallExpression.js | 46 + .../3-transform/client/visitors/ClassBody.js | 223 ++ .../3-transform/client/visitors/Comment.js | 11 + .../3-transform/client/visitors/Component.js | 32 + .../3-transform/client/visitors/ConstTag.js | 73 + .../3-transform/client/visitors/DebugTag.js | 22 + .../3-transform/client/visitors/EachBlock.js | 317 ++ .../client/visitors/ExportNamedDeclaration.js | 19 + .../client/visitors/ExpressionStatement.js | 23 + .../3-transform/client/visitors/Fragment.js | 236 ++ .../client/visitors/FunctionDeclaration.js | 31 + .../client/visitors/FunctionExpression.js | 11 + .../3-transform/client/visitors/HtmlTag.js | 27 + .../3-transform/client/visitors/Identifier.js | 41 + .../3-transform/client/visitors/IfBlock.js | 55 + .../client/visitors/ImportDeclaration.js | 15 + .../3-transform/client/visitors/KeyBlock.js | 19 + .../client/visitors/LabeledStatement.js | 64 + .../client/visitors/LetDirective.js | 50 + .../client/visitors/MemberExpression.js | 28 + .../client/visitors/OnDirective.js | 11 + .../client/visitors/RegularElement.js | 719 ++++ .../3-transform/client/visitors/RenderTag.js | 47 + .../client/visitors/SlotElement.js | 66 + .../client/visitors/SnippetBlock.js | 86 + .../client/visitors/SpreadAttribute.js | 10 + .../3-transform/client/visitors/SvelteBody.js | 14 + .../client/visitors/SvelteComponent.js | 12 + .../client/visitors/SvelteDocument.js | 14 + .../client/visitors/SvelteElement.js | 235 ++ .../client/visitors/SvelteFragment.js | 17 + .../3-transform/client/visitors/SvelteHead.js | 20 + .../3-transform/client/visitors/SvelteSelf.js | 12 + .../client/visitors/SvelteWindow.js | 14 + .../client/visitors/TitleElement.js | 39 + .../client/visitors/TransitionDirective.js | 29 + .../client/visitors/UpdateExpression.js | 115 + .../client/visitors/UseDirective.js | 34 + .../client/visitors/VariableDeclaration.js | 328 ++ .../3-transform/client/visitors/global.js | 163 - .../client/visitors/javascript-legacy.js | 188 - .../client/visitors/javascript-runes.js | 513 --- .../3-transform/client/visitors/javascript.js | 30 - .../client/visitors/shared/component.js | 366 ++ .../client/visitors/shared/element.js | 279 ++ .../client/visitors/shared/fragment.js | 159 + .../client/visitors/shared/function.js | 34 + .../client/visitors/shared/utils.js | 335 ++ .../3-transform/client/visitors/template.js | 3418 ----------------- .../src/compiler/phases/3-transform/index.js | 2 +- .../3-transform/server/transform-server.js | 74 +- .../3-transform/server/visitors/ClassBody.js | 7 +- .../server/visitors/ExpressionStatement.js | 21 +- .../server/visitors/LabeledStatement.js | 7 +- .../server/visitors/MemberExpression.js | 8 +- .../server/visitors/PropertyDefinition.js | 4 +- .../server/visitors/VariableDeclaration.js | 213 +- .../src/compiler/phases/3-transform/utils.js | 5 +- packages/svelte/src/compiler/phases/scope.js | 21 +- 71 files changed, 5050 insertions(+), 4635 deletions(-) create mode 100644 .changeset/twelve-scissors-kneel.md create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/ArrowFunctionExpression.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/Attribute.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/BreakStatement.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/Comment.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/DebugTag.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/ExportNamedDeclaration.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionExpression.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/ImportDeclaration.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/SpreadAttribute.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBody.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteComponent.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteDocument.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteFragment.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteHead.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteSelf.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteWindow.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/TransitionDirective.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/UseDirective.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js delete mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js delete mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-legacy.js delete mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js delete mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/function.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js delete mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js diff --git a/.changeset/twelve-scissors-kneel.md b/.changeset/twelve-scissors-kneel.md new file mode 100644 index 0000000000..0932c3b81b --- /dev/null +++ b/.changeset/twelve-scissors-kneel.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: internal refactoring of client transform visitors 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 c6ac88b0de..d37e34dc65 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 @@ -5,51 +5,122 @@ import { walk } from 'zimmerframe'; import * as b from '../../../utils/builders.js'; import { set_scope } from '../../scope.js'; -import { template_visitors } from './visitors/template.js'; -import { global_visitors } from './visitors/global.js'; -import { javascript_visitors } from './visitors/javascript.js'; -import { javascript_visitors_runes } from './visitors/javascript-runes.js'; -import { javascript_visitors_legacy } from './visitors/javascript-legacy.js'; import { serialize_get_binding } from './utils.js'; import { render_stylesheet } from '../css/index.js'; import { dev, filename } from '../../../state.js'; +import { AnimateDirective } from './visitors/AnimateDirective.js'; +import { ArrowFunctionExpression } from './visitors/ArrowFunctionExpression.js'; +import { AssignmentExpression } from './visitors/AssignmentExpression.js'; +import { Attribute } from './visitors/Attribute.js'; +import { AwaitBlock } from './visitors/AwaitBlock.js'; +import { BinaryExpression } from './visitors/BinaryExpression.js'; +import { BindDirective } from './visitors/BindDirective.js'; +import { BreakStatement } from './visitors/BreakStatement.js'; +import { CallExpression } from './visitors/CallExpression.js'; +import { ClassBody } from './visitors/ClassBody.js'; +import { Comment } from './visitors/Comment.js'; +import { Component } from './visitors/Component.js'; +import { ConstTag } from './visitors/ConstTag.js'; +import { DebugTag } from './visitors/DebugTag.js'; +import { EachBlock } from './visitors/EachBlock.js'; +import { ExportNamedDeclaration } from './visitors/ExportNamedDeclaration.js'; +import { ExpressionStatement } from './visitors/ExpressionStatement.js'; +import { Fragment } from './visitors/Fragment.js'; +import { FunctionDeclaration } from './visitors/FunctionDeclaration.js'; +import { FunctionExpression } from './visitors/FunctionExpression.js'; +import { HtmlTag } from './visitors/HtmlTag.js'; +import { Identifier } from './visitors/Identifier.js'; +import { IfBlock } from './visitors/IfBlock.js'; +import { ImportDeclaration } from './visitors/ImportDeclaration.js'; +import { KeyBlock } from './visitors/KeyBlock.js'; +import { LabeledStatement } from './visitors/LabeledStatement.js'; +import { LetDirective } from './visitors/LetDirective.js'; +import { MemberExpression } from './visitors/MemberExpression.js'; +import { OnDirective } from './visitors/OnDirective.js'; +import { RegularElement } from './visitors/RegularElement.js'; +import { RenderTag } from './visitors/RenderTag.js'; +import { SlotElement } from './visitors/SlotElement.js'; +import { SnippetBlock } from './visitors/SnippetBlock.js'; +import { SpreadAttribute } from './visitors/SpreadAttribute.js'; +import { SvelteBody } from './visitors/SvelteBody.js'; +import { SvelteComponent } from './visitors/SvelteComponent.js'; +import { SvelteDocument } from './visitors/SvelteDocument.js'; +import { SvelteElement } from './visitors/SvelteElement.js'; +import { SvelteFragment } from './visitors/SvelteFragment.js'; +import { SvelteHead } from './visitors/SvelteHead.js'; +import { SvelteSelf } from './visitors/SvelteSelf.js'; +import { SvelteWindow } from './visitors/SvelteWindow.js'; +import { TitleElement } from './visitors/TitleElement.js'; +import { TransitionDirective } from './visitors/TransitionDirective.js'; +import { UpdateExpression } from './visitors/UpdateExpression.js'; +import { UseDirective } from './visitors/UseDirective.js'; +import { VariableDeclaration } from './visitors/VariableDeclaration.js'; + +/** @type {Visitors} */ +const visitors = { + _: set_scope, + AnimateDirective, + ArrowFunctionExpression, + AssignmentExpression, + Attribute, + AwaitBlock, + BinaryExpression, + BindDirective, + BreakStatement, + CallExpression, + ClassBody, + Comment, + Component, + ConstTag, + DebugTag, + EachBlock, + ExportNamedDeclaration, + ExpressionStatement, + Fragment, + FunctionDeclaration, + FunctionExpression, + HtmlTag, + Identifier, + IfBlock, + ImportDeclaration, + KeyBlock, + LabeledStatement, + LetDirective, + MemberExpression, + OnDirective, + RegularElement, + RenderTag, + SlotElement, + SnippetBlock, + SpreadAttribute, + SvelteBody, + SvelteComponent, + SvelteDocument, + SvelteElement, + SvelteFragment, + SvelteHead, + SvelteSelf, + SvelteWindow, + TitleElement, + TransitionDirective, + UpdateExpression, + UseDirective, + VariableDeclaration +}; /** - * This function ensures visitor sets don't accidentally clobber each other - * @param {...Visitors} array - * @returns {Visitors} - */ -function combine_visitors(...array) { - /** @type {Record} */ - const visitors = {}; - - for (const member of array) { - for (const key in member) { - if (visitors[key]) { - throw new Error(`Duplicate visitor: ${key}`); - } - - // @ts-ignore - visitors[key] = member[key]; - } - } - - return visitors; -} - -/** - * @param {string} source * @param {ComponentAnalysis} analysis * @param {ValidatedCompileOptions} options * @returns {ESTree.Program} */ -export function client_component(source, analysis, options) { +export function client_component(analysis, options) { /** @type {ComponentClientTransformState} */ const state = { analysis, options, scope: analysis.module.scope, - scopes: analysis.template.scopes, + scopes: analysis.module.scopes, + is_instance: false, hoisted: [b.import_all('$', 'svelte/internal/client')], node: /** @type {any} */ (null), // populated by the root node legacy_reactive_statements: new Map(), @@ -78,57 +149,25 @@ export function client_component(source, analysis, options) { }; const module = /** @type {ESTree.Program} */ ( - walk( - /** @type {SvelteNode} */ (analysis.module.ast), - state, - combine_visitors( - set_scope(analysis.module.scopes), - global_visitors, - // @ts-expect-error TODO - javascript_visitors, - analysis.runes ? javascript_visitors_runes : javascript_visitors_legacy - ) - ) + walk(/** @type {SvelteNode} */ (analysis.module.ast), state, visitors) ); - const instance_state = { ...state, scope: analysis.instance.scope }; + const instance_state = { + ...state, + scope: analysis.instance.scope, + scopes: analysis.instance.scopes, + is_instance: true + }; + const instance = /** @type {ESTree.Program} */ ( - walk( - /** @type {SvelteNode} */ (analysis.instance.ast), - instance_state, - combine_visitors( - set_scope(analysis.instance.scopes), - global_visitors, - // @ts-expect-error TODO - javascript_visitors, - analysis.runes ? javascript_visitors_runes : javascript_visitors_legacy, - { - ImportDeclaration(node) { - state.hoisted.push(node); - return b.empty; - }, - ExportNamedDeclaration(node, context) { - if (node.declaration) { - return context.visit(node.declaration); - } - - return b.empty; - } - } - ) - ) + walk(/** @type {SvelteNode} */ (analysis.instance.ast), instance_state, visitors) ); const template = /** @type {ESTree.Program} */ ( walk( /** @type {SvelteNode} */ (analysis.template.ast), - { ...state, scope: analysis.instance.scope }, - combine_visitors( - set_scope(analysis.template.scopes), - global_visitors, - // @ts-expect-error TODO - template_visitors - ) + { ...state, scope: analysis.instance.scope, scopes: analysis.template.scopes }, + visitors ) ); @@ -589,17 +628,7 @@ export function client_module(analysis, options) { }; const module = /** @type {ESTree.Program} */ ( - walk( - /** @type {SvelteNode} */ (analysis.module.ast), - state, - combine_visitors( - set_scope(analysis.module.scopes), - global_visitors, - // @ts-expect-error - javascript_visitors, - javascript_visitors_runes - ) - ) + walk(/** @type {SvelteNode} */ (analysis.module.ast), state, visitors) ); return { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 15cc09cf8b..b850e1b35c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -35,6 +35,7 @@ export interface ComponentClientTransformState extends ClientTransformState { readonly options: ValidatedCompileOptions; readonly hoisted: Array; readonly events: Set; + readonly is_instance: boolean; /** Stuff that happens before the render effect(s) */ readonly before_init: Statement[]; @@ -78,7 +79,7 @@ export interface StateField { } export type Context = import('zimmerframe').Context; -export type Visitors = import('zimmerframe').Visitors; +export type Visitors = import('zimmerframe').Visitors; export type ComponentContext = import('zimmerframe').Context< SvelteNode, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index 704ccbe6d8..60d5ebf802 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -476,37 +476,6 @@ export function serialize_proxy_reassignment(value, proxy_reference) { : b.call('$.proxy', value); } -/** - * @param {ArrowFunctionExpression | FunctionExpression} node - * @param {ComponentContext} context - */ -export const function_visitor = (node, context) => { - const metadata = node.metadata; - - let state = context.state; - - if (node.type === 'FunctionExpression') { - const parent = /** @type {Node} */ (context.path.at(-1)); - const in_constructor = parent.type === 'MethodDefinition' && parent.kind === 'constructor'; - - state = { ...context.state, in_constructor }; - } else { - state = { ...context.state, in_constructor: false }; - } - - if (metadata?.hoistable === true) { - const params = serialize_hoistable_params(node, context); - - return /** @type {FunctionExpression} */ ({ - ...node, - params, - body: context.visit(node.body, state) - }); - } - - context.next(state); -}; - /** * @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node * @param {ComponentContext} context diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js new file mode 100644 index 0000000000..a70adbf334 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AnimateDirective.js @@ -0,0 +1,28 @@ +/** @import { Expression } from 'estree' */ +/** @import { AnimateDirective } from '#compiler' */ +/** @import { ComponentContext } from '../types' */ +import * as b from '../../../../utils/builders.js'; +import { parse_directive_name } from './shared/utils.js'; + +/** + * @param {AnimateDirective} node + * @param {ComponentContext} context + */ +export function AnimateDirective(node, context) { + const expression = + node.expression === null + ? b.literal(null) + : b.thunk(/** @type {Expression} */ (context.visit(node.expression))); + + // in after_update to ensure it always happens after bind:this + context.state.after_update.push( + b.stmt( + b.call( + '$.animation', + context.state.node, + b.thunk(/** @type {Expression} */ (context.visit(parse_directive_name(node.name)))), + expression + ) + ) + ); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ArrowFunctionExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ArrowFunctionExpression.js new file mode 100644 index 0000000000..d06f2cdf67 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ArrowFunctionExpression.js @@ -0,0 +1,11 @@ +/** @import { ArrowFunctionExpression } from 'estree' */ +/** @import { ComponentContext } from '../types' */ +import { visit_function } from './shared/function.js'; + +/** + * @param {ArrowFunctionExpression} node + * @param {ComponentContext} context + */ +export function ArrowFunctionExpression(node, context) { + return visit_function(node, context); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js new file mode 100644 index 0000000000..6b52647587 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -0,0 +1,11 @@ +/** @import { AssignmentExpression } from 'estree' */ +/** @import { Context } from '../types' */ +import { serialize_set_binding } from '../utils.js'; + +/** + * @param {AssignmentExpression} node + * @param {Context} context + */ +export function AssignmentExpression(node, context) { + return serialize_set_binding(node, context, context.next); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Attribute.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Attribute.js new file mode 100644 index 0000000000..732382a5d6 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Attribute.js @@ -0,0 +1,14 @@ +/** @import { Attribute } from '#compiler' */ +/** @import { ComponentContext } from '../types' */ +import { is_event_attribute } from '../../../../utils/ast.js'; +import { serialize_event_attribute } from './shared/element.js'; + +/** + * @param {Attribute} node + * @param {ComponentContext} context + */ +export function Attribute(node, context) { + if (is_event_attribute(node)) { + serialize_event_attribute(node, context); + } +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js new file mode 100644 index 0000000000..21f16f3449 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -0,0 +1,67 @@ +/** @import { BlockStatement, Expression, Pattern } from 'estree' */ +/** @import { AwaitBlock } from '#compiler' */ +/** @import { ComponentContext } from '../types' */ +import * as b from '../../../../utils/builders.js'; +import { create_derived_block_argument } from '../utils.js'; + +/** + * @param {AwaitBlock} node + * @param {ComponentContext} context + */ +export function AwaitBlock(node, context) { + context.state.template.push(''); + + let then_block; + let catch_block; + + if (node.then) { + /** @type {Pattern[]} */ + const args = [b.id('$$anchor')]; + const block = /** @type {BlockStatement} */ (context.visit(node.then)); + + if (node.value) { + const argument = create_derived_block_argument(node.value, context); + + args.push(argument.id); + + if (argument.declarations !== null) { + block.body.unshift(...argument.declarations); + } + } + + then_block = b.arrow(args, block); + } + + if (node.catch) { + /** @type {Pattern[]} */ + const args = [b.id('$$anchor')]; + const block = /** @type {BlockStatement} */ (context.visit(node.catch)); + + if (node.error) { + const argument = create_derived_block_argument(node.error, context); + + args.push(argument.id); + + if (argument.declarations !== null) { + block.body.unshift(...argument.declarations); + } + } + + catch_block = b.arrow(args, block); + } + + context.state.init.push( + b.stmt( + b.call( + '$.await', + context.state.node, + b.thunk(/** @type {Expression} */ (context.visit(node.expression))), + node.pending + ? b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.pending))) + : b.literal(null), + then_block, + catch_block + ) + ) + ); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js new file mode 100644 index 0000000000..c8c54a5a59 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BinaryExpression.js @@ -0,0 +1,34 @@ +/** @import { Expression, BinaryExpression } from 'estree' */ +/** @import { ComponentContext } from '../types' */ +import { dev } from '../../../../state.js'; +import * as b from '../../../../utils/builders.js'; + +/** + * @param {BinaryExpression} node + * @param {ComponentContext} context + */ +export function BinaryExpression(node, context) { + if (dev) { + const operator = node.operator; + + if (operator === '===' || operator === '!==') { + return b.call( + '$.strict_equals', + /** @type {Expression} */ (context.visit(node.left)), + /** @type {Expression} */ (context.visit(node.right)), + operator === '!==' && b.literal(false) + ); + } + + if (operator === '==' || operator === '!=') { + return b.call( + '$.equals', + /** @type {Expression} */ (context.visit(node.left)), + /** @type {Expression} */ (context.visit(node.right)), + operator === '!=' && b.literal(false) + ); + } + } + + context.next(); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js new file mode 100644 index 0000000000..5211cbe392 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js @@ -0,0 +1,250 @@ +/** @import { CallExpression, Expression, MemberExpression } from 'estree' */ +/** @import { Attribute, BindDirective } from '#compiler' */ +/** @import { ComponentContext } from '../types' */ +import { dev, is_ignored } from '../../../../state.js'; +import { is_text_attribute } from '../../../../utils/ast.js'; +import * as b from '../../../../utils/builders.js'; +import { binding_properties } from '../../../bindings.js'; +import { serialize_set_binding } from '../utils.js'; +import { serialize_attribute_value } from './shared/element.js'; +import { serialize_bind_this, serialize_validate_binding } from './shared/utils.js'; + +/** + * @param {BindDirective} node + * @param {ComponentContext} context + */ +export function BindDirective(node, context) { + const expression = node.expression; + const property = binding_properties[node.name]; + + const parent = /** @type {import('#compiler').SvelteNode} */ (context.path.at(-1)); + + if ( + dev && + context.state.analysis.runes && + expression.type === 'MemberExpression' && + (node.name !== 'this' || + context.path.some( + ({ type }) => + type === 'IfBlock' || type === 'EachBlock' || type === 'AwaitBlock' || type === 'KeyBlock' + )) && + !is_ignored(node, 'binding_property_non_reactive') + ) { + context.state.init.push( + serialize_validate_binding( + context.state, + node, + /**@type {MemberExpression} */ (context.visit(expression)) + ) + ); + } + + const getter = b.thunk(/** @type {Expression} */ (context.visit(expression))); + const assignment = b.assignment('=', expression, b.id('$$value')); + const setter = b.arrow( + [b.id('$$value')], + serialize_set_binding( + assignment, + context, + () => /** @type {Expression} */ (context.visit(assignment)), + null, + { + skip_proxy_and_freeze: true + } + ) + ); + + /** @type {CallExpression} */ + let call; + + if (property?.event) { + call = b.call( + '$.bind_property', + b.literal(node.name), + b.literal(property.event), + context.state.node, + setter, + property.bidirectional && getter + ); + } else { + // special cases + switch (node.name) { + // window + case 'online': + call = b.call(`$.bind_online`, setter); + break; + + case 'scrollX': + case 'scrollY': + call = b.call( + '$.bind_window_scroll', + b.literal(node.name === 'scrollX' ? 'x' : 'y'), + getter, + setter + ); + break; + + case 'innerWidth': + case 'innerHeight': + case 'outerWidth': + case 'outerHeight': + call = b.call('$.bind_window_size', b.literal(node.name), setter); + break; + + // document + case 'activeElement': + call = b.call('$.bind_active_element', setter); + break; + + // media + case 'muted': + call = b.call(`$.bind_muted`, context.state.node, getter, setter); + break; + case 'paused': + call = b.call(`$.bind_paused`, context.state.node, getter, setter); + break; + case 'volume': + call = b.call(`$.bind_volume`, context.state.node, getter, setter); + break; + case 'playbackRate': + call = b.call(`$.bind_playback_rate`, context.state.node, getter, setter); + break; + case 'currentTime': + call = b.call(`$.bind_current_time`, context.state.node, getter, setter); + break; + case 'buffered': + call = b.call(`$.bind_buffered`, context.state.node, setter); + break; + case 'played': + call = b.call(`$.bind_played`, context.state.node, setter); + break; + case 'seekable': + call = b.call(`$.bind_seekable`, context.state.node, setter); + break; + case 'seeking': + call = b.call(`$.bind_seeking`, context.state.node, setter); + break; + case 'ended': + call = b.call(`$.bind_ended`, context.state.node, setter); + break; + case 'readyState': + call = b.call(`$.bind_ready_state`, context.state.node, setter); + break; + + // dimensions + case 'contentRect': + case 'contentBoxSize': + case 'borderBoxSize': + case 'devicePixelContentBoxSize': + call = b.call('$.bind_resize_observer', context.state.node, b.literal(node.name), setter); + break; + + case 'clientWidth': + case 'clientHeight': + case 'offsetWidth': + case 'offsetHeight': + call = b.call('$.bind_element_size', context.state.node, b.literal(node.name), setter); + break; + + // various + case 'value': { + if (parent?.type === 'RegularElement' && parent.name === 'select') { + call = b.call(`$.bind_select_value`, context.state.node, getter, setter); + } else { + call = b.call(`$.bind_value`, context.state.node, getter, setter); + } + break; + } + + case 'files': + call = b.call(`$.bind_files`, context.state.node, getter, setter); + break; + + case 'this': + call = serialize_bind_this(expression, context.state.node, context); + break; + + case 'textContent': + case 'innerHTML': + case 'innerText': + call = b.call( + '$.bind_content_editable', + b.literal(node.name), + context.state.node, + getter, + setter + ); + break; + + // checkbox/radio + case 'checked': + call = b.call(`$.bind_checked`, context.state.node, getter, setter); + break; + + case 'focused': + call = b.call(`$.bind_focused`, context.state.node, setter); + break; + + case 'group': { + const indexes = node.metadata.parent_each_blocks.map((each) => { + // if we have a keyed block with an index, the index is wrapped in a source + return each.metadata.keyed && each.index + ? b.call('$.get', each.metadata.index) + : each.metadata.index; + }); + + // We need to additionally invoke the value attribute signal to register it as a dependency, + // so that when the value is updated, the group binding is updated + let group_getter = getter; + + if (parent?.type === 'RegularElement') { + const value = /** @type {any[]} */ ( + /** @type {Attribute} */ ( + parent.attributes.find( + (a) => + a.type === 'Attribute' && + a.name === 'value' && + !is_text_attribute(a) && + a.value !== true + ) + )?.value + ); + if (value !== undefined) { + group_getter = b.thunk( + b.block([ + b.stmt(serialize_attribute_value(value, context)[1]), + b.return(/** @type {Expression} */ (context.visit(expression))) + ]) + ); + } + } + + call = b.call( + '$.bind_group', + node.metadata.binding_group_name, + b.array(indexes), + context.state.node, + group_getter, + setter + ); + break; + } + + default: + throw new Error('unknown binding ' + node.name); + } + } + + // Bindings need to happen after attribute updates, therefore after the render effect, and in order with events/actions. + // bind:this is a special case as it's one-way and could influence the render effect. + if (node.name === 'this') { + context.state.init.push(b.stmt(call)); + } else { + const has_action_directive = + parent.type === 'RegularElement' && parent.attributes.find((a) => a.type === 'UseDirective'); + + context.state.after_update.push( + b.stmt(has_action_directive ? b.call('$.effect', b.thunk(call)) : call) + ); + } +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BreakStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BreakStatement.js new file mode 100644 index 0000000000..66b66c64f2 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BreakStatement.js @@ -0,0 +1,20 @@ +/** @import { BreakStatement } from 'estree' */ +/** @import { ComponentContext } from '../types' */ +import * as b from '../../../../utils/builders.js'; + +/** + * @param {BreakStatement} node + * @param {ComponentContext} context + */ +export function BreakStatement(node, context) { + if (context.state.analysis.runes || !node.label || node.label.name !== '$') { + return; + } + + const in_reactive_statement = + context.path[1].type === 'LabeledStatement' && context.path[1].label.name === '$'; + + if (in_reactive_statement) { + return b.return(); + } +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js new file mode 100644 index 0000000000..d58dfe73a7 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -0,0 +1,46 @@ +/** @import { CallExpression, Expression } from 'estree' */ +/** @import { Context } from '../types' */ +import { is_ignored } from '../../../../state.js'; +import * as b from '../../../../utils/builders.js'; +import { get_rune } from '../../../scope.js'; +import { transform_inspect_rune } from '../../utils.js'; + +/** + * @param {CallExpression} node + * @param {Context} context + */ +export function CallExpression(node, context) { + switch (get_rune(node, context.state.scope)) { + case '$host': + return b.id('$$props.$$host'); + + case '$effect.tracking': + return b.call('$.effect_tracking'); + + case '$state.snapshot': + return b.call( + '$.snapshot', + /** @type {Expression} */ (context.visit(node.arguments[0])), + is_ignored(node, 'state_snapshot_uncloneable') && b.true + ); + + case '$state.is': + return b.call( + '$.is', + /** @type {Expression} */ (context.visit(node.arguments[0])), + /** @type {Expression} */ (context.visit(node.arguments[1])) + ); + + case '$effect.root': + return b.call( + '$.effect_root', + .../** @type {Expression[]} */ (node.arguments.map((arg) => context.visit(arg))) + ); + + case '$inspect': + case '$inspect().with': + return transform_inspect_rune(node, context); + } + + context.next(); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js new file mode 100644 index 0000000000..8d65f275ed --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ClassBody.js @@ -0,0 +1,223 @@ +/** @import { ClassBody, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition } from 'estree' */ +/** @import { } from '#compiler' */ +/** @import { Context, StateField } from '../types' */ +import { dev, is_ignored } from '../../../../state.js'; +import * as b from '../../../../utils/builders.js'; +import { regex_invalid_identifier_chars } from '../../../patterns.js'; +import { get_rune } from '../../../scope.js'; +import { serialize_proxy_reassignment, should_proxy_or_freeze } from '../utils.js'; + +/** + * @param {ClassBody} node + * @param {Context} context + */ +export function ClassBody(node, context) { + if (!context.state.analysis.runes) { + context.next(); + return; + } + + /** @type {Map} */ + const public_state = new Map(); + + /** @type {Map} */ + const private_state = new Map(); + + /** @type {string[]} */ + const private_ids = []; + + for (const definition of node.body) { + if ( + definition.type === 'PropertyDefinition' && + (definition.key.type === 'Identifier' || + definition.key.type === 'PrivateIdentifier' || + definition.key.type === 'Literal') + ) { + const type = definition.key.type; + const name = get_name(definition.key); + if (!name) continue; + + const is_private = type === 'PrivateIdentifier'; + if (is_private) private_ids.push(name); + + if (definition.value?.type === 'CallExpression') { + const rune = get_rune(definition.value, context.state.scope); + if ( + rune === '$state' || + rune === '$state.frozen' || + rune === '$derived' || + rune === '$derived.by' + ) { + /** @type {StateField} */ + const field = { + kind: + rune === '$state' + ? 'state' + : rune === '$state.frozen' + ? 'frozen_state' + : rune === '$derived.by' + ? 'derived_call' + : 'derived', + // @ts-expect-error this is set in the next pass + id: is_private ? definition.key : null + }; + + if (is_private) { + private_state.set(name, field); + } else { + public_state.set(name, field); + } + } + } + } + } + + // each `foo = $state()` needs a backing `#foo` field + for (const [name, field] of public_state) { + let deconflicted = name; + while (private_ids.includes(deconflicted)) { + deconflicted = '_' + deconflicted; + } + + private_ids.push(deconflicted); + field.id = b.private_id(deconflicted); + } + + /** @type {Array} */ + const body = []; + + const child_state = { ...context.state, public_state, private_state }; + + // Replace parts of the class body + for (const definition of node.body) { + if ( + definition.type === 'PropertyDefinition' && + (definition.key.type === 'Identifier' || + definition.key.type === 'PrivateIdentifier' || + definition.key.type === 'Literal') + ) { + const name = get_name(definition.key); + if (!name) continue; + + const is_private = definition.key.type === 'PrivateIdentifier'; + const field = (is_private ? private_state : public_state).get(name); + + if (definition.value?.type === 'CallExpression' && field !== undefined) { + let value = null; + + if (definition.value.arguments.length > 0) { + const init = /** @type {Expression} **/ ( + context.visit(definition.value.arguments[0], child_state) + ); + + value = + field.kind === 'state' + ? b.call( + '$.source', + should_proxy_or_freeze(init, context.state.scope) ? b.call('$.proxy', init) : init + ) + : field.kind === 'frozen_state' + ? b.call( + '$.source', + should_proxy_or_freeze(init, context.state.scope) + ? b.call('$.freeze', init) + : init + ) + : field.kind === 'derived_call' + ? b.call('$.derived', init) + : b.call('$.derived', b.thunk(init)); + } else { + // if no arguments, we know it's state as `$derived()` is a compile error + value = b.call('$.source'); + } + + if (is_private) { + body.push(b.prop_def(field.id, value)); + } else { + // #foo; + const member = b.member(b.this, field.id); + body.push(b.prop_def(field.id, value)); + + // get foo() { return this.#foo; } + body.push(b.method('get', definition.key, [], [b.return(b.call('$.get', member))])); + + if (field.kind === 'state') { + // set foo(value) { this.#foo = value; } + const value = b.id('value'); + body.push( + b.method( + 'set', + definition.key, + [value], + [b.stmt(b.call('$.set', member, serialize_proxy_reassignment(value, field.id)))] + ) + ); + } + + if (field.kind === 'frozen_state') { + // set foo(value) { this.#foo = value; } + const value = b.id('value'); + body.push( + b.method( + 'set', + definition.key, + [value], + [b.stmt(b.call('$.set', member, b.call('$.freeze', value)))] + ) + ); + } + + if (dev && (field.kind === 'derived' || field.kind === 'derived_call')) { + body.push( + b.method( + 'set', + definition.key, + [b.id('_')], + [b.throw_error(`Cannot update a derived property ('${name}')`)] + ) + ); + } + } + continue; + } + } + + body.push(/** @type {MethodDefinition} **/ (context.visit(definition, child_state))); + } + + if (dev && public_state.size > 0) { + // add an `[$.ADD_OWNER]` method so that a class with state fields can widen ownership + body.push( + b.method( + 'method', + b.id('$.ADD_OWNER'), + [b.id('owner')], + Array.from(public_state.keys()).map((name) => + b.stmt( + b.call( + '$.add_owner', + b.call('$.get', b.member(b.this, b.private_id(name))), + b.id('owner'), + b.literal(false), + is_ignored(node, 'ownership_invalid_binding') && b.true + ) + ) + ), + true + ) + ); + } + + return { ...node, body }; +} + +/** + * @param {Identifier | PrivateIdentifier | Literal} node + */ +function get_name(node) { + if (node.type === 'Literal') { + return node.value?.toString().replace(regex_invalid_identifier_chars, '_'); + } else { + return node.name; + } +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Comment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Comment.js new file mode 100644 index 0000000000..b8d3c45903 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Comment.js @@ -0,0 +1,11 @@ +/** @import { Comment } from '#compiler' */ +/** @import { ComponentContext } from '../types' */ + +/** + * @param {Comment} node + * @param {ComponentContext} context + */ +export function Comment(node, context) { + // We'll only get here if comments are not filtered out, which they are unless preserveComments is true + context.state.template.push(``); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js new file mode 100644 index 0000000000..b212fac0bc --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js @@ -0,0 +1,32 @@ +/** @import { Expression } from 'estree' */ +/** @import { Component } from '#compiler' */ +/** @import { ComponentContext } from '../types' */ +import * as b from '../../../../utils/builders.js'; +import { serialize_component } from './shared/component.js'; + +/** + * @param {Component} node + * @param {ComponentContext} context + */ +export function Component(node, context) { + if (node.metadata.dynamic) { + // Handle dynamic references to what seems like static inline components + const component = serialize_component(node, '$$component', context, b.id('$$anchor')); + context.state.init.push( + b.stmt( + b.call( + '$.component', + context.state.node, + // TODO use untrack here to not update when binding changes? + // Would align with Svelte 4 behavior, but it's arguably nicer/expected to update this + b.thunk(/** @type {Expression} */ (context.visit(b.member_id(node.name)))), + b.arrow([b.id('$$anchor'), b.id('$$component')], b.block([component])) + ) + ) + ); + return; + } + + const component = serialize_component(node, node.name, context); + context.state.init.push(component); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js new file mode 100644 index 0000000000..10623ce3f5 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js @@ -0,0 +1,73 @@ +/** @import { Expression, Pattern } from 'estree' */ +/** @import { ConstTag } from '#compiler' */ +/** @import { ComponentContext } from '../types' */ +import { dev } from '../../../../state.js'; +import { extract_identifiers } from '../../../../utils/ast.js'; +import * as b from '../../../../utils/builders.js'; +import { create_derived } from '../utils.js'; + +/** + * @param {ConstTag} node + * @param {ComponentContext} context + */ +export function ConstTag(node, context) { + const declaration = node.declaration.declarations[0]; + // TODO we can almost certainly share some code with $derived(...) + if (declaration.id.type === 'Identifier') { + context.state.init.push( + b.const( + declaration.id, + create_derived( + context.state, + b.thunk(/** @type {Expression} */ (context.visit(declaration.init))) + ) + ) + ); + + context.state.getters[declaration.id.name] = b.call('$.get', declaration.id); + + // we need to eagerly evaluate the expression in order to hit any + // 'Cannot access x before initialization' errors + if (dev) { + context.state.init.push(b.stmt(b.call('$.get', declaration.id))); + } + } else { + const identifiers = extract_identifiers(declaration.id); + const tmp = b.id(context.state.scope.generate('computed_const')); + + const getters = { ...context.state.getters }; + + // Make all identifiers that are declared within the following computed regular + // variables, as they are not signals in that context yet + for (const node of identifiers) { + getters[node.name] = node; + } + + const child_state = { ...context.state, getters }; + + // TODO optimise the simple `{ x } = y` case — we can just return `y` + // instead of destructuring it only to return a new object + const fn = b.arrow( + [], + b.block([ + b.const( + /** @type {Pattern} */ (context.visit(declaration.id, child_state)), + /** @type {Expression} */ (context.visit(declaration.init, child_state)) + ), + b.return(b.object(identifiers.map((node) => b.prop('init', node, node)))) + ]) + ); + + context.state.init.push(b.const(tmp, create_derived(context.state, fn))); + + // we need to eagerly evaluate the expression in order to hit any + // 'Cannot access x before initialization' errors + if (dev) { + context.state.init.push(b.stmt(b.call('$.get', tmp))); + } + + for (const node of identifiers) { + context.state.getters[node.name] = b.member(b.call('$.get', tmp), node); + } + } +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/DebugTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/DebugTag.js new file mode 100644 index 0000000000..7c6fd0d458 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/DebugTag.js @@ -0,0 +1,22 @@ +/** @import { Expression} from 'estree' */ +/** @import { DebugTag } from '#compiler' */ +/** @import { ComponentContext } from '../types' */ +import * as b from '../../../../utils/builders.js'; + +/** + * @param {DebugTag} node + * @param {ComponentContext} context + */ +export function DebugTag(node, context) { + const object = b.object( + node.identifiers.map((identifier) => + b.prop('init', identifier, /** @type {Expression} */ (context.visit(identifier))) + ) + ); + + const call = b.call('console.log', object); + + context.state.init.push( + b.stmt(b.call('$.template_effect', b.thunk(b.block([b.stmt(call), b.debugger])))) + ); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js new file mode 100644 index 0000000000..8329c822bc --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -0,0 +1,317 @@ +/** @import { BlockStatement, Expression, Identifier, MemberExpression, Pattern, Statement } from 'estree' */ +/** @import { Binding, EachBlock } from '#compiler' */ +/** @import { ComponentContext } from '../types' */ +import { + EACH_INDEX_REACTIVE, + EACH_IS_ANIMATED, + EACH_IS_CONTROLLED, + EACH_IS_STRICT_EQUALS, + EACH_ITEM_REACTIVE, + EACH_KEYED +} from '../../../../../constants.js'; +import { dev } from '../../../../state.js'; +import { extract_paths, object } from '../../../../utils/ast.js'; +import * as b from '../../../../utils/builders.js'; +import { + get_assignment_value, + serialize_get_binding, + serialize_set_binding, + with_loc +} from '../utils.js'; + +/** + * @param {EachBlock} node + * @param {ComponentContext} context + */ +export function EachBlock(node, context) { + const each_node_meta = node.metadata; + const collection = /** @type {Expression} */ (context.visit(node.expression)); + + if (!each_node_meta.is_controlled) { + context.state.template.push(''); + } + + if (each_node_meta.array_name !== null) { + context.state.init.push(b.const(each_node_meta.array_name, b.thunk(collection))); + } + + let flags = 0; + + if (node.metadata.keyed) { + flags |= EACH_KEYED; + + if (node.index) { + flags |= EACH_INDEX_REACTIVE; + } + + // In runes mode, if key === item, we don't need to wrap the item in a source + const key_is_item = + /** @type {Expression} */ (node.key).type === 'Identifier' && + node.context.type === 'Identifier' && + node.context.name === node.key.name; + + if (!context.state.analysis.runes || !key_is_item) { + flags |= EACH_ITEM_REACTIVE; + } + } else { + flags |= EACH_ITEM_REACTIVE; + } + + // Since `animate:` can only appear on elements that are the sole child of a keyed each block, + // we can determine at compile time whether the each block is animated or not (in which + // case it should measure animated elements before and after reconciliation). + if ( + node.key && + node.body.nodes.some((child) => { + if (child.type !== 'RegularElement' && child.type !== 'SvelteElement') return false; + return child.attributes.some((attr) => attr.type === 'AnimateDirective'); + }) + ) { + flags |= EACH_IS_ANIMATED; + } + + if (each_node_meta.is_controlled) { + flags |= EACH_IS_CONTROLLED; + } + + if (context.state.analysis.runes) { + flags |= EACH_IS_STRICT_EQUALS; + } + + // If the array is a store expression, we need to invalidate it when the array is changed. + // This doesn't catch all cases, but all the ones that Svelte 4 catches, too. + let store_to_invalidate = ''; + if (node.expression.type === 'Identifier' || node.expression.type === 'MemberExpression') { + const id = object(node.expression); + if (id) { + const binding = context.state.scope.get(id.name); + if (binding?.kind === 'store_sub') { + store_to_invalidate = id.name; + } + } + } + + // Legacy mode: find the parent each blocks which contain the arrays to invalidate + const indirect_dependencies = collect_parent_each_blocks(context).flatMap((block) => { + const array = /** @type {Expression} */ (context.visit(block.expression)); + const transitive_dependencies = serialize_transitive_dependencies( + block.metadata.references, + context + ); + return [array, ...transitive_dependencies]; + }); + + if (each_node_meta.array_name) { + indirect_dependencies.push(b.call(each_node_meta.array_name)); + } else { + indirect_dependencies.push(collection); + + const transitive_dependencies = serialize_transitive_dependencies( + each_node_meta.references, + context + ); + indirect_dependencies.push(...transitive_dependencies); + } + + const child_state = { + ...context.state, + getters: { ...context.state.getters } + }; + + /** The state used when generating the key function, if necessary */ + const key_state = { + ...context.state, + getters: { ...context.state.getters } + }; + + /** + * @param {Pattern} expression_for_id + * @returns {Binding['mutation']} + */ + const create_mutation = (expression_for_id) => { + return (assignment, context) => { + if (assignment.left.type !== 'Identifier' && assignment.left.type !== 'MemberExpression') { + // serialize_set_binding turns other patterns into IIFEs and separates the assignments + // into separate expressions, at which point this is called again with an identifier or member expression + return serialize_set_binding(assignment, context, () => assignment); + } + + const left = object(assignment.left); + const value = get_assignment_value(assignment, context); + const invalidate = b.call( + '$.invalidate_inner_signals', + b.thunk(b.sequence(indirect_dependencies)) + ); + const invalidate_store = store_to_invalidate + ? b.call('$.invalidate_store', b.id('$$stores'), b.literal(store_to_invalidate)) + : undefined; + + const sequence = []; + if (!context.state.analysis.runes) sequence.push(invalidate); + if (invalidate_store) sequence.push(invalidate_store); + + if (left === assignment.left) { + const assign = b.assignment('=', expression_for_id, value); + sequence.unshift(assign); + return b.sequence(sequence); + } else { + const original_left = /** @type {MemberExpression} */ (assignment.left); + const left = context.visit(original_left); + const assign = b.assignment(assignment.operator, left, value); + sequence.unshift(assign); + return b.sequence(sequence); + } + }; + }; + + // We need to generate a unique identifier in case there's a bind:group below + // which needs a reference to the index + const index = + each_node_meta.contains_group_binding || !node.index ? each_node_meta.index : b.id(node.index); + const item = each_node_meta.item; + const binding = /** @type {Binding} */ (context.state.scope.get(item.name)); + const getter = (/** @type {Identifier} */ id) => { + const item_with_loc = with_loc(item, id); + return b.call('$.unwrap', item_with_loc); + }; + child_state.getters[item.name] = getter; + + if (node.index) { + child_state.getters[node.index] = (id) => { + const index_with_loc = with_loc(index, id); + return (flags & EACH_INDEX_REACTIVE) === 0 ? index_with_loc : b.call('$.get', index_with_loc); + }; + + key_state.getters[node.index] = b.id(node.index); + } + + /** @type {Statement[]} */ + const declarations = []; + + if (node.context.type === 'Identifier') { + binding.mutation = create_mutation( + b.member( + each_node_meta.array_name ? b.call(each_node_meta.array_name) : collection, + index, + true + ) + ); + + key_state.getters[node.context.name] = node.context; + } else { + const unwrapped = getter(binding.node); + const paths = extract_paths(node.context); + + for (const path of paths) { + const name = /** @type {Identifier} */ (path.node).name; + const binding = /** @type {Binding} */ (context.state.scope.get(name)); + const needs_derived = path.has_default_value; // to ensure that default value is only called once + const fn = b.thunk( + /** @type {Expression} */ (context.visit(path.expression?.(unwrapped), child_state)) + ); + + declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn)); + + const getter = needs_derived ? b.call('$.get', b.id(name)) : b.call(name); + child_state.getters[name] = getter; + binding.mutation = create_mutation( + /** @type {Pattern} */ (path.update_expression(unwrapped)) + ); + + // we need to eagerly evaluate the expression in order to hit any + // 'Cannot access x before initialization' errors + if (dev) { + declarations.push(b.stmt(getter)); + } + + key_state.getters[name] = path.node; + } + } + + const block = /** @type {BlockStatement} */ (context.visit(node.body, child_state)); + + /** @type {Expression} */ + let key_function = b.id('$.index'); + + if (node.metadata.keyed) { + const expression = /** @type {Expression} */ ( + context.visit(/** @type {Expression} */ (node.key), key_state) + ); + + key_function = b.arrow([node.context, index], expression); + } + + if (node.index && each_node_meta.contains_group_binding) { + // We needed to create a unique identifier for the index above, but we want to use the + // original index name in the template, therefore create another binding + declarations.push(b.let(node.index, index)); + } + + if (dev && (flags & EACH_KEYED) !== 0) { + context.state.init.push( + b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function)) + ); + } + + /** @type {Expression[]} */ + const args = [ + context.state.node, + b.literal(flags), + each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection), + key_function, + b.arrow([b.id('$$anchor'), item, index], b.block(declarations.concat(block.body))) + ]; + + if (node.fallback) { + args.push( + b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.fallback))) + ); + } + + context.state.init.push(b.stmt(b.call('$.each', ...args))); +} + +/** + * @param {ComponentContext} context + */ +function collect_parent_each_blocks(context) { + return /** @type {EachBlock[]} */ (context.path.filter((node) => node.type === 'EachBlock')); +} + +/** + * @param {Binding[]} references + * @param {ComponentContext} context + */ +function serialize_transitive_dependencies(references, context) { + /** @type {Set} */ + const dependencies = new Set(); + + for (const ref of references) { + const deps = collect_transitive_dependencies(ref); + for (const dep of deps) { + dependencies.add(dep); + } + } + + return [...dependencies].map((dep) => serialize_get_binding({ ...dep.node }, context.state)); +} + +/** + * @param {Binding} binding + * @param {Set} seen + * @returns {Binding[]} + */ +function collect_transitive_dependencies(binding, seen = new Set()) { + if (binding.kind !== 'legacy_reactive') return []; + + for (const dep of binding.legacy_dependencies) { + if (!seen.has(dep)) { + seen.add(dep); + for (const transitive_dep of collect_transitive_dependencies(dep, seen)) { + seen.add(transitive_dep); + } + } + } + + return [...seen]; +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExportNamedDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExportNamedDeclaration.js new file mode 100644 index 0000000000..cab7f90c3d --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExportNamedDeclaration.js @@ -0,0 +1,19 @@ +/** @import { ExportNamedDeclaration } from 'estree' */ +/** @import { ComponentContext } from '../types' */ +import * as b from '../../../../utils/builders.js'; + +/** + * @param {ExportNamedDeclaration} node + * @param {ComponentContext} context + */ +export function ExportNamedDeclaration(node, context) { + if (context.state.is_instance) { + if (node.declaration) { + return context.visit(node.declaration); + } + + return b.empty; + } + + return context.next(); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js new file mode 100644 index 0000000000..2aa416ec39 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ExpressionStatement.js @@ -0,0 +1,23 @@ +/** @import { Expression, ExpressionStatement } from 'estree' */ +/** @import { ComponentContext } from '../types' */ +import * as b from '../../../../utils/builders.js'; +import { get_rune } from '../../../scope.js'; + +/** + * @param {ExpressionStatement} node + * @param {ComponentContext} context + */ +export function ExpressionStatement(node, context) { + if (node.expression.type === 'CallExpression') { + const rune = get_rune(node.expression, context.state.scope); + + if (rune === '$effect' || rune === '$effect.pre') { + const callee = rune === '$effect' ? '$.user_effect' : '$.user_pre_effect'; + const func = /** @type {Expression} */ (context.visit(node.expression.arguments[0])); + + return b.stmt(b.call(callee, /** @type {Expression} */ (func))); + } + } + + context.next(); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js new file mode 100644 index 0000000000..368f33fd11 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -0,0 +1,236 @@ +/** @import { Expression, Identifier, Statement } from 'estree' */ +/** @import { Fragment, Namespace, RegularElement } from '#compiler' */ +/** @import { SourceLocation } from '#shared' */ +/** @import { ComponentClientTransformState, ComponentContext } from '../types' */ +import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../../../constants.js'; +import { dev } from '../../../../state.js'; +import * as b from '../../../../utils/builders.js'; +import { clean_nodes, infer_namespace } from '../../utils.js'; +import { process_children } from './shared/fragment.js'; +import { serialize_render_stmt } from './shared/utils.js'; + +/** + * @param {Fragment} node + * @param {ComponentContext} context + */ +export function Fragment(node, context) { + // Creates a new block which looks roughly like this: + // ```js + // // hoisted: + // const block_name = $.template(`...`); + // + // // for the main block: + // const id = block_name(); + // // init stuff and possibly render effect + // $.append($$anchor, id); + // ``` + // Adds the hoisted parts to `context.state.hoisted` and returns the statements of the main block. + + const parent = context.path.at(-1) ?? node; + + const namespace = infer_namespace(context.state.metadata.namespace, parent, node.nodes); + + const { hoisted, trimmed, is_standalone, is_text_first } = clean_nodes( + parent, + node.nodes, + context.path, + namespace, + context.state, + context.state.preserve_whitespace, + context.state.options.preserveComments + ); + + if (hoisted.length === 0 && trimmed.length === 0) { + return b.block([]); + } + + const is_single_element = trimmed.length === 1 && trimmed[0].type === 'RegularElement'; + const is_single_child_not_needing_template = + trimmed.length === 1 && + (trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement'); + + const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent + + /** @type {Statement[]} */ + const body = []; + + /** @type {Statement | undefined} */ + let close = undefined; + + /** @type {ComponentClientTransformState} */ + const state = { + ...context.state, + before_init: [], + init: [], + update: [], + after_update: [], + template: [], + locations: [], + getters: { ...context.state.getters }, + metadata: { + context: { + template_needs_import_node: false, + template_contains_script_tag: false + }, + namespace, + bound_contenteditable: context.state.metadata.bound_contenteditable + } + }; + + for (const node of hoisted) { + context.visit(node, state); + } + + if (is_text_first) { + // skip over inserted comment + body.push(b.stmt(b.call('$.next'))); + } + + /** + * @param {Identifier} template_name + * @param {Expression[]} args + */ + const add_template = (template_name, args) => { + let call = b.call(get_template_function(namespace, state), ...args); + if (dev) { + call = b.call( + '$.add_locations', + call, + b.member(b.id(context.state.analysis.name), b.id('$.FILENAME'), true), + serialize_locations(state.locations) + ); + } + + context.state.hoisted.push(b.var(template_name, call)); + }; + + if (is_single_element) { + const element = /** @type {RegularElement} */ (trimmed[0]); + + const id = b.id(context.state.scope.generate(element.name)); + + context.visit(element, { + ...state, + node: id + }); + + /** @type {Expression[]} */ + const args = [b.template([b.quasi(state.template.join(''), true)], [])]; + + if (state.metadata.context.template_needs_import_node) { + args.push(b.literal(TEMPLATE_USE_IMPORT_NODE)); + } + + add_template(template_name, args); + + body.push(b.var(id, b.call(template_name)), ...state.before_init, ...state.init); + close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); + } else if (is_single_child_not_needing_template) { + context.visit(trimmed[0], state); + body.push(...state.before_init, ...state.init); + } else if (trimmed.length > 0) { + const id = b.id(context.state.scope.generate('fragment')); + + const use_space_template = + trimmed.some((node) => node.type === 'ExpressionTag') && + trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag'); + + if (use_space_template) { + // special case — we can use `$.text` instead of creating a unique template + const id = b.id(context.state.scope.generate('text')); + + process_children(trimmed, () => id, false, { + ...context, + state + }); + + body.push(b.var(id, b.call('$.text')), ...state.before_init, ...state.init); + close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); + } else { + if (is_standalone) { + // no need to create a template, we can just use the existing block's anchor + process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state }); + } else { + /** @type {(is_text: boolean) => Expression} */ + const expression = (is_text) => b.call('$.first_child', id, is_text && b.true); + + process_children(trimmed, expression, false, { ...context, state }); + + let flags = TEMPLATE_FRAGMENT; + + if (state.metadata.context.template_needs_import_node) { + flags |= TEMPLATE_USE_IMPORT_NODE; + } + + if (state.template.length === 1 && state.template[0] === '') { + // special case — we can use `$.comment` instead of creating a unique template + body.push(b.var(id, b.call('$.comment'))); + } else { + add_template(template_name, [ + b.template([b.quasi(state.template.join(''), true)], []), + b.literal(flags) + ]); + + body.push(b.var(id, b.call(template_name))); + } + + close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); + } + + body.push(...state.before_init, ...state.init); + } + } else { + body.push(...state.before_init, ...state.init); + } + + if (state.update.length > 0) { + body.push(serialize_render_stmt(state.update)); + } + + body.push(...state.after_update); + + if (close !== undefined) { + // It's important that close is the last statement in the block, as any previous statements + // could contain element insertions into the template, which the close statement needs to + // know of when constructing the list of current inner elements. + body.push(close); + } + + return b.block(body); +} + +/** + * + * @param {Namespace} namespace + * @param {ComponentClientTransformState} state + * @returns + */ +function get_template_function(namespace, state) { + const contains_script_tag = state.metadata.context.template_contains_script_tag; + return namespace === 'svg' + ? contains_script_tag + ? '$.svg_template_with_script' + : '$.ns_template' + : namespace === 'mathml' + ? '$.mathml_template' + : contains_script_tag + ? '$.template_with_script' + : '$.template'; +} + +/** + * @param {SourceLocation[]} locations + */ +function serialize_locations(locations) { + return b.array( + locations.map((loc) => { + const expression = b.array([b.literal(loc[0]), b.literal(loc[1])]); + + if (loc.length === 3) { + expression.elements.push(serialize_locations(loc[2])); + } + + return expression; + }) + ); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js new file mode 100644 index 0000000000..8f2eca91b1 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js @@ -0,0 +1,31 @@ +/** @import { FunctionDeclaration } from 'estree' */ +/** @import { ComponentContext } from '../types' */ +import { serialize_hoistable_params } from '../utils.js'; +import * as b from '../../../../utils/builders.js'; + +/** + * @param {FunctionDeclaration} node + * @param {ComponentContext} context + */ +export function FunctionDeclaration(node, context) { + const metadata = node.metadata; + + const state = { ...context.state, in_constructor: false }; + + if (metadata?.hoistable === true) { + const params = serialize_hoistable_params(node, context); + + context.state.hoisted.push( + /** @type {FunctionDeclaration} */ ({ + ...node, + id: node.id !== null ? context.visit(node.id, state) : null, + params, + body: context.visit(node.body, state) + }) + ); + + return b.empty; + } + + context.next(state); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionExpression.js new file mode 100644 index 0000000000..884dfc8a33 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionExpression.js @@ -0,0 +1,11 @@ +/** @import { FunctionExpression } from 'estree' */ +/** @import { ComponentContext } from '../types' */ +import { visit_function } from './shared/function.js'; + +/** + * @param {FunctionExpression} node + * @param {ComponentContext} context + */ +export function FunctionExpression(node, context) { + return visit_function(node, context); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js new file mode 100644 index 0000000000..e9725f29e3 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js @@ -0,0 +1,27 @@ +/** @import { Expression } from 'estree' */ +/** @import { HtmlTag } from '#compiler' */ +/** @import { ComponentContext } from '../types' */ +import { is_ignored } from '../../../../state.js'; +import * as b from '../../../../utils/builders.js'; + +/** + * @param {HtmlTag} node + * @param {ComponentContext} context + */ +export function HtmlTag(node, context) { + context.state.template.push(''); + + // push into init, so that bindings run afterwards, which might trigger another run and override hydration + context.state.init.push( + b.stmt( + b.call( + '$.html', + context.state.node, + b.thunk(/** @type {Expression} */ (context.visit(node.expression))), + b.literal(context.state.metadata.namespace === 'svg'), + b.literal(context.state.metadata.namespace === 'mathml'), + is_ignored(node, 'hydration_html_changed') && b.true + ) + ) + ); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js new file mode 100644 index 0000000000..7acdf179b6 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Identifier.js @@ -0,0 +1,41 @@ +/** @import { Identifier, Node } from 'estree' */ +/** @import { Context } from '../types' */ +import is_reference from 'is-reference'; +import * as b from '../../../../utils/builders.js'; +import { serialize_get_binding } from '../utils.js'; + +/** + * @param {Identifier} node + * @param {Context} context + */ +export function Identifier(node, context) { + const parent = /** @type {Node} */ (context.path.at(-1)); + + if (is_reference(node, parent)) { + if (node.name === '$$props') { + return b.id('$$sanitized_props'); + } + + // Optimize prop access: If it's a member read access, we can use the $$props object directly + const binding = context.state.scope.get(node.name); + if ( + context.state.analysis.runes && // can't do this in legacy mode because the proxy does more than just read/write + binding !== null && + node !== binding.node && + binding.kind === 'rest_prop' + ) { + const grand_parent = context.path.at(-2); + + if ( + parent?.type === 'MemberExpression' && + !parent.computed && + grand_parent?.type !== 'AssignmentExpression' && + grand_parent?.type !== 'UpdateExpression' + ) { + return b.id('$$props'); + } + } + + return serialize_get_binding(node, context.state); + } +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js new file mode 100644 index 0000000000..888f672be3 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -0,0 +1,55 @@ +/** @import { BlockStatement, Expression } from 'estree' */ +/** @import { IfBlock } from '#compiler' */ +/** @import { ComponentContext } from '../types' */ +import * as b from '../../../../utils/builders.js'; + +/** + * @param {IfBlock} node + * @param {ComponentContext} context + */ +export function IfBlock(node, context) { + context.state.template.push(''); + + const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent)); + + const args = [ + context.state.node, + b.thunk(/** @type {Expression} */ (context.visit(node.test))), + b.arrow([b.id('$$anchor')], consequent) + ]; + + if (node.alternate || node.elseif) { + args.push( + node.alternate + ? b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.alternate))) + : b.literal(null) + ); + } + + if (node.elseif) { + // We treat this... + // + // {#if x} + // ... + // {:else} + // {#if y} + //
...
+ // {/if} + // {/if} + // + // ...slightly differently to this... + // + // {#if x} + // ... + // {:else if y} + //
...
+ // {/if} + // + // ...even though they're logically equivalent. In the first case, the + // transition will only play when `y` changes, but in the second it + // should play when `x` or `y` change — both are considered 'local' + args.push(b.literal(true)); + } + + context.state.init.push(b.stmt(b.call('$.if', ...args))); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ImportDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ImportDeclaration.js new file mode 100644 index 0000000000..a222353687 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ImportDeclaration.js @@ -0,0 +1,15 @@ +/** @import { ImportDeclaration } from 'estree' */ +/** @import { ComponentContext } from '../types' */ +import * as b from '../../../../utils/builders.js'; + +/** + * @param {ImportDeclaration} node + * @param {ComponentContext} context + */ +export function ImportDeclaration(node, context) { + if ('hoisted' in context.state) { + context.state.hoisted.push(node); + } + + return b.empty; +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js new file mode 100644 index 0000000000..106a030515 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js @@ -0,0 +1,19 @@ +/** @import { Expression } from 'estree' */ +/** @import { KeyBlock } from '#compiler' */ +/** @import { ComponentContext } from '../types' */ +import * as b from '../../../../utils/builders.js'; + +/** + * @param {KeyBlock} node + * @param {ComponentContext} context + */ +export function KeyBlock(node, context) { + context.state.template.push(''); + + const key = /** @type {Expression} */ (context.visit(node.expression)); + const body = /** @type {Expression} */ (context.visit(node.fragment)); + + context.state.init.push( + b.stmt(b.call('$.key', context.state.node, b.thunk(key), b.arrow([b.id('$$anchor')], body))) + ); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js new file mode 100644 index 0000000000..24a6ed832c --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js @@ -0,0 +1,64 @@ +/** @import { Expression, LabeledStatement, Statement } from 'estree' */ +/** @import { ReactiveStatement } from '#compiler' */ +/** @import { ComponentContext } from '../types' */ +import * as b from '../../../../utils/builders.js'; +import { serialize_get_binding } from '../utils.js'; + +/** + * @param {LabeledStatement} node + * @param {ComponentContext} context + */ +export function LabeledStatement(node, context) { + if (context.state.analysis.runes || context.path.length > 1 || node.label.name !== '$') { + context.next(); + return; + } + + // To recreate Svelte 4 behaviour, we track the dependencies + // the compiler can 'see', but we untrack the effect itself + const reactive_statement = /** @type {ReactiveStatement} */ ( + context.state.analysis.reactive_statements.get(node) + ); + + if (!reactive_statement) return; // not the instance context + + let serialized_body = /** @type {Statement} */ (context.visit(node.body)); + + if (serialized_body.type !== 'BlockStatement') { + serialized_body = b.block([serialized_body]); + } + + const body = serialized_body.body; + + /** @type {Expression[]} */ + const sequence = []; + + for (const binding of reactive_statement.dependencies) { + if (binding.kind === 'normal') continue; + + const name = binding.node.name; + let serialized = serialize_get_binding(b.id(name), context.state); + + // If the binding is a prop, we need to deep read it because it could be fine-grained $state + // from a runes-component, where mutations don't trigger an update on the prop as a whole. + if (name === '$$props' || name === '$$restProps' || binding.kind === 'bindable_prop') { + serialized = b.call('$.deep_read_state', serialized); + } + + sequence.push(serialized); + } + + // these statements will be topologically ordered later + context.state.legacy_reactive_statements.set( + node, + b.stmt( + b.call( + '$.legacy_pre_effect', + sequence.length > 0 ? b.thunk(b.sequence(sequence)) : b.thunk(b.block([])), + b.thunk(b.block(body)) + ) + ) + ); + + return b.empty; +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js new file mode 100644 index 0000000000..2423c6e50e --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js @@ -0,0 +1,50 @@ +/** @import { Expression } from 'estree' */ +/** @import { LetDirective } from '#compiler' */ +/** @import { ComponentContext } from '../types' */ +import * as b from '../../../../utils/builders.js'; +import { create_derived } from '../utils.js'; + +/** + * @param {LetDirective} node + * @param {ComponentContext} context + */ +export function LetDirective(node, context) { + // let:x --> const x = $.derived(() => $$slotProps.x); + // let:x={{y, z}} --> const derived_x = $.derived(() => { const { y, z } = $$slotProps.x; return { y, z })); + if (node.expression && node.expression.type !== 'Identifier') { + const name = context.state.scope.generate(node.name); + const bindings = context.state.scope.get_bindings(node); + + for (const binding of bindings) { + context.state.getters[binding.node.name] = b.member( + b.call('$.get', b.id(name)), + b.id(binding.node.name) + ); + } + + return b.const( + name, + b.call( + '$.derived', + b.thunk( + b.block([ + b.let( + /** @type {Expression} */ (node.expression).type === 'ObjectExpression' + ? // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine + b.object_pattern(node.expression.properties) + : // @ts-expect-error types don't match, but it can't contain spread elements and the structure is otherwise fine + b.array_pattern(node.expression.elements), + b.member(b.id('$$slotProps'), b.id(node.name)) + ), + b.return(b.object(bindings.map((binding) => b.init(binding.node.name, binding.node)))) + ]) + ) + ) + ); + } else { + return b.const( + node.expression === null ? node.name : node.expression.name, + create_derived(context.state, b.thunk(b.member(b.id('$$slotProps'), b.id(node.name)))) + ); + } +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js new file mode 100644 index 0000000000..bfa5b53e6f --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/MemberExpression.js @@ -0,0 +1,28 @@ +/** @import { MemberExpression } from 'estree' */ +/** @import { Context } from '../types' */ +import * as b from '../../../../utils/builders.js'; + +/** + * @param {MemberExpression} node + * @param {Context} context + */ +export function MemberExpression(node, context) { + // rewrite `this.#foo` as `this.#foo.v` inside a constructor + if (node.property.type === 'PrivateIdentifier') { + const field = context.state.private_state.get(node.property.name); + if (field) { + return context.state.in_constructor ? b.member(node, b.id('v')) : b.call('$.get', node); + } + } else if (node.object.type === 'ThisExpression') { + // rewrite `this.foo` as `this.#foo.v` inside a constructor + if (node.property.type === 'Identifier' && !node.computed) { + const field = context.state.public_state.get(node.property.name); + + if (field && context.state.in_constructor) { + return b.member(b.member(b.this, field.id), b.id('v')); + } + } + } + + context.next(); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js new file mode 100644 index 0000000000..171b19844e --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js @@ -0,0 +1,11 @@ +/** @import { OnDirective } from '#compiler' */ +/** @import { ComponentContext } from '../types' */ +import { serialize_event } from './shared/element.js'; + +/** + * @param {OnDirective} node + * @param {ComponentContext} context + */ +export function OnDirective(node, context) { + serialize_event(node, node.metadata.expression, context); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js new file mode 100644 index 0000000000..ffbde5977f --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -0,0 +1,719 @@ +/** @import { Expression, ExpressionStatement, Identifier, Literal, MemberExpression, ObjectExpression, Statement } from 'estree' */ +/** @import { Attribute, BindDirective, ClassDirective, RegularElement, SpreadAttribute, StyleDirective } from '#compiler' */ +/** @import { SourceLocation } from '#shared' */ +/** @import { ComponentClientTransformState, ComponentContext } from '../types' */ +/** @import { Scope } from '../../../scope' */ +import { DOMBooleanAttributes } from '../../../../../constants.js'; +import { escape_html } from '../../../../../escaping.js'; +import { dev, is_ignored, locator } from '../../../../state.js'; +import { + get_attribute_expression, + is_event_attribute, + is_text_attribute +} from '../../../../utils/ast.js'; +import * as b from '../../../../utils/builders.js'; +import { DOMProperties, LoadErrorElements, VoidElements } from '../../../constants.js'; +import { is_custom_element_node } from '../../../nodes.js'; +import { clean_nodes, determine_namespace_for_children } from '../../utils.js'; +import { serialize_get_binding } from '../utils.js'; +import { + get_attribute_name, + serialize_attribute_value, + serialize_class_directives, + serialize_event_attribute, + serialize_style_directives +} from './shared/element.js'; +import { process_children } from './shared/fragment.js'; +import { + serialize_render_stmt, + serialize_update, + serialize_update_assignment +} from './shared/utils.js'; + +/** + * @param {RegularElement} node + * @param {ComponentContext} context + */ +export function RegularElement(node, context) { + /** @type {SourceLocation} */ + let location = [-1, -1]; + + if (dev) { + const loc = locator(node.start); + if (loc) { + location[0] = loc.line; + location[1] = loc.column; + context.state.locations.push(location); + } + } + + if (node.name === 'noscript') { + context.state.template.push(''); + return; + } + + if (node.name === 'script') { + context.state.metadata.context.template_contains_script_tag = true; + } + + const metadata = context.state.metadata; + const child_metadata = { + ...context.state.metadata, + namespace: determine_namespace_for_children(node, context.state.metadata.namespace) + }; + + context.state.template.push(`<${node.name}`); + + /** @type {Array} */ + const attributes = []; + + /** @type {ClassDirective[]} */ + const class_directives = []; + + /** @type {StyleDirective[]} */ + const style_directives = []; + + /** @type {ExpressionStatement[]} */ + const lets = []; + + const is_custom_element = is_custom_element_node(node); + let needs_input_reset = false; + let needs_content_reset = false; + + /** @type {BindDirective | null} */ + let value_binding = null; + + /** If true, needs `__value` for inputs */ + let needs_special_value_handling = node.name === 'option' || node.name === 'select'; + let is_content_editable = false; + let has_content_editable_binding = false; + let img_might_be_lazy = false; + let might_need_event_replaying = false; + let has_direction_attribute = false; + let has_style_attribute = false; + + if (is_custom_element) { + // cloneNode is faster, but it does not instantiate the underlying class of the + // custom element until the template is connected to the dom, which would + // cause problems when setting properties on the custom element. + // Therefore we need to use importNode instead, which doesn't have this caveat. + metadata.context.template_needs_import_node = true; + } + + for (const attribute of node.attributes) { + if (attribute.type === 'Attribute') { + attributes.push(attribute); + if (node.name === 'img' && attribute.name === 'loading') { + img_might_be_lazy = true; + } + if (attribute.name === 'dir') { + has_direction_attribute = true; + } + if (attribute.name === 'style') { + has_style_attribute = true; + } + if ( + (attribute.name === 'value' || attribute.name === 'checked') && + !is_text_attribute(attribute) + ) { + needs_input_reset = true; + needs_content_reset = true; + } else if ( + attribute.name === 'contenteditable' && + (attribute.value === true || + (is_text_attribute(attribute) && attribute.value[0].data === 'true')) + ) { + is_content_editable = true; + } + } else if (attribute.type === 'SpreadAttribute') { + attributes.push(attribute); + needs_input_reset = true; + needs_content_reset = true; + if (LoadErrorElements.includes(node.name)) { + might_need_event_replaying = true; + } + } else if (attribute.type === 'ClassDirective') { + class_directives.push(attribute); + } else if (attribute.type === 'StyleDirective') { + style_directives.push(attribute); + } else if (attribute.type === 'LetDirective') { + lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); + } else { + if (attribute.type === 'BindDirective') { + if (attribute.name === 'group' || attribute.name === 'checked') { + needs_special_value_handling = true; + needs_input_reset = true; + } else if (attribute.name === 'value') { + value_binding = attribute; + needs_content_reset = true; + needs_input_reset = true; + } else if ( + attribute.name === 'innerHTML' || + attribute.name === 'innerText' || + attribute.name === 'textContent' + ) { + has_content_editable_binding = true; + } + } else if (attribute.type === 'UseDirective' && LoadErrorElements.includes(node.name)) { + might_need_event_replaying = true; + } + context.visit(attribute); + } + } + + if (child_metadata.namespace === 'foreign') { + // input/select etc could mean something completely different in foreign namespace, so don't special-case them + needs_content_reset = false; + needs_input_reset = false; + needs_special_value_handling = false; + value_binding = null; + } + + if (is_content_editable && has_content_editable_binding) { + child_metadata.bound_contenteditable = true; + } + + if (needs_input_reset && node.name === 'input') { + context.state.init.push(b.stmt(b.call('$.remove_input_defaults', context.state.node))); + } + + if (needs_content_reset && node.name === 'textarea') { + context.state.init.push(b.stmt(b.call('$.remove_textarea_child', context.state.node))); + } + + if (value_binding !== null && node.name === 'select') { + setup_select_synchronization(value_binding, context); + } + + const node_id = context.state.node; + + // Let bindings first, they can be used on attributes + context.state.init.push(...lets); + + // Then do attributes + let is_attributes_reactive = false; + if (node.metadata.has_spread) { + if (node.name === 'img') { + img_might_be_lazy = true; + } + serialize_element_spread_attributes( + attributes, + context, + node, + node_id, + // If value binding exists, that one takes care of calling $.init_select + value_binding === null && node.name === 'select' && child_metadata.namespace !== 'foreign' + ); + is_attributes_reactive = true; + } else { + for (const attribute of /** @type {Attribute[]} */ (attributes)) { + if (is_event_attribute(attribute)) { + if ( + (attribute.name === 'onload' || attribute.name === 'onerror') && + LoadErrorElements.includes(node.name) + ) { + might_need_event_replaying = true; + } + serialize_event_attribute(attribute, context); + continue; + } + + if (needs_special_value_handling && attribute.name === 'value') { + serialize_element_special_value_attribute(node.name, node_id, attribute, context); + continue; + } + + if ( + !is_custom_element && + attribute.name !== 'autofocus' && + (attribute.value === true || is_text_attribute(attribute)) + ) { + const name = get_attribute_name(node, attribute, context); + const literal_value = /** @type {Literal} */ ( + serialize_attribute_value(attribute.value, context)[1] + ).value; + if (name !== 'class' || literal_value) { + // TODO namespace=foreign probably doesn't want to do template stuff at all and instead use programmatic methods + // to create the elements it needs. + context.state.template.push( + ` ${attribute.name}${ + DOMBooleanAttributes.includes(name) && literal_value === true + ? '' + : `="${literal_value === true ? '' : escape_html(literal_value, true)}"` + }` + ); + continue; + } + } + + const is = + is_custom_element && child_metadata.namespace !== 'foreign' + ? serialize_custom_element_attribute_update_assignment(node_id, attribute, context) + : serialize_element_attribute_update_assignment(node, node_id, attribute, context); + if (is) is_attributes_reactive = true; + } + } + + // Apply the src and loading attributes for elements after the element is appended to the document + if (img_might_be_lazy) { + context.state.after_update.push(b.stmt(b.call('$.handle_lazy_img', node_id))); + } + + // class/style directives must be applied last since they could override class/style attributes + serialize_class_directives(class_directives, node_id, context, is_attributes_reactive); + serialize_style_directives( + style_directives, + node_id, + context, + is_attributes_reactive, + has_style_attribute || node.metadata.has_spread + ); + + if (might_need_event_replaying) { + context.state.after_update.push(b.stmt(b.call('$.replay_events', node_id))); + } + + context.state.template.push('>'); + + /** @type {SourceLocation[]} */ + const child_locations = []; + + /** @type {ComponentClientTransformState} */ + const state = { + ...context.state, + metadata: child_metadata, + locations: child_locations, + scope: /** @type {Scope} */ (context.state.scopes.get(node.fragment)), + preserve_whitespace: + context.state.preserve_whitespace || + ((node.name === 'pre' || node.name === 'textarea') && child_metadata.namespace !== 'foreign') + }; + + const { hoisted, trimmed } = clean_nodes( + node, + node.fragment.nodes, + context.path, + child_metadata.namespace, + state, + node.name === 'script' || state.preserve_whitespace, + state.options.preserveComments + ); + + /** Whether or not we need to wrap the children in `{...}` to avoid declaration conflicts */ + const has_declaration = node.fragment.nodes.some((node) => node.type === 'SnippetBlock'); + + const child_state = has_declaration + ? { ...state, init: [], update: [], after_update: [] } + : state; + + for (const node of hoisted) { + context.visit(node, child_state); + } + + /** @type {Expression} */ + let arg = context.state.node; + + // If `hydrate_node` is set inside the element, we need to reset it + // after the element has been hydrated + let needs_reset = trimmed.some((node) => node.type !== 'Text'); + + // The same applies if it's a `