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} attributes + * @param {Array} 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} attributes + * @param {Array} 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 — `` — 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..8c8742769e0d --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Attachment.js @@ -0,0 +1,26 @@ +/** @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) { + context.state.init.push( + b.stmt( + b.call( + '$.attach', + context.state.node, + b.thunk( + /** @type {Expression} */ ( + context.visit(attachment.type === 'SpreadElement' ? attachment.argument : 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} */ + /** @type {Array} */ 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; } /** An `animate:` directive */ @@ -279,7 +280,7 @@ export namespace AST { interface BaseElement extends BaseNode { name: string; - attributes: Array; + attributes: Array; 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..6c491f3f5ed1 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 @@ -
{}} {@attach (node) => {}}>
+
+
+
+
+
{})>
diff --git a/packages/svelte/tests/parser-modern/samples/attachments/output.json b/packages/svelte/tests/parser-modern/samples/attachments/output.json index 42e9880fccdd..b2edd4bfd9a9 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,346 @@ { "type": "RegularElement", "start": 0, - "end": 57, + "end": 21, "name": "div", "attributes": [ { - "type": "AttachTag", + "type": "Attachment", "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" + } + ] + } + ], + "fragment": { + "type": "Fragment", + "nodes": [] + } + }, + { + "type": "Text", + "start": 21, + "end": 22, + "raw": "\n", + "data": "\n" + }, + { + "type": "RegularElement", + "start": 22, + "end": 49, + "name": "div", + "attributes": [ + { + "type": "Attachment", + "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" + } + ] + } + ], + "fragment": { + "type": "Fragment", + "nodes": [] + } + }, + { + "type": "Text", + "start": 49, + "end": 50, + "raw": "\n", + "data": "\n" + }, + { + "type": "RegularElement", + "start": 50, + "end": 78, + "name": "div", + "attributes": [ + { + "type": "Attachment", + "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, + } + ] + } + ], + "fragment": { + "type": "Fragment", + "nodes": [] + } + }, + { + "type": "Text", + "start": 78, + "end": 79, + "raw": "\n", + "data": "\n" + }, + { + "type": "RegularElement", + "start": 79, + "end": 116, + "name": "div", + "attributes": [ + { + "type": "Attachment", + "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, + } + ] + } + ], + "fragment": { + "type": "Fragment", + "nodes": [] + } + }, + { + "type": "Text", + "start": 116, + "end": 117, + "raw": "\n", + "data": "\n" + }, + { + "type": "RegularElement", + "start": 117, + "end": 149, + "name": "div", + "attributes": [ + { + "type": "Attachment", + "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": [] + } } - } + ] } ], "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 @@ -
node.textContent = node.nodeName}>
+
node.textContent = node.nodeName)>
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'; - node.textContent = 'set from component'} /> + 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); -
node.textContent = value}>
+
node.textContent = value)>
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 @@ - node.textContent = node.nodeName}> + node.textContent = node.nodeName)> diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 7b3a77ae6fce..ad328b11acf4 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'; /** @@ -1050,10 +1050,10 @@ declare module 'svelte/compiler' { expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression }); } - /** 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; } /** An `animate:` directive */ @@ -1138,7 +1138,7 @@ declare module 'svelte/compiler' { interface BaseElement extends BaseNode { name: string; - attributes: Array; + attributes: Array; fragment: Fragment; } @@ -1328,7 +1328,7 @@ declare module 'svelte/compiler' { | AST.Attribute | AST.SpreadAttribute | Directive - | AST.AttachTag + | AST.Attachment | AST.Comment | Block;