UNPKG

3.45 kBJavaScriptView Raw
1/** @typedef {import('./index').Logger} Logger */
2
3import debug from 'debug'
4import inspect from 'object-inspect'
5
6import { configurationError } from './messages.js'
7import { ConfigEmptyError, ConfigFormatError } from './symbols.js'
8import { validateBraces } from './validateBraces.js'
9
10const debugLog = debug('lint-staged:validateConfig')
11
12const isObject = (test) => test && typeof test === 'object' && !Array.isArray(test)
13
14const TEST_DEPRECATED_KEYS = new Map([
15 ['concurrent', (key) => typeof key === 'boolean'],
16 ['chunkSize', (key) => typeof key === 'number'],
17 ['globOptions', isObject],
18 ['linters', isObject],
19 ['ignore', (key) => Array.isArray(key)],
20 ['subTaskConcurrency', (key) => typeof key === 'number'],
21 ['renderer', (key) => typeof key === 'string'],
22 ['relative', (key) => typeof key === 'boolean'],
23])
24
25/**
26 * Runs config validation. Throws error if the config is not valid.
27 * @param {Object} config
28 * @param {string} configPath
29 * @param {Logger} logger
30 * @returns {Object} config
31 */
32export const validateConfig = (config, configPath, logger) => {
33 debugLog('Validating config from `%s`...', configPath)
34
35 if (!config || (typeof config !== 'object' && typeof config !== 'function')) {
36 throw ConfigFormatError
37 }
38
39 /**
40 * Function configurations receive all staged files as their argument.
41 * They are not further validated here to make sure the function gets
42 * evaluated only once.
43 *
44 * @see makeCmdTasks
45 */
46 if (typeof config === 'function') {
47 return { '*': config }
48 }
49
50 if (Object.entries(config).length === 0) {
51 throw ConfigEmptyError
52 }
53
54 const errors = []
55
56 /**
57 * Create a new validated config because the keys (patterns) might change.
58 * Since the Object.reduce method already loops through each entry in the config,
59 * it can be used for validating the values at the same time.
60 */
61 const validatedConfig = Object.entries(config).reduce((collection, [pattern, task]) => {
62 /** Versions < 9 had more complex configuration options that are no longer supported. */
63 if (TEST_DEPRECATED_KEYS.has(pattern)) {
64 const testFn = TEST_DEPRECATED_KEYS.get(pattern)
65 if (testFn(task)) {
66 errors.push(
67 configurationError(pattern, 'Advanced configuration has been deprecated.', task)
68 )
69 }
70
71 /** Return early for deprecated keys to skip validating their (deprecated) values */
72 return collection
73 }
74
75 if (
76 (!Array.isArray(task) ||
77 task.some((item) => typeof item !== 'string' && typeof item !== 'function')) &&
78 typeof task !== 'string' &&
79 typeof task !== 'function'
80 ) {
81 errors.push(
82 configurationError(
83 pattern,
84 'Should be a string, a function, or an array of strings and functions.',
85 task
86 )
87 )
88 }
89
90 /**
91 * A typical configuration error is using invalid brace expansion, like `*.{js}`.
92 * These are automatically fixed and warned about.
93 */
94 const fixedPattern = validateBraces(pattern, logger)
95
96 return { ...collection, [fixedPattern]: task }
97 }, {})
98
99 if (errors.length) {
100 const message = errors.join('\n\n')
101
102 logger.error(`Could not parse lint-staged config.
103
104${message}
105
106See https://github.com/okonet/lint-staged#configuration.`)
107
108 throw new Error(message)
109 }
110
111 debugLog('Validated config from `%s`:', configPath)
112 debugLog(inspect(config, { indent: 2 }))
113
114 return validatedConfig
115}