-
Notifications
You must be signed in to change notification settings - Fork 551
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #886 from snyk/feat/helper-to-find-files
feat: find files func in prep for auto detection
- Loading branch information
Showing
17 changed files
with
235 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import * as fs from 'fs'; | ||
import * as pathLib from 'path'; | ||
// TODO: use util.promisify once we move to node 8 | ||
|
||
/** | ||
* Returns files inside given file path. | ||
* | ||
* @param path file path. | ||
*/ | ||
export async function readDirectory(path: string): Promise<string[]> { | ||
return await new Promise((resolve, reject) => { | ||
fs.readdir(path, (err, files) => { | ||
if (err) { | ||
reject(err); | ||
} | ||
resolve(files); | ||
}); | ||
}); | ||
} | ||
|
||
/** | ||
* Returns file stats object for given file path. | ||
* | ||
* @param path path to file or directory. | ||
*/ | ||
export async function getStats(path: string): Promise<fs.Stats> { | ||
return await new Promise((resolve, reject) => { | ||
fs.stat(path, (err, stats) => { | ||
if (err) { | ||
reject(err); | ||
} | ||
resolve(stats); | ||
}); | ||
}); | ||
} | ||
|
||
/** | ||
* Find all files in given search path. Returns paths to files found. | ||
* | ||
* @param path file path to search. | ||
* @param ignore (optional) files to ignore. Will always ignore node_modules. | ||
* @param filter (optional) file names to find. If not provided all files are returned. | ||
* @param levelsDeep (optional) how many levels deep to search, defaults to two, this path and one sub directory. | ||
*/ | ||
export async function find( | ||
path: string, | ||
ignore: string[] = [], | ||
filter: string[] = [], | ||
levelsDeep = 2, | ||
): Promise<string[]> { | ||
const found: string[] = []; | ||
// ensure we ignore find against node_modules path. | ||
if (path.endsWith('node_modules')) { | ||
return found; | ||
} | ||
// ensure node_modules is always ignored | ||
if (!ignore.includes('node_modules')) { | ||
ignore.push('node_modules'); | ||
} | ||
try { | ||
if (levelsDeep < 0) { | ||
return found; | ||
} else { | ||
levelsDeep--; | ||
} | ||
const fileStats = await getStats(path); | ||
if (fileStats.isDirectory()) { | ||
const files = await findInDirectory(path, ignore, filter, levelsDeep); | ||
found.push(...files); | ||
} else if (fileStats.isFile()) { | ||
const fileFound = findFile(path, filter); | ||
if (fileFound) { | ||
found.push(fileFound); | ||
} | ||
} | ||
return found; | ||
} catch (err) { | ||
throw new Error(`Error finding files in path '${path}'.\n${err.message}`); | ||
} | ||
} | ||
|
||
function findFile(path: string, filter: string[] = []): string | null { | ||
if (filter.length > 0) { | ||
const filename = pathLib.basename(path); | ||
if (filter.includes(filename)) { | ||
return path; | ||
} | ||
} else { | ||
return path; | ||
} | ||
return null; | ||
} | ||
|
||
async function findInDirectory( | ||
path: string, | ||
ignore: string[] = [], | ||
filter: string[] = [], | ||
levelsDeep = 2, | ||
): Promise<string[]> { | ||
const files = await readDirectory(path); | ||
const toFind = files | ||
.filter((file) => !ignore.includes(file)) | ||
.map((file) => { | ||
const resolvedPath = pathLib.resolve(path, file); | ||
return find(resolvedPath, ignore, filter, levelsDeep); | ||
}); | ||
const found = await Promise.all(toFind); | ||
return Array.prototype.concat.apply([], found); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import * as path from 'path'; | ||
import { test } from 'tap'; | ||
import { find } from '../src/lib/find-files'; | ||
|
||
const testFixture = path.join(__dirname, 'fixtures', 'find-files'); | ||
|
||
test('find all files in test fixture', async (t) => { | ||
// four levels deep to find all | ||
const result = await find(testFixture, [], [], 4); | ||
const expected = [ | ||
path.join(testFixture, 'README.md'), | ||
path.join(testFixture, 'gradle', 'build.gradle'), | ||
path.join(testFixture, 'gradle', 'subproject', 'build.gradle'), | ||
path.join(testFixture, 'maven', 'pom.xml'), | ||
path.join(testFixture, 'maven', 'test.txt'), | ||
path.join(testFixture, 'mvn', 'pom.xml'), | ||
path.join(testFixture, 'mvn', 'test.txt'), | ||
path.join(testFixture, 'npm', 'package.json'), | ||
path.join(testFixture, 'npm', 'test.txt'), | ||
path.join(testFixture, 'ruby', 'Gemfile'), | ||
path.join(testFixture, 'ruby', 'test.txt'), | ||
].sort(); | ||
t.same(result.sort(), expected, 'should return all files'); | ||
}); | ||
|
||
test('find all files in test fixture ignoring node_modules', async (t) => { | ||
// four levels deep to ensure node_modules is tested | ||
const result = await find(testFixture, ['node_modules'], [], 4); | ||
const expected = [ | ||
path.join(testFixture, 'README.md'), | ||
path.join(testFixture, 'gradle', 'build.gradle'), | ||
path.join(testFixture, 'gradle', 'subproject', 'build.gradle'), | ||
path.join(testFixture, 'maven', 'pom.xml'), | ||
path.join(testFixture, 'maven', 'test.txt'), | ||
path.join(testFixture, 'mvn', 'pom.xml'), | ||
path.join(testFixture, 'mvn', 'test.txt'), | ||
path.join(testFixture, 'npm', 'package.json'), | ||
path.join(testFixture, 'npm', 'test.txt'), | ||
path.join(testFixture, 'ruby', 'Gemfile'), | ||
path.join(testFixture, 'ruby', 'test.txt'), | ||
].sort(); | ||
t.same(result.sort(), expected, 'should return expected files'); | ||
}); | ||
|
||
test('find package.json file in test fixture ignoring node_modules', async (t) => { | ||
// four levels deep to ensure node_modules is tested | ||
const nodeModulesPath = path.join(testFixture, 'node_modules'); | ||
const result = await find(nodeModulesPath, [], ['package.json'], 4); | ||
const expected = []; | ||
t.same(result, expected, 'should return expected file'); | ||
}); | ||
|
||
test('find package.json file in test fixture (by default ignoring node_modules)', async (t) => { | ||
// four levels deep to ensure node_modules is tested | ||
const result = await find(testFixture, [], ['package.json'], 4); | ||
const expected = [path.join(testFixture, 'npm', 'package.json')]; | ||
t.same(result, expected, 'should return expected file'); | ||
}); | ||
|
||
test('find package.json file in test fixture (by default ignoring node_modules)', async (t) => { | ||
// four levels deep to ensure node_modules is tested | ||
const result = await find(testFixture, [], ['package.json'], 4); | ||
const expected = [path.join(testFixture, 'npm', 'package.json')]; | ||
t.same(result, expected, 'should return expected file'); | ||
}); | ||
|
||
test('find Gemfile file in test fixture', async (t) => { | ||
const result = await find(testFixture, [], ['Gemfile']); | ||
const expected = [path.join(testFixture, 'ruby', 'Gemfile')]; | ||
t.same(result, expected, 'should return expected file'); | ||
}); | ||
|
||
test('find pom.xml files in test fixture', async (t) => { | ||
const result = await find(testFixture, [], ['pom.xml']); | ||
const expected = [ | ||
path.join(testFixture, 'maven', 'pom.xml'), | ||
path.join(testFixture, 'mvn', 'pom.xml'), | ||
].sort(); | ||
t.same(result.sort(), expected, 'should return expected files'); | ||
}); | ||
|
||
test('find build.gradle, but stop at first build.gradle found', async (t) => { | ||
t.todo('stop recursion for given file names'); | ||
}); | ||
|
||
test('find path that does not exist', async (t) => { | ||
try { | ||
await find('does-not-exist'); | ||
t.fail('expected exception to be thrown'); | ||
} catch (err) { | ||
t.match( | ||
err.message, | ||
'Error finding files in path', | ||
'throws expected exception', | ||
); | ||
} | ||
}); | ||
|
||
test('find path is empty string', async (t) => { | ||
try { | ||
await find(''); | ||
t.fail('expected exception to be thrown'); | ||
} catch (err) { | ||
t.match( | ||
err.message, | ||
'Error finding files in path', | ||
'throws expected exception', | ||
); | ||
} | ||
}); | ||
|
||
test('find path is relative', async (t) => { | ||
try { | ||
await find('fixtures/find-files'); | ||
t.fail('expected exception to be thrown'); | ||
} catch (err) { | ||
t.match( | ||
err.message, | ||
'Error finding files in path', | ||
'throws expected exception', | ||
); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# find-files | ||
|
||
Files in this directory are used by `find-files.test.ts` to test `find-files.ts` functions. |
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.