UNPKG

6.29 kBJavaScriptView Raw
1/* eslint no-prototype-builtins: 0 */
2
3'use strict'
4
5const chalk = require('chalk')
6const format = require('stringify-object')
7const intersection = require('lodash/intersection')
8const defaultsDeep = require('lodash/defaultsDeep')
9const isArray = require('lodash/isArray')
10const isFunction = require('lodash/isFunction')
11const isObject = require('lodash/isObject')
12const isString = require('lodash/isString')
13const isGlob = require('is-glob')
14const yup = require('yup')
15
16const debug = require('debug')('lint-staged:cfg')
17
18/**
19 * Default config object
20 *
21 * @type {{concurrent: boolean, chunkSize: number, globOptions: {matchBase: boolean, dot: boolean}, linters: {}, subTaskConcurrency: number, renderer: string}}
22 */
23const defaultConfig = {
24 concurrent: true,
25 chunkSize: Number.MAX_SAFE_INTEGER,
26 globOptions: {
27 matchBase: true,
28 dot: true
29 },
30 linters: {},
31 ignore: [],
32 subTaskConcurrency: 1,
33 renderer: 'update',
34 relative: false
35}
36
37/**
38 * Configuration schema to validate configuration
39 */
40const schema = yup.object().shape({
41 concurrent: yup.boolean().default(defaultConfig.concurrent),
42 chunkSize: yup
43 .number()
44 .positive()
45 .default(defaultConfig.chunkSize),
46 globOptions: yup.object().shape({
47 matchBase: yup.boolean().default(defaultConfig.globOptions.matchBase),
48 dot: yup.boolean().default(defaultConfig.globOptions.dot)
49 }),
50 linters: yup.object(),
51 ignore: yup.array().of(yup.string()),
52 subTaskConcurrency: yup
53 .number()
54 .positive()
55 .integer()
56 .default(defaultConfig.subTaskConcurrency),
57 renderer: yup
58 .mixed()
59 .test(
60 'renderer',
61 "Should be 'update', 'verbose' or a function.",
62 value => value === 'update' || value === 'verbose' || isFunction(value)
63 ),
64 relative: yup.boolean().default(defaultConfig.relative)
65})
66
67/**
68 * Check if the config is "simple" i.e. doesn't contains any of full config keys
69 *
70 * @param config
71 * @returns {boolean}
72 */
73function isSimple(config) {
74 return (
75 isObject(config) &&
76 !config.hasOwnProperty('linters') &&
77 intersection(Object.keys(defaultConfig), Object.keys(config)).length === 0
78 )
79}
80
81const logDeprecation = (opt, helpMsg) =>
82 console.warn(`● Deprecation Warning:
83
84 Option ${chalk.bold(opt)} was removed.
85
86 ${helpMsg}
87
88 Please remove ${chalk.bold(opt)} from your configuration.
89
90Please refer to https://github.com/okonet/lint-staged#configuration for more information...`)
91
92const logUnknown = (opt, helpMsg, value) =>
93 console.warn(`● Validation Warning:
94
95 Unknown option ${chalk.bold(`"${opt}"`)} with value ${chalk.bold(
96 format(value, { inlineCharacterLimit: Number.POSITIVE_INFINITY })
97 )} was found in the config root.
98
99 ${helpMsg}
100
101Please refer to https://github.com/okonet/lint-staged#configuration for more information...`)
102
103const formatError = helpMsg => `● Validation Error:
104
105 ${helpMsg}
106
107Please refer to https://github.com/okonet/lint-staged#configuration for more information...`
108
109const createError = (opt, helpMsg, value) =>
110 formatError(`Invalid value for '${chalk.bold(opt)}'.
111
112 ${helpMsg}.
113
114 Configured value is: ${chalk.bold(
115 format(value, { inlineCharacterLimit: Number.POSITIVE_INFINITY })
116 )}`)
117
118/**
119 * Reporter for unknown options
120 * @param config
121 * @param option
122 * @returns {void}
123 */
124function unknownValidationReporter(config, option) {
125 /**
126 * If the unkonwn property is a glob this is probably
127 * a typical mistake of mixing simple and advanced configs
128 */
129 if (isGlob(option)) {
130 // prettier-ignore
131 const message = `You are probably trying to mix simple and advanced config formats. Adding
132
133 ${chalk.bold(`"linters": {
134 "${option}": ${JSON.stringify(config[option])}
135 }`)}
136
137 will fix it and remove this message.`
138
139 return logUnknown(option, message, config[option])
140 }
141
142 // If it is not glob pattern, simply notify of unknown value
143 return logUnknown(option, '', config[option])
144}
145
146/**
147 * For a given configuration object that we retrive from .lintstagedrc or package.json
148 * construct a full configuration with all options set.
149 *
150 * This is a bit tricky since we support 2 different syntxes: simple and full
151 * For simple config, only the `linters` configuration is provided.
152 *
153 * @param {Object} sourceConfig
154 * @returns {{
155 * concurrent: boolean, chunkSize: number, globOptions: {matchBase: boolean, dot: boolean}, linters: {}, subTaskConcurrency: number, renderer: string
156 * }}
157 */
158function getConfig(sourceConfig, debugMode) {
159 debug('Normalizing config')
160 const config = defaultsDeep(
161 {}, // Do not mutate sourceConfig!!!
162 isSimple(sourceConfig) ? { linters: sourceConfig } : sourceConfig,
163 defaultConfig
164 )
165
166 // Check if renderer is set in sourceConfig and if not, set accordingly to verbose
167 if (isObject(sourceConfig) && !sourceConfig.hasOwnProperty('renderer')) {
168 config.renderer = debugMode ? 'verbose' : 'update'
169 }
170
171 return config
172}
173
174/**
175 * Runs config validation. Throws error if the config is not valid.
176 * @param config {Object}
177 * @returns config {Object}
178 */
179function validateConfig(config) {
180 debug('Validating config')
181
182 const deprecatedConfig = {
183 gitDir: "lint-staged now automatically resolves '.git' directory.",
184 verbose: `Use the command line flag ${chalk.bold('--debug')} instead.`
185 }
186
187 const errors = []
188
189 try {
190 schema.validateSync(config, { abortEarly: false, strict: true })
191 } catch (error) {
192 error.errors.forEach(message => errors.push(formatError(message)))
193 }
194
195 if (isObject(config.linters)) {
196 Object.keys(config.linters).forEach(key => {
197 if (
198 (!isArray(config.linters[key]) || config.linters[key].some(item => !isString(item))) &&
199 !isString(config.linters[key])
200 ) {
201 errors.push(
202 createError(`linters[${key}]`, 'Should be a string or an array of strings', key)
203 )
204 }
205 })
206 }
207
208 Object.keys(config)
209 .filter(key => !defaultConfig.hasOwnProperty(key))
210 .forEach(option => {
211 if (deprecatedConfig.hasOwnProperty(option)) {
212 logDeprecation(option, deprecatedConfig[option])
213 return
214 }
215
216 unknownValidationReporter(config, option)
217 })
218
219 if (errors.length) {
220 throw new Error(errors.join('\n'))
221 }
222
223 return config
224}
225
226module.exports = {
227 getConfig,
228 validateConfig
229}