Skip to content

Commit da1aa7c

Browse files
authored
feat: support type annotations in {@const ...} tag (#9609)
* support type for const tag * use expression directly * lint * format * format * revert * legacy mode * format * revert and update .prettierignore
1 parent 075c268 commit da1aa7c

File tree

13 files changed

+129
-18
lines changed

13 files changed

+129
-18
lines changed

.changeset/seven-ravens-check.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
feat: support type definition in {@const}

.prettierignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,7 @@ sites/svelte.dev/src/lib/generated
3737
.changeset
3838
pnpm-lock.yaml
3939
pnpm-workspace.yaml
40+
41+
# Temporarily ignore this file to avoid merge conflicts.
42+
# see: https://github.com/sveltejs/svelte/pull/9609
43+
documentation/docs/05-misc/03-typescript.md

packages/svelte/src/compiler/legacy.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,33 @@ export function convert(source, ast) {
209209
};
210210
},
211211
// @ts-ignore
212+
ConstTag(node) {
213+
if (
214+
/** @type {import('./types/legacy-nodes.js').LegacyConstTag} */ (node).expression !==
215+
undefined
216+
) {
217+
return node;
218+
}
219+
220+
const modern_node = /** @type {import('#compiler').ConstTag} */ (node);
221+
const { id: left } = { ...modern_node.declaration.declarations[0] };
222+
// @ts-ignore
223+
delete left.typeAnnotation;
224+
return {
225+
type: 'ConstTag',
226+
start: modern_node.start,
227+
end: node.end,
228+
expression: {
229+
type: 'AssignmentExpression',
230+
start: (modern_node.declaration.start ?? 0) + 'const '.length,
231+
end: modern_node.declaration.end ?? 0,
232+
operator: '=',
233+
left,
234+
right: modern_node.declaration.declarations[0].init
235+
}
236+
};
237+
},
238+
// @ts-ignore
212239
KeyBlock(node, { visit }) {
213240
remove_surrounding_whitespace_nodes(node.fragment.nodes);
214241
return {

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

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import read_context from '../read/context.js';
22
import read_expression from '../read/expression.js';
33
import { error } from '../../../errors.js';
44
import { create_fragment } from '../utils/create.js';
5-
import { parse_expression_at } from '../acorn.js';
65
import { walk } from 'zimmerframe';
6+
import { parse } from '../acorn.js';
77

88
const regex_whitespace_with_closing_curly_brace = /^\s*}/;
99

@@ -532,21 +532,54 @@ function special(parser) {
532532
// {@const a = b}
533533
parser.require_whitespace();
534534

535-
const expression = read_expression(parser);
535+
const CONST_LENGTH = 'const '.length;
536+
parser.index = parser.index - CONST_LENGTH;
537+
538+
let end_index = parser.index;
539+
/** @type {import('estree').VariableDeclaration | undefined} */
540+
let declaration = undefined;
536541

537-
if (!(expression.type === 'AssignmentExpression' && expression.operator === '=')) {
542+
const dummy_spaces = parser.template.substring(0, parser.index).replace(/[^\n]/g, ' ');
543+
while (true) {
544+
end_index = parser.template.indexOf('}', end_index + 1);
545+
if (end_index === -1) break;
546+
try {
547+
const node = parse(
548+
dummy_spaces + parser.template.substring(parser.index, end_index),
549+
parser.ts
550+
).body[0];
551+
if (node?.type === 'VariableDeclaration') {
552+
declaration = node;
553+
break;
554+
}
555+
} catch (e) {
556+
continue;
557+
}
558+
}
559+
560+
if (
561+
declaration === undefined ||
562+
declaration.declarations.length !== 1 ||
563+
declaration.declarations[0].init === undefined
564+
) {
538565
error(start, 'invalid-const');
539566
}
540567

541-
parser.allow_whitespace();
568+
parser.index = end_index;
542569
parser.eat('}', true);
543570

571+
const id = declaration.declarations[0].id;
572+
if (id.type === 'Identifier') {
573+
// Tidy up some stuff left behind by acorn-typescript
574+
id.end = (id.start ?? 0) + id.name.length;
575+
}
576+
544577
parser.append(
545578
/** @type {import('#compiler').ConstTag} */ ({
546579
type: 'ConstTag',
547580
start,
548581
end: parser.index,
549-
expression
582+
declaration
550583
})
551584
);
552585
}

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

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1653,19 +1653,20 @@ export const template_visitors = {
16531653
);
16541654
},
16551655
ConstTag(node, { state, visit }) {
1656+
const declaration = node.declaration.declarations[0];
16561657
// TODO we can almost certainly share some code with $derived(...)
1657-
if (node.expression.left.type === 'Identifier') {
1658+
if (declaration.id.type === 'Identifier') {
16581659
state.init.push(
16591660
b.const(
1660-
node.expression.left,
1661+
declaration.id,
16611662
b.call(
16621663
'$.derived',
1663-
b.thunk(/** @type {import('estree').Expression} */ (visit(node.expression.right)))
1664+
b.thunk(/** @type {import('estree').Expression} */ (visit(declaration.init)))
16641665
)
16651666
)
16661667
);
16671668
} else {
1668-
const identifiers = extract_identifiers(node.expression.left);
1669+
const identifiers = extract_identifiers(declaration.id);
16691670
const tmp = b.id(state.scope.generate('computed_const'));
16701671

16711672
// Make all identifiers that are declared within the following computed regular
@@ -1681,8 +1682,8 @@ export const template_visitors = {
16811682
[],
16821683
b.block([
16831684
b.const(
1684-
/** @type {import('estree').Pattern} */ (visit(node.expression.left)),
1685-
/** @type {import('estree').Expression} */ (visit(node.expression.right))
1685+
/** @type {import('estree').Pattern} */ (visit(declaration.id)),
1686+
/** @type {import('estree').Expression} */ (visit(declaration.init))
16861687
),
16871688
b.return(b.object(identifiers.map((node) => b.prop('init', node, node))))
16881689
])

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,8 +1080,9 @@ const template_visitors = {
10801080
state.template.push(t_expression(id));
10811081
},
10821082
ConstTag(node, { state, visit }) {
1083-
const pattern = /** @type {import('estree').Pattern} */ (visit(node.expression.left));
1084-
const init = /** @type {import('estree').Expression} */ (visit(node.expression.right));
1083+
const declaration = node.declaration.declarations[0];
1084+
const pattern = /** @type {import('estree').Pattern} */ (visit(declaration.id));
1085+
const init = /** @type {import('estree').Expression} */ (visit(declaration.init));
10851086
state.init.push(b.declaration('const', pattern, init));
10861087
},
10871088
DebugTag(node, { state, visit }) {

packages/svelte/src/compiler/phases/scope.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -437,15 +437,21 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
437437
next();
438438
},
439439

440-
VariableDeclaration(node, { state, next }) {
440+
VariableDeclaration(node, { state, path, next }) {
441+
const is_parent_const_tag = path.at(-1)?.type === 'ConstTag';
441442
for (const declarator of node.declarations) {
442443
/** @type {import('#compiler').Binding[]} */
443444
const bindings = [];
444445

445446
state.scope.declarators.set(declarator, bindings);
446447

447448
for (const id of extract_identifiers(declarator.id)) {
448-
const binding = state.scope.declare(id, 'normal', node.kind, declarator.init);
449+
const binding = state.scope.declare(
450+
id,
451+
is_parent_const_tag ? 'derived' : 'normal',
452+
node.kind,
453+
declarator.init
454+
);
449455
bindings.push(binding);
450456
}
451457
}
@@ -593,7 +599,8 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
593599
},
594600

595601
ConstTag(node, { state, next }) {
596-
for (const identifier of extract_identifiers(node.expression.left)) {
602+
const declaration = node.declaration.declarations[0];
603+
for (const identifier of extract_identifiers(declaration.id)) {
597604
state.scope.declare(
598605
/** @type {import('estree').Identifier} */ (identifier),
599606
'derived',

packages/svelte/src/compiler/types/legacy-nodes.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { StyleDirective as LegacyStyleDirective, Text } from '#compiler';
22
import type {
33
ArrayExpression,
4+
AssignmentExpression,
45
Expression,
56
Identifier,
67
MemberExpression,
@@ -168,6 +169,11 @@ export interface LegacyTitle extends BaseElement {
168169
name: 'title';
169170
}
170171

172+
export interface LegacyConstTag extends BaseNode {
173+
type: 'ConstTag';
174+
expression: AssignmentExpression;
175+
}
176+
171177
export interface LegacyTransition extends BaseNode {
172178
type: 'Transition';
173179
/** The 'x' in `transition:x` */
@@ -215,6 +221,7 @@ export type LegacyElementLike =
215221
| LegacyWindow;
216222

217223
export type LegacySvelteNode =
224+
| LegacyConstTag
218225
| LegacyElementLike
219226
| LegacyAttributeLike
220227
| LegacyAttributeShorthand

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import type { Binding } from '#compiler';
22
import type {
33
ArrayExpression,
44
ArrowFunctionExpression,
5-
AssignmentExpression,
5+
VariableDeclaration,
6+
VariableDeclarator,
67
Expression,
78
FunctionDeclaration,
89
FunctionExpression,
@@ -130,7 +131,9 @@ export interface Comment extends BaseNode {
130131
/** A `{@const ...}` tag */
131132
export interface ConstTag extends BaseNode {
132133
type: 'ConstTag';
133-
expression: AssignmentExpression;
134+
declaration: VariableDeclaration & {
135+
declarations: [VariableDeclarator & { id: Identifier; init: Expression }];
136+
};
134137
}
135138

136139
/** A `{@debug ...}` tag */
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: '<p>10 * 10 = 100</p><p>20 * 20 = 400</p>'
5+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script lang="ts">
2+
const boxes = [ { width: 10, height: 10 }, { width: 20, height: 20 } ];
3+
</script>
4+
5+
{#each boxes as box}
6+
{@const area: number = box.width * box.height}
7+
<p>{box.width} * {box.height} = {area}</p>
8+
{/each}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
html: '<p>{}</p>'
5+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<script lang="ts">
2+
</script>
3+
4+
{@const name: string = "{}"}
5+
<p>{name}</p>

0 commit comments

Comments
 (0)