From de8a38ba0e3bc46b54cac0817aec26ca73f73fbf Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Tue, 18 Mar 2025 18:16:18 +0100 Subject: [PATCH 01/17] feat: templateless template generation --- .changeset/smart-boats-accept.md | 5 + .../client/transform-template/index.js | 12 ++ .../client/transform-template/to-string.js | 170 ++++++++++++++++++ .../phases/3-transform/client/types.d.ts | 14 +- .../3-transform/client/visitors/AwaitBlock.js | 2 +- .../3-transform/client/visitors/Comment.js | 2 +- .../3-transform/client/visitors/EachBlock.js | 2 +- .../3-transform/client/visitors/Fragment.js | 10 +- .../3-transform/client/visitors/HtmlTag.js | 2 +- .../3-transform/client/visitors/IfBlock.js | 2 +- .../3-transform/client/visitors/KeyBlock.js | 2 +- .../client/visitors/RegularElement.js | 36 ++-- .../3-transform/client/visitors/RenderTag.js | 2 +- .../client/visitors/SlotElement.js | 2 +- .../client/visitors/SvelteBoundary.js | 2 +- .../client/visitors/SvelteElement.js | 2 +- .../client/visitors/shared/component.js | 25 ++- .../client/visitors/shared/fragment.js | 11 +- 18 files changed, 265 insertions(+), 38 deletions(-) create mode 100644 .changeset/smart-boats-accept.md create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-string.js diff --git a/.changeset/smart-boats-accept.md b/.changeset/smart-boats-accept.md new file mode 100644 index 000000000000..22be423e1f6b --- /dev/null +++ b/.changeset/smart-boats-accept.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: templateless template generation 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..a2854abad298 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js @@ -0,0 +1,12 @@ +/** + * @import { TemplateOperations } from "../types.js" + */ +import { template_to_string } from './to-string'; + +/** + * @param {TemplateOperations} items + */ +export function transform_template(items) { + // here we will check if we need to use `$.template` or create a series of `document.createElement` calls + return template_to_string(items); +} 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..c4a0b1565c67 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-string.js @@ -0,0 +1,170 @@ +/** + * @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) { + if (instruction.kind === 'push_element' && last_current_element) { + elements_stack.push(last_current_element); + continue; + } + 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]( + ...[ + ...(instruction.kind === 'set_prop' ? [last_current_element] : []), + ...(instruction.args ?? []) + ] + ); + 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); + } + 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') { + str += `<${el.element}`; + for (let [prop, value] of Object.entries(el.props ?? {})) { + if (value == null) { + str += ` ${prop}`; + } else { + str += ` ${prop}="${value}"`; + } + } + str += `>`; + for (let child of el.children ?? []) { + str += stringify(child); + } + 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 63fe3223cf7d..98917e737237 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,18 @@ 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; +}>; export interface ComponentClientTransformState extends ClientTransformState { readonly analysis: ComponentAnalysis; readonly options: ValidatedCompileOptions; @@ -56,7 +68,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; 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..27ad894ccf0b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -7,6 +7,7 @@ import { 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'; @@ -118,7 +119,7 @@ export function Fragment(node, context) { }); /** @type {Expression[]} */ - const args = [join_template(state.template)]; + const args = [b.template([b.quasi(transform_template(state.template), true)], [])]; if (state.metadata.context.template_needs_import_node) { args.push(b.literal(TEMPLATE_USE_IMPORT_NODE)); @@ -168,11 +169,14 @@ 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)]); + add_template(template_name, [ + b.template([b.quasi(transform_template(state.template), true)], []), + b.literal(flags) + ]); body.push(b.var(id, b.call(template_name))); } 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..78cfd4d22200 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,10 @@ 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] + }); /** @type {Array} */ const attributes = []; @@ -110,7 +116,10 @@ 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', escape_html(value.value, true)] + }); continue; } } @@ -286,13 +295,14 @@ 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 ? '' : escape_html(value, true)] + ) + }); } } else if (name === 'autofocus') { let { value } = build_attribute_value(attribute.value, context); @@ -324,8 +334,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, @@ -446,10 +455,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..90fe4d93e188 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,24 @@ 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') { + 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 { + 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 +447,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..025d39b2e91f 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,16 @@ 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) => node.raw).join('')] + }); return; } - - state.template.push(' '); + state.template.push({ + kind: 'create_text', + args: [' '] + }); const { has_state, value } = build_template_chunk(sequence, visit, state); From 575908afc1157f6f8ae6b1840cca91f1fd98e21a Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Tue, 18 Mar 2025 18:22:45 +0100 Subject: [PATCH 02/17] fix: auto import fumble --- .../phases/3-transform/client/transform-template/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js index a2854abad298..7cda7a4759dc 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js @@ -1,7 +1,7 @@ /** * @import { TemplateOperations } from "../types.js" */ -import { template_to_string } from './to-string'; +import { template_to_string } from './to-string.js'; /** * @param {TemplateOperations} items From 8a737f10701febdd17665a7c07a2c45368b8a138 Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Tue, 18 Mar 2025 23:47:41 +0100 Subject: [PATCH 03/17] feat: add option `preventTemplateCloning` and functions transformation --- .../3-transform/client/transform-client.js | 1 + .../client/transform-template/index.js | 96 ++++++++- .../client/transform-template/to-functions.js | 188 ++++++++++++++++++ .../phases/3-transform/client/types.d.ts | 1 + .../3-transform/client/visitors/Fragment.js | 120 +---------- packages/svelte/src/compiler/types/index.d.ts | 6 + .../svelte/src/compiler/validate-options.js | 2 + .../prevent-template-cloning/_config.js | 7 + .../_expected/client/index.svelte.js | 49 +++++ .../_expected/server/index.svelte.js | 14 ++ .../prevent-template-cloning/index.svelte | 16 ++ packages/svelte/types/index.d.ts | 12 ++ 12 files changed, 393 insertions(+), 119 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js create mode 100644 packages/svelte/tests/snapshot/samples/prevent-template-cloning/_config.js create mode 100644 packages/svelte/tests/snapshot/samples/prevent-template-cloning/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/prevent-template-cloning/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/prevent-template-cloning/index.svelte 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 ac8263b91669..bdf70408f1bd 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: [], + prevent_template_cloning: options.preventTemplateCloning, // 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/index.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js index 7cda7a4759dc..ca7f29505ef7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js @@ -1,12 +1,98 @@ /** - * @import { TemplateOperations } from "../types.js" + * @import { ComponentContext, TemplateOperations, ComponentClientTransformState } from "../types.js" + * @import { Identifier, Expression } from "estree" + * @import { AST, Namespace } from '#compiler' + * @import { SourceLocation } from '#shared' */ +import { TEMPLATE_FRAGMENT } from '../../../../../constants.js'; +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 {TemplateOperations} items + * + * @param {Namespace} namespace + * @param {ComponentClientTransformState} state + * @returns */ -export function transform_template(items) { - // here we will check if we need to use `$.template` or create a series of `document.createElement` calls - return template_to_string(items); +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; + }) + ); +} + +/** + * @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) { + if (context.state.prevent_template_cloning) { + context.state.hoisted.push( + b.var( + template_name, + template_to_functions( + state.template, + namespace, + flags != null && (flags & TEMPLATE_FRAGMENT) !== 0 + ) + ) + ); + + return; + } + + /** + * @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 = [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..9b9cab28a2e3 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js @@ -0,0 +1,188 @@ +/** + * @import { TemplateOperations } from "../types.js" + * @import { Namespace } from "#compiler" + * @import { Statement } from "estree" + */ +import { NAMESPACE_SVG } from 'svelte/internal/client'; +import * as b from '../../../../utils/builders.js'; +import { NAMESPACE_MATHML } from '../../../../../constants.js'; + +class Scope { + declared = new Map(); + + /** + * @param {string} _name + */ + generate(_name) { + let name = _name.replace(/[^a-zA-Z0-9_$]/g, '_').replace(/^[0-9]/, '_'); + if (!this.declared.has(name)) { + this.declared.set(name, 1); + return name; + } + let count = this.declared.get(name); + this.declared.set(name, count + 1); + return `${name}_${count}`; + } +} + +/** + * @param {TemplateOperations} items + * @param {Namespace} namespace + * @param {boolean} use_fragment + */ +export function template_to_functions(items, namespace, use_fragment = false) { + let elements = []; + + let body = []; + + let scope = new Scope(); + + /** + * @type {Array} + */ + let elements_stack = []; + + /** + * @type {Element | undefined} + */ + let last_current_element; + + for (let instruction of items) { + if (instruction.kind === 'push_element' && last_current_element) { + elements_stack.push(last_current_element); + continue; + } + 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 === 'set_prop' ? [last_current_element] : [scope]), + ...(instruction.kind === 'create_element' ? [namespace] : []), + ...(instruction.args ?? []) + ] + ); + + if (value) { + body.push(value.call); + } + + if (instruction.kind !== 'set_prop') { + if (elements_stack.length >= 1 && value) { + const { call } = map.insert(/** @type {Element} */ (elements_stack.at(-1)), value); + body.push(call); + } else if (value) { + elements.push(b.id(value.name)); + } + if (instruction.kind === 'create_element') { + last_current_element = /** @type {Element} */ (value); + } + } + } + if (elements.length > 1 || use_fragment) { + const fragment = scope.generate('fragment'); + body.push(b.var(fragment, b.call('document.createDocumentFragment'))); + body.push(b.call(fragment + '.append', ...elements)); + body.push(b.return(b.id(fragment))); + } else { + body.push(b.return(elements[0])); + } + + return b.arrow([], b.block(body)); +} + +/** + * @typedef {{ call: Statement, name: string }} Element + */ + +/** + * @typedef {{ call: Statement, name: string }} Anchor + */ + +/** + * @typedef {{ call: Statement, name: string }} Text + */ + +/** + * @typedef { Element | Anchor| Text } Node + */ + +/** + * @param {Scope} scope + * @param {Namespace} namespace + * @param {string} element + * @returns {Element} + */ +function create_element(scope, namespace, element) { + const name = scope.generate(element); + let fn = namespace !== 'html' ? 'document.createElementNS' : 'document.createElement'; + let args = [b.literal(element)]; + if (namespace !== 'html') { + args.unshift(namespace === 'svg' ? b.literal(NAMESPACE_SVG) : b.literal(NAMESPACE_MATHML)); + } + return { + call: b.var(name, b.call(fn, ...args)), + name + }; +} + +/** + * @param {Scope} scope + * @param {string} data + * @returns {Anchor} + */ +function create_anchor(scope, data = '') { + const name = scope.generate('comment'); + return { + call: b.var(name, b.call('document.createComment', b.literal(data))), + name + }; +} + +/** + * @param {Scope} scope + * @param {string} value + * @returns {Text} + */ +function create_text(scope, value) { + const name = scope.generate('text'); + return { + call: b.var(name, b.call('document.createTextNode', b.literal(value))), + name + }; +} + +/** + * + * @param {Element} el + * @param {string} prop + * @param {string} value + */ +function set_prop(el, prop, value) { + return { + call: b.call(el.name + '.setAttribute', b.literal(prop), b.literal(value)) + }; +} + +/** + * + * @param {Element} el + * @param {Node} child + * @param {Node} [anchor] + */ +function insert(el, child, anchor) { + return { + call: b.call(el.name + '.insertBefore', b.id(child.name), b.id(anchor?.name ?? 'undefined')) + }; +} + +let map = { + create_element, + create_text, + create_anchor, + set_prop, + insert +}; 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 98917e737237..c32e5075546f 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 @@ -90,6 +90,7 @@ export interface ComponentClientTransformState extends ClientTransformState { }; }; readonly preserve_whitespace: boolean; + readonly prevent_template_cloning?: boolean; /** The anchor node for the current context */ readonly node: Identifier; 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 27ad894ccf0b..29a34d6db157 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,11 +1,8 @@ -/** @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'; @@ -90,24 +87,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]); @@ -118,14 +97,13 @@ export function Fragment(node, context) { node: id }); - /** @type {Expression[]} */ - const args = [b.template([b.quasi(transform_template(state.template), true)], [])]; + 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)); @@ -173,10 +151,7 @@ export function Fragment(node, context) { // special case — we can use `$.comment` instead of creating a unique template body.push(b.var(id, b.call('$.comment'))); } else { - add_template(template_name, [ - b.template([b.quasi(transform_template(state.template), true)], []), - b.literal(flags) - ]); + transform_template(state, context, namespace, template_name, flags); body.push(b.var(id, b.call(template_name))); } @@ -203,86 +178,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/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index eec41bad9d25..4161c7120648 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 `true`, the template will get compiled to a series of `document.createElement` calls instead of using `template.innerHTML`. + * + * @default false + */ + preventTemplateCloning?: boolean; /** * 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..5e6eaf5ef6f4 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), + preventTemplateCloning: boolean(false), + preserveWhitespace: boolean(false), runes: boolean(undefined), diff --git a/packages/svelte/tests/snapshot/samples/prevent-template-cloning/_config.js b/packages/svelte/tests/snapshot/samples/prevent-template-cloning/_config.js new file mode 100644 index 000000000000..e37acdeaf292 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/prevent-template-cloning/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + preventTemplateCloning: true + } +}); diff --git a/packages/svelte/tests/snapshot/samples/prevent-template-cloning/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/prevent-template-cloning/_expected/client/index.svelte.js new file mode 100644 index 000000000000..074de003b4ec --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/prevent-template-cloning/_expected/client/index.svelte.js @@ -0,0 +1,49 @@ +import 'svelte/internal/disclose-version'; +import * as $ from 'svelte/internal/client'; + +function increment(_, counter) { + counter.count += 1; +} + +var root = () => { + var button = document.createElement('button'); + var text = document.createTextNode(' '); + + button.insertBefore(text, undefined) + + var text_1 = document.createTextNode(' '); + var comment = document.createComment(''); + var text_2 = document.createTextNode(' '); + var fragment = document.createDocumentFragment(); + + fragment.append(button, text_1, comment, text_2) + return fragment; +}; + +export default function Prevent_template_cloning($$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/prevent-template-cloning/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/prevent-template-cloning/_expected/server/index.svelte.js new file mode 100644 index 000000000000..2f5584e5f473 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/prevent-template-cloning/_expected/server/index.svelte.js @@ -0,0 +1,14 @@ +import * as $ from 'svelte/internal/server'; + +export default function Prevent_template_cloning($$payload) { + let counter = { count: 0 }; + const promise = Promise.resolve(counter); + + function increment() { + counter.count += 1; + } + + $$payload.out += ` `; + $.await(promise, () => {}, (counter) => {}, () => {}); + $$payload.out += ` ${$.escape(counter.count)}`; +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/prevent-template-cloning/index.svelte b/packages/svelte/tests/snapshot/samples/prevent-template-cloning/index.svelte new file mode 100644 index 000000000000..3dc6c6626283 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/prevent-template-cloning/index.svelte @@ -0,0 +1,16 @@ + + + + +{#await promise then counter}{/await} + +{counter.count} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index c6000fc4b67f..ef0179f3f175 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 `true`, the template will get compiled to a series of `document.createElement` calls instead of using `template.innerHTML`. + * + * @default false + */ + preventTemplateCloning?: boolean; /** * 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 `true`, the template will get compiled to a series of `document.createElement` calls instead of using `template.innerHTML`. + * + * @default false + */ + preventTemplateCloning?: boolean; /** * 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. From 7c7a85754be206bdc57db8cf972e441cb0eba5ab Mon Sep 17 00:00:00 2001 From: paoloricciuti Date: Thu, 20 Mar 2025 10:34:50 +0100 Subject: [PATCH 04/17] feat: make all tests pass with functional templates --- .changeset/smart-boats-accept.md | 2 +- .../fix-attribute-casing.js | 18 +++ .../client/transform-template/index.js | 42 +++---- .../client/transform-template/to-functions.js | 107 ++++++++++++---- .../phases/3-transform/client/types.d.ts | 4 + .../3-transform/client/visitors/Fragment.js | 33 +++-- .../client/visitors/RegularElement.js | 38 ++++-- .../client/visitors/shared/fragment.js | 11 +- .../3-transform/server/visitors/Fragment.js | 4 +- .../server/visitors/RegularElement.js | 4 +- .../src/compiler/phases/3-transform/utils.js | 12 +- .../src/internal/client/dom/template.js | 119 ++++++++++++++++++ packages/svelte/src/internal/client/index.js | 5 + 13 files changed, 328 insertions(+), 71 deletions(-) create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/transform-template/fix-attribute-casing.js diff --git a/.changeset/smart-boats-accept.md b/.changeset/smart-boats-accept.md index 22be423e1f6b..b08063eb8d00 100644 --- a/.changeset/smart-boats-accept.md +++ b/.changeset/smart-boats-accept.md @@ -2,4 +2,4 @@ 'svelte': minor --- -feat: templateless template generation +feat: functional template generation 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 index ca7f29505ef7..98b830a907c4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js @@ -4,7 +4,6 @@ * @import { AST, Namespace } from '#compiler' * @import { SourceLocation } from '#shared' */ -import { TEMPLATE_FRAGMENT } from '../../../../../constants.js'; import { dev } from '../../../../state.js'; import * as b from '../../../../utils/builders.js'; import { template_to_functions } from './to-functions.js'; @@ -18,15 +17,17 @@ import { template_to_string } from './to-string.js'; */ 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'; + 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.prevent_template_cloning ? '_fn' : ''); } /** @@ -54,21 +55,6 @@ function build_locations(locations) { * @param {number} [flags] */ export function transform_template(state, context, namespace, template_name, flags) { - if (context.state.prevent_template_cloning) { - context.state.hoisted.push( - b.var( - template_name, - template_to_functions( - state.template, - namespace, - flags != null && (flags & TEMPLATE_FRAGMENT) !== 0 - ) - ) - ); - - return; - } - /** * @param {Identifier} template_name * @param {Expression[]} args @@ -88,7 +74,11 @@ export function transform_template(state, context, namespace, template_name, fla }; /** @type {Expression[]} */ - const args = [b.template([b.quasi(template_to_string(state.template), true)], [])]; + const args = [ + state.prevent_template_cloning + ? template_to_functions(state.template, namespace) + : b.template([b.quasi(template_to_string(state.template), true)], []) + ]; if (flags) { args.push(b.literal(flags)); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js index 9b9cab28a2e3..9437fae20e11 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js @@ -1,11 +1,12 @@ /** * @import { TemplateOperations } from "../types.js" * @import { Namespace } from "#compiler" - * @import { Statement } from "estree" + * @import { CallExpression, Statement } from "estree" */ import { NAMESPACE_SVG } from 'svelte/internal/client'; -import * as b from '../../../../utils/builders.js'; import { NAMESPACE_MATHML } from '../../../../../constants.js'; +import * as b from '../../../../utils/builders.js'; +import fix_attribute_casing from './fix-attribute-casing.js'; class Scope { declared = new Map(); @@ -28,9 +29,8 @@ class Scope { /** * @param {TemplateOperations} items * @param {Namespace} namespace - * @param {boolean} use_fragment */ -export function template_to_functions(items, namespace, use_fragment = false) { +export function template_to_functions(items, namespace) { let elements = []; let body = []; @@ -42,26 +42,61 @@ export function template_to_functions(items, namespace, use_fragment = false) { */ let elements_stack = []; + /** + * @type {Array} + */ + let namespace_stack = []; + + /** + * @type {number} + */ + let foreign_object_count = 0; + /** * @type {Element | undefined} */ let last_current_element; + if (items[0].kind === 'create_anchor') { + items.unshift({ kind: 'create_anchor' }); + } + for (let instruction of items) { if (instruction.kind === 'push_element' && last_current_element) { elements_stack.push(last_current_element); continue; } if (instruction.kind === 'pop_element') { - elements_stack.pop(); + const removed = elements_stack.pop(); + if (removed?.namespaced) { + namespace_stack.pop(); + } + if (removed?.element === 'foreignObject') { + foreign_object_count--; + } continue; } + if (instruction.metadata?.svg || instruction.metadata?.mathml) { + namespace_stack.push(instruction.metadata.svg ? NAMESPACE_SVG : NAMESPACE_MATHML); + } + // @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 === 'set_prop' ? [last_current_element] : [scope]), - ...(instruction.kind === 'create_element' ? [namespace] : []), + ...(instruction.kind === 'create_element' + ? [ + foreign_object_count > 0 + ? undefined + : namespace_stack.at(-1) ?? + (namespace === 'svg' + ? NAMESPACE_SVG + : namespace === 'mathml' + ? NAMESPACE_MATHML + : undefined) + ] + : []), ...(instruction.args ?? []) ] ); @@ -79,23 +114,22 @@ export function template_to_functions(items, namespace, use_fragment = false) { } if (instruction.kind === 'create_element') { last_current_element = /** @type {Element} */ (value); + if (last_current_element.element === 'foreignObject') { + foreign_object_count++; + } } } } - if (elements.length > 1 || use_fragment) { - const fragment = scope.generate('fragment'); - body.push(b.var(fragment, b.call('document.createDocumentFragment'))); - body.push(b.call(fragment + '.append', ...elements)); - body.push(b.return(b.id(fragment))); - } else { - body.push(b.return(elements[0])); - } + const fragment = scope.generate('fragment'); + body.push(b.var(fragment, b.call('document.createDocumentFragment'))); + body.push(b.call(fragment + '.append', ...elements)); + body.push(b.return(b.id(fragment))); return b.arrow([], b.block(body)); } /** - * @typedef {{ call: Statement, name: string }} Element + * @typedef {{ call: Statement, name: string, add_is: (value: string)=>void, namespaced: boolean; element: string; }} Element */ /** @@ -118,14 +152,26 @@ export function template_to_functions(items, namespace, use_fragment = false) { */ function create_element(scope, namespace, element) { const name = scope.generate(element); - let fn = namespace !== 'html' ? 'document.createElementNS' : 'document.createElement'; + let fn = namespace != null ? 'document.createElementNS' : 'document.createElement'; let args = [b.literal(element)]; - if (namespace !== 'html') { - args.unshift(namespace === 'svg' ? b.literal(NAMESPACE_SVG) : b.literal(NAMESPACE_MATHML)); + if (namespace != null) { + args.unshift(b.literal(namespace)); + } + const call = b.var(name, b.call(fn, ...args)); + /** + * @param {string} value + */ + function add_is(value) { + /** @type {CallExpression} */ (call.declarations[0].init).arguments.push( + b.object([b.prop('init', b.literal('is'), b.literal(value))]) + ); } return { - call: b.var(name, b.call(fn, ...args)), - name + call, + name, + element, + add_is, + namespaced: namespace != null }; } @@ -162,8 +208,21 @@ function create_text(scope, value) { * @param {string} value */ function set_prop(el, prop, value) { + if (prop === 'is') { + el.add_is(value); + return; + } + + const [namespace] = prop.split(':'); + let fn = namespace !== prop ? '.setAttributeNS' : '.setAttribute'; + let args = [b.literal(fix_attribute_casing(prop)), b.literal(value ?? '')]; + + if (namespace === 'xlink') { + args.unshift(b.literal('http://www.w3.org/1999/xlink')); + } + return { - call: b.call(el.name + '.setAttribute', b.literal(prop), b.literal(value)) + call: b.call(el.name + fn, ...args) }; } @@ -175,7 +234,11 @@ function set_prop(el, prop, value) { */ function insert(el, child, anchor) { return { - call: b.call(el.name + '.insertBefore', b.id(child.name), b.id(anchor?.name ?? 'undefined')) + call: b.call( + el.name + (el.element === 'template' ? '.content' : '') + '.insertBefore', + b.id(child.name), + b.id(anchor?.name ?? 'undefined') + ) }; } 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 c32e5075546f..a73e0e0e8358 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 @@ -50,6 +50,10 @@ type TemplateOperationsKind = type TemplateOperations = Array<{ kind: TemplateOperationsKind; args?: Array; + metadata?: { + svg: boolean; + mathml: boolean; + }; }>; export interface ComponentClientTransformState extends ClientTransformState { readonly analysis: ComponentAnalysis; 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 29a34d6db157..98f8b0c2e96b 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 @@ -36,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.prevent_template_cloning ); if (hoisted.length === 0 && trimmed.length === 0) { @@ -124,22 +125,40 @@ export function Fragment(node, context) { // special case — we can use `$.text` instead of creating a unique template const id = b.id(context.state.scope.generate('text')); - process_children(trimmed, () => id, false, { - ...context, - state - }); + process_children( + trimmed, + () => id, + false, + { + ...context, + state + }, + context.state.prevent_template_cloning + ); body.push(b.var(id, b.call('$.text'))); close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); } else { if (is_standalone) { // no need to create a template, we can just use the existing block's anchor - process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state }); + process_children( + trimmed, + () => b.id('$$anchor'), + false, + { ...context, state }, + context.state.prevent_template_cloning + ); } else { /** @type {(is_text: boolean) => Expression} */ const expression = (is_text) => b.call('$.first_child', id, is_text && b.true); - process_children(trimmed, expression, false, { ...context, state }); + process_children( + trimmed, + expression, + false, + { ...context, state }, + context.state.prevent_template_cloning + ); let flags = TEMPLATE_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 78cfd4d22200..fb2bb1da20b3 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 @@ -77,7 +77,11 @@ export function RegularElement(node, context) { context.state.template.push({ kind: 'create_element', - args: [node.name] + args: [node.name], + metadata: { + svg: node.metadata.svg, + mathml: node.metadata.mathml + } }); /** @type {Array} */ @@ -118,7 +122,12 @@ export function RegularElement(node, context) { if (value.type === 'Literal' && typeof value.value === 'string') { context.state.template.push({ kind: 'set_prop', - args: ['is', escape_html(value.value, true)] + args: [ + 'is', + context.state.prevent_template_cloning + ? value.value + : escape_html(value.value, true) + ] }); continue; } @@ -300,7 +309,13 @@ export function RegularElement(node, context) { args: [attribute.name].concat( is_boolean_attribute(name) && value === true ? [] - : [value === true ? '' : escape_html(value, true)] + : [ + value === true + ? '' + : context.state.prevent_template_cloning + ? value + : escape_html(value, true) + ] ) }); } @@ -370,7 +385,8 @@ export function RegularElement(node, context) { state.metadata.namespace, state, node.name === 'script' || state.preserve_whitespace, - state.options.preserveComments + state.options.preserveComments, + state.prevent_template_cloning ); /** @type {typeof state} */ @@ -414,10 +430,16 @@ export function RegularElement(node, context) { arg = b.member(arg, 'content'); } - process_children(trimmed, (is_text) => b.call('$.child', arg, is_text && b.true), true, { - ...context, - state: child_state - }); + process_children( + trimmed, + (is_text) => b.call('$.child', arg, is_text && b.true), + true, + { + ...context, + state: child_state + }, + context.state.prevent_template_cloning + ); if (needs_reset) { child_state.init.push(b.stmt(b.call('$.reset', context.state.node))); 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 025d39b2e91f..6ec6b5322c58 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 @@ -15,8 +15,15 @@ import { build_template_chunk } from './utils.js'; * @param {(is_text: boolean) => Expression} initial * @param {boolean} is_element * @param {ComponentContext} context + * @param {boolean} [prevent_template_cloning] */ -export function process_children(nodes, initial, is_element, { visit, state }) { +export function process_children( + nodes, + initial, + is_element, + { visit, state }, + prevent_template_cloning +) { const within_bound_contenteditable = state.metadata.bound_contenteditable; let prev = initial; let skipped = 0; @@ -66,7 +73,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) { skipped += 1; state.template.push({ kind: 'create_text', - args: [sequence.map((node) => node.raw).join('')] + args: [sequence.map((node) => (prevent_template_cloning ? node.data : node.raw)).join('')] }); return; } 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..57856ac20466 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, + // prevent template cloning should always be false on the server + 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..ff7115f9610a 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, + // prevent template cloning should always be false on the server + 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..e1ef6ef4c9b4 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} [prevent_template_cloning] */ 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, + prevent_template_cloning ) { if (!state.analysis.runes) { nodes = sort_const_tags(nodes, state); @@ -272,11 +274,15 @@ 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' &&
+		(parent.name === 'pre' || (prevent_template_cloning && 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) {
+			if (text === tmp || prevent_template_cloning) {
 				first.data = text;
 				first.raw = first.raw.replace(regex_starts_with_newline, '');
 				if (first.data === '') {
diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js
index de2df62c927f..bd4d60837d62 100644
--- a/packages/svelte/src/internal/client/dom/template.js
+++ b/packages/svelte/src/internal/client/dom/template.js
@@ -64,6 +64,47 @@ export function template(content, flags) {
 	};
 }
 
+/**
+ * @param {()=>(DocumentFragment | Node)} fn
+ * @param {number} flags
+ * @returns {() => Node | Node[]}
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function template_fn(fn, 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 = fn();
+			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 +116,16 @@ export function template_with_script(content, flags) {
 	return () => run_scripts(/** @type {Element | DocumentFragment} */ (fn()));
 }
 
+/**
+ * @param {()=>(DocumentFragment | Node)} fn
+ * @param {number} flags
+ * @returns {() => Node | Node[]}
+ */ /*#__NO_SIDE_EFFECTS__*/
+export function template_with_script_fn(fn, flags) {
+	var templated_fn = template_fn(fn, flags);
+	return () => run_scripts(/** @type {Element | DocumentFragment} */ (templated_fn()));
+}
+
 /**
  * @param {string} content
  * @param {number} flags
@@ -130,6 +181,53 @@ export function ns_template(content, flags, ns = 'svg') {
 	};
 }
 
+/**
+ * @param {()=>(DocumentFragment | Node)} fn
+ * @param {number} flags
+ * @param {'svg' | 'math'} ns
+ * @returns {() => Node | Node[]}
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function ns_template_fn(fn, 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 = /** @type {DocumentFragment} */ (fn());
+
+			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 +239,17 @@ export function svg_template_with_script(content, flags) {
 	return () => run_scripts(/** @type {Element | DocumentFragment} */ (fn()));
 }
 
+/**
+ * @param {()=>(DocumentFragment | Node)} fn
+ * @param {number} flags
+ * @returns {() => Node | Node[]}
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function svg_template_with_script_fn(fn, flags) {
+	var templated_fn = ns_template_fn(fn, flags);
+	return () => run_scripts(/** @type {Element | DocumentFragment} */ (templated_fn()));
+}
+
 /**
  * @param {string} content
  * @param {number} flags
@@ -151,6 +260,16 @@ export function mathml_template(content, flags) {
 	return ns_template(content, flags, 'math');
 }
 
+/**
+ * @param {()=>(DocumentFragment | Node)} fn
+ * @param {number} flags
+ * @returns {() => Node | Node[]}
+ */
+/*#__NO_SIDE_EFFECTS__*/
+export function mathml_template_fn(fn, flags) {
+	return ns_template_fn(fn, 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 31da00dbb448..018d0529a18a 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';

From 8424395ac427ce545f8625f58d1bc41ad44bd9ba Mon Sep 17 00:00:00 2001
From: paoloricciuti 
Date: Thu, 20 Mar 2025 10:40:12 +0100
Subject: [PATCH 05/17] chore: remove `prevent-template-cloning` test

---
 .../prevent-template-cloning/_config.js       |  7 ---
 .../_expected/client/index.svelte.js          | 49 -------------------
 .../_expected/server/index.svelte.js          | 14 ------
 .../prevent-template-cloning/index.svelte     | 16 ------
 4 files changed, 86 deletions(-)
 delete mode 100644 packages/svelte/tests/snapshot/samples/prevent-template-cloning/_config.js
 delete mode 100644 packages/svelte/tests/snapshot/samples/prevent-template-cloning/_expected/client/index.svelte.js
 delete mode 100644 packages/svelte/tests/snapshot/samples/prevent-template-cloning/_expected/server/index.svelte.js
 delete mode 100644 packages/svelte/tests/snapshot/samples/prevent-template-cloning/index.svelte

diff --git a/packages/svelte/tests/snapshot/samples/prevent-template-cloning/_config.js b/packages/svelte/tests/snapshot/samples/prevent-template-cloning/_config.js
deleted file mode 100644
index e37acdeaf292..000000000000
--- a/packages/svelte/tests/snapshot/samples/prevent-template-cloning/_config.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { test } from '../../test';
-
-export default test({
-	compileOptions: {
-		preventTemplateCloning: true
-	}
-});
diff --git a/packages/svelte/tests/snapshot/samples/prevent-template-cloning/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/prevent-template-cloning/_expected/client/index.svelte.js
deleted file mode 100644
index 074de003b4ec..000000000000
--- a/packages/svelte/tests/snapshot/samples/prevent-template-cloning/_expected/client/index.svelte.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import 'svelte/internal/disclose-version';
-import * as $ from 'svelte/internal/client';
-
-function increment(_, counter) {
-	counter.count += 1;
-}
-
-var root = () => {
-	var button = document.createElement('button');
-	var text = document.createTextNode(' ');
-
-	button.insertBefore(text, undefined)
-
-	var text_1 = document.createTextNode(' ');
-	var comment = document.createComment('');
-	var text_2 = document.createTextNode(' ');
-	var fragment = document.createDocumentFragment();
-
-	fragment.append(button, text_1, comment, text_2)
-	return fragment;
-};
-
-export default function Prevent_template_cloning($$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/prevent-template-cloning/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/prevent-template-cloning/_expected/server/index.svelte.js
deleted file mode 100644
index 2f5584e5f473..000000000000
--- a/packages/svelte/tests/snapshot/samples/prevent-template-cloning/_expected/server/index.svelte.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import * as $ from 'svelte/internal/server';
-
-export default function Prevent_template_cloning($$payload) {
-	let counter = { count: 0 };
-	const promise = Promise.resolve(counter);
-
-	function increment() {
-		counter.count += 1;
-	}
-
-	$$payload.out += ` `;
-	$.await(promise, () => {}, (counter) => {}, () => {});
-	$$payload.out += ` ${$.escape(counter.count)}`;
-}
\ No newline at end of file
diff --git a/packages/svelte/tests/snapshot/samples/prevent-template-cloning/index.svelte b/packages/svelte/tests/snapshot/samples/prevent-template-cloning/index.svelte
deleted file mode 100644
index 3dc6c6626283..000000000000
--- a/packages/svelte/tests/snapshot/samples/prevent-template-cloning/index.svelte
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-{#await promise then counter}{/await}
-
-{counter.count}

From c75c42949b3364d74089f1912900a7901f4079b4 Mon Sep 17 00:00:00 2001
From: paoloricciuti 
Date: Thu, 20 Mar 2025 14:45:32 +0100
Subject: [PATCH 06/17] chore: run test suite on both `functional` and `string`
 templating

---
 packages/svelte/package.json                  |   2 +-
 packages/svelte/src/internal/server/dev.js    |   2 +-
 packages/svelte/tests/helpers.js              |   9 +-
 packages/svelte/tests/hydration/test.ts       |  29 +-
 packages/svelte/tests/runtime-browser/test.ts |  44 ++-
 .../main.svelte                               |   5 +-
 .../main.svelte                               |   6 +-
 .../window-binding-scroll-store/_config.js    |   3 +-
 .../svelte/tests/runtime-legacy/shared.ts     |  53 +++-
 packages/svelte/tests/runtime-legacy/test.ts  |   3 +-
 .../custom-element-attributes/main.svelte     |  26 +-
 packages/svelte/tests/runtime-runes/test.ts   |   3 +-
 .../client-functional/index.svelte.js         |  52 ++++
 .../client-functional/index.svelte.js         |  45 +++
 .../client-functional/index.svelte.js         |   7 +
 .../client-functional/index.svelte.js         |  27 ++
 .../client-functional/index.svelte.js         |  16 +
 .../client-functional/main.svelte.js          |  53 ++++
 .../client-functional/index.svelte.js         |  19 ++
 .../client-functional/index.svelte.js         |   4 +
 .../client-functional/index.svelte.js         |  27 ++
 .../client-functional/index.svelte.js         |  21 ++
 .../client-functional/index.svelte.js         |  32 ++
 .../_expected/client-functional/export.js     |   1 +
 .../client-functional/index.svelte.js         |   8 +
 .../client-functional/module.svelte.js        |   5 +
 .../client-functional/index.svelte.js         |  54 ++++
 .../client-functional/index.svelte.js         |  17 ++
 .../client-functional/index.svelte.js         |  34 +++
 .../client-functional/index.svelte.js         | 283 ++++++++++++++++++
 .../client-functional/index.svelte.js         |  50 ++++
 .../client-functional/index.svelte.js         |  11 +
 .../client-functional/index.svelte.js         |  34 +++
 packages/svelte/tests/snapshot/test.ts        |  14 +-
 packages/svelte/tests/suite.ts                |  42 ++-
 pnpm-lock.yaml                                |   2 +-
 36 files changed, 975 insertions(+), 68 deletions(-)
 create mode 100644 packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client-functional/index.svelte.js
 create mode 100644 packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client-functional/index.svelte.js
 create mode 100644 packages/svelte/tests/snapshot/samples/bind-this/_expected/client-functional/index.svelte.js
 create mode 100644 packages/svelte/tests/snapshot/samples/class-state-field-constructor-assignment/_expected/client-functional/index.svelte.js
 create mode 100644 packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client-functional/index.svelte.js
 create mode 100644 packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client-functional/main.svelte.js
 create mode 100644 packages/svelte/tests/snapshot/samples/each-string-template/_expected/client-functional/index.svelte.js
 create mode 100644 packages/svelte/tests/snapshot/samples/export-state/_expected/client-functional/index.svelte.js
 create mode 100644 packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client-functional/index.svelte.js
 create mode 100644 packages/svelte/tests/snapshot/samples/hello-world/_expected/client-functional/index.svelte.js
 create mode 100644 packages/svelte/tests/snapshot/samples/hmr/_expected/client-functional/index.svelte.js
 create mode 100644 packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client-functional/export.js
 create mode 100644 packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client-functional/index.svelte.js
 create mode 100644 packages/svelte/tests/snapshot/samples/imports-in-modules/_expected/client-functional/module.svelte.js
 create mode 100644 packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client-functional/index.svelte.js
 create mode 100644 packages/svelte/tests/snapshot/samples/props-identifier/_expected/client-functional/index.svelte.js
 create mode 100644 packages/svelte/tests/snapshot/samples/purity/_expected/client-functional/index.svelte.js
 create mode 100644 packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client-functional/index.svelte.js
 create mode 100644 packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client-functional/index.svelte.js
 create mode 100644 packages/svelte/tests/snapshot/samples/svelte-element/_expected/client-functional/index.svelte.js
 create mode 100644 packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client-functional/index.svelte.js

diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index 6f10b2a9ea60..9e9f5c8a8d8a 100644
--- a/packages/svelte/package.json
+++ b/packages/svelte/package.json
@@ -149,9 +149,9 @@
   "dependencies": {
     "@ampproject/remapping": "^2.3.0",
     "@jridgewell/sourcemap-codec": "^1.5.0",
+    "@sveltejs/acorn-typescript": "^1.0.5",
     "@types/estree": "^1.0.5",
     "acorn": "^8.12.1",
-    "@sveltejs/acorn-typescript": "^1.0.5",
     "aria-query": "^5.3.1",
     "axobject-query": "^4.1.0",
     "clsx": "^2.1.1",
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..164d855e89f4 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' ? `-${templating_mode}` : ''}`;
 
 	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,
+			preventTemplateCloning: templating_mode === 'functional'
 		};
 
 		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..0f7b259715b4 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,
+							preventTemplateCloning: templating_mode === 'functional'
 						});
 
-						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,
+								preventTemplateCloning: templating_mode === 'functional'
 							});
 
 							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..8acac668f905 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,
+		preventTemplateCloning: templating_mode === 'functional'
 	};
 
 	// 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, {
@@ -289,11 +308,17 @@ async function run_test_variant(
 				idPrefix: config.id_prefix
 			});
 
-			fs.writeFileSync(`${cwd}/_output/rendered.html`, html);
+			fs.writeFileSync(
+				`${cwd}/_output/rendered${templating_mode === 'functional' ? '-functional' : ''}.html`,
+				html
+			);
 			target.innerHTML = html;
 
 			if (head) {
-				fs.writeFileSync(`${cwd}/_output/rendered_head.html`, head);
+				fs.writeFileSync(
+					`${cwd}/_output/rendered_head${templating_mode === 'functional' ? '-functional' : ''}.html`,
+					head
+				);
 				window.document.head.innerHTML = window.document.head.innerHTML + head;
 			}
 
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..262454e9c329
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/await-block-scope/_expected/client-functional/index.svelte.js
@@ -0,0 +1,52 @@
+import 'svelte/internal/disclose-version';
+import * as $ from 'svelte/internal/client';
+
+function increment(_, counter) {
+	counter.count += 1;
+}
+
+var root = $.template_fn(
+	() => {
+		var button = document.createElement('button');
+		var text = document.createTextNode(' ');
+
+		button.insertBefore(text, undefined)
+
+		var text_1 = document.createTextNode(' ');
+		var comment = document.createComment('');
+		var text_2 = document.createTextNode(' ');
+		var fragment = document.createDocumentFragment();
+
+		fragment.append(button, text_1, comment, text_2)
+		return fragment;
+	},
+	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..e4b1e2d1e0e0
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client-functional/index.svelte.js
@@ -0,0 +1,45 @@
+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(
+	() => {
+		var comment = document.createComment('');
+		var comment_1 = document.createComment('');
+		var text = document.createTextNode(' ');
+		var fragment = document.createDocumentFragment();
+
+		fragment.append(comment, comment_1, text)
+		return fragment;
+	},
+	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, $.proxy($$value));
+		}
+	});
+
+	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..2898f31a6fb5
--- /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, $.proxy(value));
+		}
+
+		#b = $.state();
+
+		constructor() {
+			this.a = 1;
+			this.#b.v = 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..9651713c52f5
--- /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, $.proxy(array[0])),
+		$.set(b, $.proxy(array[1]))
+	);
+
+	[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..c05184f1f82a
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client-functional/main.svelte.js
@@ -0,0 +1,53 @@
+import 'svelte/internal/disclose-version';
+import * as $ from 'svelte/internal/client';
+
+var root = $.template_fn(
+	() => {
+		var div = document.createElement('div');
+		var text = document.createTextNode(' ');
+		var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+		var text_1 = document.createTextNode(' ');
+		var custom_element = document.createElement('custom-element');
+		var text_2 = document.createTextNode(' ');
+		var div_1 = document.createElement('div');
+		var text_3 = document.createTextNode(' ');
+		var svg_1 = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+		var text_4 = document.createTextNode(' ');
+		var custom_element_1 = document.createElement('custom-element');
+		var fragment = document.createDocumentFragment();
+
+		fragment.append(div, text, svg, text_1, custom_element, text_2, div_1, text_3, svg_1, text_4, custom_element_1)
+		return fragment;
+	},
+	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..c545608bcacf
--- /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, $.proxy(plusOne($.get(count)))),
+		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..5f55a12c20c1
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/hello-world/_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(() => {
+	var h1 = document.createElement('h1');
+	var text = document.createTextNode('hello world');
+
+	h1.insertBefore(text, undefined)
+
+	var fragment = document.createDocumentFragment();
+
+	fragment.append(h1)
+	return fragment;
+});
+
+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..6196ed18bbd8
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/hmr/_expected/client-functional/index.svelte.js
@@ -0,0 +1,32 @@
+import 'svelte/internal/disclose-version';
+import 'svelte/internal/flags/legacy';
+import * as $ from 'svelte/internal/client';
+
+var root = $.template_fn(() => {
+	var h1 = document.createElement('h1');
+	var text = document.createTextNode('hello world');
+
+	h1.insertBefore(text, undefined)
+
+	var fragment = document.createDocumentFragment();
+
+	fragment.append(h1)
+	return fragment;
+});
+
+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..6b48b03635c5
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/nullish-coallescence-omittance/_expected/client-functional/index.svelte.js
@@ -0,0 +1,54 @@
+import 'svelte/internal/disclose-version';
+import * as $ from 'svelte/internal/client';
+
+var on_click = (_, count) => $.update(count);
+
+var root = $.template_fn(
+	() => {
+		var h1 = document.createElement('h1');
+		var text = document.createTextNode(' ');
+		var b = document.createElement('b');
+		var text_1 = document.createTextNode(' ');
+		var button = document.createElement('button');
+		var text_2 = document.createTextNode(' ');
+
+		button.insertBefore(text_2, undefined)
+
+		var text_3 = document.createTextNode(' ');
+		var h1_1 = document.createElement('h1');
+		var fragment = document.createDocumentFragment();
+
+		fragment.append(h1, text, b, text_1, button, text_3, h1_1)
+		return fragment;
+	},
+	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..409a0d93aa49
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/purity/_expected/client-functional/index.svelte.js
@@ -0,0 +1,34 @@
+import 'svelte/internal/disclose-version';
+import 'svelte/internal/flags/legacy';
+import * as $ from 'svelte/internal/client';
+
+var root = $.template_fn(
+	() => {
+		var p = document.createElement('p');
+		var text = document.createTextNode(' ');
+		var p_1 = document.createElement('p');
+		var text_1 = document.createTextNode(' ');
+		var comment = document.createComment('');
+		var fragment = document.createDocumentFragment();
+
+		fragment.append(p, text, p_1, text_1, comment)
+		return fragment;
+	},
+	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..8023738996ba
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/skip-static-subtree/_expected/client-functional/index.svelte.js
@@ -0,0 +1,283 @@
+import 'svelte/internal/disclose-version';
+import * as $ from 'svelte/internal/client';
+
+var root = $.template_fn(
+	() => {
+		var header = document.createElement('header');
+		var nav = document.createElement('nav');
+
+		header.insertBefore(nav, undefined)
+
+		var a = document.createElement('a');
+
+		nav.insertBefore(a, undefined)
+		a.setAttribute('href', '/')
+
+		var text = document.createTextNode('Home');
+
+		a.insertBefore(text, undefined)
+
+		var text_1 = document.createTextNode(' ');
+
+		nav.insertBefore(text_1, undefined)
+
+		var a_1 = document.createElement('a');
+
+		nav.insertBefore(a_1, undefined)
+		a_1.setAttribute('href', '/away')
+
+		var text_2 = document.createTextNode('Away');
+
+		a_1.insertBefore(text_2, undefined)
+
+		var text_3 = document.createTextNode(' ');
+		var main = document.createElement('main');
+		var h1 = document.createElement('h1');
+
+		main.insertBefore(h1, undefined)
+
+		var text_4 = document.createTextNode(' ');
+
+		h1.insertBefore(text_4, undefined)
+
+		var text_5 = document.createTextNode(' ');
+
+		main.insertBefore(text_5, undefined)
+
+		var div = document.createElement('div');
+
+		main.insertBefore(div, undefined)
+		div.setAttribute('class', 'static')
+
+		var p = document.createElement('p');
+
+		div.insertBefore(p, undefined)
+
+		var text_6 = document.createTextNode('we don\'t need to traverse these nodes');
+
+		p.insertBefore(text_6, undefined)
+
+		var text_7 = document.createTextNode(' ');
+
+		main.insertBefore(text_7, undefined)
+
+		var p_1 = document.createElement('p');
+
+		main.insertBefore(p_1, undefined)
+
+		var text_8 = document.createTextNode('or');
+
+		p_1.insertBefore(text_8, undefined)
+
+		var text_9 = document.createTextNode(' ');
+
+		main.insertBefore(text_9, undefined)
+
+		var p_2 = document.createElement('p');
+
+		main.insertBefore(p_2, undefined)
+
+		var text_10 = document.createTextNode('these');
+
+		p_2.insertBefore(text_10, undefined)
+
+		var text_11 = document.createTextNode(' ');
+
+		main.insertBefore(text_11, undefined)
+
+		var p_3 = document.createElement('p');
+
+		main.insertBefore(p_3, undefined)
+
+		var text_12 = document.createTextNode('ones');
+
+		p_3.insertBefore(text_12, undefined)
+
+		var text_13 = document.createTextNode(' ');
+
+		main.insertBefore(text_13, undefined)
+
+		var comment = document.createComment('');
+
+		main.insertBefore(comment, undefined)
+
+		var text_14 = document.createTextNode(' ');
+
+		main.insertBefore(text_14, undefined)
+
+		var p_4 = document.createElement('p');
+
+		main.insertBefore(p_4, undefined)
+
+		var text_15 = document.createTextNode('these');
+
+		p_4.insertBefore(text_15, undefined)
+
+		var text_16 = document.createTextNode(' ');
+
+		main.insertBefore(text_16, undefined)
+
+		var p_5 = document.createElement('p');
+
+		main.insertBefore(p_5, undefined)
+
+		var text_17 = document.createTextNode('trailing');
+
+		p_5.insertBefore(text_17, undefined)
+
+		var text_18 = document.createTextNode(' ');
+
+		main.insertBefore(text_18, undefined)
+
+		var p_6 = document.createElement('p');
+
+		main.insertBefore(p_6, undefined)
+
+		var text_19 = document.createTextNode('nodes');
+
+		p_6.insertBefore(text_19, undefined)
+
+		var text_20 = document.createTextNode(' ');
+
+		main.insertBefore(text_20, undefined)
+
+		var p_7 = document.createElement('p');
+
+		main.insertBefore(p_7, undefined)
+
+		var text_21 = document.createTextNode('can');
+
+		p_7.insertBefore(text_21, undefined)
+
+		var text_22 = document.createTextNode(' ');
+
+		main.insertBefore(text_22, undefined)
+
+		var p_8 = document.createElement('p');
+
+		main.insertBefore(p_8, undefined)
+
+		var text_23 = document.createTextNode('be');
+
+		p_8.insertBefore(text_23, undefined)
+
+		var text_24 = document.createTextNode(' ');
+
+		main.insertBefore(text_24, undefined)
+
+		var p_9 = document.createElement('p');
+
+		main.insertBefore(p_9, undefined)
+
+		var text_25 = document.createTextNode('completely');
+
+		p_9.insertBefore(text_25, undefined)
+
+		var text_26 = document.createTextNode(' ');
+
+		main.insertBefore(text_26, undefined)
+
+		var p_10 = document.createElement('p');
+
+		main.insertBefore(p_10, undefined)
+
+		var text_27 = document.createTextNode('ignored');
+
+		p_10.insertBefore(text_27, undefined)
+
+		var text_28 = document.createTextNode(' ');
+		var cant_skip = document.createElement('cant-skip');
+		var custom_elements = document.createElement('custom-elements');
+
+		cant_skip.insertBefore(custom_elements, undefined)
+
+		var text_29 = document.createTextNode(' ');
+		var div_1 = document.createElement('div');
+		var input = document.createElement('input');
+
+		div_1.insertBefore(input, undefined)
+
+		var text_30 = document.createTextNode(' ');
+		var div_2 = document.createElement('div');
+		var source = document.createElement('source');
+
+		div_2.insertBefore(source, undefined)
+
+		var text_31 = document.createTextNode(' ');
+		var select = document.createElement('select');
+		var option = document.createElement('option');
+
+		select.insertBefore(option, undefined)
+
+		var text_32 = document.createTextNode('a');
+
+		option.insertBefore(text_32, undefined)
+
+		var text_33 = document.createTextNode(' ');
+		var img = document.createElement('img');
+
+		img.setAttribute('src', '...')
+		img.setAttribute('alt', '')
+		img.setAttribute('loading', 'lazy')
+
+		var text_34 = document.createTextNode(' ');
+		var div_3 = document.createElement('div');
+		var img_1 = document.createElement('img');
+
+		div_3.insertBefore(img_1, undefined)
+		img_1.setAttribute('src', '...')
+		img_1.setAttribute('alt', '')
+		img_1.setAttribute('loading', 'lazy')
+
+		var fragment = document.createDocumentFragment();
+
+		fragment.append(header, text_3, main, text_28, cant_skip, text_29, div_1, text_30, div_2, text_31, select, text_33, img, text_34, div_3)
+		return fragment;
+	},
+	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..4247dcfda3f5
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client-functional/index.svelte.js
@@ -0,0 +1,50 @@
+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(
+	() => {
+		var input = document.createElement('input');
+		var text = document.createTextNode(' ');
+		var input_1 = document.createElement('input');
+		var text_1 = document.createTextNode(' ');
+		var button = document.createElement('button');
+		var text_2 = document.createTextNode('reset');
+
+		button.insertBefore(text_2, undefined)
+
+		var fragment = document.createDocumentFragment();
+
+		fragment.append(input, text, input_1, text_1, button)
+		return fragment;
+	},
+	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..5163dc350967
--- /dev/null
+++ b/packages/svelte/tests/snapshot/samples/text-nodes-deriveds/_expected/client-functional/index.svelte.js
@@ -0,0 +1,34 @@
+import 'svelte/internal/disclose-version';
+import * as $ from 'svelte/internal/client';
+
+var root = $.template_fn(() => {
+	var p = document.createElement('p');
+	var text = document.createTextNode(' ');
+
+	p.insertBefore(text, undefined)
+
+	var fragment = document.createDocumentFragment();
+
+	fragment.append(p)
+	return fragment;
+});
+
+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..bbcee10d8595 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
@@ -41,4 +48,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/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==}

From 64219ed5e4ed14fd848d6d24bc53d3deff295a39 Mon Sep 17 00:00:00 2001
From: paoloricciuti 
Date: Thu, 20 Mar 2025 14:55:58 +0100
Subject: [PATCH 07/17] chore: change options from `boolean` to `list`
 (templatingMode: "functional")

---
 .../phases/3-transform/client/transform-client.js    |  2 +-
 .../3-transform/client/transform-template/index.js   |  4 ++--
 .../compiler/phases/3-transform/client/types.d.ts    |  2 +-
 .../phases/3-transform/client/visitors/Fragment.js   |  8 ++++----
 .../3-transform/client/visitors/RegularElement.js    |  8 ++++----
 .../3-transform/client/visitors/shared/fragment.js   |  8 +++++---
 .../svelte/src/compiler/phases/3-transform/utils.js  |  8 ++++----
 packages/svelte/src/compiler/types/index.d.ts        |  6 +++---
 packages/svelte/src/compiler/validate-options.js     |  2 +-
 packages/svelte/tests/helpers.js                     |  2 +-
 packages/svelte/tests/runtime-browser/test.ts        |  4 ++--
 packages/svelte/tests/runtime-legacy/shared.ts       |  2 +-
 packages/svelte/types/index.d.ts                     | 12 ++++++------
 13 files changed, 35 insertions(+), 33 deletions(-)

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 bdf70408f1bd..0481621ce117 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,7 +167,7 @@ export function client_component(analysis, options) {
 		in_constructor: false,
 		instance_level_snippets: [],
 		module_level_snippets: [],
-		prevent_template_cloning: options.preventTemplateCloning,
+		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/index.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js
index 98b830a907c4..f28e2cf78ec3 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js
@@ -27,7 +27,7 @@ function get_template_function(namespace, state) {
 				: contains_script_tag
 					? '$.template_with_script'
 					: '$.template'
-	).concat(state.prevent_template_cloning ? '_fn' : '');
+	).concat(state.is_functional_template_mode ? '_fn' : '');
 }
 
 /**
@@ -75,7 +75,7 @@ export function transform_template(state, context, namespace, template_name, fla
 
 	/** @type {Expression[]} */
 	const args = [
-		state.prevent_template_cloning
+		state.is_functional_template_mode
 			? template_to_functions(state.template, namespace)
 			: b.template([b.quasi(template_to_string(state.template), true)], [])
 	];
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 a73e0e0e8358..fb20ebe79fe8 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
@@ -94,7 +94,7 @@ export interface ComponentClientTransformState extends ClientTransformState {
 		};
 	};
 	readonly preserve_whitespace: boolean;
-	readonly prevent_template_cloning?: 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/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js
index 98f8b0c2e96b..12abc8e9e0d4 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
@@ -37,7 +37,7 @@ export function Fragment(node, context) {
 		context.state,
 		context.state.preserve_whitespace,
 		context.state.options.preserveComments,
-		context.state.prevent_template_cloning
+		context.state.is_functional_template_mode
 	);
 
 	if (hoisted.length === 0 && trimmed.length === 0) {
@@ -133,7 +133,7 @@ export function Fragment(node, context) {
 					...context,
 					state
 				},
-				context.state.prevent_template_cloning
+				context.state.is_functional_template_mode
 			);
 
 			body.push(b.var(id, b.call('$.text')));
@@ -146,7 +146,7 @@ export function Fragment(node, context) {
 					() => b.id('$$anchor'),
 					false,
 					{ ...context, state },
-					context.state.prevent_template_cloning
+					context.state.is_functional_template_mode
 				);
 			} else {
 				/** @type {(is_text: boolean) => Expression} */
@@ -157,7 +157,7 @@ export function Fragment(node, context) {
 					expression,
 					false,
 					{ ...context, state },
-					context.state.prevent_template_cloning
+					context.state.is_functional_template_mode
 				);
 
 				let flags = TEMPLATE_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 fb2bb1da20b3..fd8f27bc4a84 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
@@ -124,7 +124,7 @@ export function RegularElement(node, context) {
 							kind: 'set_prop',
 							args: [
 								'is',
-								context.state.prevent_template_cloning
+								context.state.is_functional_template_mode
 									? value.value
 									: escape_html(value.value, true)
 							]
@@ -312,7 +312,7 @@ export function RegularElement(node, context) {
 								: [
 										value === true
 											? ''
-											: context.state.prevent_template_cloning
+											: context.state.is_functional_template_mode
 												? value
 												: escape_html(value, true)
 									]
@@ -386,7 +386,7 @@ export function RegularElement(node, context) {
 		state,
 		node.name === 'script' || state.preserve_whitespace,
 		state.options.preserveComments,
-		state.prevent_template_cloning
+		state.is_functional_template_mode
 	);
 
 	/** @type {typeof state} */
@@ -438,7 +438,7 @@ export function RegularElement(node, context) {
 				...context,
 				state: child_state
 			},
-			context.state.prevent_template_cloning
+			context.state.is_functional_template_mode
 		);
 
 		if (needs_reset) {
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 6ec6b5322c58..fd6f6853d03c 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
@@ -15,14 +15,14 @@ import { build_template_chunk } from './utils.js';
  * @param {(is_text: boolean) => Expression} initial
  * @param {boolean} is_element
  * @param {ComponentContext} context
- * @param {boolean} [prevent_template_cloning]
+ * @param {boolean} [is_functional_template_mode]
  */
 export function process_children(
 	nodes,
 	initial,
 	is_element,
 	{ visit, state },
-	prevent_template_cloning
+	is_functional_template_mode
 ) {
 	const within_bound_contenteditable = state.metadata.bound_contenteditable;
 	let prev = initial;
@@ -73,7 +73,9 @@ export function process_children(
 			skipped += 1;
 			state.template.push({
 				kind: 'create_text',
-				args: [sequence.map((node) => (prevent_template_cloning ? node.data : node.raw)).join('')]
+				args: [
+					sequence.map((node) => (is_functional_template_mode ? node.data : node.raw)).join('')
+				]
 			});
 			return;
 		}
diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js
index e1ef6ef4c9b4..93e4c34479f5 100644
--- a/packages/svelte/src/compiler/phases/3-transform/utils.js
+++ b/packages/svelte/src/compiler/phases/3-transform/utils.js
@@ -141,7 +141,7 @@ function sort_const_tags(nodes, state) {
  * @param {TransformState & { options: ValidatedCompileOptions }} state
  * @param {boolean} preserve_whitespace
  * @param {boolean} preserve_comments
- * @param {boolean} [prevent_template_cloning]
+ * @param {boolean} [is_functional_template_mode]
  */
 export function clean_nodes(
 	parent,
@@ -154,7 +154,7 @@ export function clean_nodes(
 	// rather than from `ClientTransformState` and `ServerTransformState`
 	preserve_whitespace,
 	preserve_comments,
-	prevent_template_cloning
+	is_functional_template_mode
 ) {
 	if (!state.analysis.runes) {
 		nodes = sort_const_tags(nodes, state);
@@ -276,13 +276,13 @@ export function clean_nodes(
 	// initial newline inside a `
` is disregarded, if not followed by another newline
 	if (
 		parent.type === 'RegularElement' &&
-		(parent.name === 'pre' || (prevent_template_cloning && parent.name === 'textarea')) &&
+		(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 || prevent_template_cloning) {
+			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 4161c7120648..4aa244ad5191 100644
--- a/packages/svelte/src/compiler/types/index.d.ts
+++ b/packages/svelte/src/compiler/types/index.d.ts
@@ -114,11 +114,11 @@ export interface CompileOptions extends ModuleCompileOptions {
 	 */
 	preserveWhitespace?: boolean;
 	/**
-	 *  If `true`, the template will get compiled to a series of `document.createElement` calls instead of using `template.innerHTML`.
+	 *  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 false
+	 * @default 'string'
 	 */
-	preventTemplateCloning?: boolean;
+	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 5e6eaf5ef6f4..1d67951fd855 100644
--- a/packages/svelte/src/compiler/validate-options.js
+++ b/packages/svelte/src/compiler/validate-options.js
@@ -110,7 +110,7 @@ export const validate_component_options =
 
 			preserveComments: boolean(false),
 
-			preventTemplateCloning: boolean(false),
+			templatingMode: list(['string', 'functional']),
 
 			preserveWhitespace: boolean(false),
 
diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js
index 164d855e89f4..684bde38750e 100644
--- a/packages/svelte/tests/helpers.js
+++ b/packages/svelte/tests/helpers.js
@@ -80,7 +80,7 @@ export async function compile_directory(
 			filename: path.join(cwd, file),
 			...compileOptions,
 			generate,
-			preventTemplateCloning: templating_mode === 'functional'
+			templatingMode: templating_mode
 		};
 
 		if (file.endsWith('.js')) {
diff --git a/packages/svelte/tests/runtime-browser/test.ts b/packages/svelte/tests/runtime-browser/test.ts
index 0f7b259715b4..02823ad87ba6 100644
--- a/packages/svelte/tests/runtime-browser/test.ts
+++ b/packages/svelte/tests/runtime-browser/test.ts
@@ -106,7 +106,7 @@ async function run_test(
 							immutable: config.immutable,
 							customElement: test_dir.includes('custom-elements-samples'),
 							accessors: 'accessors' in config ? config.accessors : true,
-							preventTemplateCloning: templating_mode === 'functional'
+							templatingMode: templating_mode
 						});
 
 						write(
@@ -171,7 +171,7 @@ async function run_test(
 								immutable: config.immutable,
 								customElement: test_dir.includes('custom-elements-samples'),
 								accessors: 'accessors' in config ? config.accessors : true,
-								preventTemplateCloning: templating_mode === 'functional'
+								templatingMode: templating_mode
 							});
 
 							return {
diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts
index 8acac668f905..4630a4924cd9 100644
--- a/packages/svelte/tests/runtime-legacy/shared.ts
+++ b/packages/svelte/tests/runtime-legacy/shared.ts
@@ -168,7 +168,7 @@ async function common_setup(
 		immutable: config.immutable,
 		accessors: 'accessors' in config ? config.accessors : true,
 		runes,
-		preventTemplateCloning: templating_mode === 'functional'
+		templatingMode: templating_mode
 	};
 
 	// load_compiled can be used for debugging a test. It means the compiler will not run on the input
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index ef0179f3f175..19bb6e9279b6 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -845,11 +845,11 @@ declare module 'svelte/compiler' {
 		 */
 		preserveWhitespace?: boolean;
 		/**
-		 *  If `true`, the template will get compiled to a series of `document.createElement` calls instead of using `template.innerHTML`.
+		 *  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 false
+		 * @default 'string'
 		 */
-		preventTemplateCloning?: boolean;
+		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.
@@ -2561,11 +2561,11 @@ declare module 'svelte/types/compiler/interfaces' {
 		 */
 		preserveWhitespace?: boolean;
 		/**
-		 *  If `true`, the template will get compiled to a series of `document.createElement` calls instead of using `template.innerHTML`.
+		 *  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 false
+		 * @default 'string'
 		 */
-		preventTemplateCloning?: boolean;
+		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.

From 95ce311613e2f08da1637b995812ee87bfcbb029 Mon Sep 17 00:00:00 2001
From: paoloricciuti 
Date: Thu, 20 Mar 2025 14:58:05 +0100
Subject: [PATCH 08/17] chore: revert unneeded change to `package.json`

---
 packages/svelte/package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/svelte/package.json b/packages/svelte/package.json
index 9e9f5c8a8d8a..6f10b2a9ea60 100644
--- a/packages/svelte/package.json
+++ b/packages/svelte/package.json
@@ -149,9 +149,9 @@
   "dependencies": {
     "@ampproject/remapping": "^2.3.0",
     "@jridgewell/sourcemap-codec": "^1.5.0",
-    "@sveltejs/acorn-typescript": "^1.0.5",
     "@types/estree": "^1.0.5",
     "acorn": "^8.12.1",
+    "@sveltejs/acorn-typescript": "^1.0.5",
     "aria-query": "^5.3.1",
     "axobject-query": "^4.1.0",
     "clsx": "^2.1.1",

From 59902ccead473e054b158a4f806b60e34995d733 Mon Sep 17 00:00:00 2001
From: paoloricciuti 
Date: Thu, 20 Mar 2025 15:06:17 +0100
Subject: [PATCH 09/17] chore: simplify `process_children`

---
 .../3-transform/client/visitors/Fragment.js   | 31 +++++--------------
 .../client/visitors/RegularElement.js         | 17 ++++------
 .../client/visitors/shared/fragment.js        | 13 +++-----
 .../3-transform/server/visitors/Fragment.js   |  2 +-
 .../server/visitors/RegularElement.js         |  2 +-
 5 files changed, 19 insertions(+), 46 deletions(-)

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 12abc8e9e0d4..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
@@ -69,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,
@@ -125,40 +126,22 @@ export function Fragment(node, context) {
 			// special case — we can use `$.text` instead of creating a unique template
 			const id = b.id(context.state.scope.generate('text'));
 
-			process_children(
-				trimmed,
-				() => id,
-				false,
-				{
-					...context,
-					state
-				},
-				context.state.is_functional_template_mode
-			);
+			process_children(trimmed, () => id, false, {
+				...context,
+				state
+			});
 
 			body.push(b.var(id, b.call('$.text')));
 			close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
 		} else {
 			if (is_standalone) {
 				// no need to create a template, we can just use the existing block's anchor
-				process_children(
-					trimmed,
-					() => b.id('$$anchor'),
-					false,
-					{ ...context, state },
-					context.state.is_functional_template_mode
-				);
+				process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state });
 			} else {
 				/** @type {(is_text: boolean) => Expression} */
 				const expression = (is_text) => b.call('$.first_child', id, is_text && b.true);
 
-				process_children(
-					trimmed,
-					expression,
-					false,
-					{ ...context, state },
-					context.state.is_functional_template_mode
-				);
+				process_children(trimmed, expression, false, { ...context, state });
 
 				let flags = TEMPLATE_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 fd8f27bc4a84..db2ddf7fc238 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
@@ -375,7 +375,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(
@@ -430,16 +431,10 @@ export function RegularElement(node, context) {
 			arg = b.member(arg, 'content');
 		}
 
-		process_children(
-			trimmed,
-			(is_text) => b.call('$.child', arg, is_text && b.true),
-			true,
-			{
-				...context,
-				state: child_state
-			},
-			context.state.is_functional_template_mode
-		);
+		process_children(trimmed, (is_text) => b.call('$.child', arg, is_text && b.true), true, {
+			...context,
+			state: child_state
+		});
 
 		if (needs_reset) {
 			child_state.init.push(b.stmt(b.call('$.reset', context.state.node)));
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 fd6f6853d03c..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
@@ -15,15 +15,8 @@ import { build_template_chunk } from './utils.js';
  * @param {(is_text: boolean) => Expression} initial
  * @param {boolean} is_element
  * @param {ComponentContext} context
- * @param {boolean} [is_functional_template_mode]
  */
-export function process_children(
-	nodes,
-	initial,
-	is_element,
-	{ visit, state },
-	is_functional_template_mode
-) {
+export function process_children(nodes, initial, is_element, { visit, state }) {
 	const within_bound_contenteditable = state.metadata.bound_contenteditable;
 	let prev = initial;
 	let skipped = 0;
@@ -74,7 +67,9 @@ export function process_children(
 			state.template.push({
 				kind: 'create_text',
 				args: [
-					sequence.map((node) => (is_functional_template_mode ? node.data : node.raw)).join('')
+					sequence
+						.map((node) => (state.is_functional_template_mode ? node.data : node.raw))
+						.join('')
 				]
 			});
 			return;
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 57856ac20466..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
@@ -20,7 +20,7 @@ export function Fragment(node, context) {
 		context.state,
 		context.state.preserve_whitespace,
 		context.state.options.preserveComments,
-		// prevent template cloning should always be false on the server
+		// templating mode doesn't affect server builds
 		false
 	);
 
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 ff7115f9610a..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
@@ -48,7 +48,7 @@ export function RegularElement(node, context) {
 		},
 		state.preserve_whitespace,
 		state.options.preserveComments,
-		// prevent template cloning should always be false on the server
+		// templating mode doesn't affect server builds
 		false
 	);
 

From 403b17e944ae9f9c37db481aab81e93a2c1ff7b0 Mon Sep 17 00:00:00 2001
From: paoloricciuti 
Date: Thu, 20 Mar 2025 15:44:23 +0100
Subject: [PATCH 10/17] chore: update snapshots

---
 .../_expected/client-functional/index.svelte.js               | 2 +-
 .../_expected/client-functional/index.svelte.js               | 4 ++--
 .../_expected/client-functional/index.svelte.js               | 4 ++--
 .../_expected/client-functional/index.svelte.js               | 2 +-
 4 files changed, 6 insertions(+), 6 deletions(-)

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
index e4b1e2d1e0e0..8c15aa74ba1f 100644
--- 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
@@ -34,7 +34,7 @@ export default function Bind_component_snippet($$anchor) {
 			return $.get(value);
 		},
 		set value($$value) {
-			$.set(value, $.proxy($$value));
+			$.set(value, $$value, true);
 		}
 	});
 
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
index 2898f31a6fb5..21339741761f 100644
--- 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
@@ -12,14 +12,14 @@ export default function Class_state_field_constructor_assignment($$anchor, $$pro
 		}
 
 		set a(value) {
-			$.set(this.#a, $.proxy(value));
+			$.set(this.#a, value, true);
 		}
 
 		#b = $.state();
 
 		constructor() {
 			this.a = 1;
-			this.#b.v = 2;
+			$.set(this.#b, 2);
 		}
 	}
 
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
index 9651713c52f5..47f297bce9c7 100644
--- 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
@@ -8,8 +8,8 @@ let d = 4;
 
 export function update(array) {
 	(
-		$.set(a, $.proxy(array[0])),
-		$.set(b, $.proxy(array[1]))
+		$.set(a, array[0], true),
+		$.set(b, array[1], true)
 	);
 
 	[c, d] = array;
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
index c545608bcacf..762a23754c9b 100644
--- 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
@@ -13,7 +13,7 @@ export default function Function_prop_no_getter($$anchor) {
 	Button($$anchor, {
 		onmousedown: () => $.set(count, $.get(count) + 1),
 		onmouseup,
-		onmouseenter: () => $.set(count, $.proxy(plusOne($.get(count)))),
+		onmouseenter: () => $.set(count, plusOne($.get(count)), true),
 		children: ($$anchor, $$slotProps) => {
 			$.next();
 

From ba237c7f507e03781025418703bae1dd0754cb00 Mon Sep 17 00:00:00 2001
From: paoloricciuti 
Date: Thu, 20 Mar 2025 15:55:37 +0100
Subject: [PATCH 11/17] chore: don't write different `rendered`

---
 packages/svelte/tests/runtime-legacy/shared.ts | 10 ++--------
 1 file changed, 2 insertions(+), 8 deletions(-)

diff --git a/packages/svelte/tests/runtime-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts
index 4630a4924cd9..33bf3d1ccbec 100644
--- a/packages/svelte/tests/runtime-legacy/shared.ts
+++ b/packages/svelte/tests/runtime-legacy/shared.ts
@@ -308,17 +308,11 @@ async function run_test_variant(
 				idPrefix: config.id_prefix
 			});
 
-			fs.writeFileSync(
-				`${cwd}/_output/rendered${templating_mode === 'functional' ? '-functional' : ''}.html`,
-				html
-			);
+			fs.writeFileSync(`${cwd}/_output/rendered.html`, html);
 			target.innerHTML = html;
 
 			if (head) {
-				fs.writeFileSync(
-					`${cwd}/_output/rendered_head${templating_mode === 'functional' ? '-functional' : ''}.html`,
-					head
-				);
+				fs.writeFileSync(`${cwd}/_output/rendered_head.html`, head);
 				window.document.head.innerHTML = window.document.head.innerHTML + head;
 			}
 

From 4daa63a8c9866f261080069c73ddf3260150f2cb Mon Sep 17 00:00:00 2001
From: paoloricciuti 
Date: Thu, 20 Mar 2025 16:10:13 +0100
Subject: [PATCH 12/17] fix: snapshot test runner

---
 packages/svelte/tests/helpers.js       |  2 +-
 packages/svelte/tests/snapshot/test.ts | 14 ++++++++++++--
 2 files changed, 13 insertions(+), 3 deletions(-)

diff --git a/packages/svelte/tests/helpers.js b/packages/svelte/tests/helpers.js
index 684bde38750e..5e6e1903d495 100644
--- a/packages/svelte/tests/helpers.js
+++ b/packages/svelte/tests/helpers.js
@@ -68,7 +68,7 @@ export async function compile_directory(
 	preprocessor,
 	templating_mode
 ) {
-	const output_dir = `${cwd}/_output/${generate}${templating_mode === 'functional' ? `-${templating_mode}` : ''}`;
+	const output_dir = `${cwd}/_output/${generate}${templating_mode === 'functional' ? `-functional` : ''}`;
 
 	fs.rmSync(output_dir, { recursive: true, force: true });
 
diff --git a/packages/svelte/tests/snapshot/test.ts b/packages/svelte/tests/snapshot/test.ts
index bbcee10d8595..ebf1a46daa4b 100644
--- a/packages/svelte/tests/snapshot/test.ts
+++ b/packages/svelte/tests/snapshot/test.ts
@@ -25,8 +25,18 @@ const { test, run } = suite(async (config, cwd, templating_mode) =
 		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);
 

From 72f93e361fdd141bd242f403e070bdaf30f3d063 Mon Sep 17 00:00:00 2001
From: paoloricciuti 
Date: Fri, 21 Mar 2025 09:25:29 +0100
Subject: [PATCH 13/17] chore: sprinkle comments here and there

---
 .../3-transform/client/transform-client.js    |  2 +-
 .../client/transform-template/to-functions.js | 25 +++++++++++++++++++
 .../client/transform-template/to-string.js    | 13 ++++++++++
 .../client/visitors/RegularElement.js         |  6 ++++-
 .../client/visitors/shared/component.js       |  2 ++
 .../src/compiler/phases/3-transform/utils.js  |  4 +++
 6 files changed, 50 insertions(+), 2 deletions(-)

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 0481621ce117..8016c828734e 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,7 +167,7 @@ export function client_component(analysis, options) {
 		in_constructor: false,
 		instance_level_snippets: [],
 		module_level_snippets: [],
-		is_functional_template_mode: options.templatingMode === 'functional',
+		is_functional_template_mode: true, //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/to-functions.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js
index 9437fae20e11..2126b00fde5b 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js
@@ -57,15 +57,20 @@ export function template_to_functions(items, namespace) {
 	 */
 	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') {
 			const removed = elements_stack.pop();
 			if (removed?.namespaced) {
@@ -77,6 +82,8 @@ export function template_to_functions(items, namespace) {
 			continue;
 		}
 
+		// if the inserted node is in the svg/mathml we push the namespace to the stack because we need to
+		// create with createElementNS
 		if (instruction.metadata?.svg || instruction.metadata?.mathml) {
 			namespace_stack.push(instruction.metadata.svg ? NAMESPACE_SVG : NAMESPACE_MATHML);
 		}
@@ -84,7 +91,12 @@ export function template_to_functions(items, namespace) {
 		// @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)...for all the rest
+				// the first prop is a the scope to generate the name of the variable
 				...(instruction.kind === 'set_prop' ? [last_current_element] : [scope]),
+				// for create element we also need to add the namespace...namespaces in the stack get's precedence over
+				// the "global" namespace (and if we are in a foreignObject we default to html)
 				...(instruction.kind === 'create_element'
 					? [
 							foreign_object_count > 0
@@ -102,9 +114,12 @@ export function template_to_functions(items, namespace) {
 		);
 
 		if (value) {
+			// this will compose the body of the function
 			body.push(value.call);
 		}
 
+		// 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) {
 				const { call } = map.insert(/** @type {Element} */ (elements_stack.at(-1)), value);
@@ -112,6 +127,7 @@ export function template_to_functions(items, namespace) {
 			} else if (value) {
 				elements.push(b.id(value.name));
 			}
+			// 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);
 				if (last_current_element.element === 'foreignObject') {
@@ -120,6 +136,7 @@ export function template_to_functions(items, namespace) {
 			}
 		}
 	}
+	// every function needs to return a fragment so we create one and push all the elements there
 	const fragment = scope.generate('fragment');
 	body.push(b.var(fragment, b.call('document.createDocumentFragment')));
 	body.push(b.call(fragment + '.append', ...elements));
@@ -159,6 +176,11 @@ function create_element(scope, namespace, element) {
 	}
 	const call = b.var(name, b.call(fn, ...args));
 	/**
+	 * if there's an "is" attribute we can't just add it as a property, it needs to be
+	 * specified on creation like this `document.createElement('button', { is: 'my-button' })`
+	 *
+	 * Since the props are appended after the creation we change the generated call arguments and we push
+	 * the is attribute later on on `set_prop`
 	 * @param {string} value
 	 */
 	function add_is(value) {
@@ -208,6 +230,7 @@ function create_text(scope, value) {
  * @param {string} value
  */
 function set_prop(el, prop, value) {
+	// see comment above about the "is" attribute
 	if (prop === 'is') {
 		el.add_is(value);
 		return;
@@ -217,6 +240,7 @@ function set_prop(el, prop, value) {
 	let fn = namespace !== prop ? '.setAttributeNS' : '.setAttribute';
 	let args = [b.literal(fix_attribute_casing(prop)), b.literal(value ?? '')];
 
+	// attributes like `xlink:href` need to be set with the `xlink` namespace
 	if (namespace === 'xlink') {
 		args.unshift(b.literal('http://www.w3.org/1999/xlink'));
 	}
@@ -235,6 +259,7 @@ function set_prop(el, prop, value) {
 function insert(el, child, anchor) {
 	return {
 		call: b.call(
+			// if we have a template element we need to push into it's content rather than the element itself
 			el.name + (el.element === 'template' ? '.content' : '') + '.insertBefore',
 			b.id(child.name),
 			b.id(anchor?.name ?? 'undefined')
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-string.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-string.js
index c4a0b1565c67..ed0b73dd3ee0 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-string.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-string.js
@@ -20,10 +20,13 @@ export function template_to_string(items) {
 	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;
@@ -34,16 +37,21 @@ export function template_to_string(items) {
 		// @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);
 			}
@@ -141,7 +149,9 @@ let map = {
 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 += ``;
 		}
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 db2ddf7fc238..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
@@ -124,6 +124,8 @@ export function RegularElement(node, context) {
 							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)
@@ -312,7 +314,9 @@ export function RegularElement(node, context) {
 								: [
 										value === true
 											? ''
-											: context.state.is_functional_template_mode
+											: // 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)
 									]
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 90fe4d93e188..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
@@ -427,11 +427,13 @@ export function build_component(node, component_name, context, anchor = context.
 		 */
 		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' });
diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js
index 93e4c34479f5..07beb27e022a 100644
--- a/packages/svelte/src/compiler/phases/3-transform/utils.js
+++ b/packages/svelte/src/compiler/phases/3-transform/utils.js
@@ -276,12 +276,16 @@ export function clean_nodes(
 	// initial newline inside a `
` is disregarded, if not followed by another newline
 	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, '');
+			// 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, '');

From ad56847ce9dfae2401723c0b9318ad11238a6f9a Mon Sep 17 00:00:00 2001
From: paoloricciuti 
Date: Fri, 21 Mar 2025 09:29:08 +0100
Subject: [PATCH 14/17] =?UTF-8?q?fix:=20silly=20goose=20am=20i=20?=
 =?UTF-8?q?=F0=9F=AA=BF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/compiler/phases/3-transform/client/transform-client.js  | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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 8016c828734e..0481621ce117 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,7 +167,7 @@ export function client_component(analysis, options) {
 		in_constructor: false,
 		instance_level_snippets: [],
 		module_level_snippets: [],
-		is_functional_template_mode: true, //options.templatingMode === 'functional',
+		is_functional_template_mode: options.templatingMode === 'functional',
 
 		// these are set inside the `Fragment` visitor, and cannot be used until then
 		init: /** @type {any} */ (null),

From d8afd8e46c47b4b890c970a2e90102e3973e5ea3 Mon Sep 17 00:00:00 2001
From: Rich Harris 
Date: Fri, 21 Mar 2025 16:14:10 -0400
Subject: [PATCH 15/17] fix (this broke the sandbox)

---
 .../3-transform/client/transform-template/to-functions.js      | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js
index 2126b00fde5b..b109ea334ff6 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js
@@ -3,8 +3,7 @@
  * @import { Namespace } from "#compiler"
  * @import { CallExpression, Statement } from "estree"
  */
-import { NAMESPACE_SVG } from 'svelte/internal/client';
-import { NAMESPACE_MATHML } from '../../../../../constants.js';
+import { NAMESPACE_SVG, NAMESPACE_MATHML } from '../../../../../constants.js';
 import * as b from '../../../../utils/builders.js';
 import fix_attribute_casing from './fix-attribute-casing.js';
 

From be398671cac73d7213cd6b2ab6dfb2ce882a410c Mon Sep 17 00:00:00 2001
From: Paolo Ricciuti 
Date: Tue, 1 Apr 2025 23:27:02 +0200
Subject: [PATCH 16/17] chore: alterative functional templating syntax (#15599)

* chore: alterative functional templating syntax

* fix: remove `.at(-1)`

* chore: move create elements, text, comment etc to `operations`

* chore: only use `append` to append to the fragment
---
 .../client/transform-template/index.js        |   2 +-
 .../client/transform-template/to-functions.js | 221 ++++--------
 .../src/internal/client/dom/operations.js     |  41 +++
 .../src/internal/client/dom/template.js       | 110 +++++-
 .../client-functional/index.svelte.js         |  18 +-
 .../client-functional/index.svelte.js         |  13 +-
 .../client-functional/main.svelte.js          |  30 +-
 .../client-functional/index.svelte.js         |  12 +-
 .../client-functional/index.svelte.js         |  12 +-
 .../client-functional/index.svelte.js         |  26 +-
 .../client-functional/index.svelte.js         |  15 +-
 .../client-functional/index.svelte.js         | 320 +++++-------------
 .../client-functional/index.svelte.js         |  22 +-
 .../client-functional/index.svelte.js         |  12 +-
 14 files changed, 322 insertions(+), 532 deletions(-)

diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js
index f28e2cf78ec3..becf987be915 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/index.js
@@ -76,7 +76,7 @@ export function transform_template(state, context, namespace, template_name, fla
 	/** @type {Expression[]} */
 	const args = [
 		state.is_functional_template_mode
-			? template_to_functions(state.template, namespace)
+			? template_to_functions(state.template)
 			: b.template([b.quasi(template_to_string(state.template), true)], [])
 	];
 
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js
index b109ea334ff6..ecf8151836ae 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-template/to-functions.js
@@ -1,56 +1,24 @@
 /**
  * @import { TemplateOperations } from "../types.js"
  * @import { Namespace } from "#compiler"
- * @import { CallExpression, Statement } from "estree"
+ * @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';
 
-class Scope {
-	declared = new Map();
-
-	/**
-	 * @param {string} _name
-	 */
-	generate(_name) {
-		let name = _name.replace(/[^a-zA-Z0-9_$]/g, '_').replace(/^[0-9]/, '_');
-		if (!this.declared.has(name)) {
-			this.declared.set(name, 1);
-			return name;
-		}
-		let count = this.declared.get(name);
-		this.declared.set(name, count + 1);
-		return `${name}_${count}`;
-	}
-}
-
 /**
  * @param {TemplateOperations} items
- * @param {Namespace} namespace
  */
-export function template_to_functions(items, namespace) {
-	let elements = [];
-
-	let body = [];
-
-	let scope = new Scope();
+export function template_to_functions(items) {
+	let elements = b.array([]);
 
 	/**
 	 * @type {Array}
 	 */
 	let elements_stack = [];
 
-	/**
-	 * @type {Array}
-	 */
-	let namespace_stack = [];
-
-	/**
-	 * @type {number}
-	 */
-	let foreign_object_count = 0;
-
 	/**
 	 * @type {Element | undefined}
 	 */
@@ -71,89 +39,48 @@ export function template_to_functions(items, namespace) {
 		// 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') {
-			const removed = elements_stack.pop();
-			if (removed?.namespaced) {
-				namespace_stack.pop();
-			}
-			if (removed?.element === 'foreignObject') {
-				foreign_object_count--;
-			}
+			elements_stack.pop();
 			continue;
 		}
 
-		// if the inserted node is in the svg/mathml we push the namespace to the stack because we need to
-		// create with createElementNS
-		if (instruction.metadata?.svg || instruction.metadata?.mathml) {
-			namespace_stack.push(instruction.metadata.svg ? NAMESPACE_SVG : NAMESPACE_MATHML);
-		}
-
 		// @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)...for all the rest
-				// the first prop is a the scope to generate the name of the variable
-				...(instruction.kind === 'set_prop' ? [last_current_element] : [scope]),
-				// for create element we also need to add the namespace...namespaces in the stack get's precedence over
-				// the "global" namespace (and if we are in a foreignObject we default to html)
 				...(instruction.kind === 'create_element'
-					? [
-							foreign_object_count > 0
-								? undefined
-								: namespace_stack.at(-1) ??
-									(namespace === 'svg'
-										? NAMESPACE_SVG
-										: namespace === 'mathml'
-											? NAMESPACE_MATHML
-											: undefined)
-						]
-					: []),
+					? []
+					: [instruction.kind === 'set_prop' ? last_current_element : elements_stack.at(-1)]),
 				...(instruction.args ?? [])
 			]
 		);
 
-		if (value) {
-			// this will compose the body of the function
-			body.push(value.call);
-		}
-
 		// 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) {
-				const { call } = map.insert(/** @type {Element} */ (elements_stack.at(-1)), value);
-				body.push(call);
-			} else if (value) {
-				elements.push(b.id(value.name));
+			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);
-				if (last_current_element.element === 'foreignObject') {
-					foreign_object_count++;
-				}
 			}
 		}
 	}
-	// every function needs to return a fragment so we create one and push all the elements there
-	const fragment = scope.generate('fragment');
-	body.push(b.var(fragment, b.call('document.createDocumentFragment')));
-	body.push(b.call(fragment + '.append', ...elements));
-	body.push(b.return(b.id(fragment)));
 
-	return b.arrow([], b.block(body));
+	return elements;
 }
 
 /**
- * @typedef {{ call: Statement, name: string, add_is: (value: string)=>void, namespaced: boolean; element: string; }} Element
+ * @typedef {ObjectExpression} Element
  */
 
 /**
- * @typedef {{ call: Statement, name: string }} Anchor
+ * @typedef {void | null | ArrayExpression} Anchor
  */
 
 /**
- * @typedef {{ call: Statement, name: string }} Text
+ * @typedef {void | Literal} Text
  */
 
 /**
@@ -161,109 +88,89 @@ export function template_to_functions(items, namespace) {
  */
 
 /**
- * @param {Scope} scope
- * @param {Namespace} namespace
  * @param {string} element
  * @returns {Element}
  */
-function create_element(scope, namespace, element) {
-	const name = scope.generate(element);
-	let fn = namespace != null ? 'document.createElementNS' : 'document.createElement';
-	let args = [b.literal(element)];
-	if (namespace != null) {
-		args.unshift(b.literal(namespace));
-	}
-	const call = b.var(name, b.call(fn, ...args));
-	/**
-	 * if there's an "is" attribute we can't just add it as a property, it needs to be
-	 * specified on creation like this `document.createElement('button', { is: 'my-button' })`
-	 *
-	 * Since the props are appended after the creation we change the generated call arguments and we push
-	 * the is attribute later on on `set_prop`
-	 * @param {string} value
-	 */
-	function add_is(value) {
-		/** @type {CallExpression} */ (call.declarations[0].init).arguments.push(
-			b.object([b.prop('init', b.literal('is'), b.literal(value))])
-		);
+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 {
-		call,
-		name,
-		element,
-		add_is,
-		namespaced: namespace != null
-	};
+	return /** @type {Property} */ (prop);
 }
 
 /**
- * @param {Scope} scope
+ * @param {Element} element
  * @param {string} data
  * @returns {Anchor}
  */
-function create_anchor(scope, data = '') {
-	const name = scope.generate('comment');
-	return {
-		call: b.var(name, b.call('document.createComment', b.literal(data))),
-		name
-	};
+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 {Scope} scope
+ * @param {Element} element
  * @param {string} value
  * @returns {Text}
  */
-function create_text(scope, value) {
-	const name = scope.generate('text');
-	return {
-		call: b.var(name, b.call('document.createTextNode', b.literal(value))),
-		name
-	};
+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} el
+ * @param {Element} element
  * @param {string} prop
  * @param {string} value
  */
-function set_prop(el, prop, value) {
-	// see comment above about the "is" attribute
+function set_prop(element, prop, value) {
+	const p = get_or_create_prop(element, 'p', b.object([]));
+
 	if (prop === 'is') {
-		el.add_is(value);
+		element.properties.push(b.prop('init', b.id(prop), b.literal(value)));
 		return;
 	}
 
-	const [namespace] = prop.split(':');
-	let fn = namespace !== prop ? '.setAttributeNS' : '.setAttribute';
-	let args = [b.literal(fix_attribute_casing(prop)), b.literal(value ?? '')];
+	const prop_correct_case = fix_attribute_casing(prop);
 
-	// attributes like `xlink:href` need to be set with the `xlink` namespace
-	if (namespace === 'xlink') {
-		args.unshift(b.literal('http://www.w3.org/1999/xlink'));
-	}
+	const is_valid_id = regex_is_valid_identifier.test(prop_correct_case);
 
-	return {
-		call: b.call(el.name + fn, ...args)
-	};
+	/** @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} el
- * @param {Node} child
- * @param {Node} [anchor]
+ * @param {Element} element
+ * @param {Element} child
  */
-function insert(el, child, anchor) {
-	return {
-		call: b.call(
-			// if we have a template element we need to push into it's content rather than the element itself
-			el.name + (el.element === 'template' ? '.content' : '') + '.insertBefore',
-			b.id(child.name),
-			b.id(anchor?.name ?? 'undefined')
-		)
-	};
+function insert(element, child) {
+	const c = get_or_create_prop(element, 'c', b.array([]));
+	/** @type {ArrayExpression} */ (c.value).elements.push(child);
 }
 
 let map = {
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 bd4d60837d62..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
@@ -65,12 +78,75 @@ export function template(content, flags) {
 }
 
 /**
- * @param {()=>(DocumentFragment | Node)} fn
+ * @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(fn, flags) {
+export function template_fn(structure, flags) {
 	var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0;
 	var use_import_node = (flags & TEMPLATE_USE_IMPORT_NODE) !== 0;
 
@@ -84,7 +160,7 @@ export function template_fn(fn, flags) {
 		}
 
 		if (node === undefined) {
-			node = fn();
+			node = structure_to_fragment(structure);
 			if (!is_fragment) node = /** @type {Node} */ (get_first_child(node));
 		}
 
@@ -117,12 +193,12 @@ export function template_with_script(content, flags) {
 }
 
 /**
- * @param {()=>(DocumentFragment | Node)} fn
+ * @param {Array} structure
  * @param {number} flags
  * @returns {() => Node | Node[]}
  */ /*#__NO_SIDE_EFFECTS__*/
-export function template_with_script_fn(fn, flags) {
-	var templated_fn = template_fn(fn, flags);
+export function template_with_script_fn(structure, flags) {
+	var templated_fn = template_fn(structure, flags);
 	return () => run_scripts(/** @type {Element | DocumentFragment} */ (templated_fn()));
 }
 
@@ -182,13 +258,13 @@ export function ns_template(content, flags, ns = 'svg') {
 }
 
 /**
- * @param {()=>(DocumentFragment | Node)} fn
+ * @param {Array} structure
  * @param {number} flags
  * @param {'svg' | 'math'} ns
  * @returns {() => Node | Node[]}
  */
 /*#__NO_SIDE_EFFECTS__*/
-export function ns_template_fn(fn, flags, ns = 'svg') {
+export function ns_template_fn(structure, flags, ns = 'svg') {
 	var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0;
 
 	/** @type {Element | DocumentFragment} */
@@ -201,7 +277,7 @@ export function ns_template_fn(fn, flags, ns = 'svg') {
 		}
 
 		if (!node) {
-			var fragment = /** @type {DocumentFragment} */ (fn());
+			var fragment = structure_to_fragment(structure, ns);
 
 			if (is_fragment) {
 				node = document.createDocumentFragment();
@@ -240,13 +316,13 @@ export function svg_template_with_script(content, flags) {
 }
 
 /**
- * @param {()=>(DocumentFragment | Node)} fn
+ * @param {Array} structure
  * @param {number} flags
  * @returns {() => Node | Node[]}
  */
 /*#__NO_SIDE_EFFECTS__*/
-export function svg_template_with_script_fn(fn, flags) {
-	var templated_fn = ns_template_fn(fn, flags);
+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()));
 }
 
@@ -261,13 +337,13 @@ export function mathml_template(content, flags) {
 }
 
 /**
- * @param {()=>(DocumentFragment | Node)} fn
+ * @param {Array} structure
  * @param {number} flags
  * @returns {() => Node | Node[]}
  */
 /*#__NO_SIDE_EFFECTS__*/
-export function mathml_template_fn(fn, flags) {
-	return ns_template_fn(fn, flags, 'math');
+export function mathml_template_fn(structure, flags) {
+	return ns_template_fn(structure, flags, 'math');
 }
 
 /**
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
index 262454e9c329..44b0cd1557aa 100644
--- 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
@@ -5,23 +5,7 @@ function increment(_, counter) {
 	counter.count += 1;
 }
 
-var root = $.template_fn(
-	() => {
-		var button = document.createElement('button');
-		var text = document.createTextNode(' ');
-
-		button.insertBefore(text, undefined)
-
-		var text_1 = document.createTextNode(' ');
-		var comment = document.createComment('');
-		var text_2 = document.createTextNode(' ');
-		var fragment = document.createDocumentFragment();
-
-		fragment.append(button, text_1, comment, text_2)
-		return fragment;
-	},
-	1
-);
+var root = $.template_fn([{ e: 'button', c: [' '] }, ' ', , ' '], 1);
 
 export default function Await_block_scope($$anchor) {
 	let counter = $.proxy({ count: 0 });
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
index 8c15aa74ba1f..e06c3bbf6bc8 100644
--- 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
@@ -10,18 +10,7 @@ const snippet = ($$anchor) => {
 	$.append($$anchor, text);
 };
 
-var root = $.template_fn(
-	() => {
-		var comment = document.createComment('');
-		var comment_1 = document.createComment('');
-		var text = document.createTextNode(' ');
-		var fragment = document.createDocumentFragment();
-
-		fragment.append(comment, comment_1, text)
-		return fragment;
-	},
-	1
-);
+var root = $.template_fn([,, ' '], 1);
 
 export default function Bind_component_snippet($$anchor) {
 	let value = $.state('');
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
index c05184f1f82a..6bf2d77a00bd 100644
--- 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
@@ -2,23 +2,19 @@ import 'svelte/internal/disclose-version';
 import * as $ from 'svelte/internal/client';
 
 var root = $.template_fn(
-	() => {
-		var div = document.createElement('div');
-		var text = document.createTextNode(' ');
-		var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
-		var text_1 = document.createTextNode(' ');
-		var custom_element = document.createElement('custom-element');
-		var text_2 = document.createTextNode(' ');
-		var div_1 = document.createElement('div');
-		var text_3 = document.createTextNode(' ');
-		var svg_1 = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
-		var text_4 = document.createTextNode(' ');
-		var custom_element_1 = document.createElement('custom-element');
-		var fragment = document.createDocumentFragment();
-
-		fragment.append(div, text, svg, text_1, custom_element, text_2, div_1, text_3, svg_1, text_4, custom_element_1)
-		return fragment;
-	},
+	[
+		{ e: 'div' },
+		' ',
+		{ e: 'svg' },
+		' ',
+		{ e: 'custom-element' },
+		' ',
+		{ e: 'div' },
+		' ',
+		{ e: 'svg' },
+		' ',
+		{ e: 'custom-element' }
+	],
 	3
 );
 
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
index 5f55a12c20c1..3cc49718838f 100644
--- 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
@@ -2,17 +2,7 @@ import 'svelte/internal/disclose-version';
 import 'svelte/internal/flags/legacy';
 import * as $ from 'svelte/internal/client';
 
-var root = $.template_fn(() => {
-	var h1 = document.createElement('h1');
-	var text = document.createTextNode('hello world');
-
-	h1.insertBefore(text, undefined)
-
-	var fragment = document.createDocumentFragment();
-
-	fragment.append(h1)
-	return fragment;
-});
+var root = $.template_fn([{ e: 'h1', c: ['hello world'] }]);
 
 export default function Hello_world($$anchor) {
 	var h1 = root();
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
index 6196ed18bbd8..d5bb01474cf9 100644
--- 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
@@ -2,17 +2,7 @@ import 'svelte/internal/disclose-version';
 import 'svelte/internal/flags/legacy';
 import * as $ from 'svelte/internal/client';
 
-var root = $.template_fn(() => {
-	var h1 = document.createElement('h1');
-	var text = document.createTextNode('hello world');
-
-	h1.insertBefore(text, undefined)
-
-	var fragment = document.createDocumentFragment();
-
-	fragment.append(h1)
-	return fragment;
-});
+var root = $.template_fn([{ e: 'h1', c: ['hello world'] }]);
 
 function Hmr($$anchor) {
 	var h1 = root();
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
index 6b48b03635c5..3a2d0ecaa29e 100644
--- 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
@@ -4,23 +4,15 @@ import * as $ from 'svelte/internal/client';
 var on_click = (_, count) => $.update(count);
 
 var root = $.template_fn(
-	() => {
-		var h1 = document.createElement('h1');
-		var text = document.createTextNode(' ');
-		var b = document.createElement('b');
-		var text_1 = document.createTextNode(' ');
-		var button = document.createElement('button');
-		var text_2 = document.createTextNode(' ');
-
-		button.insertBefore(text_2, undefined)
-
-		var text_3 = document.createTextNode(' ');
-		var h1_1 = document.createElement('h1');
-		var fragment = document.createDocumentFragment();
-
-		fragment.append(h1, text, b, text_1, button, text_3, h1_1)
-		return fragment;
-	},
+	[
+		{ e: 'h1' },
+		' ',
+		{ e: 'b' },
+		' ',
+		{ e: 'button', c: [' '] },
+		' ',
+		{ e: 'h1' }
+	],
 	1
 );
 
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
index 409a0d93aa49..d6de4ff4d04f 100644
--- 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
@@ -2,20 +2,7 @@ import 'svelte/internal/disclose-version';
 import 'svelte/internal/flags/legacy';
 import * as $ from 'svelte/internal/client';
 
-var root = $.template_fn(
-	() => {
-		var p = document.createElement('p');
-		var text = document.createTextNode(' ');
-		var p_1 = document.createElement('p');
-		var text_1 = document.createTextNode(' ');
-		var comment = document.createComment('');
-		var fragment = document.createDocumentFragment();
-
-		fragment.append(p, text, p_1, text_1, comment)
-		return fragment;
-	},
-	1
-);
+var root = $.template_fn([{ e: 'p' }, ' ', { e: 'p' }, ' ', ,], 1);
 
 export default function Purity($$anchor) {
 	var fragment = root();
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
index 8023738996ba..b838acb2d6c8 100644
--- 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
@@ -2,238 +2,94 @@ import 'svelte/internal/disclose-version';
 import * as $ from 'svelte/internal/client';
 
 var root = $.template_fn(
-	() => {
-		var header = document.createElement('header');
-		var nav = document.createElement('nav');
-
-		header.insertBefore(nav, undefined)
-
-		var a = document.createElement('a');
-
-		nav.insertBefore(a, undefined)
-		a.setAttribute('href', '/')
-
-		var text = document.createTextNode('Home');
-
-		a.insertBefore(text, undefined)
-
-		var text_1 = document.createTextNode(' ');
-
-		nav.insertBefore(text_1, undefined)
-
-		var a_1 = document.createElement('a');
-
-		nav.insertBefore(a_1, undefined)
-		a_1.setAttribute('href', '/away')
-
-		var text_2 = document.createTextNode('Away');
-
-		a_1.insertBefore(text_2, undefined)
-
-		var text_3 = document.createTextNode(' ');
-		var main = document.createElement('main');
-		var h1 = document.createElement('h1');
-
-		main.insertBefore(h1, undefined)
-
-		var text_4 = document.createTextNode(' ');
-
-		h1.insertBefore(text_4, undefined)
-
-		var text_5 = document.createTextNode(' ');
-
-		main.insertBefore(text_5, undefined)
-
-		var div = document.createElement('div');
-
-		main.insertBefore(div, undefined)
-		div.setAttribute('class', 'static')
-
-		var p = document.createElement('p');
-
-		div.insertBefore(p, undefined)
-
-		var text_6 = document.createTextNode('we don\'t need to traverse these nodes');
-
-		p.insertBefore(text_6, undefined)
-
-		var text_7 = document.createTextNode(' ');
-
-		main.insertBefore(text_7, undefined)
-
-		var p_1 = document.createElement('p');
-
-		main.insertBefore(p_1, undefined)
-
-		var text_8 = document.createTextNode('or');
-
-		p_1.insertBefore(text_8, undefined)
-
-		var text_9 = document.createTextNode(' ');
-
-		main.insertBefore(text_9, undefined)
-
-		var p_2 = document.createElement('p');
-
-		main.insertBefore(p_2, undefined)
-
-		var text_10 = document.createTextNode('these');
-
-		p_2.insertBefore(text_10, undefined)
-
-		var text_11 = document.createTextNode(' ');
-
-		main.insertBefore(text_11, undefined)
-
-		var p_3 = document.createElement('p');
-
-		main.insertBefore(p_3, undefined)
-
-		var text_12 = document.createTextNode('ones');
-
-		p_3.insertBefore(text_12, undefined)
-
-		var text_13 = document.createTextNode(' ');
-
-		main.insertBefore(text_13, undefined)
-
-		var comment = document.createComment('');
-
-		main.insertBefore(comment, undefined)
-
-		var text_14 = document.createTextNode(' ');
-
-		main.insertBefore(text_14, undefined)
-
-		var p_4 = document.createElement('p');
-
-		main.insertBefore(p_4, undefined)
-
-		var text_15 = document.createTextNode('these');
-
-		p_4.insertBefore(text_15, undefined)
-
-		var text_16 = document.createTextNode(' ');
-
-		main.insertBefore(text_16, undefined)
-
-		var p_5 = document.createElement('p');
-
-		main.insertBefore(p_5, undefined)
-
-		var text_17 = document.createTextNode('trailing');
-
-		p_5.insertBefore(text_17, undefined)
-
-		var text_18 = document.createTextNode(' ');
-
-		main.insertBefore(text_18, undefined)
-
-		var p_6 = document.createElement('p');
-
-		main.insertBefore(p_6, undefined)
-
-		var text_19 = document.createTextNode('nodes');
-
-		p_6.insertBefore(text_19, undefined)
-
-		var text_20 = document.createTextNode(' ');
-
-		main.insertBefore(text_20, undefined)
-
-		var p_7 = document.createElement('p');
-
-		main.insertBefore(p_7, undefined)
-
-		var text_21 = document.createTextNode('can');
-
-		p_7.insertBefore(text_21, undefined)
-
-		var text_22 = document.createTextNode(' ');
-
-		main.insertBefore(text_22, undefined)
-
-		var p_8 = document.createElement('p');
-
-		main.insertBefore(p_8, undefined)
-
-		var text_23 = document.createTextNode('be');
-
-		p_8.insertBefore(text_23, undefined)
-
-		var text_24 = document.createTextNode(' ');
-
-		main.insertBefore(text_24, undefined)
-
-		var p_9 = document.createElement('p');
-
-		main.insertBefore(p_9, undefined)
-
-		var text_25 = document.createTextNode('completely');
-
-		p_9.insertBefore(text_25, undefined)
-
-		var text_26 = document.createTextNode(' ');
-
-		main.insertBefore(text_26, undefined)
-
-		var p_10 = document.createElement('p');
-
-		main.insertBefore(p_10, undefined)
-
-		var text_27 = document.createTextNode('ignored');
-
-		p_10.insertBefore(text_27, undefined)
-
-		var text_28 = document.createTextNode(' ');
-		var cant_skip = document.createElement('cant-skip');
-		var custom_elements = document.createElement('custom-elements');
-
-		cant_skip.insertBefore(custom_elements, undefined)
-
-		var text_29 = document.createTextNode(' ');
-		var div_1 = document.createElement('div');
-		var input = document.createElement('input');
-
-		div_1.insertBefore(input, undefined)
-
-		var text_30 = document.createTextNode(' ');
-		var div_2 = document.createElement('div');
-		var source = document.createElement('source');
-
-		div_2.insertBefore(source, undefined)
-
-		var text_31 = document.createTextNode(' ');
-		var select = document.createElement('select');
-		var option = document.createElement('option');
-
-		select.insertBefore(option, undefined)
-
-		var text_32 = document.createTextNode('a');
-
-		option.insertBefore(text_32, undefined)
-
-		var text_33 = document.createTextNode(' ');
-		var img = document.createElement('img');
-
-		img.setAttribute('src', '...')
-		img.setAttribute('alt', '')
-		img.setAttribute('loading', 'lazy')
-
-		var text_34 = document.createTextNode(' ');
-		var div_3 = document.createElement('div');
-		var img_1 = document.createElement('img');
-
-		div_3.insertBefore(img_1, undefined)
-		img_1.setAttribute('src', '...')
-		img_1.setAttribute('alt', '')
-		img_1.setAttribute('loading', 'lazy')
-
-		var fragment = document.createDocumentFragment();
-
-		fragment.append(header, text_3, main, text_28, cant_skip, text_29, div_1, text_30, div_2, text_31, select, text_33, img, text_34, div_3)
-		return fragment;
-	},
+	[
+		{
+			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
 );
 
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
index 4247dcfda3f5..2be761b88dc8 100644
--- 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
@@ -9,21 +9,13 @@ function reset(_, str, tpl) {
 }
 
 var root = $.template_fn(
-	() => {
-		var input = document.createElement('input');
-		var text = document.createTextNode(' ');
-		var input_1 = document.createElement('input');
-		var text_1 = document.createTextNode(' ');
-		var button = document.createElement('button');
-		var text_2 = document.createTextNode('reset');
-
-		button.insertBefore(text_2, undefined)
-
-		var fragment = document.createDocumentFragment();
-
-		fragment.append(input, text, input_1, text_1, button)
-		return fragment;
-	},
+	[
+		{ e: 'input' },
+		' ',
+		{ e: 'input' },
+		' ',
+		{ e: 'button', c: ['reset'] }
+	],
 	1
 );
 
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
index 5163dc350967..9176f1ab92cc 100644
--- 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
@@ -1,17 +1,7 @@
 import 'svelte/internal/disclose-version';
 import * as $ from 'svelte/internal/client';
 
-var root = $.template_fn(() => {
-	var p = document.createElement('p');
-	var text = document.createTextNode(' ');
-
-	p.insertBefore(text, undefined)
-
-	var fragment = document.createDocumentFragment();
-
-	fragment.append(p)
-	return fragment;
-});
+var root = $.template_fn([{ e: 'p', c: [' '] }]);
 
 export default function Text_nodes_deriveds($$anchor) {
 	let count1 = 0;

From b043b28d45ba9638a7b8ae80822e44552ee5c9f5 Mon Sep 17 00:00:00 2001
From: paoloricciuti 
Date: Wed, 2 Apr 2025 13:17:16 +0200
Subject: [PATCH 17/17] feat: custom renderer api

---
 packages/svelte/src/compiler/index.js         |   1 +
 .../phases/2-analyze/visitors/Attribute.js    |   2 +-
 .../3-transform/client/transform-client.js    |   2 +-
 .../client/visitors/shared/events.js          |   7 +-
 packages/svelte/src/compiler/types/index.d.ts |   4 +
 .../svelte/src/compiler/validate-options.js   |   2 +
 packages/svelte/src/index.d.ts                |   4 +
 .../internal/client/dom/elements/events.js    |  16 +-
 .../src/internal/client/dom/operations.js     |  62 ++++++--
 .../src/internal/client/reactivity/effects.js |   4 +-
 .../src/internal/client/reactivity/types.d.ts |   2 +
 packages/svelte/src/internal/client/render.js |  20 ++-
 .../svelte/src/internal/client/runtime.js     |  10 +-
 packages/svelte/src/renderer/index.ts         | 145 ++++++++++++++++++
 packages/svelte/types/index.d.ts              |   8 +
 15 files changed, 269 insertions(+), 20 deletions(-)
 create mode 100644 packages/svelte/src/renderer/index.ts

diff --git a/packages/svelte/src/compiler/index.js b/packages/svelte/src/compiler/index.js
index 42427dd9c407..d3527460b15f 100644
--- a/packages/svelte/src/compiler/index.js
+++ b/packages/svelte/src/compiler/index.js
@@ -20,6 +20,7 @@ export { default as preprocess } from './preprocess/index.js';
  * @returns {CompileResult}
  */
 export function compile(source, options) {
+	options.customRenderer = true;
 	source = remove_bom(source);
 	state.reset_warning_filter(options.warningFilter);
 	const validated = validate_component_options(options, '');
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js
index 3ba81767cce3..05c8a99ae508 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js
@@ -67,7 +67,7 @@ export function Attribute(node, context) {
 			const expression = get_attribute_expression(node);
 			const delegated_event = get_delegated_event(node.name.slice(2), expression, context);
 
-			if (delegated_event !== null) {
+			if (delegated_event !== null && !context.state.options.customRenderer) {
 				if (delegated_event.hoisted) {
 					delegated_event.function.metadata.hoisted = true;
 				}
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 da0a89fb2f7e..0f4f1f2bf2fa 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,7 +167,7 @@ export function client_component(analysis, options) {
 		in_constructor: false,
 		instance_level_snippets: [],
 		module_level_snippets: [],
-		is_functional_template_mode: options.templatingMode === 'functional',
+		is_functional_template_mode: options.customRenderer || 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/visitors/shared/events.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js
index 2667a96f6aef..e140a9dfebd4 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js
@@ -68,7 +68,8 @@ export function visit_event_attribute(node, context) {
 				context.state.node,
 				handler,
 				capture,
-				is_passive_event(event_name) ? true : undefined
+				is_passive_event(event_name) ? true : undefined,
+				context.state.options.customRenderer
 			)
 		);
 
@@ -90,13 +91,15 @@ export function visit_event_attribute(node, context) {
  * @param {Expression} handler
  * @param {boolean} capture
  * @param {boolean | undefined} passive
+ * @param {boolean | undefined} custom_renderer
  */
-export function build_event(event_name, node, handler, capture, passive) {
+export function build_event(event_name, node, handler, capture, passive, custom_renderer) {
 	return b.call(
 		'$.event',
 		b.literal(event_name),
 		node,
 		handler,
+		custom_renderer && b.true,
 		capture && b.true,
 		passive === undefined ? undefined : b.literal(passive)
 	);
diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts
index 4aa244ad5191..513fa0caa1d5 100644
--- a/packages/svelte/src/compiler/types/index.d.ts
+++ b/packages/svelte/src/compiler/types/index.d.ts
@@ -119,6 +119,10 @@ export interface CompileOptions extends ModuleCompileOptions {
 	 * @default 'string'
 	 */
 	templatingMode?: 'string' | 'functional';
+	/**
+	 *  If `true` the output will be adapted to accept a custom renderer.
+	 */
+	customRenderer?: boolean;
 	/**
 	 * 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 1d67951fd855..187e80875bfc 100644
--- a/packages/svelte/src/compiler/validate-options.js
+++ b/packages/svelte/src/compiler/validate-options.js
@@ -112,6 +112,8 @@ export const validate_component_options =
 
 			templatingMode: list(['string', 'functional']),
 
+			customRenderer: boolean(false),
+
 			preserveWhitespace: boolean(false),
 
 			runes: boolean(undefined),
diff --git a/packages/svelte/src/index.d.ts b/packages/svelte/src/index.d.ts
index 38e60866898f..a6d573fd8dea 100644
--- a/packages/svelte/src/index.d.ts
+++ b/packages/svelte/src/index.d.ts
@@ -338,6 +338,10 @@ export type MountOptions = Record
 	 * @default true
 	 */
 	intro?: boolean;
+	/**
+	 * The custom renderer to use to mount the component.
+	 */
+	customRenderer?: any;
 } & ({} extends Props
 	? {
 			/**
diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js
index 0c1bb1dada83..4b2392ba722e 100644
--- a/packages/svelte/src/internal/client/dom/elements/events.js
+++ b/packages/svelte/src/internal/client/dom/elements/events.js
@@ -51,12 +51,21 @@ export function replay_events(dom) {
  * @param {EventTarget} dom
  * @param {EventListener} [handler]
  * @param {AddEventListenerOptions} [options]
+ * @param {boolean} [custom_renderer]
  */
-export function create_event(event_name, dom, handler, options = {}) {
+export function create_event(event_name, dom, handler, options = {}, custom_renderer = false) {
 	/**
 	 * @this {EventTarget}
 	 */
 	function target_handler(/** @type {Event} */ event) {
+		// if we have a custom renderer we just want to call the function
+		// without a reactive context because we don't know if event propagation
+		// is even a thing in the target renderer
+		if (custom_renderer) {
+			return without_reactive_context(() => {
+				return handler?.call(this, event);
+			});
+		}
 		if (!options.capture) {
 			// Only call in the bubble phase, else delegated events would be called before the capturing events
 			handle_event_propagation.call(dom, event);
@@ -109,13 +118,14 @@ export function on(element, type, handler, options = {}) {
  * @param {string} event_name
  * @param {Element} dom
  * @param {EventListener} [handler]
+ * @param {boolean} [custom_renderer]
  * @param {boolean} [capture]
  * @param {boolean} [passive]
  * @returns {void}
  */
-export function event(event_name, dom, handler, capture, passive) {
+export function event(event_name, dom, handler, custom_renderer, capture, passive) {
 	var options = { capture, passive };
-	var target_handler = create_event(event_name, dom, handler, options);
+	var target_handler = create_event(event_name, dom, handler, options, custom_renderer);
 
 	// @ts-ignore
 	if (dom === document.body || dom === window || dom === document) {
diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js
index 97062f04e38d..1416723726db 100644
--- a/packages/svelte/src/internal/client/dom/operations.js
+++ b/packages/svelte/src/internal/client/dom/operations.js
@@ -19,6 +19,8 @@ var first_child_getter;
 /** @type {() => Node | null} */
 var next_sibling_getter;
 
+var renderer;
+
 /**
  * Initialize these lazily to avoid issues when using the runtime in a server context
  * where these globals are not available while avoiding a separate server entry point
@@ -28,20 +30,23 @@ export function init_operations() {
 		return;
 	}
 
-	$window = window;
-	$document = document;
-	is_firefox = /Firefox/.test(navigator.userAgent);
+	$window = typeof window === 'undefined' ? /** @type {Window} */ ({}) : window;
+	$document = typeof document === 'undefined' ? /** @type {Document} */ ({}) : document;
+	is_firefox = typeof navigator === 'undefined' ? true : /Firefox/.test(navigator.userAgent);
 
-	var element_prototype = Element.prototype;
-	var node_prototype = Node.prototype;
-	var text_prototype = Text.prototype;
+	var element_prototype;
+	if (window.Element) element_prototype = Element.prototype;
+	var node_prototype;
+	if (window.Node) node_prototype = Node.prototype;
+	var text_prototype;
+	if (window.Text) text_prototype = Text.prototype;
 
 	// @ts-ignore
-	first_child_getter = get_descriptor(node_prototype, 'firstChild').get;
+	first_child_getter = get_descriptor(node_prototype, 'firstChild')?.get;
 	// @ts-ignore
-	next_sibling_getter = get_descriptor(node_prototype, 'nextSibling').get;
+	next_sibling_getter = get_descriptor(node_prototype, 'nextSibling')?.get;
 
-	if (is_extensible(element_prototype)) {
+	if (element_prototype && is_extensible(element_prototype)) {
 		// the following assignments improve perf of lookups on DOM nodes
 		// @ts-expect-error
 		element_prototype.__click = undefined;
@@ -73,6 +78,9 @@ export function init_operations() {
  * @returns {Text}
  */
 export function create_text(value = '') {
+	if (renderer) {
+		return renderer.document.createTextNode(value);
+	}
 	return document.createTextNode(value);
 }
 
@@ -83,6 +91,9 @@ export function create_text(value = '') {
  */
 /*@__NO_SIDE_EFFECTS__*/
 export function get_first_child(node) {
+	if (renderer_first_child) {
+		return renderer_first_child.call(node);
+	}
 	return first_child_getter.call(node);
 }
 
@@ -93,6 +104,9 @@ export function get_first_child(node) {
  */
 /*@__NO_SIDE_EFFECTS__*/
 export function get_next_sibling(node) {
+	if (renderer_next_sibling) {
+		return renderer_next_sibling.call(node);
+	}
 	return next_sibling_getter.call(node);
 }
 
@@ -213,6 +227,9 @@ export function clear_text_content(node) {
  * @returns
  */
 export function create_element(tag, namespace, is) {
+	if (renderer) {
+		return renderer.document.createElement(tag);
+	}
 	let options = is ? { is } : undefined;
 	if (namespace) {
 		return document.createElementNS(namespace, tag, options);
@@ -221,6 +238,9 @@ export function create_element(tag, namespace, is) {
 }
 
 export function create_fragment() {
+	if (renderer) {
+		return renderer.document.createDocumentFragment();
+	}
 	return document.createDocumentFragment();
 }
 
@@ -229,6 +249,9 @@ export function create_fragment() {
  * @returns
  */
 export function create_comment(data = '') {
+	if (renderer) {
+		return renderer.document.createComment(data);
+	}
 	return document.createComment(data);
 }
 
@@ -245,3 +268,24 @@ export function set_attribute(element, key, value = '') {
 	}
 	return element.setAttribute(key, value);
 }
+
+var renderer_next_sibling;
+var renderer_first_child;
+
+export function push_renderer(custom_renderer) {
+	var old_next_sibling = renderer_next_sibling;
+	var old_first_child = renderer_first_child;
+	var old_renderer = renderer;
+	renderer_next_sibling = get_descriptor(custom_renderer.Node.prototype, 'nextSibling')?.get;
+	renderer_first_child = get_descriptor(custom_renderer.Node.prototype, 'firstChild')?.get;
+	renderer = custom_renderer;
+	return () => {
+		renderer_next_sibling = old_next_sibling;
+		renderer_first_child = old_first_child;
+		renderer = old_renderer;
+	};
+}
+
+export function get_renderer() {
+	return renderer;
+}
diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js
index 468bb94ab428..7022e9903d93 100644
--- a/packages/svelte/src/internal/client/reactivity/effects.js
+++ b/packages/svelte/src/internal/client/reactivity/effects.js
@@ -38,7 +38,7 @@ import { set } from './sources.js';
 import * as e from '../errors.js';
 import { DEV } from 'esm-env';
 import { define_property } from '../../shared/utils.js';
-import { get_next_sibling } from '../dom/operations.js';
+import { get_next_sibling, get_renderer } from '../dom/operations.js';
 import { derived } from './deriveds.js';
 import { component_context, dev_current_component_function } from '../context.js';
 
@@ -99,6 +99,8 @@ function create_effect(type, fn, sync, push = true) {
 		nodes_end: null,
 		f: type | DIRTY,
 		first: null,
+		// we only need to update the renderer for render effects
+		renderer: (type & RENDER_EFFECT) !== 0 ? get_renderer() : undefined,
 		fn,
 		last: null,
 		next: null,
diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts
index 5ef0097649a4..a76cef9fe252 100644
--- a/packages/svelte/src/internal/client/reactivity/types.d.ts
+++ b/packages/svelte/src/internal/client/reactivity/types.d.ts
@@ -67,6 +67,8 @@ export interface Effect extends Reaction {
 	last: null | Effect;
 	/** Parent effect */
 	parent: Effect | null;
+	/** The original renderer used when mounting the root component */
+	renderer?: any;
 	/** Dev only */
 	component_function?: any;
 }
diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js
index 3256fe827410..4985ca9a8949 100644
--- a/packages/svelte/src/internal/client/render.js
+++ b/packages/svelte/src/internal/client/render.js
@@ -6,7 +6,8 @@ import {
 	create_text,
 	get_first_child,
 	get_next_sibling,
-	init_operations
+	init_operations,
+	push_renderer
 } from './dom/operations.js';
 import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js';
 import { active_effect } from './runtime.js';
@@ -86,6 +87,7 @@ export function mount(component, options) {
  *  	context?: Map;
  * 		intro?: boolean;
  * 		recover?: boolean;
+ * 		customRenderer?: any;
  * 	} : {
  * 		target: Document | Element | ShadowRoot;
  * 		props: Props;
@@ -93,10 +95,15 @@ export function mount(component, options) {
  *  	context?: Map;
  * 		intro?: boolean;
  * 		recover?: boolean;
+ * 		customRenderer?: any;
  * 	}} options
  * @returns {Exports}
  */
 export function hydrate(component, options) {
+	let cleanup_renderer = undefined;
+	if (options.customRenderer) {
+		cleanup_renderer = push_renderer(options.customRenderer);
+	}
 	init_operations();
 	options.intro = options.intro ?? false;
 	const target = options.target;
@@ -153,6 +160,7 @@ export function hydrate(component, options) {
 		set_hydrating(was_hydrating);
 		set_hydrate_node(previous_hydrate_node);
 		reset_head_anchor();
+		cleanup_renderer?.();
 	}
 }
 
@@ -165,7 +173,14 @@ const document_listeners = new Map();
  * @param {MountOptions} options
  * @returns {Exports}
  */
-function _mount(Component, { target, anchor, props = {}, events, context, intro = true }) {
+function _mount(
+	Component,
+	{ target, anchor, props = {}, events, context, intro = true, customRenderer }
+) {
+	let cleanup_renderer = undefined;
+	if (customRenderer) {
+		cleanup_renderer = push_renderer(customRenderer);
+	}
 	init_operations();
 
 	var registered_events = new Set();
@@ -261,6 +276,7 @@ function _mount(Component, { target, anchor, props = {}, events, context, intro
 	});
 
 	mounted_components.set(component, unmount);
+	cleanup_renderer?.();
 	return component;
 }
 
diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js
index a5d26412a4e6..c7dc3a8294b8 100644
--- a/packages/svelte/src/internal/client/runtime.js
+++ b/packages/svelte/src/internal/client/runtime.js
@@ -39,7 +39,7 @@ import {
 	set_component_context,
 	set_dev_current_component_function
 } from './context.js';
-import { is_firefox } from './dom/operations.js';
+import { is_firefox, push_renderer } from './dom/operations.js';
 
 // Used for DEV time error handling
 /** @param {WeakSet} value */
@@ -421,6 +421,13 @@ export function update_reaction(reaction) {
 
 	reaction_sources = null;
 	set_component_context(reaction.ctx);
+	var cleanup_renderer = undefined;
+	if ((reaction.f & DERIVED) === 0) {
+		var effect = /** @type {Effect} */ (reaction);
+		if (effect.renderer) {
+			cleanup_renderer = push_renderer(effect.renderer);
+		}
+	}
 	untracking = false;
 	read_version++;
 
@@ -498,6 +505,7 @@ export function update_reaction(reaction) {
 		reaction_sources = previous_reaction_sources;
 		set_component_context(previous_component_context);
 		untracking = previous_untracking;
+		cleanup_renderer?.();
 
 		reaction.f ^= EFFECT_IS_UPDATING;
 	}
diff --git a/packages/svelte/src/renderer/index.ts b/packages/svelte/src/renderer/index.ts
new file mode 100644
index 000000000000..8a26007f8deb
--- /dev/null
+++ b/packages/svelte/src/renderer/index.ts
@@ -0,0 +1,145 @@
+const ELEMENT_NODE_TYPE = 1;
+const TEXT_NODE_TYPE = 3;
+const COMMENT_NODE_TYPE = 8;
+const FRAGMENT_NODE_TYPE = 11;
+
+export function createRenderer(opts) {
+	class Node {
+		tagName;
+		data;
+		parent;
+		nodeType;
+		nodeName;
+
+		#element;
+		#children = [];
+
+		constructor(node_type, element, tag) {
+			this.tagName = tag;
+			this.nodeName = tag;
+			this.#element = element;
+			this.nodeType = node_type;
+		}
+
+		get element() {
+			return this.#element;
+		}
+
+		get childNodes() {
+			return this.#children;
+		}
+
+		get firstChild() {
+			return this.#children[0];
+		}
+
+		get lastChild() {
+			return this.#children[this.#children.lenght - 1];
+		}
+
+		get nextSibling() {
+			if (this.parent) {
+				const idx = this.parent.#children.findIndex((el) => el === this);
+				return this.parent.#children[idx + 1] ?? null;
+			}
+			return null;
+		}
+
+		get className() {
+			return this.getAttribute('class');
+		}
+
+		set className(className) {
+			this.setAttribute('class', className);
+		}
+
+		setAttribute(key, value) {
+			opts.setAttribute(this.#element, key, value);
+		}
+
+		getAttribute(key) {
+			return opts.getAttribute(this.#element, key);
+		}
+
+		hasAttribute(key) {
+			return !!this.getAttribute(key);
+		}
+
+		removeAttribute(key) {
+			this.setAttribute(key, null);
+		}
+
+		addEventListener(name, handler, options) {
+			if (this.nodeType === FRAGMENT_NODE_TYPE) return;
+			opts.addEventListener(this.#element, name, handler, options);
+		}
+
+		remove() {
+			if (this.parent) {
+				this.parent.#children = this.parent.#children.filter((el) => el !== this);
+			}
+		}
+
+		appendChild(child) {
+			this.append(child);
+			return child;
+		}
+
+		before(node) {
+			if (this.parent) {
+				const idx = this.parent.#children.findIndex((el) => el === this);
+				const nodes = node.nodeType === FRAGMENT_NODE_TYPE ? node.childNodes : [node];
+				this.parent.#children.splice(idx, 0, ...nodes);
+				for (let node of nodes) {
+					opts.insert(node.#element, this.parent.#element, this.#element);
+					node.parent = this.parent;
+				}
+			}
+		}
+
+		append(...nodes) {
+			for (let node of nodes) {
+				node.parent = this;
+				this.#children.push(node);
+				if (this.nodeType !== FRAGMENT_NODE_TYPE) {
+					opts.insert(node.#element, this.#element);
+				}
+			}
+		}
+
+		cloneNode() {
+			const cloned = new Node(
+				this.nodeType,
+				this.#element != null ? opts.cloneNode(this.#element) : this.#element,
+				this.tagName
+			);
+			for (let node of this.#children) {
+				cloned.append(node.cloneNode());
+			}
+			return cloned;
+		}
+	}
+
+	class Document {
+		createElement(tag) {
+			const element = opts.createElement(tag);
+			return new Node(ELEMENT_NODE_TYPE, element, tag);
+		}
+		createDocumentFragment() {
+			return new Node(FRAGMENT_NODE_TYPE);
+		}
+		createComment(data) {
+			const comment = opts.createComment(data);
+			return new Node(COMMENT_NODE_TYPE, comment, data);
+		}
+		createTextNode(data) {
+			const element = opts.createTextNode(data);
+			return new Node(TEXT_NODE_TYPE, element, data);
+		}
+	}
+
+	return {
+		Node,
+		document: new Document()
+	};
+}
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index 19bb6e9279b6..f2553db5c469 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -850,6 +850,10 @@ declare module 'svelte/compiler' {
 		 * @default 'string'
 		 */
 		templatingMode?: 'string' | 'functional';
+		/**
+		 *  Allow to specify a module that exports a custom renderer for svelte components.
+		 */
+		customRenderer?: string;
 		/**
 		 * 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.
@@ -2566,6 +2570,10 @@ declare module 'svelte/types/compiler/interfaces' {
 		 * @default 'string'
 		 */
 		templatingMode?: 'string' | 'functional';
+		/**
+		 *  Allow to specify a module that exports a custom renderer for svelte components.
+		 */
+		customRenderer?: string;
 		/**
 		 * 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.