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 <rich.harris@vercel.com>
pull/9566/head
Rich Harris 1 year ago committed by GitHub
parent 9926347ad9
commit 459e4ff006
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: native TypeScript support

@ -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",

@ -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') {

@ -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();
}
});
}

@ -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
@ -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: [],

@ -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;
}
}

@ -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;

@ -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);
}

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

@ -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

@ -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<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}`);
}
@ -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
)
)
);

@ -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
}
)

@ -0,0 +1,53 @@
import * as b from '../../utils/builders.js';
/** @type {import('zimmerframe').Visitors<any, any>} */
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;
}
};

@ -0,0 +1,3 @@
{#each people as { name, cool = true }}
<p>{name} is {cool ? 'cool' : 'not cool'}</p>
{/each}

@ -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
}

@ -1,5 +1,7 @@
{#snippet foo()}
<p>hello</p>
<script lang="ts"></script>
{#snippet foo(msg: string)}
<p>{msg}</p>
{/snippet}
{@render foo()}
{@render foo(msg)}

@ -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"
}
}
}

@ -0,0 +1,10 @@
<script lang="ts">
let count = $state(0);
</script>
<button
on:click={(e: MouseEvent) => {
const next: number = count + 1;
count = next;
}}
>clicks: {count}</button>

@ -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"
}
}
}

@ -1,8 +1,8 @@
<script>
<script lang="ts">
let count = $state(0);
</script>
{#snippet foo(n)}
{#snippet foo(n: number)}
<p>clicks: {n}</p>
{/snippet}

@ -0,0 +1,11 @@
import { test } from '../../test';
export default test({
html: '<button>clicks: 0</button>',
async test({ assert, target }) {
const btn = target.querySelector('button');
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>clicks: 1</button>`);
}
});

@ -0,0 +1,20 @@
<script context="module" lang="ts">
interface Hello { message: 'hello' }
type Goodbye = { message: 'goodbye' };
export type { Hello };
</script>
<script>
import type { Foo } from './types';
import { type Bar, type Baz } from './types';
let count = $state(0);
</script>
<button
on:click={(e: MouseEvent) => {
const next: number = count + 1;
count = next! as number;
}}
>clicks: {count}</button>

@ -0,0 +1,3 @@
export interface Foo {}
export interface Bar {}
export interface Baz {}

@ -10,6 +10,7 @@
"noErrorTruncation": true,
"allowSyntheticDefaultImports": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"types": ["node"],
"strict": true,
"allowJs": true,

@ -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'}

Loading…
Cancel
Save