Skip to content

Commit 2acdb2d

Browse files
committed
feat(utils): implement and test helper function to find nearest file
1 parent 4b889d6 commit 2acdb2d

File tree

3 files changed

+129
-4
lines changed

3 files changed

+129
-4
lines changed

packages/utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export {
1515
fileExists,
1616
filePathToCliArg,
1717
findLineNumberInText,
18+
findNearestFile,
1819
importModule,
1920
logMultipleFileResults,
2021
pluginWorkDir,

packages/utils/src/lib/file-system.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { bold, gray } from 'ansis';
22
import { type Options, bundleRequire } from 'bundle-require';
33
import { mkdir, readFile, readdir, rm, stat } from 'node:fs/promises';
4-
import { join } from 'node:path';
4+
import { dirname, join } from 'node:path';
55
import { formatBytes } from './formatting';
66
import { logMultipleResults } from './log-results';
77
import { ui } from './logging';
@@ -120,6 +120,27 @@ export async function crawlFileSystem<T = string>(
120120
return resultsNestedArray.flat() as T[];
121121
}
122122

123+
export async function findNearestFile(
124+
fileNames: string[],
125+
cwd = process.cwd(),
126+
): Promise<string | undefined> {
127+
// eslint-disable-next-line functional/no-loop-statements
128+
for (
129+
// eslint-disable-next-line functional/no-let
130+
let directory = cwd;
131+
directory !== dirname(directory);
132+
directory = dirname(directory)
133+
) {
134+
// eslint-disable-next-line functional/no-loop-statements
135+
for (const file of fileNames) {
136+
if (await fileExists(join(directory, file))) {
137+
return join(directory, file);
138+
}
139+
}
140+
}
141+
return undefined;
142+
}
143+
123144
export function findLineNumberInText(
124145
content: string,
125146
pattern: string,

packages/utils/src/lib/file-system.unit.test.ts

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
ensureDirectoryExists,
1010
filePathToCliArg,
1111
findLineNumberInText,
12+
findNearestFile,
1213
logMultipleFileResults,
1314
projectToFilename,
1415
} from './file-system';
@@ -57,9 +58,9 @@ describe('crawlFileSystem', () => {
5758
beforeEach(() => {
5859
vol.fromJSON(
5960
{
60-
['README.md']: '# Markdown',
61-
['src/README.md']: '# Markdown',
62-
['src/index.ts']: 'const var = "markdown";',
61+
'README.md': '# Markdown',
62+
'src/README.md': '# Markdown',
63+
'src/index.ts': 'const var = "markdown";',
6364
},
6465
MEMFS_VOLUME,
6566
);
@@ -110,6 +111,108 @@ describe('crawlFileSystem', () => {
110111
});
111112
});
112113

114+
describe('findNearestFile', () => {
115+
it('should find file in current working directory', async () => {
116+
vol.fromJSON(
117+
{
118+
'eslint.config.js': '',
119+
},
120+
MEMFS_VOLUME,
121+
);
122+
await expect(findNearestFile(['eslint.config.js'])).resolves.toBe(
123+
join(MEMFS_VOLUME, 'eslint.config.js'),
124+
);
125+
});
126+
127+
it('should find first matching file in array', async () => {
128+
vol.fromJSON(
129+
{
130+
'eslint.config.cjs': '',
131+
'eslint.config.mjs': '',
132+
},
133+
MEMFS_VOLUME,
134+
);
135+
await expect(
136+
findNearestFile([
137+
'eslint.config.js',
138+
'eslint.config.cjs',
139+
'eslint.config.mjs',
140+
]),
141+
).resolves.toBe(join(MEMFS_VOLUME, 'eslint.config.cjs'));
142+
});
143+
144+
it('should resolve to undefined if file not found', async () => {
145+
vol.fromJSON({ '.eslintrc.json': '' }, MEMFS_VOLUME);
146+
await expect(
147+
findNearestFile([
148+
'eslint.config.js',
149+
'eslint.config.cjs',
150+
'eslint.config.mjs',
151+
]),
152+
).resolves.toBeUndefined();
153+
});
154+
155+
it('should find file in parent directory', async () => {
156+
vol.fromJSON(
157+
{
158+
'eslint.config.js': '',
159+
'e2e/main.spec.js': '',
160+
},
161+
MEMFS_VOLUME,
162+
);
163+
await expect(
164+
findNearestFile(
165+
['eslint.config.js', 'eslint.config.cjs', 'eslint.config.mjs'],
166+
join(MEMFS_VOLUME, 'e2e'),
167+
),
168+
).resolves.toBe(join(MEMFS_VOLUME, 'eslint.config.js'));
169+
});
170+
171+
it('should find file in directory multiple levels up', async () => {
172+
vol.fromJSON(
173+
{
174+
'eslint.config.cjs': '',
175+
'packages/core/package.json': '',
176+
},
177+
MEMFS_VOLUME,
178+
);
179+
await expect(
180+
findNearestFile(
181+
['eslint.config.js', 'eslint.config.cjs', 'eslint.config.mjs'],
182+
join(MEMFS_VOLUME, 'packages/core'),
183+
),
184+
).resolves.toBe(join(MEMFS_VOLUME, 'eslint.config.cjs'));
185+
});
186+
187+
it("should find file that's nearest to current folder", async () => {
188+
vol.fromJSON(
189+
{
190+
'eslint.config.js': '',
191+
'packages/core/eslint.config.js': '',
192+
'packages/core/package.json': '',
193+
},
194+
MEMFS_VOLUME,
195+
);
196+
await expect(
197+
findNearestFile(
198+
['eslint.config.js', 'eslint.config.cjs', 'eslint.config.mjs'],
199+
join(MEMFS_VOLUME, 'packages/core'),
200+
),
201+
).resolves.toBe(join(MEMFS_VOLUME, 'packages/core/eslint.config.js'));
202+
});
203+
204+
it('should not find file in sub-folders of current folder', async () => {
205+
vol.fromJSON({ 'packages/core/eslint.config.js': '' }, MEMFS_VOLUME);
206+
await expect(
207+
findNearestFile([
208+
'eslint.config.js',
209+
'eslint.config.cjs',
210+
'eslint.config.mjs',
211+
]),
212+
).resolves.toBeUndefined();
213+
});
214+
});
215+
113216
describe('findLineNumberInText', () => {
114217
it('should return correct line number', () => {
115218
expect(

0 commit comments

Comments
 (0)