From 5e307451ddf38b48bd1319e0c390d1eb8701793c Mon Sep 17 00:00:00 2001 From: baseballyama Date: Thu, 23 Nov 2023 16:09:36 +0900 Subject: [PATCH 1/9] support type for const tag --- .changeset/seven-ravens-check.md | 5 +++ .../src/compiler/phases/1-parse/state/tag.js | 44 ++++++++++++++++--- .../svelte/src/compiler/types/template.d.ts | 3 +- .../samples/typescript-const1/_config.js | 5 +++ .../samples/typescript-const1/main.svelte | 8 ++++ .../samples/typescript-const2/_config.js | 5 +++ .../samples/typescript-const2/main.svelte | 5 +++ 7 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 .changeset/seven-ravens-check.md create mode 100644 packages/svelte/tests/runtime-runes/samples/typescript-const1/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/typescript-const1/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/typescript-const2/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/typescript-const2/main.svelte diff --git a/.changeset/seven-ravens-check.md b/.changeset/seven-ravens-check.md new file mode 100644 index 000000000000..26063b2313bf --- /dev/null +++ b/.changeset/seven-ravens-check.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: support type definition in {@const} 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 2ad20d9cf1ce..f9df97a3c1b9 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,13 +532,40 @@ 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); parser.append( @@ -546,7 +573,14 @@ function special(parser) { type: 'ConstTag', start, end: parser.index, - expression + expression: { + type: 'AssignmentExpression', + start: (declaration.start ?? 0) + CONST_LENGTH, + end: declaration.end ?? 0, + operator: '=', + left: declaration.declarations[0].id, + right: declaration.declarations[0].init + } }) ); } diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index b69173e6b5ea..98c51b6ad04d 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -11,7 +11,8 @@ import type { Node, ObjectExpression, Pattern, - Program + Program, + VariableDeclaration } from 'estree'; export interface BaseNode { 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 000000000000..644f802ac6d8 --- /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 000000000000..1ec792c4ec4c --- /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 000000000000..646f2812a2b1 --- /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 000000000000..f1096908f803 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/typescript-const2/main.svelte @@ -0,0 +1,5 @@ + + +{@const name: string = "{}"} +

{name}

From fe4542f25b97c92c309e3eadbfb9338f2598099a Mon Sep 17 00:00:00 2001 From: baseballyama Date: Thu, 23 Nov 2023 18:14:43 +0900 Subject: [PATCH 2/9] use expression directly --- .../svelte/src/compiler/phases/1-parse/state/tag.js | 9 +-------- .../phases/3-transform/client/visitors/template.js | 13 +++++++------ .../phases/3-transform/server/transform-server.js | 5 +++-- packages/svelte/src/compiler/phases/scope.js | 13 ++++++++++--- packages/svelte/src/compiler/types/template.d.ts | 10 ++++++---- 5 files changed, 27 insertions(+), 23 deletions(-) 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 f9df97a3c1b9..0cfce31d6d7d 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -573,14 +573,7 @@ function special(parser) { type: 'ConstTag', start, end: parser.index, - expression: { - type: 'AssignmentExpression', - start: (declaration.start ?? 0) + CONST_LENGTH, - end: declaration.end ?? 0, - operator: '=', - left: declaration.declarations[0].id, - right: declaration.declarations[0].init - } + 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 207bc6faf0ec..442c19638431 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 @@ -1611,19 +1611,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 @@ -1639,8 +1640,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 78356d8f5fe0..aff250ad5b22 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 @@ -1049,8 +1049,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 95c192d4d404..c4299b015e92 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); } } @@ -594,7 +600,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/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 98c51b6ad04d..f5e55f4829a7 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, @@ -11,8 +12,7 @@ import type { Node, ObjectExpression, Pattern, - Program, - VariableDeclaration + Program } from 'estree'; export interface BaseNode { @@ -131,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 */ From 731b8cf00628dcccc9d68dc9eebb5537e06e8754 Mon Sep 17 00:00:00 2001 From: baseballyama Date: Thu, 23 Nov 2023 18:27:53 +0900 Subject: [PATCH 3/9] lint --- documentation/docs/02-template-syntax/04-special-tags.md | 2 +- documentation/docs/05-misc/03-typescript.md | 6 ++++-- packages/svelte/src/compiler/types/template.d.ts | 2 +- .../svelte-5-preview/src/lib/Input/ComponentSelector.svelte | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/documentation/docs/02-template-syntax/04-special-tags.md b/documentation/docs/02-template-syntax/04-special-tags.md index 4145d8d6d6bd..49162bc35590 100644 --- a/documentation/docs/02-template-syntax/04-special-tags.md +++ b/documentation/docs/02-template-syntax/04-special-tags.md @@ -80,7 +80,7 @@ The `{@const ...}` tag defines a local constant. {#each boxes as box} - {@const area = box.width * box.height} + {@const } {box.width} * {box.height} = {area} {/each} ``` diff --git a/documentation/docs/05-misc/03-typescript.md b/documentation/docs/05-misc/03-typescript.md index 3fe3580f7ee3..f8e20ce99e1f 100644 --- a/documentation/docs/05-misc/03-typescript.md +++ b/documentation/docs/05-misc/03-typescript.md @@ -177,9 +177,11 @@ You cannot use TypeScript in your template's markup. For example, the following let count = 10; -

Count as string: {count as string}!

+

Count as string: {count as string}!

+ {#if count > 4} - {@const countString: string = count} + {@const } + {countString} {/if} ``` diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index f5e55f4829a7..621e8092b2e5 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -132,7 +132,7 @@ export interface Comment extends BaseNode { export interface ConstTag extends BaseNode { type: 'ConstTag'; declaration: VariableDeclaration & { - declarations: [VariableDeclarator & { id: Identifier, init: Expression } ] + declarations: [VariableDeclarator & { id: Identifier; init: Expression }]; }; } diff --git a/sites/svelte-5-preview/src/lib/Input/ComponentSelector.svelte b/sites/svelte-5-preview/src/lib/Input/ComponentSelector.svelte index 71cd9241b8f8..cd6aa661fb3d 100644 --- a/sites/svelte-5-preview/src/lib/Input/ComponentSelector.svelte +++ b/sites/svelte-5-preview/src/lib/Input/ComponentSelector.svelte @@ -212,7 +212,7 @@
{#each $files as file, index (file.name)} - {@const filename = get_full_filename(file)} + {@const }
{:else if filename === editing_name} - {@const editing_file = $files.find((file) => get_full_filename(file) === editing_name)} + {@const } {#if editing_file} From 4d89c849c5716a0e038d00cb1e7676ac94016dce Mon Sep 17 00:00:00 2001 From: baseballyama Date: Thu, 23 Nov 2023 18:32:12 +0900 Subject: [PATCH 4/9] format --- documentation/docs/02-template-syntax/04-special-tags.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/02-template-syntax/04-special-tags.md b/documentation/docs/02-template-syntax/04-special-tags.md index 49162bc35590..4145d8d6d6bd 100644 --- a/documentation/docs/02-template-syntax/04-special-tags.md +++ b/documentation/docs/02-template-syntax/04-special-tags.md @@ -80,7 +80,7 @@ The `{@const ...}` tag defines a local constant. {#each boxes as box} - {@const } + {@const area = box.width * box.height} {box.width} * {box.height} = {area} {/each} ``` From bf4e7aae734adbbc60ed25f004b138d69722521d Mon Sep 17 00:00:00 2001 From: baseballyama Date: Thu, 23 Nov 2023 18:35:22 +0900 Subject: [PATCH 5/9] format --- sites/svelte-5-preview/src/lib/Input/ComponentSelector.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sites/svelte-5-preview/src/lib/Input/ComponentSelector.svelte b/sites/svelte-5-preview/src/lib/Input/ComponentSelector.svelte index cd6aa661fb3d..71cd9241b8f8 100644 --- a/sites/svelte-5-preview/src/lib/Input/ComponentSelector.svelte +++ b/sites/svelte-5-preview/src/lib/Input/ComponentSelector.svelte @@ -212,7 +212,7 @@
{#each $files as file, index (file.name)} - {@const } + {@const filename = get_full_filename(file)}
{:else if filename === editing_name} - {@const } + {@const editing_file = $files.find((file) => get_full_filename(file) === editing_name)} {#if editing_file} From a7d253aff7340ea6fefdfa498a176dd1212eb491 Mon Sep 17 00:00:00 2001 From: baseballyama Date: Thu, 23 Nov 2023 19:01:44 +0900 Subject: [PATCH 6/9] revert --- documentation/docs/05-misc/03-typescript.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/documentation/docs/05-misc/03-typescript.md b/documentation/docs/05-misc/03-typescript.md index f8e20ce99e1f..3fe3580f7ee3 100644 --- a/documentation/docs/05-misc/03-typescript.md +++ b/documentation/docs/05-misc/03-typescript.md @@ -177,11 +177,9 @@ You cannot use TypeScript in your template's markup. For example, the following let count = 10; -

Count as string: {count as string}!

- +

Count as string: {count as string}!

{#if count > 4} - {@const } - + {@const countString: string = count} {countString} {/if} ``` From dc5c19dccff0298a97dfd064758274a56cf4f00b Mon Sep 17 00:00:00 2001 From: baseballyama Date: Thu, 23 Nov 2023 20:12:28 +0900 Subject: [PATCH 7/9] legacy mode --- packages/svelte/src/compiler/legacy.js | 27 +++++++++++++++++++ .../src/compiler/phases/1-parse/state/tag.js | 6 +++++ .../src/compiler/types/legacy-nodes.d.ts | 7 +++++ 3 files changed, 40 insertions(+) diff --git a/packages/svelte/src/compiler/legacy.js b/packages/svelte/src/compiler/legacy.js index 6a0c7f1087f2..da20b9069672 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 0cfce31d6d7d..76e66d9b1bae 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -568,6 +568,12 @@ function special(parser) { 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', diff --git a/packages/svelte/src/compiler/types/legacy-nodes.d.ts b/packages/svelte/src/compiler/types/legacy-nodes.d.ts index 6564dc47ab8b..41c1ad7a3bbe 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 From 87c1ba38a39611534c8115edc3e06099664ed67d Mon Sep 17 00:00:00 2001 From: baseballyama Date: Thu, 23 Nov 2023 20:32:36 +0900 Subject: [PATCH 8/9] format --- documentation/docs/05-misc/03-typescript.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/documentation/docs/05-misc/03-typescript.md b/documentation/docs/05-misc/03-typescript.md index 3fe3580f7ee3..b4cc917cfbe9 100644 --- a/documentation/docs/05-misc/03-typescript.md +++ b/documentation/docs/05-misc/03-typescript.md @@ -177,9 +177,11 @@ You cannot use TypeScript in your template's markup. For example, the following let count = 10; -

Count as string: {count as string}!

+ +

Count as string: {count as string}!

{#if count > 4} - {@const countString: string = count} + + {@const countString: string = count} {countString} {/if} ``` From cba043c78677f9f336f3edfb4d0ad99db7140c08 Mon Sep 17 00:00:00 2001 From: baseballyama Date: Sat, 25 Nov 2023 19:12:41 +0900 Subject: [PATCH 9/9] revert and update .prettierignore --- .prettierignore | 4 ++++ documentation/docs/05-misc/03-typescript.md | 6 ++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.prettierignore b/.prettierignore index 36dcaeb08f04..45e12c6e3fbc 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/documentation/docs/05-misc/03-typescript.md b/documentation/docs/05-misc/03-typescript.md index b4cc917cfbe9..3fe3580f7ee3 100644 --- a/documentation/docs/05-misc/03-typescript.md +++ b/documentation/docs/05-misc/03-typescript.md @@ -177,11 +177,9 @@ You cannot use TypeScript in your template's markup. For example, the following let count = 10; - -

Count as string: {count as string}!

+

Count as string: {count as string}!

{#if count > 4} - - {@const countString: string = count} + {@const countString: string = count} {countString} {/if} ```