From eac93601570a63b65676dba404fa038cd0e5f338 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Tue, 20 Feb 2024 03:21:48 -0600 Subject: [PATCH 01/19] Initial working implementation of TS config files --- lib/eslint/eslint.js | 211 ++++++++++++++++++++++++++++++++++++++++++- package.json | 3 +- 2 files changed, 212 insertions(+), 2 deletions(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 97102d3fe0e..c8fca0857b6 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -94,6 +94,9 @@ const LintResultCache = require("../cli-engine/lint-result-cache"); //------------------------------------------------------------------------------ const FLAT_CONFIG_FILENAMES = [ + "eslint.config.ts", + "eslint.config.mts", + "eslint.config.cts", "eslint.config.js", "eslint.config.mjs", "eslint.config.cjs" @@ -269,6 +272,206 @@ function findFlatConfigFile(cwd) { ); } +/** + * package cache key for `findNearestPackageData` + * @param basedir The directory to start searching from. + * @returns {string} The cache key. + */ +function getFnpdCacheKey(basedir) { + return `fnpd_${basedir}`; +} + +/** + * Get cached `findNearestPackageData` value based on `basedir`. When one is found, + * and we've already traversed some directories between `basedir` and `originalBasedir`, + * we cache the value for those in-between directories as well. + * + * This makes it so the fs is only read once for a shared `basedir`. + * @param {Map} packageCache The cache to use. + * @param {string} basedir The directory to start searching from. + * @param {string} originalBasedir The original directory to start searching from. + * @returns {any | undefined} The cached value or `undefined`. + */ +function getFnpdCache(packageCache, basedir, originalBasedir) { + const cacheKey = getFnpdCacheKey(basedir); + const pkgData = packageCache.get(cacheKey); + + if (pkgData) { + traverseBetweenDirs(originalBasedir, basedir, dir => { + packageCache.set(getFnpdCacheKey(dir), pkgData); + }); + return pkgData; + } +} + +/** + * Traverse between `longerDir` (inclusive) and `shorterDir` (exclusive) and call `cb` for each dir. + * @param {string} longerDir Longer dir path, e.g. `/User/foo/bar/baz` + * @param {string} shorterDir Shorter dir path, e.g. `/User/foo` + * @param {Function} cb Callback function to call for each dir. + * @returns {void} + */ +function traverseBetweenDirs( + longerDir, + shorterDir, + cb +) { + while (longerDir !== shorterDir) { + cb(longerDir); + longerDir = path.dirname(longerDir); + } +} + +/** + * Set cached `findNearestPackageData` value based on `basedir`. When one is found, + * and we've already traversed some directories between `basedir` and `originalBasedir`, + * we cache the value for those in-between directories as well. + * @param {Map} cache The cache to use. + * @param {any} basedir The package data to cache. + * @param {string} originalBasedir The original directory to start searching from. + * @returns {any} The cached value or `null`. + */ +function getCachedData( + cache, + basedir, + originalBasedir +) { + const pkgData = cache.get(getFnpdCacheKey(basedir)); + + if (pkgData) { + traverseBetweenDirs(originalBasedir, basedir, dir => { + cache.set(getFnpdCacheKey(dir), pkgData); + }); + return pkgData; + } + return null; +} + +/** + * Set cached `findNearestPackageData` value based on `basedir`. When one is found, + * @param {Map} cache The cache to use. + * @param {any} data The package data to cache. + * @param {string} basedir The directory to cache for. + * @param {string} originalBasedir The original directory to start searching from. + * @returns {void} The cached value or `null`. + */ +function setCacheData( + cache, + data, + basedir, + originalBasedir +) { + cache.set(getFnpdCacheKey(basedir), data); + traverseBetweenDirs(originalBasedir, basedir, dir => { + cache.set(getFnpdCacheKey(dir), data); + }); +} + +/** + * Find the nearest package data for a given directory. + * @param {string} basedir The directory to start searching from. + * @returns {Promise<{ type?: 'module' | 'commonjs' }>} The package data. + */ +async function findNearestPackageData(basedir) { + + /** + * @type {Map} + */ + const packageCache = new Map(); + + const originalBasedir = basedir; + + while (basedir) { + const cached = getCachedData(packageCache, basedir, originalBasedir); + + if (cached) { + return cached; + } + + const pkgPath = path.join(basedir, "package.json"); + + if ((await fs.stat(pkgPath).catch(() => {}))?.isFile()) { + const pkgData = JSON.parse(await fs.readFile(pkgPath, "utf8")); + + if (packageCache) { + setCacheData(packageCache, pkgData, basedir, originalBasedir); + } + + return pkgData; + } + + const nextBasedir = path.dirname(basedir); + + if (nextBasedir === basedir) { + break; + } + basedir = nextBasedir; + } + + return {}; +} + +/** + * 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"); + +} + +/** + * Set cached `findNearestPackageData` value based on `basedir`. When one is found, + * @param {string} filePath The file path to check. + * @returns {Promise} `true` if the file is an ESM file, `false` if it's a CJS file. + */ +async function isFilePathESM(filePath) { + if (!isFileTS(filePath)) { + return false; + } + const pathExtension = path.extname(filePath); + + if (pathExtension === ".mts") { + return true; + } + + if (pathExtension === ".cts") { + return false; + + } + + /* + * if (/\.m[jt]s$/u.test(filePath)) { + * return true; + * } + * if (/\.c[jt]s$/u.test(filePath)) { + * return false; + * } + */ + + // if (pathExtension === ".ts") { + + /* + * // check package.json for type: "module" + * try { + * const pkg = await findNearestPackageData(path.dirname(filePath)); + */ + + /* + * return pkg?.type === "module"; + * } catch { + * return false; + * } + */ + + // } + + +} + /** * Load the config array from the given filename. * @param {string} filePath The filename to load from. @@ -312,7 +515,13 @@ async function loadFlatConfigFile(filePath) { delete require.cache[filePath]; } - const config = (await import(fileURL)).default; + const isTS = isFileTS(filePath); + + if (isTS) { + await import("tsx"); + } + + const config = (await import(fileURL.href)).default?.default ?? (await import(fileURL.href)).default; importedConfigFileModificationTime.set(filePath, mtime); diff --git a/package.json b/package.json index 9cf566264fb..f1947fda1c3 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,8 @@ "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "text-table": "^0.2.0", + "tsx": "^4.7.1" }, "devDependencies": { "@babel/core": "^7.4.3", From 3833b5b119eae2bdb2934b9948318fa289aa54e3 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Tue, 20 Feb 2024 03:33:37 -0600 Subject: [PATCH 02/19] Simplify implementation --- lib/eslint/eslint.js | 189 ------------------------------------------- 1 file changed, 189 deletions(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index c8fca0857b6..2ea45441678 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -272,145 +272,6 @@ function findFlatConfigFile(cwd) { ); } -/** - * package cache key for `findNearestPackageData` - * @param basedir The directory to start searching from. - * @returns {string} The cache key. - */ -function getFnpdCacheKey(basedir) { - return `fnpd_${basedir}`; -} - -/** - * Get cached `findNearestPackageData` value based on `basedir`. When one is found, - * and we've already traversed some directories between `basedir` and `originalBasedir`, - * we cache the value for those in-between directories as well. - * - * This makes it so the fs is only read once for a shared `basedir`. - * @param {Map} packageCache The cache to use. - * @param {string} basedir The directory to start searching from. - * @param {string} originalBasedir The original directory to start searching from. - * @returns {any | undefined} The cached value or `undefined`. - */ -function getFnpdCache(packageCache, basedir, originalBasedir) { - const cacheKey = getFnpdCacheKey(basedir); - const pkgData = packageCache.get(cacheKey); - - if (pkgData) { - traverseBetweenDirs(originalBasedir, basedir, dir => { - packageCache.set(getFnpdCacheKey(dir), pkgData); - }); - return pkgData; - } -} - -/** - * Traverse between `longerDir` (inclusive) and `shorterDir` (exclusive) and call `cb` for each dir. - * @param {string} longerDir Longer dir path, e.g. `/User/foo/bar/baz` - * @param {string} shorterDir Shorter dir path, e.g. `/User/foo` - * @param {Function} cb Callback function to call for each dir. - * @returns {void} - */ -function traverseBetweenDirs( - longerDir, - shorterDir, - cb -) { - while (longerDir !== shorterDir) { - cb(longerDir); - longerDir = path.dirname(longerDir); - } -} - -/** - * Set cached `findNearestPackageData` value based on `basedir`. When one is found, - * and we've already traversed some directories between `basedir` and `originalBasedir`, - * we cache the value for those in-between directories as well. - * @param {Map} cache The cache to use. - * @param {any} basedir The package data to cache. - * @param {string} originalBasedir The original directory to start searching from. - * @returns {any} The cached value or `null`. - */ -function getCachedData( - cache, - basedir, - originalBasedir -) { - const pkgData = cache.get(getFnpdCacheKey(basedir)); - - if (pkgData) { - traverseBetweenDirs(originalBasedir, basedir, dir => { - cache.set(getFnpdCacheKey(dir), pkgData); - }); - return pkgData; - } - return null; -} - -/** - * Set cached `findNearestPackageData` value based on `basedir`. When one is found, - * @param {Map} cache The cache to use. - * @param {any} data The package data to cache. - * @param {string} basedir The directory to cache for. - * @param {string} originalBasedir The original directory to start searching from. - * @returns {void} The cached value or `null`. - */ -function setCacheData( - cache, - data, - basedir, - originalBasedir -) { - cache.set(getFnpdCacheKey(basedir), data); - traverseBetweenDirs(originalBasedir, basedir, dir => { - cache.set(getFnpdCacheKey(dir), data); - }); -} - -/** - * Find the nearest package data for a given directory. - * @param {string} basedir The directory to start searching from. - * @returns {Promise<{ type?: 'module' | 'commonjs' }>} The package data. - */ -async function findNearestPackageData(basedir) { - - /** - * @type {Map} - */ - const packageCache = new Map(); - - const originalBasedir = basedir; - - while (basedir) { - const cached = getCachedData(packageCache, basedir, originalBasedir); - - if (cached) { - return cached; - } - - const pkgPath = path.join(basedir, "package.json"); - - if ((await fs.stat(pkgPath).catch(() => {}))?.isFile()) { - const pkgData = JSON.parse(await fs.readFile(pkgPath, "utf8")); - - if (packageCache) { - setCacheData(packageCache, pkgData, basedir, originalBasedir); - } - - return pkgData; - } - - const nextBasedir = path.dirname(basedir); - - if (nextBasedir === basedir) { - break; - } - basedir = nextBasedir; - } - - return {}; -} - /** * Check if the file is a TypeScript file. * @param {string} filePath The file path to check. @@ -420,56 +281,6 @@ function isFileTS(filePath) { const fileExtension = path.extname(filePath); return fileExtension.endsWith("ts"); - -} - -/** - * Set cached `findNearestPackageData` value based on `basedir`. When one is found, - * @param {string} filePath The file path to check. - * @returns {Promise} `true` if the file is an ESM file, `false` if it's a CJS file. - */ -async function isFilePathESM(filePath) { - if (!isFileTS(filePath)) { - return false; - } - const pathExtension = path.extname(filePath); - - if (pathExtension === ".mts") { - return true; - } - - if (pathExtension === ".cts") { - return false; - - } - - /* - * if (/\.m[jt]s$/u.test(filePath)) { - * return true; - * } - * if (/\.c[jt]s$/u.test(filePath)) { - * return false; - * } - */ - - // if (pathExtension === ".ts") { - - /* - * // check package.json for type: "module" - * try { - * const pkg = await findNearestPackageData(path.dirname(filePath)); - */ - - /* - * return pkg?.type === "module"; - * } catch { - * return false; - * } - */ - - // } - - } /** From f196b6b4a7a74bfa3680dd82665407662d4f9552 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Tue, 27 Feb 2024 04:59:37 -0600 Subject: [PATCH 03/19] Use typescript's `transpile()` instead of `tsx` to avoid side effects --- lib/eslint/eslint.js | 23 ++++++++++++++++++++++- package.json | 2 +- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 2ea45441678..5bb2e3fec66 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -329,7 +329,28 @@ async function loadFlatConfigFile(filePath) { const isTS = isFileTS(filePath); if (isTS) { - await import("tsx"); + const ts = (await import("typescript")).default; + + const tsFileContent = await fs.readFile(filePath, "utf-8"); + + const tempFileName = `eslint.config.temp${path.extname(path.basename(filePath)).replace("t", "j")}`; + + const outputPath = path.join(path.dirname(filePath), tempFileName); + + const outputText = ts.transpile(tsFileContent, + { module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Node10, target: ts.ScriptTarget.ESNext }); + + try { + await fs.writeFile(outputPath, outputText, "utf-8"); + + const outputFileURL = pathToFileURL(outputPath); + + const config = (await import(outputFileURL.href)).default; + + return config; + } finally { + await fs.unlink(outputPath); + } } const config = (await import(fileURL.href)).default?.default ?? (await import(fileURL.href)).default; diff --git a/package.json b/package.json index 45259922ce0..cda7cbd36ba 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0", - "tsx": "^4.7.1" + "typescript": "^5.3.3" }, "devDependencies": { "@babel/core": "^7.4.3", From 00af2688369a04e66de595f33fba989c3de65245 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Tue, 5 Mar 2024 21:08:26 -0600 Subject: [PATCH 04/19] Remove unnecessary nullish coalescing --- lib/eslint/eslint.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index af156526689..25154dc1c0a 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -353,7 +353,7 @@ async function loadFlatConfigFile(filePath) { } } - const config = (await import(fileURL.href)).default?.default ?? (await import(fileURL.href)).default; + const config = (await import(fileURL.href)).default; importedConfigFileModificationTime.set(filePath, mtime); From 5ee0c4c46064f918af02ca8ee38a3a1e7627d198 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Tue, 5 Mar 2024 22:28:35 -0600 Subject: [PATCH 05/19] Use `jiti` instead of TypeScript --- lib/eslint/eslint.js | 23 +++-------------------- package.json | 4 ++-- 2 files changed, 5 insertions(+), 22 deletions(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 25154dc1c0a..21c327cb8e6 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -329,28 +329,11 @@ async function loadFlatConfigFile(filePath) { const isTS = isFileTS(filePath); if (isTS) { - const ts = (await import("typescript")).default; + const jiti = (await import("jiti")).default(__filename, { interopDefault: true }); - const tsFileContent = await fs.readFile(filePath, "utf-8"); + const config = jiti(fileURL.href); - const tempFileName = `eslint.config.temp${path.extname(path.basename(filePath)).replace("t", "j")}`; - - const outputPath = path.join(path.dirname(filePath), tempFileName); - - const outputText = ts.transpile(tsFileContent, - { module: ts.ModuleKind.ESNext, moduleResolution: ts.ModuleResolutionKind.Node10, target: ts.ScriptTarget.ESNext }); - - try { - await fs.writeFile(outputPath, outputText, "utf-8"); - - const outputFileURL = pathToFileURL(outputPath); - - const config = (await import(outputFileURL.href)).default; - - return config; - } finally { - await fs.unlink(outputPath); - } + return config; } const config = (await import(fileURL.href)).default; diff --git a/package.json b/package.json index cda7cbd36ba..caeed39b3a3 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", + "jiti": "^1.21.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -96,8 +97,7 @@ "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", - "text-table": "^0.2.0", - "typescript": "^5.3.3" + "text-table": "^0.2.0" }, "devDependencies": { "@babel/core": "^7.4.3", From c341646f9b276ec6234248795b1a828da8e07356 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Tue, 2 Apr 2024 06:09:39 -0500 Subject: [PATCH 06/19] Add missing section related to `importedConfigFileModificationTime` --- lib/eslint/eslint.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index e02efe397b9..286ce7a7485 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -334,6 +334,8 @@ async function loadFlatConfigFile(filePath) { const config = jiti(fileURL.href); + importedConfigFileModificationTime.set(filePath, mtime); + return config; } From 8614fd3d0c34bb8b3ff1501b92b1c0ce9f08ae62 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Tue, 2 Apr 2024 15:50:09 -0500 Subject: [PATCH 07/19] Enable `esmResolve` for `jiti` --- lib/eslint/eslint.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 286ce7a7485..4bbbc40defb 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -330,7 +330,7 @@ async function loadFlatConfigFile(filePath) { const isTS = isFileTS(filePath); if (isTS) { - const jiti = (await import("jiti")).default(__filename, { interopDefault: true }); + const jiti = (await import("jiti")).default(__filename, { interopDefault: true, esmResolve: true }); const config = jiti(fileURL.href); From b1627442f5fd9f5b35f6fb0cf7b467ae17f1ffc3 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Tue, 14 May 2024 05:17:56 -0500 Subject: [PATCH 08/19] Make `jiti` an optional peer dependency --- package.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 8f3de4a09b0..2eb746ca230 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,6 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "jiti": "^1.21.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -136,6 +135,7 @@ "globals": "^15.0.0", "got": "^11.8.3", "gray-matter": "^4.0.3", + "jiti": "^1.21.0", "js-yaml": "^4.1.0", "knip": "^5.8.0", "lint-staged": "^11.0.0", @@ -169,6 +169,14 @@ "webpack-cli": "^4.5.0", "yorkie": "^2.0.0" }, + "peerDependencies": { + "jiti": "^1.21.0" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + }, "keywords": [ "ast", "lint", From 8aca3ebebda642e46b6cf4536a9152de1f9b1283 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Tue, 14 May 2024 05:28:44 -0500 Subject: [PATCH 09/19] Throw an error if `jiti` is not installed --- lib/eslint/eslint.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 59c71d98076..a960710851b 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -330,6 +330,13 @@ async function loadFlatConfigFile(filePath) { const isTS = isFileTS(filePath); if (isTS) { + + try { + require.resolve("jiti"); + } catch { + throw new Error("'jiti' is required for loading TypeScript configuration files. Make sure to install it."); + } + const jiti = (await import("jiti")).default(__filename, { interopDefault: true, esmResolve: true }); const config = jiti(fileURL.href); From b5326670285c856532c4d7ad6932118473c3479a Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Tue, 14 May 2024 05:29:34 -0500 Subject: [PATCH 10/19] Add `jiti` to `ignoreDependencies` in `knip.jsonc` --- knip.jsonc | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/knip.jsonc b/knip.jsonc index 577086ba3cc..25b7681f524 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -36,7 +36,10 @@ "rollup-plugin-node-polyfills", // FIXME: not sure why is eslint-config-eslint reported as unused - "eslint-config-eslint" + "eslint-config-eslint", + + // Optional peer dependency used for loading TypeScript configuration files + "jiti" ] }, "docs": { From f339591ce3606a1c63fac63eb93a34427e787cc5 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 29 May 2024 07:18:06 -0500 Subject: [PATCH 11/19] Fix broken unit tests caused by stubbed `process.version` Co-authored-by: Anthony Fu --- tests/lib/cli-engine/lint-result-cache.js | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/lib/cli-engine/lint-result-cache.js b/tests/lib/cli-engine/lint-result-cache.js index 04dd49ca84d..d5d23e5e970 100644 --- a/tests/lib/cli-engine/lint-result-cache.js +++ b/tests/lib/cli-engine/lint-result-cache.js @@ -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); - 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(); + } }); }); From 8fa2ddb9870eb08750562c47200fb20ac2718e38 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 29 May 2024 07:37:44 -0500 Subject: [PATCH 12/19] Prioritize JavaScript config files over TypeScript config files --- lib/eslint/eslint.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 859dde8deb4..241e3908980 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -95,12 +95,12 @@ const { Retrier } = require("@humanwhocodes/retry"); //------------------------------------------------------------------------------ const FLAT_CONFIG_FILENAMES = [ - "eslint.config.ts", - "eslint.config.mts", - "eslint.config.cts", "eslint.config.js", "eslint.config.mjs", - "eslint.config.cjs" + "eslint.config.cjs", + "eslint.config.ts", + "eslint.config.mts", + "eslint.config.cts" ]; const debug = require("debug")("eslint:eslint"); const privateMembers = new WeakMap(); From 822baf70eb0ffcb62134373642e662432c0f6b83 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 29 May 2024 11:17:01 -0500 Subject: [PATCH 13/19] Add some basic tests for loading TS config files --- .../ts-config-files/cts/eslint.config.cts | 9 + tests/fixtures/ts-config-files/cts/foo.js | 1 + .../cts/with-type-commonjs/eslint.config.cts | 9 + .../cts/with-type-commonjs/foo.js | 1 + .../cts/with-type-commonjs/package.json | 5 + .../cts/with-type-module/eslint.config.cts | 9 + .../cts/with-type-module/foo.js | 1 + .../cts/with-type-module/package.json | 5 + .../ts-config-files/mts/eslint.config.mts | 9 + tests/fixtures/ts-config-files/mts/foo.js | 1 + .../mts/with-type-commonjs/eslint.config.mts | 9 + .../mts/with-type-commonjs/foo.js | 1 + .../mts/with-type-commonjs/package.json | 5 + .../mts/with-type-module/eslint.config.mts | 9 + .../mts/with-type-module/foo.js | 1 + .../mts/with-type-module/package.json | 5 + .../ts-config-files/ts/eslint.config.ts | 9 + tests/fixtures/ts-config-files/ts/foo.js | 1 + .../ts/with-type-commonjs/eslint.config.ts | 9 + .../ts/with-type-commonjs/foo.js | 1 + .../ts/with-type-commonjs/package.json | 5 + .../ts/with-type-module/eslint.config.ts | 9 + .../ts/with-type-module/foo.js | 1 + .../ts/with-type-module/package.json | 5 + tests/lib/eslint/eslint.js | 156 ++++++++++++++++++ 25 files changed, 276 insertions(+) create mode 100644 tests/fixtures/ts-config-files/cts/eslint.config.cts create mode 100644 tests/fixtures/ts-config-files/cts/foo.js create mode 100644 tests/fixtures/ts-config-files/cts/with-type-commonjs/eslint.config.cts create mode 100644 tests/fixtures/ts-config-files/cts/with-type-commonjs/foo.js create mode 100644 tests/fixtures/ts-config-files/cts/with-type-commonjs/package.json create mode 100644 tests/fixtures/ts-config-files/cts/with-type-module/eslint.config.cts create mode 100644 tests/fixtures/ts-config-files/cts/with-type-module/foo.js create mode 100644 tests/fixtures/ts-config-files/cts/with-type-module/package.json create mode 100644 tests/fixtures/ts-config-files/mts/eslint.config.mts create mode 100644 tests/fixtures/ts-config-files/mts/foo.js create mode 100644 tests/fixtures/ts-config-files/mts/with-type-commonjs/eslint.config.mts create mode 100644 tests/fixtures/ts-config-files/mts/with-type-commonjs/foo.js create mode 100644 tests/fixtures/ts-config-files/mts/with-type-commonjs/package.json create mode 100644 tests/fixtures/ts-config-files/mts/with-type-module/eslint.config.mts create mode 100644 tests/fixtures/ts-config-files/mts/with-type-module/foo.js create mode 100644 tests/fixtures/ts-config-files/mts/with-type-module/package.json create mode 100644 tests/fixtures/ts-config-files/ts/eslint.config.ts create mode 100644 tests/fixtures/ts-config-files/ts/foo.js create mode 100644 tests/fixtures/ts-config-files/ts/with-type-commonjs/eslint.config.ts create mode 100644 tests/fixtures/ts-config-files/ts/with-type-commonjs/foo.js create mode 100644 tests/fixtures/ts-config-files/ts/with-type-commonjs/package.json create mode 100644 tests/fixtures/ts-config-files/ts/with-type-module/eslint.config.ts create mode 100644 tests/fixtures/ts-config-files/ts/with-type-module/foo.js create mode 100644 tests/fixtures/ts-config-files/ts/with-type-module/package.json diff --git a/tests/fixtures/ts-config-files/cts/eslint.config.cts b/tests/fixtures/ts-config-files/cts/eslint.config.cts new file mode 100644 index 00000000000..c7bb00682c4 --- /dev/null +++ b/tests/fixtures/ts-config-files/cts/eslint.config.cts @@ -0,0 +1,9 @@ +interface FlatConfig { + rules: Record; +} + +module.exports = { + rules: { + "no-undef": "error", + }, +} satisfies FlatConfig; diff --git a/tests/fixtures/ts-config-files/cts/foo.js b/tests/fixtures/ts-config-files/cts/foo.js new file mode 100644 index 00000000000..e901f01b487 --- /dev/null +++ b/tests/fixtures/ts-config-files/cts/foo.js @@ -0,0 +1 @@ +foo; diff --git a/tests/fixtures/ts-config-files/cts/with-type-commonjs/eslint.config.cts b/tests/fixtures/ts-config-files/cts/with-type-commonjs/eslint.config.cts new file mode 100644 index 00000000000..c7bb00682c4 --- /dev/null +++ b/tests/fixtures/ts-config-files/cts/with-type-commonjs/eslint.config.cts @@ -0,0 +1,9 @@ +interface FlatConfig { + rules: Record; +} + +module.exports = { + rules: { + "no-undef": "error", + }, +} satisfies FlatConfig; diff --git a/tests/fixtures/ts-config-files/cts/with-type-commonjs/foo.js b/tests/fixtures/ts-config-files/cts/with-type-commonjs/foo.js new file mode 100644 index 00000000000..e901f01b487 --- /dev/null +++ b/tests/fixtures/ts-config-files/cts/with-type-commonjs/foo.js @@ -0,0 +1 @@ +foo; diff --git a/tests/fixtures/ts-config-files/cts/with-type-commonjs/package.json b/tests/fixtures/ts-config-files/cts/with-type-commonjs/package.json new file mode 100644 index 00000000000..890a9fc6b21 --- /dev/null +++ b/tests/fixtures/ts-config-files/cts/with-type-commonjs/package.json @@ -0,0 +1,5 @@ +{ + "private": true, + "name": "cts-config-with-commonjs-pkg-type", + "type": "commonjs" +} diff --git a/tests/fixtures/ts-config-files/cts/with-type-module/eslint.config.cts b/tests/fixtures/ts-config-files/cts/with-type-module/eslint.config.cts new file mode 100644 index 00000000000..c7bb00682c4 --- /dev/null +++ b/tests/fixtures/ts-config-files/cts/with-type-module/eslint.config.cts @@ -0,0 +1,9 @@ +interface FlatConfig { + rules: Record; +} + +module.exports = { + rules: { + "no-undef": "error", + }, +} satisfies FlatConfig; diff --git a/tests/fixtures/ts-config-files/cts/with-type-module/foo.js b/tests/fixtures/ts-config-files/cts/with-type-module/foo.js new file mode 100644 index 00000000000..e901f01b487 --- /dev/null +++ b/tests/fixtures/ts-config-files/cts/with-type-module/foo.js @@ -0,0 +1 @@ +foo; diff --git a/tests/fixtures/ts-config-files/cts/with-type-module/package.json b/tests/fixtures/ts-config-files/cts/with-type-module/package.json new file mode 100644 index 00000000000..d8a8ae0886a --- /dev/null +++ b/tests/fixtures/ts-config-files/cts/with-type-module/package.json @@ -0,0 +1,5 @@ +{ + "private": true, + "name": "cts-config-with-module-pkg-type", + "type": "module" +} diff --git a/tests/fixtures/ts-config-files/mts/eslint.config.mts b/tests/fixtures/ts-config-files/mts/eslint.config.mts new file mode 100644 index 00000000000..ad9097dbe85 --- /dev/null +++ b/tests/fixtures/ts-config-files/mts/eslint.config.mts @@ -0,0 +1,9 @@ +interface FlatConfig { + rules: Record; +} + +export default { + rules: { + "no-undef": "error", + }, +} satisfies FlatConfig; diff --git a/tests/fixtures/ts-config-files/mts/foo.js b/tests/fixtures/ts-config-files/mts/foo.js new file mode 100644 index 00000000000..e901f01b487 --- /dev/null +++ b/tests/fixtures/ts-config-files/mts/foo.js @@ -0,0 +1 @@ +foo; diff --git a/tests/fixtures/ts-config-files/mts/with-type-commonjs/eslint.config.mts b/tests/fixtures/ts-config-files/mts/with-type-commonjs/eslint.config.mts new file mode 100644 index 00000000000..c7bb00682c4 --- /dev/null +++ b/tests/fixtures/ts-config-files/mts/with-type-commonjs/eslint.config.mts @@ -0,0 +1,9 @@ +interface FlatConfig { + rules: Record; +} + +module.exports = { + rules: { + "no-undef": "error", + }, +} satisfies FlatConfig; diff --git a/tests/fixtures/ts-config-files/mts/with-type-commonjs/foo.js b/tests/fixtures/ts-config-files/mts/with-type-commonjs/foo.js new file mode 100644 index 00000000000..e901f01b487 --- /dev/null +++ b/tests/fixtures/ts-config-files/mts/with-type-commonjs/foo.js @@ -0,0 +1 @@ +foo; diff --git a/tests/fixtures/ts-config-files/mts/with-type-commonjs/package.json b/tests/fixtures/ts-config-files/mts/with-type-commonjs/package.json new file mode 100644 index 00000000000..4ad8c05d8d7 --- /dev/null +++ b/tests/fixtures/ts-config-files/mts/with-type-commonjs/package.json @@ -0,0 +1,5 @@ +{ + "private": true, + "name": "mts-config-with-commonjs-pkg-type", + "type": "commonjs" +} diff --git a/tests/fixtures/ts-config-files/mts/with-type-module/eslint.config.mts b/tests/fixtures/ts-config-files/mts/with-type-module/eslint.config.mts new file mode 100644 index 00000000000..c7bb00682c4 --- /dev/null +++ b/tests/fixtures/ts-config-files/mts/with-type-module/eslint.config.mts @@ -0,0 +1,9 @@ +interface FlatConfig { + rules: Record; +} + +module.exports = { + rules: { + "no-undef": "error", + }, +} satisfies FlatConfig; diff --git a/tests/fixtures/ts-config-files/mts/with-type-module/foo.js b/tests/fixtures/ts-config-files/mts/with-type-module/foo.js new file mode 100644 index 00000000000..e901f01b487 --- /dev/null +++ b/tests/fixtures/ts-config-files/mts/with-type-module/foo.js @@ -0,0 +1 @@ +foo; diff --git a/tests/fixtures/ts-config-files/mts/with-type-module/package.json b/tests/fixtures/ts-config-files/mts/with-type-module/package.json new file mode 100644 index 00000000000..1f1474be282 --- /dev/null +++ b/tests/fixtures/ts-config-files/mts/with-type-module/package.json @@ -0,0 +1,5 @@ +{ + "private": true, + "name": "mts-config-with-module-pkg-type", + "type": "module" +} diff --git a/tests/fixtures/ts-config-files/ts/eslint.config.ts b/tests/fixtures/ts-config-files/ts/eslint.config.ts new file mode 100644 index 00000000000..c7bb00682c4 --- /dev/null +++ b/tests/fixtures/ts-config-files/ts/eslint.config.ts @@ -0,0 +1,9 @@ +interface FlatConfig { + rules: Record; +} + +module.exports = { + rules: { + "no-undef": "error", + }, +} satisfies FlatConfig; diff --git a/tests/fixtures/ts-config-files/ts/foo.js b/tests/fixtures/ts-config-files/ts/foo.js new file mode 100644 index 00000000000..e901f01b487 --- /dev/null +++ b/tests/fixtures/ts-config-files/ts/foo.js @@ -0,0 +1 @@ +foo; diff --git a/tests/fixtures/ts-config-files/ts/with-type-commonjs/eslint.config.ts b/tests/fixtures/ts-config-files/ts/with-type-commonjs/eslint.config.ts new file mode 100644 index 00000000000..c7bb00682c4 --- /dev/null +++ b/tests/fixtures/ts-config-files/ts/with-type-commonjs/eslint.config.ts @@ -0,0 +1,9 @@ +interface FlatConfig { + rules: Record; +} + +module.exports = { + rules: { + "no-undef": "error", + }, +} satisfies FlatConfig; diff --git a/tests/fixtures/ts-config-files/ts/with-type-commonjs/foo.js b/tests/fixtures/ts-config-files/ts/with-type-commonjs/foo.js new file mode 100644 index 00000000000..e901f01b487 --- /dev/null +++ b/tests/fixtures/ts-config-files/ts/with-type-commonjs/foo.js @@ -0,0 +1 @@ +foo; diff --git a/tests/fixtures/ts-config-files/ts/with-type-commonjs/package.json b/tests/fixtures/ts-config-files/ts/with-type-commonjs/package.json new file mode 100644 index 00000000000..874ec15543f --- /dev/null +++ b/tests/fixtures/ts-config-files/ts/with-type-commonjs/package.json @@ -0,0 +1,5 @@ +{ + "private": true, + "name": "ts-config-with-commonjs-pkg-type", + "type": "commonjs" +} diff --git a/tests/fixtures/ts-config-files/ts/with-type-module/eslint.config.ts b/tests/fixtures/ts-config-files/ts/with-type-module/eslint.config.ts new file mode 100644 index 00000000000..c7bb00682c4 --- /dev/null +++ b/tests/fixtures/ts-config-files/ts/with-type-module/eslint.config.ts @@ -0,0 +1,9 @@ +interface FlatConfig { + rules: Record; +} + +module.exports = { + rules: { + "no-undef": "error", + }, +} satisfies FlatConfig; diff --git a/tests/fixtures/ts-config-files/ts/with-type-module/foo.js b/tests/fixtures/ts-config-files/ts/with-type-module/foo.js new file mode 100644 index 00000000000..e901f01b487 --- /dev/null +++ b/tests/fixtures/ts-config-files/ts/with-type-module/foo.js @@ -0,0 +1 @@ +foo; diff --git a/tests/fixtures/ts-config-files/ts/with-type-module/package.json b/tests/fixtures/ts-config-files/ts/with-type-module/package.json new file mode 100644 index 00000000000..1165cb71464 --- /dev/null +++ b/tests/fixtures/ts-config-files/ts/with-type-module/package.json @@ -0,0 +1,5 @@ +{ + "private": true, + "name": "ts-config-with-module-pkg-type", + "type": "module" +} diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index 0b5ca66f3d4..4303b2b80be 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -924,6 +924,162 @@ describe("ESLint", () => { assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); }); }); + + describe("TypeScript config files", () => { + it("should find and load eslint.config.ts when present", async () => { + + const cwd = getFixturePath("ts-config-files", "ts"); + + eslint = new ESLint({ + cwd + }); + + const results = await eslint.lintText("foo"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + + }); + + it('should load eslint.config.ts when we have "type": "commonjs" in nearest `package.json`', async () => { + + const cwd = getFixturePath("ts-config-files", "ts", "with-type-commonjs"); + + eslint = new ESLint({ + cwd + }); + + const results = await eslint.lintText("foo"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + + }); + + it('should load eslint.config.ts when we have "type": "module" in nearest `package.json`', async () => { + + const cwd = getFixturePath("ts-config-files", "ts", "with-type-module"); + + eslint = new ESLint({ + cwd + }); + + const results = await eslint.lintText("foo"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + + }); + + it("should find and load eslint.config.mts when present", async () => { + + const cwd = getFixturePath("ts-config-files", "mts"); + + eslint = new ESLint({ + cwd + }); + + const results = await eslint.lintText("foo"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + + }); + + it('should load eslint.config.mts when we have "type": "commonjs" in nearest `package.json`', async () => { + + const cwd = getFixturePath("ts-config-files", "mts", "with-type-commonjs"); + + eslint = new ESLint({ + cwd + }); + + const results = await eslint.lintText("foo"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + + }); + + it('should load eslint.config.mts config file when we have "type": "module" in nearest `package.json`', async () => { + + const cwd = getFixturePath("ts-config-files", "mts", "with-type-module"); + + eslint = new ESLint({ + cwd + }); + + const results = await eslint.lintText("foo"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + + }); + + it("should find and load eslint.config.cts when present", async () => { + + const cwd = getFixturePath("ts-config-files", "cts"); + + eslint = new ESLint({ + cwd + }); + + const results = await eslint.lintText("foo"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + + }); + + it('should load eslint.config.cts config file when we have "type": "commonjs" in nearest `package.json`', async () => { + + const cwd = getFixturePath("ts-config-files", "cts", "with-type-commonjs"); + + eslint = new ESLint({ + cwd + }); + + const results = await eslint.lintText("foo"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + + }); + + it('should load .cts config file when we have "type": "module" in nearest `package.json`', async () => { + + const cwd = getFixturePath("ts-config-files", "cts", "with-type-module"); + + eslint = new ESLint({ + cwd + }); + + const results = await eslint.lintText("foo"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + + }); + + }); }); describe("lintFiles()", () => { From 33f5ee6ef2308a5af47019a569dd780448b9bb04 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 29 May 2024 11:56:20 -0500 Subject: [PATCH 14/19] Fix minor type issues in JSDocs --- lib/eslint/eslint.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index 241e3908980..e15cfac7a5f 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -63,6 +63,9 @@ const { Retrier } = require("@humanwhocodes/retry"); /** @typedef {import("../shared/types").RuleConf} RuleConf */ /** @typedef {import("../shared/types").Rule} Rule */ /** @typedef {ReturnType} ExtractedConfig */ +/** @typedef {import('../cli-engine/cli-engine').CLIEngine} CLIEngine */ +/** @typedef {import('./legacy-eslint').CLIEngineLintReport} CLIEngineLintReport */ +/** @typedef {import("markdownlint").LintResults} LintResults */ /** * The options with which to configure the ESLint instance. @@ -85,7 +88,7 @@ const { Retrier } = require("@humanwhocodes/retry"); * when a string. * @property {Record} [plugins] An array of plugin implementations. * @property {boolean} [stats] True enables added statistics on lint results. - * @property {boolean} warnIgnored Show warnings when the file list includes ignored files + * @property {boolean} [warnIgnored] Show warnings when the file list includes ignored files * @property {boolean} [passOnNoPatterns=false] When set to true, missing patterns cause * the linting operation to short circuit and not report any failures. */ @@ -312,7 +315,7 @@ async function loadFlatConfigFile(filePath) { * to always reload the config file module) as that could cause memory leaks * because entries are never removed from the import cache. */ - fileURL.searchParams.append("mtime", mtime); + fileURL.searchParams.append("mtime", mtime.toString()); /* * With queries, we can bypass the import cache. However, when import-ing a CJS module, @@ -359,7 +362,7 @@ async function loadFlatConfigFile(filePath) { * as override config file is not explicitly set to `false`, it will search * upwards from the cwd for a file named `eslint.config.js`. * @param {import("./eslint").ESLintOptions} options The ESLint instance options. - * @returns {{configFilePath:string|undefined,basePath:string,error:Error|null}} Location information for + * @returns {Promise<{configFilePath:string|undefined;basePath:string;error:Error|null}>} Location information for * the config file. */ async function locateConfigFileToUse({ configFile, cwd }) { @@ -396,8 +399,8 @@ async function locateConfigFileToUse({ configFile, cwd }) { /** * Calculates the config array for this run based on inputs. * @param {ESLint} eslint The instance to create the config array for. - * @param {import("./eslint").ESLintOptions} options The ESLint instance options. - * @returns {FlatConfigArray} The config array for `eslint``. + * @param {ESLintOptions} options The ESLint instance options. + * @returns {Promise} The config array for `eslint``. */ async function calculateConfigArray(eslint, { cwd, @@ -1168,7 +1171,7 @@ class ESLint { /** * The main formatter method. - * @param {LintResults[]} results The lint results to format. + * @param {import("markdownlint").LintResults[]} results The lint results to format. * @param {ResultsMeta} resultsMeta Warning count and max threshold. * @returns {string} The formatted lint results. */ From 90657fa72e38f3dc96984adb0ae13762f3cbccd1 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Wed, 29 May 2024 12:05:01 -0500 Subject: [PATCH 15/19] Partially Inline the `FlatConfig` type from `@types/eslint` to reuse - This is not only done to reduce some potential redundancy down the line, but to ensure that TS config files are able to handle `type` imports as that is something most people are likely going to do. --- .../ts-config-files/cts/eslint.config.cts | 4 +-- .../cts/with-type-commonjs/eslint.config.cts | 4 +-- .../cts/with-type-module/eslint.config.cts | 4 +-- tests/fixtures/ts-config-files/helper.ts | 32 +++++++++++++++++++ .../ts-config-files/mts/eslint.config.mts | 4 +-- .../mts/with-type-commonjs/eslint.config.mts | 4 +-- .../mts/with-type-module/eslint.config.mts | 4 +-- .../ts-config-files/ts/eslint.config.ts | 4 +-- .../ts/with-type-commonjs/eslint.config.ts | 4 +-- .../ts/with-type-module/eslint.config.ts | 4 +-- 10 files changed, 41 insertions(+), 27 deletions(-) create mode 100644 tests/fixtures/ts-config-files/helper.ts diff --git a/tests/fixtures/ts-config-files/cts/eslint.config.cts b/tests/fixtures/ts-config-files/cts/eslint.config.cts index c7bb00682c4..e8e1186287e 100644 --- a/tests/fixtures/ts-config-files/cts/eslint.config.cts +++ b/tests/fixtures/ts-config-files/cts/eslint.config.cts @@ -1,6 +1,4 @@ -interface FlatConfig { - rules: Record; -} +import type { FlatConfig } from "../helper"; module.exports = { rules: { diff --git a/tests/fixtures/ts-config-files/cts/with-type-commonjs/eslint.config.cts b/tests/fixtures/ts-config-files/cts/with-type-commonjs/eslint.config.cts index c7bb00682c4..85e6d05fcc3 100644 --- a/tests/fixtures/ts-config-files/cts/with-type-commonjs/eslint.config.cts +++ b/tests/fixtures/ts-config-files/cts/with-type-commonjs/eslint.config.cts @@ -1,6 +1,4 @@ -interface FlatConfig { - rules: Record; -} +import type { FlatConfig } from "../../helper"; module.exports = { rules: { diff --git a/tests/fixtures/ts-config-files/cts/with-type-module/eslint.config.cts b/tests/fixtures/ts-config-files/cts/with-type-module/eslint.config.cts index c7bb00682c4..85e6d05fcc3 100644 --- a/tests/fixtures/ts-config-files/cts/with-type-module/eslint.config.cts +++ b/tests/fixtures/ts-config-files/cts/with-type-module/eslint.config.cts @@ -1,6 +1,4 @@ -interface FlatConfig { - rules: Record; -} +import type { FlatConfig } from "../../helper"; module.exports = { rules: { diff --git a/tests/fixtures/ts-config-files/helper.ts b/tests/fixtures/ts-config-files/helper.ts new file mode 100644 index 00000000000..50800d54a8a --- /dev/null +++ b/tests/fixtures/ts-config-files/helper.ts @@ -0,0 +1,32 @@ +export type RuleLevelAndOptions = Prepend< + Partial, + RuleLevel +>; + +export type StringSeverity = "off" | "warn" | "error"; + +export type Severity = 0 | 1 | 2; + +export type RuleLevel = Severity | StringSeverity; + +export type RuleEntry = + | RuleLevel + | RuleLevelAndOptions; + +export interface RulesRecord { + [rule: string]: RuleEntry; +} + +export interface FlatConfig { + name?: string; + files?: Array; + ignores?: string[]; + linterOptions?: { + noInlineConfig?: boolean; + reportUnusedDisableDirectives?: Severity | StringSeverity | boolean; + }; + processor?: string; + plugins?: Record; + rules?: Partial; + settings?: Record; +} diff --git a/tests/fixtures/ts-config-files/mts/eslint.config.mts b/tests/fixtures/ts-config-files/mts/eslint.config.mts index ad9097dbe85..e77f29d765c 100644 --- a/tests/fixtures/ts-config-files/mts/eslint.config.mts +++ b/tests/fixtures/ts-config-files/mts/eslint.config.mts @@ -1,6 +1,4 @@ -interface FlatConfig { - rules: Record; -} +import type { FlatConfig } from "../helper"; export default { rules: { diff --git a/tests/fixtures/ts-config-files/mts/with-type-commonjs/eslint.config.mts b/tests/fixtures/ts-config-files/mts/with-type-commonjs/eslint.config.mts index c7bb00682c4..85e6d05fcc3 100644 --- a/tests/fixtures/ts-config-files/mts/with-type-commonjs/eslint.config.mts +++ b/tests/fixtures/ts-config-files/mts/with-type-commonjs/eslint.config.mts @@ -1,6 +1,4 @@ -interface FlatConfig { - rules: Record; -} +import type { FlatConfig } from "../../helper"; module.exports = { rules: { diff --git a/tests/fixtures/ts-config-files/mts/with-type-module/eslint.config.mts b/tests/fixtures/ts-config-files/mts/with-type-module/eslint.config.mts index c7bb00682c4..85e6d05fcc3 100644 --- a/tests/fixtures/ts-config-files/mts/with-type-module/eslint.config.mts +++ b/tests/fixtures/ts-config-files/mts/with-type-module/eslint.config.mts @@ -1,6 +1,4 @@ -interface FlatConfig { - rules: Record; -} +import type { FlatConfig } from "../../helper"; module.exports = { rules: { diff --git a/tests/fixtures/ts-config-files/ts/eslint.config.ts b/tests/fixtures/ts-config-files/ts/eslint.config.ts index c7bb00682c4..e8e1186287e 100644 --- a/tests/fixtures/ts-config-files/ts/eslint.config.ts +++ b/tests/fixtures/ts-config-files/ts/eslint.config.ts @@ -1,6 +1,4 @@ -interface FlatConfig { - rules: Record; -} +import type { FlatConfig } from "../helper"; module.exports = { rules: { diff --git a/tests/fixtures/ts-config-files/ts/with-type-commonjs/eslint.config.ts b/tests/fixtures/ts-config-files/ts/with-type-commonjs/eslint.config.ts index c7bb00682c4..85e6d05fcc3 100644 --- a/tests/fixtures/ts-config-files/ts/with-type-commonjs/eslint.config.ts +++ b/tests/fixtures/ts-config-files/ts/with-type-commonjs/eslint.config.ts @@ -1,6 +1,4 @@ -interface FlatConfig { - rules: Record; -} +import type { FlatConfig } from "../../helper"; module.exports = { rules: { diff --git a/tests/fixtures/ts-config-files/ts/with-type-module/eslint.config.ts b/tests/fixtures/ts-config-files/ts/with-type-module/eslint.config.ts index c7bb00682c4..85e6d05fcc3 100644 --- a/tests/fixtures/ts-config-files/ts/with-type-module/eslint.config.ts +++ b/tests/fixtures/ts-config-files/ts/with-type-module/eslint.config.ts @@ -1,6 +1,4 @@ -interface FlatConfig { - rules: Record; -} +import type { FlatConfig } from "../../helper"; module.exports = { rules: { From 76c3d28272dd5ab10fcae010c41605b5812c17d2 Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Thu, 30 May 2024 22:00:29 -0500 Subject: [PATCH 16/19] Stringify `mtime` --- lib/eslint/eslint.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index e15cfac7a5f..ff9048c40c3 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -107,6 +107,10 @@ const FLAT_CONFIG_FILENAMES = [ ]; const debug = require("debug")("eslint:eslint"); const privateMembers = new WeakMap(); + +/** + * @type {Map} + */ const importedConfigFileModificationTime = new Map(); const removedFormatters = new Set([ "checkstyle", @@ -299,7 +303,7 @@ async function loadFlatConfigFile(filePath) { debug(`Config file URL is ${fileURL}`); - const mtime = (await fs.stat(filePath)).mtime.getTime(); + const mtime = (await fs.stat(filePath)).mtime.getTime().toString(); /* * Append a query with the config file's modification time (`mtime`) in order @@ -315,7 +319,7 @@ async function loadFlatConfigFile(filePath) { * to always reload the config file module) as that could cause memory leaks * because entries are never removed from the import cache. */ - fileURL.searchParams.append("mtime", mtime.toString()); + fileURL.searchParams.append("mtime", mtime); /* * With queries, we can bypass the import cache. However, when import-ing a CJS module, From b0e5f96399dbd1e8b4ead6ac1599ded360c79fed Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Fri, 31 May 2024 05:51:29 -0500 Subject: [PATCH 17/19] Check if ESLint is running in Deno or Bun Note: `isRunningInBun` and `isRunningInDeno` are functions to make treeshaking for consuming libraries easier. --- lib/eslint/eslint.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index ff9048c40c3..f71fc8449f2 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -291,6 +291,22 @@ function isFileTS(filePath) { return fileExtension.endsWith("ts"); } +/** + * Check if ESLint is running in Bun. + * @returns {boolean} `true` if the ESLint is running Bun, `false` if it's not. + */ +function isRunningInBun() { + return !!globalThis.Bun || !!globalThis.process?.versions?.bun; +} + +/** + * Check if ESLint is running in Deno. + * @returns {boolean} `true` if the ESLint is running in Deno, `false` if it's not. + */ +function isRunningInDeno() { + return !!globalThis.Deno; +} + /** * Load the config array from the given filename. * @param {string} filePath The filename to load from. @@ -336,7 +352,11 @@ async function loadFlatConfigFile(filePath) { const isTS = isFileTS(filePath); - if (isTS) { + const isBun = isRunningInBun(); + + const isDeno = isRunningInDeno(); + + if (isTS && !isDeno && !isBun) { try { require.resolve("jiti"); From 209c79169b681cebea35c7885233bbfdb1e861ae Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Fri, 31 May 2024 09:15:11 -0500 Subject: [PATCH 18/19] Try a different approach for loading TS config files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - This is done for mainly 2 reasons:   1. We don't know how many runtime environments are going to support loading TS files natively in the future, so this saves us having to check for every single one.   2. This also ensures that we give the user the option of passing their own TS loader of choice through `NODE_OPTIONS` in CLI for example: `NODE_OPTIONS=--import=tsx/esm`, without ESLint getting in the way and potentially causing conflicts between multiple loaders. --- lib/eslint/eslint.js | 57 +++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js index f71fc8449f2..fb411291c39 100644 --- a/lib/eslint/eslint.js +++ b/lib/eslint/eslint.js @@ -291,20 +291,22 @@ function isFileTS(filePath) { return fileExtension.endsWith("ts"); } -/** - * Check if ESLint is running in Bun. - * @returns {boolean} `true` if the ESLint is running Bun, `false` if it's not. - */ -function isRunningInBun() { - return !!globalThis.Bun || !!globalThis.process?.versions?.bun; -} +const unableToLoadTSFile = Symbol("Unable to load TS file"); /** - * Check if ESLint is running in Deno. - * @returns {boolean} `true` if the ESLint is running in Deno, `false` if it's not. + * Load the TypeScript config file. + * @param {string} filePath The file path to check. + * @returns {Promise<{ default?: any } | typeof unableToLoadTSFile>} The loaded TypeScript file or `false` if it's not loaded. */ -function isRunningInDeno() { - return !!globalThis.Deno; +async function loadTSFile(filePath) { + try { + return await import(filePath); + } catch (error) { + + debug(`${error}`); + + return unableToLoadTSFile; + } } /** @@ -350,30 +352,35 @@ async function loadFlatConfigFile(filePath) { delete require.cache[filePath]; } + let config; + const isTS = isFileTS(filePath); - const isBun = isRunningInBun(); + if (isTS) { - const isDeno = isRunningInDeno(); + const loadedTSFile = await loadTSFile(fileURL.href); - if (isTS && !isDeno && !isBun) { + if (loadedTSFile === unableToLoadTSFile) { + try { + require.resolve("jiti"); + } catch (error) { + debug(`${error}`); - try { - require.resolve("jiti"); - } catch { - throw new Error("'jiti' is required for loading TypeScript configuration files. Make sure to install it."); - } + throw new Error("'jiti' is required for loading TypeScript configuration files. Make sure to install it."); + } - const jiti = (await import("jiti")).default(__filename, { interopDefault: true, esmResolve: true }); + const jiti = (await import("jiti")).default(__filename, { interopDefault: true, esmResolve: true }); - const config = jiti(fileURL.href); + config = jiti(fileURL.href); - importedConfigFileModificationTime.set(filePath, mtime); + } else { + config = loadedTSFile?.default ?? loadedTSFile; + } - return config; - } + } else { + config = (await import(fileURL.href)).default; - const config = (await import(fileURL.href)).default; + } importedConfigFileModificationTime.set(filePath, mtime); From 61abc9bfc14b8f1da19075a5fb6ac2be257c4edb Mon Sep 17 00:00:00 2001 From: Arya Emami Date: Fri, 31 May 2024 09:30:20 -0500 Subject: [PATCH 19/19] Add tests for `lintFiles` with TS config files --- tests/lib/eslint/eslint.js | 156 +++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js index 4303b2b80be..e093b30ce7a 100644 --- a/tests/lib/eslint/eslint.js +++ b/tests/lib/eslint/eslint.js @@ -4868,6 +4868,162 @@ describe("ESLint", () => { }); }); + describe("TypeScript config files", () => { + it("should find and load eslint.config.ts when present", async () => { + + const cwd = getFixturePath("ts-config-files", "ts"); + + eslint = new ESLint({ + cwd + }); + + const results = await eslint.lintFiles("foo.js"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + + }); + + it('should load eslint.config.ts when we have "type": "commonjs" in nearest `package.json`', async () => { + + const cwd = getFixturePath("ts-config-files", "ts", "with-type-commonjs"); + + eslint = new ESLint({ + cwd + }); + + const results = await eslint.lintFiles("foo.js"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + + }); + + it('should load eslint.config.ts when we have "type": "module" in nearest `package.json`', async () => { + + const cwd = getFixturePath("ts-config-files", "ts", "with-type-module"); + + eslint = new ESLint({ + cwd + }); + + const results = await eslint.lintFiles("foo.js"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + + }); + + it("should find and load eslint.config.mts when present", async () => { + + const cwd = getFixturePath("ts-config-files", "mts"); + + eslint = new ESLint({ + cwd + }); + + const results = await eslint.lintFiles("foo.js"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + + }); + + it('should load eslint.config.mts when we have "type": "commonjs" in nearest `package.json`', async () => { + + const cwd = getFixturePath("ts-config-files", "mts", "with-type-commonjs"); + + eslint = new ESLint({ + cwd + }); + + const results = await eslint.lintFiles("foo.js"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + + }); + + it('should load eslint.config.mts config file when we have "type": "module" in nearest `package.json`', async () => { + + const cwd = getFixturePath("ts-config-files", "mts", "with-type-module"); + + eslint = new ESLint({ + cwd + }); + + const results = await eslint.lintFiles("foo.js"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + + }); + + it("should find and load eslint.config.cts when present", async () => { + + const cwd = getFixturePath("ts-config-files", "cts"); + + eslint = new ESLint({ + cwd + }); + + const results = await eslint.lintFiles("foo.js"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + + }); + + it('should load eslint.config.cts config file when we have "type": "commonjs" in nearest `package.json`', async () => { + + const cwd = getFixturePath("ts-config-files", "cts", "with-type-commonjs"); + + eslint = new ESLint({ + cwd + }); + + const results = await eslint.lintFiles("foo.js"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + + }); + + it('should load .cts config file when we have "type": "module" in nearest `package.json`', async () => { + + const cwd = getFixturePath("ts-config-files", "cts", "with-type-module"); + + eslint = new ESLint({ + cwd + }); + + const results = await eslint.lintFiles("foo.js"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + + }); + + }); + it("should stop linting files if a rule crashes", async () => { const cwd = getFixturePath("files");