diff --git a/.changeset/long-crews-return.md b/.changeset/long-crews-return.md new file mode 100644 index 0000000000..41434eaf03 --- /dev/null +++ b/.changeset/long-crews-return.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: native TypeScript support diff --git a/packages/svelte/package.json b/packages/svelte/package.json index a17ba6fdbe..9c072d96fe 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -116,6 +116,7 @@ "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", "acorn": "^8.10.0", + "acorn-typescript": "^1.4.11", "aria-query": "^5.3.0", "axobject-query": "^4.0.0", "esm-env": "^1.0.0", diff --git a/packages/svelte/src/compiler/index.js b/packages/svelte/src/compiler/index.js index dcdf4032ae..9a094521a5 100644 --- a/packages/svelte/src/compiler/index.js +++ b/packages/svelte/src/compiler/index.js @@ -53,7 +53,7 @@ export function compile(source, options) { export function compileModule(source, options) { try { const validated = validate_module_options(options, ''); - const analysis = analyze_module(parse_acorn(source), validated); + const analysis = analyze_module(parse_acorn(source, false), validated); return transform_module(analysis, source, validated); } catch (e) { if (/** @type {any} */ (e).name === 'CompileError') { diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index ef18589263..0db95bdee0 100644 --- a/packages/svelte/src/compiler/phases/1-parse/acorn.js +++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js @@ -1,35 +1,51 @@ import * as acorn from 'acorn'; import { walk } from 'zimmerframe'; +import { tsPlugin } from 'acorn-typescript'; + +// @ts-expect-error +const ParserWithTS = acorn.Parser.extend(tsPlugin()); /** * @param {string} source + * @param {boolean} typescript */ -export function parse(source) { +export function parse(source, typescript) { + const parser = typescript ? ParserWithTS : acorn.Parser; const { onComment, add_comments } = get_comment_handlers(source); - const ast = acorn.parse(source, { + + const ast = parser.parse(source, { onComment, sourceType: 'module', ecmaVersion: 13, locations: true }); + + if (typescript) amend(source, ast); add_comments(ast); + return /** @type {import('estree').Program} */ (ast); } /** * @param {string} source + * @param {boolean} typescript * @param {number} index */ -export function parse_expression_at(source, index) { +export function parse_expression_at(source, typescript, index) { + const parser = typescript ? ParserWithTS : acorn.Parser; const { onComment, add_comments } = get_comment_handlers(source); - const ast = acorn.parseExpressionAt(source, index, { + + const ast = parser.parseExpressionAt(source, index, { onComment, sourceType: 'module', ecmaVersion: 13, locations: true }); + + if (typescript) amend(source, ast); add_comments(ast); - return /** @type {import('estree').Expression} */ (ast); + + return ast; } /** @@ -108,3 +124,29 @@ export function get_comment_handlers(source) { } }; } + +/** + * Tidy up some stuff left behind by acorn-typescript + * @param {string} source + * @param {import('acorn').Node} node + */ +export function amend(source, node) { + return walk(node, null, { + _(node, context) { + // @ts-expect-error + delete node.loc.start.index; + // @ts-expect-error + delete node.loc.end.index; + + if (/** @type {any} */ (node).typeAnnotation && node.end === undefined) { + // i think there might be a bug in acorn-typescript that prevents + // `end` from being assigned when there's a type annotation + let end = /** @type {any} */ (node).typeAnnotation.start; + while (/\s/.test(source[end - 1])) end -= 1; + node.end = end; + } + + context.next(); + } + }); +} diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index 8c89b4b99b..eef5178fb1 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -10,6 +10,9 @@ import read_options from './read/options.js'; const regex_position_indicator = / \(\d+:\d+\)$/; +const regex_lang_attribute = + /|]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s])\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/; + export class Parser { /** * @readonly @@ -20,6 +23,9 @@ export class Parser { /** */ index = 0; + /** Whether we're parsing in TypeScript mode */ + ts = false; + /** @type {import('#compiler').TemplateNode[]} */ stack = []; @@ -43,6 +49,8 @@ export class Parser { this.template = template.trimRight(); + this.ts = regex_lang_attribute.exec(template)?.[2] === 'ts'; + this.root = { css: null, js: [], diff --git a/packages/svelte/src/compiler/phases/1-parse/read/context.js b/packages/svelte/src/compiler/phases/1-parse/read/context.js index fffe61d8cf..2ff9c46ca6 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/context.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/context.js @@ -21,11 +21,13 @@ export default function read_context(parser) { const code = full_char_code_at(parser.template, i); if (isIdentifierStart(code, true)) { + const name = /** @type {string} */ (parser.read_identifier()); return { type: 'Identifier', - name: /** @type {string} */ (parser.read_identifier()), + name, start, - end: parser.index + end: parser.index, + typeAnnotation: read_type_annotation(parser) }; } @@ -74,10 +76,32 @@ export default function read_context(parser) { space_with_newline = space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1); - return /** @type {any} */ ( - parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, start - 1) + const expression = /** @type {any} */ ( + parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, parser.ts, start - 1) ).left; + + expression.typeAnnotation = read_type_annotation(parser); + return expression; } catch (error) { parser.acorn_error(error); } } + +/** + * @param {import('../index.js').Parser} parser + * @returns {any} + */ +function read_type_annotation(parser) { + parser.allow_whitespace(); + + if (parser.eat(':')) { + // we need to trick Acorn into parsing the type annotation + const insert = '_ as '; + let a = parser.index - insert.length; + const template = ' '.repeat(a) + insert + parser.template.slice(parser.index); + const expression = parse_expression_at(template, parser.ts, a); + + parser.index = /** @type {number} */ (expression.end); + return /** @type {any} */ (expression).typeAnnotation; + } +} diff --git a/packages/svelte/src/compiler/phases/1-parse/read/expression.js b/packages/svelte/src/compiler/phases/1-parse/read/expression.js index 3bf712a524..21d4bb96fe 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/expression.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/expression.js @@ -8,7 +8,7 @@ import { error } from '../../../errors.js'; */ export default function read_expression(parser) { try { - const node = parse_expression_at(parser.template, parser.index); + const node = parse_expression_at(parser.template, parser.ts, parser.index); let num_parens = 0; diff --git a/packages/svelte/src/compiler/phases/1-parse/read/script.js b/packages/svelte/src/compiler/phases/1-parse/read/script.js index 6bc38776d3..4761052e1c 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/script.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/script.js @@ -49,7 +49,7 @@ export function read_script(parser, start, attributes) { let ast; try { - ast = acorn.parse(source); + ast = acorn.parse(source, parser.ts); } catch (err) { parser.acorn_error(err); } 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 a4a5010c41..2ad20d9cf1 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -2,6 +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'; const regex_whitespace_with_closing_curly_brace = /^\s*}/; @@ -67,10 +69,73 @@ function open(parser) { if (parser.eat('each')) { parser.require_whitespace(); - const expression = read_expression(parser); + const template = parser.template; + let end = parser.template.length; + + /** @type {import('estree').Expression | undefined} */ + let expression; + + // we have to do this loop because `{#each x as { y = z }}` fails to parse — + // the `as { y = z }` is treated as an Expression but it's actually a Pattern. + // the 'fix' is to backtrack and hide everything from the `as` onwards, until + // we get a valid expression + while (!expression) { + try { + expression = read_expression(parser); + } catch (err) { + end = /** @type {any} */ (err).position[0] - 2; + + while (end > start && parser.template.slice(end, end + 2) !== 'as') { + end -= 1; + } + + if (end <= start) throw err; + + // @ts-expect-error parser.template is meant to be readonly, this is a special case + parser.template = template.slice(0, end); + } + } + + // @ts-expect-error + parser.template = template; + parser.allow_whitespace(); // {#each} blocks must declare a context – {#each list as item} + if (!parser.match('as')) { + // this could be a TypeScript assertion that was erroneously eaten. + + if (expression.type === 'SequenceExpression') { + expression = expression.expressions[0]; + } + + let assertion = null; + let end = expression.end; + + expression = walk(expression, null, { + // @ts-expect-error + TSAsExpression(node, context) { + if (node.end === /** @type {import('estree').Expression} */ (expression).end) { + assertion = node; + end = node.expression.end; + return node.expression; + } + + context.next(); + } + }); + + expression.end = end; + + if (assertion) { + // we can't reset `parser.index` to `expression.expression.end` because + // it will ignore any parentheses — we need to jump through this hoop + let end = /** @type {any} */ (/** @type {any} */ (assertion).typeAnnotation).start - 2; + while (parser.template.slice(end, end + 2) !== 'as') end -= 1; + + parser.index = end; + } + } parser.eat('as', true); parser.require_whitespace(); diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 1a3bd90020..9878561349 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -674,6 +674,8 @@ const runes_scope_tweaker = { } }, ExportSpecifier(node, { state }) { + if (state.ast_type !== 'instance') return; + state.analysis.exports.push({ name: node.local.name, alias: node.exported.name diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 0ced946df9..1afe2058b6 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -8,6 +8,7 @@ import { javascript_visitors } from './visitors/javascript.js'; import { javascript_visitors_runes } from './visitors/javascript-runes.js'; import { javascript_visitors_legacy } from './visitors/javascript-legacy.js'; import { serialize_get_binding } from './utils.js'; +import { remove_types } from '../typescript.js'; /** * This function ensures visitor sets don't accidentally clobber each other @@ -15,11 +16,12 @@ import { serialize_get_binding } from './utils.js'; * @returns {import('./types').Visitors} */ function combine_visitors(...array) { + /** @type {Record} */ const visitors = {}; for (const member of array) { for (const key in member) { - if (key in visitors) { + if (visitors[key]) { throw new Error(`Duplicate visitor: ${key}`); } @@ -100,6 +102,7 @@ export function client_component(source, analysis, options) { state, combine_visitors( set_scope(analysis.module.scopes), + remove_types, global_visitors, // @ts-expect-error TODO javascript_visitors, @@ -115,22 +118,23 @@ export function client_component(source, analysis, options) { instance_state, combine_visitors( set_scope(analysis.instance.scopes), + { ...remove_types, ImportDeclaration: undefined, ExportNamedDeclaration: undefined }, global_visitors, // @ts-expect-error TODO javascript_visitors, analysis.runes ? javascript_visitors_runes : javascript_visitors_legacy, { - ImportDeclaration(node, { state }) { - // @ts-expect-error TODO - state.hoisted.push(node); - return { type: 'EmptyStatement' }; + ImportDeclaration(node, context) { + // @ts-expect-error + state.hoisted.push(remove_types.ImportDeclaration(node, context)); + return b.empty; }, - ExportNamedDeclaration(node, { visit }) { + ExportNamedDeclaration(node, context) { if (node.declaration) { - return visit(node.declaration); + // @ts-expect-error + return remove_types.ExportNamedDeclaration(context.visit(node.declaration), context); } - // specifiers are handled elsewhere return b.empty; } } @@ -142,8 +146,13 @@ export function client_component(source, analysis, options) { walk( /** @type {import('#compiler').SvelteNode} */ (analysis.template.ast), { ...state, scope: analysis.instance.scope }, - // @ts-expect-error TODO - combine_visitors(set_scope(analysis.template.scopes), global_visitors, template_visitors) + combine_visitors( + set_scope(analysis.template.scopes), + remove_types, + global_visitors, + // @ts-expect-error TODO + template_visitors + ) ) ); 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 794a52995e..e008f7c4c9 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 @@ -19,6 +19,7 @@ import { create_attribute, is_element_node } from '../../nodes.js'; import { error } from '../../../errors.js'; import { binding_properties } from '../../bindings.js'; import { regex_starts_with_newline, regex_whitespaces_strict } from '../../patterns.js'; +import { remove_types } from '../typescript.js'; /** * @param {string} value @@ -1904,38 +1905,34 @@ export function server_component(analysis, options) { }; const module = /** @type {import('estree').Program} */ ( - walk( - /** @type {import('#compiler').SvelteNode} */ (analysis.module.ast), - state, - // @ts-expect-error TODO - { - ...set_scope(analysis.module.scopes), - ...global_visitors, - ...javascript_visitors, - ...(analysis.runes ? javascript_visitors_runes : javascript_visitors_legacy) - } - ) + walk(/** @type {import('#compiler').SvelteNode} */ (analysis.module.ast), state, { + ...set_scope(analysis.module.scopes), + ...global_visitors, + ...remove_types, + ...javascript_visitors, + ...(analysis.runes ? javascript_visitors_runes : javascript_visitors_legacy) + }) ); const instance = /** @type {import('estree').Program} */ ( walk( /** @type {import('#compiler').SvelteNode} */ (analysis.instance.ast), { ...state, scope: analysis.instance.scope }, - // @ts-expect-error TODO { ...set_scope(analysis.instance.scopes), ...global_visitors, + ...{ ...remove_types, ImportDeclaration: undefined, ExportNamedDeclaration: undefined }, ...javascript_visitors, ...(analysis.runes ? javascript_visitors_runes : javascript_visitors_legacy), - ImportDeclaration(node, { state }) { - // @ts-expect-error TODO the merged visitors have the lowest common denominator - // state which is ServerTransformState, but it's actually using ComponentServerTransformState - state.hoisted.push(node); - return { type: 'EmptyStatement' }; + ImportDeclaration(node, context) { + // @ts-expect-error + state.hoisted.push(remove_types.ImportDeclaration(node, context)); + return b.empty; }, - ExportNamedDeclaration(node, { state, visit }) { + ExportNamedDeclaration(node, context) { if (node.declaration) { - return visit(node.declaration); + // @ts-expect-error + return remove_types.ExportNamedDeclaration(context.visit(node.declaration), context); } return b.empty; @@ -1948,10 +1945,10 @@ export function server_component(analysis, options) { walk( /** @type {import('#compiler').SvelteNode} */ (analysis.template.ast), { ...state, scope: analysis.template.scope }, - // @ts-expect-error TODO { ...set_scope(analysis.template.scopes), ...global_visitors, + ...remove_types, ...template_visitors } ) diff --git a/packages/svelte/src/compiler/phases/3-transform/typescript.js b/packages/svelte/src/compiler/phases/3-transform/typescript.js new file mode 100644 index 0000000000..9449ced6a0 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/typescript.js @@ -0,0 +1,53 @@ +import * as b from '../../utils/builders.js'; + +/** @type {import('zimmerframe').Visitors} */ +export const remove_types = { + ImportDeclaration(node) { + if (node.importKind === 'type') return b.empty; + + if (node.specifiers?.length > 0) { + const specifiers = node.specifiers.filter((/** @type {any} */ s) => s.importKind !== 'type'); + if (specifiers.length === 0) return b.empty; + + return { ...node, specifiers }; + } + + return node; + }, + ExportNamedDeclaration(node, context) { + if (node.exportKind === 'type') return b.empty; + + if (node.declaration) { + return context.next(); + } + + if (node.specifiers) { + const specifiers = node.specifiers.filter((/** @type {any} */ s) => s.exportKind !== 'type'); + if (specifiers.length === 0) return b.empty; + + return { ...node, specifiers }; + } + + return node; + }, + ExportDefaultDeclaration(node) { + if (node.exportKind === 'type') return b.empty; + return node; + }, + ExportAllDeclaration(node) { + if (node.exportKind === 'type') return b.empty; + return node; + }, + TSAsExpression(node, context) { + return context.visit(node.expression); + }, + TSNonNullExpression(node, context) { + return context.visit(node.expression); + }, + TSInterfaceDeclaration(node, context) { + return b.empty; + }, + TSTypeAliasDeclaration(node, context) { + return b.empty; + } +}; diff --git a/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/input.svelte b/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/input.svelte new file mode 100644 index 0000000000..8ffe8a7287 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/input.svelte @@ -0,0 +1,3 @@ +{#each people as { name, cool = true }} +

{name} is {cool ? 'cool' : 'not cool'}

+{/each} diff --git a/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/output.json b/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/output.json new file mode 100644 index 0000000000..f89be70f77 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/each-block-object-pattern/output.json @@ -0,0 +1,317 @@ +{ + "css": null, + "js": [], + "start": 0, + "end": 94, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "EachBlock", + "start": 0, + "end": 94, + "expression": { + "type": "Identifier", + "start": 7, + "end": 13, + "loc": { + "start": { + "line": 1, + "column": 7 + }, + "end": { + "line": 1, + "column": 13 + } + }, + "name": "people" + }, + "body": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 39, + "end": 41, + "raw": "\n\t", + "data": "\n\t" + }, + { + "type": "RegularElement", + "start": 41, + "end": 86, + "name": "p", + "attributes": [], + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "ExpressionTag", + "start": 44, + "end": 50, + "expression": { + "type": "Identifier", + "start": 45, + "end": 49, + "loc": { + "start": { + "line": 2, + "column": 5 + }, + "end": { + "line": 2, + "column": 9 + } + }, + "name": "name" + } + }, + { + "type": "Text", + "start": 50, + "end": 54, + "raw": " is ", + "data": " is " + }, + { + "type": "ExpressionTag", + "start": 54, + "end": 82, + "expression": { + "type": "ConditionalExpression", + "start": 55, + "end": 81, + "loc": { + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 41 + } + }, + "test": { + "type": "Identifier", + "start": 55, + "end": 59, + "loc": { + "start": { + "line": 2, + "column": 15 + }, + "end": { + "line": 2, + "column": 19 + } + }, + "name": "cool" + }, + "consequent": { + "type": "Literal", + "start": 62, + "end": 68, + "loc": { + "start": { + "line": 2, + "column": 22 + }, + "end": { + "line": 2, + "column": 28 + } + }, + "value": "cool", + "raw": "'cool'" + }, + "alternate": { + "type": "Literal", + "start": 71, + "end": 81, + "loc": { + "start": { + "line": 2, + "column": 31 + }, + "end": { + "line": 2, + "column": 41 + } + }, + "value": "not cool", + "raw": "'not cool'" + } + } + } + ], + "transparent": true + } + }, + { + "type": "Text", + "start": 86, + "end": 87, + "raw": "\n", + "data": "\n" + } + ], + "transparent": false + }, + "context": { + "type": "ObjectPattern", + "start": 17, + "end": 38, + "loc": { + "start": { + "line": 1, + "column": 17 + }, + "end": { + "line": 1, + "column": 38 + } + }, + "properties": [ + { + "type": "Property", + "start": 19, + "end": 23, + "loc": { + "start": { + "line": 1, + "column": 19 + }, + "end": { + "line": 1, + "column": 23 + } + }, + "method": false, + "shorthand": true, + "computed": false, + "key": { + "type": "Identifier", + "start": 19, + "end": 23, + "loc": { + "start": { + "line": 1, + "column": 19 + }, + "end": { + "line": 1, + "column": 23 + } + }, + "name": "name" + }, + "kind": "init", + "value": { + "type": "Identifier", + "start": 19, + "end": 23, + "loc": { + "start": { + "line": 1, + "column": 19 + }, + "end": { + "line": 1, + "column": 23 + } + }, + "name": "name" + } + }, + { + "type": "Property", + "start": 25, + "end": 36, + "loc": { + "start": { + "line": 1, + "column": 25 + }, + "end": { + "line": 1, + "column": 36 + } + }, + "method": false, + "shorthand": true, + "computed": false, + "key": { + "type": "Identifier", + "start": 25, + "end": 29, + "loc": { + "start": { + "line": 1, + "column": 25 + }, + "end": { + "line": 1, + "column": 29 + } + }, + "name": "cool" + }, + "kind": "init", + "value": { + "type": "AssignmentPattern", + "start": 25, + "end": 36, + "loc": { + "start": { + "line": 1, + "column": 25 + }, + "end": { + "line": 1, + "column": 36 + } + }, + "left": { + "type": "Identifier", + "start": 25, + "end": 29, + "loc": { + "start": { + "line": 1, + "column": 25 + }, + "end": { + "line": 1, + "column": 29 + } + }, + "name": "cool" + }, + "right": { + "type": "Literal", + "start": 32, + "end": 36, + "loc": { + "start": { + "line": 1, + "column": 32 + }, + "end": { + "line": 1, + "column": 36 + } + }, + "value": true, + "raw": "true" + } + } + } + ] + } + } + ], + "transparent": false + }, + "options": null +} diff --git a/packages/svelte/tests/parser-modern/samples/snippets/input.svelte b/packages/svelte/tests/parser-modern/samples/snippets/input.svelte index 7c2a94b7eb..87c1e07324 100644 --- a/packages/svelte/tests/parser-modern/samples/snippets/input.svelte +++ b/packages/svelte/tests/parser-modern/samples/snippets/input.svelte @@ -1,5 +1,7 @@ -{#snippet foo()} -

hello

+ + +{#snippet foo(msg: string)} +

{msg}

{/snippet} -{@render foo()} +{@render foo(msg)} diff --git a/packages/svelte/tests/parser-modern/samples/snippets/output.json b/packages/svelte/tests/parser-modern/samples/snippets/output.json index 7093d8a860..e1f0a8b44b 100644 --- a/packages/svelte/tests/parser-modern/samples/snippets/output.json +++ b/packages/svelte/tests/parser-modern/samples/snippets/output.json @@ -1,48 +1,89 @@ { "css": null, "js": [], - "start": 0, - "end": 58, + "start": 29, + "end": 101, "type": "Root", "fragment": { "type": "Fragment", "nodes": [ + { + "type": "Text", + "start": 27, + "end": 29, + "raw": "\n\n", + "data": "\n\n" + }, { "type": "SnippetBlock", - "start": 0, - "end": 41, + "start": 29, + "end": 81, "expression": { "type": "Identifier", - "start": 10, - "end": 13, + "start": 39, + "end": 42, "name": "foo" }, - "context": null, + "context": { + "type": "Identifier", + "name": "msg", + "start": 43, + "end": 46, + "typeAnnotation": { + "type": "TSStringKeyword", + "start": 48, + "end": 54, + "loc": { + "start": { + "line": 1, + "column": 48 + }, + "end": { + "line": 1, + "column": 54 + } + } + } + }, "body": { "type": "Fragment", "nodes": [ { "type": "Text", - "start": 16, - "end": 18, + "start": 56, + "end": 58, "raw": "\n\t", "data": "\n\t" }, { "type": "RegularElement", - "start": 18, - "end": 30, + "start": 58, + "end": 70, "name": "p", "attributes": [], "fragment": { "type": "Fragment", "nodes": [ { - "type": "Text", - "start": 21, - "end": 26, - "raw": "hello", - "data": "hello" + "type": "ExpressionTag", + "start": 61, + "end": 66, + "expression": { + "type": "Identifier", + "start": 62, + "end": 65, + "loc": { + "start": { + "line": 4, + "column": 5 + }, + "end": { + "line": 4, + "column": 8 + } + }, + "name": "msg" + } } ], "transparent": true @@ -50,8 +91,8 @@ }, { "type": "Text", - "start": 30, - "end": 31, + "start": 70, + "end": 71, "raw": "\n", "data": "\n" } @@ -61,35 +102,73 @@ }, { "type": "Text", - "start": 41, - "end": 43, + "start": 81, + "end": 83, "raw": "\n\n", "data": "\n\n" }, { "type": "RenderTag", - "start": 43, - "end": 58, + "start": 83, + "end": 101, "expression": { "type": "Identifier", - "start": 52, - "end": 55, + "start": 92, + "end": 95, "loc": { "start": { - "line": 5, + "line": 7, "column": 9 }, "end": { - "line": 5, + "line": 7, "column": 12 } }, "name": "foo" }, - "argument": null + "argument": { + "type": "Identifier", + "start": 96, + "end": 99, + "loc": { + "start": { + "line": 7, + "column": 13 + }, + "end": { + "line": 7, + "column": 16 + } + }, + "name": "msg" + } } ], "transparent": false }, - "options": null + "options": null, + "instance": { + "type": "Script", + "start": 0, + "end": 27, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 18, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 1, + "column": 18 + } + }, + "body": [], + "sourceType": "module" + } + } } diff --git a/packages/svelte/tests/parser-modern/samples/typescript-in-event-handler/input.svelte b/packages/svelte/tests/parser-modern/samples/typescript-in-event-handler/input.svelte new file mode 100644 index 0000000000..f66af827c3 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/typescript-in-event-handler/input.svelte @@ -0,0 +1,10 @@ + + + diff --git a/packages/svelte/tests/parser-modern/samples/typescript-in-event-handler/output.json b/packages/svelte/tests/parser-modern/samples/typescript-in-event-handler/output.json new file mode 100644 index 0000000000..a23219770b --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/typescript-in-event-handler/output.json @@ -0,0 +1,482 @@ +{ + "css": null, + "js": [], + "start": 54, + "end": 173, + "type": "Root", + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 52, + "end": 54, + "raw": "\n\n", + "data": "\n\n" + }, + { + "type": "RegularElement", + "start": 54, + "end": 173, + "name": "button", + "attributes": [ + { + "start": 63, + "end": 147, + "type": "OnDirective", + "name": "click", + "modifiers": [], + "expression": { + "type": "ArrowFunctionExpression", + "start": 73, + "end": 146, + "loc": { + "start": { + "line": 6, + "column": 11 + }, + "end": { + "line": 9, + "column": 2 + } + }, + "id": null, + "expression": false, + "generator": false, + "async": false, + "params": [ + { + "type": "Identifier", + "start": 74, + "end": 75, + "loc": { + "start": { + "line": 6, + "column": 12 + }, + "end": 87 + }, + "name": "e", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 75, + "end": 87, + "loc": { + "start": { + "line": 6, + "column": 13 + }, + "end": { + "line": 6, + "column": 25 + } + }, + "typeAnnotation": { + "type": "TSTypeReference", + "start": 77, + "end": 87, + "loc": { + "start": { + "line": 6, + "column": 15 + }, + "end": { + "line": 6, + "column": 25 + } + }, + "typeName": { + "type": "Identifier", + "start": 77, + "end": 87, + "loc": { + "start": { + "line": 6, + "column": 15 + }, + "end": { + "line": 6, + "column": 25 + } + }, + "name": "MouseEvent" + } + } + } + } + ], + "body": { + "type": "BlockStatement", + "start": 92, + "end": 146, + "loc": { + "start": { + "line": 6, + "column": 30 + }, + "end": { + "line": 9, + "column": 2 + } + }, + "body": [ + { + "type": "VariableDeclaration", + "start": 96, + "end": 127, + "loc": { + "start": { + "line": 7, + "column": 2 + }, + "end": { + "line": 7, + "column": 33 + } + }, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 102, + "end": 126, + "loc": { + "start": { + "line": 7, + "column": 8 + }, + "end": { + "line": 7, + "column": 32 + } + }, + "id": { + "type": "Identifier", + "start": 102, + "end": 20, + "loc": { + "start": { + "line": 7, + "column": 8 + }, + "end": { + "line": 7, + "column": 20 + } + }, + "name": "next", + "typeAnnotation": { + "type": "TSTypeAnnotation", + "start": 106, + "end": 114, + "loc": { + "start": { + "line": 7, + "column": 12 + }, + "end": { + "line": 7, + "column": 20 + } + }, + "typeAnnotation": { + "type": "TSNumberKeyword", + "start": 108, + "end": 114, + "loc": { + "start": { + "line": 7, + "column": 14 + }, + "end": { + "line": 7, + "column": 20 + } + } + } + } + }, + "init": { + "type": "BinaryExpression", + "start": 117, + "end": 126, + "loc": { + "start": { + "line": 7, + "column": 23 + }, + "end": { + "line": 7, + "column": 32 + } + }, + "left": { + "type": "Identifier", + "start": 117, + "end": 122, + "loc": { + "start": { + "line": 7, + "column": 23 + }, + "end": { + "line": 7, + "column": 28 + } + }, + "name": "count" + }, + "operator": "+", + "right": { + "type": "Literal", + "start": 125, + "end": 126, + "loc": { + "start": { + "line": 7, + "column": 31 + }, + "end": { + "line": 7, + "column": 32 + } + }, + "value": 1, + "raw": "1" + } + } + } + ], + "kind": "const" + }, + { + "type": "ExpressionStatement", + "start": 130, + "end": 143, + "loc": { + "start": { + "line": 8, + "column": 2 + }, + "end": { + "line": 8, + "column": 15 + } + }, + "expression": { + "type": "AssignmentExpression", + "start": 130, + "end": 142, + "loc": { + "start": { + "line": 8, + "column": 2 + }, + "end": { + "line": 8, + "column": 14 + } + }, + "operator": "=", + "left": { + "type": "Identifier", + "start": 130, + "end": 135, + "loc": { + "start": { + "line": 8, + "column": 2 + }, + "end": { + "line": 8, + "column": 7 + } + }, + "name": "count" + }, + "right": { + "type": "Identifier", + "start": 138, + "end": 142, + "loc": { + "start": { + "line": 8, + "column": 10 + }, + "end": { + "line": 8, + "column": 14 + } + }, + "name": "next" + } + } + } + ] + } + } + } + ], + "fragment": { + "type": "Fragment", + "nodes": [ + { + "type": "Text", + "start": 149, + "end": 157, + "raw": "clicks: ", + "data": "clicks: " + }, + { + "type": "ExpressionTag", + "start": 157, + "end": 164, + "expression": { + "type": "Identifier", + "start": 158, + "end": 163, + "loc": { + "start": { + "line": 10, + "column": 10 + }, + "end": { + "line": 10, + "column": 15 + } + }, + "name": "count" + } + } + ], + "transparent": true + } + } + ], + "transparent": false + }, + "options": null, + "instance": { + "type": "Script", + "start": 0, + "end": 52, + "context": "default", + "content": { + "type": "Program", + "start": 18, + "end": 43, + "loc": { + "start": { + "line": 1, + "column": 0 + }, + "end": { + "line": 3, + "column": 0 + } + }, + "body": [ + { + "type": "VariableDeclaration", + "start": 20, + "end": 42, + "loc": { + "start": { + "line": 2, + "column": 1 + }, + "end": { + "line": 2, + "column": 23 + } + }, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 24, + "end": 41, + "loc": { + "start": { + "line": 2, + "column": 5 + }, + "end": { + "line": 2, + "column": 22 + } + }, + "id": { + "type": "Identifier", + "start": 24, + "end": 29, + "loc": { + "start": { + "line": 2, + "column": 5 + }, + "end": { + "line": 2, + "column": 10 + } + }, + "name": "count" + }, + "init": { + "type": "CallExpression", + "start": 32, + "end": 41, + "loc": { + "start": { + "line": 2, + "column": 13 + }, + "end": { + "line": 2, + "column": 22 + } + }, + "callee": { + "type": "Identifier", + "start": 32, + "end": 38, + "loc": { + "start": { + "line": 2, + "column": 13 + }, + "end": { + "line": 2, + "column": 19 + } + }, + "name": "$state" + }, + "arguments": [ + { + "type": "Literal", + "start": 39, + "end": 40, + "loc": { + "start": { + "line": 2, + "column": 20 + }, + "end": { + "line": 2, + "column": 21 + } + }, + "value": 0, + "raw": "0" + } + ], + "optional": false + } + } + ], + "kind": "let" + } + ], + "sourceType": "module" + } + } +} diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-argument/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-argument/main.svelte index 9337d7729b..9812293724 100644 --- a/packages/svelte/tests/runtime-runes/samples/snippet-argument/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/snippet-argument/main.svelte @@ -1,8 +1,8 @@ - -{#snippet foo(n)} +{#snippet foo(n: number)}

clicks: {n}

{/snippet} diff --git a/packages/svelte/tests/runtime-runes/samples/typescript/_config.js b/packages/svelte/tests/runtime-runes/samples/typescript/_config.js new file mode 100644 index 0000000000..2115ae298a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/typescript/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; + +export default test({ + html: '', + + async test({ assert, target }) { + const btn = target.querySelector('button'); + await btn?.click(); + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte b/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte new file mode 100644 index 0000000000..724988b55a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/typescript/main.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/typescript/types.ts b/packages/svelte/tests/runtime-runes/samples/typescript/types.ts new file mode 100644 index 0000000000..9673715eed --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/typescript/types.ts @@ -0,0 +1,3 @@ +export interface Foo {} +export interface Bar {} +export interface Baz {} diff --git a/packages/svelte/tsconfig.json b/packages/svelte/tsconfig.json index 2f90cc693e..dc8c2d134b 100644 --- a/packages/svelte/tsconfig.json +++ b/packages/svelte/tsconfig.json @@ -10,6 +10,7 @@ "noErrorTruncation": true, "allowSyntheticDefaultImports": true, "verbatimModuleSyntax": true, + "skipLibCheck": true, "types": ["node"], "strict": true, "allowJs": true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1dffc51f17..f1f8c91fd0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: acorn: specifier: ^8.10.0 version: 8.11.2 + acorn-typescript: + specifier: ^1.4.11 + version: 1.4.11(acorn@8.11.2) aria-query: specifier: ^5.3.0 version: 5.3.0 @@ -2746,6 +2749,14 @@ packages: acorn: 8.11.2 dev: true + /acorn-typescript@1.4.11(acorn@8.11.2): + resolution: {integrity: sha512-cRGgp+4HMxMZAiMS61ZmQ3iuU/+A4g4ZYZsyLZdmvrEVN/TOwfJ40rPWcLqi3H5ut75SYAdOOJj6QGCcrkK57w==} + peerDependencies: + acorn: '>=8.9.0' + dependencies: + acorn: 8.11.2 + dev: false + /acorn-walk@8.3.0: resolution: {integrity: sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==} engines: {node: '>=0.4.0'}