diff --git a/README.md b/README.md index 1f562f31a..8d0f81956 100644 --- a/README.md +++ b/README.md @@ -73,9 +73,26 @@ To set options and keep lint-staged extensible, advanced format can be used. Thi ## Options * `linters` — `Object` — keys (`String`) are glob patterns, values (`Array | String`) are commands to execute. -* `gitDir` — Sets the relative path to the `.git` root. Useful when your `package.json` is located in a sub-directory. See [working from a subdirectory](#working-from-a-subdirectory) +* `gitDir` — Sets the relative path to the `.git` root. Useful when your `package.json` is located in a sub-directory. See [working from a sub-directory](#working-from-a-sub-directory) * `concurrent` — *true* — runs linters for each glob pattern simultaneously. If you don’t want this, you can set `concurrent: false` +## Filtering files + +It is possible to run linters for certain paths only by using [minimatch](https://github.com/isaacs/minimatch) patterns. The paths used for filtering via minimatch are relative to the directory that contains the `.git` directory. The paths passed to the linters are absolute to avoid confusion in case they're executed with a different working directory, as would be the case when using the `gitDir` option. + +```js +{ + // .js files anywhere in the project + "*.js": "eslint", + // .js files anywhere in the project + "**/*.js": "eslint", + // .js file in the src directory + "src/*.js": "eslint", + // .js file anywhere within and below the src directory + "src/**/*.js": "eslint", +} +``` + ## What commands are supported? Supported are both local npm scripts (`npm run-script`), or any executables installed locally or globally via `npm` as well as any executable from your $PATH. @@ -106,9 +123,9 @@ Tools like ESLint or stylefmt can re-format your code according to an appropriat Starting from v3.1, lint-staged will stash you remaining changes (not added to the index) and restore them from stash afterwards. This allows you to create partial commits with hunks using `git add --patch`. -## Working from a subdirectory +## Working from a sub-directory -If your `package.json` is located in a subdirectory of the git root directory, you can use `gitDir` relative path to point there in order to make lint-staged work. +If your `package.json` is located in a sub-directory of the git root directory, you can use `gitDir` relative path to point there in order to make lint-staged work. ```json { diff --git a/src/generateTasks.js b/src/generateTasks.js index 5bb0befaf..8c2da3c72 100644 --- a/src/generateTasks.js +++ b/src/generateTasks.js @@ -2,17 +2,17 @@ const minimatch = require('minimatch') -module.exports = function generateTasks(config, filePaths) { +module.exports = function generateTasks(config, files) { const linters = config.linters !== undefined ? config.linters : config + const resolve = file => files[file] return Object.keys(linters) .map((pattern) => { const commands = linters[pattern] - const fileList = filePaths.filter( - minimatch.filter(pattern, { - matchBase: true, - dot: true - }) - ) + const filter = minimatch.filter(pattern, { + matchBase: true, + dot: true + }) + const fileList = Object.keys(files).filter(filter).map(resolve) if (fileList.length) { return { pattern, diff --git a/src/index.js b/src/index.js index 57565f14c..8457dceae 100644 --- a/src/index.js +++ b/src/index.js @@ -11,7 +11,6 @@ const cosmiconfig = require('cosmiconfig') const packageJson = require(appRoot.resolve('package.json')) // eslint-disable-line const runScript = require('./runScript') -const resolvePaths = require('./resolvePaths') const generateTasks = require('./generateTasks') // Force colors for packages that depend on https://www.npmjs.com/package/supports-color @@ -30,18 +29,21 @@ cosmiconfig('lint-staged', { const config = result.config const concurrent = config.concurrent || true const gitDir = config.gitDir ? path.resolve(config.gitDir) : process.cwd() - - // If gitDir is defined -> set git root as sgf's cwd - if (gitDir !== process.cwd()) { - sgf.cwd = gitDir - } + sgf.cwd = gitDir sgf('ACM', (err, files) => { if (err) { console.error(err) } - const tasks = generateTasks(config, resolvePaths(files, gitDir)) + const resolvedFiles = {} + files.forEach((file) => { + const absolute = path.resolve(gitDir, file.filename) + const relative = path.relative(gitDir, absolute) + resolvedFiles[relative] = absolute + }) + + const tasks = generateTasks(config, resolvedFiles) .map(task => ({ title: `Running tasks for ${ task.pattern }`, task: () => ( @@ -68,4 +70,3 @@ ${ parsingError } `) process.exit(1) }) - diff --git a/src/resolvePaths.js b/src/resolvePaths.js deleted file mode 100644 index 84951ad05..000000000 --- a/src/resolvePaths.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict' - -const path = require('path') - -module.exports = function resolvePaths(filePaths, relativeTo) { - const cwd = process.cwd() - const base = relativeTo || cwd - return filePaths.map(file => path.relative(cwd, path.resolve(base, file.filename))) -} diff --git a/test/generateTasks.spec.js b/test/generateTasks.spec.js index 0bff51792..27f58985e 100644 --- a/test/generateTasks.spec.js +++ b/test/generateTasks.spec.js @@ -1,68 +1,124 @@ import expect from 'expect' import generateTasks from '../src/generateTasks' -const simpleConfig = { - '*.css': 'stylelint' -} -const nestedConfig = { - gitDir: '../', - linters: { - '*.js': ['eslint --fix', 'git add'], - '*.css': 'stylelint' - } +const files = { + 'test.js': '/root/test.js', + 'deeper/test.js': '/root/deeper/test.js', + 'deeper/test2.js': '/root/deeper/test2.js', + 'even/deeper/test.js': '/root/even/deeper/test.js', + '.hidden/test.js': '/root/.hidden/test.js', + + 'test.css': '/root/test.css', + 'deeper/test.css': '/root/deeper/test.css', + 'deeper/test2.css': '/root/deeper/test2.css', + 'even/deeper/test.css': '/root/even/deeper/test.css', + '.hidden/test.css': '/root/.hidden/test.css', + + 'test.txt': '/root/test.txt', + 'deeper/test.txt': '/root/deeper/test.txt', + 'deeper/test2.txt': '/root/deeper/test2.txt', + 'even/deeper/test.txt': '/root/even/deeper/test.txt', + '.hidden/test.txt': '/root/.hidden/test.txt' } -const files = ['test.js', 'src/.hidden/test2.js', '/.storybook/test.css', '.storybook/test.css', '/absolute/path/test.txt'] +const linters = { + '*.js': 'root-js', + '**/*.js': 'any-js', + 'deeper/*.js': 'deeper-js', + '.hidden/*.js': 'hidden-js', + 'unknown/*.js': 'unknown-js', + '*.{css,js}': 'root-css-or-js' +} describe('generateTasks', () => { - it('should not generate tasks for non-matching files', () => { - const res = generateTasks(simpleConfig, ['test']) - expect(res).toBeAn('array') - expect(res.length).toEqual(0) + it('should return only linters it could find files for', () => { + const result = generateTasks(linters, files) + const commands = result.map(match => match.commands) + expect(commands).toEqual([ + 'root-js', + 'any-js', + 'deeper-js', + 'hidden-js', + // 'unknown-js' does not match any files + 'root-css-or-js' + ]) }) - it('should generate tasks for files with dots in path', () => { - const res = generateTasks(simpleConfig, files) - expect(res).toBeAn('array') - expect(res[0].fileList).toEqual(['/.storybook/test.css', '.storybook/test.css']) + it('should match pattern "*.js"', () => { + const result = generateTasks(linters, files) + const linter = result.find(item => item.pattern === '*.js') + expect(linter).toEqual({ + pattern: '*.js', + commands: 'root-js', + fileList: [ + '/root/test.js', + '/root/deeper/test.js', + '/root/deeper/test2.js', + '/root/even/deeper/test.js', + '/root/.hidden/test.js' + ] + }) }) - it('should generate tasks with complex globs', () => { - const res = generateTasks({ - '**/*.css': 'stylelint' - }, files) - expect(res).toBeAn('array') - expect(res[0].fileList).toEqual(['/.storybook/test.css', '.storybook/test.css']) + it('should match pattern "**/*.js"', () => { + const result = generateTasks(linters, files) + const linter = result.find(item => item.pattern === '**/*.js') + expect(linter).toEqual({ + pattern: '**/*.js', + commands: 'any-js', + fileList: [ + '/root/test.js', + '/root/deeper/test.js', + '/root/deeper/test2.js', + '/root/even/deeper/test.js', + '/root/.hidden/test.js' + ] + }) }) - it('should generate tasks for simple config', () => { - const res = generateTasks(simpleConfig, files) - expect(res).toBeAn('array') - expect(res.length).toBe(1) - expect(res).toEqual([ - { - pattern: '*.css', - commands: 'stylelint', - fileList: ['/.storybook/test.css', '.storybook/test.css'] - } - ]) + it('should match pattern "deeper/*.js"', () => { + const result = generateTasks(linters, files) + const linter = result.find(item => item.pattern === 'deeper/*.js') + expect(linter).toEqual({ + pattern: 'deeper/*.js', + commands: 'deeper-js', + fileList: [ + '/root/deeper/test.js', + '/root/deeper/test2.js' + ] + }) }) - it('should generate tasks for nested config', () => { - const res = generateTasks(nestedConfig, files) - expect(res).toBeAn('array') - expect(res.length).toBe(2) - expect(res).toEqual([ - { - pattern: '*.js', - commands: ['eslint --fix', 'git add'], - fileList: ['test.js', 'src/.hidden/test2.js'] - }, - { - pattern: '*.css', - commands: 'stylelint', - fileList: ['/.storybook/test.css', '.storybook/test.css'] - } - ]) + it('should match pattern ".hidden/*.js"', () => { + const result = generateTasks(linters, files) + const linter = result.find(item => item.pattern === '.hidden/*.js') + expect(linter).toEqual({ + pattern: '.hidden/*.js', + commands: 'hidden-js', + fileList: [ + '/root/.hidden/test.js' + ] + }) + }) + + it('should match pattern "*.{css,js}"', () => { + const result = generateTasks(linters, files) + const linter = result.find(item => item.pattern === '*.{css,js}') + expect(linter).toEqual({ + pattern: '*.{css,js}', + commands: 'root-css-or-js', + fileList: [ + '/root/test.js', + '/root/deeper/test.js', + '/root/deeper/test2.js', + '/root/even/deeper/test.js', + '/root/.hidden/test.js', + '/root/test.css', + '/root/deeper/test.css', + '/root/deeper/test2.css', + '/root/even/deeper/test.css', + '/root/.hidden/test.css' + ] + }) }) }) diff --git a/test/resolvePaths.spec.js b/test/resolvePaths.spec.js deleted file mode 100644 index 7a5230f26..000000000 --- a/test/resolvePaths.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint no-unused-expressions: 0 */ - -import expect from 'expect' -import path from 'path' -import fs from 'fs' -import resolvePaths from '../src/resolvePaths' - -const files = fs.readdirSync(path.resolve('test', '__fixtures__')).map(file => ({ - filename: file -})) - -const cwdParent = path.resolve('..') - -describe('resolvePaths', () => { - it('should return Array of filenames', () => { - expect(resolvePaths([])).toBeAn('array') - expect(resolvePaths(files)).toBeAn('array') - expect(resolvePaths(files)).toEqual( - [ - 'test.css', - 'test.js', - 'test.txt' - ] - ) - }) - - it('should return CWD-relative paths if second parameter is set', () => { - expect(resolvePaths(files, cwdParent)).toEqual( - [ - 'test.css', - 'test.js', - 'test.txt' - ].map(file => path.join('..', file)) - ) - }) -})