lint-staged
Advanced tools
Comparing version 10.5.4 to 14.0.0
#!/usr/bin/env node | ||
'use strict' | ||
import fs from 'node:fs/promises' | ||
const fs = require('fs') | ||
import { supportsColor } from 'chalk' | ||
import { Option, program } from 'commander' | ||
import debug from 'debug' | ||
import lintStaged from '../lib/index.js' | ||
import { CONFIG_STDIN_ERROR } from '../lib/messages.js' | ||
// Force colors for packages that depend on https://www.npmjs.com/package/supports-color | ||
const { supportsColor } = require('chalk') | ||
if (supportsColor && supportsColor.level) { | ||
if (supportsColor) { | ||
process.env.FORCE_COLOR = supportsColor.level.toString() | ||
@@ -16,77 +20,91 @@ } | ||
const pkg = require('../package.json') | ||
require('please-upgrade-node')( | ||
Object.assign({}, pkg, { | ||
engines: { | ||
node: '>=10.13.0', // First LTS release of 'Dubnium' | ||
}, | ||
}) | ||
const packageJson = JSON.parse(await fs.readFile(new URL('../package.json', import.meta.url))) | ||
const version = packageJson.version | ||
const debugLog = debug('lint-staged:bin') | ||
debugLog('Running `lint-staged@%s`', version) | ||
const cli = program.version(version) | ||
cli.option('--allow-empty', 'allow empty commits when tasks revert all staged changes', false) | ||
cli.option( | ||
'-p, --concurrent <number|boolean>', | ||
'the number of tasks to run concurrently, or false for serial', | ||
true | ||
) | ||
const cmdline = require('commander') | ||
const debugLib = require('debug') | ||
const lintStaged = require('../lib') | ||
const { CONFIG_STDIN_ERROR } = require('../lib/messages') | ||
cli.option('-c, --config [path]', 'path to configuration file, or - to read from stdin') | ||
const debug = debugLib('lint-staged:bin') | ||
cli.option('--cwd [path]', 'run all tasks in specific directory, instead of the current') | ||
cmdline | ||
.version(pkg.version) | ||
.option('--allow-empty', 'allow empty commits when tasks revert all staged changes', false) | ||
.option('-c, --config [path]', 'path to configuration file, or - to read from stdin') | ||
.option('-d, --debug', 'print additional debug information', false) | ||
.option('--no-stash', 'disable the backup stash, and do not revert in case of errors', false) | ||
.option( | ||
'-p, --concurrent <parallel tasks>', | ||
'the number of tasks to run concurrently, or false to run tasks serially', | ||
true | ||
cli.option('-d, --debug', 'print additional debug information', false) | ||
cli.addOption( | ||
new Option( | ||
'--diff [string]', | ||
'override the default "--staged" flag of "git diff" to get list of files. Implies "--no-stash".' | ||
).implies({ stash: false }) | ||
) | ||
cli.option( | ||
'--diff-filter [string]', | ||
'override the default "--diff-filter=ACMR" flag of "git diff" to get list of files' | ||
) | ||
cli.option('--max-arg-length [number]', 'maximum length of the command-line argument string', 0) | ||
/** | ||
* We don't want to show the `--stash` flag because it's on by default, and only show the | ||
* negatable flag `--no-stash` in stead. There seems to be a bug in Commander.js where | ||
* configuring only the latter won't actually set the default value. | ||
*/ | ||
cli | ||
.addOption( | ||
new Option('--stash', 'enable the backup stash, and revert in case of errors') | ||
.default(true) | ||
.hideHelp() | ||
) | ||
.option('-q, --quiet', 'disable lint-staged’s own console output', false) | ||
.option('-r, --relative', 'pass relative filepaths to tasks', false) | ||
.option('-x, --shell', 'skip parsing of tasks for better shell support', false) | ||
.option( | ||
'-v, --verbose', | ||
'show task output even when tasks succeed; by default only failed output is shown', | ||
false | ||
.addOption( | ||
new Option( | ||
'--no-stash', | ||
'disable the backup stash, and do not revert in case of errors' | ||
).default(false) | ||
) | ||
.parse(process.argv) | ||
if (cmdline.debug) { | ||
debugLib.enable('lint-staged*') | ||
} | ||
cli.option('-q, --quiet', 'disable lint-staged’s own console output', false) | ||
debug('Running `lint-staged@%s`', pkg.version) | ||
cli.option('-r, --relative', 'pass relative filepaths to tasks', false) | ||
/** | ||
* Get the maximum length of a command-line argument string based on current platform | ||
* | ||
* https://serverfault.com/questions/69430/what-is-the-maximum-length-of-a-command-line-in-mac-os-x | ||
* https://support.microsoft.com/en-us/help/830473/command-prompt-cmd-exe-command-line-string-limitation | ||
* https://unix.stackexchange.com/a/120652 | ||
*/ | ||
const getMaxArgLength = () => { | ||
switch (process.platform) { | ||
case 'darwin': | ||
return 262144 | ||
case 'win32': | ||
return 8191 | ||
default: | ||
return 131072 | ||
} | ||
cli.option('-x, --shell [path]', 'skip parsing of tasks for better shell support', false) | ||
cli.option( | ||
'-v, --verbose', | ||
'show task output even when tasks succeed; by default only failed output is shown', | ||
false | ||
) | ||
const cliOptions = cli.parse(process.argv).opts() | ||
if (cliOptions.debug) { | ||
debug.enable('lint-staged*') | ||
} | ||
const options = { | ||
allowEmpty: !!cmdline.allowEmpty, | ||
concurrent: JSON.parse(cmdline.concurrent), | ||
configPath: cmdline.config, | ||
debug: !!cmdline.debug, | ||
maxArgLength: getMaxArgLength() / 2, | ||
stash: !!cmdline.stash, // commander inverts `no-<x>` flags to `!x` | ||
quiet: !!cmdline.quiet, | ||
relative: !!cmdline.relative, | ||
shell: !!cmdline.shell, | ||
verbose: !!cmdline.verbose, | ||
allowEmpty: !!cliOptions.allowEmpty, | ||
concurrent: JSON.parse(cliOptions.concurrent), | ||
configPath: cliOptions.config, | ||
cwd: cliOptions.cwd, | ||
debug: !!cliOptions.debug, | ||
diff: cliOptions.diff, | ||
diffFilter: cliOptions.diffFilter, | ||
maxArgLength: cliOptions.maxArgLength || undefined, | ||
quiet: !!cliOptions.quiet, | ||
relative: !!cliOptions.relative, | ||
shell: cliOptions.shell /* Either a boolean or a string pointing to the shell */, | ||
stash: !!cliOptions.stash, // commander inverts `no-<x>` flags to `!x` | ||
verbose: !!cliOptions.verbose, | ||
} | ||
debug('Options parsed from command-line:', options) | ||
debugLog('Options parsed from command-line:', options) | ||
@@ -96,3 +114,3 @@ if (options.configPath === '-') { | ||
try { | ||
options.config = fs.readFileSync(process.stdin.fd, 'utf8').toString().trim() | ||
options.config = await fs.readFile(process.stdin.fd, 'utf8').toString().trim() | ||
} catch { | ||
@@ -110,8 +128,7 @@ console.error(CONFIG_STDIN_ERROR) | ||
lintStaged(options) | ||
.then((passed) => { | ||
process.exitCode = passed ? 0 : 1 | ||
}) | ||
.catch(() => { | ||
process.exitCode = 1 | ||
}) | ||
try { | ||
const passed = await lintStaged(options) | ||
process.exitCode = passed ? 0 : 1 | ||
} catch { | ||
process.exitCode = 1 | ||
} |
@@ -1,7 +0,9 @@ | ||
'use strict' | ||
import path from 'node:path' | ||
const debug = require('debug')('lint-staged:chunkFiles') | ||
const normalize = require('normalize-path') | ||
const path = require('path') | ||
import debug from 'debug' | ||
import { normalizePath } from './normalizePath.js' | ||
const debugLog = debug('lint-staged:chunkFiles') | ||
/** | ||
@@ -13,3 +15,3 @@ * Chunk array into sub-arrays | ||
*/ | ||
function chunkArray(arr, chunkCount) { | ||
const chunkArray = (arr, chunkCount) => { | ||
if (chunkCount === 1) return [arr] | ||
@@ -36,9 +38,9 @@ const chunked = [] | ||
*/ | ||
module.exports = function chunkFiles({ files, baseDir, maxArgLength = null, relative = false }) { | ||
export const chunkFiles = ({ files, baseDir, maxArgLength = null, relative = false }) => { | ||
const normalizedFiles = files.map((file) => | ||
normalize(relative || !baseDir ? file : path.resolve(baseDir, file)) | ||
normalizePath(relative || !baseDir ? file : path.resolve(baseDir, file)) | ||
) | ||
if (!maxArgLength) { | ||
debug('Skip chunking files because of undefined maxArgLength') | ||
debugLog('Skip chunking files because of undefined maxArgLength') | ||
return [normalizedFiles] // wrap in an array to return a single chunk | ||
@@ -48,8 +50,8 @@ } | ||
const fileListLength = normalizedFiles.join(' ').length | ||
debug( | ||
debugLog( | ||
`Resolved an argument string length of ${fileListLength} characters from ${normalizedFiles.length} files` | ||
) | ||
const chunkCount = Math.min(Math.ceil(fileListLength / maxArgLength), normalizedFiles.length) | ||
debug(`Creating ${chunkCount} chunks for maxArgLength of ${maxArgLength}`) | ||
debugLog(`Creating ${chunkCount} chunks for maxArgLength of ${maxArgLength}`) | ||
return chunkArray(normalizedFiles, chunkCount) | ||
} |
@@ -1,5 +0,5 @@ | ||
'use strict' | ||
import debug from 'debug' | ||
import { execa } from 'execa' | ||
const debug = require('debug')('lint-staged:git') | ||
const execa = require('execa') | ||
const debugLog = debug('lint-staged:execGit') | ||
@@ -12,6 +12,7 @@ /** | ||
const GIT_GLOBAL_OPTIONS = [...NO_SUBMODULE_RECURSE] | ||
// exported for tests | ||
export const GIT_GLOBAL_OPTIONS = [...NO_SUBMODULE_RECURSE] | ||
module.exports = async function execGit(cmd, options = {}) { | ||
debug('Running git command', cmd) | ||
export const execGit = async (cmd, options = {}) => { | ||
debugLog('Running git command', cmd) | ||
try { | ||
@@ -28,4 +29,1 @@ const { stdout } = await execa('git', GIT_GLOBAL_OPTIONS.concat(cmd), { | ||
} | ||
// exported for tests | ||
module.exports.GIT_GLOBAL_OPTIONS = GIT_GLOBAL_OPTIONS |
@@ -1,10 +0,6 @@ | ||
'use strict' | ||
import fs from 'node:fs/promises' | ||
const debug = require('debug')('lint-staged:file') | ||
const fs = require('fs') | ||
const { promisify } = require('util') | ||
import debug from 'debug' | ||
const fsReadFile = promisify(fs.readFile) | ||
const fsUnlink = promisify(fs.unlink) | ||
const fsWriteFile = promisify(fs.writeFile) | ||
const debugLog = debug('lint-staged:file') | ||
@@ -17,9 +13,9 @@ /** | ||
*/ | ||
const readFile = async (filename, ignoreENOENT = true) => { | ||
debug('Reading file `%s`', filename) | ||
export const readFile = async (filename, ignoreENOENT = true) => { | ||
debugLog('Reading file `%s`', filename) | ||
try { | ||
return await fsReadFile(filename) | ||
return await fs.readFile(filename) | ||
} catch (error) { | ||
if (ignoreENOENT && error.code === 'ENOENT') { | ||
debug("File `%s` doesn't exist, ignoring...", filename) | ||
debugLog("File `%s` doesn't exist, ignoring...", filename) | ||
return null // no-op file doesn't exist | ||
@@ -37,9 +33,9 @@ } else { | ||
*/ | ||
const unlink = async (filename, ignoreENOENT = true) => { | ||
debug('Removing file `%s`', filename) | ||
export const unlink = async (filename, ignoreENOENT = true) => { | ||
debugLog('Removing file `%s`', filename) | ||
try { | ||
await fsUnlink(filename) | ||
await fs.unlink(filename) | ||
} catch (error) { | ||
if (ignoreENOENT && error.code === 'ENOENT') { | ||
debug("File `%s` doesn't exist, ignoring...", filename) | ||
debugLog("File `%s` doesn't exist, ignoring...", filename) | ||
} else { | ||
@@ -56,11 +52,5 @@ throw error | ||
*/ | ||
const writeFile = async (filename, buffer) => { | ||
debug('Writing file `%s`', filename) | ||
await fsWriteFile(filename, buffer) | ||
export const writeFile = async (filename, buffer) => { | ||
debugLog('Writing file `%s`', filename) | ||
await fs.writeFile(filename, buffer) | ||
} | ||
module.exports = { | ||
readFile, | ||
unlink, | ||
writeFile, | ||
} |
@@ -1,9 +0,10 @@ | ||
'use strict' | ||
import path from 'node:path' | ||
const micromatch = require('micromatch') | ||
const normalize = require('normalize-path') | ||
const path = require('path') | ||
import debug from 'debug' | ||
import micromatch from 'micromatch' | ||
const debug = require('debug')('lint-staged:gen-tasks') | ||
import { normalizePath } from './normalizePath.js' | ||
const debugLog = debug('lint-staged:generateTasks') | ||
/** | ||
@@ -19,13 +20,6 @@ * Generates all task commands, and filelist | ||
*/ | ||
module.exports = function generateTasks({ | ||
config, | ||
cwd = process.cwd(), | ||
gitDir, | ||
files, | ||
relative = false, | ||
}) { | ||
debug('Generating linter tasks') | ||
export const generateTasks = ({ config, cwd = process.cwd(), files, relative = false }) => { | ||
debugLog('Generating linter tasks') | ||
const absoluteFiles = files.map((file) => normalize(path.resolve(gitDir, file))) | ||
const relativeFiles = absoluteFiles.map((file) => normalize(path.relative(cwd, file))) | ||
const relativeFiles = files.map((file) => normalizePath(path.relative(cwd, file))) | ||
@@ -35,23 +29,24 @@ return Object.entries(config).map(([pattern, commands]) => { | ||
const fileList = micromatch( | ||
relativeFiles | ||
// Only worry about children of the CWD unless the pattern explicitly | ||
// specifies that it concerns a parent directory. | ||
.filter((file) => { | ||
if (isParentDirPattern) return true | ||
return !file.startsWith('..') && !path.isAbsolute(file) | ||
}), | ||
pattern, | ||
{ | ||
cwd, | ||
dot: true, | ||
// If pattern doesn't look like a path, enable `matchBase` to | ||
// match against filenames in every directory. This makes `*.js` | ||
// match both `test.js` and `subdirectory/test.js`. | ||
matchBase: !pattern.includes('/'), | ||
} | ||
).map((file) => normalize(relative ? file : path.resolve(cwd, file))) | ||
// Only worry about children of the CWD unless the pattern explicitly | ||
// specifies that it concerns a parent directory. | ||
const filteredFiles = relativeFiles.filter((file) => { | ||
if (isParentDirPattern) return true | ||
return !file.startsWith('..') && !path.isAbsolute(file) | ||
}) | ||
const matches = micromatch(filteredFiles, pattern, { | ||
cwd, | ||
dot: true, | ||
// If the pattern doesn't look like a path, enable `matchBase` to | ||
// match against filenames in every directory. This makes `*.js` | ||
// match both `test.js` and `subdirectory/test.js`. | ||
matchBase: !pattern.includes('/'), | ||
posixSlashes: true, | ||
strictBrackets: true, | ||
}) | ||
const fileList = matches.map((file) => normalizePath(relative ? file : path.resolve(cwd, file))) | ||
const task = { pattern, commands, fileList } | ||
debug('Generated task: \n%O', task) | ||
debugLog('Generated task: \n%O', task) | ||
@@ -58,0 +53,0 @@ return task |
@@ -1,11 +0,64 @@ | ||
'use strict' | ||
import { EOL } from 'node:os' | ||
import { Writable } from 'node:stream' | ||
const getRenderer = ({ debug, quiet }, env = process.env) => { | ||
if (quiet) return { renderer: 'silent' } | ||
import { ListrLogger, ProcessOutput } from 'listr2' | ||
const EOLRegex = new RegExp(EOL + '$') | ||
const bindLogger = (consoleLogMethod) => | ||
new Writable({ | ||
write: function (chunk, encoding, next) { | ||
consoleLogMethod(chunk.toString().replace(EOLRegex, '')) | ||
next() | ||
}, | ||
}) | ||
const getMainRendererOptions = ({ debug, quiet }, logger, env) => { | ||
if (quiet) { | ||
return { | ||
renderer: 'silent', | ||
} | ||
} | ||
if (env.NODE_ENV === 'test') { | ||
return { | ||
renderer: 'test', | ||
rendererOptions: { | ||
logger: new ListrLogger({ | ||
processOutput: new ProcessOutput(bindLogger(logger.log), bindLogger(logger.error)), | ||
}), | ||
}, | ||
} | ||
} | ||
// Better support for dumb terminals: https://en.wikipedia.org/wiki/Computer_terminal#Dumb_terminals | ||
const isDumbTerminal = env.TERM === 'dumb' | ||
if (debug || isDumbTerminal || env.NODE_ENV === 'test') return { renderer: 'verbose' } | ||
return { renderer: 'update', rendererOptions: { dateFormat: false } } | ||
if (debug || env.TERM === 'dumb') { | ||
return { | ||
renderer: 'verbose', | ||
} | ||
} | ||
return { | ||
renderer: 'update', | ||
rendererOptions: { | ||
formatOutput: 'truncate', | ||
}, | ||
} | ||
} | ||
module.exports = getRenderer | ||
const getFallbackRenderer = ({ renderer }, { FORCE_COLOR }) => { | ||
if (renderer === 'silent' || renderer === 'test' || Number(FORCE_COLOR) > 0) { | ||
return renderer | ||
} | ||
return 'verbose' | ||
} | ||
export const getRenderer = (options, logger, env = process.env) => { | ||
const mainRendererOptions = getMainRendererOptions(options, logger, env) | ||
return { | ||
...mainRendererOptions, | ||
fallbackRenderer: getFallbackRenderer(mainRendererOptions, env), | ||
} | ||
} |
@@ -1,16 +0,14 @@ | ||
'use strict' | ||
import path from 'node:path' | ||
const execGit = require('./execGit') | ||
import { execGit } from './execGit.js' | ||
import { getDiffCommand } from './getDiffCommand.js' | ||
import { normalizePath } from './normalizePath.js' | ||
import { parseGitZOutput } from './parseGitZOutput.js' | ||
module.exports = async function getStagedFiles(options) { | ||
export const getStagedFiles = async ({ cwd = process.cwd(), diff, diffFilter } = {}) => { | ||
try { | ||
// Docs for --diff-filter option: https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---diff-filterACDMRTUXB82308203 | ||
// Docs for -z option: https://git-scm.com/docs/git-diff#Documentation/git-diff.txt--z | ||
const lines = await execGit( | ||
['diff', '--staged', '--diff-filter=ACMR', '--name-only', '-z'], | ||
options | ||
) | ||
// With `-z`, git prints `fileA\u0000fileB\u0000fileC\u0000` so we need to remove the last occurrence of `\u0000` before splitting | ||
// eslint-disable-next-line no-control-regex | ||
return lines ? lines.replace(/\u0000$/, '').split('\u0000') : [] | ||
const lines = await execGit(getDiffCommand(diff, diffFilter), { cwd }) | ||
if (!lines) return [] | ||
return parseGitZOutput(lines).map((file) => normalizePath(path.resolve(cwd, file))) | ||
} catch { | ||
@@ -17,0 +15,0 @@ return null |
@@ -1,9 +0,9 @@ | ||
'use strict' | ||
import path from 'node:path' | ||
const debug = require('debug')('lint-staged:git') | ||
const path = require('path') | ||
import debug from 'debug' | ||
const execGit = require('./execGit') | ||
const { readFile, unlink, writeFile } = require('./file') | ||
const { | ||
import { execGit } from './execGit.js' | ||
import { readFile, unlink, writeFile } from './file.js' | ||
import { getDiffCommand } from './getDiffCommand.js' | ||
import { | ||
GitError, | ||
@@ -16,4 +16,6 @@ RestoreOriginalStateError, | ||
RestoreUnstagedChangesError, | ||
} = require('./symbols') | ||
} from './symbols.js' | ||
const debugLog = debug('lint-staged:GitWorkflow') | ||
const MERGE_HEAD = 'MERGE_HEAD' | ||
@@ -45,3 +47,3 @@ const MERGE_MODE = 'MERGE_MODE' | ||
const STASH = 'lint-staged automatic backup' | ||
export const STASH = 'lint-staged automatic backup' | ||
@@ -68,4 +70,4 @@ const PATCH_UNSTAGED = 'lint-staged_unstaged.patch' | ||
class GitWorkflow { | ||
constructor({ allowEmpty, gitConfigDir, gitDir, matchedFileChunks }) { | ||
export class GitWorkflow { | ||
constructor({ allowEmpty, gitConfigDir, gitDir, matchedFileChunks, diff, diffFilter }) { | ||
this.execGit = (args, options = {}) => execGit(args, { ...options, cwd: gitDir }) | ||
@@ -75,3 +77,4 @@ this.deletedFiles = [] | ||
this.gitDir = gitDir | ||
this.unstagedDiff = null | ||
this.diff = diff | ||
this.diffFilter = diffFilter | ||
this.allowEmpty = allowEmpty | ||
@@ -107,3 +110,4 @@ this.matchedFileChunks = matchedFileChunks | ||
} | ||
return `refs/stash@{${index}}` | ||
return String(index) | ||
} | ||
@@ -115,3 +119,3 @@ | ||
async getDeletedFiles() { | ||
debug('Getting deleted files...') | ||
debugLog('Getting deleted files...') | ||
const lsFiles = await this.execGit(['ls-files', '--deleted']) | ||
@@ -122,3 +126,3 @@ const deletedFiles = lsFiles | ||
.map((file) => path.resolve(this.gitDir, file)) | ||
debug('Found deleted files:', deletedFiles) | ||
debugLog('Found deleted files:', deletedFiles) | ||
return deletedFiles | ||
@@ -131,3 +135,3 @@ } | ||
async backupMergeStatus() { | ||
debug('Backing up merge state...') | ||
debugLog('Backing up merge state...') | ||
await Promise.all([ | ||
@@ -138,3 +142,3 @@ readFile(this.mergeHeadFilename).then((buffer) => (this.mergeHeadBuffer = buffer)), | ||
]) | ||
debug('Done backing up merge state!') | ||
debugLog('Done backing up merge state!') | ||
} | ||
@@ -146,3 +150,3 @@ | ||
async restoreMergeStatus(ctx) { | ||
debug('Restoring merge state...') | ||
debugLog('Restoring merge state...') | ||
try { | ||
@@ -154,6 +158,6 @@ await Promise.all([ | ||
]) | ||
debug('Done restoring merge state!') | ||
debugLog('Done restoring merge state!') | ||
} catch (error) { | ||
debug('Failed restoring merge state with error:') | ||
debug(error) | ||
debugLog('Failed restoring merge state with error:') | ||
debugLog(error) | ||
handleError( | ||
@@ -173,3 +177,3 @@ new Error('Merge state could not be restored due to an error!'), | ||
async getPartiallyStagedFiles() { | ||
debug('Getting partially staged files...') | ||
debugLog('Getting partially staged files...') | ||
const status = await this.execGit(['status', '-z']) | ||
@@ -194,3 +198,3 @@ /** | ||
.filter(Boolean) // Filter empty string | ||
debug('Found partially staged files:', partiallyStaged) | ||
debugLog('Found partially staged files:', partiallyStaged) | ||
return partiallyStaged.length ? partiallyStaged : null | ||
@@ -204,3 +208,3 @@ } | ||
try { | ||
debug('Backing up original state...') | ||
debugLog('Backing up original state...') | ||
@@ -239,3 +243,3 @@ // Get a list of files with bot staged and unstaged changes. | ||
debug('Done backing up original state!') | ||
debugLog('Done backing up original state!') | ||
} catch (error) { | ||
@@ -267,3 +271,3 @@ handleError(error, ctx) | ||
async applyModifications(ctx) { | ||
debug('Adding task modifications to index...') | ||
debugLog('Adding task modifications to index...') | ||
@@ -278,5 +282,5 @@ // `matchedFileChunks` includes staged files that lint-staged originally detected and matched against a task. | ||
debug('Done adding task modifications to index!') | ||
debugLog('Done adding task modifications to index!') | ||
const stagedFilesAfterAdd = await this.execGit(['diff', '--name-only', '--cached']) | ||
const stagedFilesAfterAdd = await this.execGit(getDiffCommand(this.diff, this.diffFilter)) | ||
if (!stagedFilesAfterAdd && !this.allowEmpty) { | ||
@@ -295,3 +299,3 @@ // Tasks reverted all staged changes and the commit would be empty | ||
async restoreUnstagedChanges(ctx) { | ||
debug('Restoring unstaged changes...') | ||
debugLog('Restoring unstaged changes...') | ||
const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED) | ||
@@ -301,5 +305,5 @@ try { | ||
} catch (applyError) { | ||
debug('Error while restoring changes:') | ||
debug(applyError) | ||
debug('Retrying with 3-way merge') | ||
debugLog('Error while restoring changes:') | ||
debugLog(applyError) | ||
debugLog('Retrying with 3-way merge') | ||
try { | ||
@@ -309,4 +313,4 @@ // Retry with a 3-way merge if normal apply fails | ||
} catch (threeWayApplyError) { | ||
debug('Error while restoring unstaged changes using 3-way merge:') | ||
debug(threeWayApplyError) | ||
debugLog('Error while restoring unstaged changes using 3-way merge:') | ||
debugLog(threeWayApplyError) | ||
handleError( | ||
@@ -326,3 +330,3 @@ new Error('Unstaged changes could not be restored due to a merge conflict!'), | ||
try { | ||
debug('Restoring original state...') | ||
debugLog('Restoring original state...') | ||
await this.execGit(['reset', '--hard', 'HEAD']) | ||
@@ -340,3 +344,3 @@ await this.execGit(['stash', 'apply', '--quiet', '--index', await this.getBackupStash(ctx)]) | ||
debug('Done restoring original state!') | ||
debugLog('Done restoring original state!') | ||
} catch (error) { | ||
@@ -352,5 +356,5 @@ handleError(error, ctx, RestoreOriginalStateError) | ||
try { | ||
debug('Dropping backup stash...') | ||
debugLog('Dropping backup stash...') | ||
await this.execGit(['stash', 'drop', '--quiet', await this.getBackupStash(ctx)]) | ||
debug('Done dropping backup stash!') | ||
debugLog('Done dropping backup stash!') | ||
} catch (error) { | ||
@@ -361,3 +365,1 @@ handleError(error, ctx) | ||
} | ||
module.exports = GitWorkflow |
205
lib/index.js
@@ -1,43 +0,39 @@ | ||
'use strict' | ||
import debug from 'debug' | ||
const dedent = require('dedent') | ||
const { cosmiconfig } = require('cosmiconfig') | ||
const debugLog = require('debug')('lint-staged') | ||
const stringifyObject = require('stringify-object') | ||
import { | ||
PREVENTED_EMPTY_COMMIT, | ||
GIT_ERROR, | ||
RESTORE_STASH_EXAMPLE, | ||
NO_CONFIGURATION, | ||
} from './messages.js' | ||
import { printTaskOutput } from './printTaskOutput.js' | ||
import { runAll } from './runAll.js' | ||
import { | ||
ApplyEmptyCommitError, | ||
ConfigNotFoundError, | ||
GetBackupStashError, | ||
GitError, | ||
} from './symbols.js' | ||
import { validateOptions } from './validateOptions.js' | ||
const { PREVENTED_EMPTY_COMMIT, GIT_ERROR, RESTORE_STASH_EXAMPLE } = require('./messages') | ||
const printTaskOutput = require('./printTaskOutput') | ||
const runAll = require('./runAll') | ||
const { ApplyEmptyCommitError, GetBackupStashError, GitError } = require('./symbols') | ||
const formatConfig = require('./formatConfig') | ||
const validateConfig = require('./validateConfig') | ||
const debugLog = debug('lint-staged') | ||
const errConfigNotFound = new Error('Config could not be found') | ||
function resolveConfig(configPath) { | ||
try { | ||
return require.resolve(configPath) | ||
} catch { | ||
return configPath | ||
/** | ||
* Get the maximum length of a command-line argument string based on current platform | ||
* | ||
* https://serverfault.com/questions/69430/what-is-the-maximum-length-of-a-command-line-in-mac-os-x | ||
* https://support.microsoft.com/en-us/help/830473/command-prompt-cmd-exe-command-line-string-limitation | ||
* https://unix.stackexchange.com/a/120652 | ||
*/ | ||
const getMaxArgLength = () => { | ||
switch (process.platform) { | ||
case 'darwin': | ||
return 262144 | ||
case 'win32': | ||
return 8191 | ||
default: | ||
return 131072 | ||
} | ||
} | ||
function 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() | ||
} | ||
/** | ||
@@ -56,6 +52,8 @@ * @typedef {(...any) => void} LogFunction | ||
* @param {boolean} [options.debug] - Enable debug mode | ||
* @param {string} [options.diff] - Override the default "--staged" flag of "git diff" to get list of files | ||
* @param {string} [options.diffFilter] - Override the default "--diff-filter=ACMR" flag of "git diff" to get list of files | ||
* @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} [options.shell] - Skip parsing of tasks for better shell support | ||
* @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 | ||
@@ -67,3 +65,3 @@ * @param {boolean} [options.verbose] - Show task output even when tasks succeed; by default only failed output is shown | ||
*/ | ||
module.exports = async function lintStaged( | ||
const lintStaged = async ( | ||
{ | ||
@@ -74,100 +72,69 @@ allowEmpty = false, | ||
configPath, | ||
cwd = process.cwd(), | ||
cwd, | ||
debug = false, | ||
maxArgLength, | ||
diff, | ||
diffFilter, | ||
maxArgLength = getMaxArgLength() / 2, | ||
quiet = false, | ||
relative = false, | ||
shell = false, | ||
stash = true, | ||
// Stashing should be disabled by default when the `diff` option is used | ||
stash = diff === undefined, | ||
verbose = false, | ||
} = {}, | ||
logger = console | ||
) { | ||
try { | ||
debugLog('Loading config using `cosmiconfig`') | ||
) => { | ||
await validateOptions({ cwd, shell }, logger) | ||
const resolved = configObject | ||
? { config: configObject, filepath: '(input)' } | ||
: await loadConfig(configPath) | ||
if (resolved == null) throw errConfigNotFound | ||
// 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 | ||
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) | ||
} | ||
const options = { | ||
allowEmpty, | ||
concurrent, | ||
configObject, | ||
configPath, | ||
cwd, | ||
debug, | ||
diff, | ||
diffFilter, | ||
maxArgLength, | ||
quiet, | ||
relative, | ||
shell, | ||
stash, | ||
verbose, | ||
} | ||
// 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(options, logger) | ||
debugLog('Tasks were executed successfully!') | ||
printTaskOutput(ctx, logger) | ||
return true | ||
} catch (runAllError) { | ||
if (runAllError?.ctx?.errors) { | ||
const { ctx } = runAllError | ||
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) | ||
} | ||
if (ctx.errors.has(ConfigNotFoundError)) { | ||
logger.error(NO_CONFIGURATION) | ||
} else 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 | ||
printTaskOutput(ctx, logger) | ||
return false | ||
} | ||
} catch (lintStagedError) { | ||
if (lintStagedError === errConfigNotFound) { | ||
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 | ||
// Probably a compilation error in the config js file. Pass it up to the outer error handler for logging. | ||
throw runAllError | ||
} | ||
} | ||
export default lintStaged |
@@ -1,27 +0,9 @@ | ||
'use strict' | ||
import debug from 'debug' | ||
const cliTruncate = require('cli-truncate') | ||
const debug = require('debug')('lint-staged:make-cmd-tasks') | ||
import { configurationError } from './messages.js' | ||
import { resolveTaskFn } from './resolveTaskFn.js' | ||
const resolveTaskFn = require('./resolveTaskFn') | ||
const { createError } = require('./validateConfig') | ||
const debugLog = debug('lint-staged:makeCmdTasks') | ||
const STDOUT_COLUMNS_DEFAULT = 80 | ||
const listrPrefixLength = { | ||
update: ` X `.length, // indented task title where X is a checkmark or a cross (failure) | ||
verbose: `[STARTED] `.length, // verbose renderer uses 7-letter STARTED/SUCCESS prefixes | ||
} | ||
/** | ||
* Get length of title based on the number of available columns prefix length | ||
* @param {string} renderer The name of the Listr renderer | ||
* @returns {number} | ||
*/ | ||
const getTitleLength = (renderer, columns = process.stdout.columns) => { | ||
const prefixLength = listrPrefixLength[renderer] || 0 | ||
return (columns || STDOUT_COLUMNS_DEFAULT) - prefixLength | ||
} | ||
/** | ||
* Creates and returns an array of listr tasks which map to the given commands. | ||
@@ -31,10 +13,10 @@ * | ||
* @param {Array<string|Function>|string|Function} options.commands | ||
* @param {string} options.cwd | ||
* @param {Array<string>} options.files | ||
* @param {string} options.gitDir | ||
* @param {string} options.renderer | ||
* @param {Boolean} shell | ||
* @param {Boolean} verbose | ||
*/ | ||
const makeCmdTasks = async ({ commands, files, gitDir, renderer, shell, verbose }) => { | ||
debug('Creating listr tasks for commands %o', commands) | ||
export const makeCmdTasks = async ({ commands, cwd, files, gitDir, shell, verbose }) => { | ||
debugLog('Creating listr tasks for commands %o', commands) | ||
const commandArray = Array.isArray(commands) ? commands : [commands] | ||
@@ -55,3 +37,3 @@ const cmdTasks = [] | ||
throw new Error( | ||
createError( | ||
configurationError( | ||
'[Function]', | ||
@@ -64,6 +46,4 @@ 'Function task should return a string or an array of strings', | ||
// Truncate title to single line based on renderer | ||
const title = cliTruncate(command, getTitleLength(renderer)) | ||
const task = resolveTaskFn({ command, files, gitDir, isFn, shell, verbose }) | ||
cmdTasks.push({ title, command, task }) | ||
const task = resolveTaskFn({ command, cwd, files, gitDir, isFn, shell, verbose }) | ||
cmdTasks.push({ title: command, command, task }) | ||
} | ||
@@ -74,3 +54,1 @@ } | ||
} | ||
module.exports = makeCmdTasks |
@@ -1,36 +0,68 @@ | ||
'use strict' | ||
import { inspect } from 'node:util' | ||
const chalk = require('chalk') | ||
const { error, info, warning } = require('log-symbols') | ||
import chalk from 'chalk' | ||
const NOT_GIT_REPO = chalk.redBright(`${error} Current directory is not a git directory!`) | ||
import { error, info, warning } from './figures.js' | ||
const FAILED_GET_STAGED_FILES = chalk.redBright(`${error} Failed to get staged files!`) | ||
export const configurationError = (opt, helpMsg, value) => | ||
`${chalk.redBright(`${error} Validation Error:`)} | ||
const NO_STAGED_FILES = `${info} No staged files found.` | ||
Invalid value for '${chalk.bold(opt)}': ${chalk.bold(inspect(value))} | ||
const NO_TASKS = `${info} No staged files match any configured task.` | ||
${helpMsg}` | ||
const skippingBackup = (hasInitialCommit) => { | ||
const reason = hasInitialCommit ? '`--no-stash` was used' : 'there’s no initial commit yet' | ||
return `${warning} ${chalk.yellow(`Skipping backup because ${reason}.\n`)}` | ||
export const NOT_GIT_REPO = chalk.redBright(`${error} Current directory is not a git directory!`) | ||
export const FAILED_GET_STAGED_FILES = chalk.redBright(`${error} Failed to get staged files!`) | ||
export const incorrectBraces = (before, after) => | ||
chalk.yellow( | ||
`${warning} Detected incorrect braces with only single value: \`${before}\`. Reformatted as: \`${after}\` | ||
` | ||
) | ||
export const NO_CONFIGURATION = `${error} No valid configuration found.` | ||
export const NO_STAGED_FILES = `${info} No staged files found.` | ||
export const NO_TASKS = `${info} No staged files match any configured task.` | ||
export const skippingBackup = (hasInitialCommit, diff) => { | ||
const reason = | ||
diff !== undefined | ||
? '`--diff` was used' | ||
: hasInitialCommit | ||
? '`--no-stash` was used' | ||
: 'there’s no initial commit yet' | ||
return chalk.yellow(`${warning} Skipping backup because ${reason}.\n`) | ||
} | ||
const DEPRECATED_GIT_ADD = `${warning} ${chalk.yellow( | ||
`Some of your tasks use \`git add\` command. Please remove it from the config since all modifications made by tasks will be automatically added to the git commit index.` | ||
)} | ||
export const DEPRECATED_GIT_ADD = chalk.yellow( | ||
`${warning} Some of your tasks use \`git add\` command. Please remove it from the config since all modifications made by tasks will be automatically added to the git commit index. | ||
` | ||
) | ||
const TASK_ERROR = 'Skipped because of errors from tasks.' | ||
export const TASK_ERROR = 'Skipped because of errors from tasks.' | ||
const SKIPPED_GIT_ERROR = 'Skipped because of previous git error.' | ||
export const SKIPPED_GIT_ERROR = 'Skipped because of previous git error.' | ||
const GIT_ERROR = `\n ${error} ${chalk.red(`lint-staged failed due to a git error.`)}` | ||
export const GIT_ERROR = `\n ${chalk.redBright(`${error} lint-staged failed due to a git error.`)}` | ||
const PREVENTED_EMPTY_COMMIT = ` | ||
${warning} ${chalk.yellow(`lint-staged prevented an empty git commit. | ||
export const invalidOption = (name, value, message) => `${chalk.redBright( | ||
`${error} Validation Error:` | ||
)} | ||
Invalid value for option '${chalk.bold(name)}': ${chalk.bold(value)} | ||
${message} | ||
See https://github.com/okonet/lint-staged#command-line-flags` | ||
export const PREVENTED_EMPTY_COMMIT = ` | ||
${chalk.yellow(`${warning} lint-staged prevented an empty git commit. | ||
Use the --allow-empty option to continue, or check your task configuration`)} | ||
` | ||
const RESTORE_STASH_EXAMPLE = ` Any lost modifications can be restored from a git stash: | ||
export const RESTORE_STASH_EXAMPLE = ` Any lost modifications can be restored from a git stash: | ||
@@ -42,17 +74,2 @@ > git stash list | ||
const CONFIG_STDIN_ERROR = 'Error: Could not read config from stdin.' | ||
module.exports = { | ||
NOT_GIT_REPO, | ||
FAILED_GET_STAGED_FILES, | ||
NO_STAGED_FILES, | ||
NO_TASKS, | ||
skippingBackup, | ||
DEPRECATED_GIT_ADD, | ||
TASK_ERROR, | ||
SKIPPED_GIT_ERROR, | ||
GIT_ERROR, | ||
PREVENTED_EMPTY_COMMIT, | ||
RESTORE_STASH_EXAMPLE, | ||
CONFIG_STDIN_ERROR, | ||
} | ||
export const CONFIG_STDIN_ERROR = 'Error: Could not read config from stdin.' |
@@ -1,3 +0,1 @@ | ||
'use strict' | ||
/** | ||
@@ -8,5 +6,5 @@ * Handle logging of listr `ctx.output` to the specified `logger` | ||
*/ | ||
const printTaskOutput = (ctx = {}, logger) => { | ||
export const printTaskOutput = (ctx = {}, logger) => { | ||
if (!Array.isArray(ctx.output)) return | ||
const log = ctx.errors && ctx.errors.size > 0 ? logger.error : logger.log | ||
const log = ctx.errors?.size > 0 ? logger.error : logger.log | ||
for (const line of ctx.output) { | ||
@@ -16,3 +14,1 @@ log(line) | ||
} | ||
module.exports = printTaskOutput |
@@ -1,13 +0,11 @@ | ||
'use strict' | ||
import fs from 'node:fs/promises' | ||
import path from 'node:path' | ||
const normalize = require('normalize-path') | ||
const debugLog = require('debug')('lint-staged:resolveGitRepo') | ||
const fs = require('fs') | ||
const path = require('path') | ||
const { promisify } = require('util') | ||
import debug from 'debug' | ||
const execGit = require('./execGit') | ||
const { readFile } = require('./file') | ||
import { execGit } from './execGit.js' | ||
import { readFile } from './file.js' | ||
import { normalizePath } from './normalizePath.js' | ||
const fsLstat = promisify(fs.lstat) | ||
const debugLog = debug('lint-staged:resolveGitRepo') | ||
@@ -19,4 +17,5 @@ /** | ||
const resolveGitConfigDir = async (gitDir) => { | ||
const defaultDir = normalize(path.join(gitDir, '.git')) | ||
const stats = await fsLstat(defaultDir) | ||
// Get the real path in case it's a symlink | ||
const defaultDir = normalizePath(await fs.realpath(path.join(gitDir, '.git'))) | ||
const stats = await fs.lstat(defaultDir) | ||
// If .git is a directory, use it | ||
@@ -29,6 +28,22 @@ if (stats.isDirectory()) return defaultDir | ||
// exported for test | ||
export const determineGitDir = (cwd, relativeDir) => { | ||
// if relative dir and cwd have different endings normalize it | ||
// this happens under windows, where normalize is unable to normalize git's output | ||
if (relativeDir && relativeDir.endsWith(path.sep)) { | ||
relativeDir = relativeDir.slice(0, -1) | ||
} | ||
if (relativeDir) { | ||
// the current working dir is inside the git top-level directory | ||
return normalizePath(cwd.substring(0, cwd.lastIndexOf(relativeDir))) | ||
} else { | ||
// the current working dir is the top-level git directory | ||
return normalizePath(cwd) | ||
} | ||
} | ||
/** | ||
* Resolve git directory and possible submodule paths | ||
*/ | ||
const resolveGitRepo = async (cwd) => { | ||
export const resolveGitRepo = async (cwd = process.cwd()) => { | ||
try { | ||
@@ -43,4 +58,7 @@ debugLog('Resolving git repo from `%s`', cwd) | ||
const gitDir = normalize(await execGit(['rev-parse', '--show-toplevel'], { cwd })) | ||
const gitConfigDir = normalize(await resolveGitConfigDir(gitDir)) | ||
// read the path of the current directory relative to the top-level directory | ||
// don't read the toplevel directly, it will lead to an posix conform path on non posix systems (cygwin) | ||
const gitRel = normalizePath(await execGit(['rev-parse', '--show-prefix'], { cwd })) | ||
const gitDir = determineGitDir(normalizePath(cwd), gitRel) | ||
const gitConfigDir = normalizePath(await resolveGitConfigDir(gitDir)) | ||
@@ -56,3 +74,1 @@ debugLog('Resolved git directory to be `%s`', gitDir) | ||
} | ||
module.exports = resolveGitRepo |
@@ -1,14 +0,17 @@ | ||
'use strict' | ||
import chalk from 'chalk' | ||
import { execa, execaCommand } from 'execa' | ||
import debug from 'debug' | ||
import { parseArgsStringToArgv } from 'string-argv' | ||
import pidTree from 'pidtree' | ||
const { redBright, dim } = require('chalk') | ||
const execa = require('execa') | ||
const debug = require('debug')('lint-staged:task') | ||
const { parseArgsStringToArgv } = require('string-argv') | ||
const { error, info } = require('log-symbols') | ||
import { error, info } from './figures.js' | ||
import { getInitialState } from './state.js' | ||
import { TaskError } from './symbols.js' | ||
const { getInitialState } = require('./state') | ||
const { TaskError } = require('./symbols') | ||
const TASK_ERROR = 'lint-staged:taskError' | ||
const getTag = ({ code, killed, signal }) => signal || (killed && 'KILLED') || code || 'FAILED' | ||
const debugLog = debug('lint-staged:resolveTaskFn') | ||
const getTag = ({ code, killed, signal }) => (killed && 'KILLED') || signal || code || 'FAILED' | ||
/** | ||
@@ -32,3 +35,3 @@ * Handle task console output. | ||
if (hasOutput) { | ||
const outputTitle = isError ? redBright(`${error} ${command}:`) : `${info} ${command}:` | ||
const outputTitle = isError ? chalk.redBright(`${error} ${command}:`) : `${info} ${command}:` | ||
const output = [] | ||
@@ -42,3 +45,3 @@ .concat(ctx.quiet ? [] : ['', outputTitle]) | ||
const tag = getTag(result) | ||
const message = redBright(`\n${error} ${command} failed without output (${tag}).`) | ||
const message = chalk.redBright(`\n${error} ${command} failed without output (${tag}).`) | ||
if (!ctx.quiet) ctx.output.push(message) | ||
@@ -49,2 +52,51 @@ } | ||
/** | ||
* Kill an execa process along with all its child processes. | ||
* @param {execa.ExecaChildProcess<string>} execaProcess | ||
*/ | ||
const killExecaProcess = async (execaProcess) => { | ||
try { | ||
const childPids = await pidTree(execaProcess.pid) | ||
for (const childPid of childPids) { | ||
try { | ||
process.kill(childPid) | ||
} catch (error) { | ||
debugLog(`Failed to kill process with pid "%d": %o`, childPid, error) | ||
} | ||
} | ||
} catch (error) { | ||
// Suppress "No matching pid found" error. This probably means | ||
// the process already died before executing. | ||
debugLog(`Failed to kill process with pid "%d": %o`, execaProcess.pid, error) | ||
} | ||
// The execa process is killed separately in order to get the `KILLED` status. | ||
execaProcess.kill() | ||
} | ||
/** | ||
* Interrupts the execution of the execa process that we spawned if | ||
* another task adds an error to the context. | ||
* | ||
* @param {Object} ctx | ||
* @param {execa.ExecaChildProcess<string>} execaChildProcess | ||
* @returns {() => Promise<void>} Function that clears the interval that | ||
* checks the context. | ||
*/ | ||
const interruptExecutionOnError = (ctx, execaChildProcess) => { | ||
let killPromise | ||
const errorListener = async () => { | ||
killPromise = killExecaProcess(execaChildProcess) | ||
await killPromise | ||
} | ||
ctx.events.on(TASK_ERROR, errorListener, { once: true }) | ||
return async () => { | ||
ctx.events.off(TASK_ERROR, errorListener) | ||
await killPromise | ||
} | ||
} | ||
/** | ||
* Create a error output dependding on process result. | ||
@@ -64,5 +116,9 @@ * | ||
ctx.errors.add(TaskError) | ||
// https://nodejs.org/api/events.html#error-events | ||
ctx.events.emit(TASK_ERROR, TaskError) | ||
handleOutput(command, result, ctx, true) | ||
const tag = getTag(result) | ||
return new Error(`${redBright(command)} ${dim(`[${tag}]`)}`) | ||
return new Error(`${chalk.redBright(command)} ${chalk.dim(`[${tag}]`)}`) | ||
} | ||
@@ -75,38 +131,43 @@ | ||
* @param {string} options.command — Linter task | ||
* @param {string} [options.cwd] | ||
* @param {String} options.gitDir - Current git repo path | ||
* @param {Boolean} options.isFn - Whether the linter task is a function | ||
* @param {Array<string>} options.files — Filepaths to run the linter task against | ||
* @param {Boolean} [options.relative] — Whether the filepaths should be relative | ||
* @param {Boolean} [options.shell] — Whether to skip parsing linter task for better shell support | ||
* @param {Boolean} [options.verbose] — Always show task verbose | ||
* @returns {function(): Promise<Array<string>>} | ||
* @returns {() => Promise<Array<string>>} | ||
*/ | ||
module.exports = function resolveTaskFn({ | ||
export const resolveTaskFn = ({ | ||
command, | ||
cwd = process.cwd(), | ||
files, | ||
gitDir, | ||
isFn, | ||
relative, | ||
shell = false, | ||
verbose = false, | ||
}) { | ||
}) => { | ||
const [cmd, ...args] = parseArgsStringToArgv(command) | ||
debug('cmd:', cmd) | ||
debug('args:', args) | ||
debugLog('cmd:', cmd) | ||
debugLog('args:', args) | ||
const execaOptions = { preferLocal: true, reject: false, shell } | ||
if (relative) { | ||
execaOptions.cwd = process.cwd() | ||
} else if (/^git(\.exe)?/i.test(cmd) && gitDir !== process.cwd()) { | ||
const execaOptions = { | ||
// Only use gitDir as CWD if we are using the git binary | ||
// e.g `npm` should run tasks in the actual CWD | ||
execaOptions.cwd = gitDir | ||
cwd: /^git(\.exe)?/i.test(cmd) ? gitDir : cwd, | ||
preferLocal: true, | ||
reject: false, | ||
shell, | ||
} | ||
debug('execaOptions:', execaOptions) | ||
debugLog('execaOptions:', execaOptions) | ||
return async (ctx = getInitialState()) => { | ||
const result = await (shell | ||
? execa.command(isFn ? command : `${command} ${files.join(' ')}`, execaOptions) | ||
: execa(cmd, isFn ? args : args.concat(files), execaOptions)) | ||
const execaChildProcess = shell | ||
? execaCommand(isFn ? command : `${command} ${files.join(' ')}`, execaOptions) | ||
: execa(cmd, isFn ? args : args.concat(files), execaOptions) | ||
const quitInterruptCheck = interruptExecutionOnError(ctx, execaChildProcess) | ||
const result = await execaChildProcess | ||
await quitInterruptCheck() | ||
if (result.failed || result.killed || result.signal != null) { | ||
@@ -113,0 +174,0 @@ throw makeErr(command, result, ctx) |
@@ -1,16 +0,18 @@ | ||
'use strict' | ||
/** @typedef {import('./index').Logger} Logger */ | ||
const { Listr } = require('listr2') | ||
import path from 'node:path' | ||
const chunkFiles = require('./chunkFiles') | ||
const debugLog = require('debug')('lint-staged:run') | ||
const execGit = require('./execGit') | ||
const generateTasks = require('./generateTasks') | ||
const getRenderer = require('./getRenderer') | ||
const getStagedFiles = require('./getStagedFiles') | ||
const GitWorkflow = require('./gitWorkflow') | ||
const makeCmdTasks = require('./makeCmdTasks') | ||
const { | ||
import chalk from 'chalk' | ||
import debug from 'debug' | ||
import { Listr } from 'listr2' | ||
import { chunkFiles } from './chunkFiles.js' | ||
import { execGit } from './execGit.js' | ||
import { generateTasks } from './generateTasks.js' | ||
import { getRenderer } from './getRenderer.js' | ||
import { getStagedFiles } from './getStagedFiles.js' | ||
import { GitWorkflow } from './gitWorkflow.js' | ||
import { groupFilesByConfig } from './groupFilesByConfig.js' | ||
import { makeCmdTasks } from './makeCmdTasks.js' | ||
import { | ||
DEPRECATED_GIT_ADD, | ||
@@ -23,5 +25,6 @@ FAILED_GET_STAGED_FILES, | ||
skippingBackup, | ||
} = require('./messages') | ||
const resolveGitRepo = require('./resolveGitRepo') | ||
const { | ||
} from './messages.js' | ||
import { normalizePath } from './normalizePath.js' | ||
import { resolveGitRepo } from './resolveGitRepo.js' | ||
import { | ||
applyModificationsSkipped, | ||
@@ -35,5 +38,8 @@ cleanupEnabled, | ||
restoreUnstagedChangesSkipped, | ||
} = require('./state') | ||
const { GitRepoError, GetStagedFilesError, GitError } = require('./symbols') | ||
} from './state.js' | ||
import { GitRepoError, GetStagedFilesError, GitError, ConfigNotFoundError } from './symbols.js' | ||
import { searchConfigs } from './searchConfigs.js' | ||
const debugLog = debug('lint-staged:runAll') | ||
const createError = (ctx) => Object.assign(new Error('lint-staged failed'), { ctx }) | ||
@@ -45,7 +51,10 @@ | ||
* @param {object} options | ||
* @param {Object} [options.allowEmpty] - Allow empty commits when tasks revert all staged changes | ||
* @param {boolean} [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] - Task configuration | ||
* @param {Object} [options.cwd] - Current working directory | ||
* @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 {boolean} [options.debug] - Enable debug mode | ||
* @param {string} [options.diff] - Override the default "--staged" flag of "git diff" to get list of files | ||
* @param {string} [options.diffFilter] - Override the default "--diff-filter=ACMR" flag of "git diff" to get list of files | ||
* @param {number} [options.maxArgLength] - Maximum argument string length | ||
@@ -60,9 +69,12 @@ * @param {boolean} [options.quiet] - Disable lint-staged’s own console output | ||
*/ | ||
const runAll = async ( | ||
export const runAll = async ( | ||
{ | ||
allowEmpty = false, | ||
concurrent = true, | ||
config, | ||
cwd = process.cwd(), | ||
configObject, | ||
configPath, | ||
cwd, | ||
debug = false, | ||
diff, | ||
diffFilter, | ||
maxArgLength, | ||
@@ -72,3 +84,4 @@ quiet = false, | ||
shell = false, | ||
stash = true, | ||
// Stashing should be disabled by default when the `diff` option is used | ||
stash = diff === undefined, | ||
verbose = false, | ||
@@ -78,4 +91,9 @@ }, | ||
) => { | ||
debugLog('Running all linter scripts') | ||
debugLog('Running all linter scripts...') | ||
// Resolve relative CWD option | ||
const hasExplicitCwd = !!cwd | ||
cwd = hasExplicitCwd ? path.resolve(cwd) : process.cwd() | ||
debugLog('Using working directory `%s`', cwd) | ||
const ctx = getInitialState({ quiet }) | ||
@@ -96,9 +114,10 @@ | ||
// Lint-staged should create a backup stash only when there's an initial commit | ||
// Lint-staged will create a backup stash only when there's an initial commit, | ||
// and when using the default list of staged files by default | ||
ctx.shouldBackup = hasInitialCommit && stash | ||
if (!ctx.shouldBackup) { | ||
logger.warn(skippingBackup(hasInitialCommit)) | ||
logger.warn(skippingBackup(hasInitialCommit, diff)) | ||
} | ||
const files = await getStagedFiles({ cwd: gitDir }) | ||
const files = await getStagedFiles({ cwd: gitDir, diff, diffFilter }) | ||
if (!files) { | ||
@@ -117,6 +136,19 @@ if (!quiet) ctx.output.push(FAILED_GET_STAGED_FILES) | ||
const stagedFileChunks = chunkFiles({ baseDir: gitDir, files, maxArgLength, relative }) | ||
const chunkCount = stagedFileChunks.length | ||
if (chunkCount > 1) debugLog(`Chunked staged files into ${chunkCount} part`, chunkCount) | ||
const foundConfigs = await searchConfigs({ configObject, configPath, cwd, gitDir }, logger) | ||
const numberOfConfigs = Object.keys(foundConfigs).length | ||
// Throw if no configurations were found | ||
if (numberOfConfigs === 0) { | ||
ctx.errors.add(ConfigNotFoundError) | ||
throw createError(ctx, ConfigNotFoundError) | ||
} | ||
const filesByConfig = await groupFilesByConfig({ | ||
configs: foundConfigs, | ||
files, | ||
singleConfigMode: configObject || configPath !== undefined, | ||
}) | ||
const hasMultipleConfigs = numberOfConfigs > 1 | ||
// lint-staged 10 will automatically add modifications to index | ||
@@ -129,5 +161,4 @@ // Warn user when their command includes `git add` | ||
exitOnError: false, | ||
nonTTYRenderer: 'verbose', | ||
registerSignalListeners: false, | ||
...getRenderer({ debug, quiet }), | ||
...getRenderer({ debug, quiet }, logger), | ||
} | ||
@@ -140,37 +171,78 @@ | ||
for (const [index, files] of stagedFileChunks.entries()) { | ||
const chunkTasks = generateTasks({ config, cwd, gitDir, files, relative }) | ||
const chunkListrTasks = [] | ||
for (const [configPath, { config, files }] of Object.entries(filesByConfig)) { | ||
const configName = configPath ? normalizePath(path.relative(cwd, configPath)) : 'Config object' | ||
for (const task of chunkTasks) { | ||
const subTasks = await makeCmdTasks({ | ||
commands: task.commands, | ||
files: task.fileList, | ||
gitDir, | ||
renderer: listrOptions.renderer, | ||
shell, | ||
verbose, | ||
}) | ||
const stagedFileChunks = chunkFiles({ baseDir: gitDir, files, maxArgLength, relative }) | ||
// Add files from task to match set | ||
task.fileList.forEach((file) => { | ||
matchedFiles.add(file) | ||
}) | ||
// Use actual cwd if it's specified, or there's only a single config file. | ||
// Otherwise use the directory of the config file for each config group, | ||
// to make sure tasks are separated from each other. | ||
const groupCwd = hasMultipleConfigs && !hasExplicitCwd ? path.dirname(configPath) : cwd | ||
hasDeprecatedGitAdd = subTasks.some((subTask) => subTask.command === 'git add') | ||
const chunkCount = stagedFileChunks.length | ||
if (chunkCount > 1) { | ||
debugLog('Chunked staged files from `%s` into %d part', configPath, chunkCount) | ||
} | ||
chunkListrTasks.push({ | ||
title: `Running tasks for ${task.pattern}`, | ||
task: async () => | ||
new Listr(subTasks, { | ||
// In sub-tasks we don't want to run concurrently | ||
// and we want to abort on errors | ||
...listrOptions, | ||
concurrent: false, | ||
exitOnError: true, | ||
}), | ||
for (const [index, files] of stagedFileChunks.entries()) { | ||
const chunkListrTasks = await Promise.all( | ||
generateTasks({ config, cwd: groupCwd, files, relative }).map((task) => | ||
makeCmdTasks({ | ||
commands: task.commands, | ||
cwd: groupCwd, | ||
files: task.fileList, | ||
gitDir, | ||
shell, | ||
verbose, | ||
}).then((subTasks) => { | ||
// Add files from task to match set | ||
task.fileList.forEach((file) => { | ||
// Make sure relative files are normalized to the | ||
// group cwd, because other there might be identical | ||
// relative filenames in the entire set. | ||
const normalizedFile = path.isAbsolute(file) | ||
? file | ||
: normalizePath(path.join(groupCwd, file)) | ||
matchedFiles.add(normalizedFile) | ||
}) | ||
hasDeprecatedGitAdd = | ||
hasDeprecatedGitAdd || subTasks.some((subTask) => subTask.command === 'git add') | ||
const fileCount = task.fileList.length | ||
return { | ||
title: `${task.pattern}${chalk.dim( | ||
` — ${fileCount} ${fileCount === 1 ? 'file' : 'files'}` | ||
)}`, | ||
task: async (ctx, task) => | ||
task.newListr( | ||
subTasks, | ||
// Subtasks should not run in parallel, and should exit on error | ||
{ concurrent: false, exitOnError: true } | ||
), | ||
skip: () => { | ||
// Skip task when no files matched | ||
if (fileCount === 0) { | ||
return `${task.pattern}${chalk.dim(' — no files')}` | ||
} | ||
return false | ||
}, | ||
} | ||
}) | ||
) | ||
) | ||
listrTasks.push({ | ||
title: | ||
`${configName}${chalk.dim(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` + | ||
(chunkCount > 1 ? chalk.dim(` (chunk ${index + 1}/${chunkCount})...`) : ''), | ||
task: (ctx, task) => task.newListr(chunkListrTasks, { concurrent, exitOnError: true }), | ||
skip: () => { | ||
// Skip task when no files matched | ||
if (task.fileList.length === 0) { | ||
return `No staged files match ${task.pattern}` | ||
// Skip if the first step (backup) failed | ||
if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR | ||
// Skip chunk when no every task is skipped (due to no matches) | ||
if (chunkListrTasks.every((task) => task.skip())) { | ||
return `${configName}${chalk.dim(' — no tasks to run')}` | ||
} | ||
@@ -181,16 +253,2 @@ return false | ||
} | ||
listrTasks.push({ | ||
// No need to show number of task chunks when there's only one | ||
title: | ||
chunkCount > 1 ? `Running tasks (chunk ${index + 1}/${chunkCount})...` : 'Running tasks...', | ||
task: () => new Listr(chunkListrTasks, { ...listrOptions, concurrent }), | ||
skip: () => { | ||
// Skip if the first step (backup) failed | ||
if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR | ||
// Skip chunk when no every task is skipped (due to no matches) | ||
if (chunkListrTasks.every((task) => task.skip())) return 'No tasks to run.' | ||
return false | ||
}, | ||
}) | ||
} | ||
@@ -218,3 +276,10 @@ | ||
const git = new GitWorkflow({ allowEmpty, gitConfigDir, gitDir, matchedFileChunks }) | ||
const git = new GitWorkflow({ | ||
allowEmpty, | ||
gitConfigDir, | ||
gitDir, | ||
matchedFileChunks, | ||
diff, | ||
diffFilter, | ||
}) | ||
@@ -224,3 +289,3 @@ const runner = new Listr( | ||
{ | ||
title: 'Preparing...', | ||
title: 'Preparing lint-staged...', | ||
task: (ctx) => git.prepare(ctx), | ||
@@ -233,5 +298,9 @@ }, | ||
}, | ||
...listrTasks, | ||
{ | ||
title: 'Applying modifications...', | ||
title: `Running tasks for staged files...`, | ||
task: (ctx, task) => task.newListr(listrTasks, { concurrent }), | ||
skip: () => listrTasks.every((task) => task.skip()), | ||
}, | ||
{ | ||
title: 'Applying modifications from tasks...', | ||
task: (ctx) => git.applyModifications(ctx), | ||
@@ -253,3 +322,3 @@ skip: applyModificationsSkipped, | ||
{ | ||
title: 'Cleaning up...', | ||
title: 'Cleaning up temporary files...', | ||
task: (ctx) => git.cleanup(ctx), | ||
@@ -271,3 +340,1 @@ enabled: cleanupEnabled, | ||
} | ||
module.exports = runAll |
@@ -1,5 +0,5 @@ | ||
'use strict' | ||
import EventEmitter from 'events' | ||
const { GIT_ERROR, TASK_ERROR } = require('./messages') | ||
const { | ||
import { GIT_ERROR, TASK_ERROR } from './messages.js' | ||
import { | ||
ApplyEmptyCommitError, | ||
@@ -10,8 +10,9 @@ TaskError, | ||
RestoreUnstagedChangesError, | ||
} = require('./symbols') | ||
} from './symbols.js' | ||
const getInitialState = ({ quiet = false } = {}) => ({ | ||
export const getInitialState = ({ quiet = false } = {}) => ({ | ||
hasPartiallyStagedFiles: null, | ||
shouldBackup: null, | ||
errors: new Set([]), | ||
events: new EventEmitter(), | ||
output: [], | ||
@@ -21,5 +22,5 @@ quiet, | ||
const hasPartiallyStagedFiles = (ctx) => ctx.hasPartiallyStagedFiles | ||
export const hasPartiallyStagedFiles = (ctx) => ctx.hasPartiallyStagedFiles | ||
const applyModificationsSkipped = (ctx) => { | ||
export const applyModificationsSkipped = (ctx) => { | ||
// Always apply back unstaged modifications when skipping backup | ||
@@ -37,3 +38,3 @@ if (!ctx.shouldBackup) return false | ||
const restoreUnstagedChangesSkipped = (ctx) => { | ||
export const restoreUnstagedChangesSkipped = (ctx) => { | ||
// Should be skipped in case of git errors | ||
@@ -49,3 +50,3 @@ if (ctx.errors.has(GitError)) { | ||
const restoreOriginalStateEnabled = (ctx) => | ||
export const restoreOriginalStateEnabled = (ctx) => | ||
ctx.shouldBackup && | ||
@@ -56,3 +57,3 @@ (ctx.errors.has(TaskError) || | ||
const restoreOriginalStateSkipped = (ctx) => { | ||
export const restoreOriginalStateSkipped = (ctx) => { | ||
// Should be skipped in case of unknown git errors | ||
@@ -68,5 +69,5 @@ if ( | ||
const cleanupEnabled = (ctx) => ctx.shouldBackup | ||
export const cleanupEnabled = (ctx) => ctx.shouldBackup | ||
const cleanupSkipped = (ctx) => { | ||
export const cleanupSkipped = (ctx) => { | ||
// Should be skipped in case of unknown git errors | ||
@@ -85,12 +86,1 @@ if ( | ||
} | ||
module.exports = { | ||
getInitialState, | ||
hasPartiallyStagedFiles, | ||
applyModificationsSkipped, | ||
restoreUnstagedChangesSkipped, | ||
restoreOriginalStateEnabled, | ||
restoreOriginalStateSkipped, | ||
cleanupEnabled, | ||
cleanupSkipped, | ||
} |
@@ -1,25 +0,27 @@ | ||
'use strict' | ||
export const ApplyEmptyCommitError = Symbol('ApplyEmptyCommitError') | ||
const ApplyEmptyCommitError = Symbol('ApplyEmptyCommitError') | ||
const GetBackupStashError = Symbol('GetBackupStashError') | ||
const GetStagedFilesError = Symbol('GetStagedFilesError') | ||
const GitError = Symbol('GitError') | ||
const GitRepoError = Symbol('GitRepoError') | ||
const HideUnstagedChangesError = Symbol('HideUnstagedChangesError') | ||
const RestoreMergeStatusError = Symbol('RestoreMergeStatusError') | ||
const RestoreOriginalStateError = Symbol('RestoreOriginalStateError') | ||
const RestoreUnstagedChangesError = Symbol('RestoreUnstagedChangesError') | ||
const TaskError = Symbol('TaskError') | ||
export const ConfigNotFoundError = new Error('Configuration could not be found') | ||
module.exports = { | ||
ApplyEmptyCommitError, | ||
GetBackupStashError, | ||
GetStagedFilesError, | ||
GitError, | ||
GitRepoError, | ||
HideUnstagedChangesError, | ||
RestoreMergeStatusError, | ||
RestoreOriginalStateError, | ||
RestoreUnstagedChangesError, | ||
TaskError, | ||
} | ||
export const ConfigFormatError = new Error('Configuration should be an object or a function') | ||
export const ConfigEmptyError = new Error('Configuration should not be empty') | ||
export const GetBackupStashError = Symbol('GetBackupStashError') | ||
export const GetStagedFilesError = Symbol('GetStagedFilesError') | ||
export const GitError = Symbol('GitError') | ||
export const GitRepoError = Symbol('GitRepoError') | ||
export const HideUnstagedChangesError = Symbol('HideUnstagedChangesError') | ||
export const InvalidOptionsError = new Error('Invalid Options') | ||
export const RestoreMergeStatusError = Symbol('RestoreMergeStatusError') | ||
export const RestoreOriginalStateError = Symbol('RestoreOriginalStateError') | ||
export const RestoreUnstagedChangesError = Symbol('RestoreUnstagedChangesError') | ||
export const TaskError = Symbol('TaskError') |
@@ -1,15 +0,20 @@ | ||
/* eslint no-prototype-builtins: 0 */ | ||
/** @typedef {import('./index').Logger} Logger */ | ||
'use strict' | ||
import { inspect } from 'node:util' | ||
const chalk = require('chalk') | ||
const format = require('stringify-object') | ||
import debug from 'debug' | ||
const debug = require('debug')('lint-staged:cfg') | ||
import { configurationError } from './messages.js' | ||
import { ConfigEmptyError, ConfigFormatError } from './symbols.js' | ||
import { validateBraces } from './validateBraces.js' | ||
const debugLog = debug('lint-staged:validateConfig') | ||
const isObject = (test) => test && typeof test === 'object' && !Array.isArray(test) | ||
const TEST_DEPRECATED_KEYS = new Map([ | ||
['concurrent', (key) => typeof key === 'boolean'], | ||
['chunkSize', (key) => typeof key === 'number'], | ||
['globOptions', (key) => typeof key === 'object'], | ||
['linters', (key) => typeof key === 'object'], | ||
['globOptions', isObject], | ||
['linters', isObject], | ||
['ignore', (key) => Array.isArray(key)], | ||
@@ -21,74 +26,92 @@ ['subTaskConcurrency', (key) => typeof key === 'number'], | ||
const formatError = (helpMsg) => `● Validation Error: | ||
${helpMsg} | ||
Please refer to https://github.com/okonet/lint-staged#configuration for more information...` | ||
const createError = (opt, helpMsg, value) => | ||
formatError(`Invalid value for '${chalk.bold(opt)}'. | ||
${helpMsg}. | ||
Configured value is: ${chalk.bold( | ||
format(value, { inlineCharacterLimit: Number.POSITIVE_INFINITY }) | ||
)}`) | ||
/** | ||
* Runs config validation. Throws error if the config is not valid. | ||
* @param config {Object} | ||
* @returns config {Object} | ||
* @param {Object} config | ||
* @param {string} configPath | ||
* @param {Logger} logger | ||
* @returns {Object} config | ||
*/ | ||
module.exports = function validateConfig(config) { | ||
debug('Validating config') | ||
export const validateConfig = (config, configPath, logger) => { | ||
debugLog('Validating config from `%s`...', configPath) | ||
const errors = [] | ||
if (!config || (typeof config !== 'object' && typeof config !== 'function')) { | ||
throw ConfigFormatError | ||
} | ||
if (!config || typeof config !== 'object') { | ||
errors.push('Configuration should be an object!') | ||
} else { | ||
const entries = Object.entries(config) | ||
/** | ||
* Function configurations receive all staged files as their argument. | ||
* They are not further validated here to make sure the function gets | ||
* evaluated only once. | ||
* | ||
* @see makeCmdTasks | ||
*/ | ||
if (typeof config === 'function') { | ||
return { '*': config } | ||
} | ||
if (entries.length === 0) { | ||
errors.push('Configuration should not be empty!') | ||
} | ||
if (Object.entries(config).length === 0) { | ||
throw ConfigEmptyError | ||
} | ||
entries.forEach(([pattern, task]) => { | ||
if (TEST_DEPRECATED_KEYS.has(pattern)) { | ||
const testFn = TEST_DEPRECATED_KEYS.get(pattern) | ||
if (testFn(task)) { | ||
errors.push( | ||
createError( | ||
pattern, | ||
'Advanced configuration has been deprecated. For more info, please visit: https://github.com/okonet/lint-staged', | ||
task | ||
) | ||
) | ||
} | ||
} | ||
const errors = [] | ||
if ( | ||
(!Array.isArray(task) || | ||
task.some((item) => typeof item !== 'string' && typeof item !== 'function')) && | ||
typeof task !== 'string' && | ||
typeof task !== 'function' | ||
) { | ||
/** | ||
* Create a new validated config because the keys (patterns) might change. | ||
* Since the Object.reduce method already loops through each entry in the config, | ||
* it can be used for validating the values at the same time. | ||
*/ | ||
const validatedConfig = Object.entries(config).reduce((collection, [pattern, task]) => { | ||
/** Versions < 9 had more complex configuration options that are no longer supported. */ | ||
if (TEST_DEPRECATED_KEYS.has(pattern)) { | ||
const testFn = TEST_DEPRECATED_KEYS.get(pattern) | ||
if (testFn(task)) { | ||
errors.push( | ||
createError( | ||
pattern, | ||
'Should be a string, a function, or an array of strings and functions', | ||
task | ||
) | ||
configurationError(pattern, 'Advanced configuration has been deprecated.', task) | ||
) | ||
} | ||
}) | ||
} | ||
/** Return early for deprecated keys to skip validating their (deprecated) values */ | ||
return collection | ||
} | ||
if ( | ||
(!Array.isArray(task) || | ||
task.some((item) => typeof item !== 'string' && typeof item !== 'function')) && | ||
typeof task !== 'string' && | ||
typeof task !== 'function' | ||
) { | ||
errors.push( | ||
configurationError( | ||
pattern, | ||
'Should be a string, a function, or an array of strings and functions.', | ||
task | ||
) | ||
) | ||
} | ||
/** | ||
* A typical configuration error is using invalid brace expansion, like `*.{js}`. | ||
* These are automatically fixed and warned about. | ||
*/ | ||
const fixedPattern = validateBraces(pattern, logger) | ||
return { ...collection, [fixedPattern]: task } | ||
}, {}) | ||
if (errors.length) { | ||
throw new Error(errors.join('\n')) | ||
const message = errors.join('\n\n') | ||
logger.error(`Could not parse lint-staged config. | ||
${message} | ||
See https://github.com/okonet/lint-staged#configuration.`) | ||
throw new Error(message) | ||
} | ||
return config | ||
debugLog('Validated config from `%s`:', configPath) | ||
debugLog(inspect(config, { compact: false })) | ||
return validatedConfig | ||
} | ||
module.exports.createError = createError |
{ | ||
"name": "lint-staged", | ||
"version": "10.5.4", | ||
"version": "14.0.0", | ||
"description": "Lint files staged by git", | ||
@@ -16,4 +16,12 @@ "license": "MIT", | ||
}, | ||
"engines": { | ||
"node": "^16.14.0 || >=18.0.0" | ||
}, | ||
"type": "module", | ||
"bin": "./bin/lint-staged.js", | ||
"main": "./lib/index.js", | ||
"exports": { | ||
".": "./lib/index.js", | ||
"./bin": "./bin/lint-staged.js", | ||
"./package.json": "./package.json" | ||
}, | ||
"files": [ | ||
@@ -24,66 +32,35 @@ "bin", | ||
"scripts": { | ||
"cz": "git-cz", | ||
"lint": "eslint .", | ||
"pretest": "npm run lint", | ||
"test": "jest --coverage", | ||
"test:watch": "jest --watch" | ||
}, | ||
"husky": { | ||
"hooks": { | ||
"pre-commit": "./bin/lint-staged.js" | ||
} | ||
}, | ||
"dependencies": { | ||
"chalk": "^4.1.0", | ||
"cli-truncate": "^2.1.0", | ||
"commander": "^6.2.0", | ||
"cosmiconfig": "^7.0.0", | ||
"debug": "^4.2.0", | ||
"dedent": "^0.7.0", | ||
"enquirer": "^2.3.6", | ||
"execa": "^4.1.0", | ||
"listr2": "^3.2.2", | ||
"log-symbols": "^4.0.0", | ||
"micromatch": "^4.0.2", | ||
"normalize-path": "^3.0.0", | ||
"please-upgrade-node": "^3.2.0", | ||
"string-argv": "0.3.1", | ||
"stringify-object": "^3.3.0" | ||
"chalk": "5.3.0", | ||
"commander": "11.0.0", | ||
"debug": "4.3.4", | ||
"execa": "7.2.0", | ||
"lilconfig": "2.1.0", | ||
"listr2": "6.6.1", | ||
"micromatch": "4.0.5", | ||
"pidtree": "0.6.0", | ||
"string-argv": "0.3.2", | ||
"yaml": "2.3.1" | ||
}, | ||
"devDependencies": { | ||
"@babel/core": "^7.12.3", | ||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1", | ||
"@babel/preset-env": "^7.12.1", | ||
"babel-eslint": "10.1.0", | ||
"babel-jest": "^26.6.1", | ||
"consolemock": "^1.1.0", | ||
"eslint": "^7.12.1", | ||
"eslint-config-prettier": "^6.15.0", | ||
"eslint-plugin-import": "^2.22.1", | ||
"eslint-plugin-node": "^11.1.0", | ||
"eslint-plugin-prettier": "^3.1.4", | ||
"fs-extra": "^9.0.1", | ||
"husky": "^4.3.0", | ||
"jest": "^26.6.1", | ||
"jest-snapshot-serializer-ansi": "^1.0.0", | ||
"prettier": "^2.1.2" | ||
"@babel/core": "7.22.10", | ||
"@babel/eslint-parser": "7.22.10", | ||
"@babel/preset-env": "7.22.10", | ||
"babel-jest": "29.6.2", | ||
"babel-plugin-transform-imports": "2.0.0", | ||
"consolemock": "1.1.0", | ||
"eslint": "8.46.0", | ||
"eslint-config-prettier": "9.0.0", | ||
"eslint-plugin-import": "2.28.0", | ||
"eslint-plugin-node": "11.1.0", | ||
"eslint-plugin-prettier": "5.0.0", | ||
"husky": "8.0.3", | ||
"jest": "29.6.2", | ||
"jest-snapshot-serializer-ansi": "2.1.0", | ||
"prettier": "3.0.1" | ||
}, | ||
"config": { | ||
"commitizen": { | ||
"path": "./node_modules/cz-conventional-changelog" | ||
} | ||
}, | ||
"jest": { | ||
"collectCoverage": true, | ||
"collectCoverageFrom": [ | ||
"lib/**/*.js" | ||
], | ||
"setupFiles": [ | ||
"./testSetup.js" | ||
], | ||
"snapshotSerializers": [ | ||
"jest-snapshot-serializer-ansi" | ||
], | ||
"testEnvironment": "node" | ||
}, | ||
"keywords": [ | ||
@@ -90,0 +67,0 @@ "lint", |
416
README.md
@@ -1,14 +0,40 @@ | ||
# 🚫💩 lint-staged ![GitHub Actions](https://github.com/okonet/lint-staged/workflows/CI/badge.svg) [![Build Status for Windows](https://ci.appveyor.com/api/projects/status/github/okonet/lint-staged?branch=master&svg=true)](https://ci.appveyor.com/project/okonet/lint-staged) [![npm version](https://badge.fury.io/js/lint-staged.svg)](https://badge.fury.io/js/lint-staged) [![Codecov](https://codecov.io/gh/okonet/lint-staged/branch/master/graph/badge.svg)](https://codecov.io/gh/okonet/lint-staged) | ||
# 🚫💩 lint-staged ![GitHub Actions](https://github.com/okonet/lint-staged/workflows/CI/badge.svg) [![npm version](https://badge.fury.io/js/lint-staged.svg)](https://badge.fury.io/js/lint-staged) [![Codecov](https://codecov.io/gh/okonet/lint-staged/branch/master/graph/badge.svg)](https://codecov.io/gh/okonet/lint-staged) | ||
Run linters against staged git files and don't let :poop: slip into your code base! | ||
```bash | ||
npm install --save-dev lint-staged # requires further setup | ||
``` | ||
``` | ||
$ git commit | ||
✔ Preparing lint-staged... | ||
❯ Running tasks for staged files... | ||
❯ packages/frontend/.lintstagedrc.json — 1 file | ||
↓ *.js — no files [SKIPPED] | ||
❯ *.{json,md} — 1 file | ||
⠹ prettier --write | ||
↓ packages/backend/.lintstagedrc.json — 2 files | ||
❯ *.js — 2 files | ||
⠼ eslint --fix | ||
↓ *.{json,md} — no files [SKIPPED] | ||
◼ Applying modifications from tasks... | ||
◼ Cleaning up temporary files... | ||
``` | ||
<details> | ||
<summary>See asciinema video</summary> | ||
[![asciicast](https://asciinema.org/a/199934.svg)](https://asciinema.org/a/199934) | ||
</details> | ||
## Why | ||
Linting makes more sense when run before committing your code. By doing so you can ensure no errors go into the repository and enforce code style. But running a lint process on a whole project is slow and linting results can be irrelevant. Ultimately you only want to lint files that will be committed. | ||
Linting makes more sense when run before committing your code. By doing so you can ensure no errors go into the repository and enforce code style. But running a lint process on a whole project is slow, and linting results can be irrelevant. Ultimately you only want to lint files that will be committed. | ||
This project contains a script that will run arbitrary shell tasks with a list of staged files as an argument, filtered by a specified glob pattern. | ||
## Related blogs posts and talks | ||
## Related blog posts and talks | ||
@@ -19,3 +45,3 @@ - [Introductory Medium post - Andrey Okonetchnikov, 2016](https://medium.com/@okonetchnikov/make-linting-great-again-f3890e1ad6b8#.8qepn2b5l) | ||
- [SurviveJS interview - Juho Vepsäläinen and Andrey Okonetchnikov, 2018](https://survivejs.com/blog/lint-staged-interview/) | ||
- [Prettier your CSharp with `dotnet-format` and `lint-staged`](https://blog.johnnyreilly.com/2020/12/prettier-your-csharp-with-dotnet-format-and-lint-staged.html) | ||
- [Prettier your CSharp with `dotnet-format` and `lint-staged`](https://johnnyreilly.com/2020/12/22/prettier-your-csharp-with-dotnet-format-and-lint-staged) | ||
@@ -26,14 +52,18 @@ > If you've written one, please submit a PR with the link to it! | ||
The fastest way to start using lint-staged is to run following command in your terminal: | ||
To install _lint-staged_ in the recommended way, you need to: | ||
```bash | ||
npx mrm lint-staged | ||
``` | ||
1. Install _lint-staged_ itself: | ||
- `npm install --save-dev lint-staged` | ||
1. Set up the `pre-commit` git hook to run _lint-staged_ | ||
- [Husky](https://github.com/typicode/husky) is a popular choice for configuring git hooks | ||
- Read more about git hooks [here](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) | ||
1. Install some linters, like [ESLint](https://eslint.org) or [Prettier](https://prettier.io) | ||
1. Configure _lint-staged_ to run linters and other tasks: | ||
- for example: `{ "*.js": "eslint" }` to run ESLint for all staged JS files | ||
- See [Configuration](#configuration) for more info | ||
It will install and configure [husky](https://github.com/typicode/husky) and lint-staged depending on code quality tools from `package.json` dependencies so please make sure you install (`npm install --save-dev`) and configure all code quality tools like [Prettier](https://prettier.io), [ESlint](https://eslint.org) prior that. | ||
Don't forget to commit changes to `package.json` and `.husky` to share this setup with your team! | ||
Don't forget to commit changes to `package.json` to share this setup with your team! | ||
Now change a few files, `git add` or `git add --patch` some of them to your commit, and try to `git commit` them. | ||
Now change a few files, `git add` or `git add --patch` some of them to your commit and try to `git commit` them. | ||
See [examples](#examples) and [configuration](#configuration) for more information. | ||
@@ -43,6 +73,18 @@ | ||
See [Releases](https://github.com/okonet/lint-staged/releases) | ||
See [Releases](https://github.com/okonet/lint-staged/releases). | ||
### Migration | ||
#### v14 | ||
- Since `v14.0.0` _lint-staged_ no longer supports Node.js 14. Please upgrade your Node.js version to at least `16.14.0`. | ||
#### v13 | ||
- Since `v13.0.0` _lint-staged_ no longer supports Node.js 12. Please upgrade your Node.js version to at least `14.13.1`, or `16.0.0` onward. | ||
#### v12 | ||
- Since `v12.0.0` _lint-staged_ is a pure ESM module, so make sure your Node.js version is at least `12.20.0`, `14.13.1`, or `16.0.0`. Read more about ESM modules from the official [Node.js Documentation site here](https://nodejs.org/api/esm.html#introduction). | ||
#### v10 | ||
@@ -54,10 +96,10 @@ | ||
since trying to run multiple git operations at the same time usually results in an error. | ||
- From `v10.0.0` onwards _lint-staged_ uses git stashes to improve speed and provide backups while running. | ||
Since git stashes require at least an initial commit, you shouldn't run _lint-staged_ in an empty repo. | ||
- From `v10.0.0` onwards _lint-staged_ requires Node.js version 10.13.0 or later. | ||
- From `v10.0.0` onwards _lint-staged_ will abort the commit if linter tasks undo all staged changes. To allow creating empty commit, please use the `--allow-empty` option. | ||
- From `v10.0.0` onwards, lint-staged uses git stashes to improve speed and provide backups while running. | ||
Since git stashes require at least an initial commit, you shouldn't run lint-staged in an empty repo. | ||
- From `v10.0.0` onwards, lint-staged requires Node.js version 10.13.0 or later. | ||
- From `v10.0.0` onwards, lint-staged will abort the commit if linter tasks undo all staged changes. To allow creating an empty commit, please use the `--allow-empty` option. | ||
## Command line flags | ||
```bash | ||
``` | ||
❯ npx lint-staged --help | ||
@@ -68,16 +110,17 @@ Usage: lint-staged [options] | ||
-V, --version output the version number | ||
--allow-empty allow empty commits when tasks revert all staged changes | ||
(default: false) | ||
--allow-empty allow empty commits when tasks revert all staged changes (default: false) | ||
-p, --concurrent <number|boolean> the number of tasks to run concurrently, or false for serial (default: true) | ||
-c, --config [path] path to configuration file, or - to read from stdin | ||
--cwd [path] run all tasks in specific directory, instead of the current | ||
-d, --debug print additional debug information (default: false) | ||
--no-stash disable the backup stash, and do not revert in case of | ||
errors | ||
-p, --concurrent <parallel tasks> the number of tasks to run concurrently, or false to run | ||
tasks serially (default: true) | ||
--diff [string] override the default "--staged" flag of "git diff" to get list of files. Implies | ||
"--no-stash". | ||
--diff-filter [string] override the default "--diff-filter=ACMR" flag of "git diff" to get list of files | ||
--max-arg-length [number] maximum length of the command-line argument string (default: 0) | ||
--no-stash disable the backup stash, and do not revert in case of errors | ||
-q, --quiet disable lint-staged’s own console output (default: false) | ||
-r, --relative pass relative filepaths to tasks (default: false) | ||
-x, --shell skip parsing of tasks for better shell support (default: | ||
false) | ||
-v, --verbose show task output even when tasks succeed; by default only | ||
failed output is shown (default: false) | ||
-x, --shell [path] skip parsing of tasks for better shell support (default: false) | ||
-v, --verbose show task output even when tasks succeed; by default only failed output is shown | ||
(default: false) | ||
-h, --help display help for command | ||
@@ -87,14 +130,19 @@ ``` | ||
- **`--allow-empty`**: By default, when linter tasks undo all staged changes, lint-staged will exit with an error and abort the commit. Use this flag to allow creating empty git commits. | ||
- **`--config [path]`**: Manually specify a path to a config file or npm package name. Note: when used, lint-staged won't perform the config file search and print an error if the specified file cannot be found. If '-' is provided as the filename then the config will be read from stdin, allowing piping in the config like `cat my-config.json | npx lint-staged --config -`. | ||
- **`--debug`**: Run in debug mode. When set, it does the following: | ||
- uses [debug](https://github.com/visionmedia/debug) internally to log additional information about staged files, commands being executed, location of binaries, etc. Debug logs, which are automatically enabled by passing the flag, can also be enabled by setting the environment variable `$DEBUG` to `lint-staged*`. | ||
- uses [`verbose` renderer](https://github.com/SamVerschueren/listr-verbose-renderer) for `listr`; this causes serial, uncoloured output to the terminal, instead of the default (beautified, dynamic) output. | ||
- **`--concurrent [number | (true/false)]`**: Controls the concurrency of tasks being run by lint-staged. **NOTE**: This does NOT affect the concurrency of subtasks (they will always be run sequentially). Possible values are: | ||
- **`--concurrent [number|boolean]`**: Controls the [concurrency of tasks](#task-concurrency) being run by lint-staged. **NOTE**: This does NOT affect the concurrency of subtasks (they will always be run sequentially). Possible values are: | ||
- `false`: Run all tasks serially | ||
- `true` (default) : _Infinite_ concurrency. Runs as many tasks in parallel as possible. | ||
- `{number}`: Run the specified number of tasks in parallel, where `1` is equivalent to `false`. | ||
- **`--no-stash`**: By default a backup stash will be created before running the tasks, and all task modifications will be reverted in case of an error. This option will disable creating the stash, and instead leave all modifications in the index when aborting the commit. | ||
- **`--config [path]`**: Manually specify a path to a config file or npm package name. Note: when used, lint-staged won't perform the config file search and will print an error if the specified file cannot be found. If '-' is provided as the filename then the config will be read from stdin, allowing piping in the config like `cat my-config.json | npx lint-staged --config -`. | ||
- **`--cwd [path]`**: By default tasks run in the current working directory. Use the `--cwd some/directory` to override this. The path can be absolute or relative to the current working directory. | ||
- **`--debug`**: Run in debug mode. When set, it does the following: | ||
- uses [debug](https://github.com/visionmedia/debug) internally to log additional information about staged files, commands being executed, location of binaries, etc. Debug logs, which are automatically enabled by passing the flag, can also be enabled by setting the environment variable `$DEBUG` to `lint-staged*`. | ||
- uses [`verbose` renderer](https://listr2.kilic.dev/renderers/verbose-renderer/) for `listr2`; this causes serial, uncoloured output to the terminal, instead of the default (beautified, dynamic) output. | ||
(the [`verbose` renderer](https://listr2.kilic.dev/renderers/verbose-renderer/) can also be activated by setting the `TERM=dumb` or `NODE_ENV=test` environment variables) | ||
- **`--diff`**: By default linters are filtered against all files staged in git, generated from `git diff --staged`. This option allows you to override the `--staged` flag with arbitrary revisions. For example to get a list of changed files between two branches, use `--diff="branch1...branch2"`. You can also read more from about [git diff](https://git-scm.com/docs/git-diff) and [gitrevisions](https://git-scm.com/docs/gitrevisions). This option also implies `--no-stash`. | ||
- **`--diff-filter`**: By default only files that are _added_, _copied_, _modified_, or _renamed_ are included. Use this flag to override the default `ACMR` value with something else: _added_ (`A`), _copied_ (`C`), _deleted_ (`D`), _modified_ (`M`), _renamed_ (`R`), _type changed_ (`T`), _unmerged_ (`U`), _unknown_ (`X`), or _pairing broken_ (`B`). See also the `git diff` docs for [--diff-filter](https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---diff-filterACDMRTUXB82308203). | ||
- **`--max-arg-length`**: long commands (a lot of files) are automatically split into multiple chunks when it detects the current shell cannot handle them. Use this flag to override the maximum length of the generated command string. | ||
- **`--no-stash`**: By default a backup stash will be created before running the tasks, and all task modifications will be reverted in case of an error. This option will disable creating the stash, and instead leave all modifications in the index when aborting the commit. Can be re-enabled with `--stash`. | ||
- **`--quiet`**: Supress all CLI output, except from tasks. | ||
- **`--relative`**: Pass filepaths relative to `process.cwd()` (where `lint-staged` runs) to tasks. Default is `false`. | ||
- **`--shell`**: By default linter commands will be parsed for speed and security. This has the side-effect that regular shell scripts might not work as expected. You can skip parsing of commands with this option. | ||
- **`--shell`**: By default linter commands will be parsed for speed and security. This has the side-effect that regular shell scripts might not work as expected. You can skip parsing of commands with this option. To use a specific shell, use a path like `--shell "/bin/bash"`. | ||
- **`--verbose`**: Show task output even when tasks succeed. By default only failed output is shown. | ||
@@ -104,3 +152,3 @@ | ||
Starting with v3.1 you can now use different ways of configuring it: | ||
_Lint-staged_ can be configured in many ways: | ||
@@ -112,8 +160,13 @@ - `lint-staged` object in your `package.json` | ||
- `.lintstagedrc.yml` | ||
- `lint-staged.config.js`, `.lintstagedrc.js`, or `.lintstagedrc.cjs` file in JS format | ||
- `.lintstagedrc.mjs` or `lint-staged.config.mjs` file in ESM format | ||
- the default export value should be a configuration: `export default { ... }` | ||
- `.lintstagedrc.cjs` or `lint-staged.config.cjs` file in CommonJS format | ||
- the exports value should be a configuration: `module.exports = { ... }` | ||
- `lint-staged.config.js` or `.lintstagedrc.js` in either ESM or CommonJS format, depending on | ||
whether your project's _package.json_ contains the `"type": "module"` option or not. | ||
- Pass a configuration file using the `--config` or `-c` flag | ||
See [cosmiconfig](https://github.com/davidtheclark/cosmiconfig) for more details on what formats are supported. | ||
Configuration should be an object where each value is a command to run and its key is a glob pattern to use for this command. This package uses [micromatch](https://github.com/micromatch/micromatch) for glob patterns. JavaScript files can also export advanced configuration as a function. See [Using JS configuration files](#using-js-configuration-files) for more info. | ||
Configuration should be an object where each value is a command to run and its key is a glob pattern to use for this command. This package uses [micromatch](https://github.com/micromatch/micromatch) for glob patterns. | ||
You can also place multiple configuration files in different directories inside a project. For a given staged file, the closest configuration file will always be used. See ["How to use `lint-staged` in a multi-package monorepo?"](#how-to-use-lint-staged-in-a-multi-package-monorepo) for more info and an example. | ||
@@ -144,14 +197,45 @@ #### `package.json` example: | ||
### Task concurrency | ||
By default _lint-staged_ will run configured tasks concurrently. This means that for every glob, all the commands will be started at the same time. With the following config, both `eslint` and `prettier` will run at the same time: | ||
```json | ||
{ | ||
"*.ts": "eslint", | ||
"*.md": "prettier --list-different" | ||
} | ||
``` | ||
This is typically not a problem since the globs do not overlap, and the commands do not make changes to the files, but only report possible errors (aborting the git commit). If you want to run multiple commands for the same set of files, you can use the array syntax to make sure commands are run in order. In the following example, `prettier` will run for both globs, and in addition `eslint` will run for `*.ts` files _after_ it. Both sets of commands (for each glob) are still started at the same time (but do not overlap). | ||
```json | ||
{ | ||
"*.ts": ["prettier --list-different", "eslint"], | ||
"*.md": "prettier --list-different" | ||
} | ||
``` | ||
Pay extra attention when the configured globs overlap, and tasks make edits to files. For example, in this configuration `prettier` and `eslint` might try to make changes to the same `*.ts` file at the same time, causing a _race condition_: | ||
```json | ||
{ | ||
"*": "prettier --write", | ||
"*.ts": "eslint --fix" | ||
} | ||
``` | ||
If necessary, you can limit the concurrency using `--concurrent <number>` or disable it entirely with `--concurrent false`. | ||
## Filtering files | ||
Linter commands work on a subset of all staged files, defined by a _glob pattern_. `lint-staged´ uses [micromatch](https://github.com/micromatch/micromatch) for matching files with the following rules: | ||
Linter commands work on a subset of all staged files, defined by a _glob pattern_. lint-staged uses [micromatch](https://github.com/micromatch/micromatch) for matching files with the following rules: | ||
- If the glob pattern contains no slashes (`/`), micromatch's `matchBase` option will enabled, so globs match a file's basename regardless of directory: | ||
- **`"*.js"`** will match all JS files, like `/test.js` and `/foo/bar/test.js` | ||
- **`"!(*test).js"`**. will match all JS files, except those ending in `test.js`, so `foo.js` but not `foo.test.js` | ||
- `"*.js"` will match all JS files, like `/test.js` and `/foo/bar/test.js` | ||
- `"!(*test).js"` will match all JS files, except those ending in `test.js`, so `foo.js` but not `foo.test.js` | ||
- If the glob pattern does contain a slash (`/`), it will match for paths as well: | ||
- **`"./*.js"`** will match all JS files in the git repo root, so `/test.js` but not `/foo/bar/test.js` | ||
- **`"foo/**/\*.js"`** will match all JS files inside the`/foo`directory, so`/foo/bar/test.js`but not`/test.js` | ||
- `"./*.js"` will match all JS files in the git repo root, so `/test.js` but not `/foo/bar/test.js` | ||
- `"foo/**/*.js"` will match all JS files inside the `/foo` directory, so `/foo/bar/test.js` but not `/test.js` | ||
When matching, `lint-staged` will do the following | ||
When matching, lint-staged will do the following | ||
@@ -165,7 +249,7 @@ - Resolve the git root automatically, no configuration needed. | ||
Also see [How to use `lint-staged` in a multi package monorepo?](#how-to-use-lint-staged-in-a-multi-package-monorepo) | ||
Also see [How to use `lint-staged` in a multi-package monorepo?](#how-to-use-lint-staged-in-a-multi-package-monorepo) | ||
### Ignoring files | ||
The concept of `lint-staged` is to run configured linter (or other) tasks on files that are staged in git. `lint-staged` will always pass a list of all staged files to the task, and ignoring any files should be configured in the task itself. | ||
The concept of `lint-staged` is to run configured linter tasks (or other tasks) on files that are staged in git. `lint-staged` will always pass a list of all staged files to the task, and ignoring any files should be configured in the task itself. | ||
@@ -180,3 +264,3 @@ Consider a project that uses [`prettier`](https://prettier.io/) to keep code format consistent across all files. The project also stores minified 3rd-party vendor libraries in the `vendor/` directory. To keep `prettier` from throwing errors on these files, the vendor directory should be added to prettier's ignore configuration, the `.prettierignore` file. Running `npx prettier .` will ignore the entire vendor directory, throwing no errors. When `lint-staged` is added to the project and configured to run prettier, all modified and staged files in the vendor directory will be ignored by prettier, even though it receives them as input. | ||
> Using globally installed scripts is discouraged, since lint-staged may not work for someone who doesn’t have it installed. | ||
> Using globally installed scripts is discouraged, since lint-staged may not work for someone who doesn't have it installed. | ||
@@ -207,9 +291,9 @@ `lint-staged` uses [execa](https://github.com/sindresorhus/execa#preferlocal) to locate locally installed scripts. So in your `.lintstagedrc` you can write: | ||
## Using JS configuration file | ||
## Using JS configuration files | ||
Writing the configuration file in JavaScript is the most powerful way to configure _lint-staged_ (`lint-staged.config.js`, [similar](https://github.com/okonet/lint-staged/README.md#configuration), or passed via `--config`). From the configuration file, you can export either a single function, or an object. | ||
Writing the configuration file in JavaScript is the most powerful way to configure lint-staged (`lint-staged.config.js`, [similar](https://github.com/okonet/lint-staged#configuration), or passed via `--config`). From the configuration file, you can export either a single function or an object. | ||
If the `exports` value is a function, it will receive an array of all staged filenames. You can then build your own matchers for the files, and return a command string, or an array or command strings. These strings are considered complete and should include the filename arguments, if wanted. | ||
If the `exports` value is a function, it will receive an array of all staged filenames. You can then build your own matchers for the files and return a command string or an array of command strings. These strings are considered complete and should include the filename arguments, if wanted. | ||
If the `exports` value is an object, its keys should be glob matches (like in the normal non-js config format). The values can either be like in the normal config, or individual functions like described above. Instead of receiving all matched files, the functions in the exported object will only receive the staged files matching the corresponding glob key. | ||
If the `exports` value is an object, its keys should be glob matches (like in the normal non-js config format). The values can either be like in the normal config or individual functions like described above. Instead of receiving all matched files, the functions in the exported object will only receive the staged files matching the corresponding glob key. | ||
@@ -231,5 +315,5 @@ ### Function signature | ||
// lint-staged.config.js | ||
const micromatch = require('micromatch') | ||
import micromatch from 'micromatch' | ||
module.exports = (allStagedFiles) => { | ||
export default (allStagedFiles) => { | ||
const shFiles = micromatch(allStagedFiles, ['**/src/**/*.sh']) | ||
@@ -254,3 +338,3 @@ if (shFiles.length) { | ||
// .lintstagedrc.js | ||
module.exports = { | ||
export default { | ||
'**/*.js?(x)': (filenames) => filenames.map((filename) => `prettier --write '${filename}'`), | ||
@@ -269,3 +353,3 @@ } | ||
// lint-staged.config.js | ||
module.exports = { | ||
export default { | ||
'**/*.ts?(x)': () => 'tsc -p tsconfig.json --noEmit', | ||
@@ -277,3 +361,3 @@ } | ||
### Example: Run eslint on entire repo if more than 10 staged files | ||
### Example: Run ESLint on entire repo if more than 10 staged files | ||
@@ -285,3 +369,3 @@ <details> | ||
// .lintstagedrc.js | ||
module.exports = { | ||
export default { | ||
'**/*.js?(x)': (filenames) => | ||
@@ -299,9 +383,9 @@ filenames.length > 10 ? 'eslint .' : `eslint ${filenames.join(' ')}`, | ||
It's better to use the [function-based configuration (seen above)](https://github.com/okonet/lint-staged/README.md#example-export-a-function-to-build-your-own-matchers), if your use case is this. | ||
It's better to use the [function-based configuration (seen above)](https://github.com/okonet/lint-staged#example-export-a-function-to-build-your-own-matchers), if your use case is this. | ||
```js | ||
// lint-staged.config.js | ||
const micromatch = require('micromatch') | ||
import micromatch from 'micromatch' | ||
module.exports = { | ||
export default { | ||
'*': (allFiles) => { | ||
@@ -326,5 +410,5 @@ const codeFiles = micromatch(allFiles, ['**/*.js', '**/*.ts']) | ||
// lint-staged.config.js | ||
const micromatch = require('micromatch') | ||
import micromatch from 'micromatch' | ||
module.exports = { | ||
export default { | ||
'*.js': (files) => { | ||
@@ -348,5 +432,5 @@ // from `files` filter those _NOT_ matching `*test.js` | ||
```js | ||
const path = require('path') | ||
import path from 'path' | ||
module.exports = { | ||
export default { | ||
'*.ts': (absolutePaths) => { | ||
@@ -376,3 +460,3 @@ const cwd = process.cwd() | ||
All examples assuming you’ve already set up lint-staged and husky in the `package.json`. | ||
All examples assume you've already set up lint-staged in the `package.json` file and [husky](https://github.com/typicode/husky) in its own config file. | ||
@@ -386,7 +470,2 @@ ```json | ||
}, | ||
"husky": { | ||
"hooks": { | ||
"pre-commit": "lint-staged" | ||
} | ||
}, | ||
"lint-staged": {} | ||
@@ -396,4 +475,13 @@ } | ||
_Note we don’t pass a path as an argument for the runners. This is important since lint-staged will do this for you._ | ||
In `.husky/pre-commit` | ||
```shell | ||
#!/usr/bin/env sh | ||
. "$(dirname "$0")/_/husky.sh" | ||
npx lint-staged | ||
``` | ||
_Note: we don't pass a path as an argument for the runners. This is important since lint-staged will do this for you._ | ||
### ESLint with default parameters for `*.js` and `*.jsx` running as a pre-commit hook | ||
@@ -467,3 +555,3 @@ | ||
### Automatically fix code style with `prettier` for any format prettier supports | ||
### Automatically fix code style with `prettier` for any format Prettier supports | ||
@@ -473,25 +561,5 @@ <details> | ||
You need to add `micromatch` as a dev dependency. | ||
```sh | ||
$ npm i --save-dev micromatch | ||
``` | ||
```js | ||
// lint-staged.config.js | ||
const micromatch = require('micromatch') | ||
const prettier = require('prettier') | ||
const prettierSupportedExtensions = prettier | ||
.getSupportInfo() | ||
.languages.map(({ extensions }) => extensions) | ||
.flat() | ||
const addQuotes = (a) => `"${a}"` | ||
module.exports = (allStagedFiles) => { | ||
const prettierFiles = micromatch( | ||
allStagedFiles, | ||
prettierSupportedExtensions.map((extension) => `**/*${extension}`) | ||
) | ||
return [`prettier --write ${prettierFiles.map(addQuotes).join(' ')}`] | ||
```json | ||
{ | ||
"*": "prettier --ignore-unknown --write" | ||
} | ||
@@ -502,3 +570,3 @@ ``` | ||
### Automatically fix code style with `prettier` for javascript, typescript, markdown, HTML, or CSS | ||
### Automatically fix code style with `prettier` for JavaScript, TypeScript, Markdown, HTML, or CSS | ||
@@ -577,4 +645,43 @@ <details> | ||
### Integrate with Next.js | ||
<details> | ||
<summary>Click to expand</summary> | ||
```js | ||
// .lintstagedrc.js | ||
// See https://nextjs.org/docs/basic-features/eslint#lint-staged for details | ||
const path = require('path') | ||
const buildEslintCommand = (filenames) => | ||
`next lint --fix --file ${filenames.map((f) => path.relative(process.cwd(), f)).join(' --file ')}` | ||
module.exports = { | ||
'*.{js,jsx,ts,tsx}': [buildEslintCommand], | ||
} | ||
``` | ||
</details> | ||
## Frequently Asked Questions | ||
### The output of commit hook looks weird (no colors, duplicate lines, …) | ||
<details> | ||
<summary>Click to expand</summary> | ||
Git 2.36.0 introduced a change to hooks where they were no longer run in the original TTY. | ||
This was fixed in 2.37.0: | ||
https://raw.githubusercontent.com/git/git/master/Documentation/RelNotes/2.37.0.txt | ||
> - In Git 2.36 we revamped the way how hooks are invoked. One change | ||
> that is end-user visible is that the output of a hook is no longer | ||
> directly connected to the standard output of "git" that spawns the | ||
> hook, which was noticed post release. This is getting corrected. | ||
> (merge [a082345372](https://github.com/git/git/commit/a082345372) ab/hooks-regression-fix later to maint). | ||
</details> | ||
### Can I use `lint-staged` via node? | ||
@@ -588,3 +695,3 @@ | ||
```js | ||
const lintStaged = require('lint-staged') | ||
import lintStaged from 'lint-staged' | ||
@@ -612,5 +719,5 @@ try { | ||
relative: false, | ||
shell: false | ||
shell: false, | ||
stash: true, | ||
verbose: false | ||
verbose: false, | ||
}) | ||
@@ -680,3 +787,3 @@ ``` | ||
### How to use `lint-staged` in a multi package monorepo? | ||
### How to use `lint-staged` in a multi-package monorepo? | ||
@@ -686,9 +793,31 @@ <details> | ||
Starting with v5.0, `lint-staged` automatically resolves the git root **without any** additional configuration. You configure `lint-staged` as you normally would if your project root and git root were the same directory. | ||
Install _lint-staged_ on the monorepo root level, and add separate configuration files in each package. When running, _lint-staged_ will always use the configuration closest to a staged file, so having separate configuration files makes sure linters do not "leak" into other packages. | ||
If you wish to use `lint-staged` in a multi package monorepo, it is recommended to install [`husky`](https://github.com/typicode/husky) in the root package.json. | ||
[`lerna`](https://github.com/lerna/lerna) can be used to execute the `precommit` script in all sub-packages. | ||
For example, in a monorepo with `packages/frontend/.lintstagedrc.json` and `packages/backend/.lintstagedrc.json`, a staged file inside `packages/frontend/` will only match that configuration, and not the one in `packages/backend/`. | ||
Example repo: [sudo-suhas/lint-staged-multi-pkg](https://github.com/sudo-suhas/lint-staged-multi-pkg). | ||
**Note**: _lint-staged_ discovers the closest configuration to each staged file, even if that configuration doesn't include any matching globs. Given these example configurations: | ||
```js | ||
// ./.lintstagedrc.json | ||
{ "*.md": "prettier --write" } | ||
``` | ||
```js | ||
// ./packages/frontend/.lintstagedrc.json | ||
{ "*.js": "eslint --fix" } | ||
``` | ||
When committing `./packages/frontend/README.md`, it **will not run** _prettier_, because the configuration in the `frontend/` directory is closer to the file and doesn't include it. You should treat all _lint-staged_ configuration files as isolated and separated from each other. You can always use JS files to "extend" configurations, for example: | ||
```js | ||
import baseConfig from '../.lintstagedrc.js' | ||
export default { | ||
...baseConfig, | ||
'*.js': 'eslint --fix', | ||
} | ||
``` | ||
To support backwards-compatibility, monorepo features require multiple _lint-staged_ configuration files present in the git repo. If you still want to run _lint-staged_ in only one of the packages in a monorepo, you can either add an "empty" _lint-staged_ configuration to the root of the repo (so that there's two configs in total), or alternatively run _lint-staged_ with the `--cwd` option pointing to your package directory (for example, `lint-staged --cwd packages/frontend`). | ||
</details> | ||
@@ -714,3 +843,3 @@ | ||
### How can i ignore files from `.eslintignore` ? | ||
### Can I run `lint-staged` in CI, or when there are no staged files? | ||
@@ -720,6 +849,39 @@ <details> | ||
ESLint throws out `warning File ignored because of a matching ignore pattern. Use "--no-ignore" to override` warnings that breaks the linting process ( if you used `--max-warnings=0` which is recommended ). | ||
Lint-staged will by default run against files staged in git, and should be run during the git pre-commit hook, for example. It's also possible to override this default behaviour and run against files in a specific diff, for example | ||
all changed files between two different branches. If you want to run _lint-staged_ in the CI, maybe you can set it up to compare the branch in a _Pull Request_/_Merge Request_ to the target branch. | ||
Try out the `git diff` command until you are satisfied with the result, for example: | ||
``` | ||
git diff --diff-filter=ACMR --name-only master...my-branch | ||
``` | ||
This will print a list of _added_, _changed_, _modified_, and _renamed_ files between `master` and `my-branch`. | ||
You can then run lint-staged against the same files with: | ||
``` | ||
npx lint-staged --diff="master...my-branch" | ||
``` | ||
</details> | ||
### Can I use `lint-staged` with `ng lint` | ||
<details> | ||
<summary>Click to expand</summary> | ||
You should not use `ng lint` through _lint-staged_, because it's designed to lint an entire project. Instead, you can add `ng lint` to your git pre-commit hook the same way as you would run lint-staged. | ||
See issue [!951](https://github.com/okonet/lint-staged/issues/951) for more details and possible workarounds. | ||
</details> | ||
### How can I ignore files from `.eslintignore`? | ||
<details> | ||
<summary>Click to expand</summary> | ||
ESLint throws out `warning File ignored because of a matching ignore pattern. Use "--no-ignore" to override` warnings that breaks the linting process ( if you used `--max-warnings=0` which is recommended ). | ||
#### ESLint < 7 | ||
@@ -735,9 +897,9 @@ | ||
```js | ||
const { CLIEngine } = require('eslint') | ||
import { CLIEngine } from 'eslint' | ||
const cli = new CLIEngine({}) | ||
module.exports = { | ||
'*.js': (files) => | ||
'eslint --max-warnings=0 ' + files.filter((file) => !cli.isPathIgnored(file)).join(' '), | ||
export default { | ||
'*.js': (files) => { | ||
const cli = new CLIEngine({}) | ||
return 'eslint --max-warnings=0 ' + files.filter((file) => !cli.isPathIgnored(file)).join(' ') | ||
}, | ||
} | ||
@@ -748,3 +910,3 @@ ``` | ||
#### ESlint >= 7 | ||
#### ESLint >= 7 | ||
@@ -754,23 +916,21 @@ <details> | ||
In versions of ESlint > 7, [isPathIgnored](https://eslint.org/docs/developer-guide/nodejs-api#-eslintispathignoredfilepath) is an async function and now returns a promise. The code below can be used to reinstate the above functionality. | ||
In versions of ESLint > 7, [isPathIgnored](https://eslint.org/docs/developer-guide/nodejs-api#-eslintispathignoredfilepath) is an async function and now returns a promise. The code below can be used to reinstate the above functionality. | ||
This particular code uses a tiny package, [node-filter-async](https://www.npmjs.com/package/node-filter-async), to filter the files array with an async function. If you prefer to not have an extra dependency, it is quite simple to write a similar function. | ||
Since [10.5.3](https://github.com/okonet/lint-staged/releases), any errors due to a bad ESLint config will come through to the console. | ||
Since [10.5.3](https://github.com/okonet/lint-staged/releases), any errors due to a bad eslint config will come through to the console. | ||
```js | ||
const { ESLint } = require('eslint') | ||
const filterAsync = require('node-filter-async').default | ||
import { ESLint } from 'eslint' | ||
const eslintCli = new ESLint() | ||
const removeIgnoredFiles = async (files) => { | ||
const filteredFiles = await filterAsync(files, async (file) => { | ||
const isIgnored = await eslintCli.isPathIgnored(file) | ||
return !isIgnored | ||
}) | ||
const eslint = new ESLint() | ||
const isIgnored = await Promise.all( | ||
files.map((file) => { | ||
return eslint.isPathIgnored(file) | ||
}) | ||
) | ||
const filteredFiles = files.filter((_, i) => !isIgnored[i]) | ||
return filteredFiles.join(' ') | ||
} | ||
module.exports = { | ||
export default { | ||
'**/*.{ts,tsx,js,jsx}': async (files) => { | ||
@@ -784,1 +944,3 @@ const filesToLint = await removeIgnoredFiles(files) | ||
</details> | ||
</details> |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
113261
10
15
32
1960
910
10
Yes
+ Addedlilconfig@2.1.0
+ Addedpidtree@0.6.0
+ Addedyaml@2.3.1
+ Addedansi-escapes@5.0.0(transitive)
+ Addedansi-regex@6.1.0(transitive)
+ Addedansi-styles@6.2.1(transitive)
+ Addedchalk@5.3.0(transitive)
+ Addedcli-cursor@4.0.0(transitive)
+ Addedcli-truncate@3.1.0(transitive)
+ Addedcommander@11.0.0(transitive)
+ Addeddebug@4.3.4(transitive)
+ Addedeastasianwidth@0.2.0(transitive)
+ Addedemoji-regex@9.2.2(transitive)
+ Addedeventemitter3@5.0.1(transitive)
+ Addedexeca@7.2.0(transitive)
+ Addedget-stream@6.0.1(transitive)
+ Addedhuman-signals@4.3.1(transitive)
+ Addedis-fullwidth-code-point@4.0.0(transitive)
+ Addedis-stream@3.0.0(transitive)
+ Addedlilconfig@2.1.0(transitive)
+ Addedlistr2@6.6.1(transitive)
+ Addedlog-update@5.0.1(transitive)
+ Addedmicromatch@4.0.5(transitive)
+ Addedmimic-fn@4.0.0(transitive)
+ Addedms@2.1.2(transitive)
+ Addednpm-run-path@5.3.0(transitive)
+ Addedonetime@6.0.0(transitive)
+ Addedpath-key@4.0.0(transitive)
+ Addedpidtree@0.6.0(transitive)
+ Addedrestore-cursor@4.0.0(transitive)
+ Addedslice-ansi@5.0.0(transitive)
+ Addedstring-argv@0.3.2(transitive)
+ Addedstring-width@5.1.2(transitive)
+ Addedstrip-ansi@7.1.0(transitive)
+ Addedstrip-final-newline@3.0.0(transitive)
+ Addedtype-fest@1.4.0(transitive)
+ Addedwrap-ansi@8.1.0(transitive)
+ Addedyaml@2.3.1(transitive)
- Removedcli-truncate@^2.1.0
- Removedcosmiconfig@^7.0.0
- Removeddedent@^0.7.0
- Removedenquirer@^2.3.6
- Removedlog-symbols@^4.0.0
- Removednormalize-path@^3.0.0
- Removedplease-upgrade-node@^3.2.0
- Removedstringify-object@^3.3.0
- Removed@babel/code-frame@7.24.7(transitive)
- Removed@babel/helper-validator-identifier@7.24.7(transitive)
- Removed@babel/highlight@7.24.7(transitive)
- Removed@types/parse-json@4.0.2(transitive)
- Removedaggregate-error@3.1.0(transitive)
- Removedansi-colors@4.1.3(transitive)
- Removedansi-escapes@4.3.2(transitive)
- Removedansi-regex@5.0.1(transitive)
- Removedansi-styles@3.2.14.3.0(transitive)
- Removedastral-regex@2.0.0(transitive)
- Removedcallsites@3.1.0(transitive)
- Removedchalk@2.4.24.1.2(transitive)
- Removedclean-stack@2.2.0(transitive)
- Removedcli-cursor@3.1.0(transitive)
- Removedcli-truncate@2.1.0(transitive)
- Removedcolor-convert@1.9.32.0.1(transitive)
- Removedcolor-name@1.1.31.1.4(transitive)
- Removedcommander@6.2.1(transitive)
- Removedcosmiconfig@7.1.0(transitive)
- Removeddebug@4.3.7(transitive)
- Removeddedent@0.7.0(transitive)
- Removedemoji-regex@8.0.0(transitive)
- Removedend-of-stream@1.4.4(transitive)
- Removedenquirer@2.4.1(transitive)
- Removederror-ex@1.3.2(transitive)
- Removedescape-string-regexp@1.0.5(transitive)
- Removedexeca@4.1.0(transitive)
- Removedget-own-enumerable-property-symbols@3.0.2(transitive)
- Removedget-stream@5.2.0(transitive)
- Removedhas-flag@3.0.04.0.0(transitive)
- Removedhuman-signals@1.1.1(transitive)
- Removedimport-fresh@3.3.0(transitive)
- Removedindent-string@4.0.0(transitive)
- Removedis-arrayish@0.2.1(transitive)
- Removedis-fullwidth-code-point@3.0.0(transitive)
- Removedis-obj@1.0.1(transitive)
- Removedis-regexp@1.0.0(transitive)
- Removedis-stream@2.0.1(transitive)
- Removedis-unicode-supported@0.1.0(transitive)
- Removedjs-tokens@4.0.0(transitive)
- Removedjson-parse-even-better-errors@2.3.1(transitive)
- Removedlines-and-columns@1.2.4(transitive)
- Removedlistr2@3.14.0(transitive)
- Removedlog-symbols@4.1.0(transitive)
- Removedlog-update@4.0.0(transitive)
- Removedmicromatch@4.0.8(transitive)
- Removedms@2.1.3(transitive)
- Removednormalize-path@3.0.0(transitive)
- Removednpm-run-path@4.0.1(transitive)
- Removedonce@1.4.0(transitive)
- Removedp-map@4.0.0(transitive)
- Removedparent-module@1.0.1(transitive)
- Removedparse-json@5.2.0(transitive)
- Removedpath-type@4.0.0(transitive)
- Removedpicocolors@1.1.0(transitive)
- Removedplease-upgrade-node@3.2.0(transitive)
- Removedpump@3.0.0(transitive)
- Removedresolve-from@4.0.0(transitive)
- Removedrestore-cursor@3.1.0(transitive)
- Removedrxjs@7.8.1(transitive)
- Removedsemver-compare@1.0.0(transitive)
- Removedslice-ansi@3.0.04.0.0(transitive)
- Removedstring-argv@0.3.1(transitive)
- Removedstring-width@4.2.3(transitive)
- Removedstringify-object@3.3.0(transitive)
- Removedstrip-ansi@6.0.1(transitive)
- Removedstrip-final-newline@2.0.0(transitive)
- Removedsupports-color@5.5.07.2.0(transitive)
- Removedthrough@2.3.8(transitive)
- Removedtslib@2.7.0(transitive)
- Removedtype-fest@0.21.3(transitive)
- Removedwrap-ansi@6.2.07.0.0(transitive)
- Removedwrappy@1.0.2(transitive)
- Removedyaml@1.10.2(transitive)
Updatedchalk@5.3.0
Updatedcommander@11.0.0
Updateddebug@4.3.4
Updatedexeca@7.2.0
Updatedlistr2@6.6.1
Updatedmicromatch@4.0.5
Updatedstring-argv@0.3.2