Skip to content

Commit 459e4ff

Browse files
authored
feat: native TypeScript support (#9482)
* add typescript support to parser * fix * unnecessary * various * transform assertions * tweak * prettier * robustify * fix * see if this fixes the prettier stuff * only parse ts in ts mode * fixes * fix * fix * fix * fix * more * check * changeset * allow type annotations on all contexts --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 9926347 commit 459e4ff

File tree

25 files changed

+1221
-76
lines changed

25 files changed

+1221
-76
lines changed

.changeset/long-crews-return.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
feat: native TypeScript support

packages/svelte/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
"@ampproject/remapping": "^2.2.1",
117117
"@jridgewell/sourcemap-codec": "^1.4.15",
118118
"acorn": "^8.10.0",
119+
"acorn-typescript": "^1.4.11",
119120
"aria-query": "^5.3.0",
120121
"axobject-query": "^4.0.0",
121122
"esm-env": "^1.0.0",

packages/svelte/src/compiler/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export function compile(source, options) {
5353
export function compileModule(source, options) {
5454
try {
5555
const validated = validate_module_options(options, '');
56-
const analysis = analyze_module(parse_acorn(source), validated);
56+
const analysis = analyze_module(parse_acorn(source, false), validated);
5757
return transform_module(analysis, source, validated);
5858
} catch (e) {
5959
if (/** @type {any} */ (e).name === 'CompileError') {

packages/svelte/src/compiler/phases/1-parse/acorn.js

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,51 @@
11
import * as acorn from 'acorn';
22
import { walk } from 'zimmerframe';
3+
import { tsPlugin } from 'acorn-typescript';
4+
5+
// @ts-expect-error
6+
const ParserWithTS = acorn.Parser.extend(tsPlugin());
37

48
/**
59
* @param {string} source
10+
* @param {boolean} typescript
611
*/
7-
export function parse(source) {
12+
export function parse(source, typescript) {
13+
const parser = typescript ? ParserWithTS : acorn.Parser;
814
const { onComment, add_comments } = get_comment_handlers(source);
9-
const ast = acorn.parse(source, {
15+
16+
const ast = parser.parse(source, {
1017
onComment,
1118
sourceType: 'module',
1219
ecmaVersion: 13,
1320
locations: true
1421
});
22+
23+
if (typescript) amend(source, ast);
1524
add_comments(ast);
25+
1626
return /** @type {import('estree').Program} */ (ast);
1727
}
1828

1929
/**
2030
* @param {string} source
31+
* @param {boolean} typescript
2132
* @param {number} index
2233
*/
23-
export function parse_expression_at(source, index) {
34+
export function parse_expression_at(source, typescript, index) {
35+
const parser = typescript ? ParserWithTS : acorn.Parser;
2436
const { onComment, add_comments } = get_comment_handlers(source);
25-
const ast = acorn.parseExpressionAt(source, index, {
37+
38+
const ast = parser.parseExpressionAt(source, index, {
2639
onComment,
2740
sourceType: 'module',
2841
ecmaVersion: 13,
2942
locations: true
3043
});
44+
45+
if (typescript) amend(source, ast);
3146
add_comments(ast);
32-
return /** @type {import('estree').Expression} */ (ast);
47+
48+
return ast;
3349
}
3450

3551
/**
@@ -108,3 +124,29 @@ export function get_comment_handlers(source) {
108124
}
109125
};
110126
}
127+
128+
/**
129+
* Tidy up some stuff left behind by acorn-typescript
130+
* @param {string} source
131+
* @param {import('acorn').Node} node
132+
*/
133+
export function amend(source, node) {
134+
return walk(node, null, {
135+
_(node, context) {
136+
// @ts-expect-error
137+
delete node.loc.start.index;
138+
// @ts-expect-error
139+
delete node.loc.end.index;
140+
141+
if (/** @type {any} */ (node).typeAnnotation && node.end === undefined) {
142+
// i think there might be a bug in acorn-typescript that prevents
143+
// `end` from being assigned when there's a type annotation
144+
let end = /** @type {any} */ (node).typeAnnotation.start;
145+
while (/\s/.test(source[end - 1])) end -= 1;
146+
node.end = end;
147+
}
148+
149+
context.next();
150+
}
151+
});
152+
}

packages/svelte/src/compiler/phases/1-parse/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import read_options from './read/options.js';
1010

1111
const regex_position_indicator = / \(\d+:\d+\)$/;
1212

13+
const regex_lang_attribute =
14+
/<!--[^]*?-->|<script\s+(?:[^>]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s])\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/;
15+
1316
export class Parser {
1417
/**
1518
* @readonly
@@ -20,6 +23,9 @@ export class Parser {
2023
/** */
2124
index = 0;
2225

26+
/** Whether we're parsing in TypeScript mode */
27+
ts = false;
28+
2329
/** @type {import('#compiler').TemplateNode[]} */
2430
stack = [];
2531

@@ -43,6 +49,8 @@ export class Parser {
4349

4450
this.template = template.trimRight();
4551

52+
this.ts = regex_lang_attribute.exec(template)?.[2] === 'ts';
53+
4654
this.root = {
4755
css: null,
4856
js: [],

packages/svelte/src/compiler/phases/1-parse/read/context.js

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ export default function read_context(parser) {
2121

2222
const code = full_char_code_at(parser.template, i);
2323
if (isIdentifierStart(code, true)) {
24+
const name = /** @type {string} */ (parser.read_identifier());
2425
return {
2526
type: 'Identifier',
26-
name: /** @type {string} */ (parser.read_identifier()),
27+
name,
2728
start,
28-
end: parser.index
29+
end: parser.index,
30+
typeAnnotation: read_type_annotation(parser)
2931
};
3032
}
3133

@@ -74,10 +76,32 @@ export default function read_context(parser) {
7476
space_with_newline =
7577
space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
7678

77-
return /** @type {any} */ (
78-
parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, start - 1)
79+
const expression = /** @type {any} */ (
80+
parse_expression_at(`${space_with_newline}(${pattern_string} = 1)`, parser.ts, start - 1)
7981
).left;
82+
83+
expression.typeAnnotation = read_type_annotation(parser);
84+
return expression;
8085
} catch (error) {
8186
parser.acorn_error(error);
8287
}
8388
}
89+
90+
/**
91+
* @param {import('../index.js').Parser} parser
92+
* @returns {any}
93+
*/
94+
function read_type_annotation(parser) {
95+
parser.allow_whitespace();
96+
97+
if (parser.eat(':')) {
98+
// we need to trick Acorn into parsing the type annotation
99+
const insert = '_ as ';
100+
let a = parser.index - insert.length;
101+
const template = ' '.repeat(a) + insert + parser.template.slice(parser.index);
102+
const expression = parse_expression_at(template, parser.ts, a);
103+
104+
parser.index = /** @type {number} */ (expression.end);
105+
return /** @type {any} */ (expression).typeAnnotation;
106+
}
107+
}

packages/svelte/src/compiler/phases/1-parse/read/expression.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { error } from '../../../errors.js';
88
*/
99
export default function read_expression(parser) {
1010
try {
11-
const node = parse_expression_at(parser.template, parser.index);
11+
const node = parse_expression_at(parser.template, parser.ts, parser.index);
1212

1313
let num_parens = 0;
1414

packages/svelte/src/compiler/phases/1-parse/read/script.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export function read_script(parser, start, attributes) {
4949
let ast;
5050

5151
try {
52-
ast = acorn.parse(source);
52+
ast = acorn.parse(source, parser.ts);
5353
} catch (err) {
5454
parser.acorn_error(err);
5555
}

packages/svelte/src/compiler/phases/1-parse/state/tag.js

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import read_context from '../read/context.js';
22
import read_expression from '../read/expression.js';
33
import { error } from '../../../errors.js';
44
import { create_fragment } from '../utils/create.js';
5+
import { parse_expression_at } from '../acorn.js';
6+
import { walk } from 'zimmerframe';
57

68
const regex_whitespace_with_closing_curly_brace = /^\s*}/;
79

@@ -67,10 +69,73 @@ function open(parser) {
6769
if (parser.eat('each')) {
6870
parser.require_whitespace();
6971

70-
const expression = read_expression(parser);
72+
const template = parser.template;
73+
let end = parser.template.length;
74+
75+
/** @type {import('estree').Expression | undefined} */
76+
let expression;
77+
78+
// we have to do this loop because `{#each x as { y = z }}` fails to parse —
79+
// the `as { y = z }` is treated as an Expression but it's actually a Pattern.
80+
// the 'fix' is to backtrack and hide everything from the `as` onwards, until
81+
// we get a valid expression
82+
while (!expression) {
83+
try {
84+
expression = read_expression(parser);
85+
} catch (err) {
86+
end = /** @type {any} */ (err).position[0] - 2;
87+
88+
while (end > start && parser.template.slice(end, end + 2) !== 'as') {
89+
end -= 1;
90+
}
91+
92+
if (end <= start) throw err;
93+
94+
// @ts-expect-error parser.template is meant to be readonly, this is a special case
95+
parser.template = template.slice(0, end);
96+
}
97+
}
98+
99+
// @ts-expect-error
100+
parser.template = template;
101+
71102
parser.allow_whitespace();
72103

73104
// {#each} blocks must declare a context – {#each list as item}
105+
if (!parser.match('as')) {
106+
// this could be a TypeScript assertion that was erroneously eaten.
107+
108+
if (expression.type === 'SequenceExpression') {
109+
expression = expression.expressions[0];
110+
}
111+
112+
let assertion = null;
113+
let end = expression.end;
114+
115+
expression = walk(expression, null, {
116+
// @ts-expect-error
117+
TSAsExpression(node, context) {
118+
if (node.end === /** @type {import('estree').Expression} */ (expression).end) {
119+
assertion = node;
120+
end = node.expression.end;
121+
return node.expression;
122+
}
123+
124+
context.next();
125+
}
126+
});
127+
128+
expression.end = end;
129+
130+
if (assertion) {
131+
// we can't reset `parser.index` to `expression.expression.end` because
132+
// it will ignore any parentheses — we need to jump through this hoop
133+
let end = /** @type {any} */ (/** @type {any} */ (assertion).typeAnnotation).start - 2;
134+
while (parser.template.slice(end, end + 2) !== 'as') end -= 1;
135+
136+
parser.index = end;
137+
}
138+
}
74139
parser.eat('as', true);
75140
parser.require_whitespace();
76141

packages/svelte/src/compiler/phases/2-analyze/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,8 @@ const runes_scope_tweaker = {
674674
}
675675
},
676676
ExportSpecifier(node, { state }) {
677+
if (state.ast_type !== 'instance') return;
678+
677679
state.analysis.exports.push({
678680
name: node.local.name,
679681
alias: node.exported.name

packages/svelte/src/compiler/phases/3-transform/client/transform-client.js

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,20 @@ import { javascript_visitors } from './visitors/javascript.js';
88
import { javascript_visitors_runes } from './visitors/javascript-runes.js';
99
import { javascript_visitors_legacy } from './visitors/javascript-legacy.js';
1010
import { serialize_get_binding } from './utils.js';
11+
import { remove_types } from '../typescript.js';
1112

1213
/**
1314
* This function ensures visitor sets don't accidentally clobber each other
1415
* @param {...import('./types').Visitors} array
1516
* @returns {import('./types').Visitors}
1617
*/
1718
function combine_visitors(...array) {
19+
/** @type {Record<string, any>} */
1820
const visitors = {};
1921

2022
for (const member of array) {
2123
for (const key in member) {
22-
if (key in visitors) {
24+
if (visitors[key]) {
2325
throw new Error(`Duplicate visitor: ${key}`);
2426
}
2527

@@ -100,6 +102,7 @@ export function client_component(source, analysis, options) {
100102
state,
101103
combine_visitors(
102104
set_scope(analysis.module.scopes),
105+
remove_types,
103106
global_visitors,
104107
// @ts-expect-error TODO
105108
javascript_visitors,
@@ -115,22 +118,23 @@ export function client_component(source, analysis, options) {
115118
instance_state,
116119
combine_visitors(
117120
set_scope(analysis.instance.scopes),
121+
{ ...remove_types, ImportDeclaration: undefined, ExportNamedDeclaration: undefined },
118122
global_visitors,
119123
// @ts-expect-error TODO
120124
javascript_visitors,
121125
analysis.runes ? javascript_visitors_runes : javascript_visitors_legacy,
122126
{
123-
ImportDeclaration(node, { state }) {
124-
// @ts-expect-error TODO
125-
state.hoisted.push(node);
126-
return { type: 'EmptyStatement' };
127+
ImportDeclaration(node, context) {
128+
// @ts-expect-error
129+
state.hoisted.push(remove_types.ImportDeclaration(node, context));
130+
return b.empty;
127131
},
128-
ExportNamedDeclaration(node, { visit }) {
132+
ExportNamedDeclaration(node, context) {
129133
if (node.declaration) {
130-
return visit(node.declaration);
134+
// @ts-expect-error
135+
return remove_types.ExportNamedDeclaration(context.visit(node.declaration), context);
131136
}
132137

133-
// specifiers are handled elsewhere
134138
return b.empty;
135139
}
136140
}
@@ -142,8 +146,13 @@ export function client_component(source, analysis, options) {
142146
walk(
143147
/** @type {import('#compiler').SvelteNode} */ (analysis.template.ast),
144148
{ ...state, scope: analysis.instance.scope },
145-
// @ts-expect-error TODO
146-
combine_visitors(set_scope(analysis.template.scopes), global_visitors, template_visitors)
149+
combine_visitors(
150+
set_scope(analysis.template.scopes),
151+
remove_types,
152+
global_visitors,
153+
// @ts-expect-error TODO
154+
template_visitors
155+
)
147156
)
148157
);
149158

0 commit comments

Comments
 (0)