From 67a4d06e39c4638a546494940bf99934692fb610 Mon Sep 17 00:00:00 2001 From: Sachin Shekhar Date: Wed, 16 Sep 2020 18:46:20 +0530 Subject: [PATCH] feat: Add ability to use function as config (#913) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 Co-authored-by: Iiro Jäppinen --- README.md | 33 +++++++++++++++++-- lib/formatConfig.js | 7 ++++ lib/index.js | 4 ++- .../__snapshots__/validateConfig.spec.js.snap | 2 ++ test/formatConfig.spec.js | 17 ++++++++++ test/validateConfig.spec.js | 8 +++++ 6 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 lib/formatConfig.js create mode 100644 test/formatConfig.spec.js diff --git a/README.md b/README.md index 60508f538..bb71c1e51 100644 --- a/README.md +++ b/README.md @@ -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 +(filenames: string[]) => string | string[] | Promise ``` +### 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 @@ -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 diff --git a/lib/formatConfig.js b/lib/formatConfig.js new file mode 100644 index 000000000..376e67a8d --- /dev/null +++ b/lib/formatConfig.js @@ -0,0 +1,7 @@ +module.exports = function formatConfig(config) { + if (typeof config === 'function') { + return { '*': config } + } + + return config +} diff --git a/lib/index.js b/lib/index.js index 90ac097e6..f83b45329 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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') @@ -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:') diff --git a/test/__snapshots__/validateConfig.spec.js.snap b/test/__snapshots__/validateConfig.spec.js.snap index 98b1b885c..c58ec222b 100644 --- a/test/__snapshots__/validateConfig.spec.js.snap +++ b/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`] = `""`; diff --git a/test/formatConfig.spec.js b/test/formatConfig.spec.js new file mode 100644 index 000000000..903ce56d2 --- /dev/null +++ b/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, + }) + }) +}) diff --git a/test/validateConfig.spec.js b/test/validateConfig.spec.js index 74a278e1d..bf6d4a0f2 100644 --- a/test/validateConfig.spec.js +++ b/test/validateConfig.spec.js @@ -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(() => { @@ -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({