diff --git a/lib/parsers/gjs-gts-parser.js b/lib/parsers/gjs-gts-parser.js
index fdf0d0747d..18f61a283e 100644
--- a/lib/parsers/gjs-gts-parser.js
+++ b/lib/parsers/gjs-gts-parser.js
@@ -1,522 +1,14 @@
-const ContentTag = require('content-tag');
-const glimmer = require('@glimmer/syntax');
-const DocumentLines = require('../utils/document');
-const { visitorKeys: glimmerVisitorKeys } = require('@glimmer/syntax');
const babelParser = require('@babel/eslint-parser');
-const typescriptParser = require('@typescript-eslint/parser');
-const TypescriptScope = require('@typescript-eslint/scope-manager');
-const { Reference, Scope, Variable, Definition } = require('eslint-scope');
const { registerParsedFile } = require('../preprocessors/noop');
-const htmlTags = require('html-tags');
+const {
+ patchTs,
+ replaceExtensions,
+ syncMtsGtsSourceFiles,
+ typescriptParser,
+} = require('./ts-utils');
+const { transformForLint, preprocessGlimmerTemplates, convertAst } = require('./transform');
-/**
- * finds the nearest node scope
- * @param scopeManager
- * @param nodePath
- * @return {*|null}
- */
-function findParentScope(scopeManager, nodePath) {
- let scope = null;
- let path = nodePath;
- while (path) {
- scope = scopeManager.acquire(path.node, true);
- if (scope) {
- return scope;
- }
- path = path.parentPath;
- }
- return null;
-}
-
-/**
- * tries to find the variable names {name} in any parent scope
- * if the variable is not found it just returns the nearest scope,
- * so that it's usage can be registered.
- * @param scopeManager
- * @param nodePath
- * @param name
- * @return {{scope: null, variable: *}|{scope: (*|null)}}
- */
-function findVarInParentScopes(scopeManager, nodePath, name) {
- let scope = null;
- let path = nodePath;
- while (path) {
- scope = scopeManager.acquire(path.node, true);
- if (scope && scope.set.has(name)) {
- break;
- }
- path = path.parentPath;
- }
- const currentScope = findParentScope(scopeManager, nodePath);
- if (!scope) {
- return { scope: currentScope };
- }
- return { scope: currentScope, variable: scope.set.get(name) };
-}
-
-/**
- * registers a node variable usage in the scope.
- * @param node
- * @param scope
- * @param variable
- */
-function registerNodeInScope(node, scope, variable) {
- const ref = new Reference(node, scope, Reference.READ);
- if (variable) {
- variable.references.push(ref);
- ref.resolved = variable;
- } else {
- // register missing variable in most upper scope.
- let s = scope;
- while (s.upper) {
- s = s.upper;
- }
- s.through.push(ref);
- }
- scope.references.push(ref);
-}
-
-/**
- * traverses all nodes using the {visitorKeys} calling the callback function, visitor
- * @param visitorKeys
- * @param node
- * @param visitor
- */
-function traverse(visitorKeys, node, visitor) {
- const allVisitorKeys = visitorKeys;
- const queue = [];
-
- queue.push({
- node,
- parent: null,
- parentKey: null,
- parentPath: null,
- });
-
- while (queue.length > 0) {
- const currentPath = queue.pop();
-
- visitor(currentPath);
-
- const visitorKeys = allVisitorKeys[currentPath.node.type];
- if (!visitorKeys) {
- continue;
- }
-
- for (const visitorKey of visitorKeys) {
- const child = currentPath.node[visitorKey];
-
- if (!child) {
- continue;
- } else if (Array.isArray(child)) {
- for (const item of child) {
- queue.push({
- node: item,
- parent: currentPath.node,
- parentKey: visitorKey,
- parentPath: currentPath,
- });
- }
- } else {
- queue.push({
- node: child,
- parent: currentPath.node,
- parentKey: visitorKey,
- parentPath: currentPath,
- });
- }
- }
- }
-}
-
-function isUpperCase(char) {
- return char.toUpperCase() === char;
-}
-
-function isAlphaNumeric(code) {
- return !(
- !(code > 47 && code < 58) && // numeric (0-9)
- !(code > 64 && code < 91) && // upper alpha (A-Z)
- !(code > 96 && code < 123)
- );
-}
-
-function isWhiteSpace(code) {
- return code === ' ' || code === '\t' || code === '\r' || code === '\n' || code === '\v';
-}
-
-/**
- * simple tokenizer for templates, just splits it up into words and punctuators
- * @param template {string}
- * @param startOffset {number}
- * @param doc {DocumentLines}
- * @return {Token[]}
- */
-function tokenize(template, doc, startOffset) {
- const tokens = [];
- let current = '';
- let start = 0;
- function pushToken(value, type, range) {
- const t = {
- type,
- value,
- range,
- start: range[0],
- end: range[1],
- loc: {
- start: { ...doc.offsetToPosition(range[0]), index: range[0] },
- end: { ...doc.offsetToPosition(range[1]), index: range[1] },
- },
- };
- tokens.push(t);
- }
- for (const [i, c] of [...template].entries()) {
- if (isAlphaNumeric(c.codePointAt(0))) {
- if (current.length === 0) {
- start = i;
- }
- current += c;
- } else {
- let range = [startOffset + start, startOffset + i];
- if (current.length > 0) {
- pushToken(current, 'word', range);
- current = '';
- }
- range = [startOffset + i, startOffset + i + 1];
- if (!isWhiteSpace(c)) {
- pushToken(c, 'Punctuator', range);
- }
- }
- }
- return tokens;
-}
-
-/**
- * Preprocesses the template info, parsing the template content to Glimmer AST,
- * fixing the offsets and locations of all nodes
- * also calculates the block params locations & ranges
- * and adding it to the info
- * @param info
- * @param code
- * @return {{templateVisitorKeys: {}, comments: *[], templateInfos: {templateRange: *, range: *, replacedRange: *}[]}}
- */
-function preprocessGlimmerTemplates(info, code) {
- const templateInfos = info.templateInfos.map((r) => ({
- range: [r.contentRange.start, r.contentRange.end],
- templateRange: [r.range.start, r.range.end],
- }));
- const templateVisitorKeys = {};
- const codeLines = new DocumentLines(code);
- const comments = [];
- const textNodes = [];
- for (const tpl of templateInfos) {
- const range = tpl.range;
- const template = code.slice(...range);
- const docLines = new DocumentLines(template);
- const ast = glimmer.preprocess(template, { mode: 'codemod' });
- ast.tokens = tokenize(code.slice(...tpl.templateRange), codeLines, tpl.templateRange[0]);
- const allNodes = [];
- glimmer.traverse(ast, {
- All(node, path) {
- const n = node;
- n.parent = path.parentNode;
- allNodes.push(node);
- if (node.type === 'CommentStatement' || node.type === 'MustacheCommentStatement') {
- comments.push(node);
- }
- if (node.type === 'TextNode') {
- n.value = node.chars;
- textNodes.push(node);
- }
- },
- });
- ast.content = template;
- const allNodeTypes = new Set();
- for (const n of allNodes) {
- if (n.type === 'PathExpression') {
- n.head.range = [
- range[0] + docLines.positionToOffset(n.head.loc.start),
- range[0] + docLines.positionToOffset(n.head.loc.end),
- ];
- n.head.loc = {
- start: codeLines.offsetToPosition(n.head.range[0]),
- end: codeLines.offsetToPosition(n.head.range[1]),
- };
- }
- n.range =
- n.type === 'Template'
- ? [tpl.templateRange[0], tpl.templateRange[1]]
- : [
- range[0] + docLines.positionToOffset(n.loc.start),
- range[0] + docLines.positionToOffset(n.loc.end),
- ];
-
- n.start = n.range[0];
- n.end = n.range[1];
- n.loc = {
- start: codeLines.offsetToPosition(n.range[0]),
- end: codeLines.offsetToPosition(n.range[1]),
- };
- if (n.type === 'Template') {
- n.loc.start = codeLines.offsetToPosition(tpl.templateRange[0]);
- n.loc.end = codeLines.offsetToPosition(tpl.templateRange[1]);
- }
- // split up element node into sub nodes to be able to reference tag name
- // parts -> nodes for `Foo` and `Bar`
- if (n.type === 'ElementNode') {
- n.name = n.tag;
- n.parts = [];
- let start = n.range[0];
- let codeSlice = code.slice(...n.range);
- for (const part of n.tag.split('.')) {
- const regex = new RegExp(`\\b${part}\\b`);
- const match = codeSlice.match(regex);
- const range = [start + match.index, 0];
- range[1] = range[0] + part.length;
- codeSlice = code.slice(range[1], n.range[1]);
- start = range[1];
- n.parts.push({
- type: 'GlimmerElementNodePart',
- name: part,
- range,
- parent: n,
- loc: {
- start: codeLines.offsetToPosition(range[0]),
- end: codeLines.offsetToPosition(range[1]),
- },
- });
- }
- }
- // block params do not have location information
- // add our own nodes so we can reference them
- if ('blockParams' in n) {
- n.params = [];
- }
- if ('blockParams' in n && n.parent) {
- let part = code.slice(...n.parent.range);
- let start = n.parent.range[0];
- let idx = part.indexOf('|') + 1;
- start += idx;
- part = part.slice(idx, -1);
- idx = part.indexOf('|');
- part = part.slice(0, idx);
- for (const param of n.blockParams) {
- const regex = new RegExp(`\\b${param}\\b`);
- const match = part.match(regex);
- const range = [start + match.index, 0];
- range[1] = range[0] + param.length;
- n.params.push({
- type: 'BlockParam',
- name: param,
- range,
- parent: n,
- loc: {
- start: codeLines.offsetToPosition(range[0]),
- end: codeLines.offsetToPosition(range[1]),
- },
- });
- }
- }
- n.type = `Glimmer${n.type}`;
- allNodeTypes.add(n.type);
- }
- // ast should not contain comment nodes
- for (const comment of comments) {
- const parentBody = comment.parent.body || comment.parent.children;
- const idx = parentBody.indexOf(comment);
- parentBody.splice(idx, 1);
- // comment type can be a block comment or a line comment
- // mark comments as always block comment, this works for eslint in all cases
- comment.type = 'Block';
- }
- // tokens should not contain tokens of comments
- ast.tokens = ast.tokens.filter(
- (t) => !comments.some((c) => c.range[0] <= t.range[0] && c.range[1] >= t.range[1])
- );
- // tokens should not contain tokens of text nodes, but represent the whole node
- // remove existing tokens
- ast.tokens = ast.tokens.filter(
- (t) => !textNodes.some((c) => c.range[0] <= t.range[0] && c.range[1] >= t.range[1])
- );
- // merge in text nodes
- let currentTextNode = textNodes.pop();
- for (let i = ast.tokens.length - 1; i >= 0; i--) {
- const t = ast.tokens[i];
- while (currentTextNode && t.range[0] < currentTextNode.range[0]) {
- ast.tokens.splice(i + 1, 0, currentTextNode);
- currentTextNode = textNodes.pop();
- }
- }
- ast.contents = template;
- tpl.ast = ast;
- }
- for (const [k, v] of Object.entries(glimmerVisitorKeys)) {
- templateVisitorKeys[`Glimmer${k}`] = [...v];
- }
- return {
- templateVisitorKeys,
- templateInfos,
- comments,
- };
-}
-
-/**
- * traverses the AST and replaces the transformed template parts with the Glimmer
- * AST.
- * This also creates the scopes for the Glimmer Blocks and registers the block params
- * in the scope, and also any usages of variables in path expressions
- * this allows the basic eslint rules no-undef and no-unsused to work also for the
- * templates without needing any custom rules
- * @param result
- * @param preprocessedResult
- * @param visitorKeys
- */
-function convertAst(result, preprocessedResult, visitorKeys) {
- const templateInfos = preprocessedResult.templateInfos;
- let counter = 0;
- result.ast.comments.push(...preprocessedResult.comments);
-
- for (const ti of templateInfos) {
- const firstIdx = result.ast.tokens.findIndex((t) => t.range[0] === ti.templateRange[0]);
- const lastIdx = result.ast.tokens.findIndex((t) => t.range[1] === ti.templateRange[1]);
- result.ast.tokens.splice(firstIdx, lastIdx - firstIdx + 1, ...ti.ast.tokens);
- }
-
- // eslint-disable-next-line complexity
- traverse(visitorKeys, result.ast, (path) => {
- const node = path.node;
- if (
- node.type === 'ExpressionStatement' ||
- node.type === 'StaticBlock' ||
- node.type === 'TaggedTemplateExpression' ||
- node.type === 'ExportDefaultDeclaration'
- ) {
- let range = node.range;
- if (node.type === 'ExportDefaultDeclaration') {
- range = [node.declaration.range[0], node.declaration.range[1]];
- }
-
- const template = templateInfos.find(
- (t) =>
- t.templateRange[0] === range[0] &&
- (t.templateRange[1] === range[1] || t.templateRange[1] === range[1] + 1)
- );
- if (!template) {
- return null;
- }
- counter++;
- const ast = template.ast;
- Object.assign(node, ast);
- }
-
- if (node.type === 'GlimmerPathExpression' && node.head.type === 'VarHead') {
- const name = node.head.name;
- if (glimmer.isKeyword(name)) {
- return null;
- }
- const { scope, variable } = findVarInParentScopes(result.scopeManager, path, name) || {};
- if (scope) {
- node.head.parent = node;
- registerNodeInScope(node.head, scope, variable);
- }
- }
- if (node.type === 'GlimmerElementNode') {
- // always reference first part of tag name, this also has the advantage
- // that errors regarding this tag will only mark the tag name instead of
- // the whole tag + children
- const n = node.parts[0];
- const { scope, variable } = findVarInParentScopes(result.scopeManager, path, n.name) || {};
- if (
- scope &&
- (variable ||
- isUpperCase(n.name[0]) ||
- node.name.includes('.') ||
- !htmlTags.includes(node.name))
- ) {
- registerNodeInScope(n, scope, variable);
- }
- }
-
- if ('blockParams' in node) {
- const upperScope = findParentScope(result.scopeManager, path);
- const scope = result.isTypescript
- ? new TypescriptScope.BlockScope(result.scopeManager, upperScope, node)
- : new Scope(result.scopeManager, 'block', upperScope, node);
- for (const [i, b] of node.params.entries()) {
- const v = new Variable(b.name, scope);
- v.identifiers.push(b);
- v.defs.push(new Definition('Parameter', b, node, node, i, 'Block Param'));
- scope.variables.push(v);
- scope.set.set(b.name, v);
- }
- }
- return null;
- });
-
- if (counter !== templateInfos.length) {
- throw new Error('failed to process all templates');
- }
-}
-
-function replaceRange(s, start, end, substitute) {
- return s.slice(0, start) + substitute + s.slice(end);
-}
-
-function transformForLint(code) {
- let jsCode = code;
- const processor = new ContentTag.Preprocessor();
- /**
- *
- * @type {{
- * type: 'expression' | 'class-member';
- * tagName: 'template';
- * contents: string;
- * range: {
- * start: number;
- * end: number;
- * };
- * contentRange: {
- * start: number;
- * end: number;
- * };
- * startRange: {
- * end: number;
- * start: number;
- * };
- * endRange: {
- * start: number;
- * end: number;
- * };
- * }[]}
- */
- const result = processor.parse(code);
- for (const tplInfo of result.reverse()) {
- const lineBreaks = [...tplInfo.contents].reduce(
- (prev, curr) => prev + (DocumentLines.isLineBreak(curr.codePointAt(0)) ? 1 : 0),
- 0
- );
- if (tplInfo.type === 'class-member') {
- const tplLength = tplInfo.range.end - tplInfo.range.start;
- const spaces = tplLength - 'static{`'.length - '`}'.length - lineBreaks;
- const total = ' '.repeat(spaces) + '\n'.repeat(lineBreaks);
- const replacementCode = `static{\`${total}\`}`;
- jsCode = replaceRange(jsCode, tplInfo.range.start, tplInfo.range.end, replacementCode);
- } else {
- const tplLength = tplInfo.range.end - tplInfo.range.start;
- const spaces = tplLength - '""`'.length - '`'.length - lineBreaks;
- const total = ' '.repeat(spaces) + '\n'.repeat(lineBreaks);
- const replacementCode = `""\`${total}\``;
- jsCode = replaceRange(jsCode, tplInfo.range.start, tplInfo.range.end, replacementCode);
- }
- }
- if (jsCode.length !== code.length) {
- throw new Error('bad transform');
- }
- return {
- templateInfos: result,
- output: jsCode,
- };
-}
+patchTs();
/**
* implements https://eslint.org/docs/latest/extend/custom-parsers
@@ -539,8 +31,17 @@ module.exports = {
const isTypescript = options.filePath.endsWith('.gts');
let result = null;
+ const filePath = options.filePath;
+ if (options.project) {
+ jsCode = replaceExtensions(jsCode);
+ }
+
+ if (isTypescript && !typescriptParser) {
+ throw new Error('Please install typescript to process gts');
+ }
+
result = isTypescript
- ? typescriptParser.parseForESLint(jsCode, { ...options, ranges: true })
+ ? typescriptParser.parseForESLint(jsCode, { ...options, ranges: true, filePath })
: babelParser.parseForESLint(jsCode, { ...options, ranges: true });
if (!info.templateInfos?.length) {
return result;
@@ -550,6 +51,9 @@ module.exports = {
const visitorKeys = { ...result.visitorKeys, ...templateVisitorKeys };
result.isTypescript = isTypescript;
convertAst(result, preprocessedResult, visitorKeys);
+ if (result.services?.program) {
+ syncMtsGtsSourceFiles(result.services?.program);
+ }
return { ...result, visitorKeys };
},
};
diff --git a/lib/parsers/transform.js b/lib/parsers/transform.js
new file mode 100644
index 0000000000..61b62aebed
--- /dev/null
+++ b/lib/parsers/transform.js
@@ -0,0 +1,519 @@
+const ContentTag = require('content-tag');
+const glimmer = require('@glimmer/syntax');
+const DocumentLines = require('../utils/document');
+const { visitorKeys: glimmerVisitorKeys } = require('@glimmer/syntax');
+const TypescriptScope = require('@typescript-eslint/scope-manager');
+const { Reference, Scope, Variable, Definition } = require('eslint-scope');
+const htmlTags = require('html-tags');
+
+/**
+ * finds the nearest node scope
+ * @param scopeManager
+ * @param nodePath
+ * @return {*|null}
+ */
+function findParentScope(scopeManager, nodePath) {
+ let scope = null;
+ let path = nodePath;
+ while (path) {
+ scope = scopeManager.acquire(path.node, true);
+ if (scope) {
+ return scope;
+ }
+ path = path.parentPath;
+ }
+ return null;
+}
+
+/**
+ * tries to find the variable names {name} in any parent scope
+ * if the variable is not found it just returns the nearest scope,
+ * so that it's usage can be registered.
+ * @param scopeManager
+ * @param nodePath
+ * @param name
+ * @return {{scope: null, variable: *}|{scope: (*|null)}}
+ */
+function findVarInParentScopes(scopeManager, nodePath, name) {
+ let scope = null;
+ let path = nodePath;
+ while (path) {
+ scope = scopeManager.acquire(path.node, true);
+ if (scope && scope.set.has(name)) {
+ break;
+ }
+ path = path.parentPath;
+ }
+ const currentScope = findParentScope(scopeManager, nodePath);
+ if (!scope) {
+ return { scope: currentScope };
+ }
+ return { scope: currentScope, variable: scope.set.get(name) };
+}
+
+/**
+ * registers a node variable usage in the scope.
+ * @param node
+ * @param scope
+ * @param variable
+ */
+function registerNodeInScope(node, scope, variable) {
+ const ref = new Reference(node, scope, Reference.READ);
+ if (variable) {
+ variable.references.push(ref);
+ ref.resolved = variable;
+ } else {
+ // register missing variable in most upper scope.
+ let s = scope;
+ while (s.upper) {
+ s = s.upper;
+ }
+ s.through.push(ref);
+ }
+ scope.references.push(ref);
+}
+
+/**
+ * traverses all nodes using the {visitorKeys} calling the callback function, visitor
+ * @param visitorKeys
+ * @param node
+ * @param visitor
+ */
+function traverse(visitorKeys, node, visitor) {
+ const allVisitorKeys = visitorKeys;
+ const queue = [];
+
+ queue.push({
+ node,
+ parent: null,
+ parentKey: null,
+ parentPath: null,
+ });
+
+ while (queue.length > 0) {
+ const currentPath = queue.pop();
+
+ visitor(currentPath);
+
+ const visitorKeys = allVisitorKeys[currentPath.node.type];
+ if (!visitorKeys) {
+ continue;
+ }
+
+ for (const visitorKey of visitorKeys) {
+ const child = currentPath.node[visitorKey];
+
+ if (!child) {
+ continue;
+ } else if (Array.isArray(child)) {
+ for (const item of child) {
+ queue.push({
+ node: item,
+ parent: currentPath.node,
+ parentKey: visitorKey,
+ parentPath: currentPath,
+ });
+ }
+ } else {
+ queue.push({
+ node: child,
+ parent: currentPath.node,
+ parentKey: visitorKey,
+ parentPath: currentPath,
+ });
+ }
+ }
+ }
+}
+
+function isUpperCase(char) {
+ return char.toUpperCase() === char;
+}
+
+function isAlphaNumeric(code) {
+ return !(
+ !(code > 47 && code < 58) && // numeric (0-9)
+ !(code > 64 && code < 91) && // upper alpha (A-Z)
+ !(code > 96 && code < 123)
+ );
+}
+
+function isWhiteSpace(code) {
+ return code === ' ' || code === '\t' || code === '\r' || code === '\n' || code === '\v';
+}
+
+/**
+ * simple tokenizer for templates, just splits it up into words and punctuators
+ * @param template {string}
+ * @param startOffset {number}
+ * @param doc {DocumentLines}
+ * @return {Token[]}
+ */
+function tokenize(template, doc, startOffset) {
+ const tokens = [];
+ let current = '';
+ let start = 0;
+ function pushToken(value, type, range) {
+ const t = {
+ type,
+ value,
+ range,
+ start: range[0],
+ end: range[1],
+ loc: {
+ start: { ...doc.offsetToPosition(range[0]), index: range[0] },
+ end: { ...doc.offsetToPosition(range[1]), index: range[1] },
+ },
+ };
+ tokens.push(t);
+ }
+ for (const [i, c] of [...template].entries()) {
+ if (isAlphaNumeric(c.codePointAt(0))) {
+ if (current.length === 0) {
+ start = i;
+ }
+ current += c;
+ } else {
+ let range = [startOffset + start, startOffset + i];
+ if (current.length > 0) {
+ pushToken(current, 'word', range);
+ current = '';
+ }
+ range = [startOffset + i, startOffset + i + 1];
+ if (!isWhiteSpace(c)) {
+ pushToken(c, 'Punctuator', range);
+ }
+ }
+ }
+ return tokens;
+}
+
+/**
+ * Preprocesses the template info, parsing the template content to Glimmer AST,
+ * fixing the offsets and locations of all nodes
+ * also calculates the block params locations & ranges
+ * and adding it to the info
+ * @param info
+ * @param code
+ * @return {{templateVisitorKeys: {}, comments: *[], templateInfos: {templateRange: *, range: *, replacedRange: *}[]}}
+ */
+module.exports.preprocessGlimmerTemplates = function preprocessGlimmerTemplates(info, code) {
+ const templateInfos = info.templateInfos.map((r) => ({
+ range: [r.contentRange.start, r.contentRange.end],
+ templateRange: [r.range.start, r.range.end],
+ }));
+ const templateVisitorKeys = {};
+ const codeLines = new DocumentLines(code);
+ const comments = [];
+ const textNodes = [];
+ for (const tpl of templateInfos) {
+ const range = tpl.range;
+ const template = code.slice(...range);
+ const docLines = new DocumentLines(template);
+ const ast = glimmer.preprocess(template, { mode: 'codemod' });
+ ast.tokens = tokenize(code.slice(...tpl.templateRange), codeLines, tpl.templateRange[0]);
+ const allNodes = [];
+ glimmer.traverse(ast, {
+ All(node, path) {
+ const n = node;
+ n.parent = path.parentNode;
+ allNodes.push(node);
+ if (node.type === 'CommentStatement' || node.type === 'MustacheCommentStatement') {
+ comments.push(node);
+ }
+ if (node.type === 'TextNode') {
+ n.value = node.chars;
+ textNodes.push(node);
+ }
+ },
+ });
+ ast.content = template;
+ const allNodeTypes = new Set();
+ for (const n of allNodes) {
+ if (n.type === 'PathExpression') {
+ n.head.range = [
+ range[0] + docLines.positionToOffset(n.head.loc.start),
+ range[0] + docLines.positionToOffset(n.head.loc.end),
+ ];
+ n.head.loc = {
+ start: codeLines.offsetToPosition(n.head.range[0]),
+ end: codeLines.offsetToPosition(n.head.range[1]),
+ };
+ }
+ n.range =
+ n.type === 'Template'
+ ? [tpl.templateRange[0], tpl.templateRange[1]]
+ : [
+ range[0] + docLines.positionToOffset(n.loc.start),
+ range[0] + docLines.positionToOffset(n.loc.end),
+ ];
+
+ n.start = n.range[0];
+ n.end = n.range[1];
+ n.loc = {
+ start: codeLines.offsetToPosition(n.range[0]),
+ end: codeLines.offsetToPosition(n.range[1]),
+ };
+ if (n.type === 'Template') {
+ n.loc.start = codeLines.offsetToPosition(tpl.templateRange[0]);
+ n.loc.end = codeLines.offsetToPosition(tpl.templateRange[1]);
+ }
+ // split up element node into sub nodes to be able to reference tag name
+ // parts -> nodes for `Foo` and `Bar`
+ if (n.type === 'ElementNode') {
+ n.name = n.tag;
+ n.parts = [];
+ let start = n.range[0];
+ let codeSlice = code.slice(...n.range);
+ for (const part of n.tag.split('.')) {
+ const regex = new RegExp(`\\b${part}\\b`);
+ const match = codeSlice.match(regex);
+ const range = [start + match.index, 0];
+ range[1] = range[0] + part.length;
+ codeSlice = code.slice(range[1], n.range[1]);
+ start = range[1];
+ n.parts.push({
+ type: 'GlimmerElementNodePart',
+ name: part,
+ range,
+ parent: n,
+ loc: {
+ start: codeLines.offsetToPosition(range[0]),
+ end: codeLines.offsetToPosition(range[1]),
+ },
+ });
+ }
+ }
+ // block params do not have location information
+ // add our own nodes so we can reference them
+ if ('blockParams' in n) {
+ n.params = [];
+ }
+ if ('blockParams' in n && n.parent) {
+ let part = code.slice(...n.parent.range);
+ let start = n.parent.range[0];
+ let idx = part.indexOf('|') + 1;
+ start += idx;
+ part = part.slice(idx, -1);
+ idx = part.indexOf('|');
+ part = part.slice(0, idx);
+ for (const param of n.blockParams) {
+ const regex = new RegExp(`\\b${param}\\b`);
+ const match = part.match(regex);
+ const range = [start + match.index, 0];
+ range[1] = range[0] + param.length;
+ n.params.push({
+ type: 'BlockParam',
+ name: param,
+ range,
+ parent: n,
+ loc: {
+ start: codeLines.offsetToPosition(range[0]),
+ end: codeLines.offsetToPosition(range[1]),
+ },
+ });
+ }
+ }
+ n.type = `Glimmer${n.type}`;
+ allNodeTypes.add(n.type);
+ }
+ // ast should not contain comment nodes
+ for (const comment of comments) {
+ const parentBody = comment.parent.body || comment.parent.children;
+ const idx = parentBody.indexOf(comment);
+ parentBody.splice(idx, 1);
+ // comment type can be a block comment or a line comment
+ // mark comments as always block comment, this works for eslint in all cases
+ comment.type = 'Block';
+ }
+ // tokens should not contain tokens of comments
+ ast.tokens = ast.tokens.filter(
+ (t) => !comments.some((c) => c.range[0] <= t.range[0] && c.range[1] >= t.range[1])
+ );
+ // tokens should not contain tokens of text nodes, but represent the whole node
+ // remove existing tokens
+ ast.tokens = ast.tokens.filter(
+ (t) => !textNodes.some((c) => c.range[0] <= t.range[0] && c.range[1] >= t.range[1])
+ );
+ // merge in text nodes
+ let currentTextNode = textNodes.pop();
+ for (let i = ast.tokens.length - 1; i >= 0; i--) {
+ const t = ast.tokens[i];
+ while (currentTextNode && t.range[0] < currentTextNode.range[0]) {
+ ast.tokens.splice(i + 1, 0, currentTextNode);
+ currentTextNode = textNodes.pop();
+ }
+ }
+ ast.contents = template;
+ tpl.ast = ast;
+ }
+ for (const [k, v] of Object.entries(glimmerVisitorKeys)) {
+ templateVisitorKeys[`Glimmer${k}`] = [...v];
+ }
+ return {
+ templateVisitorKeys,
+ templateInfos,
+ comments,
+ };
+};
+
+/**
+ * traverses the AST and replaces the transformed template parts with the Glimmer
+ * AST.
+ * This also creates the scopes for the Glimmer Blocks and registers the block params
+ * in the scope, and also any usages of variables in path expressions
+ * this allows the basic eslint rules no-undef and no-unsused to work also for the
+ * templates without needing any custom rules
+ * @param result
+ * @param preprocessedResult
+ * @param visitorKeys
+ */
+module.exports.convertAst = function convertAst(result, preprocessedResult, visitorKeys) {
+ const templateInfos = preprocessedResult.templateInfos;
+ let counter = 0;
+ result.ast.comments.push(...preprocessedResult.comments);
+
+ for (const ti of templateInfos) {
+ const firstIdx = result.ast.tokens.findIndex((t) => t.range[0] === ti.templateRange[0]);
+ const lastIdx = result.ast.tokens.findIndex((t) => t.range[1] === ti.templateRange[1]);
+ result.ast.tokens.splice(firstIdx, lastIdx - firstIdx + 1, ...ti.ast.tokens);
+ }
+
+ // eslint-disable-next-line complexity
+ traverse(visitorKeys, result.ast, (path) => {
+ const node = path.node;
+ if (
+ node.type === 'ExpressionStatement' ||
+ node.type === 'StaticBlock' ||
+ node.type === 'TaggedTemplateExpression' ||
+ node.type === 'ExportDefaultDeclaration'
+ ) {
+ let range = node.range;
+ if (node.type === 'ExportDefaultDeclaration') {
+ range = [node.declaration.range[0], node.declaration.range[1]];
+ }
+
+ const template = templateInfos.find(
+ (t) =>
+ t.templateRange[0] === range[0] &&
+ (t.templateRange[1] === range[1] || t.templateRange[1] === range[1] + 1)
+ );
+ if (!template) {
+ return null;
+ }
+ counter++;
+ const ast = template.ast;
+ Object.assign(node, ast);
+ }
+
+ if (node.type === 'GlimmerPathExpression' && node.head.type === 'VarHead') {
+ const name = node.head.name;
+ if (glimmer.isKeyword(name)) {
+ return null;
+ }
+ const { scope, variable } = findVarInParentScopes(result.scopeManager, path, name) || {};
+ if (scope) {
+ node.head.parent = node;
+ registerNodeInScope(node.head, scope, variable);
+ }
+ }
+ if (node.type === 'GlimmerElementNode') {
+ // always reference first part of tag name, this also has the advantage
+ // that errors regarding this tag will only mark the tag name instead of
+ // the whole tag + children
+ const n = node.parts[0];
+ const { scope, variable } = findVarInParentScopes(result.scopeManager, path, n.name) || {};
+ if (
+ scope &&
+ (variable ||
+ isUpperCase(n.name[0]) ||
+ node.name.includes('.') ||
+ !htmlTags.includes(node.name))
+ ) {
+ registerNodeInScope(n, scope, variable);
+ }
+ }
+
+ if ('blockParams' in node) {
+ const upperScope = findParentScope(result.scopeManager, path);
+ const scope = result.isTypescript
+ ? new TypescriptScope.BlockScope(result.scopeManager, upperScope, node)
+ : new Scope(result.scopeManager, 'block', upperScope, node);
+ for (const [i, b] of node.params.entries()) {
+ const v = new Variable(b.name, scope);
+ v.identifiers.push(b);
+ v.defs.push(new Definition('Parameter', b, node, node, i, 'Block Param'));
+ scope.variables.push(v);
+ scope.set.set(b.name, v);
+ }
+ }
+ return null;
+ });
+
+ if (counter !== templateInfos.length) {
+ throw new Error('failed to process all templates');
+ }
+};
+
+const replaceRange = function replaceRange(s, start, end, substitute) {
+ return s.slice(0, start) + substitute + s.slice(end);
+};
+module.exports.replaceRange = replaceRange;
+
+const processor = new ContentTag.Preprocessor();
+
+module.exports.transformForLint = function transformForLint(code) {
+ let jsCode = code;
+ /**
+ *
+ * @type {{
+ * type: 'expression' | 'class-member';
+ * tagName: 'template';
+ * contents: string;
+ * range: {
+ * start: number;
+ * end: number;
+ * };
+ * contentRange: {
+ * start: number;
+ * end: number;
+ * };
+ * startRange: {
+ * end: number;
+ * start: number;
+ * };
+ * endRange: {
+ * start: number;
+ * end: number;
+ * };
+ * }[]}
+ */
+ const result = processor.parse(code);
+ for (const tplInfo of result.reverse()) {
+ const lineBreaks = [...tplInfo.contents].reduce(
+ (prev, curr) => prev + (DocumentLines.isLineBreak(curr.codePointAt(0)) ? 1 : 0),
+ 0
+ );
+ if (tplInfo.type === 'class-member') {
+ const tplLength = tplInfo.range.end - tplInfo.range.start;
+ const spaces = tplLength - 'static{`'.length - '`}'.length - lineBreaks;
+ const total = ' '.repeat(spaces) + '\n'.repeat(lineBreaks);
+ const replacementCode = `static{\`${total}\`}`;
+ jsCode = replaceRange(jsCode, tplInfo.range.start, tplInfo.range.end, replacementCode);
+ } else {
+ const tplLength = tplInfo.range.end - tplInfo.range.start;
+ const spaces = tplLength - '""`'.length - '`'.length - lineBreaks;
+ const total = ' '.repeat(spaces) + '\n'.repeat(lineBreaks);
+ const replacementCode = `""\`${total}\``;
+ jsCode = replaceRange(jsCode, tplInfo.range.start, tplInfo.range.end, replacementCode);
+ }
+ }
+ /* istanbul ignore next */
+ if (jsCode.length !== code.length) {
+ throw new Error('bad transform');
+ }
+ return {
+ templateInfos: result,
+ output: jsCode,
+ };
+};
diff --git a/lib/parsers/ts-utils.js b/lib/parsers/ts-utils.js
new file mode 100644
index 0000000000..027c1fa4f1
--- /dev/null
+++ b/lib/parsers/ts-utils.js
@@ -0,0 +1,117 @@
+const fs = require('node:fs');
+const { transformForLint } = require('./transform');
+const babel = require('@babel/core');
+const { replaceRange } = require('./transform');
+
+let patchTs, replaceExtensions, syncMtsGtsSourceFiles, typescriptParser;
+
+try {
+ const ts = require('typescript');
+ typescriptParser = require('@typescript-eslint/parser');
+ patchTs = function patchTs() {
+ const sys = { ...ts.sys };
+ const newSys = {
+ ...ts.sys,
+ readDirectory(...args) {
+ const results = sys.readDirectory.call(this, ...args);
+ return [
+ ...results,
+ ...results.filter((x) => x.endsWith('.gts')).map((f) => f.replace(/\.gts$/, '.mts')),
+ ];
+ },
+ fileExists(fileName) {
+ return fs.existsSync(fileName.replace(/\.mts$/, '.gts')) || fs.existsSync(fileName);
+ },
+ readFile(fname) {
+ let fileName = fname;
+ let content = '';
+ try {
+ content = fs.readFileSync(fileName).toString();
+ } catch {
+ fileName = fileName.replace(/\.mts$/, '.gts');
+ content = fs.readFileSync(fileName).toString();
+ }
+ if (fileName.endsWith('.gts')) {
+ content = transformForLint(content).output;
+ }
+ if (
+ (!fileName.endsWith('.d.ts') && fileName.endsWith('.ts')) ||
+ fileName.endsWith('.gts')
+ ) {
+ content = replaceExtensions(content);
+ }
+ return content;
+ },
+ };
+ ts.setSys(newSys);
+ };
+
+ replaceExtensions = function replaceExtensions(code) {
+ let jsCode = code;
+ const babelParseResult = babel.parse(jsCode, {
+ parserOpts: { ranges: true, plugins: ['typescript'] },
+ });
+ const length = jsCode.length;
+ for (const b of babelParseResult.program.body) {
+ if (b.type === 'ImportDeclaration' && b.source.value.endsWith('.gts')) {
+ const value = b.source.value.replace(/\.gts$/, '.mts');
+ const strWrapper = jsCode[b.source.start];
+ jsCode = replaceRange(
+ jsCode,
+ b.source.start,
+ b.source.end,
+ strWrapper + value + strWrapper
+ );
+ }
+ }
+ if (length !== jsCode.length) {
+ throw new Error('bad replacement');
+ }
+ return jsCode;
+ };
+
+ /**
+ *
+ * @param program {ts.Program}
+ */
+ syncMtsGtsSourceFiles = function syncMtsGtsSourceFiles(program) {
+ const sourceFiles = program.getSourceFiles();
+ for (const sourceFile of sourceFiles) {
+ // check for deleted gts files, need to remove mts as well
+ if (sourceFile.path.endsWith('.mts') && sourceFile.isVirtualGts) {
+ const gtsFile = program.getSourceFile(sourceFile.path.replace(/\.mts$/, '.gts'));
+ if (!gtsFile) {
+ sourceFile.version = null;
+ }
+ }
+ if (sourceFile.path.endsWith('.gts')) {
+ /**
+ * @type {ts.SourceFile}
+ */
+ const mtsSourceFile = program.getSourceFile(sourceFile.path.replace(/\.gts$/, '.mts'));
+ if (mtsSourceFile) {
+ const keep = {
+ fileName: mtsSourceFile.fileName,
+ path: mtsSourceFile.path,
+ originalFileName: mtsSourceFile.originalFileName,
+ resolvedPath: mtsSourceFile.resolvedPath,
+ };
+ Object.assign(mtsSourceFile, sourceFile, keep);
+ mtsSourceFile.isVirtualGts = true;
+ }
+ }
+ }
+ };
+} catch /* istanbul ignore next */ {
+ // typescript not available
+ patchTs = () => null;
+ replaceExtensions = (code) => code;
+ syncMtsGtsSourceFiles = () => null;
+}
+
+module.exports = {
+ patchTs,
+ replaceExtensions,
+ syncMtsGtsSourceFiles,
+ typescriptParser,
+};
diff --git a/package.json b/package.json
index ba30193bf8..01f037bcdc 100644
--- a/package.json
+++ b/package.json
@@ -68,6 +68,7 @@
]
},
"dependencies": {
+ "@babel/core": "^7.23.3",
"@babel/eslint-parser": "^7.22.15",
"@ember-data/rfc395-data": "^0.0.4",
"@glimmer/syntax": "^0.85.12",
@@ -116,7 +117,13 @@
"typescript": "^5.2.2"
},
"peerDependencies": {
- "eslint": ">= 8"
+ "eslint": ">= 8",
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
},
"engines": {
"node": "18.* || 20.* || >= 21"
diff --git a/tests/lib/rules-preprocessor/ember_ts/bar.gts b/tests/lib/rules-preprocessor/ember_ts/bar.gts
new file mode 100644
index 0000000000..32a5baebb3
--- /dev/null
+++ b/tests/lib/rules-preprocessor/ember_ts/bar.gts
@@ -0,0 +1,5 @@
+export const fortyTwoFromGTS = '42';
+
+
+ {{fortyTwoFromGTS}}
+
diff --git a/tests/lib/rules-preprocessor/ember_ts/baz.ts b/tests/lib/rules-preprocessor/ember_ts/baz.ts
new file mode 100644
index 0000000000..84e700e1a6
--- /dev/null
+++ b/tests/lib/rules-preprocessor/ember_ts/baz.ts
@@ -0,0 +1 @@
+export const fortyTwoFromTS = '42';
diff --git a/tests/lib/rules-preprocessor/ember_ts/foo.gts b/tests/lib/rules-preprocessor/ember_ts/foo.gts
new file mode 100644
index 0000000000..35aaba7c6b
--- /dev/null
+++ b/tests/lib/rules-preprocessor/ember_ts/foo.gts
@@ -0,0 +1,14 @@
+import { fortyTwoFromGTS } from './bar.gts';
+import { fortyTwoFromTS } from './baz.ts';
+
+export const fortyTwoLocal = '42';
+
+const helloWorldFromTS = fortyTwoFromTS[0] === '4' ? 'hello' : 'world';
+const helloWorldFromGTS = fortyTwoFromGTS[0] === '4' ? 'hello' : 'world';
+const helloWorld = fortyTwoLocal[0] === '4' ? 'hello' : 'world';
+//
+
+ {{helloWorldFromGTS}}
+ {{helloWorldFromTS}}
+ {{helloWorld}}
+
diff --git a/tests/lib/rules-preprocessor/gjs-gts-parser-test.js b/tests/lib/rules-preprocessor/gjs-gts-parser-test.js
index fddf56b539..a98e296bd7 100644
--- a/tests/lib/rules-preprocessor/gjs-gts-parser-test.js
+++ b/tests/lib/rules-preprocessor/gjs-gts-parser-test.js
@@ -9,6 +9,8 @@
const { ESLint } = require('eslint');
const plugin = require('../../../lib');
+const { writeFileSync, readFileSync } = require('node:fs');
+const { join } = require('node:path');
const gjsGtsParser = require.resolve('../../../lib/parsers/gjs-gts-parser');
@@ -388,18 +390,28 @@ const invalid = [
code: `
import Component from '@glimmer/component';
+ const foo: any = '';
+
export default class MyComponent extends Component {
foo = 'bar';
${' '}
+ {{foo}}
}`,
errors: [
+ {
+ message: 'Unexpected any. Specify a different type.',
+ line: 4,
+ endLine: 4,
+ column: 18,
+ endColumn: 21,
+ },
{
message: 'Trailing spaces not allowed.',
- line: 8,
- endLine: 8,
+ line: 10,
+ endLine: 10,
column: 22,
endColumn: 24,
},
@@ -765,4 +777,113 @@ describe('multiple tokens in same file', () => {
expect(resultErrors[2].message).toBe("'bar' is not defined.");
expect(resultErrors[2].line).toBe(17);
});
+
+ it('lints while being type aware', async () => {
+ const eslint = new ESLint({
+ ignore: false,
+ useEslintrc: false,
+ plugins: { ember: plugin },
+ overrideConfig: {
+ root: true,
+ env: {
+ browser: true,
+ },
+ plugins: ['ember'],
+ extends: ['plugin:ember/recommended'],
+ overrides: [
+ {
+ files: ['**/*.gts'],
+ parser: 'eslint-plugin-ember/gjs-gts-parser',
+ parserOptions: {
+ project: './tsconfig.eslint.json',
+ tsconfigRootDir: __dirname,
+ extraFileExtensions: ['.gts'],
+ },
+ extends: [
+ 'plugin:@typescript-eslint/recommended-requiring-type-checking',
+ 'plugin:ember/recommended',
+ ],
+ rules: {
+ 'no-trailing-spaces': 'error',
+ '@typescript-eslint/prefer-string-starts-ends-with': 'error',
+ },
+ },
+ {
+ files: ['**/*.ts'],
+ parser: '@typescript-eslint/parser',
+ parserOptions: {
+ project: './tsconfig.eslint.json',
+ tsconfigRootDir: __dirname,
+ extraFileExtensions: ['.gts'],
+ },
+ extends: [
+ 'plugin:@typescript-eslint/recommended-requiring-type-checking',
+ 'plugin:ember/recommended',
+ ],
+ rules: {
+ 'no-trailing-spaces': 'error',
+ },
+ },
+ ],
+ rules: {
+ quotes: ['error', 'single'],
+ semi: ['error', 'always'],
+ 'object-curly-spacing': ['error', 'always'],
+ 'lines-between-class-members': 'error',
+ 'no-undef': 'error',
+ 'no-unused-vars': 'error',
+ 'ember/no-get': 'off',
+ 'ember/no-array-prototype-extensions': 'error',
+ 'ember/no-unused-services': 'error',
+ },
+ },
+ });
+
+ let results = await eslint.lintFiles(['**/*.gts', '**/*.ts']);
+
+ let resultErrors = results.flatMap((result) => result.messages);
+ expect(resultErrors).toHaveLength(3);
+
+ expect(resultErrors[0].message).toBe("Use 'String#startsWith' method instead.");
+ expect(resultErrors[0].line).toBe(6);
+
+ expect(resultErrors[1].line).toBe(7);
+ expect(resultErrors[1].message).toBe("Use 'String#startsWith' method instead.");
+
+ expect(resultErrors[2].line).toBe(8);
+ expect(resultErrors[2].message).toBe("Use 'String#startsWith' method instead.");
+
+ const filePath = join(__dirname, 'ember_ts', 'bar.gts');
+ const content = readFileSync(filePath).toString();
+ try {
+ writeFileSync(filePath, content.replace("'42'", '42'));
+
+ results = await eslint.lintFiles(['**/*.gts', '**/*.ts']);
+
+ resultErrors = results.flatMap((result) => result.messages);
+ expect(resultErrors).toHaveLength(2);
+
+ expect(resultErrors[0].message).toBe("Use 'String#startsWith' method instead.");
+ expect(resultErrors[0].line).toBe(6);
+
+ expect(resultErrors[1].line).toBe(8);
+ expect(resultErrors[1].message).toBe("Use 'String#startsWith' method instead.");
+ } finally {
+ writeFileSync(filePath, content);
+ }
+
+ results = await eslint.lintFiles(['**/*.gts', '**/*.ts']);
+
+ resultErrors = results.flatMap((result) => result.messages);
+ expect(resultErrors).toHaveLength(3);
+
+ expect(resultErrors[0].message).toBe("Use 'String#startsWith' method instead.");
+ expect(resultErrors[0].line).toBe(6);
+
+ expect(resultErrors[1].message).toBe("Use 'String#startsWith' method instead.");
+ expect(resultErrors[1].line).toBe(7);
+
+ expect(resultErrors[2].line).toBe(8);
+ expect(resultErrors[2].message).toBe("Use 'String#startsWith' method instead.");
+ });
});
diff --git a/tests/lib/rules-preprocessor/tsconfig.eslint.json b/tests/lib/rules-preprocessor/tsconfig.eslint.json
index 2767865654..b58959351c 100644
--- a/tests/lib/rules-preprocessor/tsconfig.eslint.json
+++ b/tests/lib/rules-preprocessor/tsconfig.eslint.json
@@ -5,6 +5,7 @@
"strictNullChecks": true
},
"include": [
- "*"
- ]
+ "**/*.ts",
+ "**/*.gts"
+ ],
}
diff --git a/yarn.lock b/yarn.lock
index 7635799ce3..af1be9943c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -49,6 +49,27 @@
json5 "^2.2.3"
semver "^6.3.1"
+"@babel/core@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.3.tgz#5ec09c8803b91f51cc887dedc2654a35852849c9"
+ integrity sha512-Jg+msLuNuCJDyBvFv5+OKOUjWMZgd85bKjbICd3zWrKAo+bJ49HJufi7CQE0q0uR8NGyO6xkCACScNqyjHSZew==
+ dependencies:
+ "@ampproject/remapping" "^2.2.0"
+ "@babel/code-frame" "^7.22.13"
+ "@babel/generator" "^7.23.3"
+ "@babel/helper-compilation-targets" "^7.22.15"
+ "@babel/helper-module-transforms" "^7.23.3"
+ "@babel/helpers" "^7.23.2"
+ "@babel/parser" "^7.23.3"
+ "@babel/template" "^7.22.15"
+ "@babel/traverse" "^7.23.3"
+ "@babel/types" "^7.23.3"
+ convert-source-map "^2.0.0"
+ debug "^4.1.0"
+ gensync "^1.0.0-beta.2"
+ json5 "^2.2.3"
+ semver "^6.3.1"
+
"@babel/eslint-parser@^7.22.15":
version "7.22.15"
resolved "https://registry.yarnpkg.com/@babel/eslint-parser/-/eslint-parser-7.22.15.tgz#263f059c476e29ca4972481a17b8b660cb025a34"
@@ -68,6 +89,16 @@
"@jridgewell/trace-mapping" "^0.3.17"
jsesc "^2.5.1"
+"@babel/generator@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.3.tgz#86e6e83d95903fbe7613f448613b8b319f330a8e"
+ integrity sha512-keeZWAV4LU3tW0qRi19HRpabC/ilM0HRBBzf9/k8FFiG4KVpiv0FIy4hHfLfFQZNhziCTPTmd59zoyv6DNISzg==
+ dependencies:
+ "@babel/types" "^7.23.3"
+ "@jridgewell/gen-mapping" "^0.3.2"
+ "@jridgewell/trace-mapping" "^0.3.17"
+ jsesc "^2.5.1"
+
"@babel/helper-annotate-as-pure@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882"
@@ -146,6 +177,17 @@
"@babel/helper-split-export-declaration" "^7.22.6"
"@babel/helper-validator-identifier" "^7.22.20"
+"@babel/helper-module-transforms@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1"
+ integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==
+ dependencies:
+ "@babel/helper-environment-visitor" "^7.22.20"
+ "@babel/helper-module-imports" "^7.22.15"
+ "@babel/helper-simple-access" "^7.22.5"
+ "@babel/helper-split-export-declaration" "^7.22.6"
+ "@babel/helper-validator-identifier" "^7.22.20"
+
"@babel/helper-optimise-call-expression@^7.22.5":
version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e"
@@ -226,6 +268,11 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719"
integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==
+"@babel/parser@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.3.tgz#0ce0be31a4ca4f1884b5786057cadcb6c3be58f9"
+ integrity sha512-uVsWNvlVsIninV2prNz/3lHCb+5CJ+e+IUBfbjToAHODtfGYLfCFuY4AU7TskI+dAKk+njsPiBjq1gKTvZOBaw==
+
"@babel/plugin-proposal-class-properties@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3"
@@ -375,6 +422,22 @@
debug "^4.1.0"
globals "^11.1.0"
+"@babel/traverse@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.3.tgz#26ee5f252e725aa7aca3474aa5b324eaf7908b5b"
+ integrity sha512-+K0yF1/9yR0oHdE0StHuEj3uTPzwwbrLGfNOndVJVV2TqA5+j3oljJUb4nmB954FLGjNem976+B+eDuLIjesiQ==
+ dependencies:
+ "@babel/code-frame" "^7.22.13"
+ "@babel/generator" "^7.23.3"
+ "@babel/helper-environment-visitor" "^7.22.20"
+ "@babel/helper-function-name" "^7.23.0"
+ "@babel/helper-hoist-variables" "^7.22.5"
+ "@babel/helper-split-export-declaration" "^7.22.6"
+ "@babel/parser" "^7.23.3"
+ "@babel/types" "^7.23.3"
+ debug "^4.1.0"
+ globals "^11.1.0"
+
"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.3.3":
version "7.23.0"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb"
@@ -384,6 +447,15 @@
"@babel/helper-validator-identifier" "^7.22.20"
to-fast-properties "^2.0.0"
+"@babel/types@^7.23.3":
+ version "7.23.3"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.3.tgz#d5ea892c07f2ec371ac704420f4dcdb07b5f9598"
+ integrity sha512-OZnvoH2l8PK5eUvEcUyCt/sXgr/h+UWpVuBbOljwcrAgUl6lpchoQ++PHGyQy1AtYnVA6CEq3y5xeEI10brpXw==
+ dependencies:
+ "@babel/helper-string-parser" "^7.22.5"
+ "@babel/helper-validator-identifier" "^7.22.20"
+ to-fast-properties "^2.0.0"
+
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"