lint-staged
Version:
Lint files staged by git
130 lines (102 loc) • 4.67 kB
JavaScript
/** @typedef {import('./index').Logger} Logger */
import path from 'node:path'
import debug from 'debug'
import { execGit } from './execGit.js'
import { loadConfig } from './loadConfig.js'
import { normalizePath } from './normalizePath.js'
import { parseGitZOutput } from './parseGitZOutput.js'
import { validateConfig } from './validateConfig.js'
import { CONFIG_FILE_NAMES } from './configFiles.js'
const debugLog = debug('lint-staged:searchConfigs')
const EXEC_GIT = ['ls-files', '-z', '--full-name', '-t']
const filterPossibleConfigFiles = (file) => CONFIG_FILE_NAMES.includes(path.basename(file))
const numberOfLevels = (file) => file.split('/').length
const sortDeepestParth = (a, b) => (numberOfLevels(a) > numberOfLevels(b) ? -1 : 1)
const isInsideDirectory = (dir) => (file) => file.startsWith(normalizePath(dir))
/**
* Search all config files from the git repository, preferring those inside `cwd`.
*
* @param {object} options
* @param {Object} [options.configObject] - Explicit config object from the js API
* @param {string} [options.configPath] - Explicit path to a config file
* @param {string} [options.cwd] - Current working directory
* @param {Logger} logger
*
* @returns {Promise<{ [key: string]: { config: *, files: string[] } }>} found configs with filepath as key, and config as value
*/
export const searchConfigs = async (
{ configObject, configPath, cwd = process.cwd(), gitDir = cwd },
logger
) => {
debugLog('Searching for configuration files...')
// Return explicit config object from js API
if (configObject) {
debugLog('Using single direct configuration object...')
return { '': validateConfig(configObject, 'config object', logger) }
}
// Use only explicit config path instead of discovering multiple
if (configPath) {
debugLog('Using single configuration path...')
const { config, filepath } = await loadConfig({ configPath }, logger)
if (!config) return {}
return { [configPath]: validateConfig(config, filepath, logger) }
}
const [cachedFilesWithStatus, otherFilesWithStatus] = await Promise.all([
/** Get all possible config files known to git */
execGit(EXEC_GIT, { cwd: gitDir }).then(parseGitZOutput),
/** Get all possible config files from uncommitted files */
execGit([...EXEC_GIT, '--others', '--exclude-standard'], { cwd: gitDir }).then(parseGitZOutput),
])
/** Sort possible config files so that deepest is first */
const possibleConfigFiles = [...cachedFilesWithStatus, ...otherFilesWithStatus]
.flatMap(
/**
* Leave out lines starting with "S " to ignore not-checked-out files in a sparse repo.
* The "S" status means a tracked file that is "skip-worktree"
* @see https://git-scm.com/docs/git-ls-files#Documentation/git-ls-files.txt--t
*/ (line) => (line.startsWith('S ') ? [] : [line.replace(/^[HSMRCK?U] /, '')])
)
.filter(filterPossibleConfigFiles)
.map((file) => normalizePath(path.join(gitDir, file)))
.filter(isInsideDirectory(cwd))
.sort(sortDeepestParth)
debugLog('Found possible config files:', possibleConfigFiles)
/** Create object with key as config file, and value as null */
const configs = possibleConfigFiles.reduce(
(acc, configPath) => Object.assign(acc, { [configPath]: null }),
{}
)
/** Load and validate all configs to the above object */
await Promise.all(
Object.keys(configs).map((configPath) =>
loadConfig({ configPath }, logger).then(({ config, filepath }) => {
if (config) {
if (configPath !== filepath) {
debugLog('Config file "%s" resolved to "%s"', configPath, filepath)
}
configs[configPath] = validateConfig(config, filepath, logger)
}
})
)
)
/** Get validated configs from the above object, without any `null` values (not found) */
const foundConfigs = Object.entries(configs)
.filter(([, value]) => !!value)
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
/**
* Try to find a single config from parent directories
* to match old behavior before monorepo support
*/
if (!Object.keys(foundConfigs).length) {
debugLog('Could not find config files inside "%s"', cwd)
const { config, filepath } = await loadConfig({ cwd }, logger)
if (config) {
debugLog('Found parent configuration file from "%s"', filepath)
foundConfigs[filepath] = validateConfig(config, filepath, logger)
} else {
debugLog('Could not find parent configuration files from "%s"', cwd)
}
}
debugLog('Found %d config files', Object.keys(foundConfigs).length)
return foundConfigs
}