UNPKG

5.5 kBJavaScriptView Raw
1'use strict'
2
3const dedent = require('dedent')
4const { cosmiconfig } = require('cosmiconfig')
5const debugLog = require('debug')('lint-staged')
6const stringifyObject = require('stringify-object')
7
8const { PREVENTED_EMPTY_COMMIT, GIT_ERROR, RESTORE_STASH_EXAMPLE } = require('./messages')
9const printTaskOutput = require('./printTaskOutput')
10const runAll = require('./runAll')
11const { ApplyEmptyCommitError, GetBackupStashError, GitError } = require('./symbols')
12const formatConfig = require('./formatConfig')
13const validateConfig = require('./validateConfig')
14
15const errConfigNotFound = new Error('Config could not be found')
16
17function resolveConfig(configPath) {
18 try {
19 return require.resolve(configPath)
20 } catch {
21 return configPath
22 }
23}
24
25function loadConfig(configPath) {
26 const explorer = cosmiconfig('lint-staged', {
27 searchPlaces: [
28 'package.json',
29 '.lintstagedrc',
30 '.lintstagedrc.json',
31 '.lintstagedrc.yaml',
32 '.lintstagedrc.yml',
33 '.lintstagedrc.js',
34 '.lintstagedrc.cjs',
35 'lint-staged.config.js',
36 'lint-staged.config.cjs',
37 ],
38 })
39
40 return configPath ? explorer.load(resolveConfig(configPath)) : explorer.search()
41}
42
43/**
44 * @typedef {(...any) => void} LogFunction
45 * @typedef {{ error: LogFunction, log: LogFunction, warn: LogFunction }} Logger
46 *
47 * Root lint-staged function that is called from `bin/lint-staged`.
48 *
49 * @param {object} options
50 * @param {Object} [options.allowEmpty] - Allow empty commits when tasks revert all staged changes
51 * @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
52 * @param {object} [options.config] - Object with configuration for programmatic API
53 * @param {string} [options.configPath] - Path to configuration file
54 * @param {Object} [options.cwd] - Current working directory
55 * @param {boolean} [options.debug] - Enable debug mode
56 * @param {number} [options.maxArgLength] - Maximum argument string length
57 * @param {boolean} [options.quiet] - Disable lint-staged’s own console output
58 * @param {boolean} [options.relative] - Pass relative filepaths to tasks
59 * @param {boolean} [options.shell] - Skip parsing of tasks for better shell support
60 * @param {boolean} [options.stash] - Enable the backup stash, and revert in case of errors
61 * @param {boolean} [options.verbose] - Show task output even when tasks succeed; by default only failed output is shown
62 * @param {Logger} [logger]
63 *
64 * @returns {Promise<boolean>} Promise of whether the linting passed or failed
65 */
66module.exports = async function lintStaged(
67 {
68 allowEmpty = false,
69 concurrent = true,
70 config: configObject,
71 configPath,
72 cwd = process.cwd(),
73 debug = false,
74 maxArgLength,
75 quiet = false,
76 relative = false,
77 shell = false,
78 stash = true,
79 verbose = false,
80 } = {},
81 logger = console
82) {
83 try {
84 debugLog('Loading config using `cosmiconfig`')
85
86 const resolved = configObject
87 ? { config: configObject, filepath: '(input)' }
88 : await loadConfig(configPath)
89 if (resolved == null) throw errConfigNotFound
90
91 debugLog('Successfully loaded config from `%s`:\n%O', resolved.filepath, resolved.config)
92 // resolved.config is the parsed configuration object
93 // resolved.filepath is the path to the config file that was found
94 const formattedConfig = formatConfig(resolved.config)
95 const config = validateConfig(formattedConfig)
96 if (debug) {
97 // Log using logger to be able to test through `consolemock`.
98 logger.log('Running lint-staged with the following config:')
99 logger.log(stringifyObject(config, { indent: ' ' }))
100 } else {
101 // We might not be in debug mode but `DEBUG=lint-staged*` could have
102 // been set.
103 debugLog('lint-staged config:\n%O', config)
104 }
105
106 // Unset GIT_LITERAL_PATHSPECS to not mess with path interpretation
107 debugLog('Unset GIT_LITERAL_PATHSPECS (was `%s`)', process.env.GIT_LITERAL_PATHSPECS)
108 delete process.env.GIT_LITERAL_PATHSPECS
109
110 try {
111 const ctx = await runAll(
112 {
113 allowEmpty,
114 concurrent,
115 config,
116 cwd,
117 debug,
118 maxArgLength,
119 quiet,
120 relative,
121 shell,
122 stash,
123 verbose,
124 },
125 logger
126 )
127 debugLog('Tasks were executed successfully!')
128 printTaskOutput(ctx, logger)
129 return true
130 } catch (runAllError) {
131 const { ctx } = runAllError
132 if (ctx.errors.has(ApplyEmptyCommitError)) {
133 logger.warn(PREVENTED_EMPTY_COMMIT)
134 } else if (ctx.errors.has(GitError) && !ctx.errors.has(GetBackupStashError)) {
135 logger.error(GIT_ERROR)
136 if (ctx.shouldBackup) {
137 // No sense to show this if the backup stash itself is missing.
138 logger.error(RESTORE_STASH_EXAMPLE)
139 }
140 }
141
142 printTaskOutput(ctx, logger)
143 return false
144 }
145 } catch (lintStagedError) {
146 if (lintStagedError === errConfigNotFound) {
147 logger.error(`${lintStagedError.message}.`)
148 } else {
149 // It was probably a parsing error
150 logger.error(dedent`
151 Could not parse lint-staged config.
152
153 ${lintStagedError}
154 `)
155 }
156 logger.error() // empty line
157 // Print helpful message for all errors
158 logger.error(dedent`
159 Please make sure you have created it correctly.
160 See https://github.com/okonet/lint-staged#configuration.
161 `)
162
163 throw lintStagedError
164 }
165}