diff --git a/packages/typescript-eslint/package.json b/packages/typescript-eslint/package.json index f68c8c9991b7..6c0144e06b20 100644 --- a/packages/typescript-eslint/package.json +++ b/packages/typescript-eslint/package.json @@ -52,6 +52,7 @@ "dependencies": { "@typescript-eslint/eslint-plugin": "8.36.0", "@typescript-eslint/parser": "8.36.0", + "@typescript-eslint/typescript-estree": "8.36.0", "@typescript-eslint/utils": "8.36.0" }, "peerDependencies": { diff --git a/packages/typescript-eslint/src/getTSConfigRootDirFromStack.ts b/packages/typescript-eslint/src/getTSConfigRootDirFromStack.ts new file mode 100644 index 000000000000..01378b1618f3 --- /dev/null +++ b/packages/typescript-eslint/src/getTSConfigRootDirFromStack.ts @@ -0,0 +1,16 @@ +import { fileURLToPath } from 'node:url'; + +export function getTSConfigRootDirFromStack(stack: string): string | undefined { + for (const line of stack.split('\n').map(line => line.trim())) { + const candidate = /(\S+)eslint\.config\.(c|m)?(j|t)s/.exec(line)?.[1]; + if (!candidate) { + continue; + } + + return candidate.startsWith('file://') + ? fileURLToPath(candidate) + : candidate; + } + + return undefined; +} diff --git a/packages/typescript-eslint/src/index.ts b/packages/typescript-eslint/src/index.ts index 86870c855cd0..9892ede6065a 100644 --- a/packages/typescript-eslint/src/index.ts +++ b/packages/typescript-eslint/src/index.ts @@ -3,8 +3,10 @@ import type { TSESLint } from '@typescript-eslint/utils'; import pluginBase from '@typescript-eslint/eslint-plugin'; import rawPlugin from '@typescript-eslint/eslint-plugin/use-at-your-own-risk/raw-plugin'; +import { addCandidateTSConfigRootDir } from '@typescript-eslint/typescript-estree'; import { config } from './config-helper'; +import { getTSConfigRootDirFromStack } from './getTSConfigRootDirFromStack'; export const parser: TSESLint.FlatConfig.Parser = rawPlugin.parser; @@ -36,7 +38,7 @@ export const plugin: TSESLint.FlatConfig.Plugin = pluginBase as Omit< 'configs' >; -export const configs = { +export const configs = createConfigsGetters({ /** * Enables each the rules provided as a part of typescript-eslint. Note that many rules are not applicable in all codebases, or are meant to be configured. * @see {@link https://typescript-eslint.io/users/configs#all} @@ -120,7 +122,36 @@ export const configs = { */ stylisticTypeCheckedOnly: rawPlugin.flatConfigs['flat/stylistic-type-checked-only'], -}; +}); + +function createConfigsGetters(values: T): T { + const configs = {}; + + Object.defineProperties( + configs, + Object.fromEntries( + Object.entries(values).map(([key, value]: [string, unknown]) => [ + key, + { + enumerable: true, + get: () => { + const candidateRootDir = getTSConfigRootDirFromStack( + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + new Error().stack!, + ); + if (candidateRootDir) { + addCandidateTSConfigRootDir(candidateRootDir); + } + + return value; + }, + }, + ]), + ), + ); + + return configs as T; +} export type Config = TSESLint.FlatConfig.ConfigFile; diff --git a/packages/typescript-eslint/tests/configs.test.ts b/packages/typescript-eslint/tests/configs.test.ts index fea7a41196f4..cac7e4f46bd6 100644 --- a/packages/typescript-eslint/tests/configs.test.ts +++ b/packages/typescript-eslint/tests/configs.test.ts @@ -4,9 +4,25 @@ import type { } from '@typescript-eslint/utils/ts-eslint'; import rules from '@typescript-eslint/eslint-plugin/use-at-your-own-risk/rules'; +import { clearCandidateTSConfigRootDirs } from '@typescript-eslint/typescript-estree'; import tseslint from '../src/index.js'; +vi.mock('@typescript-eslint/typescript-estree', async () => ({ + ...(await vi.importActual('@typescript-eslint/typescript-estree')), + get addCandidateTSConfigRootDir() { + return mockAddCandidateTSConfigRootDir; + }, +})); + +const mockGetTSConfigRootDirFromStack = vi.fn(); + +vi.mock('../src/getTSConfigRootDirFromStack', () => ({ + get getTSConfigRootDirFromStack() { + return mockGetTSConfigRootDirFromStack; + }, +})); + const RULE_NAME_PREFIX = '@typescript-eslint/'; const EXTENSION_RULES = Object.entries(rules) .filter(([, rule]) => rule.meta.docs.extendsBaseRule) @@ -384,3 +400,36 @@ describe('stylistic-type-checked-only.ts', () => { }, ); }); + +const mockAddCandidateTSConfigRootDir = vi.fn(); + +describe('Candidate tsconfigRootDirs', () => { + beforeEach(() => { + clearCandidateTSConfigRootDirs(); + mockAddCandidateTSConfigRootDir.mockClear(); + }); + + describe.each(Object.keys(tseslint.configs))('%s', configKey => { + it('does not populate a candidate tsconfigRootDir when accessed and one cannot be inferred from the stack', () => { + mockGetTSConfigRootDirFromStack.mockReturnValue(undefined); + + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + tseslint.configs[configKey as keyof typeof tseslint.configs]; + + expect(mockAddCandidateTSConfigRootDir).not.toHaveBeenCalled(); + }); + + it('populates a candidate tsconfigRootDir when accessed and one can be inferred from the stack', () => { + const tsconfigRootDir = 'a/b/c/'; + + mockGetTSConfigRootDirFromStack.mockReturnValue(tsconfigRootDir); + + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + tseslint.configs[configKey as keyof typeof tseslint.configs]; + + expect(mockAddCandidateTSConfigRootDir).toHaveBeenCalledWith( + tsconfigRootDir, + ); + }); + }); +}); diff --git a/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts b/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts new file mode 100644 index 000000000000..57a3b0b5f2a3 --- /dev/null +++ b/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts @@ -0,0 +1,54 @@ +import path from 'node:path'; + +import { getTSConfigRootDirFromStack } from '../src/getTSConfigRootDirFromStack'; + +const isWindows = process.platform === 'win32'; + +describe(getTSConfigRootDirFromStack, () => { + it('returns undefined when no file path seems to be an ESLint config', () => { + const actual = getTSConfigRootDirFromStack( + [ + `Error`, + ' at file:///other.config.js', + ' at ModuleJob.run', + 'at async NodeHfs.walk(...)', + ].join('\n'), + ); + + expect(actual).toBeUndefined(); + }); + + it.runIf(!isWindows)( + 'returns a Posix config file path when a file:// path to an ESLint config is in the stack', + () => { + const actual = getTSConfigRootDirFromStack( + [ + `Error`, + ' at file:///path/to/file/eslint.config.js', + ' at ModuleJob.run', + 'at async NodeHfs.walk(...)', + ].join('\n'), + ); + + expect(actual).toBe('/path/to/file/'); + }, + ); + + it.each(['cjs', 'cts', 'js', 'mjs', 'mts', 'ts'])( + 'returns the path to the config file when its extension is %s', + extension => { + const expected = isWindows ? 'C:\\path\\to\\file\\' : '/path/to/file/'; + + const actual = getTSConfigRootDirFromStack( + [ + `Error`, + ` at ${path.join(expected, `eslint.config.${extension}`)}`, + ' at ModuleJob.run', + 'at async NodeHfs.walk(...)', + ].join('\n'), + ); + + expect(actual).toBe(expected); + }, + ); +}); diff --git a/packages/typescript-eslint/tsconfig.build.json b/packages/typescript-eslint/tsconfig.build.json index c41d1d4c30ed..8f377becd3cd 100644 --- a/packages/typescript-eslint/tsconfig.build.json +++ b/packages/typescript-eslint/tsconfig.build.json @@ -2,6 +2,9 @@ "extends": "../../tsconfig.build.json", "compilerOptions": {}, "references": [ + { + "path": "../typescript-estree/tsconfig.build.json" + }, { "path": "../utils/tsconfig.build.json" }, diff --git a/packages/typescript-eslint/tsconfig.json b/packages/typescript-eslint/tsconfig.json index d12794a533bf..f94f467e0b00 100644 --- a/packages/typescript-eslint/tsconfig.json +++ b/packages/typescript-eslint/tsconfig.json @@ -3,6 +3,9 @@ "files": [], "include": [], "references": [ + { + "path": "../typescript-estree" + }, { "path": "../utils" }, diff --git a/packages/typescript-estree/src/clear-caches.ts b/packages/typescript-estree/src/clear-caches.ts index 5e9867d3beab..3d5897953b70 100644 --- a/packages/typescript-estree/src/clear-caches.ts +++ b/packages/typescript-estree/src/clear-caches.ts @@ -3,6 +3,7 @@ import { clearDefaultProjectMatchedFiles, clearProgramCache as clearProgramCacheOriginal, } from './parser'; +import { clearCandidateTSConfigRootDirs } from './parseSettings/candidateTSConfigRootDirs'; import { clearTSConfigMatchCache, clearTSServerProjectService, @@ -17,6 +18,7 @@ import { clearGlobCache } from './parseSettings/resolveProjectList'; * - In custom lint tooling that iteratively lints one project at a time to prevent OOMs. */ export function clearCaches(): void { + clearCandidateTSConfigRootDirs(); clearDefaultProjectMatchedFiles(); clearProgramCacheOriginal(); clearWatchCaches(); diff --git a/packages/typescript-estree/src/index.ts b/packages/typescript-estree/src/index.ts index 39e921e8762a..654ea73982d2 100644 --- a/packages/typescript-estree/src/index.ts +++ b/packages/typescript-estree/src/index.ts @@ -16,6 +16,10 @@ export type { ParserServicesWithTypeInformation, TSESTreeOptions, } from './parser-options'; +export { + addCandidateTSConfigRootDir, + clearCandidateTSConfigRootDirs, +} from './parseSettings/candidateTSConfigRootDirs'; export { simpleTraverse } from './simple-traverse'; export * from './ts-estree'; export { typescriptVersionIsAtLeast } from './version-check'; diff --git a/packages/typescript-estree/src/parseSettings/candidateTSConfigRootDirs.ts b/packages/typescript-estree/src/parseSettings/candidateTSConfigRootDirs.ts new file mode 100644 index 000000000000..13e19c8aac8a --- /dev/null +++ b/packages/typescript-estree/src/parseSettings/candidateTSConfigRootDirs.ts @@ -0,0 +1,31 @@ +const candidateTSConfigRootDirs = new Set(); + +export function addCandidateTSConfigRootDir(candidate: string): void { + candidateTSConfigRootDirs.add(candidate); +} + +export function clearCandidateTSConfigRootDirs(): void { + candidateTSConfigRootDirs.clear(); +} + +export function getInferredTSConfigRootDir(): string { + const entries = [...candidateTSConfigRootDirs]; + + switch (entries.length) { + case 0: + return process.cwd(); + + case 1: + return entries[0]; + + default: + throw new Error( + [ + 'No tsconfigRootDir was set, and multiple candidate TSConfigRootDirs are present:', + ...entries.map(candidate => ` - ${candidate}`), + "You'll need to explicitly set tsconfigRootDir in your parser options.", + 'See: https://typescript-eslint.io/packages/parser/#tsconfigrootdir', + ].join('\n'), + ); + } +} diff --git a/packages/typescript-estree/src/parseSettings/createParseSettings.ts b/packages/typescript-estree/src/parseSettings/createParseSettings.ts index 678b771ac3a5..6710e36b870f 100644 --- a/packages/typescript-estree/src/parseSettings/createParseSettings.ts +++ b/packages/typescript-estree/src/parseSettings/createParseSettings.ts @@ -15,6 +15,7 @@ import type { MutableParseSettings } from './index'; import { ensureAbsolutePath } from '../create-program/shared'; import { validateDefaultProjectForFilesGlob } from '../create-program/validateDefaultProjectForFilesGlob'; import { isSourceFile } from '../source-files'; +import { getInferredTSConfigRootDir } from './candidateTSConfigRootDirs'; import { DEFAULT_TSCONFIG_CACHE_DURATION_SECONDS, ExpiringCache, @@ -52,7 +53,7 @@ export function createParseSettings( const tsconfigRootDir = typeof tsestreeOptions.tsconfigRootDir === 'string' ? tsestreeOptions.tsconfigRootDir - : process.cwd(); + : getInferredTSConfigRootDir(); const passedLoggerFn = typeof tsestreeOptions.loggerFn === 'function'; const filePath = ensureAbsolutePath( typeof tsestreeOptions.filePath === 'string' && diff --git a/packages/typescript-estree/tests/lib/candidateTSConfigRootDirs.test.ts b/packages/typescript-estree/tests/lib/candidateTSConfigRootDirs.test.ts new file mode 100644 index 000000000000..7536d56a205d --- /dev/null +++ b/packages/typescript-estree/tests/lib/candidateTSConfigRootDirs.test.ts @@ -0,0 +1,40 @@ +import { addCandidateTSConfigRootDir } from '../../src'; +import { + clearCandidateTSConfigRootDirs, + getInferredTSConfigRootDir, +} from '../../src/parseSettings/candidateTSConfigRootDirs'; + +describe(getInferredTSConfigRootDir, () => { + beforeEach(() => { + clearCandidateTSConfigRootDirs(); + }); + + it('returns process.cwd() when there are no candidates', () => { + const actual = getInferredTSConfigRootDir(); + + expect(actual).toBe(process.cwd()); + }); + + it('returns the candidate when there is one candidate', () => { + const candidate = 'a/b/c'; + addCandidateTSConfigRootDir(candidate); + + const actual = getInferredTSConfigRootDir(); + + expect(actual).toBe(candidate); + }); + + it('throws an error when there are multiple candidates', () => { + addCandidateTSConfigRootDir('a'); + addCandidateTSConfigRootDir('b'); + + expect(() => getInferredTSConfigRootDir()) + .toThrowErrorMatchingInlineSnapshot(` + [Error: No tsconfigRootDir was set, and multiple candidate TSConfigRootDirs are present: + - a + - b + You'll need to explicitly set tsconfigRootDir in your parser options. + See: https://typescript-eslint.io/packages/parser/#tsconfigrootdir] + `); + }); +}); diff --git a/packages/typescript-estree/tests/lib/createParseSettings.test.ts b/packages/typescript-estree/tests/lib/createParseSettings.test.ts index ccb210fd8dbd..d3f5fea99819 100644 --- a/packages/typescript-estree/tests/lib/createParseSettings.test.ts +++ b/packages/typescript-estree/tests/lib/createParseSettings.test.ts @@ -1,3 +1,7 @@ +import { + addCandidateTSConfigRootDir, + clearCandidateTSConfigRootDirs, +} from '../../src/parseSettings/candidateTSConfigRootDirs'; import { createParseSettings } from '../../src/parseSettings/createParseSettings'; const projectService = { service: true }; @@ -61,4 +65,36 @@ describe(createParseSettings, () => { ); }); }); + + describe('tsconfigRootDir', () => { + beforeEach(() => { + clearCandidateTSConfigRootDirs(); + }); + + it('uses the provided tsconfigRootDir when it exists and no candidates exist', () => { + const tsconfigRootDir = 'a/b/c'; + + const parseSettings = createParseSettings('', { tsconfigRootDir }); + + expect(parseSettings.tsconfigRootDir).toBe(tsconfigRootDir); + }); + + it('uses the provided tsconfigRootDir when it exists and a candidate exists', () => { + addCandidateTSConfigRootDir('candidate'); + const tsconfigRootDir = 'a/b/c'; + + const parseSettings = createParseSettings('', { tsconfigRootDir }); + + expect(parseSettings.tsconfigRootDir).toBe(tsconfigRootDir); + }); + + it('uses the inferred candidate when no tsconfigRootDir is provided and a candidate exists', () => { + const tsconfigRootDir = 'a/b/c'; + addCandidateTSConfigRootDir(tsconfigRootDir); + + const parseSettings = createParseSettings(''); + + expect(parseSettings.tsconfigRootDir).toBe(tsconfigRootDir); + }); + }); }); diff --git a/yarn.lock b/yarn.lock index a4ac6c4b95ec..08bde6338a88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19802,6 +19802,7 @@ __metadata: dependencies: "@typescript-eslint/eslint-plugin": 8.36.0 "@typescript-eslint/parser": 8.36.0 + "@typescript-eslint/typescript-estree": 8.36.0 "@typescript-eslint/utils": 8.36.0 "@vitest/coverage-v8": ^3.1.3 eslint: "*" pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy