From f2d4792406421c1f1ac716ef5cf84d2a4fd2cc74 Mon Sep 17 00:00:00 2001
From: Rich Harris <rich.harris@vercel.com>
Date: Tue, 21 Jan 2025 09:28:10 -0500
Subject: [PATCH 1/4] alternative attachment syntax

---
 .../compiler/phases/1-parse/read/script.js    |   2 +-
 .../src/compiler/phases/1-parse/read/style.js |   2 +-
 .../compiler/phases/1-parse/state/element.js  |  44 +-
 .../2-analyze/visitors/shared/component.js    |   6 +-
 .../3-transform/client/transform-client.js    |   4 +-
 .../3-transform/client/visitors/AttachTag.js  |  21 -
 .../3-transform/client/visitors/Attachment.js |  34 ++
 .../client/visitors/RegularElement.js         |   4 +-
 .../client/visitors/shared/component.js       |  22 +-
 .../svelte/src/compiler/types/template.d.ts   |  15 +-
 .../client/dom/elements/attachments.js        |  14 +-
 .../samples/attachments/input.svelte          |   6 +-
 .../samples/attachments/output.json           | 417 ++++++++++++++----
 .../samples/attachment-basic/main.svelte      |   2 +-
 .../samples/attachment-component/main.svelte  |   2 +-
 .../samples/attachment-reactive/main.svelte   |   2 +-
 .../attachment-svelte-element/main.svelte     |   2 +-
 packages/svelte/types/index.d.ts              |   4 +-
 18 files changed, 446 insertions(+), 157 deletions(-)
 delete mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js
 create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/Attachment.js

diff --git a/packages/svelte/src/compiler/phases/1-parse/read/script.js b/packages/svelte/src/compiler/phases/1-parse/read/script.js
index 629012781188..429abfcad4a6 100644
--- a/packages/svelte/src/compiler/phases/1-parse/read/script.js
+++ b/packages/svelte/src/compiler/phases/1-parse/read/script.js
@@ -16,7 +16,7 @@ const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module'];
 /**
  * @param {Parser} parser
  * @param {number} start
- * @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag>} attributes
+ * @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.Attachment>} attributes
  * @returns {AST.Script}
  */
 export function read_script(parser, start, attributes) {
diff --git a/packages/svelte/src/compiler/phases/1-parse/read/style.js b/packages/svelte/src/compiler/phases/1-parse/read/style.js
index 8f01af3b7507..bfcf469ff9fe 100644
--- a/packages/svelte/src/compiler/phases/1-parse/read/style.js
+++ b/packages/svelte/src/compiler/phases/1-parse/read/style.js
@@ -18,7 +18,7 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/;
 /**
  * @param {Parser} parser
  * @param {number} start
- * @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag>} attributes
+ * @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.Attachment>} attributes
  * @returns {AST.CSS.StyleSheet}
  */
 export default function read_style(parser, start, attributes) {
diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js
index 13d83e0e09f9..bad5269d4e58 100644
--- a/packages/svelte/src/compiler/phases/1-parse/state/element.js
+++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js
@@ -1,4 +1,4 @@
-/** @import { Expression } from 'estree' */
+/** @import { CallExpression, Expression } from 'estree' */
 /** @import { AST } from '#compiler' */
 /** @import { Parser } from '../index.js' */
 import { is_void } from '../../../../utils.js';
@@ -14,6 +14,8 @@ import { get_attribute_expression, is_expression_attribute } from '../../../util
 import { closing_tag_omitted } from '../../../../html-tree-validation.js';
 import { list } from '../../../utils/string.js';
 import { regex_whitespace } from '../../patterns.js';
+import { find_matching_bracket } from '../utils/bracket.js';
+import { parse_expression_at } from '../acorn.js';
 
 const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/;
 const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
@@ -480,31 +482,37 @@ function read_static_attribute(parser) {
 
 /**
  * @param {Parser} parser
- * @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag | null}
+ * @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.Attachment | null}
  */
 function read_attribute(parser) {
 	const start = parser.index;
 
-	if (parser.eat('{')) {
-		parser.allow_whitespace();
+	if (parser.match('attach(')) {
+		const end = find_matching_bracket(parser.template, start + 7, '(');
 
-		if (parser.eat('@attach')) {
-			parser.require_whitespace();
+		if (end === undefined) {
+			e.unexpected_eof(parser.template.length);
+		}
 
-			const expression = read_expression(parser);
-			parser.allow_whitespace();
-			parser.eat('}', true);
+		const sliced = parser.template.slice(0, end + 1);
 
-			/** @type {AST.AttachTag} */
-			const attachment = {
-				type: 'AttachTag',
-				start,
-				end: parser.index,
-				expression
-			};
+		const call = /** @type {CallExpression} */ (parse_expression_at(sliced, parser.ts, start));
 
-			return attachment;
-		}
+		/** @type {AST.Attachment} */
+		const attachment = {
+			type: 'Attachment',
+			start,
+			end,
+			attachments: call.arguments
+		};
+
+		parser.index = end + 1;
+
+		return attachment;
+	}
+
+	if (parser.eat('{')) {
+		parser.allow_whitespace();
 
 		if (parser.eat('...')) {
 			const expression = read_expression(parser);
diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js
index aca87fab811c..d99af4c247f1 100644
--- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js
+++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js
@@ -76,7 +76,7 @@ export function visit_component(node, context) {
 			attribute.type !== 'LetDirective' &&
 			attribute.type !== 'OnDirective' &&
 			attribute.type !== 'BindDirective' &&
-			attribute.type !== 'AttachTag'
+			attribute.type !== 'Attachment'
 		) {
 			e.component_invalid_directive(attribute);
 		}
@@ -110,10 +110,6 @@ export function visit_component(node, context) {
 		if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
 			context.state.analysis.uses_component_bindings = true;
 		}
-
-		if (attribute.type === 'AttachTag') {
-			disallow_unparenthesized_sequences(attribute.expression, context.state.analysis.source);
-		}
 	}
 
 	// If the component has a slot attribute — `<Foo slot="whatever" .../>` —
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 210381e2fdd2..4eba938d56b5 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
@@ -56,7 +56,7 @@ import { TitleElement } from './visitors/TitleElement.js';
 import { TransitionDirective } from './visitors/TransitionDirective.js';
 import { UpdateExpression } from './visitors/UpdateExpression.js';
 import { UseDirective } from './visitors/UseDirective.js';
-import { AttachTag } from './visitors/AttachTag.js';
+import { Attachment } from './visitors/Attachment.js';
 import { VariableDeclaration } from './visitors/VariableDeclaration.js';
 
 /** @type {Visitors} */
@@ -132,7 +132,7 @@ const visitors = {
 	TransitionDirective,
 	UpdateExpression,
 	UseDirective,
-	AttachTag,
+	Attachment,
 	VariableDeclaration
 };
 
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js
deleted file mode 100644
index 062604cacc16..000000000000
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js
+++ /dev/null
@@ -1,21 +0,0 @@
-/** @import { Expression } from 'estree' */
-/** @import { AST } from '#compiler' */
-/** @import { ComponentContext } from '../types' */
-import * as b from '../../../../utils/builders.js';
-
-/**
- * @param {AST.AttachTag} node
- * @param {ComponentContext} context
- */
-export function AttachTag(node, context) {
-	context.state.init.push(
-		b.stmt(
-			b.call(
-				'$.attach',
-				context.state.node,
-				b.thunk(/** @type {Expression} */ (context.visit(node.expression)))
-			)
-		)
-	);
-	context.next();
-}
diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Attachment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Attachment.js
new file mode 100644
index 000000000000..735f3a2cab4a
--- /dev/null
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Attachment.js
@@ -0,0 +1,34 @@
+/** @import { Expression } from 'estree' */
+/** @import { AST } from '#compiler' */
+/** @import { ComponentContext } from '../types' */
+import * as b from '../../../../utils/builders.js';
+
+/**
+ * @param {AST.Attachment} node
+ * @param {ComponentContext} context
+ */
+export function Attachment(node, context) {
+	for (const attachment of node.attachments) {
+		if (attachment.type === 'SpreadElement') {
+			context.state.init.push(
+				b.stmt(
+					b.call(
+						'$.attach_all',
+						context.state.node,
+						b.thunk(/** @type {Expression} */ (context.visit(attachment.argument)))
+					)
+				)
+			);
+		} else {
+			context.state.init.push(
+				b.stmt(
+					b.call(
+						'$.attach',
+						context.state.node,
+						b.thunk(/** @type {Expression} */ (context.visit(attachment)))
+					)
+				)
+			);
+		}
+	}
+}
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 eef2aa3aa5ef..ce2d52df91be 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
@@ -82,7 +82,7 @@ export function RegularElement(node, context) {
 	/** @type {AST.StyleDirective[]} */
 	const style_directives = [];
 
-	/** @type {Array<AST.AnimateDirective | AST.BindDirective | AST.OnDirective | AST.TransitionDirective | AST.UseDirective | AST.AttachTag>} */
+	/** @type {Array<AST.AnimateDirective | AST.BindDirective | AST.OnDirective | AST.TransitionDirective | AST.UseDirective | AST.Attachment>} */
 	const other_directives = [];
 
 	/** @type {ExpressionStatement[]} */
@@ -153,7 +153,7 @@ export function RegularElement(node, context) {
 				other_directives.push(attribute);
 				break;
 
-			case 'AttachTag':
+			case 'Attachment':
 				other_directives.push(attribute);
 				break;
 		}
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 2882da7d2f52..47b7b4436878 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
@@ -261,16 +261,20 @@ export function build_component(node, component_name, context, anchor = context.
 					);
 				}
 			}
-		} else if (attribute.type === 'AttachTag') {
+		} else if (attribute.type === 'Attachment') {
 			// TODO do we need to create a derived here?
-			push_prop(
-				b.prop(
-					'get',
-					b.call('Symbol'),
-					/** @type {Expression} */ (context.visit(attribute.expression)),
-					true
-				)
-			);
+			for (const attachment of attribute.attachments) {
+				push_prop(
+					b.prop(
+						'get',
+						b.call('Symbol'),
+						/** @type {Expression} */ (
+							context.visit(attachment.type === 'SpreadElement' ? attachment.argument : attachment)
+						),
+						true
+					)
+				);
+			}
 		}
 	}
 
diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts
index cafea6894cac..c0746b7a7434 100644
--- a/packages/svelte/src/compiler/types/template.d.ts
+++ b/packages/svelte/src/compiler/types/template.d.ts
@@ -15,7 +15,8 @@ import type {
 	Program,
 	ChainExpression,
 	SimpleCallExpression,
-	SequenceExpression
+	SequenceExpression,
+	SpreadElement
 } from 'estree';
 import type { Scope } from '../phases/scope';
 import type { _CSS } from './css';
@@ -174,10 +175,10 @@ export namespace AST {
 		};
 	}
 
-	/** A `{@attach foo(...)} tag */
-	export interface AttachTag extends BaseNode {
-		type: 'AttachTag';
-		expression: Expression;
+	/** An `attach(...)` attribute */
+	export interface Attachment extends BaseNode {
+		type: 'Attachment';
+		attachments: Array<Expression | SpreadElement>;
 	}
 
 	/** An `animate:` directive */
@@ -279,7 +280,7 @@ export namespace AST {
 
 	interface BaseElement extends BaseNode {
 		name: string;
-		attributes: Array<Attribute | SpreadAttribute | Directive | AttachTag>;
+		attributes: Array<Attribute | SpreadAttribute | Directive | Attachment>;
 		fragment: Fragment;
 	}
 
@@ -555,7 +556,7 @@ export namespace AST {
 		| AST.Attribute
 		| AST.SpreadAttribute
 		| Directive
-		| AST.AttachTag
+		| AST.Attachment
 		| AST.Comment
 		| Block;
 
diff --git a/packages/svelte/src/internal/client/dom/elements/attachments.js b/packages/svelte/src/internal/client/dom/elements/attachments.js
index 6e3089a384c1..c758f20d07f0 100644
--- a/packages/svelte/src/internal/client/dom/elements/attachments.js
+++ b/packages/svelte/src/internal/client/dom/elements/attachments.js
@@ -6,10 +6,16 @@ import { effect } from '../../reactivity/effects.js';
  */
 export function attach(node, get_fn) {
 	effect(() => {
-		const fn = get_fn();
+		const attachment = get_fn();
 
-		// we use `&&` rather than `?.` so that things like
-		// `{@attach DEV && something_dev_only()}` work
-		return fn && fn(node);
+		if (Array.isArray(attachment)) {
+			for (const fn of attachment) {
+				if (fn) {
+					$effect(() => fn(node));
+				}
+			}
+		} else if (attachment) {
+			return attachment(node);
+		}
 	});
 }
diff --git a/packages/svelte/tests/parser-modern/samples/attachments/input.svelte b/packages/svelte/tests/parser-modern/samples/attachments/input.svelte
index 9faae8d1bf40..592370ec7528 100644
--- a/packages/svelte/tests/parser-modern/samples/attachments/input.svelte
+++ b/packages/svelte/tests/parser-modern/samples/attachments/input.svelte
@@ -1 +1,5 @@
-<div {@attach (node) => {}} {@attach (node) => {}}></div>
+<div attach(a)></div>
+<div attach(a, b, c)></div>
+<div attach(...stuff)></div>
+<div attach(a, b, c, ...stuff)></div>
+<div attach((node) => {})></div>
diff --git a/packages/svelte/tests/parser-modern/samples/attachments/output.json b/packages/svelte/tests/parser-modern/samples/attachments/output.json
index 42e9880fccdd..69617856e3ec 100644
--- a/packages/svelte/tests/parser-modern/samples/attachments/output.json
+++ b/packages/svelte/tests/parser-modern/samples/attachments/output.json
@@ -2,7 +2,7 @@
 	"css": null,
 	"js": [],
 	"start": 0,
-	"end": 57,
+	"end": 149,
 	"type": "Root",
 	"fragment": {
 		"type": "Fragment",
@@ -10,124 +10,381 @@
 			{
 				"type": "RegularElement",
 				"start": 0,
-				"end": 57,
+				"end": 21,
 				"name": "div",
 				"attributes": [
 					{
 						"type": "AttachTag",
 						"start": 5,
-						"end": 27,
-						"expression": {
-							"type": "ArrowFunctionExpression",
-							"start": 14,
-							"end": 26,
-							"loc": {
-								"start": {
-									"line": 1,
-									"column": 14
+						"end": 13,
+						"attachments": [
+							{
+								"type": "Identifier",
+								"start": 12,
+								"end": 13,
+								"loc": {
+									"start": {
+										"line": 1,
+										"column": 12
+									},
+									"end": {
+										"line": 1,
+										"column": 13
+									}
 								},
-								"end": {
-									"line": 1,
-									"column": 26
-								}
+								"name": "a"
+							}
+						]
+					},
+					{
+						"type": "Attribute",
+						"start": 13,
+						"end": 14,
+						"name": ")",
+						"value": true
+					}
+				],
+				"fragment": {
+					"type": "Fragment",
+					"nodes": []
+				}
+			},
+			{
+				"type": "Text",
+				"start": 21,
+				"end": 22,
+				"raw": "\n",
+				"data": "\n"
+			},
+			{
+				"type": "RegularElement",
+				"start": 22,
+				"end": 49,
+				"name": "div",
+				"attributes": [
+					{
+						"type": "AttachTag",
+						"start": 27,
+						"end": 41,
+						"attachments": [
+							{
+								"type": "Identifier",
+								"start": 34,
+								"end": 35,
+								"loc": {
+									"start": {
+										"line": 2,
+										"column": 12
+									},
+									"end": {
+										"line": 2,
+										"column": 13
+									}
+								},
+								"name": "a"
 							},
-							"id": null,
-							"expression": false,
-							"generator": false,
-							"async": false,
-							"params": [
-								{
+							{
+								"type": "Identifier",
+								"start": 37,
+								"end": 38,
+								"loc": {
+									"start": {
+										"line": 2,
+										"column": 15
+									},
+									"end": {
+										"line": 2,
+										"column": 16
+									}
+								},
+								"name": "b"
+							},
+							{
+								"type": "Identifier",
+								"start": 40,
+								"end": 41,
+								"loc": {
+									"start": {
+										"line": 2,
+										"column": 18
+									},
+									"end": {
+										"line": 2,
+										"column": 19
+									}
+								},
+								"name": "c"
+							}
+						]
+					},
+					{
+						"type": "Attribute",
+						"start": 41,
+						"end": 42,
+						"name": ")",
+						"value": true
+					}
+				],
+				"fragment": {
+					"type": "Fragment",
+					"nodes": []
+				}
+			},
+			{
+				"type": "Text",
+				"start": 49,
+				"end": 50,
+				"raw": "\n",
+				"data": "\n"
+			},
+			{
+				"type": "RegularElement",
+				"start": 50,
+				"end": 78,
+				"name": "div",
+				"attributes": [
+					{
+						"type": "AttachTag",
+						"start": 55,
+						"end": 70,
+						"attachments": [
+							{
+								"type": "SpreadElement",
+								"start": 62,
+								"end": 70,
+								"loc": {
+									"start": {
+										"line": 3,
+										"column": 12
+									},
+									"end": {
+										"line": 3,
+										"column": 20
+									}
+								},
+								"argument": {
 									"type": "Identifier",
-									"start": 15,
-									"end": 19,
+									"start": 65,
+									"end": 70,
 									"loc": {
 										"start": {
-											"line": 1,
+											"line": 3,
 											"column": 15
 										},
 										"end": {
-											"line": 1,
-											"column": 19
+											"line": 3,
+											"column": 20
 										}
 									},
-									"name": "node"
+									"name": "stuff"
 								}
-							],
-							"body": {
-								"type": "BlockStatement",
-								"start": 24,
-								"end": 26,
+							}
+						]
+					},
+					{
+						"type": "Attribute",
+						"start": 70,
+						"end": 71,
+						"name": ")",
+						"value": true
+					}
+				],
+				"fragment": {
+					"type": "Fragment",
+					"nodes": []
+				}
+			},
+			{
+				"type": "Text",
+				"start": 78,
+				"end": 79,
+				"raw": "\n",
+				"data": "\n"
+			},
+			{
+				"type": "RegularElement",
+				"start": 79,
+				"end": 116,
+				"name": "div",
+				"attributes": [
+					{
+						"type": "AttachTag",
+						"start": 84,
+						"end": 108,
+						"attachments": [
+							{
+								"type": "Identifier",
+								"start": 91,
+								"end": 92,
 								"loc": {
 									"start": {
-										"line": 1,
-										"column": 24
+										"line": 4,
+										"column": 12
 									},
 									"end": {
-										"line": 1,
-										"column": 26
+										"line": 4,
+										"column": 13
 									}
 								},
-								"body": []
-							}
-						}
-					},
-					{
-						"type": "AttachTag",
-						"start": 28,
-						"end": 50,
-						"expression": {
-							"type": "ArrowFunctionExpression",
-							"start": 37,
-							"end": 49,
-							"loc": {
-								"start": {
-									"line": 1,
-									"column": 37
+								"name": "a"
+							},
+							{
+								"type": "Identifier",
+								"start": 94,
+								"end": 95,
+								"loc": {
+									"start": {
+										"line": 4,
+										"column": 15
+									},
+									"end": {
+										"line": 4,
+										"column": 16
+									}
 								},
-								"end": {
-									"line": 1,
-									"column": 49
-								}
+								"name": "b"
 							},
-							"id": null,
-							"expression": false,
-							"generator": false,
-							"async": false,
-							"params": [
-								{
+							{
+								"type": "Identifier",
+								"start": 97,
+								"end": 98,
+								"loc": {
+									"start": {
+										"line": 4,
+										"column": 18
+									},
+									"end": {
+										"line": 4,
+										"column": 19
+									}
+								},
+								"name": "c"
+							},
+							{
+								"type": "SpreadElement",
+								"start": 100,
+								"end": 108,
+								"loc": {
+									"start": {
+										"line": 4,
+										"column": 21
+									},
+									"end": {
+										"line": 4,
+										"column": 29
+									}
+								},
+								"argument": {
 									"type": "Identifier",
-									"start": 38,
-									"end": 42,
+									"start": 103,
+									"end": 108,
 									"loc": {
 										"start": {
-											"line": 1,
-											"column": 38
+											"line": 4,
+											"column": 24
 										},
 										"end": {
-											"line": 1,
-											"column": 42
+											"line": 4,
+											"column": 29
 										}
 									},
-									"name": "node"
+									"name": "stuff"
 								}
-							],
-							"body": {
-								"type": "BlockStatement",
-								"start": 47,
-								"end": 49,
+							}
+						]
+					},
+					{
+						"type": "Attribute",
+						"start": 108,
+						"end": 109,
+						"name": ")",
+						"value": true
+					}
+				],
+				"fragment": {
+					"type": "Fragment",
+					"nodes": []
+				}
+			},
+			{
+				"type": "Text",
+				"start": 116,
+				"end": 117,
+				"raw": "\n",
+				"data": "\n"
+			},
+			{
+				"type": "RegularElement",
+				"start": 117,
+				"end": 149,
+				"name": "div",
+				"attributes": [
+					{
+						"type": "AttachTag",
+						"start": 122,
+						"end": 141,
+						"attachments": [
+							{
+								"type": "ArrowFunctionExpression",
+								"start": 129,
+								"end": 141,
 								"loc": {
 									"start": {
-										"line": 1,
-										"column": 47
+										"line": 5,
+										"column": 12
 									},
 									"end": {
-										"line": 1,
-										"column": 49
+										"line": 5,
+										"column": 24
 									}
 								},
-								"body": []
+								"id": null,
+								"expression": false,
+								"generator": false,
+								"async": false,
+								"params": [
+									{
+										"type": "Identifier",
+										"start": 130,
+										"end": 134,
+										"loc": {
+											"start": {
+												"line": 5,
+												"column": 13
+											},
+											"end": {
+												"line": 5,
+												"column": 17
+											}
+										},
+										"name": "node"
+									}
+								],
+								"body": {
+									"type": "BlockStatement",
+									"start": 139,
+									"end": 141,
+									"loc": {
+										"start": {
+											"line": 5,
+											"column": 22
+										},
+										"end": {
+											"line": 5,
+											"column": 24
+										}
+									},
+									"body": []
+								}
 							}
-						}
+						]
+					},
+					{
+						"type": "Attribute",
+						"start": 141,
+						"end": 142,
+						"name": ")",
+						"value": true
 					}
 				],
 				"fragment": {
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-basic/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-basic/main.svelte
index 1a1f74e4a94a..2f80374306bb 100644
--- a/packages/svelte/tests/runtime-runes/samples/attachment-basic/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-basic/main.svelte
@@ -1 +1 @@
-<div {@attach (node) => node.textContent = node.nodeName}></div>
+<div attach((node) => node.textContent = node.nodeName)></div>
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte
index 3468ee50a2cd..f78921f8e1e7 100644
--- a/packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-component/main.svelte
@@ -2,4 +2,4 @@
 	import Child from './Child.svelte';
 </script>
 
-<Child {@attach (node) => node.textContent = 'set from component'} />
+<Child attach((node) => node.textContent = 'set from component') />
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte
index 9fa3cfdb6798..c57e54d93627 100644
--- a/packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-reactive/main.svelte
@@ -2,5 +2,5 @@
 	let value = $state(1);
 </script>
 
-<div {@attach (node) => node.textContent = value}></div>
+<div attach((node) => node.textContent = value)></div>
 <button onclick={() => value += 1}>increment</button>
diff --git a/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte b/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte
index bd4b52342f32..942332791f35 100644
--- a/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte
+++ b/packages/svelte/tests/runtime-runes/samples/attachment-svelte-element/main.svelte
@@ -1 +1 @@
-<svelte:element this={'div'} {@attach (node) => node.textContent = node.nodeName}></svelte:element>
+<svelte:element this={'div'} attach((node) => node.textContent = node.nodeName)></svelte:element>
diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index 7b3a77ae6fce..730cda45f51f 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -622,7 +622,7 @@ declare module 'svelte/animate' {
 }
 
 declare module 'svelte/compiler' {
-	import type { Expression, Identifier, ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree';
+	import type { Expression, Identifier, ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression, SpreadElement } from 'estree';
 	import type { SourceMap } from 'magic-string';
 	import type { Location } from 'locate-character';
 	/**
@@ -1053,7 +1053,7 @@ declare module 'svelte/compiler' {
 		/** A `{@attach foo(...)} tag */
 		export interface AttachTag extends BaseNode {
 			type: 'AttachTag';
-			expression: Expression;
+			attachments: Array<Expression | SpreadElement>;
 		}
 
 		/** An `animate:` directive */

From 9a1805bda9a3392483d271be13641f73b8e59f83 Mon Sep 17 00:00:00 2001
From: Rich Harris <rich.harris@vercel.com>
Date: Tue, 21 Jan 2025 12:33:14 -0500
Subject: [PATCH 2/4] fix test

---
 .../samples/attachments/output.json           | 45 +++----------------
 1 file changed, 5 insertions(+), 40 deletions(-)

diff --git a/packages/svelte/tests/parser-modern/samples/attachments/output.json b/packages/svelte/tests/parser-modern/samples/attachments/output.json
index 69617856e3ec..b2edd4bfd9a9 100644
--- a/packages/svelte/tests/parser-modern/samples/attachments/output.json
+++ b/packages/svelte/tests/parser-modern/samples/attachments/output.json
@@ -14,7 +14,7 @@
 				"name": "div",
 				"attributes": [
 					{
-						"type": "AttachTag",
+						"type": "Attachment",
 						"start": 5,
 						"end": 13,
 						"attachments": [
@@ -35,13 +35,6 @@
 								"name": "a"
 							}
 						]
-					},
-					{
-						"type": "Attribute",
-						"start": 13,
-						"end": 14,
-						"name": ")",
-						"value": true
 					}
 				],
 				"fragment": {
@@ -63,7 +56,7 @@
 				"name": "div",
 				"attributes": [
 					{
-						"type": "AttachTag",
+						"type": "Attachment",
 						"start": 27,
 						"end": 41,
 						"attachments": [
@@ -116,13 +109,6 @@
 								"name": "c"
 							}
 						]
-					},
-					{
-						"type": "Attribute",
-						"start": 41,
-						"end": 42,
-						"name": ")",
-						"value": true
 					}
 				],
 				"fragment": {
@@ -144,7 +130,7 @@
 				"name": "div",
 				"attributes": [
 					{
-						"type": "AttachTag",
+						"type": "Attachment",
 						"start": 55,
 						"end": 70,
 						"attachments": [
@@ -180,13 +166,6 @@
 								}
 							}
 						]
-					},
-					{
-						"type": "Attribute",
-						"start": 70,
-						"end": 71,
-						"name": ")",
-						"value": true
 					}
 				],
 				"fragment": {
@@ -208,7 +187,7 @@
 				"name": "div",
 				"attributes": [
 					{
-						"type": "AttachTag",
+						"type": "Attachment",
 						"start": 84,
 						"end": 108,
 						"attachments": [
@@ -292,13 +271,6 @@
 								}
 							}
 						]
-					},
-					{
-						"type": "Attribute",
-						"start": 108,
-						"end": 109,
-						"name": ")",
-						"value": true
 					}
 				],
 				"fragment": {
@@ -320,7 +292,7 @@
 				"name": "div",
 				"attributes": [
 					{
-						"type": "AttachTag",
+						"type": "Attachment",
 						"start": 122,
 						"end": 141,
 						"attachments": [
@@ -378,13 +350,6 @@
 								}
 							}
 						]
-					},
-					{
-						"type": "Attribute",
-						"start": 141,
-						"end": 142,
-						"name": ")",
-						"value": true
 					}
 				],
 				"fragment": {

From 1d4cb8eab3f637e6ff1e09fbafa64b9ec9cf5416 Mon Sep 17 00:00:00 2001
From: Rich Harris <rich.harris@vercel.com>
Date: Tue, 21 Jan 2025 12:37:54 -0500
Subject: [PATCH 3/4] regenerate

---
 packages/svelte/types/index.d.ts | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts
index 730cda45f51f..ad328b11acf4 100644
--- a/packages/svelte/types/index.d.ts
+++ b/packages/svelte/types/index.d.ts
@@ -1050,9 +1050,9 @@ declare module 'svelte/compiler' {
 			expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression });
 		}
 
-		/** A `{@attach foo(...)} tag */
-		export interface AttachTag extends BaseNode {
-			type: 'AttachTag';
+		/** An `attach(...)` attribute */
+		export interface Attachment extends BaseNode {
+			type: 'Attachment';
 			attachments: Array<Expression | SpreadElement>;
 		}
 
@@ -1138,7 +1138,7 @@ declare module 'svelte/compiler' {
 
 		interface BaseElement extends BaseNode {
 			name: string;
-			attributes: Array<Attribute | SpreadAttribute | Directive | AttachTag>;
+			attributes: Array<Attribute | SpreadAttribute | Directive | Attachment>;
 			fragment: Fragment;
 		}
 
@@ -1328,7 +1328,7 @@ declare module 'svelte/compiler' {
 			| AST.Attribute
 			| AST.SpreadAttribute
 			| Directive
-			| AST.AttachTag
+			| AST.Attachment
 			| AST.Comment
 			| Block;
 

From 35b9d21316ffad7218bf180f25cc1e1600484e25 Mon Sep 17 00:00:00 2001
From: Rich Harris <rich.harris@vercel.com>
Date: Tue, 21 Jan 2025 12:41:14 -0500
Subject: [PATCH 4/4] fix

---
 .../3-transform/client/visitors/Attachment.js | 30 +++++++------------
 .../client/dom/elements/attachments.js        |  2 +-
 2 files changed, 12 insertions(+), 20 deletions(-)

diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Attachment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Attachment.js
index 735f3a2cab4a..8c8742769e0d 100644
--- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Attachment.js
+++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Attachment.js
@@ -9,26 +9,18 @@ import * as b from '../../../../utils/builders.js';
  */
 export function Attachment(node, context) {
 	for (const attachment of node.attachments) {
-		if (attachment.type === 'SpreadElement') {
-			context.state.init.push(
-				b.stmt(
-					b.call(
-						'$.attach_all',
-						context.state.node,
-						b.thunk(/** @type {Expression} */ (context.visit(attachment.argument)))
+		context.state.init.push(
+			b.stmt(
+				b.call(
+					'$.attach',
+					context.state.node,
+					b.thunk(
+						/** @type {Expression} */ (
+							context.visit(attachment.type === 'SpreadElement' ? attachment.argument : attachment)
+						)
 					)
 				)
-			);
-		} else {
-			context.state.init.push(
-				b.stmt(
-					b.call(
-						'$.attach',
-						context.state.node,
-						b.thunk(/** @type {Expression} */ (context.visit(attachment)))
-					)
-				)
-			);
-		}
+			)
+		);
 	}
 }
diff --git a/packages/svelte/src/internal/client/dom/elements/attachments.js b/packages/svelte/src/internal/client/dom/elements/attachments.js
index c758f20d07f0..6c491f3f5ed1 100644
--- a/packages/svelte/src/internal/client/dom/elements/attachments.js
+++ b/packages/svelte/src/internal/client/dom/elements/attachments.js
@@ -11,7 +11,7 @@ export function attach(node, get_fn) {
 		if (Array.isArray(attachment)) {
 			for (const fn of attachment) {
 				if (fn) {
-					$effect(() => fn(node));
+					effect(() => fn(node));
 				}
 			}
 		} else if (attachment) {