From 85bb02e317f54dad4084b079bddeb63b32c12b72 Mon Sep 17 00:00:00 2001 From: Dominik Ferber Date: Thu, 1 Oct 2015 23:01:11 +0200 Subject: [PATCH] refactor(): support matching Meteor projects downwards Until now the path was only checked upwards for Meteor projects. Now it checks all children recursively. This enables having one common eslint configuration above multiple child directories containing Meteor applications. --- .meteor/release | 8 +++ lib/index.js | 10 ++-- lib/rules/no-session.js | 4 +- lib/util/getMeteorMeta.js | 21 +++++-- lib/util/getMeteorProjectRootPath.js | 19 ------- lib/util/getProjectRootPaths.js | 48 ++++++++++++++++ lib/util/isMeteorProject.js | 7 +++ package.json | 6 +- tests/lib/util/getMeteorMeta.js | 32 +++++++---- tests/lib/util/getMeteorProjectRootPath.js | 32 ----------- tests/lib/util/getProjectRootPaths.js | 64 ++++++++++++++++++++++ 11 files changed, 173 insertions(+), 78 deletions(-) create mode 100644 .meteor/release delete mode 100644 lib/util/getMeteorProjectRootPath.js create mode 100644 lib/util/getProjectRootPaths.js create mode 100644 lib/util/isMeteorProject.js delete mode 100644 tests/lib/util/getMeteorProjectRootPath.js create mode 100644 tests/lib/util/getProjectRootPaths.js diff --git a/.meteor/release b/.meteor/release new file mode 100644 index 0000000000..613e92781a --- /dev/null +++ b/.meteor/release @@ -0,0 +1,8 @@ +# This is a static file for test purposes. +# It simulates the npm package root as being a Meteor Project. +# +# When requiring lib/index.js (dist/index.js) in tests, that file tries to +# find out all possible Meteor project root directories based on cwd before +# returning the rules. +# To stop it from searching anywhere, the existence of this file simulates the +# npm package to be a Meteor project, so the search stops immediately. diff --git a/lib/index.js b/lib/index.js index 6a4ea3dc12..cf71da5131 100755 --- a/lib/index.js +++ b/lib/index.js @@ -1,13 +1,11 @@ -import getMeteorProjectRootPath from './util/getMeteorProjectRootPath.js' +import getProjectRootPaths from './util/getProjectRootPaths' +import isMeteorProject from './util/isMeteorProject' -// const getMeteorProjectRootPath = require('./util/getMeteorProjectRootPath.js'); - -const cwd = process.cwd() -const rootPath = getMeteorProjectRootPath(cwd) +const rootPaths = getProjectRootPaths(process.cwd(), isMeteorProject) function unpack (rule) { const packedRule = require(rule) - const unpackedRule = packedRule(rootPath) + const unpackedRule = packedRule(rootPaths) Object.keys(packedRule).map(function (key) { unpackedRule[key] = packedRule[key] }) diff --git a/lib/rules/no-session.js b/lib/rules/no-session.js index d4981e0eb9..2254e16696 100644 --- a/lib/rules/no-session.js +++ b/lib/rules/no-session.js @@ -9,8 +9,8 @@ // Rule Definition // ----------------------------------------------------------------------------- -module.exports = (/* rootPath */) => context => { - // const fileInfo = getMeteorMeta(rootPath, context.getFilename()) +module.exports = (/* rootPaths */) => context => { + // const fileInfo = getMeteorMeta(rootPaths, context.getFilename()) // console.log(fileInfo) // fileInfo is false => not in Meteor Project diff --git a/lib/util/getMeteorMeta.js b/lib/util/getMeteorMeta.js index 916b4992c2..30fb7dbccf 100644 --- a/lib/util/getMeteorMeta.js +++ b/lib/util/getMeteorMeta.js @@ -89,14 +89,27 @@ function getMeteorFileInfo (rootPath, filename) { } } -export default function getMeteorMeta (rootPath, filename) { +function hasFile (parent, filename) { + return filename.substr(0, parent.length) === parent +} - if (typeof rootPath === 'object') { +export default function getMeteorMeta (rootPaths, filename) { - // rule is in test-mode. return the given environment - return rootPath + if (!Array.isArray(rootPaths)) { + if (typeof rootPaths === 'object') { + + // rule is in test-mode. return the given environment + return rootPaths + } + + throw new Error('rootPath must be an Array') // or object for test-mode } + + const rootPath = rootPaths.find(function (currentPath) { + return hasFile(currentPath, filename) + }) + if (!rootPath) { // not in a Meteor Project diff --git a/lib/util/getMeteorProjectRootPath.js b/lib/util/getMeteorProjectRootPath.js deleted file mode 100644 index 6a28933af2..0000000000 --- a/lib/util/getMeteorProjectRootPath.js +++ /dev/null @@ -1,19 +0,0 @@ -import path from 'path' - -// must be require for rewire to work -var pathExists = require('path-exists') - -export default function getMeteorProjectRootPath (currentDirectory) { - - // No folder with '.meteor/release' in it found - if (currentDirectory === path.sep) { - return false - } - - const meteorPath = path.join(currentDirectory, '.meteor', 'release') - if (pathExists.sync(meteorPath)) { - return currentDirectory - } - - return getMeteorProjectRootPath(path.join(currentDirectory, '..')) -} diff --git a/lib/util/getProjectRootPaths.js b/lib/util/getProjectRootPaths.js new file mode 100644 index 0000000000..e903b67bad --- /dev/null +++ b/lib/util/getProjectRootPaths.js @@ -0,0 +1,48 @@ +import path from 'path' +var walk = require('walkdir') + +function findOneUpwards (currentDirectory, matcher, attempts = 0) { + + // No folder with '.meteor/release' in it found + if (attempts > 50 || currentDirectory === path.sep) { + return false + } + + if (matcher(currentDirectory)) { + return currentDirectory + } + + return findOneUpwards(path.join(currentDirectory, '..'), matcher, attempts + 1) +} + + +function findAllDownwards (startPath, matcher) { + const matchedPaths = [] + const options = {follow_symlinks: false, no_recurse: false, max_depth: 20} + walk.sync( + startPath, + options, + function (currentPath) { + if (matcher(currentPath)) { + matchedPaths.push(currentPath) + this.ignore(currentPath) + } + } + ) + return matchedPaths +} + + +export default function getProjectRootPaths (currentDirectory, matcher) { + + if (matcher(currentDirectory)) { + return [currentDirectory] + } + + const upwards = findOneUpwards(currentDirectory, matcher) + if (upwards) { + return [upwards] + } + + return findAllDownwards(currentDirectory, matcher) +} diff --git a/lib/util/isMeteorProject.js b/lib/util/isMeteorProject.js new file mode 100644 index 0000000000..de2d7a04d4 --- /dev/null +++ b/lib/util/isMeteorProject.js @@ -0,0 +1,7 @@ +import path from 'path' +import pathExists from 'path-exists' + +export default function isMeteorProject (currentDirectory) { + const meteorPath = path.join(currentDirectory, '.meteor', 'release') + return pathExists.sync(meteorPath) +} diff --git a/package.json b/package.json index 269eb77a61..7eb3e9ca44 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,7 @@ "prepublish": "npm run build", "semantic-release": "semantic-release pre && npm publish && semantic-release post", "test": "npm run lint && npm run build && npm run unit-test", - "unit-test": "istanbul cover --dir reports/coverage node_modules/mocha/bin/_mocha tests/**/*.js -- --reporter dot --compilers js:babel/register", - "unit-test:n": "node_modules/mocha/bin/_mocha tests/**/*.js --recursive --reporter dot --compilers js:babel/register" + "unit-test": "istanbul cover --dir reports/coverage node_modules/mocha/bin/_mocha tests/**/*.js -- --reporter dot --compilers js:babel/register" }, "files": [ "LICENSE", @@ -29,7 +28,8 @@ "bugs": "https://github.com/dferber90/eslint-plugin-meteor/issues", "dependencies": { "invariant": "2.1.1", - "path-exists": "2.0.0" + "path-exists": "2.0.0", + "walkdir": "0.0.10" }, "devDependencies": { "babel": "5.8.23", diff --git a/tests/lib/util/getMeteorMeta.js b/tests/lib/util/getMeteorMeta.js index ddd5a308b6..e19fa17e44 100644 --- a/tests/lib/util/getMeteorMeta.js +++ b/tests/lib/util/getMeteorMeta.js @@ -8,6 +8,7 @@ var getMeteorMeta = require('../../../dist/util/getMeteorMeta.js') const rootPath = path.join('User', 'anon', 'meteor-project') +const rootPaths = [rootPath] describe('getMeteorMeta', function () { it('has working rule test-mode', function () { @@ -15,9 +16,16 @@ describe('getMeteorMeta', function () { assert.equal(getMeteorMeta(someObj), someObj) }) + it('does not accept anything that is not an object or array', function () { + assert.throws(getMeteorMeta.bind(null), Error) + assert.throws(getMeteorMeta.bind(null, true), Error) + assert.throws(getMeteorMeta.bind(null, false), Error) + assert.throws(getMeteorMeta.bind(null, 2), Error) + }) + describe('when not in Meteor project', function () { it('returns default env', function () { - var result = getMeteorMeta(false, 'file.js') + var result = getMeteorMeta([], 'file.js') assert.equal(typeof result, 'object') assert.equal(result.isInMeteorProject, false) assert.equal(Object.keys(result).length, 1) @@ -27,7 +35,7 @@ describe('getMeteorMeta', function () { describe('in public', function () { it('detects the environment', function () { var filename = path.join(rootPath, 'public', 'file.js') - var result = getMeteorMeta(rootPath, filename) + var result = getMeteorMeta(rootPaths, filename) assert.equal(typeof result, 'object') assert.equal(result.path, 'public/file.js') assert.equal(result.env, ENVIRONMENT.PUBLIC) @@ -39,7 +47,7 @@ describe('getMeteorMeta', function () { describe('in private', function () { it('detects the environment', function () { var filename = path.join(rootPath, 'private', 'file.js') - var result = getMeteorMeta(rootPath, filename) + var result = getMeteorMeta(rootPaths, filename) assert.equal(typeof result, 'object') assert.equal(result.path, 'private/file.js') assert.equal(result.env, ENVIRONMENT.PRIVATE) @@ -51,7 +59,7 @@ describe('getMeteorMeta', function () { describe('in package', function () { it('detects the environment', function () { var filename = path.join(rootPath, 'packages', 'awesome-pkg', 'file.js') - var result = getMeteorMeta(rootPath, filename) + var result = getMeteorMeta(rootPaths, filename) assert.equal(typeof result, 'object') assert.equal(result.path, 'packages/awesome-pkg/file.js') assert.equal(result.env, ENVIRONMENT.PACKAGE) @@ -63,7 +71,7 @@ describe('getMeteorMeta', function () { describe('on no special folder', function () { it('has universal environment', function () { var filename = path.join(rootPath, 'file.js') - var result = getMeteorMeta(rootPath, filename) + var result = getMeteorMeta(rootPaths, filename) assert.equal(typeof result, 'object') assert.equal(result.path, 'file.js') assert.equal(result.env, ENVIRONMENT.UNIVERSAL) @@ -76,7 +84,7 @@ describe('getMeteorMeta', function () { it('returns file info', function () { var filename = path.join(rootPath, 'client', 'lib', 'file.js') - var result = getMeteorMeta(rootPath, filename) + var result = getMeteorMeta(rootPaths, filename) assert.equal(typeof result, 'object') assert.equal(result.path, 'client/lib/file.js') assert.equal(result.env, ENVIRONMENT.CLIENT) @@ -86,7 +94,7 @@ describe('getMeteorMeta', function () { it('does not detect compatibility when directly in client-folder ', function () { var filename = path.join(rootPath, 'client', 'file.js') - var result = getMeteorMeta(rootPath, filename) + var result = getMeteorMeta(rootPaths, filename) assert.equal(typeof result, 'object') assert.equal(result.path, 'client/file.js') assert.equal(result.env, ENVIRONMENT.CLIENT) @@ -96,7 +104,7 @@ describe('getMeteorMeta', function () { it('detects compatibility mode', function () { var filename = path.join(rootPath, 'client', 'compatibility', 'file.js') - var result = getMeteorMeta(rootPath, filename) + var result = getMeteorMeta(rootPaths, filename) assert.equal(typeof result, 'object') assert.equal(result.path, 'client/compatibility/file.js') assert.equal(result.env, ENVIRONMENT.CLIENT) @@ -108,7 +116,7 @@ describe('getMeteorMeta', function () { describe('on server', function () { it('detects the environment', function () { var filename = path.join(rootPath, 'server', 'file.js') - var result = getMeteorMeta(rootPath, filename) + var result = getMeteorMeta(rootPaths, filename) assert.equal(typeof result, 'object') assert.equal(result.path, 'server/file.js') assert.equal(result.env, ENVIRONMENT.SERVER) @@ -119,7 +127,7 @@ describe('getMeteorMeta', function () { describe('that is nested', function () { it('detects the environment', function () { var filename = path.join(rootPath, 'lib', 'server', 'file.js') - var result = getMeteorMeta(rootPath, filename) + var result = getMeteorMeta(rootPaths, filename) assert.equal(typeof result, 'object') assert.equal(result.path, 'lib/server/file.js') assert.equal(result.env, ENVIRONMENT.SERVER) @@ -131,7 +139,7 @@ describe('getMeteorMeta', function () { describe('in tests', function () { var filename = path.join(rootPath, 'tests', 'file.js') - var result = getMeteorMeta(rootPath, filename) + var result = getMeteorMeta(rootPaths, filename) assert.equal(typeof result, 'object') assert.equal(result.path, 'tests/file.js') assert.equal(result.env, ENVIRONMENT.TEST) @@ -141,7 +149,7 @@ describe('getMeteorMeta', function () { describe('in node_modules', function () { var filename = path.join(rootPath, 'node_modules', 'my-module', 'file.js') - var result = getMeteorMeta(rootPath, filename) + var result = getMeteorMeta(rootPaths, filename) assert.equal(typeof result, 'object') assert.equal(result.path, 'node_modules/my-module/file.js') assert.equal(result.env, ENVIRONMENT.NODE_MODULE) diff --git a/tests/lib/util/getMeteorProjectRootPath.js b/tests/lib/util/getMeteorProjectRootPath.js deleted file mode 100644 index 0abfa2c90a..0000000000 --- a/tests/lib/util/getMeteorProjectRootPath.js +++ /dev/null @@ -1,32 +0,0 @@ -/* eslint-env mocha */ - -var assert = require('assert') -var path = require('path') -var rewire = require('rewire') -var getMeteorProjectRootPath = rewire('../../../dist/util/getMeteorProjectRootPath.js') - - -var isInMeteorProject -getMeteorProjectRootPath.__set__('pathExists', { - sync: function () { - return isInMeteorProject - } -}) - - -describe('getMeteorProjectRootPath', function () { - it('returns false for top-level directory', function () { - assert.equal(getMeteorProjectRootPath(path.sep), false) - }) - - it('returns false when not in meteor project', function () { - isInMeteorProject = false - assert.equal(getMeteorProjectRootPath('/not-in-meteor/sub'), false) - }) - - it('returns the directory when in meteor project', function () { - isInMeteorProject = true - var meteorPath = '/Users/anon/meteor-project' - assert.equal(getMeteorProjectRootPath(meteorPath), meteorPath) - }) -}) diff --git a/tests/lib/util/getProjectRootPaths.js b/tests/lib/util/getProjectRootPaths.js new file mode 100644 index 0000000000..c751122588 --- /dev/null +++ b/tests/lib/util/getProjectRootPaths.js @@ -0,0 +1,64 @@ +/* eslint-env mocha */ + +var assert = require('assert') +var rewire = require('rewire') +var getProjectRootPaths = rewire('../../../dist/util/getProjectRootPaths.js') + +var walkShouldFindRoot +getProjectRootPaths.__set__('walk', { + sync: function (start, options, cb) { + var context = {ignore: function () {}} + if (walkShouldFindRoot) { + cb.call(context, '/User/anon/a/b/meteor-project') + cb.call(context, '/User/anon/a/b/c/meteor-project') + } else { + cb.call(context, '/User/otherguy') + cb.call(context, '/User/otherguy/a') + cb.call(context, '/User/otherguy/b') + cb.call(context, '/User/otherguy/a/b') + } + } +}) + + +function matcher (filename) { + return ( + filename === '/User/anon/a/b/meteor-project' || + filename === '/User/anon/a/b/c/meteor-project' + ) +} + +describe('getProjectRootPaths', function () { + + beforeEach(function () { + walkShouldFindRoot = true + }) + + it(`returns an empty array when no project is found`, function () { + walkShouldFindRoot = false + var result = getProjectRootPaths('/User/otherguy/', matcher) + assert.ok(Array.isArray(result)) + assert.equal(result.length, 0) + }) + + it(`returns the path when it's the same directory`, function () { + var result = getProjectRootPaths('/User/anon/a/b/meteor-project', matcher) + assert.equal(result.length, 1) + assert.equal(result[0], '/User/anon/a/b/meteor-project') + }) + + it(`returns a parent Meteor project`, function () { + var result = getProjectRootPaths('/User/anon/a/b/meteor-project/a/b/c', matcher) + + assert.equal(result.length, 1) + assert.equal(result[0], '/User/anon/a/b/meteor-project') + }) + + it(`returns all child Meteor projects`, function () { + var result = getProjectRootPaths('/User/anon', matcher) + + assert.equal(result.length, 2) + assert.equal(result[0], '/User/anon/a/b/meteor-project') + assert.equal(result[1], '/User/anon/a/b/c/meteor-project') + }) +})