lint-staged 16.2.6 → 16.3.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/README.md CHANGED
@@ -34,6 +34,10 @@ $ git commit
34
34
 
35
35
  </details>
36
36
 
37
+ > [!Tip]
38
+ > Do you only want to check staged files for errors, but not edit them automatically?
39
+ > You might be interested in this simpler shell script: [`lint-staged.sh`](https://github.com/lint-staged/lint-staged.sh).
40
+
37
41
  ## Table of Contents
38
42
 
39
43
  - [Why](#why)
package/lib/colors.js CHANGED
@@ -2,14 +2,14 @@ import nodeTty from 'node:tty'
2
2
 
3
3
  /**
4
4
  * @example NO_COLOR
5
- * @exmaple NO_COLOR=1
6
- * @exmaple NO_COLOR=true
5
+ * @example NO_COLOR=1
6
+ * @example NO_COLOR=true
7
7
  */
8
8
  const TRUTHRY_ENV_VAR_VALUES = ['', '1', 'true']
9
9
 
10
10
  /**
11
- * @exmaple FORCE_COLOR=0
12
- * @exmaple FORCE_COLOR=false
11
+ * @example FORCE_COLOR=0
12
+ * @example FORCE_COLOR=false
13
13
  */
14
14
  const FALSY_ENV_VAR_VALUES = ['0', 'false']
15
15
 
package/lib/execGit.js CHANGED
@@ -1,4 +1,4 @@
1
- import spawn, { SubprocessError } from 'nano-spawn'
1
+ import { exec } from 'tinyexec'
2
2
 
3
3
  import { createDebug } from './debug.js'
4
4
 
@@ -13,22 +13,25 @@ const NO_SUBMODULE_RECURSE = ['-c', 'submodule.recurse=false']
13
13
  // exported for tests
14
14
  export const GIT_GLOBAL_OPTIONS = [...NO_SUBMODULE_RECURSE]
15
15
 
16
- /** @type {(cmd: string[], options?: import('nano-spawn').Options) => Promise<string>} */
16
+ /** @type {(cmd: string[], options?: { cwd?: string }) => Promise<string>} */
17
17
  export const execGit = async (cmd, options) => {
18
- debugLog('Running git command', cmd)
19
- try {
20
- const result = await spawn('git', [...NO_SUBMODULE_RECURSE, ...cmd], {
21
- ...options,
22
- cwd: options?.cwd ?? process.cwd(),
23
- stdin: 'ignore',
24
- })
25
-
26
- return result.stdout
27
- } catch (error) {
28
- if (error instanceof SubprocessError) {
29
- throw new Error(error.output, { cause: error })
30
- }
31
-
32
- throw error
18
+ debugLog('Running git command:', cmd)
19
+ const result = exec('git', [...NO_SUBMODULE_RECURSE, ...cmd], {
20
+ nodeOptions: {
21
+ cwd: options?.cwd,
22
+ stdio: ['ignore'],
23
+ },
24
+ })
25
+
26
+ let output = ''
27
+ for await (const line of result) {
28
+ output += line + '\n'
29
+ }
30
+ output = output.trimEnd()
31
+
32
+ if (result.exitCode > 0) {
33
+ throw new Error(output, { cause: result })
33
34
  }
35
+
36
+ return output
34
37
  }
@@ -0,0 +1,18 @@
1
+ export const Signal = {
2
+ SIGINT: 'SIGINT',
3
+ SIGKILL: 'SIGKILL',
4
+ }
5
+
6
+ /**
7
+ * Get an AbortController used to cancel running tasks on failure/interruption.
8
+ * @returns AbortController
9
+ */
10
+ export const getAbortController = (nodeProcess = process) => {
11
+ const abortController = new AbortController()
12
+
13
+ nodeProcess.on(Signal.SIGINT, () => {
14
+ abortController.abort(Signal.SIGINT)
15
+ })
16
+
17
+ return abortController
18
+ }
@@ -1,5 +1,5 @@
1
1
  import { createDebug } from './debug.js'
2
- import { makeErr } from './getSpawnedTask.js'
2
+ import { createTaskError } from './getSpawnedTask.js'
3
3
 
4
4
  const debugLog = createDebug('lint-staged:getFunctionTasks')
5
5
 
@@ -24,7 +24,7 @@ export const getFunctionTask = async (command, files) => {
24
24
  try {
25
25
  await command.task(files.map((file) => file.filepath))
26
26
  } catch (e) {
27
- throw makeErr(command.title, e, ctx)
27
+ throw createTaskError(command.title, e, ctx)
28
28
  }
29
29
  }
30
30
 
@@ -1,106 +1,42 @@
1
- import spawn, { SubprocessError } from 'nano-spawn'
2
- import pidtree from 'pidtree'
3
1
  import { parseArgsStringToArgv } from 'string-argv'
2
+ import { exec } from 'tinyexec'
4
3
 
5
4
  import { blackBright, red } from './colors.js'
6
5
  import { createDebug } from './debug.js'
7
6
  import { error, info } from './figures.js'
7
+ import { Signal } from './getAbortController.js'
8
+ import { killSubProcesses } from './killSubprocesses.js'
8
9
  import { getInitialState } from './state.js'
9
10
  import { TaskError } from './symbols.js'
10
11
 
11
- const TASK_ERROR = 'lint-staged:taskError'
12
-
13
12
  const debugLog = createDebug('lint-staged:getSpawnedTask')
14
13
 
15
- /** @type {(error: import('nano-spawn').SubprocessError) => string} */
16
- const getTag = (error) => {
17
- return error.signalName ?? 'FAILED'
18
- }
19
-
20
14
  /**
21
15
  * Handle task console output.
22
16
  *
23
17
  * @param {string} command
24
- * @param {import('nano-spawn').Result | import('nano-spawn').SubprocessError} result
25
- * @param {Object} ctx
26
- * @returns {Error}
18
+ * @param {string} output
19
+ * @param {ReturnType<typeof getInitialState>} ctx context
20
+ * @param {keyof typeof Signal | undefined} signal
21
+ * @param {import('tinyexec').Result} [errorResult]
27
22
  */
28
- const handleOutput = (command, result, ctx, isError = false) => {
29
- if (result.output) {
30
- const outputTitle = isError ? red(`${error} ${command}:`) : `${info} ${command}:`
31
- const output = [...(ctx.quiet ? [] : ['', outputTitle]), result.output]
32
- ctx.output.push(output.join('\n'))
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
33
28
  }
34
29
 
35
- if (ctx.quiet) return
36
-
37
- if (result instanceof SubprocessError) {
38
- ctx.output.push(red(`\n${error} ${command} failed to spawn:`), result.message, result.cause)
39
- } else if (isError) {
40
- // Show generic error when task had no output
41
- const tag = getTag(result)
42
- const message = red(`\n${error} ${command} failed without output (${tag}).`)
43
- ctx.output.push(message)
30
+ if (ctx.quiet) {
31
+ return
44
32
  }
45
- }
46
33
 
47
- /**
48
- * Kill subprocess along with all its child processes.
49
- * @param {import('nano-spawn').Subprocess} subprocess
50
- */
51
- const killSubprocess = async (subprocess) => {
52
- let childProcess
53
-
54
- try {
55
- childProcess = await subprocess.nodeChildProcess
56
- } catch {
57
- /** ignore internal nano-spawn errors, if child process isn't available it can't be killed */
58
- }
59
-
60
- if (childProcess?.pid !== undefined) {
61
- try {
62
- for (const childPid of await pidtree(childProcess.pid)) {
63
- try {
64
- process.kill(childPid, 'SIGKILL')
65
- } catch (error) {
66
- debugLog(`Failed to kill process with pid "%d": %o`, childPid, error)
67
- }
68
- }
69
- } catch (error) {
70
- // Suppress "No matching pid found" error. This probably means
71
- // the process already died before executing.
72
- debugLog(`Failed to list child processes of pid "%d": %o`, childProcess.pid, error)
73
- }
74
- }
75
-
76
- // The child process is terminated separately in order to get the `KILLED` status.
77
- childProcess?.kill('SIGKILL')
78
- }
79
-
80
- /**
81
- * Interrupts the execution of the subprocess that we spawned if
82
- * another task adds an error to the context.
83
- *
84
- * @param {Object} ctx
85
- * @param {import('nano-spawn').Subprocess} subprocess
86
- * @returns {() => Promise<void>} Function that clears the interval that
87
- * checks the context.
88
- */
89
- const interruptExecutionOnError = (ctx, subprocess) => {
90
- let killPromise
91
-
92
- const errorListener = async () => {
93
- killPromise = killSubprocess(subprocess)
94
- await killPromise
95
- }
96
-
97
- ctx.events.on(TASK_ERROR, errorListener, { once: true })
98
-
99
- return async () => {
100
- ctx.events.off(TASK_ERROR, errorListener)
101
- if (killPromise) {
102
- await killPromise
103
- }
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)
104
40
  }
105
41
  }
106
42
 
@@ -108,25 +44,21 @@ const interruptExecutionOnError = (ctx, subprocess) => {
108
44
  * Create a error output depending on process result.
109
45
  *
110
46
  * @param {string} command
111
- * @param {import('nano-spawn').SubprocessError} error
112
- * @param {Object} ctx
47
+ * @param {import('tinyexec').Result} result
48
+ * @param {ReturnType<typeof getInitialState>} ctx context
49
+ * @param {keyof typeof Signal | undefined} signal
113
50
  * @returns {Error}
114
51
  */
115
- export const makeErr = (command, error, ctx) => {
52
+ export const createTaskError = (command, result, ctx, signal = 'FAILED') => {
116
53
  ctx.errors.add(TaskError)
117
-
118
- // https://nodejs.org/api/events.html#error-events
119
- ctx.events.emit(TASK_ERROR, TaskError)
120
-
121
- handleOutput(command, error, ctx, true)
122
- const tag = getTag(error)
123
- return new Error(`${red(command)} ${blackBright(`[${tag}]`)}`)
54
+ return new Error(`${red(command)} ${blackBright(`[${signal}]`)}`, { cause: result })
124
55
  }
125
56
 
126
57
  /**
127
58
  * Returns the task function for the linter.
128
59
  *
129
60
  * @param {Object} options
61
+ * @param {AbortController} options.abortController
130
62
  * @param {boolean} [options.color]
131
63
  * @param {string} options.command — Linter task
132
64
  * @param {string} [options.continueOnError]
@@ -138,6 +70,7 @@ export const makeErr = (command, error, ctx) => {
138
70
  * @returns {() => Promise<Array<string>>}
139
71
  */
140
72
  export const getSpawnedTask = ({
73
+ abortController,
141
74
  color,
142
75
  command,
143
76
  continueOnError = false,
@@ -151,35 +84,78 @@ export const getSpawnedTask = ({
151
84
  debugLog('cmd:', cmd)
152
85
  debugLog('args:', args)
153
86
 
154
- const spawnOptions = {
155
- // Only use topLevelDir as CWD if we are using the git binary
156
- // e.g `npm` should run tasks in the actual CWD
157
- cwd: /^git(\.exe)?/i.test(cmd) ? topLevelDir : cwd,
158
- preferLocal: true,
159
- stdin: 'ignore',
160
- env: color ? { FORCE_COLOR: 'true' } : { NO_COLOR: 'true' },
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
+ detached: true,
94
+ env: color ? { FORCE_COLOR: 'true' } : { NO_COLOR: 'true' },
95
+ stdio: ['ignore'],
96
+ },
161
97
  }
162
98
 
163
- debugLog('Spawn options:', spawnOptions)
99
+ debugLog('Tinyexec options:', tinyExecOptions)
164
100
 
101
+ /** @param {ReturnType<typeof getInitialState>} ctx context */
165
102
  return async (ctx = getInitialState()) => {
166
- let quitInterruptCheck
103
+ const result = exec(cmd, isFn ? args : args.concat(files), tinyExecOptions)
167
104
 
105
+ const taskFailed = () => result.exitCode > 0 || result.process?.signalCode
106
+
107
+ /** @type {keyof typeof Signal | undefined} */
108
+ let signal
109
+
110
+ abortController.signal.addEventListener(
111
+ 'abort',
112
+ async () => {
113
+ if (taskFailed() || !result.process) return
114
+
115
+ signal = abortController.signal.reason
116
+ const pid = result.process.pid
117
+ result.process.kill(abortController.signal.reason)
118
+ await killSubProcesses(pid)
119
+ },
120
+ { once: true }
121
+ )
122
+
123
+ let output = ''
168
124
  try {
169
- const subprocess = spawn(cmd, isFn ? args : args.concat(files), spawnOptions)
170
- if (!continueOnError) {
171
- quitInterruptCheck = interruptExecutionOnError(ctx, subprocess)
172
- }
173
- const result = await subprocess
174
- if (verbose) {
175
- handleOutput(command, result, ctx)
125
+ for await (const line of result) {
126
+ output += line + '\n'
176
127
  }
177
128
  } catch (error) {
178
- throw makeErr(command, error, ctx)
179
- } finally {
180
- if (quitInterruptCheck) {
181
- await quitInterruptCheck()
129
+ /** Probably failed to spawn (ENOENT) */
130
+ const errorSignal = (error instanceof Error && error.code) || 'FAILED'
131
+
132
+ if (continueOnError !== true) {
133
+ /** Other tasks should be killed */
134
+ abortController.abort(Signal.SIGKILL)
135
+ }
136
+
137
+ handleTaskOutput(command, output, ctx, errorSignal, result)
138
+ throw createTaskError(command, result, ctx, errorSignal)
139
+ }
140
+
141
+ output = output.trimEnd()
142
+
143
+ if (taskFailed()) {
144
+ if (continueOnError !== true) {
145
+ /** Other tasks should be killed */
146
+ abortController.abort(Signal.SIGKILL)
182
147
  }
148
+
149
+ if (result.process?.pid) {
150
+ await killSubProcesses(result.process.pid)
151
+ }
152
+
153
+ handleTaskOutput(command, output, ctx, signal, result)
154
+ throw createTaskError(command, result, ctx, result.process?.signalCode ?? signal)
155
+ }
156
+
157
+ if (verbose) {
158
+ handleTaskOutput(command, output, ctx, signal)
183
159
  }
184
160
  }
185
161
  }
@@ -8,6 +8,7 @@ const debugLog = createDebug('lint-staged:getSpawnedTasks')
8
8
  * Creates and returns an array of listr tasks which map to the given commands.
9
9
  *
10
10
  * @param {object} options
11
+ * @param {AbortController} options.abortController
11
12
  * @param {boolean} [options.color]
12
13
  * @param {Array<string|Function>|string|Function} options.commands
13
14
  * @param {string} options.continueOnError
@@ -17,6 +18,7 @@ const debugLog = createDebug('lint-staged:getSpawnedTasks')
17
18
  * @param {Boolean} verbose
18
19
  */
19
20
  export const getSpawnedTasks = async ({
21
+ abortController,
20
22
  color,
21
23
  commands,
22
24
  continueOnError,
@@ -55,6 +57,7 @@ export const getSpawnedTasks = async ({
55
57
  }
56
58
 
57
59
  const task = getSpawnedTask({
60
+ abortController,
58
61
  color,
59
62
  command,
60
63
  continueOnError,
@@ -0,0 +1,42 @@
1
+ import { exec } from 'node:child_process'
2
+ import { promisify } from 'node:util'
3
+
4
+ const execAsync = promisify(exec)
5
+
6
+ /**
7
+ * End process by pid, forcefully, including child processes
8
+ *
9
+ * @see {@link https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/taskkill}
10
+ *
11
+ * @param {number} pid
12
+ */
13
+ const killWin32Subprocesses = async (pid) => {
14
+ await execAsync(`taskkill /pid ${pid} /T /F`)
15
+ }
16
+
17
+ /**
18
+ * Kill all processes in the group by using negative pid
19
+ *
20
+ * @see {@link https://pubs.opengroup.org/onlinepubs/9699919799/functions/kill.html}
21
+ *
22
+ * @param {number} pid
23
+ */
24
+ const killUnixSubprocesses = async (pid) => {
25
+ process.kill(-pid, 'SIGKILL')
26
+ }
27
+
28
+ /**
29
+ * @param {number} pid
30
+ * @param {boolean} [isWin32]
31
+ */
32
+ export const killSubProcesses = async (pid, isWin32 = process.platform === 'win32') => {
33
+ try {
34
+ if (isWin32) {
35
+ await killWin32Subprocesses(pid)
36
+ } else {
37
+ await killUnixSubprocesses(pid)
38
+ }
39
+ } catch {
40
+ /** ignore errors */
41
+ }
42
+ }
package/lib/runAll.js CHANGED
@@ -9,6 +9,7 @@ import { blackBright } from './colors.js'
9
9
  import { createDebug } from './debug.js'
10
10
  import { execGit } from './execGit.js'
11
11
  import { generateTasks } from './generateTasks.js'
12
+ import { getAbortController } from './getAbortController.js'
12
13
  import { getFunctionTask, isFunctionTask } from './getFunctionTask.js'
13
14
  import { getRenderer } from './getRenderer.js'
14
15
  import { getSpawnedTasks } from './getSpawnedTasks.js'
@@ -187,20 +188,14 @@ export const runAll = async (
187
188
  ...getRenderer({ color, debug, quiet }, logger),
188
189
  }
189
190
 
190
- /**
191
- * This is used to set max event listener count to the total number
192
- * of generated tasks. The event listener is used to keep track of
193
- * the interrupt signal and kill all tasks when it happens. See the
194
- * `interruptExecutionOnError` in `getSpawnedTask`.
195
- */
196
- let listrTaskCount = 0
197
-
198
191
  const listrTasks = []
199
192
 
200
193
  // Set of all staged files that matched a task glob. Values in a set are unique.
201
194
  /** @type {Set<import('./getStagedFiles.js').StagedFile>} */
202
195
  const matchedFiles = new Set()
203
196
 
197
+ const abortController = getAbortController()
198
+
204
199
  for (const [configPath, { config, files }] of Object.entries(filesByConfig)) {
205
200
  const configName = configPath ? normalizePath(path.relative(cwd, configPath)) : 'Config object'
206
201
 
@@ -222,6 +217,7 @@ export const runAll = async (
222
217
  (isFunctionTask(task.commands)
223
218
  ? getFunctionTask(task.commands, task.fileList)
224
219
  : getSpawnedTasks({
220
+ abortController,
225
221
  color,
226
222
  commands: task.commands,
227
223
  continueOnError,
@@ -273,8 +269,6 @@ export const runAll = async (
273
269
  )
274
270
  )
275
271
 
276
- listrTaskCount += chunkListrTasks.length
277
-
278
272
  listrTasks.push({
279
273
  title:
280
274
  `${configName}${blackBright(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` +
@@ -368,9 +362,6 @@ export const runAll = async (
368
362
  listrOptions
369
363
  )
370
364
 
371
- debugLog('Set max event listeners to the number of tasks: %i', listrTaskCount)
372
- ctx.events.setMaxListeners(listrTaskCount)
373
-
374
365
  await runner.run()
375
366
 
376
367
  if (ctx.errors.size > 0) {
package/lib/state.js CHANGED
@@ -1,5 +1,3 @@
1
- import EventEmitter from 'events'
2
-
3
1
  import { GIT_ERROR, TASK_ERROR } from './messages.js'
4
2
  import {
5
3
  FailOnChangesError,
@@ -18,7 +16,6 @@ export const getInitialState = ({
18
16
  } = {}) => ({
19
17
  backupHash: null,
20
18
  errors: new Set([]),
21
- events: new EventEmitter(),
22
19
  shouldFailOnChanges: failOnChanges,
23
20
  hasFilesToHide: null,
24
21
  output: [],
@@ -28,7 +28,8 @@ import { incorrectBraces } from './messages.js'
28
28
  * - *.${js} // dollar-sign inhibits expansion, so treated literally
29
29
  * - *.{js\,ts} // the comma is escaped, so treated literally
30
30
  */
31
- export const INCORRECT_BRACES_REGEXP = /(?<![\\$])({)(?:(?!(?<!\\),|\.\.|\{|\}).)*?(?<!\\)(})/g
31
+ export const getIncorrectBracesRegexp = () =>
32
+ /(?<![\\$])({)(?:(?!(?<!\\),|\.\.|\{|\}).)*?(?<!\\)(})/g
32
33
 
33
34
  /**
34
35
  * @param {string} pattern
@@ -36,12 +37,15 @@ export const INCORRECT_BRACES_REGEXP = /(?<![\\$])({)(?:(?!(?<!\\),|\.\.|\{|\}).
36
37
  */
37
38
  const stripIncorrectBraces = (pattern) => {
38
39
  let output = `${pattern}`
39
- let match = null
40
+ const regexp = getIncorrectBracesRegexp()
41
+ let match = regexp.exec(output)
40
42
 
41
- while ((match = INCORRECT_BRACES_REGEXP.exec(pattern))) {
43
+ while (match) {
42
44
  const fullMatch = match[0]
43
45
  const withoutBraces = fullMatch.replace(/{/, '').replace(/}/, '')
44
46
  output = output.replace(fullMatch, withoutBraces)
47
+ regexp.lastIndex = 0 // restart iteration
48
+ match = regexp.exec(output)
45
49
  }
46
50
 
47
51
  return output
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lint-staged",
3
- "version": "16.2.6",
3
+ "version": "16.3.0",
4
4
  "description": "Lint files staged by git",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -48,35 +48,35 @@
48
48
  "tag": "npx changeset tag"
49
49
  },
50
50
  "dependencies": {
51
- "commander": "^14.0.1",
51
+ "commander": "^14.0.3",
52
52
  "listr2": "^9.0.5",
53
53
  "micromatch": "^4.0.8",
54
54
  "nano-spawn": "^2.0.0",
55
- "pidtree": "^0.6.0",
56
55
  "string-argv": "^0.3.2",
57
- "yaml": "^2.8.1"
56
+ "tinyexec": "^1.0.2",
57
+ "yaml": "^2.8.2"
58
58
  },
59
59
  "devDependencies": {
60
- "@changesets/changelog-github": "0.5.1",
61
- "@changesets/cli": "2.29.7",
62
- "@commitlint/cli": "20.1.0",
63
- "@commitlint/config-conventional": "20.0.0",
64
- "@eslint/js": "9.38.0",
65
- "@vitest/coverage-v8": "4.0.1",
66
- "@vitest/eslint-plugin": "1.3.23",
60
+ "@changesets/changelog-github": "0.5.2",
61
+ "@changesets/cli": "2.29.8",
62
+ "@commitlint/cli": "20.4.2",
63
+ "@commitlint/config-conventional": "20.4.2",
64
+ "@eslint/js": "10.0.1",
65
+ "@vitest/coverage-v8": "4.0.18",
66
+ "@vitest/eslint-plugin": "1.6.9",
67
67
  "consolemock": "1.1.0",
68
68
  "cross-env": "10.1.0",
69
- "eslint": "9.38.0",
69
+ "eslint": "10.0.2",
70
70
  "eslint-config-prettier": "10.1.8",
71
- "eslint-plugin-n": "17.23.1",
72
- "eslint-plugin-prettier": "5.5.4",
71
+ "eslint-plugin-n": "17.24.0",
72
+ "eslint-plugin-prettier": "5.5.5",
73
73
  "eslint-plugin-simple-import-sort": "12.1.1",
74
74
  "husky": "9.1.7",
75
75
  "mock-stdin": "1.0.0",
76
- "prettier": "3.6.2",
77
- "semver": "7.7.3",
76
+ "prettier": "3.8.1",
77
+ "semver": "7.7.4",
78
78
  "typescript": "5.9.3",
79
- "vitest": "4.0.1"
79
+ "vitest": "4.0.18"
80
80
  },
81
81
  "keywords": [
82
82
  "lint",