Skip to content

feat: attachments #15000

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 36 commits into from
May 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
531e75d
parse attachments
Rich-Harris Dec 20, 2024
b29e1e3
basic attachments working
Rich-Harris Dec 20, 2024
2329284
working
Rich-Harris Dec 20, 2024
0c914eb
rename to attach
Rich-Harris Dec 20, 2024
1988ba4
fix
Rich-Harris Dec 21, 2024
e1b940c
restrict which symbols are recognised as attachment keys
Rich-Harris Dec 23, 2024
ed3bf01
merge main
Rich-Harris Jan 13, 2025
0690ba2
allow cleanup to be returned directly
Rich-Harris Jan 13, 2025
e0620a1
changeset
Rich-Harris Jan 13, 2025
2ae3aa0
fix
Rich-Harris Jan 13, 2025
7046427
lint
Rich-Harris Jan 14, 2025
85cc9bc
remove createAttachmentKey/isAttachmentKey
Rich-Harris Jan 14, 2025
bec5708
fix spreading of symbol properties onto component
Rich-Harris Jan 14, 2025
afab150
types
Rich-Harris Jan 14, 2025
7e5d4d9
fix
Rich-Harris Jan 14, 2025
c599e90
update name
Rich-Harris Jan 14, 2025
8699771
reserve ability to use sequence expressions in future
Rich-Harris Jan 14, 2025
1664fd8
Update packages/svelte/src/internal/client/dom/elements/attachments.js
Rich-Harris Jan 15, 2025
6402161
actually let's do this instead
Rich-Harris Jan 15, 2025
d09cdf6
expose createAttachmentKey
Rich-Harris May 13, 2025
8239d57
make room for `@attach` docs
Rich-Harris May 13, 2025
df3342f
add docs
Rich-Harris May 13, 2025
a466021
failing test
Rich-Harris May 13, 2025
d6d9f0c
fix
Rich-Harris May 13, 2025
5f7d9dd
lock down
Rich-Harris May 13, 2025
dd8c17a
merge/fix
Rich-Harris May 13, 2025
fe78c1c
add missing reference docs
Rich-Harris May 14, 2025
be46c94
prevent conflicts
Rich-Harris May 14, 2025
f8e6696
update docs
Rich-Harris May 14, 2025
b880f74
regenerate
Rich-Harris May 14, 2025
a7aa1a7
fix link
Rich-Harris May 14, 2025
bd0157a
add Attachment interface
Rich-Harris May 14, 2025
c69c5d7
beef up test
Rich-Harris May 14, 2025
999a05a
regenerate
Rich-Harris May 14, 2025
339ce84
tweak types
Rich-Harris May 14, 2025
8255bc6
fix
Rich-Harris May 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/poor-days-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: attachments
138 changes: 138 additions & 0 deletions documentation/docs/03-template-syntax/[email protected]
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
---
title: {@attach ...}
---

Attachments are functions that run when an element is mounted to the DOM. Optionally, they can return a function that is called when the element is later removed from the DOM.

> [!NOTE]
> Attachments are available in Svelte 5.29 and newer.

```svelte
<!--- file: App.svelte --->
<script>
/** @type {import('svelte/attachments').Attachment} */
function myAttachment(element) {
console.log(element.nodeName); // 'DIV'

return () => {
console.log('cleaning up');
};
}
</script>

<div {@attach myAttachment}>...</div>
```

An element can have any number of attachments.

## Attachment factories

A useful pattern is for a function, such as `tooltip` in this example, to _return_ an attachment ([demo](/playground/untitled#H4sIAAAAAAAAE3VT0XLaMBD8lavbDiaNCUlbHhTItG_5h5AH2T5ArdBppDOEMv73SkbGJGnH47F9t3un3TsfMyO3mInsh2SW1Sa7zlZKo8_E0zHjg42pGAjxBPxp7cTvUHOMldLjv-IVGUbDoUw295VTlh-WZslqa8kxsLL2ACtHWxh175NffnQfAAGikSGxYQGfPEvGfPSIWtOH0TiBVo2pWJEBJtKhQp4YYzjG9JIdcuMM5IZqHMPioY8vOSA997zQoevf4a7heO7cdp34olRiTGr07OhwH1IdoO2A7dLMbwahZq6MbRhKZWqxk7rBxTGVbuHmhCgb5qDgmIx_J6XtHHukHTrYYqx_YpzYng8aO4RYayql7hU-1ZJl0akqHBE_D9KLolwL-Dibzc7iSln9XjtqTF1UpMkJ2EmXR-BgQErsN4pxIJKr0RVO1qrxAqaTO4fbc9bKulZm3cfDY3aZDgvFGErWjmzhN7KmfX5rXyDeX8Pt1mU-hXjdBOrtuB97vK4GPUtmJ41XcRMEGDLD8do0nJ73zhUhSlyRw0t3vPqD8cjfLs-axiFgNBrkUd9Ulp50c-GLxlXAVlJX-ffpZyiSn7H0eLCUySZQcQdXlxj4El0Yv_FZvIKElqqGTruVLhzu7VRKCh22_5toOyxsWqLwwzK-cCbYNdg-hy-p9D7sbiZWUnts_wLUOF3CJgQAAA==)):

```svelte
<!--- file: App.svelte --->
<script>
import tippy from 'tippy.js';

let content = $state('Hello!');

/**
* @param {string} content
* @returns {import('svelte/attachments').Attachment}
*/
function tooltip(content) {
return (element) => {
const tooltip = tippy(element, { content });
return tooltip.destroy;
};
}
</script>

<input bind:value={content} />

<button {@attach tooltip(content)}>
Hover me
</button>
```

Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes.

## Inline attachments

Attachments can also be created inline ([demo](/playground/untitled#H4sIAAAAAAAAE71WbW_aSBD-Kyt0VaBJyGKbqoUkOhdI68qGUkh6pPSDMY6xYwyH12Ab8d9vZtYE6DX38aQQe3fennlm1jvbUmTP3VKj9KcthO3MShelJz9041Ljx7YksiWKcAP2C0V9uazGazcUuDexY_d3-84iEm4kwE3pOnZW_lLcjqOx8OfLxUqwLVvafiTYjj2tFnN2Vr3yVvbUB4NqEJ81x9H11cEounbsaG3HaL_xp2J2s1WVHa5mru_NxMtyW6TAytKgwm5u2RYlYwF4YsEIVSrYDZMaVc8VLblXPlOmZ5UmxkP9P9ynJ9cR5fKxk7EIXQGQIV9wsXL_TtxY6JE_t4W_iO5wv_yURA6uWLhYLMuicrAdi_-2RAMCUGgTReUC8gUTB9mueC2WK1ckq4j9AhVytiPHDX_Fh_-PXBVvhcsdEHl7fSXZkeTHIgtdKp7c3UegUjRYjfM3hQ9ZjpOty407efbF5dyOnxssWYXlcWlqC7sBmDz3Kl575-k8bGIXvdMuvn7uKo_Zx3Ayv_Mnn-7FaH4X2Mo0m6gPyWObR5P5g2q0dc9q6fVeS8uMdifttRxvOg_DKf-ydkEHZBuj_ayZgeFZw472JfuoTb6niZPzyP78jTvtxdpUp-o0q6tWVl87c2dtBfrGan3Ip3Mn-hqkm9Ff3xbGp_6HLwqvWwOtDnFqZvAYmMPOxgyehTW8T7oZzy1fU8yhAXuW6La9xPJ5arW0fNLiWTfTamCnmsED2DlJd6BzM3DA1gBbPQVbDv444Qw6iTXgKfjxwC43B5QbyDzPgrXRNlAm0MZoW0nX5_B06Ak-Mc-k10L7kQe81M3gHvYAz0CvkTwAvC2IOdDT7kADDq0MdSHvxMp0XnAJeXyLrQCx8hTxj3J6L2Igbp5KDIRbSNw6YSPcuDfsI5ac8KI80yFWX0AeitHuox4-pa-BpoEvzKMOOSMfWDeBGIFXwP4gzOE9cu71kF_FEpgf8AF-eYq4wQZ5z8A_2HtUF_LRwjXEaYFvrBnkA7rg00L9pCfjJYjHxNzmG8qbeBlgjndBwT1ypyCG7gtPngcY-aTd8TBPM-h41vfiiX6hjsAT9g3yw4t9ReLGdR_rSjUEOfBDtQRcyKUhSI4cwG_SNlTiD3vou5XiO2IB_zniBhusJeanORnHPpLcU92oZ9F3RjUiTizkDnx2BPUv4KK6Qc9RHIwbTGPZ632vCzqjDHlxEFOK9l3C-Yx1UiQ_XDtgkjUkf0MjR59QJ5XiEqZ-geMZasBzmds9YIK-xadPfIkenTsPsWPP_YYHB2OkxXlIqT6DopYDXaOa-1i_jvwW0JkiPHhG8AwUsfpYV6gF4tFzeXYQD9ZDo76kHoV1l3r5MYa9WtG3VA-sPfYKxW5xhbiRvYm9IqhX8HwO8Ix0UL8471hLOtd16mPip4N5UR6AgRdnJ8dvCMip1vCjbw3khfFS6h9lI-jswjnHnpY16yPHWdGPGeHzMcdZTj1J_d3B_JVRjvnopCv5wD7RVdLDPqG4kscTTpQNfvPgbI3g_f-pS4--a3TGUynH_hvJb9QpDzXJg3fo3eyld1Xq3YHjmbn23lTh7sm1m3Gpwur8Df2umMq16vtlyqLF5cpdurb4zb12Gfu522Dv-HruR_IWpQGmuDdhGMILvNQQq8TdXbwyVB3NP6dT1angaKxyUxqlXuaNf40L8qKWg8-W0XV9weQdDYPXzX4YqsprvXlQpru5Dbf0kRIMSsZ-u8wvGPydeNxPTk-LFSvjlLQEY96Ex_XBXxWv_mroRp6Yoej8hmmV0wnNB7MlEK81j3dT2PXZGxnyRJKBpOyDAYkq7Pb2FsLupzips3KnoPVOY-esXFPes7csrewtYA8Eb5lli1k19qOyAAkMMLxyEsZbuW70i5MMnRR8HntxFvErXiZhguMfmL8gPOXmB3DC-E8aEafNVzVqqEGQXtdRUAcDvq6ioopSr-97tugAqvcyOar3iy3VnZLanbb1T1jZfrjxo2mp8WSHsbv7Bx1mHBBZDAAA)):

```svelte
<!--- file: App.svelte --->
<script>
import { paint } from './gradient.js';
</script>

<canvas
width={32}
height={32}
{@attach (canvas) => {
const context = canvas.getContext('2d');

$effect(() => {
let frame = requestAnimationFrame(function loop(t) {
frame = requestAnimationFrame(loop);
paint(context, t);
});

return () => {
cancelAnimationFrame(frame);
};
});
}}
></canvas>
```

## Passing attachments to components

When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments.

This allows you to create _wrapper components_ that augment elements ([demo](/playground/untitled#H4sIAAAAAAAAE3VUS3ObMBD-KxvajnFqsJM2PhA7TXrKob31FjITAbKtRkiMtDhJPfz3LiAMdpxhGJvdb1_fPnaeYjn3Iu-WIbJ04028lZDcetHDzsO3olbVApI74F1RhHbLJdayhFl-Sp5qhVwhufEWNjWiwJtYxSjyQhsEFEXxBiujcxg1_8O_dnQ9APwsEbVyiHDafjrvDZCgkiO4MLCEzxYZcn90z6XUZ6OxA61KlaIgV6i1pFC-sxjDrlbHaDiWRoGvdMbHsLzp5DES0mJnRxGaRBvcBHb7yFUTCQeunEWYcYtGv12TqgFUDbCK1WLaM6IWQhUlQiJUFm2ZLPly51xXMG0Rjoyd69C7UqqG2nu95QZyXvtvLVpri2-SN4hoLXXCZFfhQ8aQBU1VgdEaH_vSgyBZR_BpPp_vi0tY-rw2ulRZkGqpTQRbZvwa2BPgFC8bgbw31CbjJjAsE6WNYBZeGp7vtQXLMqHWnZx-5kM1TR5ycpkZXQR2wzL94l8Ur1C_3-g168SfQf1MyfRi3LW9fs77emJEw5QV9SREoLTq06tcczq7d6xEUcJX2vAhO1b843XK34e5unZEMBr15ekuKEusluWAF8lXhE2ZTP2r2RcIHJ-163FPKerCgYJLOB9i4GvNwviI5-gAQiFFBk3tBTOU3HFXEk0R8o86WvUD64aINhv5K3oRmpJXkw8uxMG6Hh6JY9X7OwGSqfUy9tDG3sHNoEi0d_d_fv9qndxRU0VClFqo3KVo3U655Hnt1PXB3Qra2Y2QGdEwgTAMCxopsoxOe6SD0gD8movDhT0LAnhqlE8gVCpLWnRoV7OJCkFAwEXitrYL1W7p7pbiE_P7XH6E_rihODm5s52XtiH9Ekaw0VgI9exadWL1uoEYjPtg2672k5szsxbKyWB2fdT0w5Y_0hcT8oXOlRetmLS8-g-6TLXXQgYAAA==)):

```svelte
<!--- file: Button.svelte --->
<script>
/** @type {import('svelte/elements').HTMLButtonAttributes} */
let { children, ...props } = $props();
</script>

<!-- `props` includes attachments -->
<button {...props}>
{@render children?.()}
</button>
```

```svelte
<!--- file: App.svelte --->
<script>
import tippy from 'tippy.js';
import Button from './Button.svelte';

let content = $state('Hello!');

/**
* @param {string} content
* @returns {import('svelte/attachments').Attachment}
*/
function tooltip(content) {
return (element) => {
const tooltip = tippy(element, { content });
return tooltip.destroy;
};
}
</script>

<input bind:value={content} />

<Button {@attach tooltip(content)}>
Hover me
</Button>
```

## Creating attachments programmatically

To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
title: use:
---

> [!NOTE]
> In Svelte 5.29 and newer, consider using [attachments](@attach) instead, as they are more flexible and composable.

Actions are functions that are called when an element is mounted. They are added with the `use:` directive, and will typically use an `$effect` so that they can reset any state when the element is unmounted:

```svelte
Expand Down
5 changes: 5 additions & 0 deletions documentation/docs/98-reference/21-svelte-attachments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
title: svelte/attachments
---

> MODULE: svelte/attachments
5 changes: 5 additions & 0 deletions packages/svelte/elements.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.8

import type { Attachment } from 'svelte/attachments';

// Note: We also allow `null` as a valid value because Svelte treats this the same as `undefined`

type Booleanish = boolean | 'true' | 'false';
Expand Down Expand Up @@ -860,6 +862,9 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D

// allow any data- attribute
[key: `data-${string}`]: any;

// allow any attachment
[key: symbol]: Attachment<T>;
}

export type HTMLAttributeAnchorTarget = '_self' | '_blank' | '_parent' | '_top' | (string & {});
Expand Down
4 changes: 4 additions & 0 deletions packages/svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@
"types": "./types/index.d.ts",
"default": "./src/animate/index.js"
},
"./attachments": {
"types": "./types/index.d.ts",
"default": "./src/attachments/index.js"
},
"./compiler": {
"types": "./types/index.d.ts",
"require": "./compiler/index.js",
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/scripts/generate-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ await createBundle({
[pkg.name]: `${dir}/src/index.d.ts`,
[`${pkg.name}/action`]: `${dir}/src/action/public.d.ts`,
[`${pkg.name}/animate`]: `${dir}/src/animate/public.d.ts`,
[`${pkg.name}/attachments`]: `${dir}/src/attachments/public.d.ts`,
[`${pkg.name}/compiler`]: `${dir}/src/compiler/public.d.ts`,
[`${pkg.name}/easing`]: `${dir}/src/easing/index.js`,
[`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`,
Expand Down
27 changes: 27 additions & 0 deletions packages/svelte/src/attachments/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ATTACHMENT_KEY } from '../constants.js';

/**
* Creates an object key that will be recognised as an attachment when the object is spread onto an element,
* as a programmatic alternative to using `{@attach ...}`. This can be useful for library authors, though
* is generally not needed when building an app.
*
* ```svelte
* <script>
* import { createAttachmentKey } from 'svelte/attachments';
*
* const props = {
* class: 'cool',
* onclick: () => alert('clicked'),
* [createAttachmentKey()]: (node) => {
* node.textContent = 'attached!';
* }
* };
* </script>
*
* <button {...props}>click me</button>
* ```
* @since 5.29
*/
export function createAttachmentKey() {
return Symbol(ATTACHMENT_KEY);
}
12 changes: 12 additions & 0 deletions packages/svelte/src/attachments/public.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* An [attachment](https://svelte.dev/docs/svelte/@attach) is a function that runs when an element is mounted
* to the DOM, and optionally returns a function that is called when the element is later removed.
*
* It can be attached to an element with an `{@attach ...}` tag, or by spreading an object containing
* a property created with [`createAttachmentKey`](https://svelte.dev/docs/svelte/svelte-attachments#createAttachmentKey).
*/
export interface Attachment<T extends EventTarget = Element> {
(element: T): void | (() => void);
}

export * from './index.js';
2 changes: 1 addition & 1 deletion packages/svelte/src/compiler/phases/1-parse/read/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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>} attributes
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag>} attributes
* @returns {AST.Script}
*/
export function read_script(parser, start, attributes) {
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/compiler/phases/1-parse/read/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/;
/**
* @param {Parser} parser
* @param {number} start
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive>} attributes
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag>} attributes
* @returns {AST.CSS.StyleSheet}
*/
export default function read_style(parser, start, attributes) {
Expand Down
23 changes: 22 additions & 1 deletion packages/svelte/src/compiler/phases/1-parse/state/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -480,14 +480,35 @@ function read_static_attribute(parser) {

/**
* @param {Parser} parser
* @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | null}
* @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag | null}
*/
function read_attribute(parser) {
const start = parser.index;

if (parser.eat('{')) {
parser.allow_whitespace();

if (parser.eat('@attach')) {
parser.require_whitespace();

const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);

/** @type {AST.AttachTag} */
const attachment = {
type: 'AttachTag',
start,
end: parser.index,
expression,
metadata: {
expression: create_expression_metadata()
}
};

return attachment;
}

if (parser.eat('...')) {
const expression = read_expression(parser);

Expand Down
2 changes: 2 additions & 0 deletions packages/svelte/src/compiler/phases/2-analyze/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js';
import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js';
import { ArrowFunctionExpression } from './visitors/ArrowFunctionExpression.js';
import { AssignmentExpression } from './visitors/AssignmentExpression.js';
import { AttachTag } from './visitors/AttachTag.js';
import { Attribute } from './visitors/Attribute.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
import { BindDirective } from './visitors/BindDirective.js';
Expand Down Expand Up @@ -133,6 +134,7 @@ const visitors = {
},
ArrowFunctionExpression,
AssignmentExpression,
AttachTag,
Attribute,
AwaitBlock,
BindDirective,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */

import { mark_subtree_dynamic } from './shared/fragment.js';

/**
* @param {AST.AttachTag} node
* @param {Context} context
*/
export function AttachTag(node, context) {
mark_subtree_dynamic(context.path);
context.next({ ...context.state, expression: node.metadata.expression });
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { AnalysisState, Context } from '../../types' */
import * as e from '../../../../errors.js';
Expand Down Expand Up @@ -74,7 +75,8 @@ export function visit_component(node, context) {
attribute.type !== 'SpreadAttribute' &&
attribute.type !== 'LetDirective' &&
attribute.type !== 'OnDirective' &&
attribute.type !== 'BindDirective'
attribute.type !== 'BindDirective' &&
attribute.type !== 'AttachTag'
) {
e.component_invalid_directive(attribute);
}
Expand All @@ -91,15 +93,10 @@ export function visit_component(node, context) {
validate_attribute(attribute, node);

if (is_expression_attribute(attribute)) {
const expression = get_attribute_expression(attribute);
if (expression.type === 'SequenceExpression') {
let i = /** @type {number} */ (expression.start);
while (--i > 0) {
const char = context.state.analysis.source[i];
if (char === '(') break; // parenthesized sequence expressions are ok
if (char === '{') e.attribute_invalid_sequence_expression(expression);
}
}
disallow_unparenthesized_sequences(
get_attribute_expression(attribute),
context.state.analysis.source
);
}
}

Expand All @@ -113,6 +110,10 @@ 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" .../>` —
Expand Down Expand Up @@ -158,3 +159,18 @@ export function visit_component(node, context) {
context.visit({ ...node.fragment, nodes: nodes[slot_name] }, state);
}
}

/**
* @param {Expression} expression
* @param {string} source
*/
function disallow_unparenthesized_sequences(expression, source) {
if (expression.type === 'SequenceExpression') {
let i = /** @type {number} */ (expression.start);
while (--i > 0) {
const char = source[i];
if (char === '(') break; // parenthesized sequence expressions are ok
if (char === '{') e.attribute_invalid_sequence_expression(expression);
}
}
}
Loading