UNPKG

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