Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: native TypeScript support #9482

Merged
merged 22 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/long-crews-return.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

feat: native TypeScript support
1 change: 1 addition & 0 deletions packages/svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/compiler/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
52 changes: 47 additions & 5 deletions packages/svelte/src/compiler/phases/1-parse/acorn.js
Original file line number Diff line number Diff line change
@@ -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;
}

/**
Expand Down Expand Up @@ -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();
}
});
}
8 changes: 8 additions & 0 deletions packages/svelte/src/compiler/phases/1-parse/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import read_options from './read/options.js';

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

const regex_lang_attribute =
/<!--[^]*?-->|<script\s+(?:[^>]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s])\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/;

export class Parser {
/**
* @readonly
Expand All @@ -20,6 +23,9 @@ export class Parser {
/** */
index = 0;

/** Whether we're parsing in TypeScript mode */
ts = false;

/** @type {import('#compiler').TemplateNode[]} */
stack = [];

Expand All @@ -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: [],
Expand Down
32 changes: 28 additions & 4 deletions packages/svelte/src/compiler/phases/1-parse/read/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
};
}

Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/compiler/phases/1-parse/read/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
67 changes: 66 additions & 1 deletion packages/svelte/src/compiler/phases/1-parse/state/tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -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*}/;

Expand Down Expand Up @@ -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();

Expand Down
2 changes: 2 additions & 0 deletions packages/svelte/src/compiler/phases/2-analyze/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,20 @@ 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
* @param {...import('./types').Visitors} array
* @returns {import('./types').Visitors}
*/
function combine_visitors(...array) {
/** @type {Record<string, any>} */
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}`);
}

Expand Down Expand Up @@ -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,
Expand All @@ -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;
}
}
Expand All @@ -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
)
)
);

Expand Down
Loading