Skip to content

Commit

Permalink
feat: find files func in prep for auto detection
Browse files Browse the repository at this point in the history
Added find-files module with find function to help search for files.
Added test for find and test fixtures.
Ignore node_modules in sub directories or as given path.

Co-authored-by: Lili Kastilio <lili@lilianakastilio.co.uk>
  • Loading branch information
gitphill and lili2311 committed Dec 6, 2019
1 parent e4ff909 commit 8ef2bd9
Show file tree
Hide file tree
Showing 17 changed files with 235 additions and 0 deletions.
109 changes: 109 additions & 0 deletions src/lib/find-files.ts
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);
}
123 changes: 123 additions & 0 deletions test/find-files.test.ts
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',
);
}
});
3 changes: 3 additions & 0 deletions test/fixtures/find-files/README.md
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.

0 comments on commit 8ef2bd9

Please sign in to comment.