Skip to content

Commit

Permalink
Add Node based file finder and use on Windows
Browse files Browse the repository at this point in the history
The default file finder (findMatchingFiles) executes a `find ... | egrep
...` command to find files matching a variable name. This doesn't work
on Windows, so I'm exploring a native Node file finder as an
alternative.

I'm using glob [1], which is designed to work cross-platform.

From what I can tell, this is the only big thing to take care of in
order to make import-js work on Windows. I'm currently running tests on
the sublime plugin to make things work cross platforms, and my initial
tests are positive.

[1]: https://github.com/isaacs/node-glob
  • Loading branch information
trotzig committed May 28, 2016
1 parent d93f7e0 commit f8a6a4a
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 94 deletions.
174 changes: 96 additions & 78 deletions lib/__tests__/findMatchingFiles-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,103 +6,121 @@ import rimraf from 'rimraf';

import findMatchingFiles from '../findMatchingFiles';

describe('findMatchingFiles', () => {
const tmpDir = './tmp-findMatchingFiles';
let subject;
let word;
let existingFiles;
let lookupPath;

beforeEach(() => {
fs.mkdirSync(tmpDir);
word = 'foo';
existingFiles = [];
lookupPath = tmpDir;

subject = () => {
existingFiles.forEach((file) => {
const fullPath = `${tmpDir}/${file}`;
mkdirp.sync(path.dirname(fullPath));
fs.closeSync(fs.openSync(fullPath, 'w')); // create empty file
['node', 'find'].forEach((finder) => {
describe(`findMatchingFiles finder: ${finder}`, () => {
const tmpDir = './tmp-findMatchingFiles';
let subject;
let word;
let existingFiles;
let lookupPath;
let originalPlatform;

if (finder === 'node') {
beforeEach(() => {
originalPlatform = process.platform;
Object.defineProperty(process, 'platform', {
value: 'win32',
});
});

return findMatchingFiles(lookupPath, word);
};
});

afterEach((done) => {
rimraf(tmpDir, done);
});
afterEach(() => {
Object.defineProperty(process, 'platform', {
value: originalPlatform,
});
});
}

describe('when there are no matching files', () => {
beforeEach(() => {
existingFiles = [
'car/door.js',
];
fs.mkdirSync(tmpDir);
word = 'foo';
existingFiles = [];
lookupPath = tmpDir;

subject = () => {
existingFiles.forEach((file) => {
const fullPath = `${tmpDir}/${file}`;
mkdirp.sync(path.dirname(fullPath));
fs.closeSync(fs.openSync(fullPath, 'w')); // create empty file
});

return findMatchingFiles(lookupPath, word, finder);
};
});

it('returns an empty array', () => {
expect(subject()).toEqual([]);
afterEach((done) => {
rimraf(tmpDir, done);
});
});

describe('when the lookup path is empty', () => {
beforeEach(() => {
lookupPath = '';
});
describe('when there are no matching files', () => {
beforeEach(() => {
existingFiles = [
'car/door.js',
];
});

it('throws an error', () => {
expect(subject).toThrowError(/empty/);
it('returns an empty array', () => {
expect(subject()).toEqual([]);
});
});
});

describe('when there are multiple files matching word', () => {
beforeEach(() => {
existingFiles = [
'bar/foo.js',
'car/Foo.jsx',
'tzar/foo/index.js',
'car/door.js',
'har-har/foo/package.json',
'car/door/index.js',
'react/foo/index.jsx',
];
});
describe('when the lookup path is empty', () => {
beforeEach(() => {
lookupPath = '';
});

it('returns the matching files', () => {
expect(subject().sort()).toEqual([
`${tmpDir}/bar/foo.js`,
`${tmpDir}/car/Foo.jsx`,
`${tmpDir}/har-har/foo/package.json`,
`${tmpDir}/react/foo/index.jsx`,
`${tmpDir}/tzar/foo/index.js`,
]);
it('throws an error', () => {
expect(subject).toThrowError(/empty/);
});
});
});

describe('when the word has camelCase', () => {
beforeEach(() => {
word = 'FooBar';
});
describe('when there are multiple files matching word', () => {
beforeEach(() => {
existingFiles = [
'bar/foo.js',
'car/Foo.jsx',
'tzar/foo/index.js',
'car/door.js',
'har-har/foo/package.json',
'car/door/index.js',
'react/foo/index.jsx',
];
});

it('matches snake_case names', () => {
existingFiles = ['foo_bar.js'];
expect(subject()).toEqual([`${tmpDir}/foo_bar.js`]);
it('returns the matching files', () => {
expect(subject().sort()).toEqual([
`${tmpDir}/bar/foo.js`,
`${tmpDir}/car/Foo.jsx`,
`${tmpDir}/har-har/foo/package.json`,
`${tmpDir}/react/foo/index.jsx`,
`${tmpDir}/tzar/foo/index.js`,
]);
});
});

it('matches camelCase names', () => {
existingFiles = ['fooBar.js'];
expect(subject()).toEqual([`${tmpDir}/fooBar.js`]);
});
describe('when the word has camelCase', () => {
beforeEach(() => {
word = 'FooBar';
});

it('matches folder + file name', () => {
existingFiles = ['foo/Bar.js'];
expect(subject()).toEqual([`${tmpDir}/foo/Bar.js`]);
});
it('matches snake_case names', () => {
existingFiles = ['foo_bar.js'];
expect(subject()).toEqual([`${tmpDir}/foo_bar.js`]);
});

it('matches camelCase names', () => {
existingFiles = ['fooBar.js'];
expect(subject()).toEqual([`${tmpDir}/fooBar.js`]);
});

it('matches plural folder name + file name', () => {
existingFiles = ['foos/Bar.js'];
expect(subject()).toEqual([`${tmpDir}/foos/Bar.js`]);
it('matches folder + file name', () => {
existingFiles = ['foo/Bar.js'];
expect(subject()).toEqual([`${tmpDir}/foo/Bar.js`]);
});

it('matches plural folder name + file name', () => {
existingFiles = ['foos/Bar.js'];
expect(subject()).toEqual([`${tmpDir}/foos/Bar.js`]);
});
});
});
});
51 changes: 35 additions & 16 deletions lib/findMatchingFiles.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,20 @@
// @flow

import childProcess from 'child_process';
import glob from 'glob';

import formattedToRegex from './formattedToRegex';

/**
* Finds files from the local file system matching the variable name.
*/
export default function findMatchingFiles(
function findMatchingFilesWithFind(
lookupPath: string,
variableName: string
validFilesRegex: string
): Array<string> {
if (lookupPath === '') {
// If lookupPath is an empty string, the `find` command will not work
// as desired so we bail early.
throw new Error(`lookup path cannot be empty (${lookupPath})`);
}

const formattedVarName = formattedToRegex(variableName);
const egrepCommand =
`egrep -i \"(/|^)${formattedVarName}(/index)?(/package)?\.js.*\"`;

const findCommand = [
`find ${lookupPath}`,
'-name "**.js*"',
'-not -path "./node_modules/*"',
].join(' ');
const command = `${findCommand} | ${egrepCommand}`;
const command = `${findCommand} | egrep -i \"${validFilesRegex}\"`;

// TODO switch to spawn so we can start processing the stream as it comes
// in.
Expand All @@ -45,3 +33,34 @@ export default function findMatchingFiles(
// Filter out empty lines before returning
return out.split('\n').filter((file: string): boolean => !!file);
}

function findMatchingFilesWithNode(
lookupPath: string,
validFilesRegex: string
): Array<string> {
return glob.sync(`${lookupPath}/**/*.js*`, {
ignore: './node_modules/*',
}).filter((filePath: string): bool => new RegExp(validFilesRegex, 'i').test(filePath));
}

/**
* Finds files from the local file system matching the variable name.
*/
export default function findMatchingFiles(
lookupPath: string,
variableName: string
): Array<string> {
if (lookupPath === '') {
// If lookupPath is an empty string, the `find` command will not work
// as desired so we bail early.
throw new Error(`lookup path cannot be empty (${lookupPath})`);
}

const formattedVarName = formattedToRegex(variableName);
const validFilesRegex = `(/|^)${formattedVarName}(/index)?(/package)?\\.js.*`;

if (/^win/.test(process.platform)) {
return findMatchingFilesWithNode(lookupPath, validFilesRegex);
}
return findMatchingFilesWithFind(lookupPath, validFilesRegex);
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"StringScanner": "0.0.3",
"commander": "^2.9.0",
"eslint": "^2.8.0",
"glob": "^7.0.3",
"lodash.escaperegexp": "^4.1.1",
"lodash.flattendeep": "^4.1.0",
"lodash.partition": "^4.2.0",
Expand Down

0 comments on commit f8a6a4a

Please sign in to comment.