diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8bc4f10 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false + +[package.json] +indent_size = 2 diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..63370ba --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,8 @@ +{ + "root": true, + "extends": [ + "./node_modules/@satazor/eslint-config/es6.js", + "./node_modules/@satazor/eslint-config/addons/node.js", + "./node_modules/@satazor/eslint-config/addons/node-v4-es6.js" + ] +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6de6df --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/node_modules +/npm-debug.* +/test/tmp +/test/coverage \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5dd2002 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: + - "4" + - "5" +script: "npm run test-travis" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1e7a197 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2016 IndigoUnited + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0b77916 --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# detect-repo-linters + +[![NPM version][npm-image]][npm-url] [![Downloads][downloads-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Coverage Status][coveralls-image]][coveralls-url] [![Dependency status][david-dm-image]][david-dm-url] [![Dev Dependency status][david-dm-dev-image]][david-dm-dev-url] + +[npm-url]:https://npmjs.org/package/detect-repo-linters +[downloads-image]:http://img.shields.io/npm/dm/detect-repo-linters.svg +[npm-image]:http://img.shields.io/npm/v/detect-repo-linters.svg +[travis-url]:https://travis-ci.org/IndigoUnited/node-detect-repo-linters +[travis-image]:http://img.shields.io/travis/IndigoUnited/node-detect-repo-linters.svg +[coveralls-url]:https://coveralls.io/r/IndigoUnited/node-detect-repo-linters +[coveralls-image]:https://img.shields.io/coveralls/IndigoUnited/node-detect-repo-linters.svg +[david-dm-url]:https://david-dm.org/IndigoUnited/node-detect-repo-linters +[david-dm-image]:https://img.shields.io/david/IndigoUnited/node-detect-repo-linters.svg +[david-dm-dev-url]:https://david-dm.org/IndigoUnited/node-detect-repo-linters#info=devDependencies +[david-dm-dev-image]:https://img.shields.io/david/dev/IndigoUnited/node-detect-repo-linters.svg + +Scans a repository directory, searching for configured linters. + + +## Installation + +`$ npm install detect-repo-linters` + + +## Usage + +`detectRepoLinters(repo, [callback]) -> Promise` + +You may consume the API using promises or callbacks, it's up to you. + +```js +const detectRepoLinters = require('detect-repo-linters'); + +detectRepoLinters('repo-directory') +.then((linters) => { + console.log(linters); + + // { + // general: ['editorconfig'], + // js: ['eslint'], + // css: ['stylelint'], + // html: [], + // } +}); +``` + +At the moment, `detect-repo-linters` can detect the following linters: + +- `general`: [editorconfig](http://editorconfig.org) +- `js`: [eslint](http://eslint.org), [jscs](http://jscs.info) and [jshint](http://jshint.com) +- `css`: [stylelint](http://stylelint.io) and [csslint](http://csslint.net) +- `html`: [htmlhint](http://htmlhint.com) and [htmllint](http://htmllint.github.io) + +Feel free to a PR to include other linters as part of the detection! + + +## Tests + +`$ npm test` +`$ npm test-cov` to get coverage report + + +## License + +Released under the [MIT License](http://www.opensource.org/licenses/mit-license.php). diff --git a/index.js b/index.js new file mode 100644 index 0000000..4e9da0d --- /dev/null +++ b/index.js @@ -0,0 +1,67 @@ +'use strict'; + +const fs = require('fs'); +const requireDirectory = require('require-directory'); +const callMeMaybe = require('call-me-maybe'); +const detectors = requireDirectory(module, './lib', { recurse: false }); + +const linters = { + general: [ + { name: 'editorconfig', fn: detectors.editorconfig }, + ], + js: [ + { name: 'eslint', fn: detectors.eslint }, + { name: 'jscs', fn: detectors.jscs }, + { name: 'jshint', fn: detectors.jshint }, + ], + css: [ + { name: 'stylelint', fn: detectors.stylelint }, + { name: 'csslint', fn: detectors.csslint }, + ], + html: [ + { name: 'htmlhint', fn: detectors.htmlhint }, + { name: 'htmllint', fn: detectors.htmllint }, + ], +}; + +function detectLinters(dir, linters) { + return Promise.all(linters.map((linter) => linter.fn(dir))) + .then((results) => { + return linters + .filter((linter, index) => results[index]) + .map((linter) => linter.name); + }); +} + +function detectRepoLinters(dir, callback) { + // Check if dir exists + const promise = new Promise((resolve, reject) => { + fs.stat(dir, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); + }) + // Run the linter detectors and build the results + .then(() => { + const types = Object.keys(linters); + const promises = types.map((type) => detectLinters(dir, linters[type])); + + return Promise.all(promises) + .then((results) => { + const ret = {}; + + results.forEach((result, index) => { + ret[types[index]] = result; + }); + + return ret; + }); + }); + + return callMeMaybe(callback, promise); +} + +module.exports = detectRepoLinters; diff --git a/lib/csslint.js b/lib/csslint.js new file mode 100644 index 0000000..aebda78 --- /dev/null +++ b/lib/csslint.js @@ -0,0 +1,13 @@ +'use strict'; + +const path = require('path'); +const tryFiles = require('./util/tryFiles'); + +function detectCssLint(dir) { + const paths = ['.csslintrc'] + .map((entry) => path.join(dir, entry)); + + return tryFiles(paths); +} + +module.exports = detectCssLint; diff --git a/lib/editorconfig.js b/lib/editorconfig.js new file mode 100644 index 0000000..659fe58 --- /dev/null +++ b/lib/editorconfig.js @@ -0,0 +1,12 @@ +'use strict'; + +const path = require('path'); +const tryFiles = require('./util/tryFiles'); + +function detectEditorConfig(dir) { + const paths = ['.editorconfig'].map((entry) => path.join(dir, entry)); + + return tryFiles(paths); +} + +module.exports = detectEditorConfig; diff --git a/lib/eslint.js b/lib/eslint.js new file mode 100644 index 0000000..f5902b0 --- /dev/null +++ b/lib/eslint.js @@ -0,0 +1,18 @@ +'use strict'; + +const path = require('path'); +const tryFiles = require('./util/tryFiles'); +const tryPackageJson = require('./util/tryPackageJson'); + +function detectEslint(dir) { + const paths = ['.eslintrc.js', '.eslintrc.yaml', '.eslintrc.yml', '.eslintrc.json', '.eslintrc'] + .map((entry) => path.join(dir, entry)); + + return Promise.all([ + tryFiles(paths), + tryPackageJson(dir, 'eslintConfig'), + ]) + .then((booleans) => booleans.some((bool) => bool)); +} + +module.exports = detectEslint; diff --git a/lib/htmlhint.js b/lib/htmlhint.js new file mode 100644 index 0000000..2dffce3 --- /dev/null +++ b/lib/htmlhint.js @@ -0,0 +1,13 @@ +'use strict'; + +const path = require('path'); +const tryFiles = require('./util/tryFiles'); + +function detectHtmlHint(dir) { + const paths = ['.htmlhintrc'] + .map((entry) => path.join(dir, entry)); + + return tryFiles(paths); +} + +module.exports = detectHtmlHint; diff --git a/lib/htmllint.js b/lib/htmllint.js new file mode 100644 index 0000000..5fcc126 --- /dev/null +++ b/lib/htmllint.js @@ -0,0 +1,13 @@ +'use strict'; + +const path = require('path'); +const tryFiles = require('./util/tryFiles'); + +function detectHtmlHint(dir) { + const paths = ['.htmllintrc'] + .map((entry) => path.join(dir, entry)); + + return tryFiles(paths); +} + +module.exports = detectHtmlHint; diff --git a/lib/jscs.js b/lib/jscs.js new file mode 100644 index 0000000..1e373a3 --- /dev/null +++ b/lib/jscs.js @@ -0,0 +1,18 @@ +'use strict'; + +const path = require('path'); +const tryFiles = require('./util/tryFiles'); +const tryPackageJson = require('./util/tryPackageJson'); + +function detectJscs(dir) { + const paths = ['.jscsrc', '.jscs.json'] + .map((entry) => path.join(dir, entry)); + + return Promise.all([ + tryFiles(paths), + tryPackageJson(dir, 'jscsConfig'), + ]) + .then((booleans) => booleans.some((bool) => bool)); +} + +module.exports = detectJscs; diff --git a/lib/jshint.js b/lib/jshint.js new file mode 100644 index 0000000..2820e2c --- /dev/null +++ b/lib/jshint.js @@ -0,0 +1,18 @@ +'use strict'; + +const path = require('path'); +const tryFiles = require('./util/tryFiles'); +const tryPackageJson = require('./util/tryPackageJson'); + +function detectJsHint(dir) { + const paths = ['.jshintrc'] + .map((entry) => path.join(dir, entry)); + + return Promise.all([ + tryFiles(paths), + tryPackageJson(dir, 'jshintConfig'), + ]) + .then((booleans) => booleans.some((bool) => bool)); +} + +module.exports = detectJsHint; diff --git a/lib/stylelint.js b/lib/stylelint.js new file mode 100644 index 0000000..8ca2417 --- /dev/null +++ b/lib/stylelint.js @@ -0,0 +1,18 @@ +'use strict'; + +const path = require('path'); +const tryFiles = require('./util/tryFiles'); +const tryPackageJson = require('./util/tryPackageJson'); + +function detectStyleLint(dir) { + const paths = ['.stylelintrc.js', '.stylelintrc.yaml', '.stylelintrc.json', '.stylelintrc'] + .map((entry) => path.join(dir, entry)); + + return Promise.all([ + tryFiles(paths), + tryPackageJson(dir, 'stylelint'), + ]) + .then((booleans) => booleans.some((bool) => bool)); +} + +module.exports = detectStyleLint; diff --git a/lib/util/tryFiles.js b/lib/util/tryFiles.js new file mode 100644 index 0000000..95f0348 --- /dev/null +++ b/lib/util/tryFiles.js @@ -0,0 +1,30 @@ +'use strict'; + +const fs = require('fs'); + +function isFile(path) { + return new Promise((resolve, reject) => { + fs.stat(path, (err, stats) => { + if (!err) { + return resolve(stats.isFile()); + } + + if (err.code === 'ENOENT') { + return resolve(false); + } + + reject(err); + }); + }); +} + +function tryFiles(paths) { + return Promise.all(paths.map((path) => isFile(path))) + .then((results) => { + const filteredResults = results.filter((exists) => exists); + + return !!filteredResults[0]; + }); +} + +module.exports = tryFiles; diff --git a/lib/util/tryPackageJson.js b/lib/util/tryPackageJson.js new file mode 100644 index 0000000..681140e --- /dev/null +++ b/lib/util/tryPackageJson.js @@ -0,0 +1,17 @@ +'use strict'; + +const path = require('path'); +const loadJsonFile = require('load-json-file'); + +function tryPackageJson(dir, prop) { + return loadJsonFile(path.join(dir, 'package.json')) + .then((json) => !!json[prop], (err) => { + if (err.code === 'ENOENT') { + return false; + } + + throw err; + }); +} + +module.exports = tryPackageJson; diff --git a/package.json b/package.json new file mode 100644 index 0000000..75ebfa9 --- /dev/null +++ b/package.json @@ -0,0 +1,54 @@ +{ + "name": "detect-repo-linters", + "version": "0.1.0", + "description": "Scans a repository directory, searching for configured linters.", + "main": "index.js", + "scripts": { + "test": "mocha --bail", + "test-cov": "istanbul cover --dir test/coverage _mocha -- --bail && echo Coverage lies in test/coverage/lcov-report/index.html", + "test-travis": "istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- --bail && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" + }, + "bugs": { + "url": "https://github.com/IndigoUnited/node-detect-repo-linters/issues/" + }, + "repository": { + "type": "git", + "url": "git://github.com/IndigoUnited/node-detect-repo-linters.git" + }, + "keywords": [ + "linter", + "linters", + "detect", + "scan", + "repository", + "repo", + "editorconfig", + "eslint", + "jshint", + "jscs", + "jslint", + "stylelint", + "csslint", + "htmlhint", + "htmllint" + ], + "author": "IndigoUnited (http://indigounited.com)", + "license": "MIT", + "dependencies": { + "call-me-maybe": "^1.0.1", + "load-json-file": "^1.1.0", + "require-directory": "^2.1.1" + }, + "devDependencies": { + "@satazor/eslint-config": "^1.0.6", + "chai": "^3.5.0", + "coveralls": "^2.11.6", + "istanbul": "^0.4.2", + "mkdirp": "^0.5.1", + "mocha": "^2.4.5", + "rimraf": "^2.5.1" + }, + "engines": { + "node": ">=4.0.0" + } +} diff --git a/test/.eslintrc.json b/test/.eslintrc.json new file mode 100644 index 0000000..f9fbb2d --- /dev/null +++ b/test/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "env": { + "mocha": true + } +} \ No newline at end of file diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..f1dfa1f --- /dev/null +++ b/test/test.js @@ -0,0 +1,260 @@ +'use strict'; + +const fs = require('fs'); +const rimraf = require('rimraf'); +const mkdirp = require('mkdirp'); +const expect = require('chai').expect; +const detectRepoLinters = require('../'); + +describe('detect-repo-linters', () => { + const tmpFolder = `${__dirname}/tmp`; + + function cleanTmpFolder() { + rimraf.sync(tmpFolder); + mkdirp.sync(tmpFolder); + } + + afterEach(() => rimraf.sync(tmpFolder)); + + it('should detect editorconfig', () => { + cleanTmpFolder(); + fs.writeFileSync(`${tmpFolder}/.editorconfig`, ''); + + function assert(linters) { + expect(linters).to.eql({ general: ['editorconfig'], js: [], css: [], html: [] }); + } + + return detectRepoLinters(tmpFolder) + .then(assert); + }); + + it('should detect eslint', () => { + function assert(linters) { + expect(linters).to.eql({ general: [], js: ['eslint'], css: [], html: [] }); + } + + cleanTmpFolder(); + fs.writeFileSync(`${tmpFolder}/.eslintrc`, ''); + + return detectRepoLinters(tmpFolder) + .then(assert) + .then(() => { + cleanTmpFolder(); + fs.writeFileSync(`${tmpFolder}/.eslintrc.json`, ''); + + return detectRepoLinters(tmpFolder); + }) + .then(assert) + .then(() => { + cleanTmpFolder(); + fs.writeFileSync(`${tmpFolder}/.eslintrc.js`, ''); + + return detectRepoLinters(tmpFolder); + }) + .then(assert) + .then(() => { + cleanTmpFolder(); + fs.writeFileSync(`${tmpFolder}/.eslintrc.yml`, ''); + + return detectRepoLinters(tmpFolder); + }) + .then(assert) + .then(() => { + cleanTmpFolder(); + fs.writeFileSync(`${tmpFolder}/.eslintrc.yaml`, ''); + + return detectRepoLinters(tmpFolder); + }) + .then(assert) + .then(() => { + cleanTmpFolder(); + fs.writeFileSync(`${tmpFolder}/package.json`, JSON.stringify({ eslintConfig: {} })); + + return detectRepoLinters(tmpFolder); + }) + .then(assert); + }); + + it('should detect jscs', () => { + function assert(linters) { + expect(linters).to.eql({ general: [], js: ['jscs'], css: [], html: [] }); + } + + cleanTmpFolder(); + fs.writeFileSync(`${tmpFolder}/.jscsrc`, ''); + + return detectRepoLinters(tmpFolder) + .then(assert) + .then(() => { + cleanTmpFolder(); + fs.writeFileSync(`${tmpFolder}/.jscs.json`, ''); + + return detectRepoLinters(tmpFolder); + }) + .then(assert) + .then(() => { + cleanTmpFolder(); + fs.writeFileSync(`${tmpFolder}/package.json`, JSON.stringify({ jscsConfig: {} })); + + return detectRepoLinters(tmpFolder); + }) + .then(assert); + }); + + it('should detect jshint', () => { + function assert(linters) { + expect(linters).to.eql({ general: [], js: ['jshint'], css: [], html: [] }); + } + + cleanTmpFolder(); + fs.writeFileSync(`${tmpFolder}/.jshintrc`, ''); + + return detectRepoLinters(tmpFolder) + .then(assert) + .then(() => { + cleanTmpFolder(); + fs.writeFileSync(`${tmpFolder}/package.json`, JSON.stringify({ jshintConfig: {} })); + + return detectRepoLinters(tmpFolder); + }) + .then(assert); + }); + + it('should detect stylelint', () => { + function assert(linters) { + expect(linters).to.eql({ general: [], js: [], css: ['stylelint'], html: [] }); + } + + cleanTmpFolder(); + fs.writeFileSync(`${tmpFolder}/.stylelintrc`, ''); + + return detectRepoLinters(tmpFolder) + .then(assert) + .then(() => { + cleanTmpFolder(); + fs.writeFileSync(`${tmpFolder}/.stylelintrc.json`, ''); + + return detectRepoLinters(tmpFolder); + }) + .then(assert) + .then(() => { + cleanTmpFolder(); + fs.writeFileSync(`${tmpFolder}/.stylelintrc.js`, ''); + + return detectRepoLinters(tmpFolder); + }) + .then(assert) + .then(() => { + cleanTmpFolder(); + fs.writeFileSync(`${tmpFolder}/.stylelintrc.yaml`, ''); + + return detectRepoLinters(tmpFolder); + }) + .then(assert) + .then(() => { + cleanTmpFolder(); + fs.writeFileSync(`${tmpFolder}/package.json`, JSON.stringify({ stylelint: {} })); + + return detectRepoLinters(tmpFolder); + }) + .then(assert); + }); + + it('should detect csslint', () => { + function assert(linters) { + expect(linters).to.eql({ general: [], js: [], css: ['csslint'], html: [] }); + } + + cleanTmpFolder(); + fs.writeFileSync(`${tmpFolder}/.csslintrc`, ''); + + return detectRepoLinters(tmpFolder) + .then(assert); + }); + + it('should detect htmlhint', () => { + function assert(linters) { + expect(linters).to.eql({ general: [], js: [], css: [], html: ['htmlhint'] }); + } + + cleanTmpFolder(); + fs.writeFileSync(`${tmpFolder}/.htmlhintrc`, ''); + + return detectRepoLinters(tmpFolder) + .then(assert); + }); + + it('should detect htmllint', () => { + function assert(linters) { + expect(linters).to.eql({ general: [], js: [], css: [], html: ['htmllint'] }); + } + + cleanTmpFolder(); + fs.writeFileSync(`${tmpFolder}/.htmllintrc`, ''); + + return detectRepoLinters(tmpFolder) + .then(assert); + }); + + it('should detect several linters in a complex repository', () => { + cleanTmpFolder(); + fs.writeFileSync(`${tmpFolder}/.editorconfig`, ''); + fs.writeFileSync(`${tmpFolder}/.eslintrc.json`, ''); + fs.writeFileSync(`${tmpFolder}/.jshintrc`, ''); + fs.writeFileSync(`${tmpFolder}/.stylelintrc`, ''); + fs.writeFileSync(`${tmpFolder}/.csslintrc`, ''); + fs.writeFileSync(`${tmpFolder}/.htmlhintrc`, ''); + fs.writeFileSync(`${tmpFolder}/.htmllintrc`, ''); + + return detectRepoLinters(tmpFolder) + .then((linters) => { + expect(linters).to.eql({ + general: ['editorconfig'], + js: ['eslint', 'jshint'], + css: ['stylelint', 'csslint'], + html: ['htmlhint', 'htmllint'], + }); + }); + }); + + it('should fail if dir does not exist', () => { + return detectRepoLinters('some-dir-that-will-never-exist') + .then(() => { + throw new Error('expected to fail'); + }, (err) => { + expect(err).to.be.an.instanceOf(Error); + expect(err.code).to.equal('ENOENT'); + }); + }); + + it('should fail if dir is not a directory', () => { + return detectRepoLinters(`${__dirname}/../index.js`) + .then(() => { + throw new Error('expected to fail'); + }, (err) => { + expect(err).to.be.an.instanceOf(Error); + expect(err.code).to.equal('ENOTDIR'); + }); + }); + + it('should support use of callbacks', (next) => { + cleanTmpFolder(); + + const ret = detectRepoLinters(tmpFolder, (err) => { + if (err) { + return next(err); + } + + const ret = detectRepoLinters(`${__dirname}/../index.js`, (err) => { + expect(err).to.be.an.instanceOf(Error); + expect(err.code).to.equal('ENOTDIR'); + + next(); + }); + + expect(ret).to.equal(undefined); + }); + + expect(ret).to.equal(undefined); + }); +});