lint-staged
Version:
Lint files staged by git
191 lines (170 loc) • 6.13 kB
JavaScript
const dedent = require('dedent')
const { cosmiconfig } = require('cosmiconfig')
const debugLog = require('debug')('lint-staged')
const stringifyObject = require('stringify-object')
const { PREVENTED_EMPTY_COMMIT, GIT_ERROR, RESTORE_STASH_EXAMPLE } = require('./messages')
const printTaskOutput = require('./printTaskOutput')
const runAll = require('./runAll')
const {
ApplyEmptyCommitError,
ConfigNotFoundError,
GetBackupStashError,
GitError,
InvalidOptionsError,
} = require('./symbols')
const formatConfig = require('./formatConfig')
const validateConfig = require('./validateConfig')
const validateOptions = require('./validateOptions')
const resolveConfig = (configPath) => {
try {
return require.resolve(configPath)
} catch {
return configPath
}
}
const loadConfig = (configPath) => {
const explorer = cosmiconfig('lint-staged', {
searchPlaces: [
'package.json',
'.lintstagedrc',
'.lintstagedrc.json',
'.lintstagedrc.yaml',
'.lintstagedrc.yml',
'.lintstagedrc.js',
'.lintstagedrc.cjs',
'lint-staged.config.js',
'lint-staged.config.cjs',
],
})
return configPath ? explorer.load(resolveConfig(configPath)) : explorer.search()
}
/**
* @typedef {(...any) => void} LogFunction
* @typedef {{ error: LogFunction, log: LogFunction, warn: LogFunction }} Logger
*
* Root lint-staged function that is called from `bin/lint-staged`.
*
* @param {object} options
* @param {Object} [options.allowEmpty] - Allow empty commits when tasks revert all staged changes
* @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
* @param {object} [options.config] - Object with configuration for programmatic API
* @param {string} [options.configPath] - Path to configuration file
* @param {Object} [options.cwd] - Current working directory
* @param {boolean} [options.debug] - Enable debug mode
* @param {number} [options.maxArgLength] - Maximum argument string length
* @param {boolean} [options.quiet] - Disable lint-staged’s own console output
* @param {boolean} [options.relative] - Pass relative filepaths to tasks
* @param {boolean|string} [options.shell] - Skip parsing of tasks for better shell support
* @param {boolean} [options.stash] - Enable the backup stash, and revert in case of errors
* @param {boolean} [options.verbose] - Show task output even when tasks succeed; by default only failed output is shown
* @param {Logger} [logger]
*
* @returns {Promise<boolean>} Promise of whether the linting passed or failed
*/
const lintStaged = async (
{
allowEmpty = false,
concurrent = true,
config: configObject,
configPath,
cwd = process.cwd(),
debug = false,
maxArgLength,
quiet = false,
relative = false,
shell = false,
stash = true,
verbose = false,
} = {},
logger = console
) => {
try {
await validateOptions({ shell }, logger)
debugLog('Loading config using `cosmiconfig`')
const resolved = configObject
? { config: configObject, filepath: '(input)' }
: await loadConfig(configPath)
if (resolved == null) {
throw ConfigNotFoundError
}
debugLog('Successfully loaded config from `%s`:\n%O', resolved.filepath, resolved.config)
// resolved.config is the parsed configuration object
// resolved.filepath is the path to the config file that was found
const formattedConfig = formatConfig(resolved.config)
const config = validateConfig(formattedConfig)
if (debug) {
// Log using logger to be able to test through `consolemock`.
logger.log('Running lint-staged with the following config:')
logger.log(stringifyObject(config, { indent: ' ' }))
} else {
// We might not be in debug mode but `DEBUG=lint-staged*` could have
// been set.
debugLog('lint-staged config:\n%O', config)
}
// Unset GIT_LITERAL_PATHSPECS to not mess with path interpretation
debugLog('Unset GIT_LITERAL_PATHSPECS (was `%s`)', process.env.GIT_LITERAL_PATHSPECS)
delete process.env.GIT_LITERAL_PATHSPECS
try {
const ctx = await runAll(
{
allowEmpty,
concurrent,
config,
cwd,
debug,
maxArgLength,
quiet,
relative,
shell,
stash,
verbose,
},
logger
)
debugLog('Tasks were executed successfully!')
printTaskOutput(ctx, logger)
return true
} catch (runAllError) {
if (runAllError && runAllError.ctx && runAllError.ctx.errors) {
const { ctx } = runAllError
if (ctx.errors.has(ApplyEmptyCommitError)) {
logger.warn(PREVENTED_EMPTY_COMMIT)
} else if (ctx.errors.has(GitError) && !ctx.errors.has(GetBackupStashError)) {
logger.error(GIT_ERROR)
if (ctx.shouldBackup) {
// No sense to show this if the backup stash itself is missing.
logger.error(RESTORE_STASH_EXAMPLE)
}
}
printTaskOutput(ctx, logger)
return false
}
// Probably a compilation error in the config js file. Pass it up to the outer error handler for logging.
throw runAllError
}
} catch (lintStagedError) {
/** throw early because `validateOptions` options contains own logging */
if (lintStagedError === InvalidOptionsError) {
throw InvalidOptionsError
}
/** @todo move logging to `validateConfig` and remove this try/catch block */
if (lintStagedError === ConfigNotFoundError) {
logger.error(`${lintStagedError.message}.`)
} else {
// It was probably a parsing error
logger.error(dedent`
Could not parse lint-staged config.
${lintStagedError}
`)
}
logger.error() // empty line
// Print helpful message for all errors
logger.error(dedent`
Please make sure you have created it correctly.
See https://github.com/okonet/lint-staged#configuration.
`)
throw lintStagedError
}
}
module.exports = lintStaged