diff --git a/.changeset/seven-ravens-check.md b/.changeset/seven-ravens-check.md new file mode 100644 index 0000000000..26063b2313 --- /dev/null +++ b/.changeset/seven-ravens-check.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: support type definition in {@const} diff --git a/.prettierignore b/.prettierignore index 36dcaeb08f..45e12c6e3f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -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 diff --git a/packages/svelte/src/compiler/legacy.js b/packages/svelte/src/compiler/legacy.js index 6a0c7f1087..da20b90696 100644 --- a/packages/svelte/src/compiler/legacy.js +++ b/packages/svelte/src/compiler/legacy.js @@ -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 { diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 2ad20d9cf1..76e66d9b1b 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -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; - if (!(expression.type === 'AssignmentExpression' && expression.operator === '=')) { + let end_index = parser.index; + /** @type {import('estree').VariableDeclaration | undefined} */ + let declaration = undefined; + + 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 }) ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 9640e6c275..9dd6775a6a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -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)))) ]) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 505ddeaed1..a3fff51495 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -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 }) { diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index c839a78276..86d0eab67a 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -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', diff --git a/packages/svelte/src/compiler/types/legacy-nodes.d.ts b/packages/svelte/src/compiler/types/legacy-nodes.d.ts index 6564dc47ab..41c1ad7a3b 100644 --- a/packages/svelte/src/compiler/types/legacy-nodes.d.ts +++ b/packages/svelte/src/compiler/types/legacy-nodes.d.ts @@ -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 diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index b69173e6b5..621e8092b2 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -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 */ diff --git a/packages/svelte/tests/runtime-runes/samples/typescript-const1/_config.js b/packages/svelte/tests/runtime-runes/samples/typescript-const1/_config.js new file mode 100644 index 0000000000..644f802ac6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/typescript-const1/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: '

10 * 10 = 100

20 * 20 = 400

' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/typescript-const1/main.svelte b/packages/svelte/tests/runtime-runes/samples/typescript-const1/main.svelte new file mode 100644 index 0000000000..1ec792c4ec --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/typescript-const1/main.svelte @@ -0,0 +1,8 @@ + + +{#each boxes as box} + {@const area: number = box.width * box.height} +

{box.width} * {box.height} = {area}

+{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/typescript-const2/_config.js b/packages/svelte/tests/runtime-runes/samples/typescript-const2/_config.js new file mode 100644 index 0000000000..646f2812a2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/typescript-const2/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: '

{}

' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/typescript-const2/main.svelte b/packages/svelte/tests/runtime-runes/samples/typescript-const2/main.svelte new file mode 100644 index 0000000000..f1096908f8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/typescript-const2/main.svelte @@ -0,0 +1,5 @@ + + +{@const name: string = "{}"} +

{name}