Skip to content

Commit f2d4792

Browse files
committed
alternative attachment syntax
1 parent 6402161 commit f2d4792

File tree

18 files changed

+446
-157
lines changed

18 files changed

+446
-157
lines changed

packages/svelte/src/compiler/phases/1-parse/read/script.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const ALLOWED_ATTRIBUTES = ['context', 'generics', 'lang', 'module'];
1616
/**
1717
* @param {Parser} parser
1818
* @param {number} start
19-
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag>} attributes
19+
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.Attachment>} attributes
2020
* @returns {AST.Script}
2121
*/
2222
export function read_script(parser, start, attributes) {

packages/svelte/src/compiler/phases/1-parse/read/style.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const REGEX_HTML_COMMENT_CLOSE = /-->/;
1818
/**
1919
* @param {Parser} parser
2020
* @param {number} start
21-
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag>} attributes
21+
* @param {Array<AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.Attachment>} attributes
2222
* @returns {AST.CSS.StyleSheet}
2323
*/
2424
export default function read_style(parser, start, attributes) {

packages/svelte/src/compiler/phases/1-parse/state/element.js

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** @import { Expression } from 'estree' */
1+
/** @import { CallExpression, Expression } from 'estree' */
22
/** @import { AST } from '#compiler' */
33
/** @import { Parser } from '../index.js' */
44
import { is_void } from '../../../../utils.js';
@@ -14,6 +14,8 @@ import { get_attribute_expression, is_expression_attribute } from '../../../util
1414
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
1515
import { list } from '../../../utils/string.js';
1616
import { regex_whitespace } from '../../patterns.js';
17+
import { find_matching_bracket } from '../utils/bracket.js';
18+
import { parse_expression_at } from '../acorn.js';
1719

1820
const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/;
1921
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
@@ -480,31 +482,37 @@ function read_static_attribute(parser) {
480482

481483
/**
482484
* @param {Parser} parser
483-
* @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.AttachTag | null}
485+
* @returns {AST.Attribute | AST.SpreadAttribute | AST.Directive | AST.Attachment | null}
484486
*/
485487
function read_attribute(parser) {
486488
const start = parser.index;
487489

488-
if (parser.eat('{')) {
489-
parser.allow_whitespace();
490+
if (parser.match('attach(')) {
491+
const end = find_matching_bracket(parser.template, start + 7, '(');
490492

491-
if (parser.eat('@attach')) {
492-
parser.require_whitespace();
493+
if (end === undefined) {
494+
e.unexpected_eof(parser.template.length);
495+
}
493496

494-
const expression = read_expression(parser);
495-
parser.allow_whitespace();
496-
parser.eat('}', true);
497+
const sliced = parser.template.slice(0, end + 1);
497498

498-
/** @type {AST.AttachTag} */
499-
const attachment = {
500-
type: 'AttachTag',
501-
start,
502-
end: parser.index,
503-
expression
504-
};
499+
const call = /** @type {CallExpression} */ (parse_expression_at(sliced, parser.ts, start));
505500

506-
return attachment;
507-
}
501+
/** @type {AST.Attachment} */
502+
const attachment = {
503+
type: 'Attachment',
504+
start,
505+
end,
506+
attachments: call.arguments
507+
};
508+
509+
parser.index = end + 1;
510+
511+
return attachment;
512+
}
513+
514+
if (parser.eat('{')) {
515+
parser.allow_whitespace();
508516

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

packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export function visit_component(node, context) {
7676
attribute.type !== 'LetDirective' &&
7777
attribute.type !== 'OnDirective' &&
7878
attribute.type !== 'BindDirective' &&
79-
attribute.type !== 'AttachTag'
79+
attribute.type !== 'Attachment'
8080
) {
8181
e.component_invalid_directive(attribute);
8282
}
@@ -110,10 +110,6 @@ export function visit_component(node, context) {
110110
if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
111111
context.state.analysis.uses_component_bindings = true;
112112
}
113-
114-
if (attribute.type === 'AttachTag') {
115-
disallow_unparenthesized_sequences(attribute.expression, context.state.analysis.source);
116-
}
117113
}
118114

119115
// If the component has a slot attribute — `<Foo slot="whatever" .../>` —

packages/svelte/src/compiler/phases/3-transform/client/transform-client.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ import { TitleElement } from './visitors/TitleElement.js';
5656
import { TransitionDirective } from './visitors/TransitionDirective.js';
5757
import { UpdateExpression } from './visitors/UpdateExpression.js';
5858
import { UseDirective } from './visitors/UseDirective.js';
59-
import { AttachTag } from './visitors/AttachTag.js';
59+
import { Attachment } from './visitors/Attachment.js';
6060
import { VariableDeclaration } from './visitors/VariableDeclaration.js';
6161

6262
/** @type {Visitors} */
@@ -132,7 +132,7 @@ const visitors = {
132132
TransitionDirective,
133133
UpdateExpression,
134134
UseDirective,
135-
AttachTag,
135+
Attachment,
136136
VariableDeclaration
137137
};
138138

packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/** @import { Expression } from 'estree' */
2+
/** @import { AST } from '#compiler' */
3+
/** @import { ComponentContext } from '../types' */
4+
import * as b from '../../../../utils/builders.js';
5+
6+
/**
7+
* @param {AST.Attachment} node
8+
* @param {ComponentContext} context
9+
*/
10+
export function Attachment(node, context) {
11+
for (const attachment of node.attachments) {
12+
if (attachment.type === 'SpreadElement') {
13+
context.state.init.push(
14+
b.stmt(
15+
b.call(
16+
'$.attach_all',
17+
context.state.node,
18+
b.thunk(/** @type {Expression} */ (context.visit(attachment.argument)))
19+
)
20+
)
21+
);
22+
} else {
23+
context.state.init.push(
24+
b.stmt(
25+
b.call(
26+
'$.attach',
27+
context.state.node,
28+
b.thunk(/** @type {Expression} */ (context.visit(attachment)))
29+
)
30+
)
31+
);
32+
}
33+
}
34+
}

packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export function RegularElement(node, context) {
8282
/** @type {AST.StyleDirective[]} */
8383
const style_directives = [];
8484

85-
/** @type {Array<AST.AnimateDirective | AST.BindDirective | AST.OnDirective | AST.TransitionDirective | AST.UseDirective | AST.AttachTag>} */
85+
/** @type {Array<AST.AnimateDirective | AST.BindDirective | AST.OnDirective | AST.TransitionDirective | AST.UseDirective | AST.Attachment>} */
8686
const other_directives = [];
8787

8888
/** @type {ExpressionStatement[]} */
@@ -153,7 +153,7 @@ export function RegularElement(node, context) {
153153
other_directives.push(attribute);
154154
break;
155155

156-
case 'AttachTag':
156+
case 'Attachment':
157157
other_directives.push(attribute);
158158
break;
159159
}

packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -261,16 +261,20 @@ export function build_component(node, component_name, context, anchor = context.
261261
);
262262
}
263263
}
264-
} else if (attribute.type === 'AttachTag') {
264+
} else if (attribute.type === 'Attachment') {
265265
// TODO do we need to create a derived here?
266-
push_prop(
267-
b.prop(
268-
'get',
269-
b.call('Symbol'),
270-
/** @type {Expression} */ (context.visit(attribute.expression)),
271-
true
272-
)
273-
);
266+
for (const attachment of attribute.attachments) {
267+
push_prop(
268+
b.prop(
269+
'get',
270+
b.call('Symbol'),
271+
/** @type {Expression} */ (
272+
context.visit(attachment.type === 'SpreadElement' ? attachment.argument : attachment)
273+
),
274+
true
275+
)
276+
);
277+
}
274278
}
275279
}
276280

packages/svelte/src/compiler/types/template.d.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import type {
1515
Program,
1616
ChainExpression,
1717
SimpleCallExpression,
18-
SequenceExpression
18+
SequenceExpression,
19+
SpreadElement
1920
} from 'estree';
2021
import type { Scope } from '../phases/scope';
2122
import type { _CSS } from './css';
@@ -174,10 +175,10 @@ export namespace AST {
174175
};
175176
}
176177

177-
/** A `{@attach foo(...)} tag */
178-
export interface AttachTag extends BaseNode {
179-
type: 'AttachTag';
180-
expression: Expression;
178+
/** An `attach(...)` attribute */
179+
export interface Attachment extends BaseNode {
180+
type: 'Attachment';
181+
attachments: Array<Expression | SpreadElement>;
181182
}
182183

183184
/** An `animate:` directive */
@@ -279,7 +280,7 @@ export namespace AST {
279280

280281
interface BaseElement extends BaseNode {
281282
name: string;
282-
attributes: Array<Attribute | SpreadAttribute | Directive | AttachTag>;
283+
attributes: Array<Attribute | SpreadAttribute | Directive | Attachment>;
283284
fragment: Fragment;
284285
}
285286

@@ -555,7 +556,7 @@ export namespace AST {
555556
| AST.Attribute
556557
| AST.SpreadAttribute
557558
| Directive
558-
| AST.AttachTag
559+
| AST.Attachment
559560
| AST.Comment
560561
| Block;
561562

packages/svelte/src/internal/client/dom/elements/attachments.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,16 @@ import { effect } from '../../reactivity/effects.js';
66
*/
77
export function attach(node, get_fn) {
88
effect(() => {
9-
const fn = get_fn();
9+
const attachment = get_fn();
1010

11-
// we use `&&` rather than `?.` so that things like
12-
// `{@attach DEV && something_dev_only()}` work
13-
return fn && fn(node);
11+
if (Array.isArray(attachment)) {
12+
for (const fn of attachment) {
13+
if (fn) {
14+
$effect(() => fn(node));
15+
}
16+
}
17+
} else if (attachment) {
18+
return attachment(node);
19+
}
1420
});
1521
}
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
1-
<div {@attach (node) => {}} {@attach (node) => {}}></div>
1+
<div attach(a)></div>
2+
<div attach(a, b, c)></div>
3+
<div attach(...stuff)></div>
4+
<div attach(a, b, c, ...stuff)></div>
5+
<div attach((node) => {})></div>

0 commit comments

Comments
 (0)