Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support TypeScript config using importx #18440

Closed
wants to merge 32 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
84f49f8
feat: support TypeScript config using `tsx`
antfu May 10, 2024
16f7cb9
docs: update
antfu May 10, 2024
95ece9b
chore: update
antfu May 10, 2024
45e5cf6
refactor: use `isFileTS` function
antfu May 10, 2024
ed95f5f
chore: update lint
antfu May 10, 2024
aa6cdcc
chore: fix ci
antfu May 10, 2024
5fe8d19
feat: switch to `importx`
antfu May 24, 2024
e8ea5cd
chore: update tests
antfu May 24, 2024
3a9a1b3
Merge branch 'main' into feat/ts-config
antfu May 24, 2024
035d0e4
fix: tests
antfu May 27, 2024
771c46a
chore: update
antfu May 27, 2024
bd811ba
chore: update
antfu May 27, 2024
bac5ddf
chore: fix test
antfu May 27, 2024
5ebc126
fix: allow any version of `importx`
antfu May 28, 2024
d2660d6
chore: add more tests
antfu May 28, 2024
744a152
Update tests/fixtures/config-ts/cts/eslint.config.cts
antfu May 31, 2024
9d02520
chore: bump importx
antfu Jun 1, 2024
34e81d6
chore: update importx
antfu Jun 1, 2024
df00166
chore: bump importx
antfu Jun 1, 2024
d7a3132
test: add test for const enum and namespace
antfu Jun 1, 2024
17a3a4e
Merge branch 'main' into feat/ts-config
antfu Jun 3, 2024
f369bff
feat: early bail out in deno and bun
antfu Jun 18, 2024
757ebf9
merge
antfu Jun 18, 2024
547ee8d
Update docs/src/use/configure/configuration-files.md
antfu Jun 19, 2024
6a5a109
Merge branch 'main' into feat/ts-config
antfu Jun 28, 2024
b98b815
chore: update function name
antfu Jun 28, 2024
5c2a5c2
chore: update
antfu Jun 28, 2024
4039e03
chore: bump deps
antfu Jun 28, 2024
a437fa3
chore: bump importx
antfu Jun 29, 2024
568a877
fix: bump importx
antfu Jun 29, 2024
7010bb6
docs: note about top-level await
antfu Jun 29, 2024
83c3170
docs: update tag
antfu Jun 29, 2024
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
23 changes: 23 additions & 0 deletions docs/src/use/configure/configuration-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ The ESLint configuration file may be named any of the following:
* `eslint.config.js`
* `eslint.config.mjs`
* `eslint.config.cjs`
* `eslint.config.ts` (requires [TypeScript setup](#typescript-support))
* `eslint.config.mts` (requires [TypeScript setup](#typescript-support))
* `eslint.config.cts` (requires [TypeScript setup](#typescript-support))

It should be placed in the root directory of your project and export an array of [configuration objects](#configuration-objects). Here's an example:

Expand Down Expand Up @@ -495,3 +498,23 @@ npx eslint --config some-other-file.js **/*.js
```

In this case, ESLint does not search for `eslint.config.js` and instead uses `some-other-file.js`.

## TypeScript Support

::: warning
Loading TypeScript configuration files is an experimental feature and may change in future releases.
:::

ESLint supports loading configuration files written in TypeScript. When ESLint is executed in a runtime that supports TypeScript natively (Deno and Bun), native imports will be used. For other runtimes, the TypeScript support is powered by [`importx`](https://github.com/antfu-collective/importx), which is not automatically installed with ESLint. You need to install it as a dev dependency manually:

```shell
npm install --save-dev importx
```

Note that `importx` only strips off type annotations and does not perform type checking.

When both `eslint.config.js` and `eslint.config.ts` are present in the same directory, JavaScript versions always take precedence unless provided specifically via `--config` option.

::: tip
Due to the current limitation of Node.js loaders, top-level await in `eslint.config.ts` is only supported in Node.js v20.0 and later.
:::
3 changes: 3 additions & 0 deletions knip.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
"@wdio/cli",
"rollup-plugin-node-polyfills",

// Optional dependencies for loading TypeScript configs
"importx",

// FIXME: not sure why is eslint-config-eslint reported as unused
"eslint-config-eslint"
]
Expand Down
45 changes: 43 additions & 2 deletions lib/eslint/eslint.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,10 @@ const { Retrier } = require("@humanwhocodes/retry");
const FLAT_CONFIG_FILENAMES = [
"eslint.config.js",
"eslint.config.mjs",
"eslint.config.cjs"
"eslint.config.cjs",
"eslint.config.ts",
"eslint.config.mts",
"eslint.config.cts"
JoshuaKGoldberg marked this conversation as resolved.
Show resolved Hide resolved
];
const debug = require("debug")("eslint:eslint");
const privateMembers = new WeakMap();
Expand Down Expand Up @@ -271,6 +274,33 @@ function findFlatConfigFile(cwd) {
);
}

/**
* Check if the file is a TypeScript file.
* @param {string} filePath The file path to check.
* @returns {boolean} `true` if the file is a TypeScript file, `false` if it's not.
*/
function isFileTS(filePath) {
const fileExtension = path.extname(filePath);

return fileExtension.endsWith("ts");
}

/**
* Check if the current runtime supports native TypeScript.
* In those cases, we can use `import()` to load TypeScript files.
* @returns {boolean} `true` if the runtime supports TypeScript, `false` if it doesn't.
*/
function doesRuntimeSupportsTS() {

/**
* We only do cheap early checks here to avoid requiring users to install `importx` in those cases,
* full checks are done by `importx` when it's used.
*
* It's known the Bun and Deno support TypeScript natively.
*/
return Boolean(globalThis.Bun || globalThis.Deno);
}

/**
* Load the config array from the given filename.
* @param {string} filePath The filename to load from.
Expand Down Expand Up @@ -314,7 +344,18 @@ async function loadFlatConfigFile(filePath) {
delete require.cache[filePath];
}

const config = (await import(fileURL)).default;
let config;

if (isFileTS(filePath) && !doesRuntimeSupportsTS()) {
config = await import("importx")
.then(r => r.import(fileURL, __filename));
} else {
config = await import(fileURL);
}

if (config.default) {
config = await config.default;
}

importedConfigFileModificationTime.set(filePath, mtime);

Expand Down
9 changes: 9 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@
"strip-ansi": "^6.0.1",
"text-table": "^0.2.0"
},
"peerDependencies": {
"importx": "*"
},
"peerDependenciesMeta": {
"importx": {
"optional": true
}
},
"devDependencies": {
"@babel/core": "^7.4.3",
"@babel/preset-env": "^7.4.3",
Expand Down Expand Up @@ -157,6 +165,7 @@
"semver": "^7.5.3",
"shelljs": "^0.8.5",
"sinon": "^11.0.0",
"importx": "^0.3.10",
"typescript": "^5.3.3",
"vite-plugin-commonjs": "^0.10.0",
"webpack": "^5.23.0",
Expand Down
15 changes: 15 additions & 0 deletions tests/fixtures/config-ts/cts/eslint.config.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
interface FakeFlatConfigItem {
plugins?: Record<string, unknown>;
name?: string;
rules?: Record<string, unknown>;
}

const config: FakeFlatConfigItem[] = [
{
rules: {
"no-undef": "error" as string
}
},
]

module.exports = config;
15 changes: 15 additions & 0 deletions tests/fixtures/config-ts/custom/eslint.custom.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
interface FakeFlatConfigItem {
plugins?: Record<string, unknown>;
name?: string;
rules?: Record<string, unknown>;
}

const config: FakeFlatConfigItem[] = [
{
rules: {
"no-undef": "error" as string
}
},
]

export default config;
5 changes: 5 additions & 0 deletions tests/fixtures/config-ts/js-ts-mixed/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
rules: {
"no-undef": "error"
}
};
5 changes: 5 additions & 0 deletions tests/fixtures/config-ts/js-ts-mixed/eslint.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
rules: {
"no-undef": "warn" as string
}
};
1 change: 1 addition & 0 deletions tests/fixtures/config-ts/js-ts-mixed/foo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
foo;
15 changes: 15 additions & 0 deletions tests/fixtures/config-ts/mts/eslint.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
interface FakeFlatConfigItem {
plugins?: Record<string, unknown>;
name?: string;
rules?: Record<string, unknown>;
}

const config: FakeFlatConfigItem[] = [
{
rules: {
"no-undef": "error" as string
}
},
]

export default config;
17 changes: 17 additions & 0 deletions tests/fixtures/config-ts/top-level-await/eslint.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@

interface FakeFlatConfigItem {
plugins?: Record<string, unknown>;
name?: string;
rules?: Record<string, unknown>;
}

const config: FakeFlatConfigItem[] = [
// Top-level await
await Promise.resolve({
rules: {
"no-undef": "error" as string
}
}),
]

export default config;
5 changes: 5 additions & 0 deletions tests/fixtures/config-ts/ts-const-enum/enum.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const enum Level {
Error = 2,
Warn = 1,
Off = 0,
}
9 changes: 9 additions & 0 deletions tests/fixtures/config-ts/ts-const-enum/eslint.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Level } from "./enum.mts";

export default [
{
rules: {
"no-undef": Level.Error
}
},
] as const;
9 changes: 9 additions & 0 deletions tests/fixtures/config-ts/ts-namespace/eslint.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { LocalNamespace } from "./namespace.mts";

export default [
{
rules: {
"no-undef": LocalNamespace.Level.Error
}
},
] as const;
7 changes: 7 additions & 0 deletions tests/fixtures/config-ts/ts-namespace/namespace.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export namespace LocalNamespace {
export const enum Level {
Error = 2,
Warn = 1,
Off = 0,
}
}
15 changes: 15 additions & 0 deletions tests/fixtures/config-ts/ts/eslint.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
interface FakeFlatConfigItem {
plugins?: Record<string, unknown>;
name?: string;
rules?: Record<string, unknown>;
}

const config: FakeFlatConfigItem[] = [
{
rules: {
"no-undef": "error" as string
}
},
]

module.exports = config;
21 changes: 13 additions & 8 deletions tests/lib/cli-engine/lint-result-cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -163,16 +163,21 @@ describe("LintResultCache", () => {
it("contains node version during hashing", () => {
const version = "node-=-version";

sandbox.stub(process, "version").value(version);
const NewLintResultCache = proxyquire("../../../lib/cli-engine/lint-result-cache.js", {
"./hash": hashStub
});
const newLintResultCache = new NewLintResultCache(cacheFileLocation, "metadata");
const versionStub = sandbox.stub(process, "version").value(version);
antfu marked this conversation as resolved.
Show resolved Hide resolved

newLintResultCache.getCachedLintResults(filePath, fakeConfig);
try {
const NewLintResultCache = proxyquire("../../../lib/cli-engine/lint-result-cache.js", {
"./hash": hashStub
});
const newLintResultCache = new NewLintResultCache(cacheFileLocation, "metadata");

assert.ok(hashStub.calledOnce);
assert.ok(hashStub.calledWithMatch(version));
newLintResultCache.getCachedLintResults(filePath, fakeConfig);

assert.ok(hashStub.calledOnce);
assert.ok(hashStub.calledWithMatch(version));
} finally {
versionStub.restore();
}
});
});

Expand Down
Loading