lint-staged 13.2.3 → 13.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
@@ -58,7 +58,7 @@ To install _lint-staged_ in the recommended way, you need to:
58
58
  1. Install some linters, like [ESLint](https://eslint.org) or [Prettier](https://prettier.io)
59
59
  1. Configure _lint-staged_ to run linters and other tasks:
60
60
  - for example: `{ "*.js": "eslint" }` to run ESLint for all staged JS files
61
- - See [Configuration](#Configuration) for more info
61
+ - See [Configuration](#configuration) for more info
62
62
 
63
63
  Don't forget to commit changes to `package.json` and `.husky` to share this setup with your team!
64
64
 
@@ -72,6 +72,10 @@ See [Releases](https://github.com/okonet/lint-staged/releases).
72
72
 
73
73
  ### Migration
74
74
 
75
+ #### v14
76
+
77
+ - Since `v14.0.0` _lint-staged_ no longer supports Node.js 14. Please upgrade your Node.js version to at least `16.14.0`.
78
+
75
79
  #### v13
76
80
 
77
81
  - Since `v13.0.0` _lint-staged_ no longer supports Node.js 12. Please upgrade your Node.js version to at least `14.13.1`, or `16.0.0` onward.
@@ -218,11 +222,11 @@ If necessary, you can limit the concurrency using `--concurrent <number>` or dis
218
222
  Linter commands work on a subset of all staged files, defined by a _glob pattern_. lint-staged uses [micromatch](https://github.com/micromatch/micromatch) for matching files with the following rules:
219
223
 
220
224
  - If the glob pattern contains no slashes (`/`), micromatch's `matchBase` option will enabled, so globs match a file's basename regardless of directory:
221
- - **`"*.js"`** will match all JS files, like `/test.js` and `/foo/bar/test.js`
222
- - **`"!(*test).js"`**. will match all JS files, except those ending in `test.js`, so `foo.js` but not `foo.test.js`
225
+ - `"*.js"` will match all JS files, like `/test.js` and `/foo/bar/test.js`
226
+ - `"!(*test).js"` will match all JS files, except those ending in `test.js`, so `foo.js` but not `foo.test.js`
223
227
  - If the glob pattern does contain a slash (`/`), it will match for paths as well:
224
- - **`"./*.js"`** will match all JS files in the git repo root, so `/test.js` but not `/foo/bar/test.js`
225
- - **`"foo/**/*.js"`** will match all JS files inside the `/foo` directory, so `/foo/bar/test.js` but not `/test.js`
228
+ - `"./*.js"` will match all JS files in the git repo root, so `/test.js` but not `/foo/bar/test.js`
229
+ - `"foo/**/*.js"` will match all JS files inside the `/foo` directory, so `/foo/bar/test.js` but not `/test.js`
226
230
 
227
231
  When matching, lint-staged will do the following
228
232
 
@@ -624,9 +628,7 @@ See more on [this blog post](https://medium.com/@tomchentw/imagemin-lint-staged-
624
628
  const path = require('path')
625
629
 
626
630
  const buildEslintCommand = (filenames) =>
627
- `next lint --fix --file ${filenames
628
- .map((f) => path.relative(process.cwd(), f))
629
- .join(' --file ')}`
631
+ `next lint --fix --file ${filenames.map((f) => path.relative(process.cwd(), f)).join(' --file ')}`
630
632
 
631
633
  module.exports = {
632
634
  '*.{js,jsx,ts,tsx}': [buildEslintCommand],
@@ -1,8 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import fs from 'node:fs'
4
- import path from 'node:path'
5
- import { fileURLToPath } from 'node:url'
3
+ import fs from 'node:fs/promises'
6
4
 
7
5
  import { supportsColor } from 'chalk'
8
6
  import { Option, program } from 'commander'
@@ -19,8 +17,7 @@ if (supportsColor) {
19
17
  // Do not terminate main Listr process on SIGINT
20
18
  process.on('SIGINT', () => {})
21
19
 
22
- const packageJsonPath = path.join(fileURLToPath(import.meta.url), '../../package.json')
23
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath))
20
+ const packageJson = JSON.parse(await fs.readFile(new URL('../package.json', import.meta.url)))
24
21
  const version = packageJson.version
25
22
 
26
23
  const debugLog = debug('lint-staged:bin')
@@ -113,7 +110,7 @@ debugLog('Options parsed from command-line:', options)
113
110
  if (options.configPath === '-') {
114
111
  delete options.configPath
115
112
  try {
116
- options.config = fs.readFileSync(process.stdin.fd, 'utf8').toString().trim()
113
+ options.config = await fs.readFile(process.stdin.fd, 'utf8').toString().trim()
117
114
  } catch {
118
115
  console.error(CONFIG_STDIN_ERROR)
119
116
  process.exit(1)
@@ -126,10 +123,9 @@ if (options.configPath === '-') {
126
123
  }
127
124
  }
128
125
 
129
- lintStaged(options)
130
- .then((passed) => {
131
- process.exitCode = passed ? 0 : 1
132
- })
133
- .catch(() => {
134
- process.exitCode = 1
135
- })
126
+ try {
127
+ const passed = await lintStaged(options)
128
+ process.exitCode = passed ? 0 : 1
129
+ } catch {
130
+ process.exitCode = 1
131
+ }
package/lib/chunkFiles.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import path from 'node:path'
2
2
 
3
3
  import debug from 'debug'
4
- import normalize from 'normalize-path'
4
+
5
+ import { normalizePath } from './normalizePath.js'
5
6
 
6
7
  const debugLog = debug('lint-staged:chunkFiles')
7
8
 
@@ -35,7 +36,7 @@ const chunkArray = (arr, chunkCount) => {
35
36
  */
36
37
  export const chunkFiles = ({ files, baseDir, maxArgLength = null, relative = false }) => {
37
38
  const normalizedFiles = files.map((file) =>
38
- normalize(relative || !baseDir ? file : path.resolve(baseDir, file))
39
+ normalizePath(relative || !baseDir ? file : path.resolve(baseDir, file))
39
40
  )
40
41
 
41
42
  if (!maxArgLength) {
@@ -2,7 +2,8 @@ import path from 'node:path'
2
2
 
3
3
  import debug from 'debug'
4
4
  import micromatch from 'micromatch'
5
- import normalize from 'normalize-path'
5
+
6
+ import { normalizePath } from './normalizePath.js'
6
7
 
7
8
  const debugLog = debug('lint-staged:generateTasks')
8
9
 
@@ -19,7 +20,7 @@ const debugLog = debug('lint-staged:generateTasks')
19
20
  export const generateTasks = ({ config, cwd = process.cwd(), files, relative = false }) => {
20
21
  debugLog('Generating linter tasks')
21
22
 
22
- const relativeFiles = files.map((file) => normalize(path.relative(cwd, file)))
23
+ const relativeFiles = files.map((file) => normalizePath(path.relative(cwd, file)))
23
24
 
24
25
  return Object.entries(config).map(([pattern, commands]) => {
25
26
  const isParentDirPattern = pattern.startsWith('../')
@@ -42,7 +43,7 @@ export const generateTasks = ({ config, cwd = process.cwd(), files, relative = f
42
43
  strictBrackets: true,
43
44
  })
44
45
 
45
- const fileList = matches.map((file) => normalize(relative ? file : path.resolve(cwd, file)))
46
+ const fileList = matches.map((file) => normalizePath(relative ? file : path.resolve(cwd, file)))
46
47
 
47
48
  const task = { pattern, commands, fileList }
48
49
  debugLog('Generated task: \n%O', task)
@@ -1,28 +1,64 @@
1
- const getMainRendererOptions = ({ debug, quiet }, env) => {
2
- if (quiet) return { renderer: 'silent' }
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 = ({ 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
+
3
34
  // Better support for dumb terminals: https://en.wikipedia.org/wiki/Computer_terminal#Dumb_terminals
4
- const isDumbTerminal = env.TERM === 'dumb'
5
- if (debug || isDumbTerminal || env.NODE_ENV === 'test') return { renderer: 'verbose' }
6
- return { renderer: 'update', rendererOptions: { dateFormat: false } }
7
- }
35
+ if (debug || env.TERM === 'dumb') {
36
+ return {
37
+ renderer: 'verbose',
38
+ }
39
+ }
8
40
 
9
- const getFallbackRenderer = ({ renderer }, { FORCE_COLOR }) => {
10
- if (renderer === 'silent') {
11
- return 'silent'
41
+ return {
42
+ renderer: 'update',
43
+ rendererOptions: {
44
+ formatOutput: 'truncate',
45
+ },
12
46
  }
47
+ }
13
48
 
14
- // If colors are being forced, then also force non-fallback rendering
15
- if (Number(FORCE_COLOR) > 0) {
49
+ const getFallbackRenderer = ({ renderer }, { FORCE_COLOR }) => {
50
+ if (renderer === 'silent' || renderer === 'test' || Number(FORCE_COLOR) > 0) {
16
51
  return renderer
17
52
  }
18
53
 
19
54
  return 'verbose'
20
55
  }
21
56
 
22
- export const getRenderer = (options, env = process.env) => {
23
- const mainRendererOptions = getMainRendererOptions(options, env)
57
+ export const getRenderer = (options, logger, env = process.env) => {
58
+ const mainRendererOptions = getMainRendererOptions(options, logger, env)
59
+
24
60
  return {
25
61
  ...mainRendererOptions,
26
- nonTTYRenderer: getFallbackRenderer(mainRendererOptions, env),
62
+ fallbackRenderer: getFallbackRenderer(mainRendererOptions, env),
27
63
  }
28
64
  }
@@ -1,9 +1,8 @@
1
1
  import path from 'node:path'
2
2
 
3
- import normalize from 'normalize-path'
4
-
5
3
  import { execGit } from './execGit.js'
6
4
  import { getDiffCommand } from './getDiffCommand.js'
5
+ import { normalizePath } from './normalizePath.js'
7
6
  import { parseGitZOutput } from './parseGitZOutput.js'
8
7
 
9
8
  export const getStagedFiles = async ({ cwd = process.cwd(), diff, diffFilter } = {}) => {
@@ -11,7 +10,7 @@ export const getStagedFiles = async ({ cwd = process.cwd(), diff, diffFilter } =
11
10
  const lines = await execGit(getDiffCommand(diff, diffFilter), { cwd })
12
11
  if (!lines) return []
13
12
 
14
- return parseGitZOutput(lines).map((file) => normalize(path.resolve(cwd, file)))
13
+ return parseGitZOutput(lines).map((file) => normalizePath(path.resolve(cwd, file)))
15
14
  } catch {
16
15
  return null
17
16
  }
@@ -1,4 +1,3 @@
1
- import cliTruncate from 'cli-truncate'
2
1
  import debug from 'debug'
3
2
 
4
3
  import { configurationError } from './messages.js'
@@ -6,23 +5,6 @@ import { resolveTaskFn } from './resolveTaskFn.js'
6
5
 
7
6
  const debugLog = debug('lint-staged:makeCmdTasks')
8
7
 
9
- const STDOUT_COLUMNS_DEFAULT = 80
10
-
11
- const listrPrefixLength = {
12
- update: ` X `.length, // indented task title where X is a checkmark or a cross (failure)
13
- verbose: `[STARTED] `.length, // verbose renderer uses 7-letter STARTED/SUCCESS prefixes
14
- }
15
-
16
- /**
17
- * Get length of title based on the number of available columns prefix length
18
- * @param {string} renderer The name of the Listr renderer
19
- * @returns {number}
20
- */
21
- const getTitleLength = (renderer, columns = process.stdout.columns) => {
22
- const prefixLength = listrPrefixLength[renderer] || 0
23
- return (columns || STDOUT_COLUMNS_DEFAULT) - prefixLength
24
- }
25
-
26
8
  /**
27
9
  * Creates and returns an array of listr tasks which map to the given commands.
28
10
  *
@@ -31,11 +13,10 @@ const getTitleLength = (renderer, columns = process.stdout.columns) => {
31
13
  * @param {string} options.cwd
32
14
  * @param {Array<string>} options.files
33
15
  * @param {string} options.gitDir
34
- * @param {string} options.renderer
35
16
  * @param {Boolean} shell
36
17
  * @param {Boolean} verbose
37
18
  */
38
- export const makeCmdTasks = async ({ commands, cwd, files, gitDir, renderer, shell, verbose }) => {
19
+ export const makeCmdTasks = async ({ commands, cwd, files, gitDir, shell, verbose }) => {
39
20
  debugLog('Creating listr tasks for commands %o', commands)
40
21
  const commandArray = Array.isArray(commands) ? commands : [commands]
41
22
  const cmdTasks = []
@@ -60,10 +41,8 @@ export const makeCmdTasks = async ({ commands, cwd, files, gitDir, renderer, she
60
41
  )
61
42
  }
62
43
 
63
- // Truncate title to single line based on renderer
64
- const title = cliTruncate(command, getTitleLength(renderer))
65
44
  const task = resolveTaskFn({ command, cwd, files, gitDir, isFn, shell, verbose })
66
- cmdTasks.push({ title, command, task })
45
+ cmdTasks.push({ title: command, command, task })
67
46
  }
68
47
  }
69
48
 
package/lib/messages.js CHANGED
@@ -1,14 +1,13 @@
1
+ import { inspect } from 'node:util'
2
+
1
3
  import chalk from 'chalk'
2
- import inspect from 'object-inspect'
3
4
 
4
5
  import { error, info, warning } from './figures.js'
5
6
 
6
7
  export const configurationError = (opt, helpMsg, value) =>
7
8
  `${chalk.redBright(`${error} Validation Error:`)}
8
9
 
9
- Invalid value for '${chalk.bold(opt)}': ${chalk.bold(
10
- inspect(value, { inlineCharacterLimit: Number.POSITIVE_INFINITY })
11
- )}
10
+ Invalid value for '${chalk.bold(opt)}': ${chalk.bold(inspect(value))}
12
11
 
13
12
  ${helpMsg}`
14
13
 
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Reimplementation of "normalize-path"
3
+ * @see https://github.com/jonschlinkert/normalize-path/blob/52c3a95ebebc2d98c1ad7606cbafa7e658656899/index.js
4
+ */
5
+
6
+ /*!
7
+ * normalize-path <https://github.com/jonschlinkert/normalize-path>
8
+ *
9
+ * Copyright (c) 2014-2018, Jon Schlinkert.
10
+ * Released under the MIT License.
11
+ */
12
+
13
+ import path from 'node:path'
14
+
15
+ /**
16
+ * A file starting with \\?\
17
+ * @see https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces
18
+ */
19
+ const WIN32_FILE_NS = '\\\\?\\'
20
+
21
+ /**
22
+ * A file starting with \\.\
23
+ * @see https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#win32-file-namespaces
24
+ */
25
+ const WIN32_DEVICE_NS = '\\\\.\\'
26
+
27
+ /**
28
+ * Normalize input file path to use POSIX separators
29
+ * @param {String} input
30
+ * @returns String
31
+ */
32
+ export const normalizePath = (input) => {
33
+ if (input === path.posix.sep || input === path.win32.sep) {
34
+ return path.posix.sep
35
+ }
36
+
37
+ let normalized = input.split(/[/\\]+/).join(path.posix.sep)
38
+
39
+ /** Handle win32 Namespaced paths by changing e.g. \\.\ to //./ */
40
+ if (input.startsWith(WIN32_FILE_NS) || input.startsWith(WIN32_DEVICE_NS)) {
41
+ normalized = normalized.replace(/^\/(\.|\?)/, '//$1')
42
+ }
43
+
44
+ /** Remove trailing slash */
45
+ if (normalized.endsWith(path.posix.sep)) {
46
+ normalized = normalized.slice(0, -1)
47
+ }
48
+
49
+ return normalized
50
+ }
@@ -2,10 +2,10 @@ import fs from 'node:fs/promises'
2
2
  import path from 'node:path'
3
3
 
4
4
  import debug from 'debug'
5
- import normalize from 'normalize-path'
6
5
 
7
6
  import { execGit } from './execGit.js'
8
7
  import { readFile } from './file.js'
8
+ import { normalizePath } from './normalizePath.js'
9
9
 
10
10
  const debugLog = debug('lint-staged:resolveGitRepo')
11
11
 
@@ -15,7 +15,7 @@ const debugLog = debug('lint-staged:resolveGitRepo')
15
15
  */
16
16
  const resolveGitConfigDir = async (gitDir) => {
17
17
  // Get the real path in case it's a symlink
18
- const defaultDir = normalize(await fs.realpath(path.join(gitDir, '.git')))
18
+ const defaultDir = normalizePath(await fs.realpath(path.join(gitDir, '.git')))
19
19
  const stats = await fs.lstat(defaultDir)
20
20
  // If .git is a directory, use it
21
21
  if (stats.isDirectory()) return defaultDir
@@ -33,10 +33,10 @@ export const determineGitDir = (cwd, relativeDir) => {
33
33
  }
34
34
  if (relativeDir) {
35
35
  // the current working dir is inside the git top-level directory
36
- return normalize(cwd.substring(0, cwd.lastIndexOf(relativeDir)))
36
+ return normalizePath(cwd.substring(0, cwd.lastIndexOf(relativeDir)))
37
37
  } else {
38
38
  // the current working dir is the top-level git directory
39
- return normalize(cwd)
39
+ return normalizePath(cwd)
40
40
  }
41
41
  }
42
42
 
@@ -55,9 +55,9 @@ export const resolveGitRepo = async (cwd = process.cwd()) => {
55
55
 
56
56
  // read the path of the current directory relative to the top-level directory
57
57
  // don't read the toplevel directly, it will lead to an posix conform path on non posix systems (cygwin)
58
- const gitRel = normalize(await execGit(['rev-parse', '--show-prefix'], { cwd }))
59
- const gitDir = determineGitDir(normalize(cwd), gitRel)
60
- const gitConfigDir = normalize(await resolveGitConfigDir(gitDir))
58
+ const gitRel = normalizePath(await execGit(['rev-parse', '--show-prefix'], { cwd }))
59
+ const gitDir = determineGitDir(normalizePath(cwd), gitRel)
60
+ const gitConfigDir = normalizePath(await resolveGitConfigDir(gitDir))
61
61
 
62
62
  debugLog('Resolved git directory to be `%s`', gitDir)
63
63
  debugLog('Resolved git config directory to be `%s`', gitConfigDir)
package/lib/runAll.js CHANGED
@@ -5,7 +5,6 @@ import path from 'node:path'
5
5
  import chalk from 'chalk'
6
6
  import debug from 'debug'
7
7
  import { Listr } from 'listr2'
8
- import normalize from 'normalize-path'
9
8
 
10
9
  import { chunkFiles } from './chunkFiles.js'
11
10
  import { execGit } from './execGit.js'
@@ -24,6 +23,7 @@ import {
24
23
  SKIPPED_GIT_ERROR,
25
24
  skippingBackup,
26
25
  } from './messages.js'
26
+ import { normalizePath } from './normalizePath.js'
27
27
  import { resolveGitRepo } from './resolveGitRepo.js'
28
28
  import {
29
29
  applyModificationsSkipped,
@@ -151,7 +151,7 @@ export const runAll = async (
151
151
  ctx,
152
152
  exitOnError: false,
153
153
  registerSignalListeners: false,
154
- ...getRenderer({ debug, quiet }),
154
+ ...getRenderer({ debug, quiet }, logger),
155
155
  }
156
156
 
157
157
  const listrTasks = []
@@ -160,7 +160,7 @@ export const runAll = async (
160
160
  const matchedFiles = new Set()
161
161
 
162
162
  for (const [configPath, { config, files }] of Object.entries(filesByConfig)) {
163
- const configName = configPath ? normalize(path.relative(cwd, configPath)) : 'Config object'
163
+ const configName = configPath ? normalizePath(path.relative(cwd, configPath)) : 'Config object'
164
164
 
165
165
  const stagedFileChunks = chunkFiles({ baseDir: gitDir, files, maxArgLength, relative })
166
166
 
@@ -182,7 +182,6 @@ export const runAll = async (
182
182
  cwd: groupCwd,
183
183
  files: task.fileList,
184
184
  gitDir,
185
- renderer: listrOptions.renderer,
186
185
  shell,
187
186
  verbose,
188
187
  }).then((subTasks) => {
@@ -193,7 +192,7 @@ export const runAll = async (
193
192
  // relative filenames in the entire set.
194
193
  const normalizedFile = path.isAbsolute(file)
195
194
  ? file
196
- : normalize(path.join(groupCwd, file))
195
+ : normalizePath(path.join(groupCwd, file))
197
196
 
198
197
  matchedFiles.add(normalizedFile)
199
198
  })
@@ -3,10 +3,10 @@
3
3
  import path from 'node:path'
4
4
 
5
5
  import debug from 'debug'
6
- import normalize from 'normalize-path'
7
6
 
8
7
  import { execGit } from './execGit.js'
9
8
  import { loadConfig, searchPlaces } from './loadConfig.js'
9
+ import { normalizePath } from './normalizePath.js'
10
10
  import { parseGitZOutput } from './parseGitZOutput.js'
11
11
  import { validateConfig } from './validateConfig.js'
12
12
 
@@ -21,7 +21,7 @@ const numberOfLevels = (file) => file.split('/').length
21
21
 
22
22
  const sortDeepestParth = (a, b) => (numberOfLevels(a) > numberOfLevels(b) ? -1 : 1)
23
23
 
24
- const isInsideDirectory = (dir) => (file) => file.startsWith(normalize(dir))
24
+ const isInsideDirectory = (dir) => (file) => file.startsWith(normalizePath(dir))
25
25
 
26
26
  /**
27
27
  * Search all config files from the git repository, preferring those inside `cwd`.
@@ -68,7 +68,7 @@ export const searchConfigs = async (
68
68
 
69
69
  /** Sort possible config files so that deepest is first */
70
70
  const possibleConfigFiles = [...cachedFiles, ...otherFiles]
71
- .map((file) => normalize(path.join(gitDir, file)))
71
+ .map((file) => normalizePath(path.join(gitDir, file)))
72
72
  .filter(isInsideDirectory(cwd))
73
73
  .sort(sortDeepestParth)
74
74
 
@@ -17,8 +17,8 @@ import { incorrectBraces } from './messages.js'
17
17
  * braces from user configuration, but this is left to the user (after seeing the warning).
18
18
  *
19
19
  * @example <caption>Globs with brace expansions</caption>
20
- * - *.{js,tx} // expanded as *.js, *.ts
21
- * - *.{{j,t}s,css} // expanded as *.js, *.ts, *.css
20
+ * - *.{js,tx} // expanded as *.js, *.ts
21
+ * - *.{{j,t}s,css} // expanded as *.js, *.ts, *.css
22
22
  * - file_{1..10}.css // expanded as file_1.css, file_2.css, …, file_10.css
23
23
  *
24
24
  * @example <caption>Globs with incorrect brace expansions</caption>
@@ -28,17 +28,17 @@ 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 BRACES_REGEXP = /(?<![\\$])({)(?:(?!(?<!\\),|\.\.|\{|\}).)*?(?<!\\)(})/g
31
+ export const INCORRECT_BRACES_REGEXP = /(?<![\\$])({)(?:(?!(?<!\\),|\.\.|\{|\}).)*?(?<!\\)(})/g
32
32
 
33
33
  /**
34
34
  * @param {string} pattern
35
35
  * @returns {string}
36
36
  */
37
- const withoutIncorrectBraces = (pattern) => {
37
+ const stripIncorrectBraces = (pattern) => {
38
38
  let output = `${pattern}`
39
39
  let match = null
40
40
 
41
- while ((match = BRACES_REGEXP.exec(pattern))) {
41
+ while ((match = INCORRECT_BRACES_REGEXP.exec(pattern))) {
42
42
  const fullMatch = match[0]
43
43
  const withoutBraces = fullMatch.replace(/{/, '').replace(/}/, '')
44
44
  output = output.replace(fullMatch, withoutBraces)
@@ -47,6 +47,30 @@ const withoutIncorrectBraces = (pattern) => {
47
47
  return output
48
48
  }
49
49
 
50
+ /**
51
+ * This RegExp matches "duplicate" opening and closing braces, without any other braces
52
+ * in between, where the duplication is redundant and should be removed.
53
+ *
54
+ * @example *.{{js,ts}} // should just be *.{js,ts}
55
+ */
56
+ export const DOUBLE_BRACES_REGEXP = /{{[^}{]*}}/
57
+
58
+ /**
59
+ * @param {string} pattern
60
+ * @returns {string}
61
+ */
62
+ const stripDoubleBraces = (pattern) => {
63
+ let output = `${pattern}`
64
+ const match = DOUBLE_BRACES_REGEXP.exec(pattern)?.[0]
65
+
66
+ if (match) {
67
+ const withoutBraces = match.replace('{{', '{').replace('}}', '}')
68
+ output = output.replace(match, withoutBraces)
69
+ }
70
+
71
+ return output
72
+ }
73
+
50
74
  /**
51
75
  * Validate and remove incorrect brace expansions from glob pattern.
52
76
  * For example `*.{js}` is incorrect because it doesn't contain a `,` or `..`,
@@ -57,7 +81,7 @@ const withoutIncorrectBraces = (pattern) => {
57
81
  * @returns {string}
58
82
  */
59
83
  export const validateBraces = (pattern, logger) => {
60
- const fixedPattern = withoutIncorrectBraces(pattern)
84
+ const fixedPattern = stripDoubleBraces(stripIncorrectBraces(pattern))
61
85
 
62
86
  if (fixedPattern !== pattern) {
63
87
  logger.warn(incorrectBraces(pattern, fixedPattern))
@@ -1,7 +1,8 @@
1
1
  /** @typedef {import('./index').Logger} Logger */
2
2
 
3
+ import { inspect } from 'node:util'
4
+
3
5
  import debug from 'debug'
4
- import inspect from 'object-inspect'
5
6
 
6
7
  import { configurationError } from './messages.js'
7
8
  import { ConfigEmptyError, ConfigFormatError } from './symbols.js'
@@ -109,7 +110,7 @@ See https://github.com/okonet/lint-staged#configuration.`)
109
110
  }
110
111
 
111
112
  debugLog('Validated config from `%s`:', configPath)
112
- debugLog(inspect(config, { indent: 2 }))
113
+ debugLog(inspect(config, { compact: false }))
113
114
 
114
115
  return validatedConfig
115
116
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lint-staged",
3
- "version": "13.2.3",
3
+ "version": "13.3.0",
4
4
  "description": "Lint files staged by git",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/okonet/lint-staged",
@@ -14,7 +14,7 @@
14
14
  "url": "https://opencollective.com/lint-staged"
15
15
  },
16
16
  "engines": {
17
- "node": "^14.13.1 || >=16.0.0"
17
+ "node": "^16.14.0 || >=18.0.0"
18
18
  },
19
19
  "type": "module",
20
20
  "bin": "./bin/lint-staged.js",
@@ -33,37 +33,33 @@
33
33
  "test:watch": "jest --watch"
34
34
  },
35
35
  "dependencies": {
36
- "chalk": "5.2.0",
37
- "cli-truncate": "^3.1.0",
38
- "commander": "^10.0.0",
39
- "debug": "^4.3.4",
40
- "execa": "^7.0.0",
36
+ "chalk": "5.3.0",
37
+ "commander": "11.0.0",
38
+ "debug": "4.3.4",
39
+ "execa": "7.2.0",
41
40
  "lilconfig": "2.1.0",
42
- "listr2": "^5.0.7",
43
- "micromatch": "^4.0.5",
44
- "normalize-path": "^3.0.0",
45
- "object-inspect": "^1.12.3",
46
- "pidtree": "^0.6.0",
47
- "string-argv": "^0.3.1",
48
- "yaml": "^2.2.2"
41
+ "listr2": "6.6.1",
42
+ "micromatch": "4.0.5",
43
+ "pidtree": "0.6.0",
44
+ "string-argv": "0.3.2",
45
+ "yaml": "2.3.1"
49
46
  },
50
47
  "devDependencies": {
51
- "@babel/core": "^7.21.0",
52
- "@babel/eslint-parser": "^7.19.1",
53
- "@babel/preset-env": "^7.20.2",
54
- "babel-jest": "^29.5.0",
48
+ "@babel/core": "7.22.10",
49
+ "@babel/eslint-parser": "7.22.10",
50
+ "@babel/preset-env": "7.22.10",
51
+ "babel-jest": "29.6.2",
55
52
  "babel-plugin-transform-imports": "2.0.0",
56
- "consolemock": "^1.1.0",
57
- "eslint": "^8.35.0",
58
- "eslint-config-prettier": "^8.7.0",
59
- "eslint-plugin-import": "^2.27.5",
60
- "eslint-plugin-node": "^11.1.0",
61
- "eslint-plugin-prettier": "^4.2.1",
62
- "fs-extra": "^11.1.0",
63
- "husky": "^8.0.3",
64
- "jest": "^29.5.0",
65
- "jest-snapshot-serializer-ansi": "^1.0.0",
66
- "prettier": "^2.8.4"
53
+ "consolemock": "1.1.0",
54
+ "eslint": "8.46.0",
55
+ "eslint-config-prettier": "9.0.0",
56
+ "eslint-plugin-import": "2.28.0",
57
+ "eslint-plugin-node": "11.1.0",
58
+ "eslint-plugin-prettier": "5.0.0",
59
+ "husky": "8.0.3",
60
+ "jest": "29.6.2",
61
+ "jest-snapshot-serializer-ansi": "2.1.0",
62
+ "prettier": "3.0.1"
67
63
  },
68
64
  "keywords": [
69
65
  "lint",