Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: find files func in prep for auto detection #886

Merged
merged 1 commit into from
Dec 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) => {
gitphill marked this conversation as resolved.
Show resolved Hide resolved
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(
gitphill marked this conversation as resolved.
Show resolved Hide resolved
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.