@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.
@@ -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
+ }