Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ It's recommended to only use _overrides_ when defining your eslint config, so us
if we detect a typescript parser, it will also be used for all files, otherwise babel parser will be used.
If we cannot find a typescript parser when linting gts we throw an error.

If you use type-aware rules, route `.js`/`.ts` through this parser too, as
`eslint-plugin-ember`'s configs do:

```js
{
files: ['**/*.{js,ts}'],
parser: 'ember-eslint-parser',
// ...
},
```

`@typescript-eslint/parser` hands TypeScript the source as ESLint read it, so a
`.ts` file importing from a `.gts` gets an `error` type for that import. This
parser rewrites the specifier first.

## HBS (Handlebars) support

For `.hbs` template files, use the `ember-eslint-parser/hbs` parser. In ESLint's flat config format (ESLint 9+):
Expand Down
59 changes: 58 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 1 addition & 5 deletions src/parser/gjs-gts-parser.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createRequire } from 'node:module';
import { registerParsedFile } from '../preprocessor/noop.js';
import { patchTs, replaceExtensions, syncMtsGtsSourceFiles, typescriptParser } from './ts-patch.js';
import { replaceExtensions, syncMtsGtsSourceFiles, typescriptParser } from './ts-patch.js';
import { buildGlimmerVisitors } from './transforms.js';
import { toTree } from 'ember-estree';

Expand Down Expand Up @@ -48,10 +48,6 @@ export const meta = {
};

export function parseForESLint(code, options) {
// Only patch TypeScript if we actually need it.
if (options.programs || options.projectService || options.project) {
patchTs();
}
registerParsedFile(options.filePath);

const isTypescript = options.filePath.endsWith('.gts') || options.filePath.endsWith('.ts');
Expand Down
6 changes: 6 additions & 0 deletions src/parser/ts-patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ try {
return jsCode;
};

// typescript-eslint copies `ts.sys` by value when it builds a program or
// project service, on the first type-aware parse in the process — which may be
// a plain .ts file that never reaches this parser. Patching at module load is
// what puts the wrappers in that copy, and in the project's first file scan.
patchTs();

/**
*
* @param program {ts.Program}
Expand Down
32 changes: 32 additions & 0 deletions test-projects/parser-order/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use strict';

// The README's setup: only .gjs/.gts go through ember-eslint-parser, .ts is left
// to @typescript-eslint/parser. See check.mjs.

const manifest = require('@typescript-eslint/parser/package.json');
const isV8 = parseInt(manifest.version, 10) >= 8;

// projectService landed in v8 under this name; older versions get the classic
// watch program.
const useProjectService = process.env.PROJECT_SERVICE && isV8;

module.exports = {
root: true,
parserOptions: {
...(useProjectService ? { projectService: true } : { project: './tsconfig.json' }),
tsconfigRootDir: __dirname,
extraFileExtensions: ['.gts', '.gjs'],
},
overrides: [
{
files: ['**/*.ts'],
parser: '@typescript-eslint/parser',
extends: ['plugin:@typescript-eslint/recommended-type-checked'],
},
{
files: ['**/*.gts'],
parser: 'ember-eslint-parser',
extends: ['plugin:@typescript-eslint/recommended-type-checked'],
},
],
};
34 changes: 34 additions & 0 deletions test-projects/parser-order/check.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Lints the .ts file in a pass of its own, so the program is built before this
* parser is asked for anything. Unless ts.sys is patched by then, the .gts import
* in uses-dep.gts resolves to `error` and the no-unsafe-* rules fire on it.
*/
import { fileURLToPath } from 'node:url';
import { ESLint } from 'eslint';

const eslint = new ESLint({ cwd: fileURLToPath(new URL('.', import.meta.url)) });

async function lint(files) {
const results = await eslint.lintFiles(files);
if (results.length !== files.length) {
throw new Error(`expected ${files.length} file(s) linted, got ${results.length}`);
}
return results;
}

const results = [
...(await lint(['src/plain.ts'])),
...(await lint(['src/dep.gts', 'src/uses-dep.gts'])),
];

const problems = results.flatMap((result) =>
result.messages.map((m) => `${result.filePath}:${m.line}:${m.column} ${m.message} (${m.ruleId})`)
);

if (problems.length > 0) {
console.error(`${problems.length} unexpected problem(s):`);
for (const problem of problems) console.error(` ${problem}`);
process.exit(1);
}

console.log(`ok — ${results.length} files linted, no problems`);
16 changes: 16 additions & 0 deletions test-projects/parser-order/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "@test-project/parser-order",
"private": true,
"scripts": {
"test:check": "pnpm run /test:check:.*/",
"test:check:project": "node ./check.mjs",
"test:check:project-service": "PROJECT_SERVICE=true node ./check.mjs"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^8.46.4",
"@typescript-eslint/parser": "^8.46.4",
"ember-eslint-parser": "workspace:*",
"eslint": "^8.0.1",
"typescript": "^5.3.3"
}
}
9 changes: 9 additions & 0 deletions test-projects/parser-order/src/dep.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export class Dep {
greet(): string {
return 'hello';
}

<template>
<span>{{this.greet}}</span>
</template>
}
2 changes: 2 additions & 0 deletions test-projects/parser-order/src/plain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Linted first, and on its own, by check.mjs.
export const first: number = 1;
8 changes: 8 additions & 0 deletions test-projects/parser-order/src/uses-dep.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Dep } from './dep.gts';

const dep = new Dep();
export const greeting: string = dep.greet();

<template>
<div>{{greeting}}</div>
</template>
19 changes: 19 additions & 0 deletions test-projects/parser-order/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es2019",
"lib": [
"ES2018",
"DOM"
],
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"allowImportingTsExtensions": true
},
"include": [
"src/**/*.ts",
"src/**/*.gts"
]
}
38 changes: 38 additions & 0 deletions tests/ts-patch-load-order.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, expect, it } from 'vitest';
import { createRequire } from 'node:module';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';

// typescript-eslint copies ts.sys by value when it builds a program, on the
// first type-aware parse in the process — possibly a plain .ts file that never
// reaches this parser. So importing the parser, with no parseForESLint call, has
// to be enough to leave ts.sys patched.
import '../src/parser/gjs-gts-parser.js';

const require = createRequire(import.meta.url);
const parserPath = require.resolve('@typescript-eslint/parser');
const ts = require(require.resolve('typescript', { paths: [parserPath] }));

describe('ts.sys is patched by importing the parser', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ee-parser-load-order-'));
const gts = path.join(dir, 'component.gts');
fs.writeFileSync(gts, 'export const name = "x";\n<template>{{name}}</template>\n');

it('reports the virtual .mts twin of a .gts as existing', () => {
expect(ts.sys.fileExists(path.join(dir, 'component.mts'))).toBe(true);
});

it('reads the .gts through its virtual twin, transformed', () => {
const content = ts.sys.readFile(path.join(dir, 'component.mts'));

expect(content).toContain('export const name');
expect(content).not.toContain('<template>');
});

it('offers the virtual twin when TypeScript scans the directory', () => {
const found = ts.sys.readDirectory(dir, ['.ts', '.gts']);

expect(found).toContain(path.join(dir, 'component.mts'));
});
});
Loading