diff --git a/.changeset/smart-boats-accept.md b/.changeset/smart-boats-accept.md new file mode 100644 index 000000000000..b08063eb8d00 --- /dev/null +++ b/.changeset/smart-boats-accept.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: functional template generation 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 0bdfbae746d0..da0a89fb2f7e 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 @@ -167,6 +167,7 @@ export function client_component(analysis, options) { in_constructor: false, instance_level_snippets: [], module_level_snippets: [], + is_functional_template_mode: options.templatingMode === 'functional', // these are set inside the `Fragment` visitor, and cannot be used until then init: /** @type {any} */ (null), diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/fix-attribute-casing.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/fix-attribute-casing.js new file mode 100644 index 000000000000..ce56c43d7c50 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/fix-attribute-casing.js @@ -0,0 +1,18 @@ +const svg_attributes = + 'accent-height accumulate additive alignment-baseline allowReorder alphabetic amplitude arabic-form ascent attributeName attributeType autoReverse azimuth baseFrequency baseline-shift baseProfile bbox begin bias by calcMode cap-height class clip clipPathUnits clip-path clip-rule color color-interpolation color-interpolation-filters color-profile color-rendering contentScriptType contentStyleType cursor cx cy d decelerate descent diffuseConstant direction display divisor dominant-baseline dur dx dy edgeMode elevation enable-background end exponent externalResourcesRequired fill fill-opacity fill-rule filter filterRes filterUnits flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight format from fr fx fy g1 g2 glyph-name glyph-orientation-horizontal glyph-orientation-vertical glyphRef gradientTransform gradientUnits hanging height href horiz-adv-x horiz-origin-x id ideographic image-rendering in in2 intercept k k1 k2 k3 k4 kernelMatrix kernelUnitLength kerning keyPoints keySplines keyTimes lang lengthAdjust letter-spacing lighting-color limitingConeAngle local marker-end marker-mid marker-start markerHeight markerUnits markerWidth mask maskContentUnits maskUnits mathematical max media method min mode name numOctaves offset onabort onactivate onbegin onclick onend onerror onfocusin onfocusout onload onmousedown onmousemove onmouseout onmouseover onmouseup onrepeat onresize onscroll onunload opacity operator order orient orientation origin overflow overline-position overline-thickness panose-1 paint-order pathLength patternContentUnits patternTransform patternUnits pointer-events points pointsAtX pointsAtY pointsAtZ preserveAlpha preserveAspectRatio primitiveUnits r radius refX refY rendering-intent repeatCount repeatDur requiredExtensions requiredFeatures restart result rotate rx ry scale seed shape-rendering slope spacing specularConstant specularExponent speed spreadMethod startOffset stdDeviation stemh stemv stitchTiles stop-color stop-opacity strikethrough-position strikethrough-thickness string stroke stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width style surfaceScale systemLanguage tabindex tableValues target targetX targetY text-anchor text-decoration text-rendering textLength to transform type u1 u2 underline-position underline-thickness unicode unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical values version vert-adv-y vert-origin-x vert-origin-y viewBox viewTarget visibility width widths word-spacing writing-mode x x-height x1 x2 xChannelSelector xlink:actuate xlink:arcrole xlink:href xlink:role xlink:show xlink:title xlink:type xml:base xml:lang xml:space y y1 y2 yChannelSelector z zoomAndPan'.split( + ' ' + ); + +const svg_attribute_lookup = new Map(); + +svg_attributes.forEach((name) => { + svg_attribute_lookup.set(name.toLowerCase(), name); +}); + +/** + * @param {string} name + */ +export default function fix_attribute_casing(name) { + name = name.toLowerCase(); + return svg_attribute_lookup.get(name) || name; +} 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 new file mode 100644 index 000000000000..becf987be915 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js @@ -0,0 +1,88 @@ +/** + * @import { ComponentContext, TemplateOperations, ComponentClientTransformState } from "../types.js" + * @import { Identifier, Expression } from "estree" + * @import { AST, Namespace } from '#compiler' + * @import { SourceLocation } from '#shared' + */ +import { dev } from '../../../../state.js'; +import * as b from '../../../../utils/builders.js'; +import { template_to_functions } from './to-functions.js'; +import { template_to_string } from './to-string.js'; + +/** + * + * @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' + ).concat(state.is_functional_template_mode ? '_fn' : ''); +} + +/** + * @param {SourceLocation[]} locations + */ +function build_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(build_locations(loc[2])); + } + + return expression; + }) + ); +} + +/** + * @param {ComponentClientTransformState} state + * @param {ComponentContext} context + * @param {Namespace} namespace + * @param {Identifier} template_name + * @param {number} [flags] + */ +export function transform_template(state, context, namespace, template_name, flags) { + /** + * @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), '$.FILENAME', true), + build_locations(state.locations) + ); + } + + context.state.hoisted.push(b.var(template_name, call)); + }; + + /** @type {Expression[]} */ + const args = [ + state.is_functional_template_mode + ? template_to_functions(state.template) + : b.template([b.quasi(template_to_string(state.template), true)], []) + ]; + + if (flags) { + args.push(b.literal(flags)); + } + + add_template(template_name, args); +} 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 new file mode 100644 index 000000000000..ecf8151836ae --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js @@ -0,0 +1,182 @@ +/** + * @import { TemplateOperations } from "../types.js" + * @import { Namespace } from "#compiler" + * @import { CallExpression, Statement, ObjectExpression, Identifier, ArrayExpression, Property, Expression, Literal } from "estree" + */ +import { NAMESPACE_SVG, NAMESPACE_MATHML } from '../../../../../constants.js'; +import * as b from '../../../../utils/builders.js'; +import { regex_is_valid_identifier } from '../../../patterns.js'; +import fix_attribute_casing from './fix-attribute-casing.js'; + +/** + * @param {TemplateOperations} items + */ +export function template_to_functions(items) { + let elements = b.array([]); + + /** + * @type {Array} + */ + let elements_stack = []; + + /** + * @type {Element | undefined} + */ + let last_current_element; + + // 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' }); + } + + for (let instruction of items) { + // on push element we add the element to the stack, from this moment on every insert will + // happen on the last element in the stack + if (instruction.kind === 'push_element' && last_current_element) { + elements_stack.push(last_current_element); + continue; + } + // we closed one element, we remove it from the stack and eventually revert back + // the namespace to the previous one + if (instruction.kind === 'pop_element') { + elements_stack.pop(); + continue; + } + + // @ts-expect-error we can't be here if `swap_current_element` but TS doesn't know that + const value = map[instruction.kind]( + ...[ + ...(instruction.kind === 'create_element' + ? [] + : [instruction.kind === 'set_prop' ? last_current_element : elements_stack.at(-1)]), + ...(instruction.args ?? []) + ] + ); + + // with set_prop we don't need to do anything else, in all other cases we also need to + // append the element/node/anchor to the current active element or push it in the elements array + if (instruction.kind !== 'set_prop') { + if (elements_stack.length >= 1 && value !== undefined) { + map.insert(/** @type {Element} */ (elements_stack.at(-1)), value); + } else if (value !== undefined) { + elements.elements.push(value); + } + // keep track of the last created element (it will be pushed to the stack after the props are set) + if (instruction.kind === 'create_element') { + last_current_element = /** @type {Element} */ (value); + } + } + } + + return elements; +} + +/** + * @typedef {ObjectExpression} Element + */ + +/** + * @typedef {void | null | ArrayExpression} Anchor + */ + +/** + * @typedef {void | Literal} Text + */ + +/** + * @typedef { Element | Anchor| Text } Node + */ + +/** + * @param {string} element + * @returns {Element} + */ +function create_element(element) { + return b.object([b.prop('init', b.id('e'), b.literal(element))]); +} + +/** + * + * @param {Element} element + * @param {string} name + * @param {Expression} init + * @returns {Property} + */ +function get_or_create_prop(element, name, init) { + let prop = element.properties.find( + (prop) => prop.type === 'Property' && /** @type {Identifier} */ (prop.key).name === name + ); + if (!prop) { + prop = b.prop('init', b.id(name), init); + element.properties.push(prop); + } + return /** @type {Property} */ (prop); +} + +/** + * @param {Element} element + * @param {string} data + * @returns {Anchor} + */ +function create_anchor(element, data = '') { + if (!element) return data ? b.array([b.literal(data)]) : null; + const c = get_or_create_prop(element, 'c', b.array([])); + /** @type {ArrayExpression} */ (c.value).elements.push(data ? b.array([b.literal(data)]) : null); +} + +/** + * @param {Element} element + * @param {string} value + * @returns {Text} + */ +function create_text(element, value) { + if (!element) return b.literal(value); + const c = get_or_create_prop(element, 'c', b.array([])); + /** @type {ArrayExpression} */ (c.value).elements.push(b.literal(value)); +} + +/** + * + * @param {Element} element + * @param {string} prop + * @param {string} value + */ +function set_prop(element, prop, value) { + const p = get_or_create_prop(element, 'p', b.object([])); + + if (prop === 'is') { + element.properties.push(b.prop('init', b.id(prop), b.literal(value))); + return; + } + + const prop_correct_case = fix_attribute_casing(prop); + + const is_valid_id = regex_is_valid_identifier.test(prop_correct_case); + + /** @type {ObjectExpression} */ (p.value).properties.push( + b.prop( + 'init', + (is_valid_id ? b.id : b.literal)(prop_correct_case), + b.literal(value), + !is_valid_id + ) + ); +} + +/** + * + * @param {Element} element + * @param {Element} child + */ +function insert(element, child) { + const c = get_or_create_prop(element, 'c', b.array([])); + /** @type {ArrayExpression} */ (c.value).elements.push(child); +} + +let map = { + create_element, + create_text, + create_anchor, + set_prop, + insert +}; 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 new file mode 100644 index 000000000000..ed0b73dd3ee0 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-string.js @@ -0,0 +1,183 @@ +/** + * @import { TemplateOperations } from "../types.js" + */ +import { is_void } from '../../../../../utils.js'; + +/** + * @param {TemplateOperations} items + */ +export function template_to_string(items) { + let elements = []; + + /** + * @type {Array} + */ + let elements_stack = []; + + /** + * @type {Element | undefined} + */ + let last_current_element; + + for (let instruction of items) { + // on push element we add the element to the stack, from this moment on every insert will + // happen on the last element in the stack + if (instruction.kind === 'push_element' && last_current_element) { + elements_stack.push(last_current_element); + continue; + } + // we closed one element, we remove it from the stack + if (instruction.kind === 'pop_element') { + elements_stack.pop(); + continue; + } + /** + * @type {Node | void} + */ + // @ts-expect-error we can't be here if `swap_current_element` but TS doesn't know that + const value = map[instruction.kind]( + ...[ + // for set prop we need to send the last element (not the one in the stack since + // it get's added to the stack only after the push_element instruction) + ...(instruction.kind === 'set_prop' ? [last_current_element] : []), + ...(instruction.args ?? []) + ] + ); + // with set_prop we don't need to do anything else, in all other cases we also need to + // append the element/node/anchor to the current active element or push it in the elements array + if (instruction.kind !== 'set_prop') { + if (elements_stack.length >= 1 && value) { + map.insert(/** @type {Element} */ (elements_stack.at(-1)), value); + } else if (value) { + elements.push(value); + } + // keep track of the last created element (it will be pushed to the stack after the props are set) + if (instruction.kind === 'create_element') { + last_current_element = /** @type {Element} */ (value); + } + } + } + + return elements.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 {string} element + * @returns {Element} + */ +function create_element(element) { + return { + kind: 'element', + element + }; +} + +/** + * @param {string} data + * @returns {Anchor} + */ +function create_anchor(data) { + return { + kind: 'anchor', + data + }; +} + +/** + * @param {string} value + * @returns {Text} + */ +function create_text(value) { + return { + kind: 'text', + value + }; +} + +/** + * + * @param {Element} el + * @param {string} prop + * @param {string} value + */ +function set_prop(el, prop, value) { + el.props ??= {}; + el.props[prop] = value; +} + +/** + * + * @param {Element} el + * @param {Node} child + * @param {Node} [anchor] + */ +function insert(el, child, anchor) { + el.children ??= []; + el.children.push(child); +} + +let map = { + create_element, + create_text, + create_anchor, + set_prop, + insert +}; + +/** + * + * @param {Node} el + * @returns + */ +function stringify(el) { + let str = ``; + if (el.kind === 'element') { + // we create the `; + // we stringify all the children and concatenate them + for (let child of el.children ?? []) { + str += stringify(child); + } + // if it's not void we also add the closing tag + if (!is_void(el.element)) { + str += ``; + } + } else if (el.kind === 'text') { + str += el.value; + } else if (el.kind === 'anchor') { + if (el.data) { + str += ``; + } else { + str += ``; + } + } + + return str; +} 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 243e1c64a33c..35dee0a24928 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 @@ -39,6 +39,22 @@ export interface ClientTransformState extends TransformState { >; } +type TemplateOperationsKind = + | 'create_element' + | 'create_text' + | 'create_anchor' + | 'set_prop' + | 'push_element' + | 'pop_element'; + +type TemplateOperations = Array<{ + kind: TemplateOperationsKind; + args?: Array; + metadata?: { + svg: boolean; + mathml: boolean; + }; +}>; export interface ComponentClientTransformState extends ClientTransformState { readonly analysis: ComponentAnalysis; readonly options: ValidatedCompileOptions; @@ -56,7 +72,7 @@ export interface ComponentClientTransformState extends ClientTransformState { /** Expressions used inside the render effect */ readonly expressions: Expression[]; /** The HTML template string */ - readonly template: Array; + readonly template: TemplateOperations; readonly locations: SourceLocation[]; readonly metadata: { namespace: Namespace; @@ -78,6 +94,7 @@ export interface ComponentClientTransformState extends ClientTransformState { }; }; readonly preserve_whitespace: boolean; + readonly is_functional_template_mode?: boolean; /** The anchor node for the current context */ readonly node: Identifier; 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 index 7588b24280d8..404124762f08 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -11,7 +11,7 @@ import { get_value } from './shared/declarations.js'; * @param {ComponentContext} context */ export function AwaitBlock(node, context) { - context.state.template.push(''); + context.state.template.push({ kind: 'create_anchor' }); // Visit {#await } first to ensure that scopes are in the correct order const expression = b.thunk(/** @type {Expression} */ (context.visit(node.expression))); 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 index 24011e62aabd..758abc6a6752 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Comment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Comment.js @@ -7,5 +7,5 @@ */ 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(``); + context.state.template.push({ kind: 'create_anchor', args: [node.data] }); } 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 index 629cacda0148..9cdb6f1b4cf3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -32,7 +32,7 @@ export function EachBlock(node, context) { ); if (!each_node_meta.is_controlled) { - context.state.template.push(''); + context.state.template.push({ kind: 'create_anchor' }); } let flags = 0; 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 389a694741fc..b579f32e9183 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 @@ -1,12 +1,10 @@ -/** @import { Expression, Identifier, Statement, TemplateElement } from 'estree' */ -/** @import { AST, Namespace } from '#compiler' */ -/** @import { SourceLocation } from '#shared' */ +/** @import { Expression, Statement } from 'estree' */ +/** @import { AST } from '#compiler' */ /** @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 { sanitize_template_string } from '../../../../utils/sanitize_template_string.js'; 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'; @@ -38,7 +36,8 @@ export function Fragment(node, context) { namespace, context.state, context.state.preserve_whitespace, - context.state.options.preserveComments + context.state.options.preserveComments, + context.state.is_functional_template_mode ); if (hoisted.length === 0 && trimmed.length === 0) { @@ -70,6 +69,7 @@ export function Fragment(node, context) { template: [], locations: [], transform: { ...context.state.transform }, + is_functional_template_mode: context.state.is_functional_template_mode, metadata: { context: { template_needs_import_node: false, @@ -89,24 +89,6 @@ export function Fragment(node, context) { 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), '$.FILENAME', true), - build_locations(state.locations) - ); - } - - context.state.hoisted.push(b.var(template_name, call)); - }; - if (is_single_element) { const element = /** @type {AST.RegularElement} */ (trimmed[0]); @@ -117,14 +99,13 @@ export function Fragment(node, context) { node: id }); - /** @type {Expression[]} */ - const args = [join_template(state.template)]; + let flags = undefined; if (state.metadata.context.template_needs_import_node) { - args.push(b.literal(TEMPLATE_USE_IMPORT_NODE)); + flags = TEMPLATE_USE_IMPORT_NODE; } - add_template(template_name, args); + transform_template(state, context, namespace, template_name, flags); body.push(b.var(id, b.call(template_name))); close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); @@ -168,11 +149,11 @@ export function Fragment(node, context) { flags |= TEMPLATE_USE_IMPORT_NODE; } - if (state.template.length === 1 && state.template[0] === '') { + if (state.template.length === 1 && state.template[0].kind === 'create_anchor') { // 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, [join_template(state.template), b.literal(flags)]); + transform_template(state, context, namespace, template_name, flags); body.push(b.var(id, b.call(template_name))); } @@ -199,86 +180,3 @@ export function Fragment(node, context) { return b.block(body); } - -/** - * @param {Array} items - */ -function join_template(items) { - let quasi = b.quasi(''); - const template = b.template([quasi], []); - - /** - * @param {Expression} expression - */ - function push(expression) { - if (expression.type === 'TemplateLiteral') { - for (let i = 0; i < expression.expressions.length; i += 1) { - const q = expression.quasis[i]; - const e = expression.expressions[i]; - - quasi.value.cooked += /** @type {string} */ (q.value.cooked); - push(e); - } - - const last = /** @type {TemplateElement} */ (expression.quasis.at(-1)); - quasi.value.cooked += /** @type {string} */ (last.value.cooked); - } else if (expression.type === 'Literal') { - /** @type {string} */ (quasi.value.cooked) += expression.value; - } else { - template.expressions.push(expression); - template.quasis.push((quasi = b.quasi(''))); - } - } - - for (const item of items) { - if (typeof item === 'string') { - quasi.value.cooked += item; - } else { - push(item); - } - } - - for (const quasi of template.quasis) { - quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked)); - } - - quasi.tail = true; - - return template; -} - -/** - * - * @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 build_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(build_locations(loc[2])); - } - - return expression; - }) - ); -} 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 index 32439879de38..fd2256f16b5d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js @@ -9,7 +9,7 @@ import * as b from '../../../../utils/builders.js'; * @param {ComponentContext} context */ export function HtmlTag(node, context) { - context.state.template.push(''); + context.state.template.push({ kind: 'create_anchor' }); // push into init, so that bindings run afterwards, which might trigger another run and override hydration context.state.init.push( 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 index fdd21b2b7ed8..a7735c65b8da 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -8,7 +8,7 @@ import * as b from '../../../../utils/builders.js'; * @param {ComponentContext} context */ export function IfBlock(node, context) { - context.state.template.push(''); + context.state.template.push({ kind: 'create_anchor' }); const statements = []; const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent)); 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 index a013827f60bd..04ef6195e74d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js @@ -8,7 +8,7 @@ import * as b from '../../../../utils/builders.js'; * @param {ComponentContext} context */ export function KeyBlock(node, context) { - context.state.template.push(''); + context.state.template.push({ kind: 'create_anchor' }); const key = /** @type {Expression} */ (context.visit(node.expression)); const body = /** @type {Expression} */ (context.visit(node.fragment)); 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 index 45a594af1f06..983386801443 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -52,7 +52,10 @@ export function RegularElement(node, context) { } if (node.name === 'noscript') { - context.state.template.push(''); + context.state.template.push({ + kind: 'create_element', + args: ['noscript'] + }); return; } @@ -72,7 +75,14 @@ export function RegularElement(node, context) { context.state.metadata.context.template_contains_script_tag = true; } - context.state.template.push(`<${node.name}`); + context.state.template.push({ + kind: 'create_element', + args: [node.name], + metadata: { + svg: node.metadata.svg, + mathml: node.metadata.mathml + } + }); /** @type {Array} */ const attributes = []; @@ -110,7 +120,17 @@ export function RegularElement(node, context) { const { value } = build_attribute_value(attribute.value, context); if (value.type === 'Literal' && typeof value.value === 'string') { - context.state.template.push(` is="${escape_html(value.value, true)}"`); + context.state.template.push({ + kind: 'set_prop', + args: [ + 'is', + // if we are using the functional template mode we don't want to escape since we will + // create a text node from it which is already escaped + context.state.is_functional_template_mode + ? value.value + : escape_html(value.value, true) + ] + }); continue; } } @@ -286,13 +306,22 @@ export function RegularElement(node, context) { } if (name !== 'class' || value) { - context.state.template.push( - ` ${attribute.name}${ + context.state.template.push({ + kind: 'set_prop', + args: [attribute.name].concat( is_boolean_attribute(name) && value === true - ? '' - : `="${value === true ? '' : escape_html(value, true)}"` - }` - ); + ? [] + : [ + value === true + ? '' + : // if we are using the functional template mode we don't want to escape since we will + // create a text node from it which is already escaped + context.state.is_functional_template_mode + ? value + : escape_html(value, true) + ] + ) + }); } } else if (name === 'autofocus') { let { value } = build_attribute_value(attribute.value, context); @@ -324,8 +353,7 @@ export function RegularElement(node, context) { ) { context.state.after_update.push(b.stmt(b.call('$.replay_events', node_id))); } - - context.state.template.push('>'); + context.state.template.push({ kind: 'push_element' }); const metadata = { ...context.state.metadata, @@ -351,7 +379,8 @@ export function RegularElement(node, context) { locations: [], scope: /** @type {Scope} */ (context.state.scopes.get(node.fragment)), preserve_whitespace: - context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea' + context.state.preserve_whitespace || node.name === 'pre' || node.name === 'textarea', + is_functional_template_mode: context.state.is_functional_template_mode }; const { hoisted, trimmed } = clean_nodes( @@ -361,7 +390,8 @@ export function RegularElement(node, context) { state.metadata.namespace, state, node.name === 'script' || state.preserve_whitespace, - state.options.preserveComments + state.options.preserveComments, + state.is_functional_template_mode ); /** @type {typeof state} */ @@ -446,10 +476,7 @@ export function RegularElement(node, context) { // @ts-expect-error location.push(state.locations); } - - if (!is_void(node.name)) { - context.state.template.push(``); - } + context.state.template.push({ kind: 'pop_element' }); } /** diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js index 33ae6d4d2bee..a48adaf6c51a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js @@ -9,7 +9,7 @@ import * as b from '../../../../utils/builders.js'; * @param {ComponentContext} context */ export function RenderTag(node, context) { - context.state.template.push(''); + context.state.template.push({ kind: 'create_anchor' }); const expression = unwrap_optional(node.expression); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js index c6f4ba1ed383..c6e7badfa501 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SlotElement.js @@ -11,7 +11,7 @@ import { memoize_expression } from './shared/utils.js'; */ export function SlotElement(node, context) { // fallback --> $.slot($$slots.default, { get a() { .. } }, () => ...fallback); - context.state.template.push(''); + context.state.template.push({ kind: 'create_anchor' }); /** @type {Property[]} */ const props = []; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js index 9228df970375..40dde11e6e0c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBoundary.js @@ -88,7 +88,7 @@ export function SvelteBoundary(node, context) { b.call('$.boundary', context.state.node, props, b.arrow([b.id('$$anchor')], block)) ); - context.state.template.push(''); + context.state.template.push({ kind: 'create_anchor' }); context.state.init.push( external_statements.length > 0 ? b.block([...external_statements, boundary]) : boundary ); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js index 115eb6ccc11e..90a5b7ec2701 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js @@ -13,7 +13,7 @@ import { build_render_statement, get_expression_id } from './shared/utils.js'; * @param {ComponentContext} context */ export function SvelteElement(node, context) { - context.state.template.push(``); + context.state.template.push({ kind: 'create_anchor' }); /** @type {Array} */ const attributes = []; 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 2bae4486dc58..3569574f1b1f 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 @@ -422,11 +422,26 @@ export function build_component(node, component_name, context, anchor = context. } if (Object.keys(custom_css_props).length > 0) { - context.state.template.push( - context.state.metadata.namespace === 'svg' - ? '' - : '' - ); + /** + * @type {typeof context.state.template} + */ + const template_operations = []; + if (context.state.metadata.namespace === 'svg') { + // this boils down to + template_operations.push({ kind: 'create_element', args: ['g'] }); + template_operations.push({ kind: 'push_element' }); + template_operations.push({ kind: 'create_anchor' }); + template_operations.push({ kind: 'pop_element' }); + } else { + // this boils down to + template_operations.push({ kind: 'create_element', args: ['svelte-css-wrapper'] }); + template_operations.push({ kind: 'set_prop', args: ['style', 'display: contents'] }); + template_operations.push({ kind: 'push_element' }); + template_operations.push({ kind: 'create_anchor' }); + template_operations.push({ kind: 'pop_element' }); + } + + context.state.template.push(...template_operations); statements.push( b.stmt(b.call('$.css_props', anchor, b.thunk(b.object(custom_css_props)))), @@ -434,7 +449,7 @@ export function build_component(node, component_name, context, anchor = context. b.stmt(b.call('$.reset', anchor)) ); } else { - context.state.template.push(''); + context.state.template.push({ kind: 'create_anchor' }); statements.push(b.stmt(fn(anchor))); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js index f076d7c11ea9..2ab273fca551 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js @@ -64,11 +64,20 @@ export function process_children(nodes, initial, is_element, { visit, state }) { function flush_sequence(sequence) { if (sequence.every((node) => node.type === 'Text')) { skipped += 1; - state.template.push(sequence.map((node) => node.raw).join('')); + state.template.push({ + kind: 'create_text', + args: [ + sequence + .map((node) => (state.is_functional_template_mode ? node.data : node.raw)) + .join('') + ] + }); return; } - - state.template.push(' '); + state.template.push({ + kind: 'create_text', + args: [' '] + }); const { has_state, value } = build_template_chunk(sequence, visit, state); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js index a293b98e7e9e..bb394a86d5ea 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/Fragment.js @@ -19,7 +19,9 @@ export function Fragment(node, context) { namespace, context.state, context.state.preserve_whitespace, - context.state.options.preserveComments + context.state.options.preserveComments, + // templating mode doesn't affect server builds + false ); /** @type {ComponentServerTransformState} */ diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js index af50695efa62..2126ab601251 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js @@ -47,7 +47,9 @@ export function RegularElement(node, context) { scope: /** @type {Scope} */ (state.scopes.get(node.fragment)) }, state.preserve_whitespace, - state.options.preserveComments + state.options.preserveComments, + // templating mode doesn't affect server builds + false ); for (const node of hoisted) { diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js index 46872fbfcfb8..07beb27e022a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/utils.js @@ -141,6 +141,7 @@ function sort_const_tags(nodes, state) { * @param {TransformState & { options: ValidatedCompileOptions }} state * @param {boolean} preserve_whitespace * @param {boolean} preserve_comments + * @param {boolean} [is_functional_template_mode] */ export function clean_nodes( parent, @@ -152,7 +153,8 @@ export function clean_nodes( // first, we need to make `Component(Client|Server)TransformState` inherit from a new `ComponentTransformState` // rather than from `ClientTransformState` and `ServerTransformState` preserve_whitespace, - preserve_comments + preserve_comments, + is_functional_template_mode ) { if (!state.analysis.runes) { nodes = sort_const_tags(nodes, state); @@ -272,11 +274,19 @@ export function clean_nodes( var first = trimmed[0]; // initial newline inside a `
` is disregarded, if not followed by another newline
-	if (parent.type === 'RegularElement' && parent.name === 'pre' && first?.type === 'Text') {
+	if (
+		parent.type === 'RegularElement' &&
+		// we also want to do the replacement on the textarea if we are in functional template mode because createTextNode behave differently
+		// then template.innerHTML
+		(parent.name === 'pre' || (is_functional_template_mode && parent.name === 'textarea')) &&
+		first?.type === 'Text'
+	) {
 		const text = first.data.replace(regex_starts_with_newline, '');
 		if (text !== first.data) {
 			const tmp = text.replace(regex_starts_with_newline, '');
-			if (text === tmp) {
+			// do an extra replacement if we are in functional template mode because createTextNode behave differently
+			// then template.innerHTML
+			if (text === tmp || is_functional_template_mode) {
 				first.data = text;
 				first.raw = first.raw.replace(regex_starts_with_newline, '');
 				if (first.data === '') {
diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts
index eec41bad9d25..4aa244ad5191 100644
--- a/packages/svelte/src/compiler/types/index.d.ts
+++ b/packages/svelte/src/compiler/types/index.d.ts
@@ -113,6 +113,12 @@ export interface CompileOptions extends ModuleCompileOptions {
 	 * @default false
 	 */
 	preserveWhitespace?: boolean;
+	/**
+	 *  If `functional`, the template will get compiled to a series of `document.createElement` calls, if `string` it will render the template tp a string and use `template.innerHTML`.
+	 *
+	 * @default 'string'
+	 */
+	templatingMode?: 'string' | 'functional';
 	/**
 	 * Set to `true` to force the compiler into runes mode, even if there are no indications of runes usage.
 	 * Set to `false` to force the compiler into ignoring runes, even if there are indications of runes usage.
diff --git a/packages/svelte/src/compiler/validate-options.js b/packages/svelte/src/compiler/validate-options.js
index ab932ed5bca1..1d67951fd855 100644
--- a/packages/svelte/src/compiler/validate-options.js
+++ b/packages/svelte/src/compiler/validate-options.js
@@ -110,6 +110,8 @@ export const validate_component_options =
 
 			preserveComments: boolean(false),
 
+			templatingMode: list(['string', 'functional']),
+
 			preserveWhitespace: boolean(false),
 
 			runes: boolean(undefined),
diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js
index aae44d4b3989..97062f04e38d 100644
--- a/packages/svelte/src/internal/client/dom/operations.js
+++ b/packages/svelte/src/internal/client/dom/operations.js
@@ -204,3 +204,44 @@ export function sibling(node, count = 1, is_text = false) {
 export function clear_text_content(node) {
 	node.textContent = '';
 }
+
+/**
+ *
+ * @param {string} tag
+ * @param {string} [namespace]
+ * @param {string} [is]
+ * @returns
+ */
+export function create_element(tag, namespace, is) {
+	let options = is ? { is } : undefined;
+	if (namespace) {
+		return document.createElementNS(namespace, tag, options);
+	}
+	return document.createElement(tag, options);
+}
+
+export function create_fragment() {
+	return document.createDocumentFragment();
+}
+
+/**
+ * @param {string} data
+ * @returns
+ */
+export function create_comment(data = '') {
+	return document.createComment(data);
+}
+
+/**
+ * @param {Element} element
+ * @param {string} key
+ * @param {string} value
+ * @returns
+ */
+export function set_attribute(element, key, value = '') {
+	if (key.startsWith('xlink:')) {
+		element.setAttributeNS('http://www.w3.org/1999/xlink', key, value);
+		return;
+	}
+	return element.setAttribute(key, value);
+}
diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js
index de2df62c927f..21fba1f5d909 100644
--- a/packages/svelte/src/internal/client/dom/template.js
+++ b/packages/svelte/src/internal/client/dom/template.js
@@ -1,9 +1,22 @@
 /** @import { Effect, TemplateNode } from '#client' */
 import { hydrate_next, hydrate_node, hydrating, set_hydrate_node } from './hydration.js';
-import { create_text, get_first_child, is_firefox } from './operations.js';
+import {
+	create_text,
+	get_first_child,
+	is_firefox,
+	create_element,
+	create_fragment,
+	create_comment,
+	set_attribute
+} from './operations.js';
 import { create_fragment_from_html } from './reconciler.js';
 import { active_effect } from '../runtime.js';
-import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js';
+import {
+	NAMESPACE_MATHML,
+	NAMESPACE_SVG,
+	TEMPLATE_FRAGMENT,
+	TEMPLATE_USE_IMPORT_NODE
+} from '../../../constants.js';
 
 /**
  * @param {TemplateNode} start
@@ -64,6 +77,110 @@ export function template(content, flags) {
 	};
 }
 
+/**
+ * @typedef {{e: string, is?: string, p: Record, c: Array} | null | string | [string]} TemplateStructure
+ */
+
+/**
+ * @param {Array} structure
+ * @param {'svg' | 'math'} [ns]
+ * @param {Array} [namespace_stack]
+ */
+function structure_to_fragment(structure, ns, namespace_stack = [], foreign_object_count = 0) {
+	var fragment = create_fragment();
+	for (var i = 0; i < structure.length; i += 1) {
+		var item = structure[i];
+		if (item == null || Array.isArray(item)) {
+			const data = item ? item[0] : '';
+			fragment.append(create_comment(data));
+		} else if (typeof item === 'string') {
+			fragment.append(create_text(item));
+			continue;
+		} else {
+			let namespace =
+				foreign_object_count > 0
+					? undefined
+					: namespace_stack[namespace_stack.length - 1] ??
+						(ns
+							? ns === 'svg'
+								? NAMESPACE_SVG
+								: ns === 'math'
+									? NAMESPACE_MATHML
+									: undefined
+							: item.e === 'svg'
+								? NAMESPACE_SVG
+								: item.e === 'math'
+									? NAMESPACE_MATHML
+									: undefined);
+			if (namespace !== namespace_stack[namespace_stack.length - 1]) {
+				namespace_stack.push(namespace);
+			}
+			var element = create_element(item.e, namespace, item.is);
+
+			for (var key in item.p) {
+				set_attribute(element, key, item.p[key]);
+			}
+			if (item.c) {
+				(element.tagName === 'TEMPLATE'
+					? /** @type {HTMLTemplateElement} */ (element).content
+					: element
+				).append(
+					...structure_to_fragment(
+						item.c,
+						ns,
+						namespace_stack,
+						element.tagName === 'foreignObject' ? foreign_object_count + 1 : foreign_object_count
+					).childNodes
+				);
+			}
+			namespace_stack.pop();
+			fragment.append(element);
+		}
+	}
+	return fragment;
+}
+
+/**
+ * @param {Array} structure
+ * @param {number} flags
+ * @returns {() => Node | Node[]}
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function template_fn(structure, flags) {
+	var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0;
+	var use_import_node = (flags & TEMPLATE_USE_IMPORT_NODE) !== 0;
+
+	/** @type {Node} */
+	var node;
+
+	return () => {
+		if (hydrating) {
+			assign_nodes(hydrate_node, null);
+			return hydrate_node;
+		}
+
+		if (node === undefined) {
+			node = structure_to_fragment(structure);
+			if (!is_fragment) node = /** @type {Node} */ (get_first_child(node));
+		}
+
+		var clone = /** @type {TemplateNode} */ (
+			use_import_node || is_firefox ? document.importNode(node, true) : node.cloneNode(true)
+		);
+
+		if (is_fragment) {
+			var start = /** @type {TemplateNode} */ (get_first_child(clone));
+			var end = /** @type {TemplateNode} */ (clone.lastChild);
+
+			assign_nodes(start, end);
+		} else {
+			assign_nodes(clone, clone);
+		}
+
+		return clone;
+	};
+}
+
 /**
  * @param {string} content
  * @param {number} flags
@@ -75,6 +192,16 @@ export function template_with_script(content, flags) {
 	return () => run_scripts(/** @type {Element | DocumentFragment} */ (fn()));
 }
 
+/**
+ * @param {Array} structure
+ * @param {number} flags
+ * @returns {() => Node | Node[]}
+ */ /*#__NO_SIDE_EFFECTS__*/
+export function template_with_script_fn(structure, flags) {
+	var templated_fn = template_fn(structure, flags);
+	return () => run_scripts(/** @type {Element | DocumentFragment} */ (templated_fn()));
+}
+
 /**
  * @param {string} content
  * @param {number} flags
@@ -130,6 +257,53 @@ export function ns_template(content, flags, ns = 'svg') {
 	};
 }
 
+/**
+ * @param {Array} structure
+ * @param {number} flags
+ * @param {'svg' | 'math'} ns
+ * @returns {() => Node | Node[]}
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function ns_template_fn(structure, flags, ns = 'svg') {
+	var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0;
+
+	/** @type {Element | DocumentFragment} */
+	var node;
+
+	return () => {
+		if (hydrating) {
+			assign_nodes(hydrate_node, null);
+			return hydrate_node;
+		}
+
+		if (!node) {
+			var fragment = structure_to_fragment(structure, ns);
+
+			if (is_fragment) {
+				node = document.createDocumentFragment();
+				while (get_first_child(fragment)) {
+					node.appendChild(/** @type {Node} */ (get_first_child(fragment)));
+				}
+			} else {
+				node = /** @type {Element} */ (get_first_child(fragment));
+			}
+		}
+
+		var clone = /** @type {TemplateNode} */ (node.cloneNode(true));
+
+		if (is_fragment) {
+			var start = /** @type {TemplateNode} */ (get_first_child(clone));
+			var end = /** @type {TemplateNode} */ (clone.lastChild);
+
+			assign_nodes(start, end);
+		} else {
+			assign_nodes(clone, clone);
+		}
+
+		return clone;
+	};
+}
+
 /**
  * @param {string} content
  * @param {number} flags
@@ -141,6 +315,17 @@ export function svg_template_with_script(content, flags) {
 	return () => run_scripts(/** @type {Element | DocumentFragment} */ (fn()));
 }
 
+/**
+ * @param {Array} structure
+ * @param {number} flags
+ * @returns {() => Node | Node[]}
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function svg_template_with_script_fn(structure, flags) {
+	var templated_fn = ns_template_fn(structure, flags);
+	return () => run_scripts(/** @type {Element | DocumentFragment} */ (templated_fn()));
+}
+
 /**
  * @param {string} content
  * @param {number} flags
@@ -151,6 +336,16 @@ export function mathml_template(content, flags) {
 	return ns_template(content, flags, 'math');
 }
 
+/**
+ * @param {Array} structure
+ * @param {number} flags
+ * @returns {() => Node | Node[]}
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function mathml_template_fn(structure, flags) {
+	return ns_template_fn(structure, flags, 'math');
+}
+
 /**
  * Creating a document fragment from HTML that contains script tags will not execute
  * the scripts. We need to replace the script tags with new ones so that they are executed.
diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js
index a5f93e8b171b..767d1e3f785f 100644
--- a/packages/svelte/src/internal/client/index.js
+++ b/packages/svelte/src/internal/client/index.js
@@ -94,10 +94,15 @@ export {
 	append,
 	comment,
 	ns_template,
+	ns_template_fn,
 	svg_template_with_script,
+	svg_template_with_script_fn,
 	mathml_template,
+	mathml_template_fn,
 	template,
+	template_fn,
 	template_with_script,
+	template_with_script_fn,
 	text,
 	props_id
 } from './dom/template.js';
diff --git a/packages/svelte/src/internal/server/dev.js b/packages/svelte/src/internal/server/dev.js
index ecf4e67429ac..6ff5426818a9 100644
--- a/packages/svelte/src/internal/server/dev.js
+++ b/packages/svelte/src/internal/server/dev.js
@@ -22,7 +22,7 @@ import { current_component } from './context.js';
 let parent = null;
 
 /** @type {Set} */
-let seen;
+export let seen;
 
 /**
  * @param {Element} element
diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js
index 87bcb473e7e2..5e6e1903d495 100644
--- a/packages/svelte/tests/helpers.js
+++ b/packages/svelte/tests/helpers.js
@@ -58,15 +58,17 @@ export function create_deferred() {
  * @param {Partial} compileOptions
  * @param {boolean} [output_map]
  * @param {any} [preprocessor]
+ * @param {import('./suite').TemplatingMode} [templating_mode]
  */
 export async function compile_directory(
 	cwd,
 	generate,
 	compileOptions = {},
 	output_map = false,
-	preprocessor
+	preprocessor,
+	templating_mode
 ) {
-	const output_dir = `${cwd}/_output/${generate}`;
+	const output_dir = `${cwd}/_output/${generate}${templating_mode === 'functional' ? `-functional` : ''}`;
 
 	fs.rmSync(output_dir, { recursive: true, force: true });
 
@@ -77,7 +79,8 @@ export async function compile_directory(
 		let opts = {
 			filename: path.join(cwd, file),
 			...compileOptions,
-			generate
+			generate,
+			templatingMode: templating_mode
 		};
 
 		if (file.endsWith('.js')) {
diff --git a/packages/svelte/tests/hydration/test.ts b/packages/svelte/tests/hydration/test.ts
index 266ac07bff39..0fc518773367 100644
--- a/packages/svelte/tests/hydration/test.ts
+++ b/packages/svelte/tests/hydration/test.ts
@@ -41,10 +41,24 @@ function read(path: string): string | void {
 	return fs.existsSync(path) ? fs.readFileSync(path, 'utf-8') : undefined;
 }
 
-const { test, run } = suite(async (config, cwd) => {
+const { test, run } = suite(async (config, cwd, templating_mode) => {
 	if (!config.load_compiled) {
-		await compile_directory(cwd, 'client', { accessors: true, ...config.compileOptions });
-		await compile_directory(cwd, 'server', config.compileOptions);
+		await compile_directory(
+			cwd,
+			'client',
+			{ accessors: true, ...config.compileOptions },
+			undefined,
+			undefined,
+			templating_mode
+		);
+		await compile_directory(
+			cwd,
+			'server',
+			config.compileOptions,
+			undefined,
+			undefined,
+			templating_mode
+		);
 	}
 
 	const target = window.document.body;
@@ -102,7 +116,11 @@ const { test, run } = suite(async (config, cwd) => {
 		};
 
 		const component = createClassComponent({
-			component: (await import(`${cwd}/_output/client/main.svelte.js`)).default,
+			component: (
+				await import(
+					`${cwd}/_output/client${templating_mode === 'functional' ? '-functional' : ''}/main.svelte.js`
+				)
+			).default,
 			target,
 			hydrate: true,
 			props: config.props,
@@ -169,4 +187,5 @@ const { test, run } = suite(async (config, cwd) => {
 });
 export { test, assert_ok };
 
-await run(__dirname);
+await run(__dirname, 'string');
+await run(__dirname, 'functional');
diff --git a/packages/svelte/tests/runtime-browser/test.ts b/packages/svelte/tests/runtime-browser/test.ts
index 582a10edf722..02823ad87ba6 100644
--- a/packages/svelte/tests/runtime-browser/test.ts
+++ b/packages/svelte/tests/runtime-browser/test.ts
@@ -4,7 +4,7 @@ import * as fs from 'node:fs';
 import * as path from 'node:path';
 import { compile } from 'svelte/compiler';
 import { afterAll, assert, beforeAll, describe } from 'vitest';
-import { suite, suite_with_variants } from '../suite';
+import { suite, suite_with_variants, type TemplatingMode } from '../suite';
 import { write } from '../helpers';
 import type { Warning } from '#compiler';
 
@@ -35,27 +35,41 @@ const { run: run_browser_tests } = suite_with_variants<
 		return false;
 	},
 	() => {},
-	async (config, test_dir, variant) => {
-		await run_test(test_dir, config, variant === 'hydrate');
+	async (config, test_dir, variant, _, templating_mode) => {
+		await run_test(test_dir, config, variant === 'hydrate', templating_mode);
 	}
 );
 
 describe.concurrent(
 	'runtime-browser',
-	() => run_browser_tests(__dirname),
+	() => run_browser_tests(__dirname, 'string'),
+	// Browser tests are brittle and slow on CI
+	{ timeout: 20000, retry: process.env.CI ? 1 : 0 }
+);
+
+describe.concurrent(
+	'runtime-browser-functional',
+	() => run_browser_tests(__dirname, 'functional'),
 	// Browser tests are brittle and slow on CI
 	{ timeout: 20000, retry: process.env.CI ? 1 : 0 }
 );
 
 const { run: run_ce_tests } = suite>(
-	async (config, test_dir) => {
-		await run_test(test_dir, config, false);
+	async (config, test_dir, templating_mode) => {
+		await run_test(test_dir, config, false, templating_mode);
 	}
 );
 
 describe.concurrent(
 	'custom-elements',
-	() => run_ce_tests(__dirname, 'custom-elements-samples'),
+	() => run_ce_tests(__dirname, 'string', 'custom-elements-samples'),
+	// Browser tests are brittle and slow on CI
+	{ timeout: 20000, retry: process.env.CI ? 1 : 0 }
+);
+
+describe.concurrent(
+	'custom-elements',
+	() => run_ce_tests(__dirname, 'functional', 'custom-elements-samples'),
 	// Browser tests are brittle and slow on CI
 	{ timeout: 20000, retry: process.env.CI ? 1 : 0 }
 );
@@ -63,7 +77,8 @@ describe.concurrent(
 async function run_test(
 	test_dir: string,
 	config: ReturnType,
-	hydrate: boolean
+	hydrate: boolean,
+	templating_mode: TemplatingMode
 ) {
 	const warnings: any[] = [];
 
@@ -90,10 +105,14 @@ async function run_test(
 							...config.compileOptions,
 							immutable: config.immutable,
 							customElement: test_dir.includes('custom-elements-samples'),
-							accessors: 'accessors' in config ? config.accessors : true
+							accessors: 'accessors' in config ? config.accessors : true,
+							templatingMode: templating_mode
 						});
 
-						write(`${test_dir}/_output/client/${path.basename(args.path)}.js`, compiled.js.code);
+						write(
+							`${test_dir}/_output/client${templating_mode === 'functional' ? '-functional' : ''}/${path.basename(args.path)}.js`,
+							compiled.js.code
+						);
 
 						compiled.warnings.forEach((warning) => {
 							if (warning.code === 'options_deprecated_accessors') return;
@@ -103,7 +122,7 @@ async function run_test(
 						if (compiled.css !== null) {
 							compiled.js.code += `document.head.innerHTML += \`\``;
 							write(
-								`${test_dir}/_output/client/${path.basename(args.path)}.css`,
+								`${test_dir}/_output/${templating_mode === 'functional' ? '-functional' : ''}/${path.basename(args.path)}.css`,
 								compiled.css.code
 							);
 						}
@@ -151,7 +170,8 @@ async function run_test(
 								...config.compileOptions,
 								immutable: config.immutable,
 								customElement: test_dir.includes('custom-elements-samples'),
-								accessors: 'accessors' in config ? config.accessors : true
+								accessors: 'accessors' in config ? config.accessors : true,
+								templatingMode: templating_mode
 							});
 
 							return {
diff --git a/packages/svelte/tests/runtime-legacy/samples/attribute-casing-custom-element/main.svelte b/packages/svelte/tests/runtime-legacy/samples/attribute-casing-custom-element/main.svelte
index 6433d0dc768a..81c855676a6c 100644
--- a/packages/svelte/tests/runtime-legacy/samples/attribute-casing-custom-element/main.svelte
+++ b/packages/svelte/tests/runtime-legacy/samples/attribute-casing-custom-element/main.svelte
@@ -18,8 +18,9 @@
 			this.innerHTML = 'Hello ' + this._obj.text + '!';
 		}
 	}
-
-	window.customElements.define('my-custom-element', MyCustomElement);
+	if(!window.customElements.get('my-custom-element')) {
+		window.customElements.define('my-custom-element', MyCustomElement);
+	}
 
 
 
diff --git a/packages/svelte/tests/runtime-legacy/samples/attribute-custom-element-inheritance/main.svelte b/packages/svelte/tests/runtime-legacy/samples/attribute-custom-element-inheritance/main.svelte
index 1324bcc4b129..04ac58435aa3 100644
--- a/packages/svelte/tests/runtime-legacy/samples/attribute-custom-element-inheritance/main.svelte
+++ b/packages/svelte/tests/runtime-legacy/samples/attribute-custom-element-inheritance/main.svelte
@@ -26,8 +26,10 @@
 	}
 
 	class Extended extends MyCustomElement {}
-
-	window.customElements.define('my-custom-inheritance-element', Extended);
+	
+	if(!window.customElements.get('my-custom-inheritance-element')) {
+		window.customElements.define('my-custom-inheritance-element', Extended);
+	}
 
 
 
diff --git a/packages/svelte/tests/runtime-legacy/samples/window-binding-scroll-store/_config.js b/packages/svelte/tests/runtime-legacy/samples/window-binding-scroll-store/_config.js
index 115c3cfd3867..b9ea2fb27bf5 100644
--- a/packages/svelte/tests/runtime-legacy/samples/window-binding-scroll-store/_config.js
+++ b/packages/svelte/tests/runtime-legacy/samples/window-binding-scroll-store/_config.js
@@ -9,7 +9,8 @@ export default test({
 		Object.defineProperties(window, {
 			scrollY: {
 				value: 0,
-				configurable: true
+				configurable: true,
+				writable: true
 			}
 		});
 		original_scrollTo = window.scrollTo;
diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts
index fc748ce6b299..33bf3d1ccbec 100644
--- a/packages/svelte/tests/runtime-legacy/shared.ts
+++ b/packages/svelte/tests/runtime-legacy/shared.ts
@@ -10,7 +10,8 @@ import { compile_directory } from '../helpers.js';
 import { setup_html_equal } from '../html_equal.js';
 import { raf } from '../animation-helpers.js';
 import type { CompileOptions } from '#compiler';
-import { suite_with_variants, type BaseTest } from '../suite.js';
+import { suite_with_variants, type BaseTest, type TemplatingMode } from '../suite.js';
+import { seen } from '../../src/internal/server/dev.js';
 
 type Assert = typeof import('vitest').assert & {
 	htmlEqual(a: string, b: string, description?: string): void;
@@ -141,16 +142,21 @@ export function runtime_suite(runes: boolean) {
 
 			return false;
 		},
-		(config, cwd) => {
-			return common_setup(cwd, runes, config);
+		(config, cwd, templating_mode) => {
+			return common_setup(cwd, runes, config, templating_mode);
 		},
-		async (config, cwd, variant, common) => {
-			await run_test_variant(cwd, config, variant, common, runes);
+		async (config, cwd, variant, common, templating_mode) => {
+			await run_test_variant(cwd, config, variant, common, runes, templating_mode);
 		}
 	);
 }
 
-async function common_setup(cwd: string, runes: boolean | undefined, config: RuntimeTest) {
+async function common_setup(
+	cwd: string,
+	runes: boolean | undefined,
+	config: RuntimeTest,
+	templating_mode: TemplatingMode
+) {
 	const force_hmr = process.env.HMR && config.compileOptions?.dev !== false && !config.error;
 
 	const compileOptions: CompileOptions = {
@@ -161,13 +167,14 @@ async function common_setup(cwd: string, runes: boolean | undefined, config: Run
 		...config.compileOptions,
 		immutable: config.immutable,
 		accessors: 'accessors' in config ? config.accessors : true,
-		runes
+		runes,
+		templatingMode: templating_mode
 	};
 
 	// load_compiled can be used for debugging a test. It means the compiler will not run on the input
 	// so you can manipulate the output manually to see what fixes it, adding console.logs etc.
 	if (!config.load_compiled) {
-		await compile_directory(cwd, 'client', compileOptions);
+		await compile_directory(cwd, 'client', compileOptions, undefined, undefined, templating_mode);
 		await compile_directory(cwd, 'server', compileOptions);
 	}
 
@@ -179,7 +186,8 @@ async function run_test_variant(
 	config: RuntimeTest,
 	variant: 'dom' | 'hydrate' | 'ssr',
 	compileOptions: CompileOptions,
-	runes: boolean
+	runes: boolean,
+	templating_mode: TemplatingMode
 ) {
 	let unintended_error = false;
 
@@ -257,8 +265,15 @@ async function run_test_variant(
 		raf.reset();
 
 		// Put things we need on window for testing
-		const styles = globSync('**/*.css', { cwd: `${cwd}/_output/client` })
-			.map((file) => fs.readFileSync(`${cwd}/_output/client/${file}`, 'utf-8'))
+		const styles = globSync('**/*.css', {
+			cwd: `${cwd}/_output/client${templating_mode === 'functional' ? '-functional' : ''}`
+		})
+			.map((file) =>
+				fs.readFileSync(
+					`${cwd}/_output/client${templating_mode === 'functional' ? '-functional' : ''}/${file}`,
+					'utf-8'
+				)
+			)
 			.join('\n')
 			.replace(/\/\*<\/?style>\*\//g, '');
 
@@ -274,7 +289,9 @@ async function run_test_variant(
 
 		globalThis.requestAnimationFrame = globalThis.setTimeout;
 
-		let mod = await import(`${cwd}/_output/client/main.svelte.js`);
+		let mod = await import(
+			`${cwd}/_output/client${templating_mode === 'functional' ? '-functional' : ''}/main.svelte.js`
+		);
 
 		const target = window.document.querySelector('main') as HTMLElement;
 
@@ -282,6 +299,8 @@ async function run_test_variant(
 
 		if (variant === 'hydrate' || variant === 'ssr') {
 			config.before_test?.();
+			// we need to clear the seen messages between tests
+			seen?.clear?.();
 			// ssr into target
 			const SsrSvelteComponent = (await import(`${cwd}/_output/server/main.svelte.js`)).default;
 			const { html, head } = render(SsrSvelteComponent, {
diff --git a/packages/svelte/tests/runtime-legacy/test.ts b/packages/svelte/tests/runtime-legacy/test.ts
index c4617a571c08..d422d8a33637 100644
--- a/packages/svelte/tests/runtime-legacy/test.ts
+++ b/packages/svelte/tests/runtime-legacy/test.ts
@@ -11,4 +11,5 @@ const { test, run } = runtime_suite(false);
 
 export { test, ok };
 
-await run(__dirname);
+await run(__dirname, 'string');
+await run(__dirname, 'functional');
diff --git a/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte b/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte
index 4c98245e5b6b..82774f160d3b 100644
--- a/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/custom-element-attributes/main.svelte
@@ -1,18 +1,20 @@
 
 
 
diff --git a/packages/svelte/tests/runtime-runes/test.ts b/packages/svelte/tests/runtime-runes/test.ts
index 0806864060a3..5dafe62ad298 100644
--- a/packages/svelte/tests/runtime-runes/test.ts
+++ b/packages/svelte/tests/runtime-runes/test.ts
@@ -5,4 +5,5 @@ const { test, run } = runtime_suite(true);
 
 export { test, ok };
 
-await run(__dirname);
+await run(__dirname, 'string');
+await run(__dirname, 'functional');
diff --git a/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client-functional/index.svelte.js b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client-functional/index.svelte.js
new file mode 100644
index 000000000000..44b0cd1557aa
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client-functional/index.svelte.js
@@ -0,0 +1,36 @@
+import 'svelte/internal/disclose-version';
+import * as $ from 'svelte/internal/client';
+
+function increment(_, counter) {
+	counter.count += 1;
+}
+
+var root = $.template_fn([{ e: 'button', c: [' '] }, ' ', , ' '], 1);
+
+export default function Await_block_scope($$anchor) {
+	let counter = $.proxy({ count: 0 });
+	const promise = $.derived(() => Promise.resolve(counter));
+	var fragment = root();
+	var button = $.first_child(fragment);
+
+	button.__click = [increment, counter];
+
+	var text = $.child(button);
+
+	$.reset(button);
+
+	var node = $.sibling(button, 2);
+
+	$.await(node, () => $.get(promise), null, ($$anchor, counter) => {});
+
+	var text_1 = $.sibling(node);
+
+	$.template_effect(() => {
+		$.set_text(text, `clicks: ${counter.count ?? ''}`);
+		$.set_text(text_1, ` ${counter.count ?? ''}`);
+	});
+
+	$.append($$anchor, fragment);
+}
+
+$.delegate(['click']);
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client-functional/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client-functional/index.svelte.js
new file mode 100644
index 000000000000..e06c3bbf6bc8
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client-functional/index.svelte.js
@@ -0,0 +1,34 @@
+import 'svelte/internal/disclose-version';
+import * as $ from 'svelte/internal/client';
+import TextInput from './Child.svelte';
+
+const snippet = ($$anchor) => {
+	$.next();
+
+	var text = $.text('Something');
+
+	$.append($$anchor, text);
+};
+
+var root = $.template_fn([,, ' '], 1);
+
+export default function Bind_component_snippet($$anchor) {
+	let value = $.state('');
+	const _snippet = snippet;
+	var fragment = root();
+	var node = $.first_child(fragment);
+
+	TextInput(node, {
+		get value() {
+			return $.get(value);
+		},
+		set value($$value) {
+			$.set(value, $$value, true);
+		}
+	});
+
+	var text_1 = $.sibling(node);
+
+	$.template_effect(() => $.set_text(text_1, ` value: ${$.get(value) ?? ''}`));
+	$.append($$anchor, fragment);
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/bind-this/_expected/client-functional/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-this/_expected/client-functional/index.svelte.js
new file mode 100644
index 000000000000..dfd32a04e51d
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/bind-this/_expected/client-functional/index.svelte.js
@@ -0,0 +1,7 @@
+import 'svelte/internal/disclose-version';
+import 'svelte/internal/flags/legacy';
+import * as $ from 'svelte/internal/client';
+
+export default function Bind_this($$anchor) {
+	$.bind_this(Foo($$anchor, { $$legacy: true }), ($$value) => foo = $$value, () => foo);
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client-functional/index.svelte.js b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client-functional/index.svelte.js
new file mode 100644
index 000000000000..21339741761f
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client-functional/index.svelte.js
@@ -0,0 +1,27 @@
+import 'svelte/internal/disclose-version';
+import * as $ from 'svelte/internal/client';
+
+export default function Class_state_field_constructor_assignment($$anchor, $$props) {
+	$.push($$props, true);
+
+	class Foo {
+		#a = $.state();
+
+		get a() {
+			return $.get(this.#a);
+		}
+
+		set a(value) {
+			$.set(this.#a, value, true);
+		}
+
+		#b = $.state();
+
+		constructor() {
+			this.a = 1;
+			$.set(this.#b, 2);
+		}
+	}
+
+	$.pop();
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client-functional/index.svelte.js b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client-functional/index.svelte.js
new file mode 100644
index 000000000000..47f297bce9c7
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client-functional/index.svelte.js
@@ -0,0 +1,16 @@
+/* index.svelte.js generated by Svelte VERSION */
+import * as $ from 'svelte/internal/client';
+
+let a = $.state(1);
+let b = $.state(2);
+let c = 3;
+let d = 4;
+
+export function update(array) {
+	(
+		$.set(a, array[0], true),
+		$.set(b, array[1], true)
+	);
+
+	[c, d] = array;
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client-functional/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client-functional/main.svelte.js
new file mode 100644
index 000000000000..6bf2d77a00bd
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client-functional/main.svelte.js
@@ -0,0 +1,49 @@
+import 'svelte/internal/disclose-version';
+import * as $ from 'svelte/internal/client';
+
+var root = $.template_fn(
+	[
+		{ e: 'div' },
+		' ',
+		{ e: 'svg' },
+		' ',
+		{ e: 'custom-element' },
+		' ',
+		{ e: 'div' },
+		' ',
+		{ e: 'svg' },
+		' ',
+		{ e: 'custom-element' }
+	],
+	3
+);
+
+export default function Main($$anchor) {
+	// needs to be a snapshot test because jsdom does auto-correct the attribute casing
+	let x = 'test';
+	let y = () => 'test';
+	var fragment = root();
+	var div = $.first_child(fragment);
+	var svg = $.sibling(div, 2);
+	var custom_element = $.sibling(svg, 2);
+
+	$.template_effect(() => $.set_custom_element_data(custom_element, 'fooBar', x));
+
+	var div_1 = $.sibling(custom_element, 2);
+	var svg_1 = $.sibling(div_1, 2);
+	var custom_element_1 = $.sibling(svg_1, 2);
+
+	$.template_effect(() => $.set_custom_element_data(custom_element_1, 'fooBar', y()));
+
+	$.template_effect(
+		($0, $1) => {
+			$.set_attribute(div, 'foobar', x);
+			$.set_attribute(svg, 'viewBox', x);
+			$.set_attribute(div_1, 'foobar', $0);
+			$.set_attribute(svg_1, 'viewBox', $1);
+		},
+		[y, y]
+	);
+
+	$.append($$anchor, fragment);
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client-functional/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client-functional/index.svelte.js
new file mode 100644
index 000000000000..c0626bd416c9
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client-functional/index.svelte.js
@@ -0,0 +1,19 @@
+import 'svelte/internal/disclose-version';
+import 'svelte/internal/flags/legacy';
+import * as $ from 'svelte/internal/client';
+
+export default function Each_string_template($$anchor) {
+	var fragment = $.comment();
+	var node = $.first_child(fragment);
+
+	$.each(node, 0, () => ['foo', 'bar', 'baz'], $.index, ($$anchor, thing) => {
+		$.next();
+
+		var text = $.text();
+
+		$.template_effect(() => $.set_text(text, `${thing ?? ''}, `));
+		$.append($$anchor, text);
+	});
+
+	$.append($$anchor, fragment);
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/export-state/_expected/client-functional/index.svelte.js b/packages/svelte/tests/snapshot/samples/export-state/_expected/client-functional/index.svelte.js
new file mode 100644
index 000000000000..c2a6054bc6f6
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/export-state/_expected/client-functional/index.svelte.js
@@ -0,0 +1,4 @@
+/* index.svelte.js generated by Svelte VERSION */
+import * as $ from 'svelte/internal/client';
+
+export const object = $.proxy({ ok: true });
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client-functional/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client-functional/index.svelte.js
new file mode 100644
index 000000000000..762a23754c9b
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client-functional/index.svelte.js
@@ -0,0 +1,27 @@
+import 'svelte/internal/disclose-version';
+import * as $ from 'svelte/internal/client';
+
+export default function Function_prop_no_getter($$anchor) {
+	let count = $.state(0);
+
+	function onmouseup() {
+		$.set(count, $.get(count) + 2);
+	}
+
+	const plusOne = (num) => num + 1;
+
+	Button($$anchor, {
+		onmousedown: () => $.set(count, $.get(count) + 1),
+		onmouseup,
+		onmouseenter: () => $.set(count, plusOne($.get(count)), true),
+		children: ($$anchor, $$slotProps) => {
+			$.next();
+
+			var text = $.text();
+
+			$.template_effect(() => $.set_text(text, `clicks: ${$.get(count) ?? ''}`));
+			$.append($$anchor, text);
+		},
+		$$slots: { default: true }
+	});
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/hello-world/_expected/client-functional/index.svelte.js b/packages/svelte/tests/snapshot/samples/hello-world/_expected/client-functional/index.svelte.js
new file mode 100644
index 000000000000..3cc49718838f
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/hello-world/_expected/client-functional/index.svelte.js
@@ -0,0 +1,11 @@
+import 'svelte/internal/disclose-version';
+import 'svelte/internal/flags/legacy';
+import * as $ from 'svelte/internal/client';
+
+var root = $.template_fn([{ e: 'h1', c: ['hello world'] }]);
+
+export default function Hello_world($$anchor) {
+	var h1 = root();
+
+	$.append($$anchor, h1);
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/hmr/_expected/client-functional/index.svelte.js b/packages/svelte/tests/snapshot/samples/hmr/_expected/client-functional/index.svelte.js
new file mode 100644
index 000000000000..d5bb01474cf9
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/hmr/_expected/client-functional/index.svelte.js
@@ -0,0 +1,22 @@
+import 'svelte/internal/disclose-version';
+import 'svelte/internal/flags/legacy';
+import * as $ from 'svelte/internal/client';
+
+var root = $.template_fn([{ e: 'h1', c: ['hello world'] }]);
+
+function Hmr($$anchor) {
+	var h1 = root();
+
+	$.append($$anchor, h1);
+}
+
+if (import.meta.hot) {
+	Hmr = $.hmr(Hmr, () => Hmr[$.HMR].source);
+
+	import.meta.hot.accept((module) => {
+		module.default[$.HMR].source = Hmr[$.HMR].source;
+		$.set(Hmr[$.HMR].source, module.default[$.HMR].original);
+	});
+}
+
+export default Hmr;
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client-functional/export.js b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client-functional/export.js
new file mode 100644
index 000000000000..b4bb7075da08
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client-functional/export.js
@@ -0,0 +1 @@
+export * from '../../export.js';
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client-functional/index.svelte.js b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client-functional/index.svelte.js
new file mode 100644
index 000000000000..ebbe191dcbe4
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client-functional/index.svelte.js
@@ -0,0 +1,8 @@
+import 'svelte/internal/disclose-version';
+import 'svelte/internal/flags/legacy';
+import * as $ from 'svelte/internal/client';
+import { random } from './module.svelte';
+
+export default function Imports_in_modules($$anchor) {
+	
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client-functional/module.svelte.js b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client-functional/module.svelte.js
new file mode 100644
index 000000000000..0d366e6258ff
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client-functional/module.svelte.js
@@ -0,0 +1,5 @@
+/* module.svelte.js generated by Svelte VERSION */
+import * as $ from 'svelte/internal/client';
+import { random } from './export';
+
+export { random };
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client-functional/index.svelte.js b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client-functional/index.svelte.js
new file mode 100644
index 000000000000..3a2d0ecaa29e
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client-functional/index.svelte.js
@@ -0,0 +1,46 @@
+import 'svelte/internal/disclose-version';
+import * as $ from 'svelte/internal/client';
+
+var on_click = (_, count) => $.update(count);
+
+var root = $.template_fn(
+	[
+		{ e: 'h1' },
+		' ',
+		{ e: 'b' },
+		' ',
+		{ e: 'button', c: [' '] },
+		' ',
+		{ e: 'h1' }
+	],
+	1
+);
+
+export default function Nullish_coallescence_omittance($$anchor) {
+	let name = 'world';
+	let count = $.state(0);
+	var fragment = root();
+	var h1 = $.first_child(fragment);
+
+	h1.textContent = `Hello, ${name ?? ''}!`;
+
+	var b = $.sibling(h1, 2);
+
+	b.textContent = `${1 ?? 'stuff'}${2 ?? 'more stuff'}${3 ?? 'even more stuff'}`;
+
+	var button = $.sibling(b, 2);
+
+	button.__click = [on_click, count];
+
+	var text = $.child(button);
+
+	$.reset(button);
+
+	var h1_1 = $.sibling(button, 2);
+
+	h1_1.textContent = `Hello, ${name ?? 'earth' ?? ''}`;
+	$.template_effect(() => $.set_text(text, `Count is ${$.get(count) ?? ''}`));
+	$.append($$anchor, fragment);
+}
+
+$.delegate(['click']);
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client-functional/index.svelte.js b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client-functional/index.svelte.js
new file mode 100644
index 000000000000..5a46b9bbefe1
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/props-identifier/_expected/client-functional/index.svelte.js
@@ -0,0 +1,17 @@
+import 'svelte/internal/disclose-version';
+import * as $ from 'svelte/internal/client';
+
+export default function Props_identifier($$anchor, $$props) {
+	$.push($$props, true);
+
+	let props = $.rest_props($$props, ['$$slots', '$$events', '$$legacy']);
+
+	$$props.a;
+	props[a];
+	$$props.a.b;
+	$$props.a.b = true;
+	props.a = true;
+	props[a] = true;
+	props;
+	$.pop();
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/purity/_expected/client-functional/index.svelte.js b/packages/svelte/tests/snapshot/samples/purity/_expected/client-functional/index.svelte.js
new file mode 100644
index 000000000000..d6de4ff4d04f
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/purity/_expected/client-functional/index.svelte.js
@@ -0,0 +1,21 @@
+import 'svelte/internal/disclose-version';
+import 'svelte/internal/flags/legacy';
+import * as $ from 'svelte/internal/client';
+
+var root = $.template_fn([{ e: 'p' }, ' ', { e: 'p' }, ' ', ,], 1);
+
+export default function Purity($$anchor) {
+	var fragment = root();
+	var p = $.first_child(fragment);
+
+	p.textContent = Math.max(0, Math.min(0, 100));
+
+	var p_1 = $.sibling(p, 2);
+
+	p_1.textContent = location.href;
+
+	var node = $.sibling(p_1, 2);
+
+	Child(node, { prop: encodeURIComponent('hello') });
+	$.append($$anchor, fragment);
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client-functional/index.svelte.js b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client-functional/index.svelte.js
new file mode 100644
index 000000000000..b838acb2d6c8
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client-functional/index.svelte.js
@@ -0,0 +1,139 @@
+import 'svelte/internal/disclose-version';
+import * as $ from 'svelte/internal/client';
+
+var root = $.template_fn(
+	[
+		{
+			e: 'header',
+			c: [
+				{
+					e: 'nav',
+					c: [
+						{ e: 'a', p: { href: '/' }, c: ['Home'] },
+						' ',
+						{
+							e: 'a',
+							p: { href: '/away' },
+							c: ['Away']
+						}
+					]
+				}
+			]
+		},
+		' ',
+		{
+			e: 'main',
+			c: [
+				{ e: 'h1', c: [' '] },
+				' ',
+				{
+					e: 'div',
+					p: { class: 'static' },
+					c: [
+						{
+							e: 'p',
+							c: ['we don\'t need to traverse these nodes']
+						}
+					]
+				},
+				' ',
+				{ e: 'p', c: ['or'] },
+				' ',
+				{ e: 'p', c: ['these'] },
+				' ',
+				{ e: 'p', c: ['ones'] },
+				' ',
+				,
+				' ',
+				{ e: 'p', c: ['these'] },
+				' ',
+				{ e: 'p', c: ['trailing'] },
+				' ',
+				{ e: 'p', c: ['nodes'] },
+				' ',
+				{ e: 'p', c: ['can'] },
+				' ',
+				{ e: 'p', c: ['be'] },
+				' ',
+				{ e: 'p', c: ['completely'] },
+				' ',
+				{ e: 'p', c: ['ignored'] }
+			]
+		},
+		' ',
+		{
+			e: 'cant-skip',
+			c: [{ e: 'custom-elements' }]
+		},
+		' ',
+		{ e: 'div', c: [{ e: 'input' }] },
+		' ',
+		{ e: 'div', c: [{ e: 'source' }] },
+		' ',
+		{
+			e: 'select',
+			c: [{ e: 'option', c: ['a'] }]
+		},
+		' ',
+		{
+			e: 'img',
+			p: { src: '...', alt: '', loading: 'lazy' }
+		},
+		' ',
+		{
+			e: 'div',
+			c: [
+				{
+					e: 'img',
+					p: { src: '...', alt: '', loading: 'lazy' }
+				}
+			]
+		}
+	],
+	3
+);
+
+export default function Skip_static_subtree($$anchor, $$props) {
+	var fragment = root();
+	var main = $.sibling($.first_child(fragment), 2);
+	var h1 = $.child(main);
+	var text = $.child(h1, true);
+
+	$.reset(h1);
+
+	var node = $.sibling(h1, 10);
+
+	$.html(node, () => $$props.content, false, false);
+	$.next(14);
+	$.reset(main);
+
+	var cant_skip = $.sibling(main, 2);
+	var custom_elements = $.child(cant_skip);
+
+	$.set_custom_element_data(custom_elements, 'with', 'attributes');
+	$.reset(cant_skip);
+
+	var div = $.sibling(cant_skip, 2);
+	var input = $.child(div);
+
+	$.autofocus(input, true);
+	$.reset(div);
+
+	var div_1 = $.sibling(div, 2);
+	var source = $.child(div_1);
+
+	source.muted = true;
+	$.reset(div_1);
+
+	var select = $.sibling(div_1, 2);
+	var option = $.child(select);
+
+	option.value = null == (option.__value = 'a') ? '' : 'a';
+	$.reset(select);
+
+	var img = $.sibling(select, 2);
+
+	$.next(2);
+	$.template_effect(() => $.set_text(text, $$props.title));
+	$.append($$anchor, fragment);
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client-functional/index.svelte.js b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client-functional/index.svelte.js
new file mode 100644
index 000000000000..2be761b88dc8
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client-functional/index.svelte.js
@@ -0,0 +1,42 @@
+import 'svelte/internal/disclose-version';
+import * as $ from 'svelte/internal/client';
+
+function reset(_, str, tpl) {
+	$.set(str, '');
+	$.set(str, ``);
+	$.set(tpl, '');
+	$.set(tpl, ``);
+}
+
+var root = $.template_fn(
+	[
+		{ e: 'input' },
+		' ',
+		{ e: 'input' },
+		' ',
+		{ e: 'button', c: ['reset'] }
+	],
+	1
+);
+
+export default function State_proxy_literal($$anchor) {
+	let str = $.state('');
+	let tpl = $.state(``);
+	var fragment = root();
+	var input = $.first_child(fragment);
+
+	$.remove_input_defaults(input);
+
+	var input_1 = $.sibling(input, 2);
+
+	$.remove_input_defaults(input_1);
+
+	var button = $.sibling(input_1, 2);
+
+	button.__click = [reset, str, tpl];
+	$.bind_value(input, () => $.get(str), ($$value) => $.set(str, $$value));
+	$.bind_value(input_1, () => $.get(tpl), ($$value) => $.set(tpl, $$value));
+	$.append($$anchor, fragment);
+}
+
+$.delegate(['click']);
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client-functional/index.svelte.js b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client-functional/index.svelte.js
new file mode 100644
index 000000000000..2270005ee0dd
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client-functional/index.svelte.js
@@ -0,0 +1,11 @@
+import 'svelte/internal/disclose-version';
+import * as $ from 'svelte/internal/client';
+
+export default function Svelte_element($$anchor, $$props) {
+	let tag = $.prop($$props, 'tag', 3, 'hr');
+	var fragment = $.comment();
+	var node = $.first_child(fragment);
+
+	$.element(node, tag, false);
+	$.append($$anchor, fragment);
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client-functional/index.svelte.js b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client-functional/index.svelte.js
new file mode 100644
index 000000000000..9176f1ab92cc
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client-functional/index.svelte.js
@@ -0,0 +1,24 @@
+import 'svelte/internal/disclose-version';
+import * as $ from 'svelte/internal/client';
+
+var root = $.template_fn([{ e: 'p', c: [' '] }]);
+
+export default function Text_nodes_deriveds($$anchor) {
+	let count1 = 0;
+	let count2 = 0;
+
+	function text1() {
+		return count1;
+	}
+
+	function text2() {
+		return count2;
+	}
+
+	var p = root();
+	var text = $.child(p);
+
+	$.reset(p);
+	$.template_effect(($0, $1) => $.set_text(text, `${$0 ?? ''}${$1 ?? ''}`), [text1, text2]);
+	$.append($$anchor, p);
+}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/test.ts b/packages/svelte/tests/snapshot/test.ts
index 0a591c6e2a71..ebf1a46daa4b 100644
--- a/packages/svelte/tests/snapshot/test.ts
+++ b/packages/svelte/tests/snapshot/test.ts
@@ -9,8 +9,15 @@ interface SnapshotTest extends BaseTest {
 	compileOptions?: Partial;
 }
 
-const { test, run } = suite(async (config, cwd) => {
-	await compile_directory(cwd, 'client', config.compileOptions);
+const { test, run } = suite(async (config, cwd, templating_mode) => {
+	await compile_directory(
+		cwd,
+		'client',
+		config.compileOptions,
+		undefined,
+		undefined,
+		templating_mode
+	);
 	await compile_directory(cwd, 'server', config.compileOptions);
 
 	// run `UPDATE_SNAPSHOTS=true pnpm test snapshot` to update snapshot tests
@@ -18,8 +25,18 @@ const { test, run } = suite(async (config, cwd) => {
 		fs.rmSync(`${cwd}/_expected`, { recursive: true, force: true });
 		fs.cpSync(`${cwd}/_output`, `${cwd}/_expected`, { recursive: true, force: true });
 	} else {
-		const actual = globSync('**', { cwd: `${cwd}/_output`, onlyFiles: true });
-		const expected = globSync('**', { cwd: `${cwd}/_expected`, onlyFiles: true });
+		const actual = globSync('**', { cwd: `${cwd}/_output`, onlyFiles: true }).filter(
+			// filters out files that might not yet be compiled (functional is executed after string)
+			(expected) =>
+				expected.startsWith('server/') ||
+				expected.startsWith(`client${templating_mode === 'functional' ? '-functional' : ''}/`)
+		);
+		const expected = globSync('**', { cwd: `${cwd}/_expected`, onlyFiles: true }).filter(
+			// filters out files that might not yet be compiled (functional is executed after string)
+			(expected) =>
+				expected.startsWith('server/') ||
+				expected.startsWith(`client${templating_mode === 'functional' ? '-functional' : ''}/`)
+		);
 
 		assert.deepEqual(actual, expected);
 
@@ -41,4 +58,5 @@ const { test, run } = suite(async (config, cwd) => {
 
 export { test };
 
-await run(__dirname);
+await run(__dirname, 'string');
+await run(__dirname, 'functional');
diff --git a/packages/svelte/tests/suite.ts b/packages/svelte/tests/suite.ts
index 0ae06e727f87..c2e7743f2b96 100644
--- a/packages/svelte/tests/suite.ts
+++ b/packages/svelte/tests/suite.ts
@@ -6,6 +6,8 @@ export interface BaseTest {
 	solo?: boolean;
 }
 
+export type TemplatingMode = 'string' | 'functional';
+
 /**
  * To filter tests, run one of these:
  *
@@ -20,14 +22,22 @@ const filter = process.env.FILTER
 		)
 	: /./;
 
-export function suite(fn: (config: Test, test_dir: string) => void) {
+export function suite(
+	fn: (config: Test, test_dir: string, templating_mode: TemplatingMode) => void
+) {
 	return {
 		test: (config: Test) => config,
-		run: async (cwd: string, samples_dir = 'samples') => {
+		run: async (
+			cwd: string,
+			templating_mode: TemplatingMode = 'string',
+			samples_dir = 'samples'
+		) => {
 			await for_each_dir(cwd, samples_dir, (config, dir) => {
 				let it_fn = config.skip ? it.skip : config.solo ? it.only : it;
 
-				it_fn(dir, () => fn(config, `${cwd}/${samples_dir}/${dir}`));
+				it_fn(`${dir} (${templating_mode})`, () =>
+					fn(config, `${cwd}/${samples_dir}/${dir}`, templating_mode)
+				);
 			});
 		}
 	};
@@ -36,12 +46,26 @@ export function suite(fn: (config: Test, test_dir: string
 export function suite_with_variants(
 	variants: Variants[],
 	should_skip_variant: (variant: Variants, config: Test) => boolean | 'no-test',
-	common_setup: (config: Test, test_dir: string) => Promise | Common,
-	fn: (config: Test, test_dir: string, variant: Variants, common: Common) => void
+	common_setup: (
+		config: Test,
+		test_dir: string,
+		templating_mode: TemplatingMode
+	) => Promise | Common,
+	fn: (
+		config: Test,
+		test_dir: string,
+		variant: Variants,
+		common: Common,
+		templating_mode: TemplatingMode
+	) => void
 ) {
 	return {
 		test: (config: Test) => config,
-		run: async (cwd: string, samples_dir = 'samples') => {
+		run: async (
+			cwd: string,
+			templating_mode: TemplatingMode = 'string',
+			samples_dir = 'samples'
+		) => {
 			await for_each_dir(cwd, samples_dir, (config, dir) => {
 				let called_common = false;
 				let common: any = undefined;
@@ -54,12 +78,12 @@ export function suite_with_variants {
+					it_fn(`${dir} (${templating_mode}-${variant})`, async () => {
 						if (!called_common) {
 							called_common = true;
-							common = await common_setup(config, `${cwd}/${samples_dir}/${dir}`);
+							common = await common_setup(config, `${cwd}/${samples_dir}/${dir}`, templating_mode);
 						}
-						return fn(config, `${cwd}/${samples_dir}/${dir}`, variant, common);
+						return fn(config, `${cwd}/${samples_dir}/${dir}`, variant, common, templating_mode);
 					});
 				}
 			});
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index c6000fc4b67f..19bb6e9279b6 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -844,6 +844,12 @@ declare module 'svelte/compiler' {
 		 * @default false
 		 */
 		preserveWhitespace?: boolean;
+		/**
+		 *  If `functional`, the template will get compiled to a series of `document.createElement` calls, if `string` it will render the template tp a string and use `template.innerHTML`.
+		 *
+		 * @default 'string'
+		 */
+		templatingMode?: 'string' | 'functional';
 		/**
 		 * Set to `true` to force the compiler into runes mode, even if there are no indications of runes usage.
 		 * Set to `false` to force the compiler into ignoring runes, even if there are indications of runes usage.
@@ -2554,6 +2560,12 @@ declare module 'svelte/types/compiler/interfaces' {
 		 * @default false
 		 */
 		preserveWhitespace?: boolean;
+		/**
+		 *  If `functional`, the template will get compiled to a series of `document.createElement` calls, if `string` it will render the template tp a string and use `template.innerHTML`.
+		 *
+		 * @default 'string'
+		 */
+		templatingMode?: 'string' | 'functional';
 		/**
 		 * Set to `true` to force the compiler into runes mode, even if there are no indications of runes usage.
 		 * Set to `false` to force the compiler into ignoring runes, even if there are indications of runes usage.
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c687db12d4a9..420e0e90fd15 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -916,7 +916,7 @@ packages:
     resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==}
 
   concat-map@0.0.1:
-    resolution: {integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=}
+    resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
 
   cross-spawn@5.1.0:
     resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==}