Skip to content

Commit

Permalink
feat: Add ability to use function as config (#913)
Browse files Browse the repository at this point in the history
* feat: add ability to use function as config

* fix: test

* test: add specific test for config formatter

* docs: clarify function based configuration option

* Apply suggestions for README.md from code review

Co-authored-by: Iiro Jäppinen <iiro@jappinen.fi>

Co-authored-by: Iiro Jäppinen <iiro@jappinen.fi>
  • Loading branch information
SachinShekhar and iiroj committed Sep 16, 2020
1 parent 643038d commit 67a4d06
Show file tree
Hide file tree
Showing 6 changed files with 67 additions and 4 deletions.
33 changes: 30 additions & 3 deletions README.md
Expand Up @@ -189,14 +189,40 @@ For example:
going to execute `eslint` and if it exits with `0` code, it will execute `prettier --write` on all staged `*.js` files.
## Using JS functions to customize tasks
## Using JS configuration file
When supplying configuration in JS format it is possible to define the task as a function, which will receive an array of staged filenames/paths and should return the complete command as a string. It is also possible to return an array of complete command strings, for example when the task supports only a single file input. The function can be either sync or async.
Writing the configuration file in JavaScript is the most powerful way to configure _lint-staged_ (`lint-staged.config.js`, [similar](https://github.com/okonet/lint-staged/README.md#configuration), or passed via `--config`). From the configuration file, you can export either a single function, or an object.
If the `exports` value is a function, it will receive an array of all staged filenames. You can then build your own matchers for the files, and return a command string, or an array or command strings. These strings are considered complete and should include the filename arguments, if wanted.
If the `exports` value is an object, its keys should be glob matches (like in the normal non-js config format). The values can either be like in the normal config, or individual functions like described above. Instead of receiving all matched files, the functions in the exported object will only receive the staged files matching the corresponding glob key.
### Function signature
The function can also be async:
```ts
type TaskFn = (filenames: string[]) => string | string[] | Promise<string | string[]>
(filenames: string[]) => string | string[] | Promise<string | string[]>
```
### Example: Export a function to build your own matchers
```js
// lint-staged.config.js
const micromatch = require('micromatch')
module.exports = (allStagedFiles) => {
const shFiles = micromatch(allStagedFiles, ['**/src/**/*.sh']);
if (shFiles.length) {
return `printf '%s\n' "Script files aren't allowed in src directory" >&2`
}
const codeFiles = micromatch(allStagedFiles, ['**/*.js', '**/*.ts']);
const docFiles = micromatch(allStagedFiles, ['**/*.md']);
return [`eslint ${codeFiles.join(' ')}`, `mdl ${docFiles.join(' ')}`];
}
```
### Example: Wrap filenames in single quotes and run once per file
```js
Expand Down Expand Up @@ -226,6 +252,7 @@ module.exports = {
```
### Example: Use your own globs
It's better to use the [function-based configuration (seen above)](https://github.com/okonet/lint-staged/README.md#example-export-a-function-to-build-your-own-matchers), if your use case is this.
```js
// lint-staged.config.js
Expand Down
7 changes: 7 additions & 0 deletions lib/formatConfig.js
@@ -0,0 +1,7 @@
module.exports = function formatConfig(config) {
if (typeof config === 'function') {
return { '*': config }
}

return config
}
4 changes: 3 additions & 1 deletion lib/index.js
Expand Up @@ -9,6 +9,7 @@ const { PREVENTED_EMPTY_COMMIT, GIT_ERROR, RESTORE_STASH_EXAMPLE } = require('./
const printTaskOutput = require('./printTaskOutput')
const runAll = require('./runAll')
const { ApplyEmptyCommitError, GetBackupStashError, GitError } = require('./symbols')
const formatConfig = require('./formatConfig')
const validateConfig = require('./validateConfig')

const errConfigNotFound = new Error('Config could not be found')
Expand Down Expand Up @@ -88,7 +89,8 @@ module.exports = async function lintStaged(
debugLog('Successfully loaded config from `%s`:\n%O', resolved.filepath, resolved.config)
// resolved.config is the parsed configuration object
// resolved.filepath is the path to the config file that was found
const config = validateConfig(resolved.config)
const formattedConfig = formatConfig(resolved.config)
const config = validateConfig(formattedConfig)
if (debug) {
// Log using logger to be able to test through `consolemock`.
logger.log('Running lint-staged with the following config:')
Expand Down
2 changes: 2 additions & 0 deletions test/__snapshots__/validateConfig.spec.js.snap
@@ -1,5 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`validateConfig should not throw and should print nothing for function config 1`] = `""`;

exports[`validateConfig should not throw and should print nothing for function task 1`] = `""`;

exports[`validateConfig should not throw and should print nothing for valid config 1`] = `""`;
Expand Down
17 changes: 17 additions & 0 deletions test/formatConfig.spec.js
@@ -0,0 +1,17 @@
import formatConfig from '../lib/formatConfig'

describe('formatConfig', () => {
it('Object config should return as is', () => {
const simpleConfig = {
'*.js': ['eslint --fix', 'git add'],
}
expect(formatConfig(simpleConfig)).toEqual(simpleConfig)
})

it('Function config should be converted to object', () => {
const functionConfig = (stagedFiles) => [`eslint --fix ${stagedFiles}', 'git add`]
expect(formatConfig(functionConfig)).toEqual({
'*': functionConfig,
})
})
})
8 changes: 8 additions & 0 deletions test/validateConfig.spec.js
Expand Up @@ -2,6 +2,8 @@ import makeConsoleMock from 'consolemock'

import validateConfig from '../lib/validateConfig'

import formatConfig from '../lib/formatConfig'

describe('validateConfig', () => {
const originalConsole = global.console
beforeAll(() => {
Expand Down Expand Up @@ -36,6 +38,12 @@ describe('validateConfig', () => {
expect(console.printHistory()).toMatchSnapshot()
})

it('should not throw and should print nothing for function config', () => {
const functionConfig = (stagedFiles) => [`eslint ${stagedFiles.join(' ')}`]
expect(() => validateConfig(formatConfig(functionConfig))).not.toThrow()
expect(console.printHistory()).toMatchSnapshot()
})

it('should not throw and should print nothing for function task', () => {
expect(() =>
validateConfig({
Expand Down

0 comments on commit 67a4d06

Please sign in to comment.