/* eslint no-prototype-builtins: 0 */ 'use strict' const chalk = require('chalk') const format = require('stringify-object') const intersection = require('lodash/intersection') const defaultsDeep = require('lodash/defaultsDeep') const isArray = require('lodash/isArray') const isFunction = require('lodash/isFunction') const isObject = require('lodash/isObject') const isString = require('lodash/isString') const isGlob = require('is-glob') const yup = require('yup') const debug = require('debug')('lint-staged:cfg') /** * Default config object * * @type {{concurrent: boolean, chunkSize: number, globOptions: {matchBase: boolean, dot: boolean}, linters: {}, subTaskConcurrency: number, renderer: string}} */ const defaultConfig = { concurrent: true, chunkSize: Number.MAX_SAFE_INTEGER, globOptions: { matchBase: true, dot: true }, linters: {}, ignore: [], subTaskConcurrency: 1, renderer: 'update', relative: false } /** * Configuration schema to validate configuration */ const schema = yup.object().shape({ concurrent: yup.boolean().default(defaultConfig.concurrent), chunkSize: yup .number() .positive() .default(defaultConfig.chunkSize), globOptions: yup.object().shape({ matchBase: yup.boolean().default(defaultConfig.globOptions.matchBase), dot: yup.boolean().default(defaultConfig.globOptions.dot) }), linters: yup.object(), ignore: yup.array().of(yup.string()), subTaskConcurrency: yup .number() .positive() .integer() .default(defaultConfig.subTaskConcurrency), renderer: yup .mixed() .test( 'renderer', "Should be 'update', 'verbose' or a function.", value => value === 'update' || value === 'verbose' || isFunction(value) ), relative: yup.boolean().default(defaultConfig.relative) }) /** * 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 ) } const logDeprecation = (opt, helpMsg) => console.warn(`● Deprecation Warning: Option ${chalk.bold(opt)} was removed. ${helpMsg} Please remove ${chalk.bold(opt)} from your configuration. Please refer to https://github.com/okonet/lint-staged#configuration for more information...`) const logUnknown = (opt, helpMsg, value) => console.warn(`● Validation Warning: Unknown option ${chalk.bold(`"${opt}"`)} with value ${chalk.bold( format(value, { inlineCharacterLimit: Number.POSITIVE_INFINITY }) )} was found in the config root. ${helpMsg} Please refer to https://github.com/okonet/lint-staged#configuration for more information...`) const formatError = helpMsg => `● Validation Error: ${helpMsg} Please refer to https://github.com/okonet/lint-staged#configuration for more information...` const createError = (opt, helpMsg, value) => formatError(`Invalid value for '${chalk.bold(opt)}'. ${helpMsg}. Configured value is: ${chalk.bold( format(value, { inlineCharacterLimit: Number.POSITIVE_INFINITY }) )}`) /** * Reporter for unknown options * @param config * @param option * @returns {void} */ function unknownValidationReporter(config, option) { /** * If the unkonwn property is a glob this is probably * a typical mistake of mixing simple and advanced configs */ if (isGlob(option)) { // prettier-ignore const message = `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.` return logUnknown(option, message, config[option]) } // If it is not glob pattern, simply notify of unknown value return logUnknown(option, '', config[option]) } /** * 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, globOptions: {matchBase: boolean, dot: boolean}, linters: {}, subTaskConcurrency: number, renderer: string * }} */ function getConfig(sourceConfig, debugMode) { debug('Normalizing config') 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 = debugMode ? 'verbose' : 'update' } return config } /** * Runs config validation. Throws error if the config is not valid. * @param config {Object} * @returns config {Object} */ function validateConfig(config) { debug('Validating config') const deprecatedConfig = { gitDir: "lint-staged now automatically resolves '.git' directory.", verbose: `Use the command line flag ${chalk.bold('--debug')} instead.` } const errors = [] try { schema.validateSync(config, { abortEarly: false, strict: true }) } catch (error) { error.errors.forEach(message => errors.push(formatError(message))) } if (isObject(config.linters)) { Object.keys(config.linters).forEach(key => { if ( (!isArray(config.linters[key]) || config.linters[key].some(item => !isString(item))) && !isString(config.linters[key]) ) { errors.push( createError(`linters[${key}]`, 'Should be a string or an array of strings', key) ) } }) } Object.keys(config) .filter(key => !defaultConfig.hasOwnProperty(key)) .forEach(option => { if (deprecatedConfig.hasOwnProperty(option)) { logDeprecation(option, deprecatedConfig[option]) return } unknownValidationReporter(config, option) }) if (errors.length) { throw new Error(errors.join('\n')) } return config } module.exports = { getConfig, validateConfig }