parse with teasel instead of acorn

goodbye-acorn
Nic 2 days ago
parent 5895c637b0
commit 0a61385aa9

@ -165,6 +165,7 @@
"esbuild": "^0.28.1", "esbuild": "^0.28.1",
"rollup": "^4.59.0", "rollup": "^4.59.0",
"source-map": "^0.7.4", "source-map": "^0.7.4",
"acorn": "^8.18.0",
"tinyglobby": "^0.2.12", "tinyglobby": "^0.2.12",
"typescript": "^5.5.4", "typescript": "^5.5.4",
"vitest": "^4.1.7", "vitest": "^4.1.7",
@ -173,9 +174,8 @@
"dependencies": { "dependencies": {
"@jridgewell/remapping": "^2.3.5", "@jridgewell/remapping": "^2.3.5",
"@jridgewell/sourcemap-codec": "^1.6.0", "@jridgewell/sourcemap-codec": "^1.6.0",
"@sveltejs/acorn-typescript": "^1.0.13", "@teasel/parser": "link:../../../teasel/bindings/node",
"@types/estree": "^1.0.9", "@types/estree": "^1.0.9",
"acorn": "^8.18.0",
"aria-query": "5.3.1", "aria-query": "5.3.1",
"axobject-query": "^4.1.0", "axobject-query": "^4.1.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",

@ -1,289 +0,0 @@
/** @import { Comment, Program, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from './index.js' */
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from '@sveltejs/acorn-typescript';
import * as e from '../../errors.js';
import { locator } from '../../state.js';
const JSParser = acorn.Parser;
const TSParser = JSParser.extend(tsPlugin());
/**
* @typedef {Comment & {
* start: number;
* end: number;
* }} CommentWithLocation
*/
/**
* @param {string} source
* @param {AST.JSComment[]} comments
* @param {boolean} typescript
* @param {boolean} [is_script]
*/
export function parse(source, comments, typescript, is_script) {
const acorn = typescript ? TSParser : JSParser;
const { onComment, add_comments } = get_comment_handlers(
source,
/** @type {CommentWithLocation[]} */ (comments)
);
// @ts-expect-error
const parse_statement = acorn.prototype.parseStatement;
// If we're dealing with a <script> then it might contain an export
// for something that doesn't exist directly inside but is inside the
// component instead, so we need to ensure that Acorn doesn't throw
// an error in these cases
if (is_script) {
// @ts-ignore
acorn.prototype.parseStatement = function (...args) {
const v = parse_statement.call(this, ...args);
// @ts-ignore
this.undefinedExports = {};
return v;
};
}
try {
const ast = acorn.parse(source, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true
});
add_comments(ast);
return /** @type {Program} */ (ast);
} catch (err) {
// TODO the `return` is necessary for TS<7 due to a bug; otherwise
// the `finally` block is regarded as unreachable
return handle_parse_error(err);
} finally {
if (is_script) {
// @ts-expect-error
acorn.prototype.parseStatement = parse_statement;
}
}
}
/**
* @param {Parser} parser
* @param {string} source
* @param {number} index
* @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }}
*/
export function parse_expression_at(parser, source, index) {
const acorn = parser.ts ? TSParser : JSParser;
const { onComment, add_comments } = get_comment_handlers(source, parser.root.comments, index);
try {
const ast = acorn.parseExpressionAt(source, index, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true,
preserveParens: true,
startLocation: start_location(parser, index)
});
add_comments(ast);
return ast;
} catch (e) {
handle_parse_error(e);
}
}
/**
* @param {Parser} parser
* @param {string} source
* @param {number} index
* @returns {Statement}
*/
export function parse_statement_at(parser, source, index) {
// cast to `any`: acorn's Parser constructor and parseStatement/nextToken aren't in its public types
const acorn = /** @type {any} */ (parser.ts ? TSParser : JSParser);
const { onComment, add_comments } = get_comment_handlers(source, parser.root.comments, index);
try {
// This is like parseExpressionAt but for statements
const p = new acorn(
{
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true,
startLocation: start_location(parser, index)
},
source,
index
);
p.nextToken();
const statement = /** @type {Statement} */ (p.parseStatement(null, true, Object.create(null)));
add_comments(/** @type {acorn.Node} */ (statement));
return statement;
} catch (err) {
// A statement that runs to the end of the source (e.g. an unterminated declaration tag)
// is an EOF, not a stray token; preserve the friendlier `unexpected_eof` diagnostic.
if (/** @type {any} */ (err).pos === source.length) e.unexpected_eof(source.length);
handle_parse_error(err);
}
}
const regex_non_lf_line_break = /\r(?!\n)|[\u2028\u2029]/;
let last_template = '';
let lf_only = true;
/**
* Without `startLocation`, acorn counts the lines before `index` on every call
* @param {Parser} parser
* @param {number} index
*/
function start_location(parser, index) {
return has_lf_line_breaks_only(parser) ? locator(index) : undefined;
}
/**
* acorn breaks lines on bare `\r`, `\u2028` and `\u2029`, which the locator doesn't
* @param {Parser} parser
*/
export function has_lf_line_breaks_only(parser) {
if (parser.template !== last_template) {
last_template = parser.template;
lf_only = !regex_non_lf_line_break.test(last_template);
}
return lf_only;
}
const regex_position_indicator = / \(\d+:\d+\)$/;
/**
* @param {any} err
* @returns {never}
*/
function handle_parse_error(err) {
e.js_parse_error(err.pos, err.message.replace(regex_position_indicator, ''));
}
/**
* @param {acorn.Expression} node
* @returns {acorn.Expression}
*/
export function remove_parens(node) {
return walk(node, null, {
ParenthesizedExpression(node, context) {
return context.visit(node.expression);
}
});
}
/**
* Acorn doesn't add comments to the AST by itself. This factory returns the capabilities
* to add them after the fact. They are needed in order to support `svelte-ignore` comments
* in JS code and so that `prettier-plugin-svelte` doesn't remove all comments when formatting.
* @param {string} source
* @param {CommentWithLocation[]} comments
* @param {number} index
*/
function get_comment_handlers(source, comments, index = 0) {
return {
/**
* @param {boolean} block
* @param {string} value
* @param {number} start
* @param {number} end
* @param {import('acorn').Position} [start_loc]
* @param {import('acorn').Position} [end_loc]
*/
onComment: (block, value, start, end, start_loc, end_loc) => {
if (block && /\n/.test(value)) {
let a = start;
while (a > 0 && source[a - 1] !== '\n') a -= 1;
let b = a;
while (/[ \t]/.test(source[b])) b += 1;
const indentation = source.slice(a, b);
value = value.replace(new RegExp(`^${indentation}`, 'gm'), '');
}
comments.push({
type: block ? 'Block' : 'Line',
value,
start,
end,
loc: {
start: /** @type {import('acorn').Position} */ (start_loc),
end: /** @type {import('acorn').Position} */ (end_loc)
}
});
},
/** @param {acorn.Node & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }} ast */
add_comments(ast) {
if (comments.length === 0) return;
comments = comments
.filter((comment) => comment.start >= index)
.map(({ type, value, start, end }) => ({ type, value, start, end }));
walk(ast, null, {
_(node, { next, path }) {
let comment;
while (comments[0] && comments[0].start < node.start) {
comment = /** @type {CommentWithLocation} */ (comments.shift());
(node.leadingComments ||= []).push(comment);
}
next();
if (comments[0]) {
const parent = /** @type {any} */ (path.at(-1));
if (parent === undefined || node.end !== parent.end) {
const slice = source.slice(node.end, comments[0].start);
const is_last_in_body =
((parent?.type === 'BlockStatement' || parent?.type === 'Program') &&
parent.body.indexOf(node) === parent.body.length - 1) ||
(parent?.type === 'ArrayExpression' &&
parent.elements.indexOf(node) === parent.elements.length - 1) ||
(parent?.type === 'ObjectExpression' &&
parent.properties.indexOf(node) === parent.properties.length - 1);
if (is_last_in_body) {
// Special case: There can be multiple trailing comments after the last node in a block,
// and they can be separated by newlines
let end = node.end;
while (comments.length) {
const comment = comments[0];
if (parent && comment.start >= parent.end) break;
(node.trailingComments ||= []).push(comment);
comments.shift();
end = comment.end;
}
} else if (node.end <= comments[0].start && /^[,) \t]*$/.test(slice)) {
node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())];
}
}
}
}
});
// Special case: Trailing comments after the root node (which can only happen for expression tags or for Program nodes).
// Adding them ensures that we can later detect the end of the expression tag correctly.
if (comments.length > 0 && (comments[0].start >= ast.end || ast.type === 'Program')) {
(ast.trailingComments ||= []).push(...comments.splice(0));
}
}
};
}

@ -1,8 +1,7 @@
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Location } from 'locate-character' */ /** @import { Location } from 'locate-character' */
/** @import * as ESTree from 'estree' */ /** @import * as ESTree from 'estree' */
// @ts-expect-error acorn type definitions are borked in the release we use import { Source, isIdentifierStart, isIdentifierChar } from '@teasel/parser';
import { isIdentifierStart, isIdentifierChar } from 'acorn';
import fragment from './state/fragment.js'; import fragment from './state/fragment.js';
import * as e from '../../errors.js'; import * as e from '../../errors.js';
import { create_fragment } from './utils/create.js'; import { create_fragment } from './utils/create.js';
@ -53,6 +52,9 @@ export class Parser {
/** Whether we're parsing in TypeScript mode */ /** Whether we're parsing in TypeScript mode */
ts = false; ts = false;
/** The template as the JavaScript parser holds it, for every expression, pattern and tag in it */
js;
/** @type {AST.TemplateNode[]} */ /** @type {AST.TemplateNode[]} */
stack = []; stack = [];
@ -88,6 +90,12 @@ export class Parser {
regex_lang_attribute.lastIndex = 0; // reset matched index to pass tests - otherwise declare the regex inside the constructor regex_lang_attribute.lastIndex = 0; // reset matched index to pass tests - otherwise declare the regex inside the constructor
this.ts = match_lang?.[2] === 'ts'; this.ts = match_lang?.[2] === 'ts';
this.js = new Source(this.template, {
sourceType: 'module',
typescript: this.ts,
comments: true,
locations: true
});
this.root = { this.root = {
css: null, css: null,
@ -229,14 +237,14 @@ export class Parser {
const code = /** @type {number} */ (this.template.codePointAt(this.index)); const code = /** @type {number} */ (this.template.codePointAt(this.index));
if (isIdentifierStart(code, true)) { if (isIdentifierStart(code)) {
let i = this.index; let i = this.index;
end += code <= 0xffff ? 1 : 2; end += code <= 0xffff ? 1 : 2;
while (end < this.template.length) { while (end < this.template.length) {
const code = /** @type {number} */ (this.template.codePointAt(end)); const code = /** @type {number} */ (this.template.codePointAt(end));
if (!isIdentifierChar(code, true)) break; if (!isIdentifierChar(code)) break;
end += code <= 0xffff ? 1 : 2; end += code <= 0xffff ? 1 : 2;
} }

@ -0,0 +1,132 @@
/** @import { Expression, Pattern, Program, Statement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from './index.js' */
import * as teasel from '@teasel/parser';
import * as e from '../../errors.js';
/**
* @param {string} source
* @param {AST.JSComment[]} comments
* @param {boolean} typescript
* @param {boolean} [is_script] a `<script>` may export names the component declares elsewhere
* @returns {Program}
*/
export function parse(source, comments, typescript, is_script) {
try {
const ast = teasel.parse(source, {
sourceType: 'module',
typescript,
comments: true,
locations: true,
allowUndeclaredExports: is_script
});
add_comments(source, comments, /** @type {teasel.Comment[]} */ (ast.comments));
delete ast.comments;
return ast;
} catch (err) {
return handle_parse_error(err);
}
}
/**
* @param {Parser} parser
* @param {number} index
* @param {'as' | 'in'} [until] a word operator the expression stops before, at the top level
* @returns {{ node: Expression, end: number }}
*/
export function parse_expression_at(parser, index, until) {
try {
const { node, end, comments } = parser.js.parseExpressionAt(index, until);
add_comments(parser.template, parser.root.comments, /** @type {teasel.Comment[]} */ (comments));
return { node, end };
} catch (err) {
return handle_parse_error(err);
}
}
/**
* @param {Parser} parser
* @param {number} index
* @returns {{ node: Pattern, end: number }}
*/
export function parse_pattern_at(parser, index) {
try {
const { node, end, comments } = parser.js.parsePatternAt(index);
add_comments(parser.template, parser.root.comments, /** @type {teasel.Comment[]} */ (comments));
return { node, end };
} catch (err) {
return handle_parse_error(err);
}
}
/**
* @param {Parser} parser
* @param {string} source
* @param {number} index the opening paren
* @returns {{ params: Pattern[], end: number }}
*/
export function parse_params_at(parser, index) {
try {
const { params, end, comments } = parser.js.parseParamsAt(index);
add_comments(parser.template, parser.root.comments, /** @type {teasel.Comment[]} */ (comments));
return { params, end };
} catch (err) {
return handle_parse_error(err);
}
}
/**
* @param {Parser} parser
* @param {number} index
* @returns {{ node: Statement, end: number }}
*/
export function parse_statement_at(parser, index) {
try {
const { node, end, comments } = parser.js.parseStatementAt(index);
add_comments(parser.template, parser.root.comments, /** @type {teasel.Comment[]} */ (comments));
return { node, end };
} catch (err) {
// A statement that runs to the end of the source (e.g. an unterminated declaration tag)
// is an EOF, not a stray token; preserve the friendlier `unexpected_eof` diagnostic.
if (/** @type {any} */ (err).pos === parser.template.length)
e.unexpected_eof(parser.template.length);
return handle_parse_error(err);
}
}
const regex_position_indicator = / \(\d+:\d+\)$/;
/**
* @param {any} err
* @returns {never}
*/
function handle_parse_error(err) {
e.js_parse_error(err.pos, err.message.replace(regex_position_indicator, ''));
}
/**
* Comments are needed in order to support `svelte-ignore` comments in JS code and so that
* `prettier-plugin-svelte` doesn't remove all comments when formatting. A block comment loses
* the indentation of the line it starts on.
* @param {string} source
* @param {AST.JSComment[]} comments
* @param {teasel.Comment[]} parsed
*/
function add_comments(source, comments, parsed) {
for (const comment of parsed) {
if (comment.type === 'Block' && comment.value.includes('\n')) {
let a = comment.start;
while (a > 0 && source[a - 1] !== '\n') a -= 1;
let b = a;
while (/[ \t]/.test(source[b])) b += 1;
const indentation = source.slice(a, b);
comment.value = comment.value.replace(new RegExp(`^${indentation}`, 'gm'), '');
}
comments.push(/** @type {AST.JSComment} */ (comment));
}
}

@ -1,7 +1,6 @@
/** @import { Pattern } from 'estree' */ /** @import { Pattern } from 'estree' */
/** @import { Parser } from '../index.js' */ /** @import { Parser } from '../index.js' */
import { match_bracket } from '../utils/bracket.js'; import { parse_pattern_at } from '../js.js';
import { parse_expression_at, remove_parens } from '../acorn.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
/** /**
@ -10,86 +9,28 @@ import * as e from '../../../errors.js';
*/ */
export default function read_pattern(parser) { export default function read_pattern(parser) {
const start = parser.index; const start = parser.index;
let i = parser.index;
const id = parser.read_identifier(); const id = parser.read_identifier();
if (id.name !== '') { if (id.name !== '') {
const annotation = read_type_annotation(parser); const after = parser.index;
parser.allow_whitespace();
return {
...id, // a type annotation makes it a job for the parser
typeAnnotation: annotation if (!parser.match(':')) {
}; parser.index = after;
} return id;
}
const char = parser.template[i]; } else {
const char = parser.template[start];
if (char !== '{' && char !== '[') {
e.expected_pattern(i); if (char !== '{' && char !== '[') {
} e.expected_pattern(start);
}
i = match_bracket(parser, start);
parser.index = i;
// acorn never reads before `start`, so the template itself can serve as the prefix
/** @type {any} */
let expression = remove_parens(
parse_expression_at(parser, parser.template.slice(0, i) + ' = 1', start)
);
expression = expression.left;
expression.typeAnnotation = read_type_annotation(parser);
if (expression.typeAnnotation) {
expression.end = expression.typeAnnotation.end;
}
return expression;
}
/**
* @param {Parser} parser
* @returns {any}
*/
function read_type_annotation(parser) {
const start = parser.index;
parser.allow_whitespace();
if (!parser.eat(':')) {
parser.index = start;
return undefined;
} }
// we need to trick Acorn into parsing the type annotation const { node, end } = parse_pattern_at(parser, start);
const insert = '_ as '; parser.index = end;
let a = parser.index - insert.length;
const template =
parser.template.slice(0, a) +
insert +
// If this is a type annotation for a function parameter, Acorn-TS will treat subsequent
// parameters as part of a sequence expression instead, and will then error on optional
// parameters (`?:`). Therefore replace that sequence with something that will not error.
parser.template.slice(parser.index).replace(/\?\s*:/g, ':');
let expression = remove_parens(parse_expression_at(parser, template, a));
// `foo: bar = baz` gets mangled — fix it
if (expression.type === 'AssignmentExpression') {
let b = expression.right.start;
while (template[b] !== '=') b -= 1;
expression = remove_parens(parse_expression_at(parser, template.slice(0, b), a));
}
// `array as item: string, index` becomes `string, index`, which is mistaken as a sequence expression - fix that
if (expression.type === 'SequenceExpression') {
expression = expression.expressions[0];
}
parser.index = /** @type {number} */ (expression.end); return node;
return {
type: 'TSTypeAnnotation',
start,
end: parser.index,
typeAnnotation: /** @type {any} */ (expression).typeAnnotation
};
} }

@ -1,8 +1,7 @@
/** @import { Expression, Identifier } from 'estree' */ /** @import { Expression, Identifier } from 'estree' */
/** @import { Parser } from '../index.js' */ /** @import { Parser } from '../index.js' */
// @ts-expect-error acorn type definitions are borked in the release we use import { isIdentifierStart, isIdentifierChar } from '@teasel/parser';
import { isIdentifierStart, isIdentifierChar } from 'acorn'; import { parse_expression_at } from '../js.js';
import { has_lf_line_breaks_only, parse_expression_at, remove_parens } from '../acorn.js';
import { regex_whitespace } from '../../patterns.js'; import { regex_whitespace } from '../../patterns.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { find_matching_bracket } from '../utils/bracket.js'; import { find_matching_bracket } from '../utils/bracket.js';
@ -34,23 +33,17 @@ export function get_loose_identifier(parser, opening_token) {
* @param {Parser} parser * @param {Parser} parser
* @param {string} [opening_token] * @param {string} [opening_token]
* @param {boolean} [disallow_loose] * @param {boolean} [disallow_loose]
* @param {'as' | 'in'} [until] a word operator the expression stops before, at the top level
* @returns {Expression} * @returns {Expression}
*/ */
export default function read_expression(parser, opening_token, disallow_loose) { export default function read_expression(parser, opening_token, disallow_loose, until) {
const simple = read_simple_expression(parser); const simple = read_simple_expression(parser);
if (simple) return simple; if (simple) return simple;
try { try {
const node = parse_expression_at(parser, parser.template, parser.index); const { node, end } = parse_expression_at(parser, parser.index, until);
parser.index = end;
let index = /** @type {number} */ (node.end); return node;
const last_comment = parser.root.comments.at(-1);
if (last_comment && last_comment.end > index) index = last_comment.end;
parser.index = index;
return /** @type {Expression} */ (remove_parens(node));
} catch (err) { } catch (err) {
// If we are in an each loop we need the error to be thrown in cases like // If we are in an each loop we need the error to be thrown in cases like
// `as { y = z }` so we still throw and handle the error there // `as { y = z }` so we still throw and handle the error there
@ -65,9 +58,26 @@ export default function read_expression(parser, opening_token, disallow_loose) {
} }
} }
const regex_non_lf_line_break = /\r(?!\n)|[\u2028\u2029]/;
let last_template = '';
let lf_only = true;
/**
* The parser breaks lines on bare `\r`, `\u2028` and `\u2029`, which the locator doesn't
* @param {Parser} parser
*/
function has_lf_line_breaks_only(parser) {
if (parser.template !== last_template) {
last_template = parser.template;
lf_only = !regex_non_lf_line_break.test(last_template);
}
return lf_only;
}
/** /**
* Most template expressions are an identifier or a `a.b.c` member chain followed by `}`. * Most template expressions are an identifier or a `a.b.c` member chain followed by `}`.
* Those are built directly for better parse performance, with the same shape acorn would produce; anything else goes to acorn * Those are built directly for better parse performance, with the same shape the parser would produce; anything else goes to the parser
* @param {Parser} parser * @param {Parser} parser
* @returns {Expression | null} * @returns {Expression | null}
*/ */
@ -131,13 +141,13 @@ function read_word(template, start) {
if (start >= template.length) return -1; if (start >= template.length) return -1;
const code = /** @type {number} */ (template.codePointAt(start)); const code = /** @type {number} */ (template.codePointAt(start));
if (!isIdentifierStart(code, true)) return -1; if (!isIdentifierStart(code)) return -1;
let end = start + (code <= 0xffff ? 1 : 2); let end = start + (code <= 0xffff ? 1 : 2);
while (end < template.length) { while (end < template.length) {
const code = /** @type {number} */ (template.codePointAt(end)); const code = /** @type {number} */ (template.codePointAt(end));
if (!isIdentifierChar(code, true)) break; if (!isIdentifierChar(code)) break;
end += code <= 0xffff ? 1 : 2; end += code <= 0xffff ? 1 : 2;
} }

@ -1,7 +1,7 @@
/** @import { Program } from 'estree' */ /** @import { Program } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */ /** @import { Parser } from '../index.js' */
import * as acorn from '../acorn.js'; import { parse } from '../js.js';
import { regex_not_newline_characters } from '../../patterns.js'; import { regex_not_newline_characters } from '../../patterns.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import * as w from '../../../warnings.js'; import * as w from '../../../warnings.js';
@ -31,7 +31,7 @@ export function read_script(parser, start, attributes) {
parser.template.slice(0, script_start).replace(regex_not_newline_characters, ' ') + data; parser.template.slice(0, script_start).replace(regex_not_newline_characters, ' ') + data;
parser.read(regex_starts_with_closing_script_tag); parser.read(regex_starts_with_closing_script_tag);
const ast = acorn.parse(source, parser.root.comments, parser.ts, true); const ast = parse(source, parser.root.comments, parser.ts, true);
ast.start = script_start; ast.start = script_start;

@ -1,12 +1,11 @@
/** @import { ArrowFunctionExpression, Expression, Identifier, Pattern, VariableDeclaration } from 'estree' */ /** @import { Expression, Identifier, Pattern, VariableDeclaration } from 'estree' */
/** @import { AST } from '#compiler' */ /** @import { AST } from '#compiler' */
/** @import { Parser } from '../index.js' */ /** @import { Parser } from '../index.js' */
import { walk } from 'zimmerframe';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { ExpressionMetadata } from '../../nodes.js'; import { ExpressionMetadata } from '../../nodes.js';
import { parse_expression_at, parse_statement_at } from '../acorn.js'; import { parse_params_at, parse_statement_at } from '../js.js';
import read_pattern from '../read/context.js'; import read_pattern from '../read/context.js';
import read_expression, { get_loose_identifier } from '../read/expression.js'; import read_expression from '../read/expression.js';
import { create_fragment } from '../utils/create.js'; import { create_fragment } from '../utils/create.js';
import { find_matching_bracket, match_bracket } from '../utils/bracket.js'; import { find_matching_bracket, match_bracket } from '../utils/bracket.js';
@ -90,12 +89,14 @@ function read_declaration(parser) {
/** @type {import('estree').Statement | import('estree').VariableDeclaration} */ /** @type {import('estree').Statement | import('estree').VariableDeclaration} */
let declaration; let declaration;
/** @type {number} */
let end;
try { try {
declaration = parse_statement_at(parser, parser.template, start); ({ node: declaration, end } = parse_statement_at(parser, start));
} catch (error) { } catch (error) {
if (!parser.loose) throw error; if (!parser.loose) throw error;
const end = find_matching_bracket(parser.template, start, '{'); end = /** @type {number} */ (find_matching_bracket(parser.template, start, '{'));
if (end === undefined) throw error; if (end === undefined) throw error;
parser.index = end; parser.index = end;
@ -141,7 +142,7 @@ function read_declaration(parser) {
e.declaration_tag_invalid_type(declaration); e.declaration_tag_invalid_type(declaration);
} }
parser.index = /** @type {number} */ (declaration.end); parser.index = end;
parser.allow_whitespace(); parser.allow_whitespace();
parser.eat('}', true); parser.eat('}', true);
@ -182,80 +183,14 @@ function open(parser) {
if (parser.eat('each')) { if (parser.eat('each')) {
parser.require_whitespace(); parser.require_whitespace();
const template = parser.template; // the list ends at the `as` that names the item, so a TypeScript assertion in it needs parens
let end = parser.template.length; let expression = read_expression(parser, undefined, false, 'as');
/** @type {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, undefined, true);
} catch (err) {
end = /** @type {any} */ (err).position[0] - 2;
while (end > start && parser.template.slice(end, end + 2) !== 'as') {
end -= 1;
}
if (end <= start) {
if (parser.loose) {
expression = get_loose_identifier(parser);
if (expression) {
break;
}
}
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(); parser.allow_whitespace();
// {#each} blocks must declare a context {#each list as item} // {#each} blocks must declare a context {#each list as item}
if (!parser.match('as')) { if (!parser.match('as') && expression.type === 'SequenceExpression') {
// this could be a TypeScript assertion that was erroneously eaten. expression = expression.expressions[0];
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 {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;
}
} }
/** @type {Pattern | null} */ /** @type {Pattern | null} */
@ -456,8 +391,6 @@ function open(parser) {
parser.allow_whitespace(); parser.allow_whitespace();
const params_start = parser.index;
// snippets could have a generic signature, e.g. `#snippet foo<T>(...)` // snippets could have a generic signature, e.g. `#snippet foo<T>(...)`
/** @type {string | undefined} */ /** @type {string | undefined} */
let type_params; let type_params;
@ -474,30 +407,21 @@ function open(parser) {
parser.allow_whitespace(); parser.allow_whitespace();
const matched = parser.eat('(', true, false); /** @type {import('estree').Pattern[]} */
let parameters = [];
if (matched) { if (parser.eat('(', true, false)) {
let parentheses = 1; const open = parser.index - 1;
while (parser.index < parser.template.length && (!parser.match(')') || parentheses !== 1)) { if (find_matching_bracket(parser.template, parser.index, '(') === undefined) {
if (parser.match('(')) parentheses++; e.expected_token(parser.template.length, ')');
if (parser.match(')')) parentheses--;
parser.index += 1;
} }
parser.eat(')', true); const { params, end } = parse_params_at(parser, open);
parameters = params;
parser.index = end;
} }
let function_expression = matched
? /** @type {ArrowFunctionExpression} */ (
parse_expression_at(
parser,
parser.template.slice(0, parser.index) + ' => {}',
params_start
)
)
: { params: [] };
parser.allow_whitespace(); parser.allow_whitespace();
parser.eat('}', true); parser.eat('}', true);
@ -508,7 +432,7 @@ function open(parser) {
end: -1, end: -1,
expression: id, expression: id,
typeParams: type_params, typeParams: type_params,
parameters: function_expression.params, parameters,
body: create_fragment(), body: create_fragment(),
metadata: { metadata: {
can_hoist: false, can_hoist: false,

@ -3,7 +3,7 @@
/** @import { AnalysisState, Visitors } from './types' */ /** @import { AnalysisState, Visitors } from './types' */
/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */ /** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { parse } from '../1-parse/acorn.js'; import { parse } from '../1-parse/js.js';
import * as e from '../../errors.js'; import * as e from '../../errors.js';
import * as w from '../../warnings.js'; import * as w from '../../warnings.js';
import { import {

@ -685,7 +685,7 @@ export class Scope {
const binding = this.declarations.get(node.name); const binding = this.declarations.get(node.name);
if (binding && binding.declaration_kind !== 'var' && declaration_kind !== 'var') { if (binding && binding.declaration_kind !== 'var' && declaration_kind !== 'var') {
// This also errors on function types, but that's arguably a good thing // This also errors on function types, but that's arguably a good thing
// declaring function twice is also caught by acorn in the parse phase // declaring function twice is also caught by the parser
e.declaration_duplicate(node, node.name); e.declaration_duplicate(node, node.name);
} }
} }

@ -48,6 +48,24 @@
"column": 2 "column": 2
} }
}, },
"leadingComments": [
{
"type": "Line",
"value": " comment",
"start": 584,
"end": 594,
"loc": {
"start": {
"line": 34,
"column": 11
},
"end": {
"line": 34,
"column": 21
}
}
}
],
"id": null, "id": null,
"expression": false, "expression": false,
"generator": false, "generator": false,
@ -82,6 +100,58 @@
"column": 7 "column": 7
} }
}, },
"leadingComments": [
{
"type": "Block",
"value": " another comment ",
"start": 606,
"end": 627,
"loc": {
"start": {
"line": 36,
"column": 2
},
"end": {
"line": 36,
"column": 23
}
}
}
],
"trailingComments": [
{
"type": "Line",
"value": " a trailing comment",
"start": 636,
"end": 657,
"loc": {
"start": {
"line": 37,
"column": 8
},
"end": {
"line": 37,
"column": 29
}
}
},
{
"type": "Block",
"value": " trailing block comment ",
"start": 660,
"end": 688,
"loc": {
"start": {
"line": 38,
"column": 2
},
"end": {
"line": 38,
"column": 30
}
}
}
],
"expression": { "expression": {
"type": "CallExpression", "type": "CallExpression",
"start": 630, "start": 630,
@ -114,40 +184,10 @@
}, },
"arguments": [], "arguments": [],
"optional": false "optional": false
}, }
"leadingComments": [
{
"type": "Block",
"value": " another comment ",
"start": 606,
"end": 627
}
],
"trailingComments": [
{
"type": "Line",
"value": " a trailing comment",
"start": 636,
"end": 657
},
{
"type": "Block",
"value": " trailing block comment ",
"start": 660,
"end": 688
}
]
} }
] ]
}, }
"leadingComments": [
{
"type": "Line",
"value": " comment",
"start": 584,
"end": 594
}
]
}, },
"modifiers": [] "modifiers": []
} }
@ -178,15 +218,25 @@
"column": 30 "column": 30
} }
}, },
"name": "a",
"leadingComments": [ "leadingComments": [
{ {
"type": "Block", "type": "Block",
"value": " leading block comment ", "value": " leading block comment ",
"start": 696, "start": 696,
"end": 723 "end": 723,
"loc": {
"start": {
"line": 41,
"column": 1
},
"end": {
"line": 41,
"column": 28
}
}
} }
] ],
"name": "a"
} }
}, },
{ {
@ -223,6 +273,58 @@
"column": 6 "column": 6
} }
}, },
"leadingComments": [
{
"type": "Line",
"value": " leading line comment",
"start": 739,
"end": 762,
"loc": {
"start": {
"line": 43,
"column": 2
},
"end": {
"line": 43,
"column": 25
}
}
}
],
"trailingComments": [
{
"type": "Line",
"value": " trailing line comment",
"start": 770,
"end": 794,
"loc": {
"start": {
"line": 44,
"column": 7
},
"end": {
"line": 44,
"column": 31
}
}
},
{
"type": "Block",
"value": " trailing block comment ",
"start": 796,
"end": 824,
"loc": {
"start": {
"line": 45,
"column": 1
},
"end": {
"line": 45,
"column": 29
}
}
}
],
"left": { "left": {
"type": "Identifier", "type": "Identifier",
"start": 764, "start": 764,
@ -255,29 +357,7 @@
} }
}, },
"name": "b" "name": "b"
}, }
"leadingComments": [
{
"type": "Line",
"value": " leading line comment",
"start": 739,
"end": 762
}
],
"trailingComments": [
{
"type": "Line",
"value": " trailing line comment",
"start": 770,
"end": 794
},
{
"type": "Block",
"value": " trailing block comment ",
"start": 796,
"end": 824
}
]
} }
} }
] ]
@ -316,6 +396,42 @@
"column": 13 "column": 13
} }
}, },
"leadingComments": [
{
"type": "Line",
"value": " a leading comment",
"start": 10,
"end": 30,
"loc": {
"start": {
"line": 2,
"column": 1
},
"end": {
"line": 2,
"column": 21
}
}
}
],
"trailingComments": [
{
"type": "Line",
"value": " a trailing comment",
"start": 45,
"end": 66,
"loc": {
"start": {
"line": 3,
"column": 14
},
"end": {
"line": 3,
"column": 35
}
}
}
],
"declarations": [ "declarations": [
{ {
"type": "VariableDeclarator", "type": "VariableDeclarator",
@ -366,23 +482,7 @@
} }
} }
], ],
"kind": "const", "kind": "const"
"leadingComments": [
{
"type": "Line",
"value": " a leading comment",
"start": 10,
"end": 30
}
],
"trailingComments": [
{
"type": "Line",
"value": " a trailing comment",
"start": 45,
"end": 66
}
]
}, },
{ {
"type": "VariableDeclaration", "type": "VariableDeclaration",
@ -464,6 +564,24 @@
"column": 2 "column": 2
} }
}, },
"leadingComments": [
{
"type": "Block",
"value": "* a comment ",
"start": 83,
"end": 99,
"loc": {
"start": {
"line": 6,
"column": 1
},
"end": {
"line": 6,
"column": 17
}
}
}
],
"id": { "id": {
"type": "Identifier", "type": "Identifier",
"start": 110, "start": 110,
@ -513,6 +631,24 @@
"column": 6 "column": 6
} }
}, },
"trailingComments": [
{
"type": "Line",
"value": " trailing",
"start": 125,
"end": 136,
"loc": {
"start": {
"line": 8,
"column": 7
},
"end": {
"line": 8,
"column": 18
}
}
}
],
"expression": { "expression": {
"type": "Identifier", "type": "Identifier",
"start": 120, "start": 120,
@ -528,15 +664,7 @@
} }
}, },
"name": "foo" "name": "foo"
}, }
"trailingComments": [
{
"type": "Line",
"value": " trailing",
"start": 125,
"end": 136
}
]
}, },
{ {
"type": "ExpressionStatement", "type": "ExpressionStatement",
@ -552,40 +680,54 @@
"column": 6 "column": 6
} }
}, },
"expression": {
"type": "Identifier",
"start": 217,
"end": 220,
"loc": {
"start": {
"line": 12,
"column": 2
},
"end": {
"line": 12,
"column": 5
}
},
"name": "bar"
},
"leadingComments": [ "leadingComments": [
{ {
"type": "Block", "type": "Block",
"value": " leading comment 1 ", "value": " leading comment 1 ",
"start": 139, "start": 139,
"end": 162 "end": 162,
"loc": {
"start": {
"line": 9,
"column": 2
},
"end": {
"line": 9,
"column": 25
}
}
}, },
{ {
"type": "Block", "type": "Block",
"value": " leading comment 2 ", "value": " leading comment 2 ",
"start": 165, "start": 165,
"end": 188 "end": 188,
"loc": {
"start": {
"line": 10,
"column": 2
},
"end": {
"line": 10,
"column": 25
}
}
}, },
{ {
"type": "Block", "type": "Block",
"value": " leading comment 3 ", "value": " leading comment 3 ",
"start": 191, "start": 191,
"end": 214 "end": 214,
"loc": {
"start": {
"line": 11,
"column": 2
},
"end": {
"line": 11,
"column": 25
}
}
} }
], ],
"trailingComments": [ "trailingComments": [
@ -593,32 +735,70 @@
"type": "Block", "type": "Block",
"value": " trailing comment 1 ", "value": " trailing comment 1 ",
"start": 224, "start": 224,
"end": 248 "end": 248,
"loc": {
"start": {
"line": 13,
"column": 2
},
"end": {
"line": 13,
"column": 26
}
}
}, },
{ {
"type": "Block", "type": "Block",
"value": " trailing comment 2 ", "value": " trailing comment 2 ",
"start": 251, "start": 251,
"end": 275 "end": 275,
"loc": {
"start": {
"line": 14,
"column": 2
},
"end": {
"line": 14,
"column": 26
}
}
}, },
{ {
"type": "Block", "type": "Block",
"value": " trailing comment 3 ", "value": " trailing comment 3 ",
"start": 278, "start": 278,
"end": 302 "end": 302,
"loc": {
"start": {
"line": 15,
"column": 2
},
"end": {
"line": 15,
"column": 26
}
}
} }
] ],
"expression": {
"type": "Identifier",
"start": 217,
"end": 220,
"loc": {
"start": {
"line": 12,
"column": 2
},
"end": {
"line": 12,
"column": 5
}
},
"name": "bar"
}
} }
] ]
}, }
"leadingComments": [
{
"type": "Block",
"value": "* a comment ",
"start": 83,
"end": 99
}
]
}, },
{ {
"type": "VariableDeclaration", "type": "VariableDeclaration",
@ -694,20 +874,38 @@
"column": 3 "column": 3
} }
}, },
"value": 1,
"raw": "1",
"leadingComments": [ "leadingComments": [
{ {
"type": "Line", "type": "Line",
"value": " leading comment 1", "value": " leading comment 1",
"start": 326, "start": 326,
"end": 346 "end": 346,
"loc": {
"start": {
"line": 19,
"column": 2
},
"end": {
"line": 19,
"column": 22
}
}
}, },
{ {
"type": "Line", "type": "Line",
"value": " leading comment 2", "value": " leading comment 2",
"start": 349, "start": 349,
"end": 369 "end": 369,
"loc": {
"start": {
"line": 20,
"column": 2
},
"end": {
"line": 20,
"column": 22
}
}
} }
], ],
"trailingComments": [ "trailingComments": [
@ -715,15 +913,37 @@
"type": "Line", "type": "Line",
"value": " trailing comment 1", "value": " trailing comment 1",
"start": 375, "start": 375,
"end": 396 "end": 396,
"loc": {
"start": {
"line": 21,
"column": 5
},
"end": {
"line": 21,
"column": 26
}
}
}, },
{ {
"type": "Block", "type": "Block",
"value": " trailing comment 2 ", "value": " trailing comment 2 ",
"start": 399, "start": 399,
"end": 423 "end": 423,
"loc": {
"start": {
"line": 22,
"column": 2
},
"end": {
"line": 22,
"column": 26
}
}
} }
] ],
"value": 1,
"raw": "1"
} }
] ]
} }
@ -805,6 +1025,74 @@
"column": 6 "column": 6
} }
}, },
"leadingComments": [
{
"type": "Line",
"value": " leading comment 1",
"start": 449,
"end": 469,
"loc": {
"start": {
"line": 26,
"column": 2
},
"end": {
"line": 26,
"column": 22
}
}
},
{
"type": "Line",
"value": " leading comment 2",
"start": 472,
"end": 492,
"loc": {
"start": {
"line": 27,
"column": 2
},
"end": {
"line": 27,
"column": 22
}
}
}
],
"trailingComments": [
{
"type": "Line",
"value": " trailing comment 1",
"start": 501,
"end": 522,
"loc": {
"start": {
"line": 28,
"column": 8
},
"end": {
"line": 28,
"column": 29
}
}
},
{
"type": "Block",
"value": " trailing comment 2 ",
"start": 525,
"end": 549,
"loc": {
"start": {
"line": 29,
"column": 2
},
"end": {
"line": 29,
"column": 26
}
}
}
],
"method": false, "method": false,
"shorthand": false, "shorthand": false,
"computed": false, "computed": false,
@ -841,35 +1129,7 @@
"value": 1, "value": 1,
"raw": "1" "raw": "1"
}, },
"kind": "init", "kind": "init"
"leadingComments": [
{
"type": "Line",
"value": " leading comment 1",
"start": 449,
"end": 469
},
{
"type": "Line",
"value": " leading comment 2",
"start": 472,
"end": 492
}
],
"trailingComments": [
{
"type": "Line",
"value": " trailing comment 1",
"start": 501,
"end": 522
},
{
"type": "Block",
"value": " trailing comment 2 ",
"start": 525,
"end": 549
}
]
} }
] ]
} }

@ -40,16 +40,26 @@
"column": 9 "column": 9
} }
}, },
"body": [], "innerComments": [
"sourceType": "module",
"trailingComments": [
{ {
"type": "Line", "type": "Line",
"value": " TODO write some code", "value": " TODO write some code",
"start": 10, "start": 10,
"end": 33 "end": 33,
"loc": {
"start": {
"line": 2,
"column": 1
},
"end": {
"line": 2,
"column": 24
}
}
} }
] ],
"body": [],
"sourceType": "module"
} }
}, },
"_comments": [ "_comments": [

@ -63,6 +63,24 @@
"column": 31 "column": 31
} }
}, },
"leadingComments": [
{
"type": "Block",
"value": "* ( ",
"start": 58,
"end": 66,
"loc": {
"start": {
"line": 6,
"column": 1
},
"end": {
"line": 6,
"column": 9
}
}
}
],
"expressions": [ "expressions": [
{ {
"type": "ArrowFunctionExpression", "type": "ArrowFunctionExpression",
@ -235,14 +253,6 @@
} }
} }
} }
],
"leadingComments": [
{
"type": "Block",
"value": "* ( ",
"start": 58,
"end": 66
}
] ]
}, },
"modifiers": [] "modifiers": []
@ -256,6 +266,24 @@
] ]
}, },
"options": null, "options": null,
"comments": [
{
"type": "Block",
"value": "* ( ",
"start": 58,
"end": 66,
"loc": {
"start": {
"line": 6,
"column": 1
},
"end": {
"line": 6,
"column": 9
}
}
}
],
"instance": { "instance": {
"type": "Script", "type": "Script",
"start": 0, "start": 0,

@ -25,16 +25,26 @@
"column": 9 "column": 9
} }
}, },
"value": 42,
"raw": "42",
"leadingComments": [ "leadingComments": [
{ {
"type": "Block", "type": "Block",
"value": "", "value": "",
"start": 2, "start": 2,
"end": 6 "end": 6,
"loc": {
"start": {
"line": 1,
"column": 2
},
"end": {
"line": 1,
"column": 6
}
}
} }
] ],
"value": 42,
"raw": "42"
} }
} }
] ]

@ -115,16 +115,26 @@
"column": 9 "column": 9
} }
}, },
"body": [], "innerComments": [
"sourceType": "module",
"trailingComments": [
{ {
"type": "Line", "type": "Line",
"value": " script and style but no markup", "value": " script and style but no markup",
"start": 10, "start": 10,
"end": 43 "end": 43,
"loc": {
"start": {
"line": 2,
"column": 1
},
"end": {
"line": 2,
"column": 34
}
}
} }
] ],
"body": [],
"sourceType": "module"
}, },
"attributes": [] "attributes": []
} }

@ -56,15 +56,25 @@
"column": 35 "column": 35
} }
}, },
"name": "Object",
"leadingComments": [ "leadingComments": [
{ {
"type": "Block", "type": "Block",
"value": " probe ", "value": " probe ",
"start": 17, "start": 17,
"end": 28 "end": 28,
"loc": {
"start": {
"line": 1,
"column": 17
},
"end": {
"line": 1,
"column": 28
}
}
} }
] ],
"name": "Object"
} }
} }
} }

@ -74,15 +74,12 @@ importers:
'@jridgewell/sourcemap-codec': '@jridgewell/sourcemap-codec':
specifier: ^1.6.0 specifier: ^1.6.0
version: 1.6.0 version: 1.6.0
'@sveltejs/acorn-typescript': '@teasel/parser':
specifier: ^1.0.13 specifier: link:../../../teasel/bindings/node
version: 1.0.13(acorn@8.18.0) version: link:../../../teasel/bindings/node
'@types/estree': '@types/estree':
specifier: ^1.0.9 specifier: ^1.0.9
version: 1.0.9 version: 1.0.9
acorn:
specifier: ^8.18.0
version: 8.18.0
aria-query: aria-query:
specifier: 5.3.1 specifier: 5.3.1
version: 5.3.1 version: 5.3.1
@ -141,6 +138,9 @@ importers:
'@types/trusted-types': '@types/trusted-types':
specifier: ^2.0.7 specifier: ^2.0.7
version: 2.0.7 version: 2.0.7
acorn:
specifier: ^8.18.0
version: 8.18.0
baseline-browser-mapping: baseline-browser-mapping:
specifier: ^2.10.32 specifier: ^2.10.32
version: 2.10.32 version: 2.10.32
@ -920,11 +920,6 @@ packages:
peerDependencies: peerDependencies:
eslint: '>=8.40.0' eslint: '>=8.40.0'
'@sveltejs/acorn-typescript@1.0.13':
resolution: {integrity: sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==}
peerDependencies:
acorn: ^8.9.0
'@sveltejs/eslint-config@9.0.0': '@sveltejs/eslint-config@9.0.0':
resolution: {integrity: sha512-3u9VUYlU0Mc/8Bhc7eRAPgJuUBMaKHzoE8r50WrnVujoxBLW4zfPckR/BFGGNeVUh0F7QGAtxGkV/5pL6CE1Ng==} resolution: {integrity: sha512-3u9VUYlU0Mc/8Bhc7eRAPgJuUBMaKHzoE8r50WrnVujoxBLW4zfPckR/BFGGNeVUh0F7QGAtxGkV/5pL6CE1Ng==}
peerDependencies: peerDependencies:
@ -3056,10 +3051,6 @@ snapshots:
eslint-visitor-keys: 3.4.3 eslint-visitor-keys: 3.4.3
espree: 9.6.1 espree: 9.6.1
'@sveltejs/acorn-typescript@1.0.13(acorn@8.18.0)':
dependencies:
acorn: 8.18.0
'@sveltejs/eslint-config@9.0.0(@eslint/js@10.0.1(eslint@10.0.0))(@stylistic/eslint-plugin-js@1.8.0(eslint@10.0.0))(eslint-config-prettier@9.1.0(eslint@10.0.0))(eslint-plugin-n@17.24.0(eslint@10.0.0)(typescript@5.5.4))(eslint-plugin-svelte@3.15.0(eslint@10.0.0)(svelte@packages+svelte))(eslint@10.0.0)(typescript-eslint@8.56.0(eslint@10.0.0)(typescript@5.5.4))(typescript@5.5.4)': '@sveltejs/eslint-config@9.0.0(@eslint/js@10.0.1(eslint@10.0.0))(@stylistic/eslint-plugin-js@1.8.0(eslint@10.0.0))(eslint-config-prettier@9.1.0(eslint@10.0.0))(eslint-plugin-n@17.24.0(eslint@10.0.0)(typescript@5.5.4))(eslint-plugin-svelte@3.15.0(eslint@10.0.0)(svelte@packages+svelte))(eslint@10.0.0)(typescript-eslint@8.56.0(eslint@10.0.0)(typescript@5.5.4))(typescript@5.5.4)':
dependencies: dependencies:
'@eslint/js': 10.0.1(eslint@10.0.0) '@eslint/js': 10.0.1(eslint@10.0.0)

Loading…
Cancel
Save