-
-
Notifications
You must be signed in to change notification settings - Fork 422
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(config): Config validation (#141)
- Loading branch information
Showing
26 changed files
with
923 additions
and
238 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
#!/usr/bin/env node | ||
require('./src') | ||
require('./src')() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
/* eslint no-console: 0 */ | ||
/* eslint no-prototype-builtins: 0 */ | ||
const chalk = require('chalk') | ||
const format = require('stringify-object') | ||
const intersection = require('lodash/intersection') | ||
const defaultsDeep = require('lodash/defaultsDeep') | ||
const isObject = require('lodash/isObject') | ||
const validate = require('jest-validate').validate | ||
const logValidationWarning = require('jest-validate').logValidationWarning | ||
const unknownOptionWarning = require('jest-validate/build/warnings').unknownOptionWarning | ||
const isGlob = require('is-glob') | ||
|
||
/** | ||
* Default config object | ||
* | ||
* @type {{concurrent: boolean, chunkSize: number, gitDir: string, globOptions: {matchBase: boolean, dot: boolean}, linters: {}, subTaskConcurrency: number, renderer: string, verbose: boolean}} | ||
*/ | ||
const defaultConfig = { | ||
concurrent: true, | ||
chunkSize: Number.MAX_SAFE_INTEGER, | ||
gitDir: '.', | ||
globOptions: { | ||
matchBase: true, | ||
dot: true | ||
}, | ||
linters: {}, | ||
subTaskConcurrency: 1, | ||
renderer: 'update', | ||
verbose: false | ||
} | ||
|
||
/** | ||
* Check if the config is "simple" i.e. doesn't contains any of full config keys | ||
* | ||
* @param config | ||
* @returns {boolean} | ||
*/ | ||
function isSimple(config) { | ||
return ( | ||
isObject(config) && | ||
!config.hasOwnProperty('linters') && | ||
intersection(Object.keys(defaultConfig), Object.keys(config)).length === 0 | ||
) | ||
} | ||
|
||
/** | ||
* Custom jest-validate reporter for unknown options | ||
* @param config | ||
* @param example | ||
* @param option | ||
* @param options | ||
* @returns {void} | ||
*/ | ||
function unknownValidationReporter(config, example, option, options) { | ||
/** | ||
* If the unkonwn property is a glob this is probably | ||
* a typical mistake of mixing simple and advanced configs | ||
*/ | ||
if (isGlob(option)) { | ||
const message = ` Unknown option ${chalk.bold(`"${option}"`)} with value ${chalk.bold( | ||
format(config[option], { inlineCharacterLimit: Number.POSITIVE_INFINITY }) | ||
)} was found in the config root. | ||
You are probably trying to mix simple and advanced config formats. Adding | ||
${chalk.bold(`"linters": { | ||
"${option}": ${JSON.stringify(config[option])} | ||
}`)} | ||
will fix it and remove this message.` | ||
|
||
const comment = options.comment | ||
const name = (options.title && options.title.warning) || 'WARNING' | ||
return logValidationWarning(name, message, comment) | ||
} | ||
// If it is not glob pattern, use default jest-validate reporter | ||
return unknownOptionWarning(config, example, option, options) | ||
} | ||
|
||
/** | ||
* For a given configuration object that we retrive from .lintstagedrc or package.json | ||
* construct a full configuration with all options set. | ||
* | ||
* This is a bit tricky since we support 2 different syntxes: simple and full | ||
* For simple config, only the `linters` configuration is provided. | ||
* | ||
* @param {Object} sourceConfig | ||
* @returns {{ | ||
* concurrent: boolean, chunkSize: number, gitDir: string, globOptions: {matchBase: boolean, dot: boolean}, linters: {}, subTaskConcurrency: number, renderer: string, verbose: boolean | ||
* }} | ||
*/ | ||
function getConfig(sourceConfig) { | ||
const config = defaultsDeep( | ||
{}, // Do not mutate sourceConfig!!! | ||
isSimple(sourceConfig) ? { linters: sourceConfig } : sourceConfig, | ||
defaultConfig | ||
) | ||
|
||
// Check if renderer is set in sourceConfig and if not, set accordingly to verbose | ||
if (isObject(sourceConfig) && !sourceConfig.hasOwnProperty('renderer')) { | ||
config.renderer = config.verbose ? 'verbose' : 'update' | ||
} | ||
|
||
return config | ||
} | ||
|
||
/** | ||
* Runs config validation. Throws error if the config is not valid. | ||
* @param config {Object} | ||
* @returns config {Object} | ||
*/ | ||
function validateConfig(config) { | ||
const exampleConfig = Object.assign({}, defaultConfig, { | ||
linters: { | ||
'*.js': ['eslint --fix', 'git add'], | ||
'*.css': 'stylelint' | ||
} | ||
}) | ||
|
||
const validation = validate(config, { | ||
exampleConfig, | ||
unknown: unknownValidationReporter, | ||
comment: | ||
'Please refer to https://github.com/okonet/lint-staged#configuration for more information...' | ||
}) | ||
|
||
if (!validation.isValid) { | ||
throw new Error('lint-staged config is invalid... Aborting.') | ||
} | ||
|
||
return config | ||
} | ||
|
||
module.exports = { | ||
getConfig, | ||
validateConfig | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,104 +1,63 @@ | ||
/* eslint no-console: 0 */ | ||
/* eslint no-process-exit: 0 */ | ||
/* eslint import/no-dynamic-require: 0 */ | ||
|
||
'use strict' | ||
|
||
const path = require('path') | ||
const sgf = require('staged-git-files') | ||
const appRoot = require('app-root-path') | ||
const Listr = require('listr') | ||
const cosmiconfig = require('cosmiconfig') | ||
const stringifyObject = require('stringify-object') | ||
const getConfig = require('./getConfig').getConfig | ||
const validateConfig = require('./getConfig').validateConfig | ||
const printErrors = require('./printErrors') | ||
const runAll = require('./runAll') | ||
|
||
const packageJson = require(appRoot.resolve('package.json')); // eslint-disable-line | ||
const runScript = require('./runScript') | ||
const generateTasks = require('./generateTasks') | ||
const readConfigOption = require('./readConfigOption') | ||
// Find the right package.json at the root of the project | ||
// TODO: Test if it should be aware of `gitDir` | ||
const packageJson = require(appRoot.resolve('package.json')) | ||
|
||
// Force colors for packages that depend on https://www.npmjs.com/package/supports-color | ||
// but do this only in TTY mode | ||
if (process.stdout.isTTY) { | ||
process.env.FORCE_COLOR = true | ||
} | ||
|
||
cosmiconfig('lint-staged', { | ||
rc: '.lintstagedrc', | ||
rcExtensions: true | ||
}) | ||
.then(result => { | ||
// result.config is the parsed configuration object | ||
// result.filepath is the path to the config file that was found | ||
const config = result.config | ||
|
||
const verbose = config.verbose | ||
// Output config in verbose mode | ||
if (verbose) console.log(config) | ||
const concurrent = readConfigOption(config, 'concurrent', true) | ||
const renderer = verbose ? 'verbose' : 'update' | ||
const gitDir = config.gitDir ? path.resolve(config.gitDir) : process.cwd() | ||
sgf.cwd = gitDir | ||
|
||
sgf('ACM', (err, files) => { | ||
if (err) { | ||
console.error(err) | ||
process.exit(1) | ||
/** | ||
* Root lint-staged function that is called from .bin | ||
*/ | ||
module.exports = function lintStaged() { | ||
cosmiconfig('lint-staged', { | ||
rc: '.lintstagedrc', | ||
rcExtensions: true | ||
}) | ||
.then(result => { | ||
// result.config is the parsed configuration object | ||
// result.filepath is the path to the config file that was found | ||
const config = validateConfig(getConfig(result.config)) | ||
|
||
if (config.verbose) { | ||
console.log(` | ||
Running lint-staged with the following config: | ||
${stringifyObject(config)} | ||
`) | ||
} | ||
|
||
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: () => | ||
new Listr( | ||
runScript(task.commands, task.fileList, packageJson, { | ||
gitDir, | ||
verbose, | ||
config | ||
}), | ||
{ | ||
// In sub-tasks we don't want to run concurrently | ||
// and we want to abort on errors | ||
concurrent: false, | ||
exitOnError: true | ||
} | ||
), | ||
skip: () => { | ||
if (task.fileList.length === 0) { | ||
return `No staged files match ${task.pattern}` | ||
} | ||
return false | ||
} | ||
})) | ||
|
||
if (tasks.length) { | ||
new Listr(tasks, { | ||
concurrent, | ||
renderer, | ||
exitOnError: !concurrent // Wait for all errors when running concurrently | ||
runAll(packageJson, config) | ||
.then(() => { | ||
// No errors, exiting with 0 | ||
process.exitCode = 0 | ||
}) | ||
.catch(error => { | ||
// Errors detected, printing and exiting with non-zero | ||
printErrors(error) | ||
process.exitCode = 1 | ||
}) | ||
.run() | ||
.catch(error => { | ||
if (Array.isArray(error.errors)) { | ||
error.errors.forEach(lintError => { | ||
console.error(lintError.message) | ||
}) | ||
} else { | ||
console.log(error.message) | ||
} | ||
process.exit(1) | ||
}) | ||
} | ||
}) | ||
}) | ||
.catch(parsingError => { | ||
console.error(`Could not parse lint-staged config. | ||
.catch(parsingError => { | ||
console.error(`Could not parse lint-staged config. | ||
Make sure you have created it. See https://github.com/okonet/lint-staged#readme. | ||
${parsingError} | ||
`) | ||
process.exit(1) | ||
}) | ||
process.exitCode = 1 | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
/* eslint no-console: 0 */ | ||
|
||
'use strict' | ||
|
||
module.exports = function printErrors(errorInstance) { | ||
if (Array.isArray(errorInstance.errors)) { | ||
errorInstance.errors.forEach(lintError => { | ||
console.error(lintError.message) | ||
}) | ||
} else { | ||
console.error(errorInstance.message) | ||
} | ||
} |
Oops, something went wrong.