From 17f1b6af305ce5869c1c3491f6ac066d5c4d7745 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 21 May 2025 17:00:46 -0400 Subject: [PATCH] WIP --- .../client/transform-template/index.js | 4 +- .../client/transform-template/template.js | 95 ++++++++++++++++ .../client/transform-template/to-functions.js | 84 ++++++-------- .../client/transform-template/to-string.js | 105 ++---------------- .../client/transform-template/types.d.ts | 20 ++++ .../phases/3-transform/client/types.d.ts | 3 +- .../3-transform/client/visitors/Fragment.js | 5 +- .../client/visitors/shared/component.js | 7 +- 8 files changed, 170 insertions(+), 153 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/transform-template/template.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/transform-template/types.d.ts diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js index 1f7af717fc..03b8a0ef4f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js @@ -76,8 +76,8 @@ export function transform_template(state, context, namespace, template_name, fla /** @type {Expression[]} */ const args = [ state.options.templatingMode === 'functional' - ? template_to_functions(state.template) - : b.template([b.quasi(template_to_string(state.template), true)], []) + ? template_to_functions(state.template.nodes) + : b.template([b.quasi(template_to_string(state.template.nodes), true)], []) ]; if (flags) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/template.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/template.js new file mode 100644 index 0000000000..4a2a04676e --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/template.js @@ -0,0 +1,95 @@ +/** @import { AST } from '#compiler' */ +/** @import { TemplateOperation } from '../types.js' */ +/** @import { Node, Element } from './types'; */ + +export class Template { + /** @type {Node[]} */ + nodes = []; + + /** @type {Node[][]} */ + #stack = [this.nodes]; + + /** @type {Element | undefined} */ + #element; + + #fragment = this.nodes; + + /** + * @param {...TemplateOperation} nodes + * @deprecated + */ + push(...nodes) { + for (const node of nodes) { + switch (node.kind) { + case 'create_element': + this.create_element(node.name); + break; + + case 'create_anchor': + this.create_anchor(node.data); + break; + + case 'create_text': + this.create_text(node.nodes); + break; + + case 'push_element': { + this.push_element(); + break; + } + + case 'pop_element': { + this.pop_element(); + break; + } + + case 'set_prop': { + this.set_prop(node.key, node.value); + break; + } + } + } + } + + /** @param {string} name */ + create_element(name) { + this.#element = { + type: 'element', + name, + attributes: {}, + children: [] + }; + + this.#fragment.push(this.#element); + } + + /** @param {string | undefined} data */ + create_anchor(data) { + this.#fragment.push({ type: 'anchor', data }); + } + + /** @param {AST.Text[]} nodes */ + create_text(nodes) { + this.#fragment.push({ type: 'text', nodes }); + } + + push_element() { + const element = /** @type {Element} */ (this.#element); + this.#fragment = element.children; + this.#stack.push(this.#fragment); + } + + pop_element() { + this.#stack.pop(); + this.#fragment = /** @type {Node[]} */ (this.#stack.at(-1)); + } + + /** + * @param {string} key + * @param {string | undefined} value + */ + set_prop(key, value) { + const element = /** @type {Element} */ (this.#element); + element.attributes[key] = value; + } +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js index 888f7adb7f..6bf86290e6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js @@ -1,70 +1,52 @@ -/** @import { TemplateOperation } from '../types.js' */ +/** @import { Node } from './types.js' */ /** @import { ObjectExpression, Identifier, ArrayExpression, Property, Expression, Literal } from 'estree' */ import * as b from '../../../../utils/builders.js'; import { regex_is_valid_identifier, regex_starts_with_newline } from '../../../patterns.js'; import fix_attribute_casing from './fix-attribute-casing.js'; /** - * @param {TemplateOperation[]} items + * @param {Node[]} items */ export function template_to_functions(items) { - let elements = b.array([]); + return b.array(items.map(build)); +} - /** - * @type {Array} - */ - let elements_stack = []; +/** @param {Node} item */ +function build(item) { + switch (item.type) { + case 'element': { + const element = b.object([b.prop('init', b.id('e'), b.literal(item.name))]); + + const entries = Object.entries(item.attributes); + if (entries.length > 0) { + element.properties.push( + b.prop( + 'init', + b.id('p'), + b.object( + entries.map(([key, value]) => { + return b.prop('init', b.key(key), value === undefined ? b.void0 : b.literal(value)); + }) + ) + ) + ); + } - /** - * @type {Element | undefined} - */ - let last_current_element; + if (item.children.length > 0) { + element.properties.push(b.prop('init', b.id('c'), b.array(item.children.map(build)))); + } - // if the first item is a comment we need to add another comment for effect.start - if (items[0].kind === 'create_anchor') { - items.unshift({ kind: 'create_anchor' }); - } + return element; + } - for (let instruction of items) { - const last_element_stack = /** @type {Element} */ (elements_stack.at(-1)); - /** - * @param {Expression | null | void} value - * @returns - */ - function push(value) { - if (value === undefined) return; - if (last_element_stack) { - insert(last_element_stack, value); - } else { - elements.elements.push(value); - } + case 'anchor': { + return item.data ? b.array([b.literal(item.data)]) : null; } - switch (instruction.kind) { - case 'push_element': - elements_stack.push(/** @type {Element} */ (last_current_element)); - break; - case 'pop_element': - elements_stack.pop(); - last_current_element = elements_stack.at(-1); - break; - case 'create_element': - last_current_element = create_element(instruction.name); - push(last_current_element); - break; - case 'create_text': - push(create_text(last_element_stack, instruction.nodes.map((node) => node.data).join(''))); - break; - case 'create_anchor': - push(create_anchor(last_element_stack, instruction.data)); - break; - case 'set_prop': - set_prop(/** @type {Element} */ (last_current_element), instruction.key, instruction.value); - break; + case 'text': { + return b.literal(item.nodes.map((node) => node.data).join(',')); } } - - return elements; } /** diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-string.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-string.js index a7614036cf..174f7d71e3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-string.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-string.js @@ -1,95 +1,14 @@ -/** @import { TemplateOperation } from '../types.js' */ +/** @import { Node } from './types.js' */ import { escape_html } from '../../../../../escaping.js'; import { is_void } from '../../../../../utils.js'; /** - * @param {TemplateOperation[]} items + * @param {Node[]} items */ export function template_to_string(items) { - /** - * @type {Array} - */ - let elements = []; - - /** - * @type {Array} - */ - let elements_stack = []; - - /** - * @type {Element | undefined} - */ - let last_current_element; - - /** - * @template {Node} T - * @param {T} child - */ - function insert(child) { - if (last_current_element) { - last_current_element.children ??= []; - last_current_element.children.push(child); - } else { - elements.push(/** @type {Element} */ (child)); - } - return child; - } - - for (let instruction of items) { - switch (instruction.kind) { - case 'push_element': - elements_stack.push(/** @type {Element} */ (last_current_element)); - break; - case 'pop_element': - elements_stack.pop(); - last_current_element = elements_stack.at(-1); - break; - case 'create_element': - last_current_element = insert({ - kind: 'element', - element: instruction.name - }); - break; - case 'create_text': - insert({ - kind: 'text', - value: instruction.nodes.map((node) => node.raw).join('') - }); - break; - case 'create_anchor': - insert({ - kind: 'anchor', - data: instruction.data - }); - break; - case 'set_prop': { - const el = /** @type {Element} */ (last_current_element); - el.props ??= {}; - el.props[instruction.key] = escape_html(instruction.value, true); - break; - } - } - } - - return elements.map((el) => stringify(el)).join(''); + return items.map((el) => stringify(el)).join(''); } -/** - * @typedef {{ kind: "element", element: string, props?: Record, children?: Array }} Element - */ - -/** - * @typedef {{ kind: "anchor", data?: string }} Anchor - */ - -/** - * @typedef {{ kind: "text", value?: string }} Text - */ - -/** - * @typedef { Element | Anchor| Text } Node - */ - /** * * @param {Node} el @@ -97,15 +16,15 @@ export function template_to_string(items) { */ function stringify(el) { let str = ``; - if (el.kind === 'element') { + if (el.type === 'element') { // we create the `; + if (!is_void(el.name)) { + str += ``; } - } else if (el.kind === 'text') { - str += el.value; - } else if (el.kind === 'anchor') { + } else if (el.type === 'text') { + str += el.nodes.map((node) => node.raw).join(''); + } else if (el.type === 'anchor') { if (el.data) { str += ``; } else { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/types.d.ts new file mode 100644 index 0000000000..dad8356ed7 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/types.d.ts @@ -0,0 +1,20 @@ +import type { AST } from '#compiler'; + +export interface Element { + type: 'element'; + name: string; + attributes: Record; + children: Node[]; +} + +export interface Text { + type: 'text'; + nodes: AST.Text[]; +} + +export interface Anchor { + type: 'anchor'; + data: string | undefined; +} + +export type Node = Element | Text | Anchor; 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 ba78797ef0..ecb58545ae 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 @@ -13,6 +13,7 @@ import type { AST, Namespace, StateField, ValidatedCompileOptions } from '#compi import type { TransformState } from '../types.js'; import type { ComponentAnalysis } from '../../types.js'; import type { SourceLocation } from '#shared'; +import type { Template } from './transform-template/template.js'; export interface ClientTransformState extends TransformState { /** @@ -78,7 +79,7 @@ export interface ComponentClientTransformState extends ClientTransformState { /** Expressions used inside the render effect */ readonly expressions: Expression[]; /** The HTML template string */ - readonly template: TemplateOperation[]; + readonly template: Template; readonly locations: SourceLocation[]; readonly metadata: { namespace: Namespace; 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 index d4768e087b..a897a7f55f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -7,6 +7,7 @@ import { clean_nodes, infer_namespace } from '../../utils.js'; import { transform_template } from '../transform-template/index.js'; import { process_children } from './shared/fragment.js'; import { build_render_statement } from './shared/utils.js'; +import { Template } from '../transform-template/template.js'; /** * @param {AST.Fragment} node @@ -65,7 +66,7 @@ export function Fragment(node, context) { update: [], expressions: [], after_update: [], - template: [], + template: new Template(), locations: [], transform: { ...context.state.transform }, metadata: { @@ -147,7 +148,7 @@ export function Fragment(node, context) { flags |= TEMPLATE_USE_IMPORT_NODE; } - if (state.template.length === 1 && state.template[0].kind === 'create_anchor') { + if (state.template.nodes.length === 1 && state.template.nodes[0].type === 'anchor') { // special case — we can use `$.comment` instead of creating a unique template body.push(b.var(id, b.call('$.comment'))); } else { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index da3294ea14..26e760f6d1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -1,6 +1,6 @@ /** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Pattern, Property, SequenceExpression, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ -/** @import { ComponentContext } from '../../types.js' */ +/** @import { ComponentContext, TemplateOperation } from '../../types.js' */ import { dev, is_ignored } from '../../../../../state.js'; import { get_attribute_chunks, object } from '../../../../../utils/ast.js'; import * as b from '#compiler/builders'; @@ -440,10 +440,9 @@ export function build_component(node, component_name, context, anchor = context. } if (Object.keys(custom_css_props).length > 0) { - /** - * @type {typeof context.state.template} - */ + /** @type {TemplateOperation[]} */ const template_operations = []; + if (context.state.metadata.namespace === 'svg') { // this boils down to template_operations.push({ kind: 'create_element', name: 'g' });