Skip to content

Commit a98e2a3

Browse files
Merge pull request #131 from wagenet/gjs-types
Preserve types from gjs files
2 parents 86837af + 5b39dd2 commit a98e2a3

File tree

10 files changed

+385
-38
lines changed

10 files changed

+385
-38
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"dependencies": {
2929
"@babel/eslint-parser": "^7.23.10",
3030
"@glimmer/syntax": ">= 0.92.0",
31+
"@typescript-eslint/tsconfig-utils": "^8.38.0",
3132
"content-tag": "^2.0.1",
3233
"eslint-scope": "^7.2.2",
3334
"html-tags": "^3.3.1",

pnpm-lock.yaml

Lines changed: 85 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/parser/gjs-gts-parser.js

Lines changed: 135 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
const tsconfigUtils = require('@typescript-eslint/tsconfig-utils');
12
const babelParser = require('@babel/eslint-parser');
23
const { registerParsedFile } = require('../preprocessor/noop');
34
const {
@@ -15,8 +16,119 @@ const { transformForLint, preprocessGlimmerTemplates, convertAst } = require('./
1516
* 3. preprocesses the templates info and prepares the Glimmer AST
1617
* 4. converts the js/ts AST so that it includes the Glimmer AST at the right locations, replacing the original
1718
*/
19+
20+
/**
21+
* @param {string} tsconfigPath
22+
* @param {string} rootDir
23+
* @returns {boolean|undefined}
24+
*/
25+
function parseAllowJsFromTsconfig(tsconfigPath, rootDir) {
26+
try {
27+
const parserPath = require.resolve('@typescript-eslint/parser');
28+
// eslint-disable-next-line n/no-unpublished-require
29+
const tsPath = require.resolve('typescript', { paths: [parserPath] });
30+
const ts = require(tsPath);
31+
const parsed = tsconfigUtils.getParsedConfigFile(ts, tsconfigPath, rootDir);
32+
return parsed?.options?.allowJs;
33+
} catch (e) {
34+
// eslint-disable-next-line no-console
35+
console.warn('[ember-eslint-parser] Failed to parse tsconfig:', tsconfigPath, e);
36+
return undefined;
37+
}
38+
}
39+
40+
/**
41+
* @param {Array<boolean|undefined>} values
42+
* @param {string} source
43+
* @returns {boolean|null}
44+
*/
45+
function resolveAllowJs(values, source) {
46+
const filtered = values.filter((val) => typeof val !== 'undefined');
47+
if (filtered.length > 0) {
48+
const uniqueValues = [...new Set(filtered)];
49+
if (uniqueValues.length > 1) {
50+
// eslint-disable-next-line no-console
51+
console.warn(
52+
`[ember-eslint-parser] Conflicting allowJs values in ${source}. Defaulting allowGjs to false.`
53+
);
54+
return false;
55+
} else {
56+
return uniqueValues[0];
57+
}
58+
}
59+
return null;
60+
}
61+
62+
/**
63+
* @param {Array<{getCompilerOptions?: Function}>|undefined} programs
64+
* @returns {boolean|null}
65+
*/
66+
function getAllowJsFromPrograms(programs) {
67+
if (!Array.isArray(programs) || programs.length === 0) return null;
68+
const allowJsValues = programs
69+
.map((p) => p.getCompilerOptions?.())
70+
.filter(Boolean)
71+
.map((opts) => opts.allowJs);
72+
return resolveAllowJs(allowJsValues, 'programs');
73+
}
74+
75+
/**
76+
* @param {boolean|object|undefined} projectService
77+
* @returns {string|null}
78+
*/
79+
function getProjectServiceTsconfigPath(projectService) {
80+
if (!projectService) return null;
81+
82+
// If projectService is true, use default behavior (nearest tsconfig.json, allowJs from config)
83+
if (projectService === true) {
84+
return 'tsconfig.json';
85+
}
86+
87+
// If projectService is an object, handle ProjectServiceOptions
88+
if (typeof projectService === 'object') {
89+
if (typeof projectService.allowDefaultProject !== 'undefined') {
90+
// eslint-disable-next-line no-console
91+
console.warn(
92+
'[ember-eslint-parser] projectService.allowDefaultProject is specified. Behavior may differ depending on default project config.'
93+
);
94+
}
95+
return projectService.defaultProject ?? 'tsconfig.json';
96+
}
97+
98+
return null;
99+
}
100+
101+
/**
102+
* Returns the resolved allowJs value based on priority: programs > projectService > project/tsconfig
103+
*/
104+
function getAllowJs(options) {
105+
const allowJsFromPrograms = getAllowJsFromPrograms(options.programs);
106+
if (allowJsFromPrograms !== null) return allowJsFromPrograms;
107+
108+
const rootDir = options.tsconfigRootDir || process.cwd();
109+
110+
const projectServiceTsconfigPath = getProjectServiceTsconfigPath(options.projectService);
111+
if (projectServiceTsconfigPath) {
112+
return parseAllowJsFromTsconfig(projectServiceTsconfigPath, rootDir);
113+
}
114+
115+
let tsconfigPaths = [];
116+
if (Array.isArray(options.project)) {
117+
tsconfigPaths = options.project;
118+
} else if (typeof options.project === 'string') {
119+
tsconfigPaths = [options.project];
120+
} else if (options.project) {
121+
tsconfigPaths = ['tsconfig.json'];
122+
}
123+
if (tsconfigPaths.length > 0) {
124+
const allowJsValues = tsconfigPaths.map((cfg) => parseAllowJsFromTsconfig(cfg, rootDir));
125+
return resolveAllowJs(allowJsValues, 'project');
126+
}
127+
128+
return false;
129+
}
130+
18131
/**
19-
*
20132
* @type {import('eslint').ParserModule}
21133
*/
22134
module.exports = {
@@ -26,7 +138,13 @@ module.exports = {
26138
},
27139

28140
parseForESLint(code, options) {
29-
patchTs();
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+
}
30148
registerParsedFile(options.filePath);
31149
let jsCode = code;
32150
const info = transformForLint(code, options.filePath);
@@ -73,7 +191,21 @@ module.exports = {
73191
result.isTypescript = isTypescript || useTypescript;
74192
convertAst(result, preprocessedResult, visitorKeys);
75193
if (result.services?.program) {
76-
syncMtsGtsSourceFiles(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);
77209
}
78210
return { ...result, visitorKeys };
79211
} catch (e) {

0 commit comments

Comments
 (0)