diff --git a/.cspell.json b/.cspell.json index 265533fa045..ffe51471848 100644 --- a/.cspell.json +++ b/.cspell.json @@ -46,6 +46,7 @@ "\\(#.+?\\)" ], "words": [ + "AFAICT", "Airbnb", "Airbnb's", "allowdefaultproject", diff --git a/packages/typescript-eslint/src/getTSConfigRootDirFromStack.ts b/packages/typescript-eslint/src/getTSConfigRootDirFromStack.ts index b4ac708ea3b..1d4c558eabe 100644 --- a/packages/typescript-eslint/src/getTSConfigRootDirFromStack.ts +++ b/packages/typescript-eslint/src/getTSConfigRootDirFromStack.ts @@ -1,4 +1,5 @@ import path from 'node:path'; +import { fileURLToPath } from 'node:url'; /** * Infers the `tsconfigRootDir` from the current call stack, using the V8 API. @@ -26,11 +27,18 @@ export function getTSConfigRootDirFromStack(): string | undefined { } for (const callSite of getStack()) { - const stackFrameFilePath = callSite.getFileName(); - if (!stackFrameFilePath) { + const stackFrameFilePathOrUrl = callSite.getFileName(); + if (!stackFrameFilePathOrUrl) { continue; } + // ESM seem to return a file URL, so we'll convert it to a file path. + // AFAICT this isn't documented in the v8 API docs, but it seems to be the case. + // See https://github.com/typescript-eslint/typescript-eslint/issues/11429 + const stackFrameFilePath = stackFrameFilePathOrUrl.startsWith('file://') + ? fileURLToPath(stackFrameFilePathOrUrl) + : stackFrameFilePathOrUrl; + const parsedPath = path.parse(stackFrameFilePath); if (/^eslint\.config\.(c|m)?(j|t)s$/.test(parsedPath.base)) { return parsedPath.dir; diff --git a/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts b/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts index a9c169785c6..8f37d5833df 100644 --- a/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts +++ b/packages/typescript-eslint/tests/getTsconfigRootDirFromStack.test.ts @@ -3,11 +3,34 @@ import * as normalFolder from './path-test-fixtures/tsconfigRootDirInference-nor import * as notEslintConfig from './path-test-fixtures/tsconfigRootDirInference-not-eslint-config/not-an-eslint.config.cjs'; import * as folderThatHasASpace from './path-test-fixtures/tsconfigRootDirInference-space/folder that has a space/eslint.config.cjs'; +const isWindows = process.platform === 'win32'; + describe(getTSConfigRootDirFromStack, () => { it('does stack analysis right for normal folder', () => { expect(normalFolder.get()).toBe(normalFolder.dirname()); }); + it('does stack analysis right for a file that gives a file:// URL as its name', () => { + vi.spyOn(Error, 'captureStackTrace').mockImplementationOnce( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (target: any, _constructorOpt) => { + target.stack = [ + { + getFileName() { + return !isWindows + ? 'file:///a/b/eslint.config.mts' + : 'file:///F:/a/b/eslint.config.mts'; + }, + }, + ]; + }, + ); + + const inferredTsconfigRootDir = getTSConfigRootDirFromStack(); + + expect(inferredTsconfigRootDir).toBe(!isWindows ? '/a/b' : 'F:\\a\\b'); + }); + it('does stack analysis right for folder that has a space', () => { expect(folderThatHasASpace.get()).toBe(folderThatHasASpace.dirname()); });