mirror of
https://codeberg.org/Kankys/NewLukasBlog.git
synced 2026-11-11 06:14:36 +01:00
OpravyPoAI
This commit is contained in:
1 parent
99aa797101
commit
24fd53c94c
4746 files changed
+700320
-12
No files matched your search
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Andrey Okonetchnikov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+1110
File diff suppressed because it is too large.
Load diff
+164
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { userInfo } from 'node:os'
|
||||
|
||||
import { supportsColor } from 'chalk'
|
||||
import { Option, program } from 'commander'
|
||||
import debug from 'debug'
|
||||
|
||||
import lintStaged from '../lib/index.js'
|
||||
import { CONFIG_STDIN_ERROR, RESTORE_STASH_EXAMPLE } from '../lib/messages.js'
|
||||
import { readStdin } from '../lib/readStdin.js'
|
||||
import { getVersion } from '../lib/version.js'
|
||||
|
||||
// Force colors for packages that depend on https://www.npmjs.com/package/supports-color
|
||||
if (supportsColor) {
|
||||
process.env.FORCE_COLOR = supportsColor.level.toString()
|
||||
}
|
||||
|
||||
const debugLog = debug('lint-staged:bin')
|
||||
|
||||
// Do not terminate main Listr process on SIGINT
|
||||
process.on('SIGINT', () => {})
|
||||
|
||||
program.version(await getVersion())
|
||||
|
||||
/**
|
||||
* This shouldn't be necessary for lint-staged, but add migration step just in case
|
||||
* to preserve old behavior of "commander".
|
||||
*
|
||||
* @todo remove this in the major version
|
||||
* @see https://github.com/tj/commander.js/releases/tag/v13.0.0
|
||||
* */
|
||||
program.allowExcessArguments()
|
||||
|
||||
program.option('--allow-empty', 'allow empty commits when tasks revert all staged changes', false)
|
||||
|
||||
program.option(
|
||||
'-p, --concurrent <number|boolean>',
|
||||
'the number of tasks to run concurrently, or false for serial',
|
||||
true
|
||||
)
|
||||
|
||||
program.option('-c, --config [path]', 'path to configuration file, or - to read from stdin')
|
||||
|
||||
program.option('--cwd [path]', 'run all tasks in specific directory, instead of the current')
|
||||
|
||||
program.option('-d, --debug', 'print additional debug information', false)
|
||||
|
||||
program.addOption(
|
||||
new Option(
|
||||
'--diff [string]',
|
||||
'override the default "--staged" flag of "git diff" to get list of files. Implies "--no-stash".'
|
||||
).implies({ stash: false })
|
||||
)
|
||||
|
||||
program.option(
|
||||
'--diff-filter [string]',
|
||||
'override the default "--diff-filter=ACMR" flag of "git diff" to get list of files'
|
||||
)
|
||||
|
||||
program.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.
|
||||
*/
|
||||
program
|
||||
.addOption(
|
||||
new Option('--stash', 'enable the backup stash, and revert in case of errors')
|
||||
.default(true)
|
||||
.hideHelp()
|
||||
)
|
||||
.addOption(
|
||||
new Option(
|
||||
'--no-stash',
|
||||
'disable the backup stash, and do not revert in case of errors. Implies "--no-hide-partially-staged".'
|
||||
)
|
||||
.default(false)
|
||||
.implies({ hidePartiallyStaged: false })
|
||||
)
|
||||
|
||||
/**
|
||||
* We don't want to show the `--hide-partially-staged` flag because it's on by default, and only show the
|
||||
* negatable flag `--no-hide-partially-staged` in stead. There seems to be a bug in Commander.js where
|
||||
* configuring only the latter won't actually set the default value.
|
||||
*/
|
||||
program
|
||||
.addOption(
|
||||
new Option('--hide-partially-staged', 'hide unstaged changes from partially staged files')
|
||||
.default(true)
|
||||
.hideHelp()
|
||||
)
|
||||
.addOption(
|
||||
new Option(
|
||||
'--no-hide-partially-staged',
|
||||
'disable hiding unstaged changes from partially staged files'
|
||||
).default(false)
|
||||
)
|
||||
|
||||
program.option('-q, --quiet', 'disable lint-staged’s own console output', false)
|
||||
|
||||
program.option('-r, --relative', 'pass relative filepaths to tasks', false)
|
||||
|
||||
program.option('-x, --shell [path]', 'skip parsing of tasks for better shell support', false)
|
||||
|
||||
program.option(
|
||||
'-v, --verbose',
|
||||
'show task output even when tasks succeed; by default only failed output is shown',
|
||||
false
|
||||
)
|
||||
|
||||
program.addHelpText('afterAll', '\n' + RESTORE_STASH_EXAMPLE)
|
||||
|
||||
const cliOptions = program.parse(process.argv).opts()
|
||||
|
||||
if (cliOptions.debug) {
|
||||
debug.enable('lint-staged*')
|
||||
}
|
||||
|
||||
const options = {
|
||||
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`
|
||||
hidePartiallyStaged: !!cliOptions.hidePartiallyStaged, // commander inverts `no-<x>` flags to `!x`
|
||||
verbose: !!cliOptions.verbose,
|
||||
}
|
||||
|
||||
try {
|
||||
const { shell } = userInfo()
|
||||
debugLog('Using shell: %s', shell)
|
||||
} catch {
|
||||
debugLog('Could not determine current shell')
|
||||
}
|
||||
|
||||
debugLog('Options parsed from command-line: %o', options)
|
||||
|
||||
if (options.configPath === '-') {
|
||||
delete options.configPath
|
||||
try {
|
||||
debugLog('Reading config from stdin')
|
||||
options.config = JSON.parse(await readStdin())
|
||||
} catch (error) {
|
||||
debugLog(CONFIG_STDIN_ERROR, error)
|
||||
console.error(CONFIG_STDIN_ERROR)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const passed = await lintStaged(options)
|
||||
process.exitCode = passed ? 0 : 1
|
||||
} catch {
|
||||
process.exitCode = 1
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import debug from 'debug'
|
||||
|
||||
import { normalizePath } from './normalizePath.js'
|
||||
|
||||
const debugLog = debug('lint-staged:chunkFiles')
|
||||
|
||||
/**
|
||||
* Chunk array into sub-arrays
|
||||
* @param {Array} arr
|
||||
* @param {Number} chunkCount
|
||||
* @retuns {Array<Array>}
|
||||
*/
|
||||
const chunkArray = (arr, chunkCount) => {
|
||||
if (chunkCount === 1) return [arr]
|
||||
const chunked = []
|
||||
let position = 0
|
||||
for (let i = 0; i < chunkCount; i++) {
|
||||
const chunkLength = Math.ceil((arr.length - position) / (chunkCount - i))
|
||||
chunked.push([])
|
||||
chunked[i] = arr.slice(position, chunkLength + position)
|
||||
position += chunkLength
|
||||
}
|
||||
return chunked
|
||||
}
|
||||
|
||||
/**
|
||||
* Chunk files into sub-arrays based on the length of the resulting argument string
|
||||
* @param {Object} opts
|
||||
* @param {Array<String>} opts.files
|
||||
* @param {String} [opts.baseDir] The optional base directory to resolve relative paths.
|
||||
* @param {number} [opts.maxArgLength] the maximum argument string length
|
||||
* @param {Boolean} [opts.relative] whether files are relative to `topLevelDir` or should be resolved as absolute
|
||||
* @returns {Array<Array<String>>}
|
||||
*/
|
||||
export const chunkFiles = ({ files, baseDir, maxArgLength = null, relative = false }) => {
|
||||
const normalizedFiles = files.map((file) =>
|
||||
normalizePath(relative || !baseDir ? file : path.resolve(baseDir, file))
|
||||
)
|
||||
|
||||
if (!maxArgLength) {
|
||||
debugLog('Skip chunking files because of undefined maxArgLength')
|
||||
return [normalizedFiles] // wrap in an array to return a single chunk
|
||||
}
|
||||
|
||||
const fileListLength = normalizedFiles.join(' ').length
|
||||
debugLog(
|
||||
`Resolved an argument string length of ${fileListLength} characters from ${normalizedFiles.length} files`
|
||||
)
|
||||
const chunkCount = Math.min(Math.ceil(fileListLength / maxArgLength), normalizedFiles.length)
|
||||
debugLog(`Creating ${chunkCount} chunks for maxArgLength of ${maxArgLength}`)
|
||||
return chunkArray(normalizedFiles, chunkCount)
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
export const CONFIG_NAME = 'lint-staged'
|
||||
|
||||
export const PACKAGE_JSON_FILE = 'package.json'
|
||||
|
||||
export const PACKAGE_YAML_FILES = ['package.yaml', 'package.yml']
|
||||
|
||||
/**
|
||||
* The list of files `lint-staged` will read configuration
|
||||
* from, in the declared order.
|
||||
*/
|
||||
export const CONFIG_FILE_NAMES = [
|
||||
PACKAGE_JSON_FILE,
|
||||
...PACKAGE_YAML_FILES,
|
||||
'.lintstagedrc',
|
||||
'.lintstagedrc.json',
|
||||
'.lintstagedrc.yaml',
|
||||
'.lintstagedrc.yml',
|
||||
'.lintstagedrc.mjs',
|
||||
'.lintstagedrc.mts',
|
||||
'.lintstagedrc.js',
|
||||
'.lintstagedrc.ts',
|
||||
'.lintstagedrc.cjs',
|
||||
'.lintstagedrc.cts',
|
||||
'lint-staged.config.mjs',
|
||||
'lint-staged.config.mts',
|
||||
'lint-staged.config.js',
|
||||
'lint-staged.config.ts',
|
||||
'lint-staged.config.cjs',
|
||||
'lint-staged.config.cts',
|
||||
]
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
export const dynamicImport = (path) => import(pathToFileURL(path)).then((module) => module.default)
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import debug from 'debug'
|
||||
import { execa } from 'execa'
|
||||
|
||||
const debugLog = debug('lint-staged:execGit')
|
||||
|
||||
/**
|
||||
* Explicitly never recurse commands into submodules, overriding local/global configuration.
|
||||
* @see https://git-scm.com/docs/git-config#Documentation/git-config.txt-submodulerecurse
|
||||
*/
|
||||
const NO_SUBMODULE_RECURSE = ['-c', 'submodule.recurse=false']
|
||||
|
||||
// exported for tests
|
||||
export const GIT_GLOBAL_OPTIONS = [...NO_SUBMODULE_RECURSE]
|
||||
|
||||
export const execGit = async (cmd, options = {}) => {
|
||||
debugLog('Running git command', cmd)
|
||||
try {
|
||||
const { stdout } = await execa('git', GIT_GLOBAL_OPTIONS.concat(cmd), {
|
||||
...options,
|
||||
all: true,
|
||||
cwd: options.cwd || process.cwd(),
|
||||
stdin: 'ignore',
|
||||
})
|
||||
return stdout
|
||||
} catch ({ all }) {
|
||||
throw new Error(all)
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import chalk from 'chalk'
|
||||
import { figures } from 'listr2'
|
||||
|
||||
export const info = chalk.blue(figures.arrowRight)
|
||||
|
||||
export const error = chalk.redBright(figures.cross)
|
||||
|
||||
export const warning = chalk.yellow(figures.warning)
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import fs from 'node:fs/promises'
|
||||
|
||||
import debug from 'debug'
|
||||
|
||||
const debugLog = debug('lint-staged:file')
|
||||
|
||||
/**
|
||||
* Read contents of a file to buffer
|
||||
* @param {String} filename
|
||||
* @param {Boolean} [ignoreENOENT=true] — Whether to throw if the file doesn't exist
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
export const readFile = async (filename, ignoreENOENT = true) => {
|
||||
debugLog('Reading file `%s`', filename)
|
||||
try {
|
||||
return await fs.readFile(filename)
|
||||
} catch (error) {
|
||||
if (ignoreENOENT && error.code === 'ENOENT') {
|
||||
debugLog("File `%s` doesn't exist, ignoring...", filename)
|
||||
return null // no-op file doesn't exist
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a file
|
||||
* @param {String} filename
|
||||
* @param {Boolean} [ignoreENOENT=true] — Whether to throw if the file doesn't exist
|
||||
*/
|
||||
export const unlink = async (filename, ignoreENOENT = true) => {
|
||||
debugLog('Removing file `%s`', filename)
|
||||
try {
|
||||
await fs.unlink(filename)
|
||||
} catch (error) {
|
||||
if (ignoreENOENT && error.code === 'ENOENT') {
|
||||
debugLog("File `%s` doesn't exist, ignoring...", filename)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write buffer to file
|
||||
* @param {String} filename
|
||||
* @param {Buffer} buffer
|
||||
*/
|
||||
export const writeFile = async (filename, buffer) => {
|
||||
debugLog('Writing file `%s`', filename)
|
||||
await fs.writeFile(filename, buffer)
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import debug from 'debug'
|
||||
import micromatch from 'micromatch'
|
||||
|
||||
import { normalizePath } from './normalizePath.js'
|
||||
|
||||
const debugLog = debug('lint-staged:generateTasks')
|
||||
|
||||
/**
|
||||
* Generates all task commands, and filelist
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {Object} [options.config] - Task configuration
|
||||
* @param {Object} [options.cwd] - Current working directory
|
||||
* @param {boolean} [options.files] - Staged filepaths
|
||||
* @param {boolean} [options.relative] - Whether filepaths to should be relative to cwd
|
||||
*/
|
||||
export const generateTasks = ({ config, cwd = process.cwd(), files, relative = false }) => {
|
||||
debugLog('Generating linter tasks')
|
||||
|
||||
const relativeFiles = files.map((file) => normalizePath(path.relative(cwd, file)))
|
||||
|
||||
return Object.entries(config).map(([pattern, commands]) => {
|
||||
const isParentDirPattern = pattern.startsWith('../')
|
||||
|
||||
// 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 }
|
||||
debugLog('Generated task: \n%O', task)
|
||||
|
||||
return task
|
||||
})
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/** @type {(diff?: string, diffFilter?: string) => string[]} */
|
||||
export const getDiffCommand = (diff, diffFilter) => {
|
||||
/**
|
||||
* Docs for --diff-filter option:
|
||||
* @see https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---diff-filterACDMRTUXB82308203
|
||||
*/
|
||||
const diffFilterArg = diffFilter !== undefined ? diffFilter.trim() : 'ACMR'
|
||||
|
||||
/** Use `--diff branch1...branch2` or `--diff="branch1 branch2", or fall back to default staged files */
|
||||
const diffArgs = diff !== undefined ? diff.trim().split(' ') : ['--staged']
|
||||
|
||||
/**
|
||||
* Docs for -z option:
|
||||
* @see https://git-scm.com/docs/git-diff#Documentation/git-diff.txt--z
|
||||
*/
|
||||
const diffCommand = ['diff', `--diff-filter=${diffFilterArg}`, ...diffArgs]
|
||||
|
||||
return diffCommand
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { EOL } from 'node:os'
|
||||
import { Writable } from 'node:stream'
|
||||
|
||||
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
|
||||
if (debug || env.TERM === 'dumb') {
|
||||
return {
|
||||
renderer: 'verbose',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
renderer: 'update',
|
||||
rendererOptions: {
|
||||
formatOutput: 'truncate',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { execGit } from './execGit.js'
|
||||
import { getDiffCommand } from './getDiffCommand.js'
|
||||
import { normalizePath } from './normalizePath.js'
|
||||
import { parseGitZOutput } from './parseGitZOutput.js'
|
||||
|
||||
export const getStagedFiles = async ({ cwd = process.cwd(), diff, diffFilter } = {}) => {
|
||||
try {
|
||||
/**
|
||||
* With the raw output lines look like:
|
||||
*
|
||||
* :000000 100644 0000000 780ccd3\u0000A\u0000.gitmodules\u0000
|
||||
* :000000 160000 0000000 1bb568e\u0000A\u0000submodule\u0000
|
||||
*
|
||||
* @see https://git-scm.com/docs/git-diff#_raw_output_format
|
||||
*/
|
||||
const output = await execGit([...getDiffCommand(diff, diffFilter), '--raw', '-z'], { cwd })
|
||||
|
||||
if (!output) return []
|
||||
|
||||
/**
|
||||
* Split from all colons and remove the first one, after which lines will look like:
|
||||
*
|
||||
* 000000 100644 0000000 780ccd3 A\u0000.gitmodules\u0000
|
||||
* 000000 160000 0000000 47e5cff A\u0000submodule\u0000
|
||||
*
|
||||
* where '\u0000' is the NUL character from '-z' option. After that we
|
||||
* parse the lines by splitting from NUL, and then split the first
|
||||
* part from space. This yields us enough info both filter out submodule
|
||||
* roots and get the filename.
|
||||
*/
|
||||
return output
|
||||
.slice(1)
|
||||
.split('\u0000:')
|
||||
.map(parseGitZOutput)
|
||||
.flatMap(([info, src, dst]) => {
|
||||
const [, dstMode, , , ,] = info.split(' ')
|
||||
|
||||
/**
|
||||
* Filter out submodule root directory. "160000" is the object mode for submodules.
|
||||
* @see https://github.com/git/git/blob/485f5f863615e670fd97ae40af744e14072cfe18/object.h#L114-L120
|
||||
*/
|
||||
if (dstMode === '160000') {
|
||||
return []
|
||||
}
|
||||
|
||||
/** "dst" exists when moving files, otherwise it's undefined and only "src" exists */
|
||||
const filename = dst ?? src
|
||||
|
||||
return [normalizePath(path.resolve(cwd, filename))]
|
||||
})
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import debug from 'debug'
|
||||
|
||||
import { execGit } from './execGit.js'
|
||||
import { readFile, unlink, writeFile } from './file.js'
|
||||
import { getDiffCommand } from './getDiffCommand.js'
|
||||
import {
|
||||
ApplyEmptyCommitError,
|
||||
GetBackupStashError,
|
||||
GitError,
|
||||
HideUnstagedChangesError,
|
||||
RestoreMergeStatusError,
|
||||
RestoreOriginalStateError,
|
||||
RestoreUnstagedChangesError,
|
||||
} from './symbols.js'
|
||||
|
||||
const debugLog = debug('lint-staged:GitWorkflow')
|
||||
|
||||
const MERGE_HEAD = 'MERGE_HEAD'
|
||||
const MERGE_MODE = 'MERGE_MODE'
|
||||
const MERGE_MSG = 'MERGE_MSG'
|
||||
|
||||
// In git status machine output, renames are presented as `to`NUL`from`
|
||||
// When diffing, both need to be taken into account, but in some cases on the `to`.
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const RENAME = /\x00/
|
||||
|
||||
/**
|
||||
* From list of files, split renames and flatten into two files `to`NUL`from`.
|
||||
* @param {string[]} files
|
||||
* @param {Boolean} [includeRenameFrom=true] Whether or not to include the `from` renamed file, which is no longer on disk
|
||||
*/
|
||||
const processRenames = (files, includeRenameFrom = true) =>
|
||||
files.reduce((flattened, file) => {
|
||||
if (RENAME.test(file)) {
|
||||
const [to, from] = file.split(RENAME)
|
||||
if (includeRenameFrom) flattened.push(from)
|
||||
flattened.push(to)
|
||||
} else {
|
||||
flattened.push(file)
|
||||
}
|
||||
return flattened
|
||||
}, [])
|
||||
|
||||
export const STASH = 'lint-staged automatic backup'
|
||||
|
||||
const PATCH_UNSTAGED = 'lint-staged_unstaged.patch'
|
||||
|
||||
const GIT_DIFF_ARGS = [
|
||||
'--binary', // support binary files
|
||||
'--unified=0', // do not add lines around diff for consistent behaviour
|
||||
'--no-color', // disable colors for consistent behaviour
|
||||
'--no-ext-diff', // disable external diff tools for consistent behaviour
|
||||
'--src-prefix=a/', // force prefix for consistent behaviour
|
||||
'--dst-prefix=b/', // force prefix for consistent behaviour
|
||||
'--patch', // output a patch that can be applied
|
||||
'--submodule=short', // always use the default short format for submodules
|
||||
]
|
||||
const GIT_APPLY_ARGS = ['-v', '--whitespace=nowarn', '--recount', '--unidiff-zero']
|
||||
|
||||
const handleError = (error, ctx, symbol) => {
|
||||
ctx.errors.add(GitError)
|
||||
if (symbol) ctx.errors.add(symbol)
|
||||
throw error
|
||||
}
|
||||
|
||||
export class GitWorkflow {
|
||||
constructor({ allowEmpty, gitConfigDir, topLevelDir, matchedFileChunks, diff, diffFilter }) {
|
||||
this.execGit = (args, options = {}) => execGit(args, { ...options, cwd: topLevelDir })
|
||||
this.deletedFiles = []
|
||||
this.gitConfigDir = gitConfigDir
|
||||
this.topLevelDir = topLevelDir
|
||||
this.diff = diff
|
||||
this.diffFilter = diffFilter
|
||||
this.allowEmpty = allowEmpty
|
||||
this.matchedFileChunks = matchedFileChunks
|
||||
|
||||
/**
|
||||
* These three files hold state about an ongoing git merge
|
||||
* Resolve paths during constructor
|
||||
*/
|
||||
this.mergeHeadFilename = path.resolve(gitConfigDir, MERGE_HEAD)
|
||||
this.mergeModeFilename = path.resolve(gitConfigDir, MERGE_MODE)
|
||||
this.mergeMsgFilename = path.resolve(gitConfigDir, MERGE_MSG)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get absolute path to file hidden inside .git
|
||||
* @param {string} filename
|
||||
*/
|
||||
getHiddenFilepath(filename) {
|
||||
return path.resolve(this.gitConfigDir, `./${filename}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name of backup stash
|
||||
*/
|
||||
async getBackupStash(ctx) {
|
||||
const stashes = await this.execGit(['stash', 'list'])
|
||||
|
||||
const index = stashes
|
||||
.split('\n')
|
||||
.findIndex((line) => line.includes(STASH) && line.includes(ctx.backupHash))
|
||||
|
||||
if (index === -1) {
|
||||
ctx.errors.add(GetBackupStashError)
|
||||
throw new Error('lint-staged automatic backup is missing!')
|
||||
}
|
||||
|
||||
return String(index)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of unstaged deleted files
|
||||
*/
|
||||
async getDeletedFiles() {
|
||||
debugLog('Getting deleted files...')
|
||||
const lsFiles = await this.execGit(['ls-files', '--deleted'])
|
||||
const deletedFiles = lsFiles
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map((file) => path.resolve(this.topLevelDir, file))
|
||||
debugLog('Found deleted files:', deletedFiles)
|
||||
return deletedFiles
|
||||
}
|
||||
|
||||
/**
|
||||
* Save meta information about ongoing git merge
|
||||
*/
|
||||
async backupMergeStatus() {
|
||||
debugLog('Backing up merge state...')
|
||||
await Promise.all([
|
||||
readFile(this.mergeHeadFilename).then((buffer) => (this.mergeHeadBuffer = buffer)),
|
||||
readFile(this.mergeModeFilename).then((buffer) => (this.mergeModeBuffer = buffer)),
|
||||
readFile(this.mergeMsgFilename).then((buffer) => (this.mergeMsgBuffer = buffer)),
|
||||
])
|
||||
debugLog('Done backing up merge state!')
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore meta information about ongoing git merge
|
||||
*/
|
||||
async restoreMergeStatus(ctx) {
|
||||
debugLog('Restoring merge state...')
|
||||
try {
|
||||
await Promise.all([
|
||||
this.mergeHeadBuffer && writeFile(this.mergeHeadFilename, this.mergeHeadBuffer),
|
||||
this.mergeModeBuffer && writeFile(this.mergeModeFilename, this.mergeModeBuffer),
|
||||
this.mergeMsgBuffer && writeFile(this.mergeMsgFilename, this.mergeMsgBuffer),
|
||||
])
|
||||
debugLog('Done restoring merge state!')
|
||||
} catch (error) {
|
||||
debugLog('Failed restoring merge state with error:')
|
||||
debugLog(error)
|
||||
handleError(
|
||||
new Error('Merge state could not be restored due to an error!'),
|
||||
ctx,
|
||||
RestoreMergeStatusError
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of all files with both staged and unstaged modifications.
|
||||
* Renames have special treatment, since the single status line includes
|
||||
* both the "from" and "to" filenames, where "from" is no longer on disk.
|
||||
*/
|
||||
async getPartiallyStagedFiles() {
|
||||
debugLog('Getting partially staged files...')
|
||||
const status = await this.execGit(['status', '-z'])
|
||||
/**
|
||||
* See https://git-scm.com/docs/git-status#_short_format
|
||||
* Entries returned in machine format are separated by a NUL character.
|
||||
* The first letter of each entry represents current index status,
|
||||
* and second the working tree. Index and working tree status codes are
|
||||
* separated from the file name by a space. If an entry includes a
|
||||
* renamed file, the file names are separated by a NUL character
|
||||
* (e.g. `to`\0`from`)
|
||||
*/
|
||||
const partiallyStaged = status
|
||||
// eslint-disable-next-line no-control-regex
|
||||
.split(/\x00(?=[ AMDRCU?!]{2} |$)/)
|
||||
.filter((line) => {
|
||||
const [index, workingTree] = line
|
||||
return index !== ' ' && workingTree !== ' ' && index !== '?' && workingTree !== '?'
|
||||
})
|
||||
.map((line) => line.substr(3)) // Remove first three letters (index, workingTree, and a whitespace)
|
||||
.filter(Boolean) // Filter empty string
|
||||
debugLog('Found partially staged files:', partiallyStaged)
|
||||
return partiallyStaged.length ? partiallyStaged : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a diff of partially staged files and backup stash if enabled.
|
||||
*/
|
||||
async prepare(ctx, task) {
|
||||
try {
|
||||
debugLog(task.title)
|
||||
|
||||
// Get a list of files with bot staged and unstaged changes.
|
||||
// Unstaged changes to these files should be hidden before the tasks run.
|
||||
this.partiallyStagedFiles = await this.getPartiallyStagedFiles()
|
||||
|
||||
if (this.partiallyStagedFiles) {
|
||||
ctx.hasPartiallyStagedFiles = true
|
||||
const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED)
|
||||
const files = processRenames(this.partiallyStagedFiles)
|
||||
await this.execGit(['diff', ...GIT_DIFF_ARGS, '--output', unstagedPatch, '--', ...files])
|
||||
} else {
|
||||
ctx.hasPartiallyStagedFiles = false
|
||||
}
|
||||
|
||||
/**
|
||||
* If backup stash should be skipped, no need to continue
|
||||
*/
|
||||
if (!ctx.shouldBackup) return
|
||||
|
||||
// When backup is enabled, the revert will clear ongoing merge status.
|
||||
await this.backupMergeStatus()
|
||||
|
||||
// Get a list of unstaged deleted files, because certain bugs might cause them to reappear:
|
||||
// - in git versions =< 2.13.0 the `git stash --keep-index` option resurrects deleted files
|
||||
// - git stash can't infer RD or MD states correctly, and will lose the deletion
|
||||
this.deletedFiles = await this.getDeletedFiles()
|
||||
|
||||
// Save stash of all staged files.
|
||||
// The `stash create` command creates a dangling commit without removing any files,
|
||||
// and `stash store` saves it as an actual stash.
|
||||
const stashHash = await this.execGit(['stash', 'create'])
|
||||
ctx.backupHash = await this.execGit(['rev-parse', '--short', stashHash])
|
||||
await this.execGit([
|
||||
'stash',
|
||||
'store',
|
||||
'--quiet',
|
||||
'--message',
|
||||
`${STASH} (${ctx.backupHash})`,
|
||||
ctx.backupHash,
|
||||
])
|
||||
|
||||
task.title = `Backed up original state in git stash (${ctx.backupHash})`
|
||||
debugLog(task.title)
|
||||
} catch (error) {
|
||||
handleError(error, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove unstaged changes to all partially staged files, to avoid tasks from seeing them
|
||||
*/
|
||||
async hideUnstagedChanges(ctx) {
|
||||
try {
|
||||
const files = processRenames(this.partiallyStagedFiles, false)
|
||||
await this.execGit(['checkout', '--force', '--', ...files])
|
||||
} catch (error) {
|
||||
/**
|
||||
* `git checkout --force` doesn't throw errors, so it shouldn't be possible to get here.
|
||||
* If this does fail, the handleError method will set ctx.gitError and lint-staged will fail.
|
||||
*/
|
||||
handleError(error, ctx, HideUnstagedChangesError)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies back task modifications, and unstaged changes hidden in the stash.
|
||||
* In case of a merge-conflict retry with 3-way merge.
|
||||
*/
|
||||
async applyModifications(ctx) {
|
||||
debugLog('Adding task modifications to index...')
|
||||
|
||||
// `matchedFileChunks` includes staged files that lint-staged originally detected and matched against a task.
|
||||
// Add only these files so any 3rd-party edits to other files won't be included in the commit.
|
||||
// These additions per chunk are run "serially" to prevent race conditions.
|
||||
// Git add creates a lockfile in the repo causing concurrent operations to fail.
|
||||
for (const files of this.matchedFileChunks) {
|
||||
await this.execGit(['add', '--', ...files])
|
||||
}
|
||||
|
||||
debugLog('Done adding task modifications to index!')
|
||||
|
||||
const stagedFilesAfterAdd = await this.execGit([
|
||||
...getDiffCommand(this.diff, this.diffFilter),
|
||||
'--name-only',
|
||||
'-z',
|
||||
])
|
||||
|
||||
if (!stagedFilesAfterAdd && !this.allowEmpty) {
|
||||
// Tasks reverted all staged changes and the commit would be empty
|
||||
// Throw error to stop commit unless `--allow-empty` was used
|
||||
handleError(new Error('Prevented an empty git commit!'), ctx, ApplyEmptyCommitError)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore unstaged changes to partially changed files. If it at first fails,
|
||||
* this is probably because of conflicts between new task modifications.
|
||||
* 3-way merge usually fixes this, and in case it doesn't we should just give up and throw.
|
||||
*/
|
||||
async restoreUnstagedChanges(ctx) {
|
||||
debugLog('Restoring unstaged changes...')
|
||||
const unstagedPatch = this.getHiddenFilepath(PATCH_UNSTAGED)
|
||||
try {
|
||||
await this.execGit(['apply', ...GIT_APPLY_ARGS, unstagedPatch])
|
||||
} catch (applyError) {
|
||||
debugLog('Error while restoring changes:')
|
||||
debugLog(applyError)
|
||||
debugLog('Retrying with 3-way merge')
|
||||
try {
|
||||
// Retry with a 3-way merge if normal apply fails
|
||||
await this.execGit(['apply', ...GIT_APPLY_ARGS, '--3way', unstagedPatch])
|
||||
} catch (threeWayApplyError) {
|
||||
debugLog('Error while restoring unstaged changes using 3-way merge:')
|
||||
debugLog(threeWayApplyError)
|
||||
handleError(
|
||||
new Error('Unstaged changes could not be restored due to a merge conflict!'),
|
||||
ctx,
|
||||
RestoreUnstagedChangesError
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore original HEAD state in case of errors
|
||||
*/
|
||||
async restoreOriginalState(ctx) {
|
||||
try {
|
||||
debugLog('Restoring original state...')
|
||||
await this.execGit(['reset', '--hard', 'HEAD'])
|
||||
await this.execGit(['stash', 'apply', '--quiet', '--index', await this.getBackupStash(ctx)])
|
||||
|
||||
// Restore meta information about ongoing git merge
|
||||
await this.restoreMergeStatus(ctx)
|
||||
|
||||
// If stashing resurrected deleted files, clean them out
|
||||
await Promise.all(this.deletedFiles.map((file) => unlink(file)))
|
||||
|
||||
// Clean out patch
|
||||
await unlink(this.getHiddenFilepath(PATCH_UNSTAGED))
|
||||
|
||||
debugLog('Done restoring original state!')
|
||||
} catch (error) {
|
||||
handleError(error, ctx, RestoreOriginalStateError)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop the created stashes after everything has run
|
||||
*/
|
||||
async cleanup(ctx) {
|
||||
try {
|
||||
debugLog('Dropping backup stash...')
|
||||
await this.execGit(['stash', 'drop', '--quiet', await this.getBackupStash(ctx)])
|
||||
debugLog('Done dropping backup stash!')
|
||||
} catch (error) {
|
||||
handleError(error, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import debug from 'debug'
|
||||
|
||||
const debugLog = debug('lint-staged:groupFilesByConfig')
|
||||
|
||||
export const groupFilesByConfig = async ({ configs, files, singleConfigMode }) => {
|
||||
debugLog('Grouping %d files by %d configurations', files.length, Object.keys(configs).length)
|
||||
|
||||
const filesSet = new Set(files)
|
||||
const filesByConfig = {}
|
||||
|
||||
/** Configs are sorted deepest first by `searchConfigs` */
|
||||
for (const [filepath, config] of Object.entries(configs)) {
|
||||
/** When passed an explicit config object via the Node.js API‚ or an explicit path, skip logic */
|
||||
if (singleConfigMode) {
|
||||
filesByConfig[filepath] = { config, files }
|
||||
break
|
||||
}
|
||||
|
||||
const dir = path.normalize(path.dirname(filepath))
|
||||
|
||||
/** Check if file is inside directory of the configuration file */
|
||||
const isInsideDir = (file) => {
|
||||
const relative = path.relative(dir, file)
|
||||
return relative && !relative.startsWith('..') && !path.isAbsolute(relative)
|
||||
}
|
||||
|
||||
/** This config should match all files since it has a parent glob */
|
||||
const includeAllFiles = Object.keys(config).some((glob) => glob.startsWith('..'))
|
||||
|
||||
const scopedFiles = new Set(includeAllFiles ? filesSet : undefined)
|
||||
|
||||
/**
|
||||
* Without a parent glob, if file is inside the config file's directory,
|
||||
* assign it to that configuration.
|
||||
*/
|
||||
if (!includeAllFiles) {
|
||||
filesSet.forEach((file) => {
|
||||
if (isInsideDir(file)) {
|
||||
scopedFiles.add(file)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/** Files should only match a single config */
|
||||
scopedFiles.forEach((file) => {
|
||||
filesSet.delete(file)
|
||||
})
|
||||
|
||||
filesByConfig[filepath] = { config, files: Array.from(scopedFiles) }
|
||||
}
|
||||
|
||||
return filesByConfig
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
type SyncFunctionTask = (stagedFileNames: string[]) => string | string[]
|
||||
|
||||
type AsyncFunctionTask = (stagedFileNames: string[]) => Promise<string | string[]>
|
||||
|
||||
type FunctionTask = SyncFunctionTask | AsyncFunctionTask
|
||||
|
||||
export type Configuration =
|
||||
| Record<string, string | FunctionTask | (string | FunctionTask)[]>
|
||||
| FunctionTask
|
||||
|
||||
export type Options = {
|
||||
/**
|
||||
* Allow empty commits when tasks revert all staged changes
|
||||
* @default false
|
||||
*/
|
||||
allowEmpty?: boolean
|
||||
/**
|
||||
* The number of tasks to run concurrently, or `false` to run tasks serially
|
||||
* @default true
|
||||
*/
|
||||
concurrent?: boolean | number
|
||||
/**
|
||||
* Manual task configuration; disables automatic config file discovery when used
|
||||
*/
|
||||
config?: Configuration
|
||||
/**
|
||||
* Path to single configuration file; disables automatic config file discovery when used
|
||||
*/
|
||||
configPath?: string
|
||||
/**
|
||||
* Working directory to run all tasks in, defaults to current working directory
|
||||
*/
|
||||
cwd?: string
|
||||
/**
|
||||
* Whether or not to enable debug output
|
||||
* @default false
|
||||
*/
|
||||
debug?: boolean
|
||||
/**
|
||||
* Override the default `--staged` flag of `git diff` to get list of files.
|
||||
* @warn changing this also implies `stash: false`.
|
||||
* @example HEAD...origin/main
|
||||
*/
|
||||
diff?: string
|
||||
/**
|
||||
* Override the default `--diff-filter=ACMR` flag of `git diff` to get list of files
|
||||
* @default "ACMR"
|
||||
*/
|
||||
diffFilter?: string
|
||||
/**
|
||||
* Maximum argument string length, by default automatically detected
|
||||
*/
|
||||
maxArgLength?: number
|
||||
/**
|
||||
* Disable lint-staged’s own console output
|
||||
* @default false
|
||||
*/
|
||||
quiet?: boolean
|
||||
/**
|
||||
* Pass filepaths relative to `CWD` to tasks, instead of absolute
|
||||
* @default false
|
||||
*/
|
||||
relative?: boolean
|
||||
/**
|
||||
* Skip parsing of tasks for better shell support
|
||||
* @default false
|
||||
*/
|
||||
shell?: boolean
|
||||
/**
|
||||
* Enable the backup stash, and revert in case of errors.
|
||||
* @warn Disabling this also implies `hidePartiallyStaged: false`.
|
||||
* @default true
|
||||
*/
|
||||
stash?: boolean
|
||||
/**
|
||||
* Whether to hide unstaged changes from partially staged files before running tasks
|
||||
* @default true
|
||||
*/
|
||||
hidePartiallyStaged?: boolean
|
||||
/**
|
||||
* Show task output even when tasks succeed; by default only failed output is shown
|
||||
* @default false
|
||||
*/
|
||||
verbose?: boolean
|
||||
}
|
||||
|
||||
type LogFunction = (...params: any) => void
|
||||
|
||||
type Logger = {
|
||||
log: LogFunction
|
||||
warn: LogFunction
|
||||
error: LogFunction
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean} `true` when all tasks were successful, `false` when some tasks failed with errors
|
||||
* @throws {Error} when failed to some other errors
|
||||
*/
|
||||
export default function lintStaged(options: Options, logger?: Logger): Promise<boolean>
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import debugLib from 'debug'
|
||||
|
||||
import { execGit } from './execGit.js'
|
||||
import {
|
||||
GIT_ERROR,
|
||||
NO_CONFIGURATION,
|
||||
PREVENTED_EMPTY_COMMIT,
|
||||
RESTORE_STASH_EXAMPLE,
|
||||
} from './messages.js'
|
||||
import { printTaskOutput } from './printTaskOutput.js'
|
||||
import { runAll } from './runAll.js'
|
||||
import { cleanupSkipped } from './state.js'
|
||||
import {
|
||||
ApplyEmptyCommitError,
|
||||
ConfigNotFoundError,
|
||||
GetBackupStashError,
|
||||
GitError,
|
||||
} from './symbols.js'
|
||||
import { validateOptions } from './validateOptions.js'
|
||||
import { getVersion } from './version.js'
|
||||
|
||||
const debugLog = debugLib('lint-staged')
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {(...any) => void} LogFunction
|
||||
* @typedef {{ error: LogFunction, log: LogFunction, warn: LogFunction }} Logger
|
||||
*
|
||||
* Root lint-staged function that is called from `bin/lint-staged`.
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {Object} [options.allowEmpty] - Allow empty commits when tasks revert all staged changes
|
||||
* @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially
|
||||
* @param {object} [options.config] - Object with configuration for programmatic API
|
||||
* @param {string} [options.configPath] - Path to configuration file
|
||||
* @param {Object} [options.cwd] - Current working directory
|
||||
* @param {boolean} [options.debug] - Enable debug mode
|
||||
* @param {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|string} [options.shell] - Skip parsing of tasks for better shell support
|
||||
* @param {boolean} [options.stash] - Enable the backup stash, and revert in case of errors
|
||||
* @param {boolean} [options.verbose] - Show task output even when tasks succeed; by default only failed output is shown
|
||||
* @param {Logger} [logger]
|
||||
*
|
||||
* @returns {Promise<boolean>} Promise of whether the task passed or failed
|
||||
*/
|
||||
const lintStaged = async (
|
||||
{
|
||||
allowEmpty = false,
|
||||
concurrent = true,
|
||||
config: configObject,
|
||||
configPath,
|
||||
cwd,
|
||||
debug = false,
|
||||
diff,
|
||||
diffFilter,
|
||||
maxArgLength = getMaxArgLength() / 2,
|
||||
quiet = false,
|
||||
relative = false,
|
||||
shell = false,
|
||||
// Stashing should be disabled by default when the `diff` option is used
|
||||
stash = diff === undefined,
|
||||
hidePartiallyStaged = stash,
|
||||
verbose = false,
|
||||
} = {},
|
||||
logger = console
|
||||
) => {
|
||||
// Seemingly enable debug twice (also done in bin), so that it also works when using the Node.js API
|
||||
if (debug) {
|
||||
debugLib.enable('lint-staged*')
|
||||
|
||||
debugLog(
|
||||
'Running `lint-staged@%s` on Node.js %s (%s)',
|
||||
await getVersion(),
|
||||
process.version,
|
||||
process.platform
|
||||
)
|
||||
}
|
||||
|
||||
const gitVersion = await execGit(['version', '--build-options'], { cwd })
|
||||
debugLog('%s', gitVersion)
|
||||
|
||||
const options = {
|
||||
allowEmpty,
|
||||
concurrent,
|
||||
configObject,
|
||||
configPath,
|
||||
cwd,
|
||||
debug,
|
||||
diff,
|
||||
diffFilter,
|
||||
maxArgLength,
|
||||
quiet,
|
||||
relative,
|
||||
shell,
|
||||
stash,
|
||||
hidePartiallyStaged,
|
||||
verbose,
|
||||
}
|
||||
|
||||
await validateOptions(options, logger)
|
||||
|
||||
// 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
|
||||
|
||||
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) || cleanupSkipped(ctx)) &&
|
||||
!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 + '\n')
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
export default lintStaged
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
/** @typedef {import('./index').Logger} Logger */
|
||||
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
import debug from 'debug'
|
||||
import YAML from 'yaml'
|
||||
|
||||
import {
|
||||
CONFIG_FILE_NAMES,
|
||||
CONFIG_NAME,
|
||||
PACKAGE_JSON_FILE,
|
||||
PACKAGE_YAML_FILES,
|
||||
} from './configFiles.js'
|
||||
import { dynamicImport } from './dynamicImport.js'
|
||||
import { failedToLoadConfig } from './messages.js'
|
||||
import { resolveConfig } from './resolveConfig.js'
|
||||
|
||||
const debugLog = debug('lint-staged:loadConfig')
|
||||
|
||||
const jsonParse = (filePath, content) => {
|
||||
const isPackageFile = PACKAGE_JSON_FILE.includes(path.basename(filePath))
|
||||
try {
|
||||
const json = JSON.parse(content)
|
||||
return isPackageFile ? json[CONFIG_NAME] : json
|
||||
} catch (error) {
|
||||
if (path.basename(filePath) === PACKAGE_JSON_FILE) {
|
||||
debugLog('Ignoring invalid package file `%s` with content:\n%s', filePath, content)
|
||||
return undefined
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const yamlParse = (filePath, content) => {
|
||||
const isPackageFile = PACKAGE_YAML_FILES.includes(path.basename(filePath))
|
||||
try {
|
||||
const yaml = YAML.parse(content)
|
||||
return isPackageFile ? yaml[CONFIG_NAME] : yaml
|
||||
} catch (error) {
|
||||
if (isPackageFile) {
|
||||
debugLog('Ignoring invalid package file `%s` with content:\n%s', filePath, content)
|
||||
return undefined
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const NO_EXT = 'noExt'
|
||||
|
||||
/**
|
||||
* `lilconfig` doesn't support yaml files by default,
|
||||
* so we add custom loaders for those. Files without
|
||||
* an extensions are assumed to be yaml — this
|
||||
* assumption is in `cosmiconfig` as well.
|
||||
*/
|
||||
const loaders = {
|
||||
[NO_EXT]: yamlParse,
|
||||
'.cjs': dynamicImport,
|
||||
'.cts': dynamicImport,
|
||||
'.js': dynamicImport,
|
||||
'.json': jsonParse,
|
||||
'.mjs': dynamicImport,
|
||||
'.mts': dynamicImport,
|
||||
'.ts': dynamicImport,
|
||||
'.yaml': yamlParse,
|
||||
'.yml': yamlParse,
|
||||
}
|
||||
|
||||
const readFile = async (filepath) => {
|
||||
const absolutePath = path.resolve(filepath)
|
||||
return fs.readFile(absolutePath, 'utf-8')
|
||||
}
|
||||
|
||||
const loadConfigByExt = async (filepath) => {
|
||||
filepath = path.resolve(filepath)
|
||||
const ext = path.extname(filepath) || NO_EXT
|
||||
const loader = loaders[ext]
|
||||
|
||||
/**
|
||||
* No need to read file contents when loader only takes in the filepath argument
|
||||
* and reads itself; this is for `lilconfig` compatibility
|
||||
*/
|
||||
const content = loader.length > 1 ? await readFile(filepath) : undefined
|
||||
|
||||
return {
|
||||
config: await loader(filepath, content),
|
||||
filepath,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} options
|
||||
* @param {string} [options.configPath] - Explicit path to a config file
|
||||
* @param {string} [options.cwd] - Current working directory
|
||||
*/
|
||||
export const loadConfig = async ({ configPath, cwd }, logger) => {
|
||||
try {
|
||||
let result
|
||||
|
||||
if (configPath) {
|
||||
debugLog('Loading configuration from `%s`...', configPath)
|
||||
result = await loadConfigByExt(resolveConfig(configPath))
|
||||
} else {
|
||||
debugLog('Searching for configuration from `%s`...', cwd)
|
||||
const { lilconfig } = await import('lilconfig')
|
||||
const explorer = lilconfig(CONFIG_NAME, { searchPlaces: CONFIG_FILE_NAMES, loaders })
|
||||
result = await explorer.search(cwd)
|
||||
}
|
||||
|
||||
if (!result) return {}
|
||||
|
||||
// config is a promise when using the `dynamicImport` loader
|
||||
const config = (await result.config) ?? null
|
||||
const filepath = result.filepath
|
||||
|
||||
debugLog('Successfully loaded config from `%s`:\n%O', filepath, config)
|
||||
|
||||
return { config, filepath }
|
||||
} catch (error) {
|
||||
debugLog('Failed to load configuration from `%s` with error:\n', configPath, error)
|
||||
logger.error(failedToLoadConfig(configPath))
|
||||
return {}
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import debug from 'debug'
|
||||
|
||||
import { configurationError } from './messages.js'
|
||||
import { resolveTaskFn } from './resolveTaskFn.js'
|
||||
|
||||
const debugLog = debug('lint-staged:makeCmdTasks')
|
||||
|
||||
/**
|
||||
* Creates and returns an array of listr tasks which map to the given commands.
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {Array<string|Function>|string|Function} options.commands
|
||||
* @param {string} options.cwd
|
||||
* @param {Array<string>} options.files
|
||||
* @param {string} options.topLevelDir
|
||||
* @param {Boolean} shell
|
||||
* @param {Boolean} verbose
|
||||
*/
|
||||
export const makeCmdTasks = async ({ commands, cwd, files, topLevelDir, shell, verbose }) => {
|
||||
debugLog('Creating listr tasks for commands %o', commands)
|
||||
const commandArray = Array.isArray(commands) ? commands : [commands]
|
||||
const cmdTasks = []
|
||||
|
||||
for (const cmd of commandArray) {
|
||||
// command function may return array of commands that already include `stagedFiles`
|
||||
const isFn = typeof cmd === 'function'
|
||||
|
||||
/** Pass copy of file list to prevent mutation by function from config file. */
|
||||
const resolved = isFn ? await cmd([...files]) : cmd
|
||||
|
||||
const resolvedArray = Array.isArray(resolved) ? resolved : [resolved] // Wrap non-array command as array
|
||||
|
||||
for (const command of resolvedArray) {
|
||||
// If the function linter didn't return string | string[] it won't work
|
||||
// Do the validation here instead of `validateConfig` to skip evaluating the function multiple times
|
||||
if (isFn && typeof command !== 'string') {
|
||||
throw new Error(
|
||||
configurationError(
|
||||
'[Function]',
|
||||
'Function task should return a string or an array of strings',
|
||||
resolved
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const task = resolveTaskFn({ command, cwd, files, topLevelDir, isFn, shell, verbose })
|
||||
cmdTasks.push({ title: command, command, task })
|
||||
}
|
||||
}
|
||||
|
||||
return cmdTasks
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
import { inspect } from 'node:util'
|
||||
|
||||
import chalk from 'chalk'
|
||||
|
||||
import { error, info, warning } from './figures.js'
|
||||
|
||||
export const configurationError = (opt, helpMsg, value) =>
|
||||
`${chalk.redBright(`${error} Validation Error:`)}
|
||||
|
||||
Invalid value for '${chalk.bold(opt)}': ${chalk.bold(inspect(value))}
|
||||
|
||||
${helpMsg}`
|
||||
|
||||
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`)
|
||||
}
|
||||
|
||||
export const skippingHidePartiallyStaged = (stash, diff) => {
|
||||
const reason =
|
||||
diff !== undefined
|
||||
? '`--diff` was used'
|
||||
: !stash
|
||||
? '`--no-stash` was used'
|
||||
: '`--no-hide-partially-staged` was used'
|
||||
|
||||
return chalk.yellow(
|
||||
`${warning} Skipping hiding unstaged changes from partially staged files because ${reason}.\n`
|
||||
)
|
||||
}
|
||||
|
||||
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.
|
||||
`
|
||||
)
|
||||
|
||||
export const TASK_ERROR = 'Skipped because of errors from tasks.'
|
||||
|
||||
export const SKIPPED_GIT_ERROR = 'Skipped because of previous git error.'
|
||||
|
||||
export const GIT_ERROR = `\n ${chalk.redBright(`${error} lint-staged failed due to a git error.`)}`
|
||||
|
||||
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`)}
|
||||
`
|
||||
|
||||
export const RESTORE_STASH_EXAMPLE = `Any lost modifications can be restored from a git stash:
|
||||
|
||||
> git stash list
|
||||
stash@{0}: automatic lint-staged backup
|
||||
> git stash apply --index stash@{0}`
|
||||
|
||||
export const CONFIG_STDIN_ERROR = chalk.redBright(`${error} Failed to read config from stdin.`)
|
||||
|
||||
export const failedToLoadConfig = (filepath) =>
|
||||
chalk.redBright(`${error} Failed to read config from file "${filepath}".`)
|
||||
|
||||
export const failedToParseConfig = (
|
||||
filepath,
|
||||
error
|
||||
) => `${chalk.redBright(`${error} Failed to parse config from file "${filepath}".`)}
|
||||
|
||||
${error}
|
||||
|
||||
See https://github.com/okonet/lint-staged#configuration.`
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Reimplementation of "normalize-path"
|
||||
* @see https://github.com/jonschlinkert/normalize-path/blob/52c3a95ebebc2d98c1ad7606cbafa7e658656899/index.js
|
||||
*/
|
||||
|
||||
/*!
|
||||
* normalize-path <https://github.com/jonschlinkert/normalize-path>
|
||||
*
|
||||
* Copyright (c) 2014-2018, Jon Schlinkert.
|
||||
* Released under the MIT License.
|
||||
*/
|
||||
|
||||
import path from 'node:path'
|
||||
|
||||
/**
|
||||
* A file starting with \\?\
|
||||
* @see https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces
|
||||
*/
|
||||
const WIN32_FILE_NS = '\\\\?\\'
|
||||
|
||||
/**
|
||||
* A file starting with \\.\
|
||||
* @see https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces
|
||||
*/
|
||||
const WIN32_DEVICE_NS = '\\\\.\\'
|
||||
|
||||
/**
|
||||
* Normalize input file path to use POSIX separators
|
||||
* @param {String} input
|
||||
* @returns String
|
||||
*/
|
||||
export const normalizePath = (input) => {
|
||||
if (input === path.posix.sep || input === path.win32.sep) {
|
||||
return path.posix.sep
|
||||
}
|
||||
|
||||
let normalized = input.split(/[/\\]+/).join(path.posix.sep)
|
||||
|
||||
/** Handle win32 Namespaced paths by changing e.g. \\.\ to //./ */
|
||||
if (input.startsWith(WIN32_FILE_NS) || input.startsWith(WIN32_DEVICE_NS)) {
|
||||
normalized = normalized.replace(/^\/(\.|\?)/, '//$1')
|
||||
}
|
||||
|
||||
/** Remove trailing slash */
|
||||
if (normalized.endsWith(path.posix.sep)) {
|
||||
normalized = normalized.slice(0, -1)
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Return array of strings split from the output of `git <something> -z`.
|
||||
* With `-z`, git prints `fileA\u0000fileB\u0000fileC\u0000` so we need to
|
||||
* remove the last occurrence of `\u0000` before splitting
|
||||
*/
|
||||
export const parseGitZOutput = (input) =>
|
||||
input
|
||||
? input
|
||||
.replace(/\u0000$/, '') // eslint-disable-line no-control-regex
|
||||
.split('\u0000')
|
||||
: []
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Handle logging of listr `ctx.output` to the specified `logger`
|
||||
* @param {Object} ctx - The listr initial state
|
||||
* @param {Object} logger - The logger
|
||||
*/
|
||||
export const printTaskOutput = (ctx = {}, logger) => {
|
||||
if (!Array.isArray(ctx.output)) return
|
||||
const log = ctx.errors?.size > 0 ? logger.error : logger.log
|
||||
for (const line of ctx.output) {
|
||||
log(line)
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { createInterface } from 'node:readline'
|
||||
|
||||
/**
|
||||
* Returns a promise resolving to the first line written to stdin after invoking.
|
||||
* @warn will never resolve if called after writing to stdin
|
||||
*
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export const readStdin = () => {
|
||||
const readline = createInterface({ input: process.stdin })
|
||||
|
||||
return new Promise((resolve) => {
|
||||
readline.prompt()
|
||||
readline.on('line', (line) => {
|
||||
readline.close()
|
||||
resolve(line)
|
||||
})
|
||||
})
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { createRequire } from 'node:module'
|
||||
|
||||
/**
|
||||
* require() does not exist for ESM, so we must create it to use require.resolve().
|
||||
* @see https://nodejs.org/api/module.html#modulecreaterequirefilename
|
||||
*/
|
||||
const require = createRequire(import.meta.url)
|
||||
|
||||
export function resolveConfig(configPath) {
|
||||
try {
|
||||
return require.resolve(configPath)
|
||||
} catch {
|
||||
return configPath
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import debug from 'debug'
|
||||
|
||||
import { execGit } from './execGit.js'
|
||||
import { normalizePath } from './normalizePath.js'
|
||||
|
||||
const debugLog = debug('lint-staged:resolveGitRepo')
|
||||
|
||||
/**
|
||||
* Resolve .git directory relative to repo top-level directory
|
||||
*
|
||||
* @example ".git"
|
||||
*/
|
||||
const resolveRelativeGitDir = async (cwd) => {
|
||||
/**
|
||||
* Absolute repo top-level directory
|
||||
*
|
||||
* @example <caption>Git on macOS</caption>
|
||||
* "/Users/iiro/Documents/git/lint-staged"
|
||||
*
|
||||
* @example <caption>Git for Windows</caption>
|
||||
* "C:\Users\iiro\Documents\git\lint-staged"
|
||||
*
|
||||
* @example <caption>Git installed with MSYS2, this doesn't work when used as CWD with Node.js child_process</caption>
|
||||
* "/c/Users/iiro/Documents/git/lint-staged"
|
||||
*/
|
||||
const topLevelPromise = execGit(['rev-parse', '--show-toplevel'], { cwd })
|
||||
|
||||
/**
|
||||
* Absolute .git directory, similar to top-level
|
||||
*
|
||||
* @example "/Users/iiro/Documents/git/lint-staged/.git"
|
||||
*/
|
||||
const absoluteGitDirPromise = execGit(['rev-parse', '--absolute-git-dir'], { cwd })
|
||||
|
||||
const [topLevel, absoluteGitDir] = await Promise.all([topLevelPromise, absoluteGitDirPromise])
|
||||
|
||||
return path.relative(topLevel, absoluteGitDir)
|
||||
}
|
||||
|
||||
/** Resolve git directory and possible submodule paths */
|
||||
export const resolveGitRepo = async (cwd = process.cwd()) => {
|
||||
try {
|
||||
debugLog('Resolving git repo from `%s`', cwd)
|
||||
|
||||
// Unset GIT_DIR before running any git operations in case it's pointing to an incorrect location
|
||||
debugLog('Unset GIT_DIR (was `%s`)', process.env.GIT_DIR)
|
||||
delete process.env.GIT_DIR
|
||||
debugLog('Unset GIT_WORK_TREE (was `%s`)', process.env.GIT_WORK_TREE)
|
||||
delete process.env.GIT_WORK_TREE
|
||||
|
||||
const relativeTopLevelDir = await execGit(['rev-parse', '--show-cdup'], { cwd })
|
||||
const topLevelDir = normalizePath(path.join(cwd, relativeTopLevelDir))
|
||||
debugLog('Resolved git repository top-level directory to be `%s`', topLevelDir)
|
||||
|
||||
const relativeGitConfigDir = await resolveRelativeGitDir(cwd)
|
||||
const gitConfigDir = normalizePath(path.join(topLevelDir, relativeGitConfigDir))
|
||||
debugLog('Resolved git config directory to be `%s`', gitConfigDir)
|
||||
|
||||
return { topLevelDir, gitConfigDir }
|
||||
} catch (error) {
|
||||
debugLog('Failed to resolve git repo with error:', error)
|
||||
return { error, topLevelDir: null, gitConfigDir: null }
|
||||
}
|
||||
}
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
import chalk from 'chalk'
|
||||
import debug from 'debug'
|
||||
import { execa, execaCommand } from 'execa'
|
||||
import pidTree from 'pidtree'
|
||||
import { parseArgsStringToArgv } from 'string-argv'
|
||||
|
||||
import { error, info } from './figures.js'
|
||||
import { getInitialState } from './state.js'
|
||||
import { TaskError } from './symbols.js'
|
||||
|
||||
/**
|
||||
* @see https://github.com/sindresorhus/execa/blob/f4b8b3ab601c94d1503f1010822952758dcc6350/lib/command.js#L32-L37
|
||||
*/
|
||||
const escapeSpaces = (input) => input.replaceAll(' ', '\\ ')
|
||||
|
||||
const TASK_ERROR = 'lint-staged:taskError'
|
||||
|
||||
const debugLog = debug('lint-staged:resolveTaskFn')
|
||||
|
||||
const getTag = ({ code, killed, signal }) => (killed && 'KILLED') || signal || code || 'FAILED'
|
||||
|
||||
/**
|
||||
* Handle task console output.
|
||||
*
|
||||
* @param {string} command
|
||||
* @param {Object} result
|
||||
* @param {string} result.stdout
|
||||
* @param {string} result.stderr
|
||||
* @param {boolean} result.failed
|
||||
* @param {boolean} result.killed
|
||||
* @param {string} result.signal
|
||||
* @param {Object} ctx
|
||||
* @returns {Error}
|
||||
*/
|
||||
const handleOutput = (command, result, ctx, isError = false) => {
|
||||
const { stderr, stdout } = result
|
||||
const hasOutput = !!stderr || !!stdout
|
||||
|
||||
if (hasOutput) {
|
||||
const outputTitle = isError ? chalk.redBright(`${error} ${command}:`) : `${info} ${command}:`
|
||||
const output = []
|
||||
.concat(ctx.quiet ? [] : ['', outputTitle])
|
||||
.concat(stderr ? stderr : [])
|
||||
.concat(stdout ? stdout : [])
|
||||
ctx.output.push(output.join('\n'))
|
||||
} else if (isError) {
|
||||
// Show generic error when task had no output
|
||||
const tag = getTag(result)
|
||||
const message = chalk.redBright(`\n${error} ${command} failed without output (${tag}).`)
|
||||
if (!ctx.quiet) ctx.output.push(message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 depending on process result.
|
||||
*
|
||||
* @param {string} command
|
||||
* @param {Object} result
|
||||
* @param {string} result.stdout
|
||||
* @param {string} result.stderr
|
||||
* @param {boolean} result.failed
|
||||
* @param {boolean} result.killed
|
||||
* @param {string} result.signal
|
||||
* @param {Object} ctx
|
||||
* @returns {Error}
|
||||
*/
|
||||
const makeErr = (command, result, ctx) => {
|
||||
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(`${chalk.redBright(command)} ${chalk.dim(`[${tag}]`)}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the task function for the linter.
|
||||
*
|
||||
* @param {Object} options
|
||||
* @param {string} options.command — Linter task
|
||||
* @param {string} [options.cwd]
|
||||
* @param {String} options.topLevelDir - Current git repo top-level 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.shell] — Whether to skip parsing linter task for better shell support
|
||||
* @param {Boolean} [options.verbose] — Always show task verbose
|
||||
* @returns {() => Promise<Array<string>>}
|
||||
*/
|
||||
export const resolveTaskFn = ({
|
||||
command,
|
||||
cwd = process.cwd(),
|
||||
files,
|
||||
topLevelDir,
|
||||
isFn,
|
||||
shell = false,
|
||||
verbose = false,
|
||||
}) => {
|
||||
const [cmd, ...args] = parseArgsStringToArgv(command)
|
||||
debugLog('cmd:', cmd)
|
||||
debugLog('args:', args)
|
||||
|
||||
const execaOptions = {
|
||||
// Only use topLevelDir as CWD if we are using the git binary
|
||||
// e.g `npm` should run tasks in the actual CWD
|
||||
cwd: /^git(\.exe)?/i.test(cmd) ? topLevelDir : cwd,
|
||||
preferLocal: true,
|
||||
reject: false,
|
||||
shell,
|
||||
stdin: 'ignore',
|
||||
}
|
||||
|
||||
debugLog('execaOptions:', execaOptions)
|
||||
|
||||
return async (ctx = getInitialState()) => {
|
||||
const execaChildProcess = shell
|
||||
? execaCommand(
|
||||
isFn ? command : `${command} ${files.map(escapeSpaces).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) {
|
||||
throw makeErr(command, result, ctx)
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
handleOutput(command, result, ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
/** @typedef {import('./index').Logger} Logger */
|
||||
|
||||
import path from 'node:path'
|
||||
|
||||
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,
|
||||
FAILED_GET_STAGED_FILES,
|
||||
NO_STAGED_FILES,
|
||||
NO_TASKS,
|
||||
NOT_GIT_REPO,
|
||||
SKIPPED_GIT_ERROR,
|
||||
skippingBackup,
|
||||
skippingHidePartiallyStaged,
|
||||
} from './messages.js'
|
||||
import { normalizePath } from './normalizePath.js'
|
||||
import { resolveGitRepo } from './resolveGitRepo.js'
|
||||
import { searchConfigs } from './searchConfigs.js'
|
||||
import {
|
||||
applyModificationsSkipped,
|
||||
cleanupEnabled,
|
||||
cleanupSkipped,
|
||||
getInitialState,
|
||||
restoreOriginalStateEnabled,
|
||||
restoreOriginalStateSkipped,
|
||||
restoreUnstagedChangesSkipped,
|
||||
shouldHidePartiallyStagedFiles,
|
||||
} from './state.js'
|
||||
import { ConfigNotFoundError, GetStagedFilesError, GitError, GitRepoError } from './symbols.js'
|
||||
|
||||
const debugLog = debug('lint-staged:runAll')
|
||||
|
||||
const createError = (ctx) => Object.assign(new Error('lint-staged failed'), { ctx })
|
||||
|
||||
/**
|
||||
* Executes all tasks and either resolves or rejects the promise
|
||||
*
|
||||
* @param {object} options
|
||||
* @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.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
|
||||
* @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} [options.stash] - Enable the backup stash, and revert in case of errors
|
||||
* @param {boolean} [options.verbose] - Show task output even when tasks succeed; by default only failed output is shown
|
||||
* @param {Logger} logger
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export const runAll = async (
|
||||
{
|
||||
allowEmpty = false,
|
||||
concurrent = true,
|
||||
configObject,
|
||||
configPath,
|
||||
cwd,
|
||||
debug = false,
|
||||
diff,
|
||||
diffFilter,
|
||||
maxArgLength,
|
||||
quiet = false,
|
||||
relative = false,
|
||||
shell = false,
|
||||
// Stashing should be disabled by default when the `diff` option is used
|
||||
stash = diff === undefined,
|
||||
hidePartiallyStaged = stash,
|
||||
verbose = false,
|
||||
},
|
||||
logger = console
|
||||
) => {
|
||||
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 })
|
||||
|
||||
const { topLevelDir, gitConfigDir } = await resolveGitRepo(cwd)
|
||||
if (!topLevelDir) {
|
||||
if (!quiet) ctx.output.push(NOT_GIT_REPO)
|
||||
ctx.errors.add(GitRepoError)
|
||||
throw createError(ctx)
|
||||
}
|
||||
|
||||
// Test whether we have any commits or not.
|
||||
// Stashing must be disabled with no initial commit.
|
||||
const hasInitialCommit = await execGit(['log', '-1'], { cwd: topLevelDir })
|
||||
.then(() => true)
|
||||
.catch(() => false)
|
||||
|
||||
// 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 && !quiet) {
|
||||
logger.warn(skippingBackup(hasInitialCommit, diff))
|
||||
}
|
||||
|
||||
ctx.shouldHidePartiallyStaged = hidePartiallyStaged
|
||||
if (!ctx.shouldHidePartiallyStaged && !quiet) {
|
||||
logger.warn(skippingHidePartiallyStaged(hasInitialCommit && stash, diff))
|
||||
}
|
||||
|
||||
const files = await getStagedFiles({ cwd: topLevelDir, diff, diffFilter })
|
||||
if (!files) {
|
||||
if (!quiet) ctx.output.push(FAILED_GET_STAGED_FILES)
|
||||
ctx.errors.add(GetStagedFilesError)
|
||||
throw createError(ctx, GetStagedFilesError)
|
||||
}
|
||||
debugLog('Loaded list of staged files in git:\n%O', files)
|
||||
|
||||
// If there are no files avoid executing any lint-staged logic
|
||||
if (files.length === 0) {
|
||||
if (!quiet) ctx.output.push(NO_STAGED_FILES)
|
||||
return ctx
|
||||
}
|
||||
|
||||
const foundConfigs = await searchConfigs({ configObject, configPath, cwd, topLevelDir }, 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
|
||||
// Warn user when their command includes `git add`
|
||||
let hasDeprecatedGitAdd = false
|
||||
|
||||
const listrOptions = {
|
||||
ctx,
|
||||
exitOnError: false,
|
||||
registerSignalListeners: false,
|
||||
...getRenderer({ debug, quiet }, logger),
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to set max event listener count to the total number
|
||||
* of generated tasks. The event listener is used to keep track of
|
||||
* the interrupt signal and kill all tasks when it happens. See the
|
||||
* `interruptExecutionOnError` in `resolveTaskFn`.
|
||||
*/
|
||||
let listrTaskCount = 0
|
||||
|
||||
const listrTasks = []
|
||||
|
||||
// Set of all staged files that matched a task glob. Values in a set are unique.
|
||||
const matchedFiles = new Set()
|
||||
|
||||
for (const [configPath, { config, files }] of Object.entries(filesByConfig)) {
|
||||
const configName = configPath ? normalizePath(path.relative(cwd, configPath)) : 'Config object'
|
||||
|
||||
const stagedFileChunks = chunkFiles({ baseDir: topLevelDir, files, maxArgLength, relative })
|
||||
|
||||
// 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
|
||||
|
||||
const chunkCount = stagedFileChunks.length
|
||||
if (chunkCount > 1) {
|
||||
debugLog('Chunked staged files from `%s` into %d part', configPath, chunkCount)
|
||||
}
|
||||
|
||||
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,
|
||||
topLevelDir,
|
||||
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
|
||||
},
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
listrTaskCount += chunkListrTasks.length
|
||||
|
||||
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 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')}`
|
||||
}
|
||||
return false
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (hasDeprecatedGitAdd && !quiet) {
|
||||
logger.warn(DEPRECATED_GIT_ADD)
|
||||
}
|
||||
|
||||
// If all of the configured tasks should be skipped
|
||||
// avoid executing any lint-staged logic
|
||||
if (listrTasks.every((task) => task.skip())) {
|
||||
if (!quiet) ctx.output.push(NO_TASKS)
|
||||
return ctx
|
||||
}
|
||||
|
||||
// Chunk matched files for better Windows compatibility
|
||||
const matchedFileChunks = chunkFiles({
|
||||
// matched files are relative to `cwd`, not `topLevelDir`, when `relative` is used
|
||||
baseDir: cwd,
|
||||
files: Array.from(matchedFiles),
|
||||
maxArgLength,
|
||||
relative: false,
|
||||
})
|
||||
|
||||
const git = new GitWorkflow({
|
||||
allowEmpty,
|
||||
gitConfigDir,
|
||||
topLevelDir,
|
||||
matchedFileChunks,
|
||||
diff,
|
||||
diffFilter,
|
||||
})
|
||||
|
||||
const runner = new Listr(
|
||||
[
|
||||
{
|
||||
title: ctx.shouldBackup ? 'Backing up original state...' : 'Preparing lint-staged...',
|
||||
task: (ctx, task) => git.prepare(ctx, task),
|
||||
},
|
||||
{
|
||||
title: 'Hiding unstaged changes to partially staged files...',
|
||||
task: (ctx) => git.hideUnstagedChanges(ctx),
|
||||
enabled: shouldHidePartiallyStagedFiles,
|
||||
},
|
||||
{
|
||||
title: `Running tasks for ${diff ? 'changed' : '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),
|
||||
skip: applyModificationsSkipped,
|
||||
},
|
||||
{
|
||||
title: 'Restoring unstaged changes to partially staged files...',
|
||||
task: (ctx) => git.restoreUnstagedChanges(ctx),
|
||||
enabled: shouldHidePartiallyStagedFiles,
|
||||
skip: restoreUnstagedChangesSkipped,
|
||||
},
|
||||
{
|
||||
title: 'Reverting to original state because of errors...',
|
||||
task: (ctx) => git.restoreOriginalState(ctx),
|
||||
enabled: restoreOriginalStateEnabled,
|
||||
skip: restoreOriginalStateSkipped,
|
||||
},
|
||||
{
|
||||
title: 'Cleaning up temporary files...',
|
||||
task: (ctx) => git.cleanup(ctx),
|
||||
enabled: cleanupEnabled,
|
||||
skip: cleanupSkipped,
|
||||
},
|
||||
],
|
||||
listrOptions
|
||||
)
|
||||
|
||||
debugLog('Set max event listeners to the number of tasks: %i', listrTaskCount)
|
||||
ctx.events.setMaxListeners(listrTaskCount)
|
||||
|
||||
await runner.run()
|
||||
|
||||
if (ctx.errors.size > 0) {
|
||||
throw createError(ctx)
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
/** @typedef {import('./index').Logger} Logger */
|
||||
|
||||
import path from 'node:path'
|
||||
|
||||
import debug from 'debug'
|
||||
|
||||
import { CONFIG_FILE_NAMES } from './configFiles.js'
|
||||
import { execGit } from './execGit.js'
|
||||
import { loadConfig } from './loadConfig.js'
|
||||
import { normalizePath } from './normalizePath.js'
|
||||
import { parseGitZOutput } from './parseGitZOutput.js'
|
||||
import { validateConfig } from './validateConfig.js'
|
||||
|
||||
const debugLog = debug('lint-staged:searchConfigs')
|
||||
|
||||
const EXEC_GIT = ['ls-files', '-z', '--full-name', '-t']
|
||||
|
||||
const filterPossibleConfigFiles = (file) => CONFIG_FILE_NAMES.includes(path.basename(file))
|
||||
|
||||
const numberOfLevels = (file) => file.split('/').length
|
||||
|
||||
const sortDeepestParth = (a, b) => (numberOfLevels(a) > numberOfLevels(b) ? -1 : 1)
|
||||
|
||||
const isInsideDirectory = (dir) => (file) => file.startsWith(normalizePath(dir))
|
||||
|
||||
/**
|
||||
* Search all config files from the git repository, preferring those inside `cwd`.
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {Object} [options.configObject] - Explicit config object from the js API
|
||||
* @param {string} [options.configPath] - Explicit path to a config file
|
||||
* @param {string} [options.cwd] - Current working directory
|
||||
* @param {Logger} logger
|
||||
*
|
||||
* @returns {Promise<{ [key: string]: { config: *, files: string[] } }>} found configs with filepath as key, and config as value
|
||||
*/
|
||||
export const searchConfigs = async (
|
||||
{ configObject, configPath, cwd = process.cwd(), topLevelDir = cwd },
|
||||
logger
|
||||
) => {
|
||||
debugLog('Searching for configuration files...')
|
||||
|
||||
// Return explicit config object from js API
|
||||
if (configObject) {
|
||||
debugLog('Using single direct configuration object...')
|
||||
|
||||
return { '': validateConfig(configObject, 'config object', logger) }
|
||||
}
|
||||
|
||||
// Use only explicit config path instead of discovering multiple
|
||||
if (configPath) {
|
||||
debugLog('Using single configuration path...')
|
||||
|
||||
const { config, filepath } = await loadConfig({ configPath }, logger)
|
||||
|
||||
if (!config) return {}
|
||||
return { [configPath]: validateConfig(config, filepath, logger) }
|
||||
}
|
||||
|
||||
const [cachedFilesWithStatus, otherFilesWithStatus] = await Promise.all([
|
||||
/** Get all possible config files known to git */
|
||||
execGit(EXEC_GIT, { cwd: topLevelDir }).then(parseGitZOutput),
|
||||
/** Get all possible config files from uncommitted files */
|
||||
execGit([...EXEC_GIT, '--others', '--exclude-standard'], { cwd: topLevelDir }).then(
|
||||
parseGitZOutput
|
||||
),
|
||||
])
|
||||
|
||||
/** Sort possible config files so that deepest is first */
|
||||
const possibleConfigFiles = [...cachedFilesWithStatus, ...otherFilesWithStatus]
|
||||
.flatMap(
|
||||
/**
|
||||
* Leave out lines starting with "S " to ignore not-checked-out files in a sparse repo.
|
||||
* The "S" status means a tracked file that is "skip-worktree"
|
||||
* @see https://git-scm.com/docs/git-ls-files#Documentation/git-ls-files.txt--t
|
||||
*/ (line) => (line.startsWith('S ') ? [] : [line.replace(/^[HSMRCK?U] /, '')])
|
||||
)
|
||||
.filter(filterPossibleConfigFiles)
|
||||
.map((file) => normalizePath(path.join(topLevelDir, file)))
|
||||
.filter(isInsideDirectory(cwd))
|
||||
.sort(sortDeepestParth)
|
||||
|
||||
debugLog('Found possible config files:', possibleConfigFiles)
|
||||
|
||||
/** Create object with key as config file, and value as null */
|
||||
const configs = possibleConfigFiles.reduce(
|
||||
(acc, configPath) => Object.assign(acc, { [configPath]: null }),
|
||||
{}
|
||||
)
|
||||
|
||||
/** Load and validate all configs to the above object */
|
||||
await Promise.all(
|
||||
Object.keys(configs).map((configPath) =>
|
||||
loadConfig({ configPath }, logger).then(({ config, filepath }) => {
|
||||
if (config) {
|
||||
if (configPath !== filepath) {
|
||||
debugLog('Config file "%s" resolved to "%s"', configPath, filepath)
|
||||
}
|
||||
|
||||
configs[configPath] = validateConfig(config, filepath, logger)
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
/** Get validated configs from the above object, without any `null` values (not found) */
|
||||
const foundConfigs = Object.entries(configs)
|
||||
.filter(([, value]) => !!value)
|
||||
.reduce((acc, [key, value]) => Object.assign(acc, { [key]: value }), {})
|
||||
|
||||
/**
|
||||
* Try to find a single config from parent directories
|
||||
* to match old behavior before monorepo support
|
||||
*/
|
||||
if (!Object.keys(foundConfigs).length) {
|
||||
debugLog('Could not find config files inside "%s"', cwd)
|
||||
|
||||
const { config, filepath } = await loadConfig({ cwd }, logger)
|
||||
if (config) {
|
||||
debugLog('Found parent configuration file from "%s"', filepath)
|
||||
|
||||
foundConfigs[filepath] = validateConfig(config, filepath, logger)
|
||||
} else {
|
||||
debugLog('Could not find parent configuration files from "%s"', cwd)
|
||||
}
|
||||
}
|
||||
|
||||
debugLog('Found %d config files', Object.keys(foundConfigs).length)
|
||||
|
||||
return foundConfigs
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import EventEmitter from 'events'
|
||||
|
||||
import { GIT_ERROR, TASK_ERROR } from './messages.js'
|
||||
import {
|
||||
GitError,
|
||||
RestoreOriginalStateError,
|
||||
RestoreUnstagedChangesError,
|
||||
TaskError,
|
||||
} from './symbols.js'
|
||||
|
||||
export const getInitialState = ({ quiet = false } = {}) => ({
|
||||
hasPartiallyStagedFiles: null,
|
||||
shouldBackup: null,
|
||||
backupHash: null,
|
||||
shouldHidePartiallyStaged: true,
|
||||
errors: new Set([]),
|
||||
events: new EventEmitter(),
|
||||
output: [],
|
||||
quiet,
|
||||
})
|
||||
|
||||
export const shouldHidePartiallyStagedFiles = (ctx) =>
|
||||
ctx.hasPartiallyStagedFiles && ctx.shouldHidePartiallyStaged
|
||||
|
||||
export const applyModificationsSkipped = (ctx) => {
|
||||
// Always apply back unstaged modifications when skipping backup
|
||||
if (!ctx.shouldBackup) return false
|
||||
// Should be skipped in case of git errors
|
||||
if (ctx.errors.has(GitError)) {
|
||||
return GIT_ERROR
|
||||
}
|
||||
// Should be skipped when tasks fail
|
||||
if (ctx.errors.has(TaskError)) {
|
||||
return TASK_ERROR
|
||||
}
|
||||
}
|
||||
|
||||
export const restoreUnstagedChangesSkipped = (ctx) => {
|
||||
// Should be skipped in case of git errors
|
||||
if (ctx.errors.has(GitError)) {
|
||||
return GIT_ERROR
|
||||
}
|
||||
|
||||
// Should be skipped when tasks fail
|
||||
if (ctx.errors.has(TaskError)) {
|
||||
return TASK_ERROR
|
||||
}
|
||||
}
|
||||
|
||||
export const restoreOriginalStateEnabled = (ctx) =>
|
||||
ctx.shouldBackup && (ctx.errors.has(TaskError) || ctx.errors.has(RestoreUnstagedChangesError))
|
||||
|
||||
export const restoreOriginalStateSkipped = (ctx) => {
|
||||
// Should be skipped in case of unknown git errors
|
||||
if (ctx.errors.has(GitError) && !ctx.errors.has(RestoreUnstagedChangesError)) {
|
||||
return GIT_ERROR
|
||||
}
|
||||
}
|
||||
|
||||
export const cleanupEnabled = (ctx) => ctx.shouldBackup
|
||||
|
||||
export const cleanupSkipped = (ctx) => {
|
||||
// Should be skipped in case of unknown git errors
|
||||
if (restoreOriginalStateSkipped(ctx)) {
|
||||
return GIT_ERROR
|
||||
}
|
||||
|
||||
// Should be skipped when reverting to original state fails
|
||||
if (ctx.errors.has(RestoreOriginalStateError)) {
|
||||
return GIT_ERROR
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
export const ApplyEmptyCommitError = Symbol('ApplyEmptyCommitError')
|
||||
|
||||
export const ConfigNotFoundError = new Error('Configuration could not be found')
|
||||
|
||||
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')
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import { incorrectBraces } from './messages.js'
|
||||
|
||||
/**
|
||||
* A correctly-formed brace expansion must contain unquoted opening and closing braces,
|
||||
* and at least one unquoted comma or a valid sequence expression.
|
||||
* Any incorrectly formed brace expansion is left unchanged.
|
||||
*
|
||||
* @see https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html
|
||||
*
|
||||
* Lint-staged uses `micromatch` for brace expansion, and its behavior is to treat
|
||||
* invalid brace expansions as literal strings, which means they (typically) do not match
|
||||
* anything.
|
||||
*
|
||||
* This RegExp tries to match most cases of invalid brace expansions, so that they can be
|
||||
* detected, warned about, and re-formatted by removing the braces and thus hopefully
|
||||
* matching the files as intended by the user. The only real fix is to remove the incorrect
|
||||
* braces from user configuration, but this is left to the user (after seeing the warning).
|
||||
*
|
||||
* @example <caption>Globs with brace expansions</caption>
|
||||
* - *.{js,tx} // expanded as *.js, *.ts
|
||||
* - *.{{j,t}s,css} // expanded as *.js, *.ts, *.css
|
||||
* - file_{1..10}.css // expanded as file_1.css, file_2.css, …, file_10.css
|
||||
*
|
||||
* @example <caption>Globs with incorrect brace expansions</caption>
|
||||
* - *.{js} // should just be *.js
|
||||
* - *.{js,{ts}} // should just be *.{js,ts}
|
||||
* - *.\{js\} // escaped braces, so they're treated literally
|
||||
* - *.${js} // dollar-sign inhibits expansion, so treated literally
|
||||
* - *.{js\,ts} // the comma is escaped, so treated literally
|
||||
*/
|
||||
export const INCORRECT_BRACES_REGEXP = /(?<![\\$])({)(?:(?!(?<!\\),|\.\.|\{|\}).)*?(?<!\\)(})/g
|
||||
|
||||
/**
|
||||
* @param {string} pattern
|
||||
* @returns {string}
|
||||
*/
|
||||
const stripIncorrectBraces = (pattern) => {
|
||||
let output = `${pattern}`
|
||||
let match = null
|
||||
|
||||
while ((match = INCORRECT_BRACES_REGEXP.exec(pattern))) {
|
||||
const fullMatch = match[0]
|
||||
const withoutBraces = fullMatch.replace(/{/, '').replace(/}/, '')
|
||||
output = output.replace(fullMatch, withoutBraces)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
/**
|
||||
* This RegExp matches "duplicate" opening and closing braces, without any other braces
|
||||
* in between, where the duplication is redundant and should be removed.
|
||||
*
|
||||
* @example *.{{js,ts}} // should just be *.{js,ts}
|
||||
*/
|
||||
export const DOUBLE_BRACES_REGEXP = /{{[^}{]*}}/
|
||||
|
||||
/**
|
||||
* @param {string} pattern
|
||||
* @returns {string}
|
||||
*/
|
||||
const stripDoubleBraces = (pattern) => {
|
||||
let output = `${pattern}`
|
||||
const match = DOUBLE_BRACES_REGEXP.exec(pattern)?.[0]
|
||||
|
||||
if (match) {
|
||||
const withoutBraces = match.replace('{{', '{').replace('}}', '}')
|
||||
output = output.replace(match, withoutBraces)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and remove incorrect brace expansions from glob pattern.
|
||||
* For example `*.{js}` is incorrect because it doesn't contain a `,` or `..`,
|
||||
* and will be reformatted as `*.js`.
|
||||
*
|
||||
* @param {string} pattern the glob pattern
|
||||
* @param {*} logger
|
||||
* @returns {string}
|
||||
*/
|
||||
export const validateBraces = (pattern, logger) => {
|
||||
const fixedPattern = stripDoubleBraces(stripIncorrectBraces(pattern))
|
||||
|
||||
if (fixedPattern !== pattern) {
|
||||
logger.warn(incorrectBraces(pattern, fixedPattern))
|
||||
}
|
||||
|
||||
return fixedPattern
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
/** @typedef {import('./index').Logger} Logger */
|
||||
|
||||
import { inspect } from 'node:util'
|
||||
|
||||
import debug from 'debug'
|
||||
|
||||
import { configurationError, failedToParseConfig } 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', isObject],
|
||||
['linters', isObject],
|
||||
['ignore', (key) => Array.isArray(key)],
|
||||
['subTaskConcurrency', (key) => typeof key === 'number'],
|
||||
['renderer', (key) => typeof key === 'string'],
|
||||
['relative', (key) => typeof key === 'boolean'],
|
||||
])
|
||||
|
||||
export const validateConfigLogic = (config, configPath, logger) => {
|
||||
debugLog('Validating config from `%s`...', configPath)
|
||||
|
||||
if (!config || (typeof config !== 'object' && typeof config !== 'function')) {
|
||||
throw ConfigFormatError
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (Object.entries(config).length === 0) {
|
||||
throw ConfigEmptyError
|
||||
}
|
||||
|
||||
const errors = []
|
||||
|
||||
/**
|
||||
* 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(
|
||||
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 Object.assign(collection, { [fixedPattern]: task })
|
||||
}, {})
|
||||
|
||||
if (errors.length) {
|
||||
const message = errors.join('\n\n')
|
||||
|
||||
logger.error(failedToParseConfig(configPath, message))
|
||||
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
debugLog('Validated config from `%s`:', configPath)
|
||||
debugLog(inspect(config, { compact: false }))
|
||||
|
||||
return validatedConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs config validation. Throws error if the config is not valid.
|
||||
* @param {Object} config
|
||||
* @param {string} configPath
|
||||
* @param {Logger} logger
|
||||
* @returns {Object} config
|
||||
*/
|
||||
export const validateConfig = (config, configPath, logger) => {
|
||||
try {
|
||||
return validateConfigLogic(config, configPath, logger)
|
||||
} catch (error) {
|
||||
logger.error(failedToParseConfig(configPath, error))
|
||||
throw error
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { constants } from 'node:fs'
|
||||
import fs from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
|
||||
import debug from 'debug'
|
||||
|
||||
import { invalidOption } from './messages.js'
|
||||
import { InvalidOptionsError } from './symbols.js'
|
||||
|
||||
const debugLog = debug('lint-staged:validateOptions')
|
||||
|
||||
/**
|
||||
* Validate lint-staged options, either from the Node.js API or the command line flags.
|
||||
* @param {*} options
|
||||
* @param {boolean|string} [options.cwd] - Current working directory
|
||||
* @param {boolean|string} [options.shell] - Skip parsing of tasks for better shell support
|
||||
*
|
||||
* @throws {InvalidOptionsError}
|
||||
*/
|
||||
export const validateOptions = async (options = {}, logger) => {
|
||||
debugLog('Validating options...')
|
||||
|
||||
/** Ensure the passed cwd option exists; it might also be relative */
|
||||
if (typeof options.cwd === 'string') {
|
||||
try {
|
||||
const resolved = path.resolve(options.cwd)
|
||||
await fs.access(resolved, constants.F_OK)
|
||||
} catch (error) {
|
||||
debugLog('Failed to validate options: %o', options)
|
||||
logger.error(invalidOption('cwd', options.cwd, error.message))
|
||||
throw InvalidOptionsError
|
||||
}
|
||||
}
|
||||
|
||||
/** Ensure the passed shell option is executable */
|
||||
if (typeof options.shell === 'string') {
|
||||
try {
|
||||
await fs.access(options.shell, constants.X_OK)
|
||||
} catch (error) {
|
||||
debugLog('Failed to validate options: %o', options)
|
||||
logger.error(invalidOption('shell', options.shell, error.message))
|
||||
throw InvalidOptionsError
|
||||
}
|
||||
}
|
||||
|
||||
debugLog('Validated options: %o', options)
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import fs from 'node:fs/promises'
|
||||
|
||||
export const getVersion = async () => {
|
||||
const packageJson = JSON.parse(await fs.readFile(new URL('../package.json', import.meta.url)))
|
||||
return packageJson.version
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"name": "chalk",
|
||||
"version": "5.6.2",
|
||||
"description": "Terminal string styling done right",
|
||||
"license": "MIT",
|
||||
"repository": "chalk/chalk",
|
||||
"funding": "https://github.com/chalk/chalk?sponsor=1",
|
||||
"type": "module",
|
||||
"main": "./source/index.js",
|
||||
"exports": "./source/index.js",
|
||||
"imports": {
|
||||
"#ansi-styles": "./source/vendor/ansi-styles/index.js",
|
||||
"#supports-color": {
|
||||
"node": "./source/vendor/supports-color/index.js",
|
||||
"default": "./source/vendor/supports-color/browser.js"
|
||||
}
|
||||
},
|
||||
"types": "./source/index.d.ts",
|
||||
"sideEffects": false,
|
||||
"engines": {
|
||||
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "xo && c8 ava && tsd",
|
||||
"bench": "matcha benchmark.js"
|
||||
},
|
||||
"files": [
|
||||
"source",
|
||||
"!source/index.test-d.ts"
|
||||
],
|
||||
"keywords": [
|
||||
"color",
|
||||
"colour",
|
||||
"colors",
|
||||
"terminal",
|
||||
"console",
|
||||
"cli",
|
||||
"string",
|
||||
"ansi",
|
||||
"style",
|
||||
"styles",
|
||||
"tty",
|
||||
"formatting",
|
||||
"rgb",
|
||||
"256",
|
||||
"shell",
|
||||
"xterm",
|
||||
"log",
|
||||
"logging",
|
||||
"command-line",
|
||||
"text"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/node": "^16.11.10",
|
||||
"ava": "^3.15.0",
|
||||
"c8": "^7.10.0",
|
||||
"color-convert": "^2.0.1",
|
||||
"execa": "^6.0.0",
|
||||
"log-update": "^5.0.0",
|
||||
"matcha": "^0.7.0",
|
||||
"tsd": "^0.19.0",
|
||||
"xo": "^0.57.0",
|
||||
"yoctodelay": "^2.0.0"
|
||||
},
|
||||
"xo": {
|
||||
"rules": {
|
||||
"unicorn/prefer-string-slice": "off",
|
||||
"@typescript-eslint/consistent-type-imports": "off",
|
||||
"@typescript-eslint/consistent-type-exports": "off",
|
||||
"@typescript-eslint/consistent-type-definitions": "off",
|
||||
"unicorn/expiring-todo-comments": "off"
|
||||
}
|
||||
},
|
||||
"c8": {
|
||||
"reporter": [
|
||||
"text",
|
||||
"lcov"
|
||||
],
|
||||
"exclude": [
|
||||
"source/vendor"
|
||||
]
|
||||
}
|
||||
}
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
<h1 align="center">
|
||||
<br>
|
||||
<br>
|
||||
<img width="320" src="media/logo.svg" alt="Chalk">
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
> Terminal string styling done right
|
||||
|
||||
[](https://codecov.io/gh/chalk/chalk)
|
||||
[](https://www.npmjs.com/package/chalk?activeTab=dependents)
|
||||
[](https://www.npmjs.com/package/chalk)
|
||||
|
||||

|
||||
|
||||
## Info
|
||||
|
||||
- [Why not switch to a smaller coloring package?](https://github.com/chalk/chalk?tab=readme-ov-file#why-not-switch-to-a-smaller-coloring-package)
|
||||
- See [yoctocolors](https://github.com/sindresorhus/yoctocolors) for a smaller alternative
|
||||
|
||||
## Highlights
|
||||
|
||||
- Expressive API
|
||||
- Highly performant
|
||||
- No dependencies
|
||||
- Ability to nest styles
|
||||
- [256/Truecolor color support](#256-and-truecolor-color-support)
|
||||
- Auto-detects color support
|
||||
- Doesn't extend `String.prototype`
|
||||
- Clean and focused
|
||||
- Actively maintained
|
||||
- [Used by ~115,000 packages](https://www.npmjs.com/browse/depended/chalk) as of July 4, 2024
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
npm install chalk
|
||||
```
|
||||
|
||||
**IMPORTANT:** Chalk 5 is ESM. If you want to use Chalk with TypeScript or a build tool, you will probably want to use Chalk 4 for now. [Read more.](https://github.com/chalk/chalk/releases/tag/v5.0.0)
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import chalk from 'chalk';
|
||||
|
||||
console.log(chalk.blue('Hello world!'));
|
||||
```
|
||||
|
||||
Chalk comes with an easy to use composable API where you just chain and nest the styles you want.
|
||||
|
||||
```js
|
||||
import chalk from 'chalk';
|
||||
|
||||
const log = console.log;
|
||||
|
||||
// Combine styled and normal strings
|
||||
log(chalk.blue('Hello') + ' World' + chalk.red('!'));
|
||||
|
||||
// Compose multiple styles using the chainable API
|
||||
log(chalk.blue.bgRed.bold('Hello world!'));
|
||||
|
||||
// Pass in multiple arguments
|
||||
log(chalk.blue('Hello', 'World!', 'Foo', 'bar', 'biz', 'baz'));
|
||||
|
||||
// Nest styles
|
||||
log(chalk.red('Hello', chalk.underline.bgBlue('world') + '!'));
|
||||
|
||||
// Nest styles of the same type even (color, underline, background)
|
||||
log(chalk.green(
|
||||
'I am a green line ' +
|
||||
chalk.blue.underline.bold('with a blue substring') +
|
||||
' that becomes green again!'
|
||||
));
|
||||
|
||||
// ES2015 template literal
|
||||
log(`
|
||||
CPU: ${chalk.red('90%')}
|
||||
RAM: ${chalk.green('40%')}
|
||||
DISK: ${chalk.yellow('70%')}
|
||||
`);
|
||||
|
||||
// Use RGB colors in terminal emulators that support it.
|
||||
log(chalk.rgb(123, 45, 67).underline('Underlined reddish color'));
|
||||
log(chalk.hex('#DEADED').bold('Bold gray!'));
|
||||
```
|
||||
|
||||
Easily define your own themes:
|
||||
|
||||
```js
|
||||
import chalk from 'chalk';
|
||||
|
||||
const error = chalk.bold.red;
|
||||
const warning = chalk.hex('#FFA500'); // Orange color
|
||||
|
||||
console.log(error('Error!'));
|
||||
console.log(warning('Warning!'));
|
||||
```
|
||||
|
||||
Take advantage of console.log [string substitution](https://nodejs.org/docs/latest/api/console.html#console_console_log_data_args):
|
||||
|
||||
```js
|
||||
import chalk from 'chalk';
|
||||
|
||||
const name = 'Sindre';
|
||||
console.log(chalk.green('Hello %s'), name);
|
||||
//=> 'Hello Sindre'
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### chalk.`<style>[.<style>...](string, [string...])`
|
||||
|
||||
Example: `chalk.red.bold.underline('Hello', 'world');`
|
||||
|
||||
Chain [styles](#styles) and call the last one as a method with a string argument. Order doesn't matter, and later styles take precedent in case of a conflict. This simply means that `chalk.red.yellow.green` is equivalent to `chalk.green`.
|
||||
|
||||
Multiple arguments will be separated by space.
|
||||
|
||||
### chalk.level
|
||||
|
||||
Specifies the level of color support.
|
||||
|
||||
Color support is automatically detected, but you can override it by setting the `level` property. You should however only do this in your own code as it applies globally to all Chalk consumers.
|
||||
|
||||
If you need to change this in a reusable module, create a new instance:
|
||||
|
||||
```js
|
||||
import {Chalk} from 'chalk';
|
||||
|
||||
const customChalk = new Chalk({level: 0});
|
||||
```
|
||||
|
||||
| Level | Description |
|
||||
| :---: | :--- |
|
||||
| `0` | All colors disabled |
|
||||
| `1` | Basic color support (16 colors) |
|
||||
| `2` | 256 color support |
|
||||
| `3` | Truecolor support (16 million colors) |
|
||||
|
||||
### supportsColor
|
||||
|
||||
Detect whether the terminal [supports color](https://github.com/chalk/supports-color). Used internally and handled for you, but exposed for convenience.
|
||||
|
||||
Can be overridden by the user with the flags `--color` and `--no-color`. For situations where using `--color` is not possible, use the environment variable `FORCE_COLOR=1` (level 1), `FORCE_COLOR=2` (level 2), or `FORCE_COLOR=3` (level 3) to forcefully enable color, or `FORCE_COLOR=0` to forcefully disable. The use of `FORCE_COLOR` overrides all other color support checks.
|
||||
|
||||
Explicit 256/Truecolor mode can be enabled using the `--color=256` and `--color=16m` flags, respectively.
|
||||
|
||||
### chalkStderr and supportsColorStderr
|
||||
|
||||
`chalkStderr` contains a separate instance configured with color support detected for `stderr` stream instead of `stdout`. Override rules from `supportsColor` apply to this too. `supportsColorStderr` is exposed for convenience.
|
||||
|
||||
### modifierNames, foregroundColorNames, backgroundColorNames, and colorNames
|
||||
|
||||
All supported style strings are exposed as an array of strings for convenience. `colorNames` is the combination of `foregroundColorNames` and `backgroundColorNames`.
|
||||
|
||||
This can be useful if you wrap Chalk and need to validate input:
|
||||
|
||||
```js
|
||||
import {modifierNames, foregroundColorNames} from 'chalk';
|
||||
|
||||
console.log(modifierNames.includes('bold'));
|
||||
//=> true
|
||||
|
||||
console.log(foregroundColorNames.includes('pink'));
|
||||
//=> false
|
||||
```
|
||||
|
||||
## Styles
|
||||
|
||||
### Modifiers
|
||||
|
||||
- `reset` - Reset the current style.
|
||||
- `bold` - Make the text bold.
|
||||
- `dim` - Make the text have lower opacity.
|
||||
- `italic` - Make the text italic. *(Not widely supported)*
|
||||
- `underline` - Put a horizontal line below the text. *(Not widely supported)*
|
||||
- `overline` - Put a horizontal line above the text. *(Not widely supported)*
|
||||
- `inverse`- Invert background and foreground colors.
|
||||
- `hidden` - Print the text but make it invisible.
|
||||
- `strikethrough` - Puts a horizontal line through the center of the text. *(Not widely supported)*
|
||||
- `visible`- Print the text only when Chalk has a color level above zero. Can be useful for things that are purely cosmetic.
|
||||
|
||||
### Colors
|
||||
|
||||
- `black`
|
||||
- `red`
|
||||
- `green`
|
||||
- `yellow`
|
||||
- `blue`
|
||||
- `magenta`
|
||||
- `cyan`
|
||||
- `white`
|
||||
- `blackBright` (alias: `gray`, `grey`)
|
||||
- `redBright`
|
||||
- `greenBright`
|
||||
- `yellowBright`
|
||||
- `blueBright`
|
||||
- `magentaBright`
|
||||
- `cyanBright`
|
||||
- `whiteBright`
|
||||
|
||||
### Background colors
|
||||
|
||||
- `bgBlack`
|
||||
- `bgRed`
|
||||
- `bgGreen`
|
||||
- `bgYellow`
|
||||
- `bgBlue`
|
||||
- `bgMagenta`
|
||||
- `bgCyan`
|
||||
- `bgWhite`
|
||||
- `bgBlackBright` (alias: `bgGray`, `bgGrey`)
|
||||
- `bgRedBright`
|
||||
- `bgGreenBright`
|
||||
- `bgYellowBright`
|
||||
- `bgBlueBright`
|
||||
- `bgMagentaBright`
|
||||
- `bgCyanBright`
|
||||
- `bgWhiteBright`
|
||||
|
||||
## 256 and Truecolor color support
|
||||
|
||||
Chalk supports 256 colors and [Truecolor](https://github.com/termstandard/colors) (16 million colors) on supported terminal apps.
|
||||
|
||||
Colors are downsampled from 16 million RGB values to an ANSI color format that is supported by the terminal emulator (or by specifying `{level: n}` as a Chalk option). For example, Chalk configured to run at level 1 (basic color support) will downsample an RGB value of #FF0000 (red) to 31 (ANSI escape for red).
|
||||
|
||||
Examples:
|
||||
|
||||
- `chalk.hex('#DEADED').underline('Hello, world!')`
|
||||
- `chalk.rgb(15, 100, 204).inverse('Hello!')`
|
||||
|
||||
Background versions of these models are prefixed with `bg` and the first level of the module capitalized (e.g. `hex` for foreground colors and `bgHex` for background colors).
|
||||
|
||||
- `chalk.bgHex('#DEADED').underline('Hello, world!')`
|
||||
- `chalk.bgRgb(15, 100, 204).inverse('Hello!')`
|
||||
|
||||
The following color models can be used:
|
||||
|
||||
- [`rgb`](https://en.wikipedia.org/wiki/RGB_color_model) - Example: `chalk.rgb(255, 136, 0).bold('Orange!')`
|
||||
- [`hex`](https://en.wikipedia.org/wiki/Web_colors#Hex_triplet) - Example: `chalk.hex('#FF8800').bold('Orange!')`
|
||||
- [`ansi256`](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) - Example: `chalk.bgAnsi256(194)('Honeydew, more or less')`
|
||||
|
||||
## Browser support
|
||||
|
||||
Since Chrome 69, ANSI escape codes are natively supported in the developer console.
|
||||
|
||||
## Windows
|
||||
|
||||
If you're on Windows, do yourself a favor and use [Windows Terminal](https://github.com/microsoft/terminal) instead of `cmd.exe`.
|
||||
|
||||
## FAQ
|
||||
|
||||
### Why not switch to a smaller coloring package?
|
||||
|
||||
Chalk may be larger, but there is a reason for that. It offers a more user-friendly API, well-documented types, supports millions of colors, and covers edge cases that smaller alternatives miss. Chalk is mature, reliable, and built to last.
|
||||
|
||||
But beyond the technical aspects, there's something more critical: trust and long-term maintenance. I have been active in open source for over a decade, and I'm committed to keeping Chalk maintained. Smaller packages might seem appealing now, but there's no guarantee they will be around for the long term, or that they won't become malicious over time.
|
||||
|
||||
Chalk is also likely already in your dependency tree (since 100K+ packages depend on it), so switching won’t save space—in fact, it might increase it. npm deduplicates dependencies, so multiple Chalk instances turn into one, but adding another package alongside it will increase your overall size.
|
||||
|
||||
If the goal is to clean up the ecosystem, switching away from Chalk won’t even make a dent. The real problem lies with packages that have very deep dependency trees (for example, those including a lot of polyfills). Chalk has no dependencies. It's better to focus on impactful changes rather than minor optimizations.
|
||||
|
||||
If absolute package size is important to you, I also maintain [yoctocolors](https://github.com/sindresorhus/yoctocolors), one of the smallest color packages out there.
|
||||
|
||||
*\- [Sindre](https://github.com/sindresorhus)*
|
||||
|
||||
### But the smaller coloring package has benchmarks showing it is faster
|
||||
|
||||
[Micro-benchmarks are flawed](https://sindresorhus.com/blog/micro-benchmark-fallacy) because they measure performance in unrealistic, isolated scenarios, often giving a distorted view of real-world performance. Don't believe marketing fluff. All the coloring packages are more than fast enough.
|
||||
|
||||
## Related
|
||||
|
||||
- [chalk-template](https://github.com/chalk/chalk-template) - [Tagged template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates) support for this module
|
||||
- [chalk-cli](https://github.com/chalk/chalk-cli) - CLI for this module
|
||||
- [ansi-styles](https://github.com/chalk/ansi-styles) - ANSI escape codes for styling strings in the terminal
|
||||
- [supports-color](https://github.com/chalk/supports-color) - Detect whether a terminal supports color
|
||||
- [strip-ansi](https://github.com/chalk/strip-ansi) - Strip ANSI escape codes
|
||||
- [strip-ansi-stream](https://github.com/chalk/strip-ansi-stream) - Strip ANSI escape codes from a stream
|
||||
- [has-ansi](https://github.com/chalk/has-ansi) - Check if a string has ANSI escape codes
|
||||
- [ansi-regex](https://github.com/chalk/ansi-regex) - Regular expression for matching ANSI escape codes
|
||||
- [wrap-ansi](https://github.com/chalk/wrap-ansi) - Wordwrap a string with ANSI escape codes
|
||||
- [slice-ansi](https://github.com/chalk/slice-ansi) - Slice a string with ANSI escape codes
|
||||
- [color-convert](https://github.com/qix-/color-convert) - Converts colors between different models
|
||||
- [chalk-animation](https://github.com/bokub/chalk-animation) - Animate strings in the terminal
|
||||
- [gradient-string](https://github.com/bokub/gradient-string) - Apply color gradients to strings
|
||||
- [chalk-pipe](https://github.com/LitoMore/chalk-pipe) - Create chalk style schemes with simpler style strings
|
||||
- [terminal-link](https://github.com/sindresorhus/terminal-link) - Create clickable links in the terminal
|
||||
|
||||
*(Not accepting additional entries)*
|
||||
|
||||
## Maintainers
|
||||
|
||||
- [Sindre Sorhus](https://github.com/sindresorhus)
|
||||
- [Josh Junon](https://github.com/qix-)
|
||||
+325
@@ -0,0 +1,325 @@
|
||||
// TODO: Make it this when TS suports that.
|
||||
// import {ModifierName, ForegroundColor, BackgroundColor, ColorName} from '#ansi-styles';
|
||||
// import {ColorInfo, ColorSupportLevel} from '#supports-color';
|
||||
import {
|
||||
ModifierName,
|
||||
ForegroundColorName,
|
||||
BackgroundColorName,
|
||||
ColorName,
|
||||
} from './vendor/ansi-styles/index.js';
|
||||
import {ColorInfo, ColorSupportLevel} from './vendor/supports-color/index.js';
|
||||
|
||||
export interface Options {
|
||||
/**
|
||||
Specify the color support for Chalk.
|
||||
|
||||
By default, color support is automatically detected based on the environment.
|
||||
|
||||
Levels:
|
||||
- `0` - All colors disabled.
|
||||
- `1` - Basic 16 colors support.
|
||||
- `2` - ANSI 256 colors support.
|
||||
- `3` - Truecolor 16 million colors support.
|
||||
*/
|
||||
readonly level?: ColorSupportLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
Return a new Chalk instance.
|
||||
*/
|
||||
export const Chalk: new (options?: Options) => ChalkInstance; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
|
||||
export interface ChalkInstance {
|
||||
(...text: unknown[]): string;
|
||||
|
||||
/**
|
||||
The color support for Chalk.
|
||||
|
||||
By default, color support is automatically detected based on the environment.
|
||||
|
||||
Levels:
|
||||
- `0` - All colors disabled.
|
||||
- `1` - Basic 16 colors support.
|
||||
- `2` - ANSI 256 colors support.
|
||||
- `3` - Truecolor 16 million colors support.
|
||||
*/
|
||||
level: ColorSupportLevel;
|
||||
|
||||
/**
|
||||
Use RGB values to set text color.
|
||||
|
||||
@example
|
||||
```
|
||||
import chalk from 'chalk';
|
||||
|
||||
chalk.rgb(222, 173, 237);
|
||||
```
|
||||
*/
|
||||
rgb: (red: number, green: number, blue: number) => this;
|
||||
|
||||
/**
|
||||
Use HEX value to set text color.
|
||||
|
||||
@param color - Hexadecimal value representing the desired color.
|
||||
|
||||
@example
|
||||
```
|
||||
import chalk from 'chalk';
|
||||
|
||||
chalk.hex('#DEADED');
|
||||
```
|
||||
*/
|
||||
hex: (color: string) => this;
|
||||
|
||||
/**
|
||||
Use an [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set text color.
|
||||
|
||||
@example
|
||||
```
|
||||
import chalk from 'chalk';
|
||||
|
||||
chalk.ansi256(201);
|
||||
```
|
||||
*/
|
||||
ansi256: (index: number) => this;
|
||||
|
||||
/**
|
||||
Use RGB values to set background color.
|
||||
|
||||
@example
|
||||
```
|
||||
import chalk from 'chalk';
|
||||
|
||||
chalk.bgRgb(222, 173, 237);
|
||||
```
|
||||
*/
|
||||
bgRgb: (red: number, green: number, blue: number) => this;
|
||||
|
||||
/**
|
||||
Use HEX value to set background color.
|
||||
|
||||
@param color - Hexadecimal value representing the desired color.
|
||||
|
||||
@example
|
||||
```
|
||||
import chalk from 'chalk';
|
||||
|
||||
chalk.bgHex('#DEADED');
|
||||
```
|
||||
*/
|
||||
bgHex: (color: string) => this;
|
||||
|
||||
/**
|
||||
Use a [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set background color.
|
||||
|
||||
@example
|
||||
```
|
||||
import chalk from 'chalk';
|
||||
|
||||
chalk.bgAnsi256(201);
|
||||
```
|
||||
*/
|
||||
bgAnsi256: (index: number) => this;
|
||||
|
||||
/**
|
||||
Modifier: Reset the current style.
|
||||
*/
|
||||
readonly reset: this;
|
||||
|
||||
/**
|
||||
Modifier: Make the text bold.
|
||||
*/
|
||||
readonly bold: this;
|
||||
|
||||
/**
|
||||
Modifier: Make the text have lower opacity.
|
||||
*/
|
||||
readonly dim: this;
|
||||
|
||||
/**
|
||||
Modifier: Make the text italic. *(Not widely supported)*
|
||||
*/
|
||||
readonly italic: this;
|
||||
|
||||
/**
|
||||
Modifier: Put a horizontal line below the text. *(Not widely supported)*
|
||||
*/
|
||||
readonly underline: this;
|
||||
|
||||
/**
|
||||
Modifier: Put a horizontal line above the text. *(Not widely supported)*
|
||||
*/
|
||||
readonly overline: this;
|
||||
|
||||
/**
|
||||
Modifier: Invert background and foreground colors.
|
||||
*/
|
||||
readonly inverse: this;
|
||||
|
||||
/**
|
||||
Modifier: Print the text but make it invisible.
|
||||
*/
|
||||
readonly hidden: this;
|
||||
|
||||
/**
|
||||
Modifier: Puts a horizontal line through the center of the text. *(Not widely supported)*
|
||||
*/
|
||||
readonly strikethrough: this;
|
||||
|
||||
/**
|
||||
Modifier: Print the text only when Chalk has a color level above zero.
|
||||
|
||||
Can be useful for things that are purely cosmetic.
|
||||
*/
|
||||
readonly visible: this;
|
||||
|
||||
readonly black: this;
|
||||
readonly red: this;
|
||||
readonly green: this;
|
||||
readonly yellow: this;
|
||||
readonly blue: this;
|
||||
readonly magenta: this;
|
||||
readonly cyan: this;
|
||||
readonly white: this;
|
||||
|
||||
/*
|
||||
Alias for `blackBright`.
|
||||
*/
|
||||
readonly gray: this;
|
||||
|
||||
/*
|
||||
Alias for `blackBright`.
|
||||
*/
|
||||
readonly grey: this;
|
||||
|
||||
readonly blackBright: this;
|
||||
readonly redBright: this;
|
||||
readonly greenBright: this;
|
||||
readonly yellowBright: this;
|
||||
readonly blueBright: this;
|
||||
readonly magentaBright: this;
|
||||
readonly cyanBright: this;
|
||||
readonly whiteBright: this;
|
||||
|
||||
readonly bgBlack: this;
|
||||
readonly bgRed: this;
|
||||
readonly bgGreen: this;
|
||||
readonly bgYellow: this;
|
||||
readonly bgBlue: this;
|
||||
readonly bgMagenta: this;
|
||||
readonly bgCyan: this;
|
||||
readonly bgWhite: this;
|
||||
|
||||
/*
|
||||
Alias for `bgBlackBright`.
|
||||
*/
|
||||
readonly bgGray: this;
|
||||
|
||||
/*
|
||||
Alias for `bgBlackBright`.
|
||||
*/
|
||||
readonly bgGrey: this;
|
||||
|
||||
readonly bgBlackBright: this;
|
||||
readonly bgRedBright: this;
|
||||
readonly bgGreenBright: this;
|
||||
readonly bgYellowBright: this;
|
||||
readonly bgBlueBright: this;
|
||||
readonly bgMagentaBright: this;
|
||||
readonly bgCyanBright: this;
|
||||
readonly bgWhiteBright: this;
|
||||
}
|
||||
|
||||
/**
|
||||
Main Chalk object that allows to chain styles together.
|
||||
|
||||
Call the last one as a method with a string argument.
|
||||
|
||||
Order doesn't matter, and later styles take precedent in case of a conflict.
|
||||
|
||||
This simply means that `chalk.red.yellow.green` is equivalent to `chalk.green`.
|
||||
*/
|
||||
declare const chalk: ChalkInstance;
|
||||
|
||||
export const supportsColor: ColorInfo;
|
||||
|
||||
export const chalkStderr: typeof chalk;
|
||||
export const supportsColorStderr: typeof supportsColor;
|
||||
|
||||
export {
|
||||
ModifierName, ForegroundColorName, BackgroundColorName, ColorName,
|
||||
modifierNames, foregroundColorNames, backgroundColorNames, colorNames,
|
||||
// } from '#ansi-styles';
|
||||
} from './vendor/ansi-styles/index.js';
|
||||
|
||||
export {
|
||||
ColorInfo,
|
||||
ColorSupport,
|
||||
ColorSupportLevel,
|
||||
// } from '#supports-color';
|
||||
} from './vendor/supports-color/index.js';
|
||||
|
||||
// TODO: Remove these aliases in the next major version
|
||||
/**
|
||||
@deprecated Use `ModifierName` instead.
|
||||
|
||||
Basic modifier names.
|
||||
*/
|
||||
export type Modifiers = ModifierName;
|
||||
|
||||
/**
|
||||
@deprecated Use `ForegroundColorName` instead.
|
||||
|
||||
Basic foreground color names.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
export type ForegroundColor = ForegroundColorName;
|
||||
|
||||
/**
|
||||
@deprecated Use `BackgroundColorName` instead.
|
||||
|
||||
Basic background color names.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
export type BackgroundColor = BackgroundColorName;
|
||||
|
||||
/**
|
||||
@deprecated Use `ColorName` instead.
|
||||
|
||||
Basic color names. The combination of foreground and background color names.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
export type Color = ColorName;
|
||||
|
||||
/**
|
||||
@deprecated Use `modifierNames` instead.
|
||||
|
||||
Basic modifier names.
|
||||
*/
|
||||
export const modifiers: readonly Modifiers[];
|
||||
|
||||
/**
|
||||
@deprecated Use `foregroundColorNames` instead.
|
||||
|
||||
Basic foreground color names.
|
||||
*/
|
||||
export const foregroundColors: readonly ForegroundColor[];
|
||||
|
||||
/**
|
||||
@deprecated Use `backgroundColorNames` instead.
|
||||
|
||||
Basic background color names.
|
||||
*/
|
||||
export const backgroundColors: readonly BackgroundColor[];
|
||||
|
||||
/**
|
||||
@deprecated Use `colorNames` instead.
|
||||
|
||||
Basic color names. The combination of foreground and background color names.
|
||||
*/
|
||||
export const colors: readonly Color[];
|
||||
|
||||
export default chalk;
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
import ansiStyles from '#ansi-styles';
|
||||
import supportsColor from '#supports-color';
|
||||
import { // eslint-disable-line import/order
|
||||
stringReplaceAll,
|
||||
stringEncaseCRLFWithFirstIndex,
|
||||
} from './utilities.js';
|
||||
|
||||
const {stdout: stdoutColor, stderr: stderrColor} = supportsColor;
|
||||
|
||||
const GENERATOR = Symbol('GENERATOR');
|
||||
const STYLER = Symbol('STYLER');
|
||||
const IS_EMPTY = Symbol('IS_EMPTY');
|
||||
|
||||
// `supportsColor.level` → `ansiStyles.color[name]` mapping
|
||||
const levelMapping = [
|
||||
'ansi',
|
||||
'ansi',
|
||||
'ansi256',
|
||||
'ansi16m',
|
||||
];
|
||||
|
||||
const styles = Object.create(null);
|
||||
|
||||
const applyOptions = (object, options = {}) => {
|
||||
if (options.level && !(Number.isInteger(options.level) && options.level >= 0 && options.level <= 3)) {
|
||||
throw new Error('The `level` option should be an integer from 0 to 3');
|
||||
}
|
||||
|
||||
// Detect level if not set manually
|
||||
const colorLevel = stdoutColor ? stdoutColor.level : 0;
|
||||
object.level = options.level === undefined ? colorLevel : options.level;
|
||||
};
|
||||
|
||||
export class Chalk {
|
||||
constructor(options) {
|
||||
// eslint-disable-next-line no-constructor-return
|
||||
return chalkFactory(options);
|
||||
}
|
||||
}
|
||||
|
||||
const chalkFactory = options => {
|
||||
const chalk = (...strings) => strings.join(' ');
|
||||
applyOptions(chalk, options);
|
||||
|
||||
Object.setPrototypeOf(chalk, createChalk.prototype);
|
||||
|
||||
return chalk;
|
||||
};
|
||||
|
||||
function createChalk(options) {
|
||||
return chalkFactory(options);
|
||||
}
|
||||
|
||||
Object.setPrototypeOf(createChalk.prototype, Function.prototype);
|
||||
|
||||
for (const [styleName, style] of Object.entries(ansiStyles)) {
|
||||
styles[styleName] = {
|
||||
get() {
|
||||
const builder = createBuilder(this, createStyler(style.open, style.close, this[STYLER]), this[IS_EMPTY]);
|
||||
Object.defineProperty(this, styleName, {value: builder});
|
||||
return builder;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
styles.visible = {
|
||||
get() {
|
||||
const builder = createBuilder(this, this[STYLER], true);
|
||||
Object.defineProperty(this, 'visible', {value: builder});
|
||||
return builder;
|
||||
},
|
||||
};
|
||||
|
||||
const getModelAnsi = (model, level, type, ...arguments_) => {
|
||||
if (model === 'rgb') {
|
||||
if (level === 'ansi16m') {
|
||||
return ansiStyles[type].ansi16m(...arguments_);
|
||||
}
|
||||
|
||||
if (level === 'ansi256') {
|
||||
return ansiStyles[type].ansi256(ansiStyles.rgbToAnsi256(...arguments_));
|
||||
}
|
||||
|
||||
return ansiStyles[type].ansi(ansiStyles.rgbToAnsi(...arguments_));
|
||||
}
|
||||
|
||||
if (model === 'hex') {
|
||||
return getModelAnsi('rgb', level, type, ...ansiStyles.hexToRgb(...arguments_));
|
||||
}
|
||||
|
||||
return ansiStyles[type][model](...arguments_);
|
||||
};
|
||||
|
||||
const usedModels = ['rgb', 'hex', 'ansi256'];
|
||||
|
||||
for (const model of usedModels) {
|
||||
styles[model] = {
|
||||
get() {
|
||||
const {level} = this;
|
||||
return function (...arguments_) {
|
||||
const styler = createStyler(getModelAnsi(model, levelMapping[level], 'color', ...arguments_), ansiStyles.color.close, this[STYLER]);
|
||||
return createBuilder(this, styler, this[IS_EMPTY]);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
const bgModel = 'bg' + model[0].toUpperCase() + model.slice(1);
|
||||
styles[bgModel] = {
|
||||
get() {
|
||||
const {level} = this;
|
||||
return function (...arguments_) {
|
||||
const styler = createStyler(getModelAnsi(model, levelMapping[level], 'bgColor', ...arguments_), ansiStyles.bgColor.close, this[STYLER]);
|
||||
return createBuilder(this, styler, this[IS_EMPTY]);
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const proto = Object.defineProperties(() => {}, {
|
||||
...styles,
|
||||
level: {
|
||||
enumerable: true,
|
||||
get() {
|
||||
return this[GENERATOR].level;
|
||||
},
|
||||
set(level) {
|
||||
this[GENERATOR].level = level;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const createStyler = (open, close, parent) => {
|
||||
let openAll;
|
||||
let closeAll;
|
||||
if (parent === undefined) {
|
||||
openAll = open;
|
||||
closeAll = close;
|
||||
} else {
|
||||
openAll = parent.openAll + open;
|
||||
closeAll = close + parent.closeAll;
|
||||
}
|
||||
|
||||
return {
|
||||
open,
|
||||
close,
|
||||
openAll,
|
||||
closeAll,
|
||||
parent,
|
||||
};
|
||||
};
|
||||
|
||||
const createBuilder = (self, _styler, _isEmpty) => {
|
||||
// Single argument is hot path, implicit coercion is faster than anything
|
||||
// eslint-disable-next-line no-implicit-coercion
|
||||
const builder = (...arguments_) => applyStyle(builder, (arguments_.length === 1) ? ('' + arguments_[0]) : arguments_.join(' '));
|
||||
|
||||
// We alter the prototype because we must return a function, but there is
|
||||
// no way to create a function with a different prototype
|
||||
Object.setPrototypeOf(builder, proto);
|
||||
|
||||
builder[GENERATOR] = self;
|
||||
builder[STYLER] = _styler;
|
||||
builder[IS_EMPTY] = _isEmpty;
|
||||
|
||||
return builder;
|
||||
};
|
||||
|
||||
const applyStyle = (self, string) => {
|
||||
if (self.level <= 0 || !string) {
|
||||
return self[IS_EMPTY] ? '' : string;
|
||||
}
|
||||
|
||||
let styler = self[STYLER];
|
||||
|
||||
if (styler === undefined) {
|
||||
return string;
|
||||
}
|
||||
|
||||
const {openAll, closeAll} = styler;
|
||||
if (string.includes('\u001B')) {
|
||||
while (styler !== undefined) {
|
||||
// Replace any instances already present with a re-opening code
|
||||
// otherwise only the part of the string until said closing code
|
||||
// will be colored, and the rest will simply be 'plain'.
|
||||
string = stringReplaceAll(string, styler.close, styler.open);
|
||||
|
||||
styler = styler.parent;
|
||||
}
|
||||
}
|
||||
|
||||
// We can move both next actions out of loop, because remaining actions in loop won't have
|
||||
// any/visible effect on parts we add here. Close the styling before a linebreak and reopen
|
||||
// after next line to fix a bleed issue on macOS: https://github.com/chalk/chalk/pull/92
|
||||
const lfIndex = string.indexOf('\n');
|
||||
if (lfIndex !== -1) {
|
||||
string = stringEncaseCRLFWithFirstIndex(string, closeAll, openAll, lfIndex);
|
||||
}
|
||||
|
||||
return openAll + string + closeAll;
|
||||
};
|
||||
|
||||
Object.defineProperties(createChalk.prototype, styles);
|
||||
|
||||
const chalk = createChalk();
|
||||
export const chalkStderr = createChalk({level: stderrColor ? stderrColor.level : 0});
|
||||
|
||||
export {
|
||||
modifierNames,
|
||||
foregroundColorNames,
|
||||
backgroundColorNames,
|
||||
colorNames,
|
||||
|
||||
// TODO: Remove these aliases in the next major version
|
||||
modifierNames as modifiers,
|
||||
foregroundColorNames as foregroundColors,
|
||||
backgroundColorNames as backgroundColors,
|
||||
colorNames as colors,
|
||||
} from './vendor/ansi-styles/index.js';
|
||||
|
||||
export {
|
||||
stdoutColor as supportsColor,
|
||||
stderrColor as supportsColorStderr,
|
||||
};
|
||||
|
||||
export default chalk;
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
// TODO: When targeting Node.js 16, use `String.prototype.replaceAll`.
|
||||
export function stringReplaceAll(string, substring, replacer) {
|
||||
let index = string.indexOf(substring);
|
||||
if (index === -1) {
|
||||
return string;
|
||||
}
|
||||
|
||||
const substringLength = substring.length;
|
||||
let endIndex = 0;
|
||||
let returnValue = '';
|
||||
do {
|
||||
returnValue += string.slice(endIndex, index) + substring + replacer;
|
||||
endIndex = index + substringLength;
|
||||
index = string.indexOf(substring, endIndex);
|
||||
} while (index !== -1);
|
||||
|
||||
returnValue += string.slice(endIndex);
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
export function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
|
||||
let endIndex = 0;
|
||||
let returnValue = '';
|
||||
do {
|
||||
const gotCR = string[index - 1] === '\r';
|
||||
returnValue += string.slice(endIndex, (gotCR ? index - 1 : index)) + prefix + (gotCR ? '\r\n' : '\n') + postfix;
|
||||
endIndex = index + 1;
|
||||
index = string.indexOf('\n', endIndex);
|
||||
} while (index !== -1);
|
||||
|
||||
returnValue += string.slice(endIndex);
|
||||
return returnValue;
|
||||
}
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
export interface CSPair { // eslint-disable-line @typescript-eslint/naming-convention
|
||||
/**
|
||||
The ANSI terminal control sequence for starting this style.
|
||||
*/
|
||||
readonly open: string;
|
||||
|
||||
/**
|
||||
The ANSI terminal control sequence for ending this style.
|
||||
*/
|
||||
readonly close: string;
|
||||
}
|
||||
|
||||
export interface ColorBase {
|
||||
/**
|
||||
The ANSI terminal control sequence for ending this color.
|
||||
*/
|
||||
readonly close: string;
|
||||
|
||||
ansi(code: number): string;
|
||||
|
||||
ansi256(code: number): string;
|
||||
|
||||
ansi16m(red: number, green: number, blue: number): string;
|
||||
}
|
||||
|
||||
export interface Modifier {
|
||||
/**
|
||||
Resets the current color chain.
|
||||
*/
|
||||
readonly reset: CSPair;
|
||||
|
||||
/**
|
||||
Make text bold.
|
||||
*/
|
||||
readonly bold: CSPair;
|
||||
|
||||
/**
|
||||
Emitting only a small amount of light.
|
||||
*/
|
||||
readonly dim: CSPair;
|
||||
|
||||
/**
|
||||
Make text italic. (Not widely supported)
|
||||
*/
|
||||
readonly italic: CSPair;
|
||||
|
||||
/**
|
||||
Make text underline. (Not widely supported)
|
||||
*/
|
||||
readonly underline: CSPair;
|
||||
|
||||
/**
|
||||
Make text overline.
|
||||
|
||||
Supported on VTE-based terminals, the GNOME terminal, mintty, and Git Bash.
|
||||
*/
|
||||
readonly overline: CSPair;
|
||||
|
||||
/**
|
||||
Inverse background and foreground colors.
|
||||
*/
|
||||
readonly inverse: CSPair;
|
||||
|
||||
/**
|
||||
Prints the text, but makes it invisible.
|
||||
*/
|
||||
readonly hidden: CSPair;
|
||||
|
||||
/**
|
||||
Puts a horizontal line through the center of the text. (Not widely supported)
|
||||
*/
|
||||
readonly strikethrough: CSPair;
|
||||
}
|
||||
|
||||
export interface ForegroundColor {
|
||||
readonly black: CSPair;
|
||||
readonly red: CSPair;
|
||||
readonly green: CSPair;
|
||||
readonly yellow: CSPair;
|
||||
readonly blue: CSPair;
|
||||
readonly cyan: CSPair;
|
||||
readonly magenta: CSPair;
|
||||
readonly white: CSPair;
|
||||
|
||||
/**
|
||||
Alias for `blackBright`.
|
||||
*/
|
||||
readonly gray: CSPair;
|
||||
|
||||
/**
|
||||
Alias for `blackBright`.
|
||||
*/
|
||||
readonly grey: CSPair;
|
||||
|
||||
readonly blackBright: CSPair;
|
||||
readonly redBright: CSPair;
|
||||
readonly greenBright: CSPair;
|
||||
readonly yellowBright: CSPair;
|
||||
readonly blueBright: CSPair;
|
||||
readonly cyanBright: CSPair;
|
||||
readonly magentaBright: CSPair;
|
||||
readonly whiteBright: CSPair;
|
||||
}
|
||||
|
||||
export interface BackgroundColor {
|
||||
readonly bgBlack: CSPair;
|
||||
readonly bgRed: CSPair;
|
||||
readonly bgGreen: CSPair;
|
||||
readonly bgYellow: CSPair;
|
||||
readonly bgBlue: CSPair;
|
||||
readonly bgCyan: CSPair;
|
||||
readonly bgMagenta: CSPair;
|
||||
readonly bgWhite: CSPair;
|
||||
|
||||
/**
|
||||
Alias for `bgBlackBright`.
|
||||
*/
|
||||
readonly bgGray: CSPair;
|
||||
|
||||
/**
|
||||
Alias for `bgBlackBright`.
|
||||
*/
|
||||
readonly bgGrey: CSPair;
|
||||
|
||||
readonly bgBlackBright: CSPair;
|
||||
readonly bgRedBright: CSPair;
|
||||
readonly bgGreenBright: CSPair;
|
||||
readonly bgYellowBright: CSPair;
|
||||
readonly bgBlueBright: CSPair;
|
||||
readonly bgCyanBright: CSPair;
|
||||
readonly bgMagentaBright: CSPair;
|
||||
readonly bgWhiteBright: CSPair;
|
||||
}
|
||||
|
||||
export interface ConvertColor {
|
||||
/**
|
||||
Convert from the RGB color space to the ANSI 256 color space.
|
||||
|
||||
@param red - (`0...255`)
|
||||
@param green - (`0...255`)
|
||||
@param blue - (`0...255`)
|
||||
*/
|
||||
rgbToAnsi256(red: number, green: number, blue: number): number;
|
||||
|
||||
/**
|
||||
Convert from the RGB HEX color space to the RGB color space.
|
||||
|
||||
@param hex - A hexadecimal string containing RGB data.
|
||||
*/
|
||||
hexToRgb(hex: string): [red: number, green: number, blue: number];
|
||||
|
||||
/**
|
||||
Convert from the RGB HEX color space to the ANSI 256 color space.
|
||||
|
||||
@param hex - A hexadecimal string containing RGB data.
|
||||
*/
|
||||
hexToAnsi256(hex: string): number;
|
||||
|
||||
/**
|
||||
Convert from the ANSI 256 color space to the ANSI 16 color space.
|
||||
|
||||
@param code - A number representing the ANSI 256 color.
|
||||
*/
|
||||
ansi256ToAnsi(code: number): number;
|
||||
|
||||
/**
|
||||
Convert from the RGB color space to the ANSI 16 color space.
|
||||
|
||||
@param red - (`0...255`)
|
||||
@param green - (`0...255`)
|
||||
@param blue - (`0...255`)
|
||||
*/
|
||||
rgbToAnsi(red: number, green: number, blue: number): number;
|
||||
|
||||
/**
|
||||
Convert from the RGB HEX color space to the ANSI 16 color space.
|
||||
|
||||
@param hex - A hexadecimal string containing RGB data.
|
||||
*/
|
||||
hexToAnsi(hex: string): number;
|
||||
}
|
||||
|
||||
/**
|
||||
Basic modifier names.
|
||||
*/
|
||||
export type ModifierName = keyof Modifier;
|
||||
|
||||
/**
|
||||
Basic foreground color names.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
export type ForegroundColorName = keyof ForegroundColor;
|
||||
|
||||
/**
|
||||
Basic background color names.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
export type BackgroundColorName = keyof BackgroundColor;
|
||||
|
||||
/**
|
||||
Basic color names. The combination of foreground and background color names.
|
||||
|
||||
[More colors here.](https://github.com/chalk/chalk/blob/main/readme.md#256-and-truecolor-color-support)
|
||||
*/
|
||||
export type ColorName = ForegroundColorName | BackgroundColorName;
|
||||
|
||||
/**
|
||||
Basic modifier names.
|
||||
*/
|
||||
export const modifierNames: readonly ModifierName[];
|
||||
|
||||
/**
|
||||
Basic foreground color names.
|
||||
*/
|
||||
export const foregroundColorNames: readonly ForegroundColorName[];
|
||||
|
||||
/**
|
||||
Basic background color names.
|
||||
*/
|
||||
export const backgroundColorNames: readonly BackgroundColorName[];
|
||||
|
||||
/*
|
||||
Basic color names. The combination of foreground and background color names.
|
||||
*/
|
||||
export const colorNames: readonly ColorName[];
|
||||
|
||||
declare const ansiStyles: {
|
||||
readonly modifier: Modifier;
|
||||
readonly color: ColorBase & ForegroundColor;
|
||||
readonly bgColor: ColorBase & BackgroundColor;
|
||||
readonly codes: ReadonlyMap<number, number>;
|
||||
} & ForegroundColor & BackgroundColor & Modifier & ConvertColor;
|
||||
|
||||
export default ansiStyles;
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
const ANSI_BACKGROUND_OFFSET = 10;
|
||||
|
||||
const wrapAnsi16 = (offset = 0) => code => `\u001B[${code + offset}m`;
|
||||
|
||||
const wrapAnsi256 = (offset = 0) => code => `\u001B[${38 + offset};5;${code}m`;
|
||||
|
||||
const wrapAnsi16m = (offset = 0) => (red, green, blue) => `\u001B[${38 + offset};2;${red};${green};${blue}m`;
|
||||
|
||||
const styles = {
|
||||
modifier: {
|
||||
reset: [0, 0],
|
||||
// 21 isn't widely supported and 22 does the same thing
|
||||
bold: [1, 22],
|
||||
dim: [2, 22],
|
||||
italic: [3, 23],
|
||||
underline: [4, 24],
|
||||
overline: [53, 55],
|
||||
inverse: [7, 27],
|
||||
hidden: [8, 28],
|
||||
strikethrough: [9, 29],
|
||||
},
|
||||
color: {
|
||||
black: [30, 39],
|
||||
red: [31, 39],
|
||||
green: [32, 39],
|
||||
yellow: [33, 39],
|
||||
blue: [34, 39],
|
||||
magenta: [35, 39],
|
||||
cyan: [36, 39],
|
||||
white: [37, 39],
|
||||
|
||||
// Bright color
|
||||
blackBright: [90, 39],
|
||||
gray: [90, 39], // Alias of `blackBright`
|
||||
grey: [90, 39], // Alias of `blackBright`
|
||||
redBright: [91, 39],
|
||||
greenBright: [92, 39],
|
||||
yellowBright: [93, 39],
|
||||
blueBright: [94, 39],
|
||||
magentaBright: [95, 39],
|
||||
cyanBright: [96, 39],
|
||||
whiteBright: [97, 39],
|
||||
},
|
||||
bgColor: {
|
||||
bgBlack: [40, 49],
|
||||
bgRed: [41, 49],
|
||||
bgGreen: [42, 49],
|
||||
bgYellow: [43, 49],
|
||||
bgBlue: [44, 49],
|
||||
bgMagenta: [45, 49],
|
||||
bgCyan: [46, 49],
|
||||
bgWhite: [47, 49],
|
||||
|
||||
// Bright color
|
||||
bgBlackBright: [100, 49],
|
||||
bgGray: [100, 49], // Alias of `bgBlackBright`
|
||||
bgGrey: [100, 49], // Alias of `bgBlackBright`
|
||||
bgRedBright: [101, 49],
|
||||
bgGreenBright: [102, 49],
|
||||
bgYellowBright: [103, 49],
|
||||
bgBlueBright: [104, 49],
|
||||
bgMagentaBright: [105, 49],
|
||||
bgCyanBright: [106, 49],
|
||||
bgWhiteBright: [107, 49],
|
||||
},
|
||||
};
|
||||
|
||||
export const modifierNames = Object.keys(styles.modifier);
|
||||
export const foregroundColorNames = Object.keys(styles.color);
|
||||
export const backgroundColorNames = Object.keys(styles.bgColor);
|
||||
export const colorNames = [...foregroundColorNames, ...backgroundColorNames];
|
||||
|
||||
function assembleStyles() {
|
||||
const codes = new Map();
|
||||
|
||||
for (const [groupName, group] of Object.entries(styles)) {
|
||||
for (const [styleName, style] of Object.entries(group)) {
|
||||
styles[styleName] = {
|
||||
open: `\u001B[${style[0]}m`,
|
||||
close: `\u001B[${style[1]}m`,
|
||||
};
|
||||
|
||||
group[styleName] = styles[styleName];
|
||||
|
||||
codes.set(style[0], style[1]);
|
||||
}
|
||||
|
||||
Object.defineProperty(styles, groupName, {
|
||||
value: group,
|
||||
enumerable: false,
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperty(styles, 'codes', {
|
||||
value: codes,
|
||||
enumerable: false,
|
||||
});
|
||||
|
||||
styles.color.close = '\u001B[39m';
|
||||
styles.bgColor.close = '\u001B[49m';
|
||||
|
||||
styles.color.ansi = wrapAnsi16();
|
||||
styles.color.ansi256 = wrapAnsi256();
|
||||
styles.color.ansi16m = wrapAnsi16m();
|
||||
styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET);
|
||||
styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET);
|
||||
styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
|
||||
|
||||
// From https://github.com/Qix-/color-convert/blob/3f0e0d4e92e235796ccb17f6e85c72094a651f49/conversions.js
|
||||
Object.defineProperties(styles, {
|
||||
rgbToAnsi256: {
|
||||
value(red, green, blue) {
|
||||
// We use the extended greyscale palette here, with the exception of
|
||||
// black and white. normal palette only has 4 greyscale shades.
|
||||
if (red === green && green === blue) {
|
||||
if (red < 8) {
|
||||
return 16;
|
||||
}
|
||||
|
||||
if (red > 248) {
|
||||
return 231;
|
||||
}
|
||||
|
||||
return Math.round(((red - 8) / 247) * 24) + 232;
|
||||
}
|
||||
|
||||
return 16
|
||||
+ (36 * Math.round(red / 255 * 5))
|
||||
+ (6 * Math.round(green / 255 * 5))
|
||||
+ Math.round(blue / 255 * 5);
|
||||
},
|
||||
enumerable: false,
|
||||
},
|
||||
hexToRgb: {
|
||||
value(hex) {
|
||||
const matches = /[a-f\d]{6}|[a-f\d]{3}/i.exec(hex.toString(16));
|
||||
if (!matches) {
|
||||
return [0, 0, 0];
|
||||
}
|
||||
|
||||
let [colorString] = matches;
|
||||
|
||||
if (colorString.length === 3) {
|
||||
colorString = [...colorString].map(character => character + character).join('');
|
||||
}
|
||||
|
||||
const integer = Number.parseInt(colorString, 16);
|
||||
|
||||
return [
|
||||
/* eslint-disable no-bitwise */
|
||||
(integer >> 16) & 0xFF,
|
||||
(integer >> 8) & 0xFF,
|
||||
integer & 0xFF,
|
||||
/* eslint-enable no-bitwise */
|
||||
];
|
||||
},
|
||||
enumerable: false,
|
||||
},
|
||||
hexToAnsi256: {
|
||||
value: hex => styles.rgbToAnsi256(...styles.hexToRgb(hex)),
|
||||
enumerable: false,
|
||||
},
|
||||
ansi256ToAnsi: {
|
||||
value(code) {
|
||||
if (code < 8) {
|
||||
return 30 + code;
|
||||
}
|
||||
|
||||
if (code < 16) {
|
||||
return 90 + (code - 8);
|
||||
}
|
||||
|
||||
let red;
|
||||
let green;
|
||||
let blue;
|
||||
|
||||
if (code >= 232) {
|
||||
red = (((code - 232) * 10) + 8) / 255;
|
||||
green = red;
|
||||
blue = red;
|
||||
} else {
|
||||
code -= 16;
|
||||
|
||||
const remainder = code % 36;
|
||||
|
||||
red = Math.floor(code / 36) / 5;
|
||||
green = Math.floor(remainder / 6) / 5;
|
||||
blue = (remainder % 6) / 5;
|
||||
}
|
||||
|
||||
const value = Math.max(red, green, blue) * 2;
|
||||
|
||||
if (value === 0) {
|
||||
return 30;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-bitwise
|
||||
let result = 30 + ((Math.round(blue) << 2) | (Math.round(green) << 1) | Math.round(red));
|
||||
|
||||
if (value === 2) {
|
||||
result += 60;
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
enumerable: false,
|
||||
},
|
||||
rgbToAnsi: {
|
||||
value: (red, green, blue) => styles.ansi256ToAnsi(styles.rgbToAnsi256(red, green, blue)),
|
||||
enumerable: false,
|
||||
},
|
||||
hexToAnsi: {
|
||||
value: hex => styles.ansi256ToAnsi(styles.hexToAnsi256(hex)),
|
||||
enumerable: false,
|
||||
},
|
||||
});
|
||||
|
||||
return styles;
|
||||
}
|
||||
|
||||
const ansiStyles = assembleStyles();
|
||||
|
||||
export default ansiStyles;
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {default} from './index.js';
|
||||
Generated
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
/* eslint-env browser */
|
||||
|
||||
const level = (() => {
|
||||
if (!('navigator' in globalThis)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (globalThis.navigator.userAgentData) {
|
||||
const brand = navigator.userAgentData.brands.find(({brand}) => brand === 'Chromium');
|
||||
if (brand && brand.version > 93) {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
if (/\b(Chrome|Chromium)\//.test(globalThis.navigator.userAgent)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
})();
|
||||
|
||||
const colorSupport = level !== 0 && {
|
||||
level,
|
||||
hasBasic: true,
|
||||
has256: level >= 2,
|
||||
has16m: level >= 3,
|
||||
};
|
||||
|
||||
const supportsColor = {
|
||||
stdout: colorSupport,
|
||||
stderr: colorSupport,
|
||||
};
|
||||
|
||||
export default supportsColor;
|
||||
Generated
Vendored
+55
@@ -0,0 +1,55 @@
|
||||
import type {WriteStream} from 'node:tty';
|
||||
|
||||
export type Options = {
|
||||
/**
|
||||
Whether `process.argv` should be sniffed for `--color` and `--no-color` flags.
|
||||
|
||||
@default true
|
||||
*/
|
||||
readonly sniffFlags?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
Levels:
|
||||
- `0` - All colors disabled.
|
||||
- `1` - Basic 16 colors support.
|
||||
- `2` - ANSI 256 colors support.
|
||||
- `3` - Truecolor 16 million colors support.
|
||||
*/
|
||||
export type ColorSupportLevel = 0 | 1 | 2 | 3;
|
||||
|
||||
/**
|
||||
Detect whether the terminal supports color.
|
||||
*/
|
||||
export type ColorSupport = {
|
||||
/**
|
||||
The color level.
|
||||
*/
|
||||
level: ColorSupportLevel;
|
||||
|
||||
/**
|
||||
Whether basic 16 colors are supported.
|
||||
*/
|
||||
hasBasic: boolean;
|
||||
|
||||
/**
|
||||
Whether ANSI 256 colors are supported.
|
||||
*/
|
||||
has256: boolean;
|
||||
|
||||
/**
|
||||
Whether Truecolor 16 million colors are supported.
|
||||
*/
|
||||
has16m: boolean;
|
||||
};
|
||||
|
||||
export type ColorInfo = ColorSupport | false;
|
||||
|
||||
export function createSupportsColor(stream?: WriteStream, options?: Options): ColorInfo;
|
||||
|
||||
declare const supportsColor: {
|
||||
stdout: ColorInfo;
|
||||
stderr: ColorInfo;
|
||||
};
|
||||
|
||||
export default supportsColor;
|
||||
Generated
Vendored
+190
@@ -0,0 +1,190 @@
|
||||
import process from 'node:process';
|
||||
import os from 'node:os';
|
||||
import tty from 'node:tty';
|
||||
|
||||
// From: https://github.com/sindresorhus/has-flag/blob/main/index.js
|
||||
/// function hasFlag(flag, argv = globalThis.Deno?.args ?? process.argv) {
|
||||
function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : process.argv) {
|
||||
const prefix = flag.startsWith('-') ? '' : (flag.length === 1 ? '-' : '--');
|
||||
const position = argv.indexOf(prefix + flag);
|
||||
const terminatorPosition = argv.indexOf('--');
|
||||
return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
|
||||
}
|
||||
|
||||
const {env} = process;
|
||||
|
||||
let flagForceColor;
|
||||
if (
|
||||
hasFlag('no-color')
|
||||
|| hasFlag('no-colors')
|
||||
|| hasFlag('color=false')
|
||||
|| hasFlag('color=never')
|
||||
) {
|
||||
flagForceColor = 0;
|
||||
} else if (
|
||||
hasFlag('color')
|
||||
|| hasFlag('colors')
|
||||
|| hasFlag('color=true')
|
||||
|| hasFlag('color=always')
|
||||
) {
|
||||
flagForceColor = 1;
|
||||
}
|
||||
|
||||
function envForceColor() {
|
||||
if ('FORCE_COLOR' in env) {
|
||||
if (env.FORCE_COLOR === 'true') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (env.FORCE_COLOR === 'false') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return env.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
|
||||
}
|
||||
}
|
||||
|
||||
function translateLevel(level) {
|
||||
if (level === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
level,
|
||||
hasBasic: true,
|
||||
has256: level >= 2,
|
||||
has16m: level >= 3,
|
||||
};
|
||||
}
|
||||
|
||||
function _supportsColor(haveStream, {streamIsTTY, sniffFlags = true} = {}) {
|
||||
const noFlagForceColor = envForceColor();
|
||||
if (noFlagForceColor !== undefined) {
|
||||
flagForceColor = noFlagForceColor;
|
||||
}
|
||||
|
||||
const forceColor = sniffFlags ? flagForceColor : noFlagForceColor;
|
||||
|
||||
if (forceColor === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (sniffFlags) {
|
||||
if (hasFlag('color=16m')
|
||||
|| hasFlag('color=full')
|
||||
|| hasFlag('color=truecolor')) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (hasFlag('color=256')) {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Azure DevOps pipelines.
|
||||
// Has to be above the `!streamIsTTY` check.
|
||||
if ('TF_BUILD' in env && 'AGENT_NAME' in env) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (haveStream && !streamIsTTY && forceColor === undefined) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const min = forceColor || 0;
|
||||
|
||||
if (env.TERM === 'dumb') {
|
||||
return min;
|
||||
}
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
// Windows 10 build 10586 is the first Windows release that supports 256 colors.
|
||||
// Windows 10 build 14931 is the first release that supports 16m/TrueColor.
|
||||
const osRelease = os.release().split('.');
|
||||
if (
|
||||
Number(osRelease[0]) >= 10
|
||||
&& Number(osRelease[2]) >= 10_586
|
||||
) {
|
||||
return Number(osRelease[2]) >= 14_931 ? 3 : 2;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ('CI' in env) {
|
||||
if (['GITHUB_ACTIONS', 'GITEA_ACTIONS', 'CIRCLECI'].some(key => key in env)) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (['TRAVIS', 'APPVEYOR', 'GITLAB_CI', 'BUILDKITE', 'DRONE'].some(sign => sign in env) || env.CI_NAME === 'codeship') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return min;
|
||||
}
|
||||
|
||||
if ('TEAMCITY_VERSION' in env) {
|
||||
return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
|
||||
}
|
||||
|
||||
if (env.COLORTERM === 'truecolor') {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (env.TERM === 'xterm-kitty') {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (env.TERM === 'xterm-ghostty') {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if (env.TERM === 'wezterm') {
|
||||
return 3;
|
||||
}
|
||||
|
||||
if ('TERM_PROGRAM' in env) {
|
||||
const version = Number.parseInt((env.TERM_PROGRAM_VERSION || '').split('.')[0], 10);
|
||||
|
||||
switch (env.TERM_PROGRAM) {
|
||||
case 'iTerm.app': {
|
||||
return version >= 3 ? 3 : 2;
|
||||
}
|
||||
|
||||
case 'Apple_Terminal': {
|
||||
return 2;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
}
|
||||
|
||||
if (/-256(color)?$/i.test(env.TERM)) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ('COLORTERM' in env) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return min;
|
||||
}
|
||||
|
||||
export function createSupportsColor(stream, options = {}) {
|
||||
const level = _supportsColor(stream, {
|
||||
streamIsTTY: stream && stream.isTTY,
|
||||
...options,
|
||||
});
|
||||
|
||||
return translateLevel(level);
|
||||
}
|
||||
|
||||
const supportsColor = {
|
||||
stdout: createSupportsColor({isTTY: tty.isatty(1)}),
|
||||
stderr: createSupportsColor({isTTY: tty.isatty(2)}),
|
||||
};
|
||||
|
||||
export default supportsColor;
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"name": "lint-staged",
|
||||
"version": "15.5.2",
|
||||
"description": "Lint files staged by git",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/lint-staged/lint-staged.git"
|
||||
},
|
||||
"homepage": "https://github.com/lint-staged/lint-staged#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/lint-staged/lint-staged/issues"
|
||||
},
|
||||
"author": "Andrey Okonetchnikov <andrey@okonet.ru>",
|
||||
"maintainers": [
|
||||
"Lufty Wiranda <lufty.wiranda@gmail.com>",
|
||||
"Suhas Karanth <sudo.suhas@gmail.com>",
|
||||
"Iiro Jäppinen <iiro@jappinen.fi> (https://iiro.fi)"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/lint-staged"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.12.0"
|
||||
},
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"lint-staged": "bin/lint-staged.js"
|
||||
},
|
||||
"exports": {
|
||||
".": "./lib/index.js",
|
||||
"./bin": "./bin/lint-staged.js",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"lib"
|
||||
],
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"test": "cross-env NODE_OPTIONS=--experimental-vm-modules npx jest --coverage",
|
||||
"test:watch": "npm run test -- --watch",
|
||||
"typecheck": "tsc --noEmit --strict test/types/index.ts",
|
||||
"version": "npx changeset version",
|
||||
"postversion": "npm i --package-lock-only && git commit -am \"chore(changeset): release\"",
|
||||
"tag": "npx changeset tag"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^5.4.1",
|
||||
"commander": "^13.1.0",
|
||||
"debug": "^4.4.0",
|
||||
"execa": "^8.0.1",
|
||||
"lilconfig": "^3.1.3",
|
||||
"listr2": "^8.2.5",
|
||||
"micromatch": "^4.0.8",
|
||||
"pidtree": "^0.6.0",
|
||||
"string-argv": "^0.3.2",
|
||||
"yaml": "^2.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@changesets/changelog-github": "0.5.1",
|
||||
"@changesets/cli": "2.28.1",
|
||||
"@commitlint/cli": "19.8.0",
|
||||
"@commitlint/config-conventional": "19.8.0",
|
||||
"@eslint/js": "9.22.0",
|
||||
"consolemock": "1.1.0",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "9.22.0",
|
||||
"eslint-config-prettier": "10.1.1",
|
||||
"eslint-plugin-jest": "28.11.0",
|
||||
"eslint-plugin-n": "17.16.2",
|
||||
"eslint-plugin-prettier": "5.2.3",
|
||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||
"husky": "9.1.7",
|
||||
"jest": "29.7.0",
|
||||
"jest-snapshot-serializer-ansi": "2.2.1",
|
||||
"mock-stdin": "1.0.0",
|
||||
"prettier": "3.5.3",
|
||||
"semver": "7.7.1",
|
||||
"typescript": "5.8.2"
|
||||
},
|
||||
"keywords": [
|
||||
"lint",
|
||||
"git",
|
||||
"staged",
|
||||
"eslint",
|
||||
"prettier",
|
||||
"stylelint",
|
||||
"code",
|
||||
"quality",
|
||||
"check",
|
||||
"format",
|
||||
"validate"
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user