@depup/lint-staged 16.3.2-depup.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/MIGRATION.md +72 -0
- package/README.md +1120 -0
- package/bin/lint-staged.js +175 -0
- package/lib/chunkFiles.js +65 -0
- package/lib/colors.js +106 -0
- package/lib/configFiles.js +30 -0
- package/lib/debug.js +27 -0
- package/lib/execGit.js +37 -0
- package/lib/figures.js +9 -0
- package/lib/file.js +53 -0
- package/lib/generateTasks.js +69 -0
- package/lib/getAbortController.js +18 -0
- package/lib/getDiffCommand.js +19 -0
- package/lib/getFunctionTask.js +37 -0
- package/lib/getRenderer.js +63 -0
- package/lib/getSpawnedTask.js +160 -0
- package/lib/getSpawnedTasks.js +76 -0
- package/lib/getStagedFiles.js +86 -0
- package/lib/gitWorkflow.js +434 -0
- package/lib/groupFilesByConfig.js +62 -0
- package/lib/index.d.ts +125 -0
- package/lib/index.js +184 -0
- package/lib/killSubprocesses.js +42 -0
- package/lib/loadConfig.js +102 -0
- package/lib/messages.js +91 -0
- package/lib/normalizePath.js +50 -0
- package/lib/parseGitZOutput.js +11 -0
- package/lib/printTaskOutput.js +12 -0
- package/lib/readStdin.js +19 -0
- package/lib/resolveConfig.js +15 -0
- package/lib/resolveGitRepo.js +65 -0
- package/lib/runAll.js +372 -0
- package/lib/searchConfigs.js +186 -0
- package/lib/state.js +102 -0
- package/lib/symbols.js +29 -0
- package/lib/validateBraces.js +95 -0
- package/lib/validateConfig.js +108 -0
- package/lib/validateOptions.js +33 -0
- package/lib/version.js +6 -0
- package/package.json +93 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { EOL } from 'node:os'
|
|
2
|
+
import { Writable } from 'node:stream'
|
|
3
|
+
|
|
4
|
+
import { ListrLogger, ProcessOutput } from 'listr2'
|
|
5
|
+
|
|
6
|
+
const EOLRegex = new RegExp(EOL + '$')
|
|
7
|
+
|
|
8
|
+
const bindLogger = (consoleLogMethod) =>
|
|
9
|
+
new Writable({
|
|
10
|
+
write: function (chunk, encoding, next) {
|
|
11
|
+
consoleLogMethod(chunk.toString().replace(EOLRegex, ''))
|
|
12
|
+
next()
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const getMainRendererOptions = ({ color, debug, quiet }, logger, env) => {
|
|
17
|
+
if (quiet) {
|
|
18
|
+
return {
|
|
19
|
+
renderer: 'silent',
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (env.NODE_ENV === 'test') {
|
|
24
|
+
return {
|
|
25
|
+
renderer: 'test',
|
|
26
|
+
rendererOptions: {
|
|
27
|
+
logger: new ListrLogger({
|
|
28
|
+
processOutput: new ProcessOutput(bindLogger(logger.log), bindLogger(logger.error)),
|
|
29
|
+
}),
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (debug || !color) {
|
|
35
|
+
return {
|
|
36
|
+
renderer: 'verbose',
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
renderer: 'update',
|
|
42
|
+
rendererOptions: {
|
|
43
|
+
formatOutput: 'truncate',
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const getFallbackRenderer = ({ renderer }, { color = false }) => {
|
|
49
|
+
if (renderer === 'silent' || renderer === 'test' || !color) {
|
|
50
|
+
return renderer
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return 'verbose'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const getRenderer = ({ color, debug, quiet }, logger, env = process.env) => {
|
|
57
|
+
const mainRendererOptions = getMainRendererOptions({ color, debug, quiet }, logger, env)
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
...mainRendererOptions,
|
|
61
|
+
fallbackRenderer: getFallbackRenderer(mainRendererOptions, { color }),
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { parseArgsStringToArgv } from 'string-argv'
|
|
2
|
+
import { exec } from 'tinyexec'
|
|
3
|
+
|
|
4
|
+
import { blackBright, red } from './colors.js'
|
|
5
|
+
import { createDebug } from './debug.js'
|
|
6
|
+
import { error, info } from './figures.js'
|
|
7
|
+
import { Signal } from './getAbortController.js'
|
|
8
|
+
import { killSubProcesses } from './killSubprocesses.js'
|
|
9
|
+
import { getInitialState } from './state.js'
|
|
10
|
+
import { TaskError } from './symbols.js'
|
|
11
|
+
|
|
12
|
+
const debugLog = createDebug('lint-staged:getSpawnedTask')
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Handle task console output.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} command
|
|
18
|
+
* @param {string} output
|
|
19
|
+
* @param {ReturnType<typeof getInitialState>} ctx context
|
|
20
|
+
* @param {keyof typeof Signal | undefined} signal
|
|
21
|
+
* @param {import('tinyexec').Result} [errorResult]
|
|
22
|
+
*/
|
|
23
|
+
const handleTaskOutput = (command, output, ctx, signal, errorResult) => {
|
|
24
|
+
if (output) {
|
|
25
|
+
const outputTitle = errorResult ? red(`${error} ${command}:`) : `${info} ${command}:`
|
|
26
|
+
ctx.output.push([...(ctx.quiet ? [] : ['', outputTitle]), output].join('\n'))
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (ctx.quiet) {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (signal === 'SIGINT') {
|
|
35
|
+
ctx.output.push(red(`\n${error} Task interrupted: ${command}`))
|
|
36
|
+
} else if (signal === 'SIGKILL') {
|
|
37
|
+
ctx.output.push(red(`\n${error} Task killed: ${command}`))
|
|
38
|
+
} else if (errorResult) {
|
|
39
|
+
ctx.output.push(red(`\n${error} Task failed to spawn: ${command}`), signal)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Create a error output depending on process result.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} command
|
|
47
|
+
* @param {import('tinyexec').Result} result
|
|
48
|
+
* @param {ReturnType<typeof getInitialState>} ctx context
|
|
49
|
+
* @param {keyof typeof Signal | undefined} signal
|
|
50
|
+
* @returns {Error}
|
|
51
|
+
*/
|
|
52
|
+
export const createTaskError = (command, result, ctx, signal = 'FAILED') => {
|
|
53
|
+
ctx.errors.add(TaskError)
|
|
54
|
+
return new Error(`${red(command)} ${blackBright(`[${signal}]`)}`, { cause: result })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Returns the task function for the linter.
|
|
59
|
+
*
|
|
60
|
+
* @param {Object} options
|
|
61
|
+
* @param {AbortController} options.abortController
|
|
62
|
+
* @param {boolean} [options.color]
|
|
63
|
+
* @param {string} options.command — Linter task
|
|
64
|
+
* @param {string} [options.continueOnError]
|
|
65
|
+
* @param {string} [options.cwd]
|
|
66
|
+
* @param {String} options.topLevelDir - Current git repo top-level path
|
|
67
|
+
* @param {Boolean} options.isFn - Whether the linter task is a function
|
|
68
|
+
* @param {string[]} options.files — Filepaths to run the linter task against
|
|
69
|
+
* @param {Boolean} [options.verbose] — Always show task verbose
|
|
70
|
+
* @returns {() => Promise<Array<string>>}
|
|
71
|
+
*/
|
|
72
|
+
export const getSpawnedTask = ({
|
|
73
|
+
abortController,
|
|
74
|
+
color,
|
|
75
|
+
command,
|
|
76
|
+
continueOnError = false,
|
|
77
|
+
cwd = process.cwd(),
|
|
78
|
+
files,
|
|
79
|
+
topLevelDir,
|
|
80
|
+
isFn,
|
|
81
|
+
verbose = false,
|
|
82
|
+
}) => {
|
|
83
|
+
const [cmd, ...args] = parseArgsStringToArgv(command)
|
|
84
|
+
debugLog('cmd:', cmd)
|
|
85
|
+
debugLog('args:', args)
|
|
86
|
+
|
|
87
|
+
/** @type {import('tinyexec').Options}*/
|
|
88
|
+
const tinyExecOptions = {
|
|
89
|
+
nodeOptions: {
|
|
90
|
+
// Only use topLevelDir as CWD if we are using the git binary
|
|
91
|
+
// e.g `npm` should run tasks in the actual CWD
|
|
92
|
+
cwd: /^git(\.exe)?/i.test(cmd) ? topLevelDir : cwd,
|
|
93
|
+
env: color ? { FORCE_COLOR: 'true' } : { NO_COLOR: 'true' },
|
|
94
|
+
stdio: ['ignore'],
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
debugLog('Tinyexec options:', tinyExecOptions)
|
|
99
|
+
|
|
100
|
+
/** @param {ReturnType<typeof getInitialState>} ctx context */
|
|
101
|
+
return async (ctx = getInitialState()) => {
|
|
102
|
+
const result = exec(cmd, isFn ? args : args.concat(files), tinyExecOptions)
|
|
103
|
+
|
|
104
|
+
const taskFailed = () => result.exitCode > 0 || result.process?.signalCode
|
|
105
|
+
|
|
106
|
+
/** @type {keyof typeof Signal | undefined} */
|
|
107
|
+
let signal
|
|
108
|
+
|
|
109
|
+
abortController.signal.addEventListener(
|
|
110
|
+
'abort',
|
|
111
|
+
async () => {
|
|
112
|
+
if (taskFailed() || !result.process) return
|
|
113
|
+
|
|
114
|
+
signal = abortController.signal.reason
|
|
115
|
+
const pid = result.process.pid
|
|
116
|
+
result.process.kill(abortController.signal.reason)
|
|
117
|
+
await killSubProcesses(pid)
|
|
118
|
+
},
|
|
119
|
+
{ once: true }
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
let output = ''
|
|
123
|
+
try {
|
|
124
|
+
for await (const line of result) {
|
|
125
|
+
output += line + '\n'
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
/** Probably failed to spawn (ENOENT) */
|
|
129
|
+
const errorSignal = (error instanceof Error && error.code) || 'FAILED'
|
|
130
|
+
|
|
131
|
+
if (continueOnError !== true) {
|
|
132
|
+
/** Other tasks should be killed */
|
|
133
|
+
abortController.abort(Signal.SIGKILL)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
handleTaskOutput(command, output, ctx, errorSignal, result)
|
|
137
|
+
throw createTaskError(command, result, ctx, errorSignal)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
output = output.trimEnd()
|
|
141
|
+
|
|
142
|
+
if (taskFailed()) {
|
|
143
|
+
if (continueOnError !== true) {
|
|
144
|
+
/** Other tasks should be killed */
|
|
145
|
+
abortController.abort(Signal.SIGKILL)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (result.process?.pid) {
|
|
149
|
+
await killSubProcesses(result.process.pid)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
handleTaskOutput(command, output, ctx, signal, result)
|
|
153
|
+
throw createTaskError(command, result, ctx, result.process?.signalCode ?? signal)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (verbose) {
|
|
157
|
+
handleTaskOutput(command, output, ctx, signal)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createDebug } from './debug.js'
|
|
2
|
+
import { getSpawnedTask } from './getSpawnedTask.js'
|
|
3
|
+
import { configurationError } from './messages.js'
|
|
4
|
+
|
|
5
|
+
const debugLog = createDebug('lint-staged:getSpawnedTasks')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates and returns an array of listr tasks which map to the given commands.
|
|
9
|
+
*
|
|
10
|
+
* @param {object} options
|
|
11
|
+
* @param {AbortController} options.abortController
|
|
12
|
+
* @param {boolean} [options.color]
|
|
13
|
+
* @param {Array<string|Function>|string|Function} options.commands
|
|
14
|
+
* @param {string} options.continueOnError
|
|
15
|
+
* @param {string} options.cwd
|
|
16
|
+
* @param {import('./getStagedFiles.js').StagedFile[]} options.files
|
|
17
|
+
* @param {string} options.topLevelDir
|
|
18
|
+
* @param {Boolean} verbose
|
|
19
|
+
*/
|
|
20
|
+
export const getSpawnedTasks = async ({
|
|
21
|
+
abortController,
|
|
22
|
+
color,
|
|
23
|
+
commands,
|
|
24
|
+
continueOnError,
|
|
25
|
+
cwd,
|
|
26
|
+
files,
|
|
27
|
+
topLevelDir,
|
|
28
|
+
verbose,
|
|
29
|
+
}) => {
|
|
30
|
+
debugLog('Creating Listr tasks for commands %o', commands)
|
|
31
|
+
const cmdTasks = []
|
|
32
|
+
|
|
33
|
+
const commandArray = Array.isArray(commands) ? commands : [commands]
|
|
34
|
+
|
|
35
|
+
const filepaths = files.map((f) => f.filepath)
|
|
36
|
+
|
|
37
|
+
for (const cmd of commandArray) {
|
|
38
|
+
// command function may return array of commands that already include `stagedFiles`
|
|
39
|
+
const isFn = typeof cmd === 'function'
|
|
40
|
+
|
|
41
|
+
/** Pass copy of file list to prevent mutation by function from config file. */
|
|
42
|
+
const resolved = isFn ? await cmd([...filepaths]) : cmd
|
|
43
|
+
|
|
44
|
+
const resolvedArray = Array.isArray(resolved) ? resolved : [resolved] // Wrap non-array command as array
|
|
45
|
+
|
|
46
|
+
for (const command of resolvedArray) {
|
|
47
|
+
// If the function linter didn't return string | string[] it won't work
|
|
48
|
+
// Do the validation here instead of `validateConfig` to skip evaluating the function multiple times
|
|
49
|
+
if (isFn && typeof command !== 'string') {
|
|
50
|
+
throw new Error(
|
|
51
|
+
configurationError(
|
|
52
|
+
'[Function]',
|
|
53
|
+
'Function task should return a string or an array of strings',
|
|
54
|
+
resolved
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const task = getSpawnedTask({
|
|
60
|
+
abortController,
|
|
61
|
+
color,
|
|
62
|
+
command,
|
|
63
|
+
continueOnError,
|
|
64
|
+
cwd,
|
|
65
|
+
files: filepaths,
|
|
66
|
+
topLevelDir,
|
|
67
|
+
isFn,
|
|
68
|
+
verbose,
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
cmdTasks.push({ title: command, command, task })
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return cmdTasks
|
|
76
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { execGit } from './execGit.js'
|
|
4
|
+
import { getDiffCommand } from './getDiffCommand.js'
|
|
5
|
+
import { normalizePath } from './normalizePath.js'
|
|
6
|
+
import { parseGitZOutput } from './parseGitZOutput.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {'A'|'C'|'D'|'M'|'R'|'T'|'U'|'X'} FileSatus
|
|
10
|
+
* @typedef { { filepath: string; status: FileSatus }} StagedFile
|
|
11
|
+
*
|
|
12
|
+
* @param {Object} args
|
|
13
|
+
* @param {string} [args.cwd]
|
|
14
|
+
* @param {string} [args.diff]
|
|
15
|
+
* @param {string} [args.diffFilter]
|
|
16
|
+
* @retuns {Promise<StagedFile[] | null>}
|
|
17
|
+
*/
|
|
18
|
+
export const getStagedFiles = async ({ cwd = process.cwd(), diff, diffFilter } = {}) => {
|
|
19
|
+
try {
|
|
20
|
+
/**
|
|
21
|
+
* With the raw output lines look like:
|
|
22
|
+
*
|
|
23
|
+
* :000000 100644 0000000 780ccd3\u0000A\u0000.gitmodules\u0000
|
|
24
|
+
* :000000 160000 0000000 1bb568e\u0000A\u0000submodule\u0000
|
|
25
|
+
*
|
|
26
|
+
* @see https://git-scm.com/docs/git-diff#_raw_output_format
|
|
27
|
+
*/
|
|
28
|
+
const output = await execGit([...getDiffCommand(diff, diffFilter), '--raw', '-z'], { cwd })
|
|
29
|
+
|
|
30
|
+
if (!output) return []
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Split from all colons and remove the first one, after which lines will look like:
|
|
34
|
+
*
|
|
35
|
+
* 000000 100644 0000000 780ccd3 A\u0000.gitmodules\u0000
|
|
36
|
+
* 000000 160000 0000000 47e5cff A\u0000submodule\u0000
|
|
37
|
+
*
|
|
38
|
+
* where '\u0000' is the NUL character from '-z' option. After that we
|
|
39
|
+
* parse the lines by splitting from NUL, and then split the first
|
|
40
|
+
* part from space. This yields us enough info both filter out submodule
|
|
41
|
+
* roots and get the filename.
|
|
42
|
+
*/
|
|
43
|
+
return output
|
|
44
|
+
.slice(1)
|
|
45
|
+
.split('\u0000:')
|
|
46
|
+
.map(parseGitZOutput)
|
|
47
|
+
.flatMap(([info, src, dst]) => {
|
|
48
|
+
const [, dstMode, , , statusWithScore] = info.split(' ')
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Filter out submodules and symlinks
|
|
52
|
+
* @see https://github.com/git/git/blob/cb96e1697ad6e54d11fc920c95f82977f8e438f8/Documentation/git-fast-import.adoc?plain=1#L634-L646
|
|
53
|
+
*/
|
|
54
|
+
if (dstMode === '160000' || dstMode === '120000') {
|
|
55
|
+
return []
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @example "M"
|
|
60
|
+
* @example "R86"
|
|
61
|
+
*
|
|
62
|
+
* - A: addition of a file
|
|
63
|
+
* - C: copy of a file into a new one
|
|
64
|
+
* - D: deletion of a file
|
|
65
|
+
* - M: modification of the contents or mode of a file
|
|
66
|
+
* - R: renaming of a file
|
|
67
|
+
* - T: change in the type of the file (regular file, symbolic link or submodule)
|
|
68
|
+
* - U: file is unmerged (you must complete the merge before it can be committed)
|
|
69
|
+
* - X: "unknown" change type (most probably a bug, please report it)
|
|
70
|
+
*/
|
|
71
|
+
const status = statusWithScore[0]
|
|
72
|
+
|
|
73
|
+
/** "dst" exists when moving files, otherwise it's undefined and only "src" exists */
|
|
74
|
+
const filename = dst ?? src
|
|
75
|
+
|
|
76
|
+
return [
|
|
77
|
+
{
|
|
78
|
+
filepath: normalizePath(path.resolve(cwd, filename)),
|
|
79
|
+
status,
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
})
|
|
83
|
+
} catch {
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
}
|