ember-eslint-parser 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ember-eslint-parser",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "",
5
5
  "keywords": [],
6
6
  "repository": {
@@ -9,6 +9,7 @@
9
9
  },
10
10
  "license": "ISC",
11
11
  "author": "",
12
+ "type": "module",
12
13
  "exports": {
13
14
  ".": "./src/parser/gjs-gts-parser.js",
14
15
  "./hbs": "./src/parser/hbs-parser.js",
@@ -19,11 +20,11 @@
19
20
  "src"
20
21
  ],
21
22
  "dependencies": {
22
- "@babel/eslint-parser": "^7.28.6",
23
- "@glimmer/syntax": ">= 0.92.0",
24
- "@typescript-eslint/tsconfig-utils": "^8.38.0",
25
- "content-tag": "^4.1.0",
26
- "eslint-scope": "^9.1.1",
23
+ "@glimmer/syntax": "^0.95.0",
24
+ "@typescript-eslint/tsconfig-utils": "^8.57.1",
25
+ "content-tag": "^4.1.1",
26
+ "ember-estree": "^0.4.2",
27
+ "eslint-scope": "^9.1.2",
27
28
  "html-tags": "^5.1.0",
28
29
  "mathml-tag-names": "^4.0.0",
29
30
  "svg-tags": "^1.0.0"
@@ -43,6 +44,7 @@
43
44
  "eslint-plugin-promise": "^6.0.0",
44
45
  "execa": "^8.0.1",
45
46
  "fs-extra": "^11.2.0",
47
+ "mitata": "1.0.34",
46
48
  "prettier": "^3.2.5",
47
49
  "publint": "^0.2.6",
48
50
  "release-plan": "^0.17.4",
@@ -51,7 +53,6 @@
51
53
  "vitest": "^1.2.2"
52
54
  },
53
55
  "peerDependencies": {
54
- "@babel/core": "^7.23.6",
55
56
  "@typescript-eslint/parser": "*"
56
57
  },
57
58
  "peerDependenciesMeta": {
@@ -67,7 +68,10 @@
67
68
  "lint:fix": "concurrently \"npm:lint:*:fix\" --names \"fix:\"",
68
69
  "lint:js": "eslint . --max-warnings=0",
69
70
  "lint:js:fix": "eslint . --fix --max-warnings=0",
70
- "lint:package": "pnpm publint",
71
+ "lint:package": "publint",
72
+ "bench": "node --expose-gc tests/parser.bench.mjs",
73
+ "bench:compare": "node scripts/bench-compare.mjs",
74
+ "bench:summary": "./scripts/local-bench-summary.sh",
71
75
  "test": "vitest run"
72
76
  }
73
77
  }
@@ -1,20 +1,22 @@
1
- const tsconfigUtils = require('@typescript-eslint/tsconfig-utils');
2
- const babelParser = require('@babel/eslint-parser/experimental-worker');
3
- const { registerParsedFile } = require('../preprocessor/noop');
4
- const {
5
- patchTs,
6
- replaceExtensions,
7
- syncMtsGtsSourceFiles,
8
- typescriptParser,
9
- } = require('./ts-patch');
10
- const { transformForLint, preprocessGlimmerTemplates, convertAst } = require('./transforms');
1
+ import { createRequire } from 'node:module';
2
+ import tsconfigUtils from '@typescript-eslint/tsconfig-utils';
3
+ import { registerParsedFile } from '../preprocessor/noop.js';
4
+ import { patchTs, replaceExtensions, syncMtsGtsSourceFiles, typescriptParser } from './ts-patch.js';
5
+ import { buildGlimmerVisitors } from './transforms.js';
6
+ import { toTree } from 'ember-estree';
7
+ import * as eslintScope from 'eslint-scope';
8
+
9
+ const require = createRequire(import.meta.url);
11
10
 
12
11
  /**
13
12
  * implements https://eslint.org/docs/latest/extend/custom-parsers
14
- * 1. transforms gts/gjs files into parseable ts/js without changing the offsets and locations around it
15
- * 2. parses the transformed code and generates the AST for TS ot JS
16
- * 3. preprocesses the templates info and prepares the Glimmer AST
17
- * 4. converts the js/ts AST so that it includes the Glimmer AST at the right locations, replacing the original
13
+ *
14
+ * Uses ember-estree's toTree with a custom parser option to handle
15
+ * the full pipeline: template extraction, placeholder replacement,
16
+ * JS/TS parsing, Glimmer AST processing, and AST splicing.
17
+ *
18
+ * Scope registration happens via toTree's visitors API, eliminating
19
+ * a second AST traversal.
18
20
  */
19
21
 
20
22
  /**
@@ -79,12 +81,10 @@ function getAllowJsFromPrograms(programs) {
79
81
  function getProjectServiceTsconfigPath(projectService) {
80
82
  if (!projectService) return null;
81
83
 
82
- // If projectService is true, use default behavior (nearest tsconfig.json, allowJs from config)
83
84
  if (projectService === true) {
84
85
  return 'tsconfig.json';
85
86
  }
86
87
 
87
- // If projectService is an object, handle ProjectServiceOptions
88
88
  if (typeof projectService === 'object') {
89
89
  if (typeof projectService.allowDefaultProject !== 'undefined') {
90
90
  // eslint-disable-next-line no-console
@@ -131,86 +131,115 @@ function getAllowJs(options) {
131
131
  /**
132
132
  * @type {import('eslint').ParserModule}
133
133
  */
134
- module.exports = {
135
- meta: {
136
- name: 'ember-eslint-parser',
137
- version: '*',
138
- },
139
-
140
- parseForESLint(code, options) {
141
- const allowGjsWasSet = options.allowGjs !== undefined;
142
- const allowGjs = allowGjsWasSet ? options.allowGjs : getAllowJs(options);
143
- let actualAllowGjs;
144
- // Only patch TypeScript if we actually need it.
145
- if (options.programs || options.projectService || options.project) {
146
- ({ allowGjs: actualAllowGjs } = patchTs({ allowGjs }));
147
- }
148
- registerParsedFile(options.filePath);
149
- let jsCode = code;
150
- const info = transformForLint(code, options.filePath);
151
- jsCode = info.output;
134
+ export const meta = {
135
+ name: 'ember-eslint-parser',
136
+ version: '*',
137
+ };
152
138
 
153
- const isTypescript = options.filePath.endsWith('.gts') || options.filePath.endsWith('.ts');
154
- let useTypescript = true;
139
+ export function parseForESLint(code, options) {
140
+ const allowGjsWasSet = options.allowGjs !== undefined;
141
+ const allowGjs = allowGjsWasSet ? options.allowGjs : getAllowJs(options);
142
+ let actualAllowGjs;
143
+ // Only patch TypeScript if we actually need it.
144
+ if (options.programs || options.projectService || options.project) {
145
+ ({ allowGjs: actualAllowGjs } = patchTs({ allowGjs }));
146
+ }
147
+ registerParsedFile(options.filePath);
155
148
 
156
- if (options.useBabel || !typescriptParser) {
157
- useTypescript = false;
158
- }
149
+ const isTypescript = options.filePath.endsWith('.gts') || options.filePath.endsWith('.ts');
150
+ let useTypescript = true;
159
151
 
160
- let result = null;
161
- const filePath = options.filePath;
162
- if (options.project || options.projectService) {
163
- jsCode = replaceExtensions(jsCode);
164
- }
152
+ if (options.useBabel || !typescriptParser) {
153
+ useTypescript = false;
154
+ }
165
155
 
166
- if (isTypescript && !typescriptParser) {
167
- throw new Error('Please install typescript to process gts');
168
- }
156
+ if (isTypescript && !typescriptParser) {
157
+ throw new Error('Please install typescript to process gts');
158
+ }
159
+
160
+ const filePath = options.filePath;
161
+ const useTS = isTypescript || useTypescript;
169
162
 
170
- try {
171
- result =
172
- isTypescript || useTypescript
173
- ? typescriptParser.parseForESLint(jsCode, {
163
+ // Both paths create scopeManager inside the parser callback so it's
164
+ // available when toTree invokes visitors during splice — no second pass.
165
+ let scopeManager = null;
166
+
167
+ try {
168
+ const result = toTree(code, {
169
+ filePath,
170
+ parser: useTS
171
+ ? (placeholderJS) => {
172
+ let parseCode = placeholderJS;
173
+ if (options.project || options.projectService) {
174
+ parseCode = replaceExtensions(parseCode);
175
+ }
176
+ const tsResult = typescriptParser.parseForESLint(parseCode, {
174
177
  ...options,
175
178
  ranges: true,
176
179
  extraFileExtensions: ['.gts', '.gjs'],
177
180
  filePath,
178
- })
179
- : babelParser.parseForESLint(jsCode, {
180
- ...options,
181
- requireConfigFile: false,
182
- ranges: true,
183
181
  });
184
- if (!info.templateInfos?.length) {
185
- return result;
186
- }
187
- const preprocessedResult = preprocessGlimmerTemplates(info, code);
188
- preprocessedResult.code = code;
189
- const { templateVisitorKeys } = preprocessedResult;
190
- const visitorKeys = { ...result.visitorKeys, ...templateVisitorKeys };
191
- result.isTypescript = isTypescript || useTypescript;
192
- convertAst(result, preprocessedResult, visitorKeys);
193
- if (result.services?.program) {
194
- // Compare allowJs with the actual program's compiler options
195
- const programAllowJs = result.services.program.getCompilerOptions?.()?.allowJs;
196
- if (
197
- !allowGjsWasSet &&
198
- programAllowJs !== undefined &&
199
- actualAllowGjs !== undefined &&
200
- actualAllowGjs !== programAllowJs
201
- ) {
202
- // eslint-disable-next-line no-console
203
- console.warn(
204
- '[ember-eslint-parser] allowJs does not match the actual program. Consider setting allowGjs explicitly.\n' +
205
- ` Current: ${allowGjs}, Program: ${programAllowJs}`
206
- );
207
- }
208
- syncMtsGtsSourceFiles(result.services.program);
182
+ scopeManager = tsResult.scopeManager;
183
+ return tsResult;
184
+ }
185
+ : (placeholderJS) => {
186
+ // JS path: parse with oxc, create scope manager from placeholder AST
187
+ const { parseSync } = require('oxc-parser');
188
+ const oxcResult = parseSync(filePath || 'input.js', placeholderJS);
189
+ const program = oxcResult.program;
190
+ program.tokens = oxcResult.tokens || [];
191
+ program.comments = oxcResult.comments || [];
192
+ scopeManager = eslintScope.analyze(program, {
193
+ ecmaVersion: 2022,
194
+ sourceType: 'module',
195
+ range: true,
196
+ });
197
+ return { ast: program, scopeManager };
198
+ },
199
+ visitors: buildGlimmerVisitors(() => scopeManager, useTS),
200
+ });
201
+
202
+ if (!result.scopeManager) result.scopeManager = scopeManager;
203
+
204
+ if (result.services?.program) {
205
+ const programAllowJs = result.services.program.getCompilerOptions?.()?.allowJs;
206
+ if (
207
+ !allowGjsWasSet &&
208
+ programAllowJs !== undefined &&
209
+ actualAllowGjs !== undefined &&
210
+ actualAllowGjs !== programAllowJs
211
+ ) {
212
+ // eslint-disable-next-line no-console
213
+ console.warn(
214
+ '[ember-eslint-parser] allowJs does not match the actual program. Consider setting allowGjs explicitly.\n' +
215
+ ` Current: ${allowGjs}, Program: ${programAllowJs}`
216
+ );
209
217
  }
210
- return { ...result, visitorKeys };
211
- } catch (e) {
212
- console.error(e);
213
- throw e;
218
+ syncMtsGtsSourceFiles(result.services.program);
214
219
  }
215
- },
216
- };
220
+
221
+ delete result.templateInfos;
222
+ delete result.isTypescript;
223
+
224
+ return result;
225
+ } catch (e) {
226
+ // eslint-disable-next-line no-console
227
+ console.error(e);
228
+ // Convert content-tag parse errors to ESLint-compatible format
229
+ if (e.message?.includes('Parse Error at')) {
230
+ const [line, column] = e.message
231
+ .split(':')
232
+ .slice(-2)
233
+ .map((x) => parseInt(x));
234
+ const err = new Error(e.source_code || e.message);
235
+ err.lineNumber = line;
236
+ err.column = column;
237
+ err.fileName = filePath;
238
+ err.index = undefined;
239
+ throw err;
240
+ }
241
+ throw e;
242
+ }
243
+ }
244
+
245
+ export default { meta, parseForESLint };
@@ -1,6 +1,8 @@
1
- const eslintScope = require('eslint-scope');
2
- const DocumentLines = require('../utils/document');
3
- const { processGlimmerTemplate, buildGlimmerVisitorKeys } = require('./transforms');
1
+ import * as eslintScope from 'eslint-scope';
2
+ import { toTree, glimmerVisitorKeys, DocumentLines } from 'ember-estree';
3
+
4
+ // Constant: Program + all Glimmer node types. Computed once at module load.
5
+ const hbsVisitorKeys = { Program: ['body'], ...glimmerVisitorKeys };
4
6
 
5
7
  /**
6
8
  * implements https://eslint.org/docs/latest/extend/custom-parsers
@@ -14,76 +16,76 @@ const { processGlimmerTemplate, buildGlimmerVisitorKeys } = require('./transform
14
16
  /**
15
17
  * @type {import('eslint').ParserModule}
16
18
  */
17
- module.exports = {
18
- meta: {
19
- name: 'ember-eslint-parser/hbs',
20
- version: '*',
21
- },
19
+ export const meta = {
20
+ name: 'ember-eslint-parser/hbs',
21
+ version: '*',
22
+ };
22
23
 
23
- parseForESLint(code, options) {
24
- const filePath = (options && options.filePath) || '<hbs>';
25
- const codeLines = new DocumentLines(code);
24
+ export function parseForESLint(code, options) {
25
+ const filePath = (options && options.filePath) || '<hbs>';
26
26
 
27
- let result;
28
- try {
29
- result = processGlimmerTemplate({
30
- templateContent: code,
31
- codeLines,
32
- templateRange: [0, code.length],
27
+ let result;
28
+ try {
29
+ result = toTree(code, { templateOnly: true });
30
+ } catch (e) {
31
+ // Transform glimmer parse error to ESLint-compatible error
32
+ const loc = e.location || (e.hash && e.hash.loc);
33
+ if (loc && loc.start) {
34
+ const codeLines = new DocumentLines(code);
35
+ const err = Object.assign(new SyntaxError(e.message), {
36
+ lineNumber: loc.start.line,
37
+ column: loc.start.column,
38
+ index: codeLines.positionToOffset(loc.start),
39
+ fileName: filePath,
33
40
  });
34
- } catch (e) {
35
- // Transform glimmer parse error to ESLint-compatible error
36
- const loc = e.location || (e.hash && e.hash.loc);
37
- if (loc && loc.start) {
38
- const err = Object.assign(new SyntaxError(e.message), {
39
- lineNumber: loc.start.line,
40
- column: loc.start.column,
41
- index: codeLines.positionToOffset(loc.start),
42
- fileName: filePath,
43
- });
44
- throw err;
45
- }
46
- throw e;
41
+ throw err;
47
42
  }
43
+ throw e;
44
+ }
45
+
46
+ const { ast: templateNode, comments } = result;
47
+
48
+ // Use the Template node's loc.end for the Program's end position
49
+ // (avoids creating a duplicate DocumentLines just for this)
50
+ const endLoc = templateNode.loc?.end || { line: 1, column: code.length };
48
51
 
49
- const { ast: templateNode, comments } = result;
52
+ // Wrap in a synthetic Program node (required by ESLint)
53
+ const program = {
54
+ type: 'Program',
55
+ body: [templateNode],
56
+ tokens: templateNode.tokens,
57
+ comments,
58
+ range: [0, code.length],
59
+ start: 0,
60
+ end: code.length,
61
+ loc: {
62
+ start: { line: 1, column: 0 },
63
+ end: endLoc,
64
+ },
65
+ };
50
66
 
51
- // Wrap in a synthetic Program node (required by ESLint)
52
- const program = {
67
+ // Build visitor keys: Program + all Glimmer node types
68
+ const visitorKeys = hbsVisitorKeys;
69
+
70
+ // Create an empty scope manager.
71
+ // For HBS, all locals are assumed to be defined at runtime,
72
+ // so we don't track variable references (no no-undef errors).
73
+ const scopeManager = eslintScope.analyze(
74
+ {
53
75
  type: 'Program',
54
- body: [templateNode],
55
- tokens: templateNode.tokens,
56
- comments,
76
+ body: [],
57
77
  range: [0, code.length],
58
- start: 0,
59
- end: code.length,
60
- loc: {
61
- start: { line: 1, column: 0 },
62
- end: codeLines.offsetToPosition(code.length),
63
- },
64
- };
65
-
66
- // Build visitor keys: Program + all Glimmer node types
67
- const visitorKeys = { Program: ['body'], ...buildGlimmerVisitorKeys() };
78
+ loc: program.loc,
79
+ },
80
+ { range: true }
81
+ );
68
82
 
69
- // Create an empty scope manager.
70
- // For HBS, all locals are assumed to be defined at runtime,
71
- // so we don't track variable references (no no-undef errors).
72
- const scopeManager = eslintScope.analyze(
73
- {
74
- type: 'Program',
75
- body: [],
76
- range: [0, code.length],
77
- loc: program.loc,
78
- },
79
- { range: true }
80
- );
83
+ return {
84
+ ast: program,
85
+ scopeManager,
86
+ visitorKeys,
87
+ services: {},
88
+ };
89
+ }
81
90
 
82
- return {
83
- ast: program,
84
- scopeManager,
85
- visitorKeys,
86
- services: {},
87
- };
88
- },
89
- };
91
+ export default { meta, parseForESLint };