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 = + /| + +{#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'}