Skip to content

Commit ac55ef5

Browse files
committed
Refactor working directory detection to prioritize lockfiles and follow ESLint best practices
1 parent d55c506 commit ac55ef5

File tree

1 file changed

+129
-43
lines changed

1 file changed

+129
-43
lines changed

server/src/eslint.ts

Lines changed: 129 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -799,30 +799,34 @@ export namespace ESLint {
799799
'javascript', 'javascriptreact'
800800
]);
801801

802-
const projectFolderIndicators: {
803-
fileName: string;
804-
isRoot: boolean;
805-
isFlatConfig: boolean;
806-
}[] = [
807-
{ fileName: 'eslint.config.js', isRoot: true, isFlatConfig: true },
808-
{ fileName: 'eslint.config.cjs', isRoot: true, isFlatConfig: true },
809-
{ fileName: 'eslint.config.mjs', isRoot: true, isFlatConfig: true },
810-
{ fileName: 'eslint.config.ts', isRoot: true, isFlatConfig: true },
811-
{ fileName: 'eslint.config.cts', isRoot: true, isFlatConfig: true },
812-
{ fileName: 'eslint.config.mts', isRoot: true, isFlatConfig: true },
813-
{ fileName: 'package-lock.json', isRoot: true, isFlatConfig: false },
814-
{ fileName: 'yarn.lock', isRoot: true, isFlatConfig: false },
815-
{ fileName: 'pnpm-lock.yaml', isRoot: true, isFlatConfig: false },
816-
{ fileName: 'npm-shrinkwrap.json', isRoot: true, isFlatConfig: false },
817-
{ fileName: 'bun.lockb', isRoot: true, isFlatConfig: false },
818-
{ fileName: 'bun.lock', isRoot: true, isFlatConfig: false },
819-
{ fileName: 'package.json', isRoot: false, isFlatConfig: false },
820-
{ fileName: '.eslintignore', isRoot: true, isFlatConfig: false },
821-
{ fileName: '.eslintrc', isRoot: false, isFlatConfig: false },
822-
{ fileName: '.eslintrc.json', isRoot: false, isFlatConfig: false },
823-
{ fileName: '.eslintrc.js', isRoot: false, isFlatConfig: false },
824-
{ fileName: '.eslintrc.yaml', isRoot: false, isFlatConfig: false },
825-
{ fileName: '.eslintrc.yml', isRoot: false, isFlatConfig: false }
802+
const flatConfigFiles = [
803+
'eslint.config.js',
804+
'eslint.config.cjs',
805+
'eslint.config.mjs',
806+
'eslint.config.ts',
807+
'eslint.config.cts',
808+
'eslint.config.mts'
809+
];
810+
811+
const legacyConfigFiles = [
812+
'.eslintrc',
813+
'.eslintrc.json',
814+
'.eslintrc.js',
815+
'.eslintrc.yaml',
816+
'.eslintrc.yml'
817+
];
818+
819+
const lockfileAndWorkspaceFiles = [
820+
'package-lock.json',
821+
'yarn.lock',
822+
'pnpm-lock.yaml',
823+
'npm-shrinkwrap.json',
824+
'bun.lockb',
825+
'pnpm-workspace.yaml',
826+
'.yarnrc.yml',
827+
'rush.json',
828+
'nx.json',
829+
'lerna.json'
826830
];
827831

828832
const path2Library: Map<string, ESLintModule> = new Map<string, ESLintModule>();
@@ -1296,6 +1300,104 @@ export namespace ESLint {
12961300
}
12971301
}
12981302

1303+
interface DirectoryIndicators {
1304+
directory: string;
1305+
flatConfigs: string[];
1306+
legacyConfigs: string[];
1307+
lockfiles: string[];
1308+
hasPackageJson: boolean;
1309+
}
1310+
1311+
function collectProjectIndicators(directory: string): DirectoryIndicators {
1312+
const indicators: DirectoryIndicators = {
1313+
directory,
1314+
flatConfigs: [],
1315+
legacyConfigs: [],
1316+
lockfiles: [],
1317+
hasPackageJson: false
1318+
};
1319+
1320+
for (const fileName of flatConfigFiles) {
1321+
if (fs.existsSync(path.join(directory, fileName))) {
1322+
indicators.flatConfigs.push(fileName);
1323+
}
1324+
}
1325+
1326+
for (const fileName of legacyConfigFiles) {
1327+
if (fs.existsSync(path.join(directory, fileName))) {
1328+
indicators.legacyConfigs.push(fileName);
1329+
}
1330+
}
1331+
1332+
for (const fileName of lockfileAndWorkspaceFiles) {
1333+
if (fs.existsSync(path.join(directory, fileName))) {
1334+
indicators.lockfiles.push(fileName);
1335+
}
1336+
}
1337+
1338+
if (fs.existsSync(path.join(directory, 'package.json'))) {
1339+
indicators.hasPackageJson = true;
1340+
}
1341+
1342+
return indicators;
1343+
}
1344+
1345+
function traverseUpwards(startDirectory: string, workspaceFolder: string): DirectoryIndicators[] {
1346+
const candidates: DirectoryIndicators[] = [];
1347+
let directory: string | undefined = startDirectory;
1348+
1349+
while (directory !== undefined && directory.startsWith(workspaceFolder)) {
1350+
const indicators = collectProjectIndicators(directory);
1351+
candidates.push(indicators);
1352+
1353+
const parent = path.dirname(directory);
1354+
directory = parent !== directory ? parent : undefined;
1355+
}
1356+
1357+
return candidates;
1358+
}
1359+
1360+
function selectWorkingDirectory(candidates: DirectoryIndicators[], workspaceFolder: string): [string, boolean] {
1361+
const lockfileRoot = candidates.find(c => c.lockfiles.length > 0);
1362+
1363+
const nearestFlatConfig = candidates.find(c => c.flatConfigs.length > 0);
1364+
1365+
// Find flat config at or above lockfile root (if lockfile exists)
1366+
const flatConfigAtOrAboveLockfile = lockfileRoot
1367+
? candidates.slice(candidates.indexOf(lockfileRoot)).find(c => c.flatConfigs.length > 0)
1368+
: undefined;
1369+
1370+
const uppermostPackageJson = [...candidates].reverse().find(c => c.hasPackageJson);
1371+
1372+
// Priority 1: Flat config at or above lockfile root (best practice)
1373+
if (lockfileRoot && flatConfigAtOrAboveLockfile) {
1374+
return [flatConfigAtOrAboveLockfile.directory, true];
1375+
}
1376+
1377+
// Priority 2: Lockfile root with legacy config
1378+
if (lockfileRoot && lockfileRoot.legacyConfigs.length > 0) {
1379+
return [lockfileRoot.directory, false];
1380+
}
1381+
1382+
// Priority 3: Lockfile root (dependency boundary)
1383+
if (lockfileRoot) {
1384+
return [lockfileRoot.directory, false];
1385+
}
1386+
1387+
// Priority 4: Any flat config (if no lockfile structure)
1388+
if (nearestFlatConfig) {
1389+
return [nearestFlatConfig.directory, true];
1390+
}
1391+
1392+
// Priority 5: Uppermost package.json (fallback for non-lockfile projects)
1393+
if (uppermostPackageJson) {
1394+
return [uppermostPackageJson.directory, false];
1395+
}
1396+
1397+
// Priority 6: Workspace folder
1398+
return [workspaceFolder, false];
1399+
}
1400+
12991401
export function findWorkingDirectory(workspaceFolder: string, file: string | undefined): [string, boolean] {
13001402
if (file === undefined || isUNC(file)) {
13011403
return [workspaceFolder, false];
@@ -1305,25 +1407,9 @@ export namespace ESLint {
13051407
return [workspaceFolder, false];
13061408
}
13071409

1308-
let result: string = workspaceFolder;
1309-
let flatConfig: boolean = false;
1310-
let directory: string | undefined = path.dirname(file);
1311-
outer: while (directory !== undefined && directory.startsWith(workspaceFolder)) {
1312-
for (const { fileName, isRoot, isFlatConfig } of projectFolderIndicators) {
1313-
if (fs.existsSync(path.join(directory, fileName))) {
1314-
result = directory;
1315-
flatConfig = isFlatConfig;
1316-
if (isRoot) {
1317-
break outer;
1318-
} else {
1319-
break;
1320-
}
1321-
}
1322-
}
1323-
const parent = path.dirname(directory);
1324-
directory = parent !== directory ? parent : undefined;
1325-
}
1326-
return [result, flatConfig];
1410+
const startDirectory = path.dirname(file);
1411+
const candidates = traverseUpwards(startDirectory, workspaceFolder);
1412+
return selectWorkingDirectory(candidates, workspaceFolder);
13271413
}
13281414

13291415
export namespace ErrorHandlers {

0 commit comments

Comments
 (0)