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
pull/9667/head
Yuichiro Yamashita 7 months ago committed by GitHub
parent 075c268f42
commit da1aa7c4a8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: support type definition in {@const}

@ -37,3 +37,7 @@ sites/svelte.dev/src/lib/generated
.changeset
pnpm-lock.yaml
pnpm-workspace.yaml
# Temporarily ignore this file to avoid merge conflicts.
# see: https://github.com/sveltejs/svelte/pull/9609
documentation/docs/05-misc/03-typescript.md

@ -209,6 +209,33 @@ export function convert(source, ast) {
};
},
// @ts-ignore
ConstTag(node) {
if (
/** @type {import('./types/legacy-nodes.js').LegacyConstTag} */ (node).expression !==
undefined
) {
return node;
}
const modern_node = /** @type {import('#compiler').ConstTag} */ (node);
const { id: left } = { ...modern_node.declaration.declarations[0] };
// @ts-ignore
delete left.typeAnnotation;
return {
type: 'ConstTag',
start: modern_node.start,
end: node.end,
expression: {
type: 'AssignmentExpression',
start: (modern_node.declaration.start ?? 0) + 'const '.length,
end: modern_node.declaration.end ?? 0,
operator: '=',
left,
right: modern_node.declaration.declarations[0].init
}
};
},
// @ts-ignore
KeyBlock(node, { visit }) {
remove_surrounding_whitespace_nodes(node.fragment.nodes);
return {

@ -2,8 +2,8 @@ import read_context from '../read/context.js';
import read_expression from '../read/expression.js';
import { error } from '../../../errors.js';
import { create_fragment } from '../utils/create.js';
import { parse_expression_at } from '../acorn.js';
import { walk } from 'zimmerframe';
import { parse } from '../acorn.js';
const regex_whitespace_with_closing_curly_brace = /^\s*}/;
@ -532,21 +532,54 @@ function special(parser) {
// {@const a = b}
parser.require_whitespace();
const expression = read_expression(parser);
const CONST_LENGTH = 'const '.length;
parser.index = parser.index - CONST_LENGTH;
let end_index = parser.index;
/** @type {import('estree').VariableDeclaration | undefined} */
let declaration = undefined;
if (!(expression.type === 'AssignmentExpression' && expression.operator === '=')) {
const dummy_spaces = parser.template.substring(0, parser.index).replace(/[^\n]/g, ' ');
while (true) {
end_index = parser.template.indexOf('}', end_index + 1);
if (end_index === -1) break;
try {
const node = parse(
dummy_spaces + parser.template.substring(parser.index, end_index),
parser.ts
).body[0];
if (node?.type === 'VariableDeclaration') {
declaration = node;
break;
}
} catch (e) {
continue;
}
}
if (
declaration === undefined ||
declaration.declarations.length !== 1 ||
declaration.declarations[0].init === undefined
) {
error(start, 'invalid-const');
}
parser.allow_whitespace();
parser.index = end_index;
parser.eat('}', true);
const id = declaration.declarations[0].id;
if (id.type === 'Identifier') {
// Tidy up some stuff left behind by acorn-typescript
id.end = (id.start ?? 0) + id.name.length;
}
parser.append(
/** @type {import('#compiler').ConstTag} */ ({
type: 'ConstTag',
start,
end: parser.index,
expression
declaration
})
);
}

@ -1653,19 +1653,20 @@ export const template_visitors = {
);
},
ConstTag(node, { state, visit }) {
const declaration = node.declaration.declarations[0];
// TODO we can almost certainly share some code with $derived(...)
if (node.expression.left.type === 'Identifier') {
if (declaration.id.type === 'Identifier') {
state.init.push(
b.const(
node.expression.left,
declaration.id,
b.call(
'$.derived',
b.thunk(/** @type {import('estree').Expression} */ (visit(node.expression.right)))
b.thunk(/** @type {import('estree').Expression} */ (visit(declaration.init)))
)
)
);
} else {
const identifiers = extract_identifiers(node.expression.left);
const identifiers = extract_identifiers(declaration.id);
const tmp = b.id(state.scope.generate('computed_const'));
// Make all identifiers that are declared within the following computed regular
@ -1681,8 +1682,8 @@ export const template_visitors = {
[],
b.block([
b.const(
/** @type {import('estree').Pattern} */ (visit(node.expression.left)),
/** @type {import('estree').Expression} */ (visit(node.expression.right))
/** @type {import('estree').Pattern} */ (visit(declaration.id)),
/** @type {import('estree').Expression} */ (visit(declaration.init))
),
b.return(b.object(identifiers.map((node) => b.prop('init', node, node))))
])

@ -1080,8 +1080,9 @@ const template_visitors = {
state.template.push(t_expression(id));
},
ConstTag(node, { state, visit }) {
const pattern = /** @type {import('estree').Pattern} */ (visit(node.expression.left));
const init = /** @type {import('estree').Expression} */ (visit(node.expression.right));
const declaration = node.declaration.declarations[0];
const pattern = /** @type {import('estree').Pattern} */ (visit(declaration.id));
const init = /** @type {import('estree').Expression} */ (visit(declaration.init));
state.init.push(b.declaration('const', pattern, init));
},
DebugTag(node, { state, visit }) {

@ -437,7 +437,8 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
next();
},
VariableDeclaration(node, { state, next }) {
VariableDeclaration(node, { state, path, next }) {
const is_parent_const_tag = path.at(-1)?.type === 'ConstTag';
for (const declarator of node.declarations) {
/** @type {import('#compiler').Binding[]} */
const bindings = [];
@ -445,7 +446,12 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
state.scope.declarators.set(declarator, bindings);
for (const id of extract_identifiers(declarator.id)) {
const binding = state.scope.declare(id, 'normal', node.kind, declarator.init);
const binding = state.scope.declare(
id,
is_parent_const_tag ? 'derived' : 'normal',
node.kind,
declarator.init
);
bindings.push(binding);
}
}
@ -593,7 +599,8 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
},
ConstTag(node, { state, next }) {
for (const identifier of extract_identifiers(node.expression.left)) {
const declaration = node.declaration.declarations[0];
for (const identifier of extract_identifiers(declaration.id)) {
state.scope.declare(
/** @type {import('estree').Identifier} */ (identifier),
'derived',

@ -1,6 +1,7 @@
import type { StyleDirective as LegacyStyleDirective, Text } from '#compiler';
import type {
ArrayExpression,
AssignmentExpression,
Expression,
Identifier,
MemberExpression,
@ -168,6 +169,11 @@ export interface LegacyTitle extends BaseElement {
name: 'title';
}
export interface LegacyConstTag extends BaseNode {
type: 'ConstTag';
expression: AssignmentExpression;
}
export interface LegacyTransition extends BaseNode {
type: 'Transition';
/** The 'x' in `transition:x` */
@ -215,6 +221,7 @@ export type LegacyElementLike =
| LegacyWindow;
export type LegacySvelteNode =
| LegacyConstTag
| LegacyElementLike
| LegacyAttributeLike
| LegacyAttributeShorthand

@ -2,7 +2,8 @@ import type { Binding } from '#compiler';
import type {
ArrayExpression,
ArrowFunctionExpression,
AssignmentExpression,
VariableDeclaration,
VariableDeclarator,
Expression,
FunctionDeclaration,
FunctionExpression,
@ -130,7 +131,9 @@ export interface Comment extends BaseNode {
/** A `{@const ...}` tag */
export interface ConstTag extends BaseNode {
type: 'ConstTag';
expression: AssignmentExpression;
declaration: VariableDeclaration & {
declarations: [VariableDeclarator & { id: Identifier; init: Expression }];
};
}
/** A `{@debug ...}` tag */

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: '<p>10 * 10 = 100</p><p>20 * 20 = 400</p>'
});

@ -0,0 +1,8 @@
<script lang="ts">
const boxes = [ { width: 10, height: 10 }, { width: 20, height: 20 } ];
</script>
{#each boxes as box}
{@const area: number = box.width * box.height}
<p>{box.width} * {box.height} = {area}</p>
{/each}

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: '<p>{}</p>'
});

@ -0,0 +1,5 @@
<script lang="ts">
</script>
{@const name: string = "{}"}
<p>{name}</p>
Loading…
Cancel
Save