diff --git a/lib/index.js b/lib/index.js index c10b298b32..6a4ea3dc12 100755 --- a/lib/index.js +++ b/lib/index.js @@ -1,9 +1,25 @@ +import getMeteorProjectRootPath from './util/getMeteorProjectRootPath.js' + +// const getMeteorProjectRootPath = require('./util/getMeteorProjectRootPath.js'); + +const cwd = process.cwd() +const rootPath = getMeteorProjectRootPath(cwd) + +function unpack (rule) { + const packedRule = require(rule) + const unpackedRule = packedRule(rootPath) + Object.keys(packedRule).map(function (key) { + unpackedRule[key] = packedRule[key] + }) + return unpackedRule +} + module.exports = { rules: { - 'no-session': require('./rules/no-session'), - 'no-blaze-lifecycle-assignment': require('./rules/no-blaze-lifecycle-assignment'), - 'no-zero-timeout': require('./rules/no-zero-timeout'), - 'audit-argument-checks': require('./rules/audit-argument-checks') + 'no-session': unpack('./rules/no-session'), + 'no-blaze-lifecycle-assignment': unpack('./rules/no-blaze-lifecycle-assignment'), + 'no-zero-timeout': unpack('./rules/no-zero-timeout'), + 'audit-argument-checks': unpack('./rules/audit-argument-checks') }, rulesConfig: { 'no-session': 0, diff --git a/lib/rules/audit-argument-checks.js b/lib/rules/audit-argument-checks.js index 02b4cce8b3..f06c9eadcc 100644 --- a/lib/rules/audit-argument-checks.js +++ b/lib/rules/audit-argument-checks.js @@ -8,7 +8,7 @@ // ----------------------------------------------------------------------------- -module.exports = function (context) { +module.exports = () => (context) => { // --------------------------------------------------------------------------- // Helpers diff --git a/lib/rules/no-blaze-lifecycle-assignment.js b/lib/rules/no-blaze-lifecycle-assignment.js index 8766e819c0..3eaf271bce 100644 --- a/lib/rules/no-blaze-lifecycle-assignment.js +++ b/lib/rules/no-blaze-lifecycle-assignment.js @@ -7,7 +7,7 @@ // Rule Definition // ----------------------------------------------------------------------------- -module.exports = function (context) { +module.exports = () => (context) => { // --------------------------------------------------------------------------- // Helpers diff --git a/lib/rules/no-session.js b/lib/rules/no-session.js index 4ed5baedca..d4981e0eb9 100644 --- a/lib/rules/no-session.js +++ b/lib/rules/no-session.js @@ -3,15 +3,22 @@ * @author Dominik Ferber */ +// import getMeteorMeta from '../util/getMeteorMeta.js' + // ----------------------------------------------------------------------------- // Rule Definition // ----------------------------------------------------------------------------- -module.exports = function (context) { +module.exports = (/* rootPath */) => context => { + // const fileInfo = getMeteorMeta(rootPath, context.getFilename()) + // console.log(fileInfo) - // ------------------------------------------------------------------------- - // Public - // ------------------------------------------------------------------------- + // fileInfo is false => not in Meteor Project + // fileInfo => {env, path, isCompatibilityFile} + + // ------------------------------------------------------------------------- + // Public + // ------------------------------------------------------------------------- return { @@ -22,7 +29,6 @@ module.exports = function (context) { } } - } module.exports.schema = [] diff --git a/lib/rules/no-zero-timeout.js b/lib/rules/no-zero-timeout.js index 5189432969..21149bbea3 100644 --- a/lib/rules/no-zero-timeout.js +++ b/lib/rules/no-zero-timeout.js @@ -7,7 +7,7 @@ // Rule Definition // ----------------------------------------------------------------------------- -module.exports = function (context) { +module.exports = () => (context) => { // ------------------------------------------------------------------------- // Public diff --git a/lib/util/environment.js b/lib/util/environment.js new file mode 100644 index 0000000000..35775c7aa7 --- /dev/null +++ b/lib/util/environment.js @@ -0,0 +1,10 @@ +export default { + PUBLIC: '-public', + PRIVATE: '-private', + CLIENT: '-client', + SERVER: '-server', + PACKAGE: '-pkg', + TEST: '-test', + NODE_MODULE: '-node_module', + UNIVERSAL: '-universal' +} diff --git a/lib/util/folderNames.js b/lib/util/folderNames.js new file mode 100644 index 0000000000..db9b488522 --- /dev/null +++ b/lib/util/folderNames.js @@ -0,0 +1,10 @@ +export default { + PUBLIC: 'public', + PRIVATE: 'private', + CLIENT: 'client', + SERVER: 'server', + TESTS: 'tests', + PACKAGES: 'packages', + NODE_MODULES: 'node_modules', + COMPATIBILITY: 'compatibility' +} diff --git a/lib/util/getMeteorMeta.js b/lib/util/getMeteorMeta.js new file mode 100644 index 0000000000..916b4992c2 --- /dev/null +++ b/lib/util/getMeteorMeta.js @@ -0,0 +1,107 @@ +import path from 'path' +import invariant from 'invariant' +import ENVIRONMENT from './environment.js' +import folderNames from './folderNames.js' + +function matchFirst (dirs, list) { + for (let i = 0; i < dirs.length; i++) { + if (list.indexOf(dirs[i]) !== -1) { + return dirs[i] + } + } + return false +} + +function isCompatibilityMode (pathList) { + var clientIndex = pathList.indexOf(folderNames.CLIENT) + + // file is directly in client-folder, so it can't be in COMPATIBILITY + if (pathList.length - 2 === clientIndex) { + return false + } + + return pathList[clientIndex + 1] === folderNames.COMPATIBILITY +} + +function determineEnvironment (pathList) { + + if (pathList[0] === folderNames.PUBLIC) { + return ENVIRONMENT.PUBLIC + } + + if (pathList[0] === folderNames.PRIVATE) { + return ENVIRONMENT.PRIVATE + } + + if (pathList.length > 2 && pathList[0] === folderNames.PACKAGES) { + return ENVIRONMENT.PACKAGE + } + + const specialFolders = [ + folderNames.CLIENT, + folderNames.SERVER, + folderNames.TESTS, + folderNames.NODE_MODULES + ] + + // remove filename + const dirList = pathList.slice(0, -1) + const matchedEnvironment = matchFirst(dirList, specialFolders) + + switch (matchedEnvironment) { + case folderNames.CLIENT: + return ENVIRONMENT.CLIENT + case folderNames.SERVER: + return ENVIRONMENT.SERVER + case folderNames.TESTS: + return ENVIRONMENT.TEST + case folderNames.NODE_MODULES: + return ENVIRONMENT.NODE_MODULE + default: + return ENVIRONMENT.UNIVERSAL + } + +} + +function stripPathPrefix (parent, child) { + const normalizedParent = path.normalize(parent) + const normalizedChild = path.normalize(child) + + invariant( + normalizedChild.substr(0, normalizedParent.length) === parent, + 'Linted file is not in CWD' + ) + + // also strip the / at the end, which is not in normalizedParent + return normalizedChild.substr(normalizedParent.length + 1) +} + +function getMeteorFileInfo (rootPath, filename) { + const pathInProject = stripPathPrefix(rootPath, filename) + const pathList = pathInProject.split(path.sep) + const environment = determineEnvironment(pathList) + + return { + path: pathInProject, + env: environment, + isCompatibilityFile: environment === ENVIRONMENT.CLIENT && isCompatibilityMode(pathList), + isInMeteorProject: true + } +} + +export default function getMeteorMeta (rootPath, filename) { + + if (typeof rootPath === 'object') { + + // rule is in test-mode. return the given environment + return rootPath + } + + if (!rootPath) { + + // not in a Meteor Project + return {isInMeteorProject: false} + } + + return getMeteorFileInfo(rootPath, filename) +} diff --git a/lib/util/getMeteorProjectRootPath.js b/lib/util/getMeteorProjectRootPath.js new file mode 100644 index 0000000000..6a28933af2 --- /dev/null +++ b/lib/util/getMeteorProjectRootPath.js @@ -0,0 +1,19 @@ +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/package.json b/package.json index 6b5414f5c8..269eb77a61 100644 --- a/package.json +++ b/package.json @@ -4,17 +4,17 @@ "description": "Meteor specific linting rules for ESLint", "main": "dist/index.js", "scripts": { - "build": "babel lib -d dist", + "build": "babel lib -d dist --auxiliary-comment-before \"istanbul ignore next\"", "build:w": "npm run build -- -w", "clean": "rimraf dist", "coveralls": "cat ./reports/coverage/lcov.info | coveralls", "lint": "eslint ./", "prebuild": "npm run clean && mkdir dist", "prepublish": "npm run build", - "preunit-test": "npm run build", "semantic-release": "semantic-release pre && npm publish && semantic-release post", - "test": "npm run lint && npm run unit-test", - "unit-test": "istanbul cover --dir reports/coverage node_modules/mocha/bin/_mocha tests/**/*.js -- --reporter dot --compilers js:babel/register" + "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" }, "files": [ "LICENSE", @@ -27,18 +27,23 @@ }, "homepage": "https://github.com/dferber90/eslint-plugin-meteor", "bugs": "https://github.com/dferber90/eslint-plugin-meteor/issues", - "dependencies": {}, + "dependencies": { + "invariant": "2.1.1", + "path-exists": "2.0.0" + }, "devDependencies": { "babel": "5.8.23", + "babel-eslint": "4.1.3", "coveralls": "2.11.4", "cz-conventional-changelog": "1.0.1", "eslint": "1.5.1", - "babel-eslint": "4.1.3", "ghooks": "0.3.2", "istanbul": "0.3.21", "mocha": "2.3.3", + "rewire": "2.3.4", + "rimraf": "2.4.3", "semantic-release": "4.3.5", - "rimraf": "2.4.3" + "validate-commit-msg": "1.0.0" }, "peerDependencies": { "eslint": ">=0.8.0" diff --git a/tests/index.js b/tests/index.js index bdfaeaa22d..953e2f2a10 100755 --- a/tests/index.js +++ b/tests/index.js @@ -18,10 +18,10 @@ var defaultSettings = {} describe('all rule files should be exported by the plugin', function() { rules.forEach(function(ruleName) { - it('should export ' + ruleName, function() { + it('should export ' + ruleName, function () { assert.equal( - plugin.rules[ruleName], - require(path.join('../dist/rules', ruleName)) + typeof plugin.rules[ruleName], + 'function' ) }) diff --git a/tests/lib/rules/audit-argument-checks.js b/tests/lib/rules/audit-argument-checks.js index 6f47be51d8..80993d4f78 100644 --- a/tests/lib/rules/audit-argument-checks.js +++ b/tests/lib/rules/audit-argument-checks.js @@ -16,7 +16,7 @@ var RuleTester = require('eslint').RuleTester // ----------------------------------------------------------------------------- var ruleTester = new RuleTester() -ruleTester.run('audit-argument-checks', rule, { +ruleTester.run('audit-argument-checks', rule(), { valid: [ 'foo()', diff --git a/tests/lib/rules/no-blaze-lifecycle-assignment.js b/tests/lib/rules/no-blaze-lifecycle-assignment.js index b39b4caa98..c071aba49e 100644 --- a/tests/lib/rules/no-blaze-lifecycle-assignment.js +++ b/tests/lib/rules/no-blaze-lifecycle-assignment.js @@ -18,7 +18,7 @@ var RuleTester = require('eslint').RuleTester // ----------------------------------------------------------------------------- var ruleTester = new RuleTester() -ruleTester.run('no-blaze-lifecycle-assignment', rule, { +ruleTester.run('no-blaze-lifecycle-assignment', rule(), { valid: [ 'x += 1', diff --git a/tests/lib/rules/no-session.js b/tests/lib/rules/no-session.js index 8bd5672539..e20feabe1f 100644 --- a/tests/lib/rules/no-session.js +++ b/tests/lib/rules/no-session.js @@ -9,16 +9,53 @@ // Requirements // ----------------------------------------------------------------------------- +import {SERVER, CLIENT} from '../../../dist/util/environment.js' var rule = require('../../../dist/rules/no-session') var RuleTester = require('eslint').RuleTester +// ----------------------------------------------------------------------------- +// Environments +// ----------------------------------------------------------------------------- + +const serverEnv = { + path: 'server/methods.js', + env: SERVER, + isCompatibilityFile: false, + isInMeteorProject: true +} + +const clientEnv = { + path: 'server/methods.js', + env: CLIENT, + isCompatibilityFile: false, + isInMeteorProject: true +} + + // ----------------------------------------------------------------------------- // Tests // ----------------------------------------------------------------------------- + var ruleTester = new RuleTester() -ruleTester.run('no-session', rule, { +ruleTester.run('no-session', rule(serverEnv), { + + valid: [ + 'session.get("foo")', + 'foo(Session)' + ], + + invalid: [ + {code: 'Session.set("foo", true)', errors: [{message: 'Unexpected Session statement.', type: 'MemberExpression'}]}, + {code: 'Session.get("foo")', errors: [{message: 'Unexpected Session statement.', type: 'MemberExpression'}]}, + {code: 'Session.clear("foo")', errors: [{message: 'Unexpected Session statement.', type: 'MemberExpression'}]}, + {code: 'Session.all()', errors: [{message: 'Unexpected Session statement.', type: 'MemberExpression'}]} + ] + +}) + +ruleTester.run('no-session', rule(clientEnv), { valid: [ 'session.get("foo")', diff --git a/tests/lib/rules/no-zero-timeout.js b/tests/lib/rules/no-zero-timeout.js index 53c33524b9..f7ffcb6274 100644 --- a/tests/lib/rules/no-zero-timeout.js +++ b/tests/lib/rules/no-zero-timeout.js @@ -16,7 +16,7 @@ var RuleTester = require('eslint').RuleTester // ----------------------------------------------------------------------------- var ruleTester = new RuleTester() -ruleTester.run('no-zero-timeout', rule, { +ruleTester.run('no-zero-timeout', rule(), { valid: [ 'Meteor.setTimeout()', diff --git a/tests/lib/util/environment.js b/tests/lib/util/environment.js new file mode 100644 index 0000000000..490d4748f9 --- /dev/null +++ b/tests/lib/util/environment.js @@ -0,0 +1,9 @@ +/* eslint-env mocha */ + +var assert = require('assert') + +describe('environment', function () { + it('is defined', function () { + assert.ok(require('../../../dist/util/environment.js')) + }) +}) diff --git a/tests/lib/util/folderNames.js b/tests/lib/util/folderNames.js new file mode 100644 index 0000000000..75615c9777 --- /dev/null +++ b/tests/lib/util/folderNames.js @@ -0,0 +1,9 @@ +/* eslint-env mocha */ + +var assert = require('assert') + +describe('folder names', function () { + it('is defined', function () { + assert.ok(require('../../../dist/util/folderNames.js')) + }) +}) diff --git a/tests/lib/util/getMeteorMeta.js b/tests/lib/util/getMeteorMeta.js new file mode 100644 index 0000000000..ddd5a308b6 --- /dev/null +++ b/tests/lib/util/getMeteorMeta.js @@ -0,0 +1,153 @@ +/* eslint-env mocha */ + +var assert = require('assert') +var path = require('path') + +var ENVIRONMENT = require('../../../dist/util/environment.js') +var getMeteorMeta = require('../../../dist/util/getMeteorMeta.js') + + +const rootPath = path.join('User', 'anon', 'meteor-project') + +describe('getMeteorMeta', function () { + it('has working rule test-mode', function () { + var someObj = {} + assert.equal(getMeteorMeta(someObj), someObj) + }) + + describe('when not in Meteor project', function () { + it('returns default env', function () { + var result = getMeteorMeta(false, 'file.js') + assert.equal(typeof result, 'object') + assert.equal(result.isInMeteorProject, false) + assert.equal(Object.keys(result).length, 1) + }) + }) + + describe('in public', function () { + it('detects the environment', function () { + var filename = path.join(rootPath, 'public', 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'public/file.js') + assert.equal(result.env, ENVIRONMENT.PUBLIC) + assert.equal(result.isCompatibilityFile, false) + assert.equal(result.isInMeteorProject, true) + }) + }) + + describe('in private', function () { + it('detects the environment', function () { + var filename = path.join(rootPath, 'private', 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'private/file.js') + assert.equal(result.env, ENVIRONMENT.PRIVATE) + assert.equal(result.isCompatibilityFile, false) + assert.equal(result.isInMeteorProject, true) + }) + }) + + describe('in package', function () { + it('detects the environment', function () { + var filename = path.join(rootPath, 'packages', 'awesome-pkg', 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'packages/awesome-pkg/file.js') + assert.equal(result.env, ENVIRONMENT.PACKAGE) + assert.equal(result.isCompatibilityFile, false) + assert.equal(result.isInMeteorProject, true) + }) + }) + + describe('on no special folder', function () { + it('has universal environment', function () { + var filename = path.join(rootPath, 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'file.js') + assert.equal(result.env, ENVIRONMENT.UNIVERSAL) + assert.equal(result.isCompatibilityFile, false) + assert.equal(result.isInMeteorProject, true) + }) + }) + + describe('on client', function () { + + it('returns file info', function () { + var filename = path.join(rootPath, 'client', 'lib', 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'client/lib/file.js') + assert.equal(result.env, ENVIRONMENT.CLIENT) + assert.equal(result.isCompatibilityFile, false) + assert.equal(result.isInMeteorProject, true) + }) + + it('does not detect compatibility when directly in client-folder ', function () { + var filename = path.join(rootPath, 'client', 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'client/file.js') + assert.equal(result.env, ENVIRONMENT.CLIENT) + assert.equal(result.isCompatibilityFile, false) + assert.equal(result.isInMeteorProject, true) + }) + + it('detects compatibility mode', function () { + var filename = path.join(rootPath, 'client', 'compatibility', 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'client/compatibility/file.js') + assert.equal(result.env, ENVIRONMENT.CLIENT) + assert.equal(result.isCompatibilityFile, true) + assert.equal(result.isInMeteorProject, true) + }) + }) + + describe('on server', function () { + it('detects the environment', function () { + var filename = path.join(rootPath, 'server', 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'server/file.js') + assert.equal(result.env, ENVIRONMENT.SERVER) + assert.equal(result.isCompatibilityFile, false) + assert.equal(result.isInMeteorProject, true) + }) + + describe('that is nested', function () { + it('detects the environment', function () { + var filename = path.join(rootPath, 'lib', 'server', 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'lib/server/file.js') + assert.equal(result.env, ENVIRONMENT.SERVER) + assert.equal(result.isCompatibilityFile, false) + assert.equal(result.isInMeteorProject, true) + }) + }) + }) + + describe('in tests', function () { + var filename = path.join(rootPath, 'tests', 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'tests/file.js') + assert.equal(result.env, ENVIRONMENT.TEST) + assert.equal(result.isCompatibilityFile, false) + assert.equal(result.isInMeteorProject, true) + }) + + describe('in node_modules', function () { + var filename = path.join(rootPath, 'node_modules', 'my-module', 'file.js') + var result = getMeteorMeta(rootPath, filename) + assert.equal(typeof result, 'object') + assert.equal(result.path, 'node_modules/my-module/file.js') + assert.equal(result.env, ENVIRONMENT.NODE_MODULE) + assert.equal(result.isCompatibilityFile, false) + assert.equal(result.isInMeteorProject, true) + }) + + +}) diff --git a/tests/lib/util/getMeteorProjectRootPath.js b/tests/lib/util/getMeteorProjectRootPath.js new file mode 100644 index 0000000000..0abfa2c90a --- /dev/null +++ b/tests/lib/util/getMeteorProjectRootPath.js @@ -0,0 +1,32 @@ +/* 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) + }) +})