lint-staged 11.0.0 → 11.1.2

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
@@ -25,7 +25,7 @@ This project contains a script that will run arbitrary shell tasks with a list o
25
25
  The fastest way to start using lint-staged is to run the following command in your terminal:
26
26
 
27
27
  ```bash
28
- npx mrm lint-staged
28
+ npx mrm@2 lint-staged
29
29
  ```
30
30
 
31
31
  This command will install and configure [husky](https://github.com/typicode/husky) and lint-staged depending on the code quality tools from your project's `package.json` dependencies, so please make sure you install (`npm install --save-dev`) and configure all code quality tools like [Prettier](https://prettier.io) and [ESLint](https://eslint.org) prior to that.
@@ -71,7 +71,7 @@ Options:
71
71
  tasks serially (default: true)
72
72
  -q, --quiet disable lint-staged’s own console output (default: false)
73
73
  -r, --relative pass relative filepaths to tasks (default: false)
74
- -x, --shell skip parsing of tasks for better shell support (default:
74
+ -x, --shell [path] skip parsing of tasks for better shell support (default:
75
75
  false)
76
76
  -v, --verbose show task output even when tasks succeed; by default only
77
77
  failed output is shown (default: false)
@@ -90,7 +90,7 @@ Options:
90
90
  - **`--no-stash`**: By default a backup stash will be created before running the tasks, and all task modifications will be reverted in case of an error. This option will disable creating the stash, and instead leave all modifications in the index when aborting the commit.
91
91
  - **`--quiet`**: Supress all CLI output, except from tasks.
92
92
  - **`--relative`**: Pass filepaths relative to `process.cwd()` (where `lint-staged` runs) to tasks. Default is `false`.
93
- - **`--shell`**: By default linter commands will be parsed for speed and security. This has the side-effect that regular shell scripts might not work as expected. You can skip parsing of commands with this option.
93
+ - **`--shell`**: By default linter commands will be parsed for speed and security. This has the side-effect that regular shell scripts might not work as expected. You can skip parsing of commands with this option. To use a specific shell, use a path like `--shell "/bin/bash"`.
94
94
  - **`--verbose`**: Show task output even when tasks succeed. By default only failed output is shown.
95
95
 
96
96
  ## Configuration
@@ -445,36 +445,14 @@ For example, here is `jest` running on all `.js` files with the `NODE_ENV` varia
445
445
 
446
446
  </details>
447
447
 
448
- ### Automatically fix code style with `prettier` for any format prettier supports
448
+ ### Automatically fix code style with `prettier` for any format Prettier supports
449
449
 
450
450
  <details>
451
451
  <summary>Click to expand</summary>
452
452
 
453
- You need to add `micromatch` as a dev dependency.
454
-
455
- ```sh
456
- $ npm i --save-dev micromatch
457
- ```
458
-
459
- ```js
460
- // lint-staged.config.js
461
- const micromatch = require('micromatch')
462
- const prettier = require('prettier')
463
-
464
- const prettierSupportedExtensions = prettier
465
- .getSupportInfo()
466
- .languages.map(({ extensions }) => extensions)
467
- .flat()
468
- const addQuotes = (a) => `"${a}"`
469
-
470
- module.exports = (allStagedFiles) => {
471
- const prettierFiles = micromatch(
472
- allStagedFiles,
473
- prettierSupportedExtensions.map((extension) => `**/*${extension}`)
474
- )
475
- return prettierFiles.length > 0
476
- ? [`prettier --write ${prettierFiles.map(addQuotes).join(' ')}`]
477
- : []
453
+ ```json
454
+ {
455
+ "*": "prettier --ignore-unknown --write"
478
456
  }
479
457
  ```
480
458
 
@@ -42,7 +42,7 @@ cmdline
42
42
  )
43
43
  .option('-q, --quiet', 'disable lint-staged’s own console output', false)
44
44
  .option('-r, --relative', 'pass relative filepaths to tasks', false)
45
- .option('-x, --shell', 'skip parsing of tasks for better shell support', false)
45
+ .option('-x, --shell [path]', 'skip parsing of tasks for better shell support', false)
46
46
  .option(
47
47
  '-v, --verbose',
48
48
  'show task output even when tasks succeed; by default only failed output is shown',
@@ -85,7 +85,7 @@ const options = {
85
85
  stash: !!cmdlineOptions.stash, // commander inverts `no-<x>` flags to `!x`
86
86
  quiet: !!cmdlineOptions.quiet,
87
87
  relative: !!cmdlineOptions.relative,
88
- shell: !!cmdlineOptions.shell,
88
+ shell: cmdlineOptions.shell /* Either a boolean or a string pointing to the shell */,
89
89
  verbose: !!cmdlineOptions.verbose,
90
90
  }
91
91
 
@@ -16,39 +16,35 @@ const debug = require('debug')('lint-staged:gen-tasks')
16
16
  * @param {boolean} [options.files] - Staged filepaths
17
17
  * @param {boolean} [options.relative] - Whether filepaths to should be relative to gitDir
18
18
  */
19
- module.exports = function generateTasks({
20
- config,
21
- cwd = process.cwd(),
22
- gitDir,
23
- files,
24
- relative = false,
25
- }) {
19
+ const generateTasks = ({ config, cwd = process.cwd(), gitDir, files, relative = false }) => {
26
20
  debug('Generating linter tasks')
27
21
 
28
22
  const absoluteFiles = files.map((file) => normalize(path.resolve(gitDir, file)))
29
23
  const relativeFiles = absoluteFiles.map((file) => normalize(path.relative(cwd, file)))
30
24
 
31
- return Object.entries(config).map(([pattern, commands]) => {
25
+ return Object.entries(config).map(([rawPattern, commands]) => {
26
+ let pattern = rawPattern
27
+
32
28
  const isParentDirPattern = pattern.startsWith('../')
33
29
 
34
- const fileList = micromatch(
35
- relativeFiles
36
- // Only worry about children of the CWD unless the pattern explicitly
37
- // specifies that it concerns a parent directory.
38
- .filter((file) => {
39
- if (isParentDirPattern) return true
40
- return !file.startsWith('..') && !path.isAbsolute(file)
41
- }),
42
- pattern,
43
- {
44
- cwd,
45
- dot: true,
46
- // If pattern doesn't look like a path, enable `matchBase` to
47
- // match against filenames in every directory. This makes `*.js`
48
- // match both `test.js` and `subdirectory/test.js`.
49
- matchBase: !pattern.includes('/'),
50
- }
51
- ).map((file) => normalize(relative ? file : path.resolve(cwd, file)))
30
+ // Only worry about children of the CWD unless the pattern explicitly
31
+ // specifies that it concerns a parent directory.
32
+ const filteredFiles = relativeFiles.filter((file) => {
33
+ if (isParentDirPattern) return true
34
+ return !file.startsWith('..') && !path.isAbsolute(file)
35
+ })
36
+
37
+ const matches = micromatch(filteredFiles, pattern, {
38
+ cwd,
39
+ dot: true,
40
+ // If the pattern doesn't look like a path, enable `matchBase` to
41
+ // match against filenames in every directory. This makes `*.js`
42
+ // match both `test.js` and `subdirectory/test.js`.
43
+ matchBase: !pattern.includes('/'),
44
+ strictBrackets: true,
45
+ })
46
+
47
+ const fileList = matches.map((file) => normalize(relative ? file : path.resolve(cwd, file)))
52
48
 
53
49
  const task = { pattern, commands, fileList }
54
50
  debug('Generated task: \n%O', task)
@@ -56,3 +52,5 @@ module.exports = function generateTasks({
56
52
  return task
57
53
  })
58
54
  }
55
+
56
+ module.exports = generateTasks
package/lib/index.js CHANGED
@@ -1,6 +1,5 @@
1
1
  'use strict'
2
2
 
3
- const dedent = require('dedent')
4
3
  const { cosmiconfig } = require('cosmiconfig')
5
4
  const debugLog = require('debug')('lint-staged')
6
5
  const stringifyObject = require('stringify-object')
@@ -8,13 +7,16 @@ const stringifyObject = require('stringify-object')
8
7
  const { PREVENTED_EMPTY_COMMIT, GIT_ERROR, RESTORE_STASH_EXAMPLE } = require('./messages')
9
8
  const printTaskOutput = require('./printTaskOutput')
10
9
  const runAll = require('./runAll')
11
- const { ApplyEmptyCommitError, GetBackupStashError, GitError } = require('./symbols')
12
- const formatConfig = require('./formatConfig')
10
+ const {
11
+ ApplyEmptyCommitError,
12
+ ConfigNotFoundError,
13
+ GetBackupStashError,
14
+ GitError,
15
+ } = require('./symbols')
13
16
  const validateConfig = require('./validateConfig')
17
+ const validateOptions = require('./validateOptions')
14
18
 
15
- const errConfigNotFound = new Error('Config could not be found')
16
-
17
- function resolveConfig(configPath) {
19
+ const resolveConfig = (configPath) => {
18
20
  try {
19
21
  return require.resolve(configPath)
20
22
  } catch {
@@ -22,7 +24,7 @@ function resolveConfig(configPath) {
22
24
  }
23
25
  }
24
26
 
25
- function loadConfig(configPath) {
27
+ const loadConfig = (configPath) => {
26
28
  const explorer = cosmiconfig('lint-staged', {
27
29
  searchPlaces: [
28
30
  'package.json',
@@ -56,14 +58,14 @@ function loadConfig(configPath) {
56
58
  * @param {number} [options.maxArgLength] - Maximum argument string length
57
59
  * @param {boolean} [options.quiet] - Disable lint-staged’s own console output
58
60
  * @param {boolean} [options.relative] - Pass relative filepaths to tasks
59
- * @param {boolean} [options.shell] - Skip parsing of tasks for better shell support
61
+ * @param {boolean|string} [options.shell] - Skip parsing of tasks for better shell support
60
62
  * @param {boolean} [options.stash] - Enable the backup stash, and revert in case of errors
61
63
  * @param {boolean} [options.verbose] - Show task output even when tasks succeed; by default only failed output is shown
62
64
  * @param {Logger} [logger]
63
65
  *
64
66
  * @returns {Promise<boolean>} Promise of whether the linting passed or failed
65
67
  */
66
- module.exports = async function lintStaged(
68
+ const lintStaged = async (
67
69
  {
68
70
  allowEmpty = false,
69
71
  concurrent = true,
@@ -79,92 +81,80 @@ module.exports = async function lintStaged(
79
81
  verbose = false,
80
82
  } = {},
81
83
  logger = console
82
- ) {
83
- try {
84
- debugLog('Loading config using `cosmiconfig`')
85
-
86
- const resolved = configObject
87
- ? { config: configObject, filepath: '(input)' }
88
- : await loadConfig(configPath)
89
- if (resolved == null) throw errConfigNotFound
90
-
91
- debugLog('Successfully loaded config from `%s`:\n%O', resolved.filepath, resolved.config)
92
- // resolved.config is the parsed configuration object
93
- // resolved.filepath is the path to the config file that was found
94
- const formattedConfig = formatConfig(resolved.config)
95
- const config = validateConfig(formattedConfig)
96
- if (debug) {
97
- // Log using logger to be able to test through `consolemock`.
98
- logger.log('Running lint-staged with the following config:')
99
- logger.log(stringifyObject(config, { indent: ' ' }))
100
- } else {
101
- // We might not be in debug mode but `DEBUG=lint-staged*` could have
102
- // been set.
103
- debugLog('lint-staged config:\n%O', config)
104
- }
84
+ ) => {
85
+ await validateOptions({ shell }, logger)
105
86
 
106
- // Unset GIT_LITERAL_PATHSPECS to not mess with path interpretation
107
- debugLog('Unset GIT_LITERAL_PATHSPECS (was `%s`)', process.env.GIT_LITERAL_PATHSPECS)
108
- delete process.env.GIT_LITERAL_PATHSPECS
109
-
110
- try {
111
- const ctx = await runAll(
112
- {
113
- allowEmpty,
114
- concurrent,
115
- config,
116
- cwd,
117
- debug,
118
- maxArgLength,
119
- quiet,
120
- relative,
121
- shell,
122
- stash,
123
- verbose,
124
- },
125
- logger
126
- )
127
- debugLog('Tasks were executed successfully!')
128
- printTaskOutput(ctx, logger)
129
- return true
130
- } catch (runAllError) {
131
- if (runAllError && runAllError.ctx && runAllError.ctx.errors) {
132
- const { ctx } = runAllError
133
- if (ctx.errors.has(ApplyEmptyCommitError)) {
134
- logger.warn(PREVENTED_EMPTY_COMMIT)
135
- } else if (ctx.errors.has(GitError) && !ctx.errors.has(GetBackupStashError)) {
136
- logger.error(GIT_ERROR)
137
- if (ctx.shouldBackup) {
138
- // No sense to show this if the backup stash itself is missing.
139
- logger.error(RESTORE_STASH_EXAMPLE)
140
- }
141
- }
87
+ debugLog('Loading config using `cosmiconfig`')
88
+
89
+ const resolved = configObject
90
+ ? { config: configObject, filepath: '(input)' }
91
+ : await loadConfig(configPath)
92
+
93
+ if (resolved == null) {
94
+ logger.error(`${ConfigNotFoundError.message}.`)
95
+ throw ConfigNotFoundError
96
+ }
97
+
98
+ debugLog('Successfully loaded config from `%s`:\n%O', resolved.filepath, resolved.config)
99
+
100
+ // resolved.config is the parsed configuration object
101
+ // resolved.filepath is the path to the config file that was found
102
+ const config = validateConfig(resolved.config, logger)
142
103
 
143
- printTaskOutput(ctx, logger)
144
- return false
104
+ if (debug) {
105
+ // Log using logger to be able to test through `consolemock`.
106
+ logger.log('Running lint-staged with the following config:')
107
+ logger.log(stringifyObject(config, { indent: ' ' }))
108
+ } else {
109
+ // We might not be in debug mode but `DEBUG=lint-staged*` could have
110
+ // been set.
111
+ debugLog('lint-staged config:\n%O', config)
112
+ }
113
+
114
+ // Unset GIT_LITERAL_PATHSPECS to not mess with path interpretation
115
+ debugLog('Unset GIT_LITERAL_PATHSPECS (was `%s`)', process.env.GIT_LITERAL_PATHSPECS)
116
+ delete process.env.GIT_LITERAL_PATHSPECS
117
+
118
+ try {
119
+ const ctx = await runAll(
120
+ {
121
+ allowEmpty,
122
+ concurrent,
123
+ config,
124
+ cwd,
125
+ debug,
126
+ maxArgLength,
127
+ quiet,
128
+ relative,
129
+ shell,
130
+ stash,
131
+ verbose,
132
+ },
133
+ logger
134
+ )
135
+ debugLog('Tasks were executed successfully!')
136
+ printTaskOutput(ctx, logger)
137
+ return true
138
+ } catch (runAllError) {
139
+ if (runAllError && runAllError.ctx && runAllError.ctx.errors) {
140
+ const { ctx } = runAllError
141
+ if (ctx.errors.has(ApplyEmptyCommitError)) {
142
+ logger.warn(PREVENTED_EMPTY_COMMIT)
143
+ } else if (ctx.errors.has(GitError) && !ctx.errors.has(GetBackupStashError)) {
144
+ logger.error(GIT_ERROR)
145
+ if (ctx.shouldBackup) {
146
+ // No sense to show this if the backup stash itself is missing.
147
+ logger.error(RESTORE_STASH_EXAMPLE)
148
+ }
145
149
  }
146
150
 
147
- // Probably a compilation error in the config js file. Pass it up to the outer error handler for logging.
148
- throw runAllError
149
- }
150
- } catch (lintStagedError) {
151
- if (lintStagedError === errConfigNotFound) {
152
- logger.error(`${lintStagedError.message}.`)
153
- } else {
154
- // It was probably a parsing error
155
- logger.error(dedent`
156
- Could not parse lint-staged config.
157
-
158
- ${lintStagedError}
159
- `)
151
+ printTaskOutput(ctx, logger)
152
+ return false
160
153
  }
161
- logger.error() // empty line
162
- // Print helpful message for all errors
163
- logger.error(dedent`
164
- Please make sure you have created it correctly.
165
- See https://github.com/okonet/lint-staged#configuration.
166
- `)
167
-
168
- throw lintStagedError
154
+
155
+ // Probably a compilation error in the config js file. Pass it up to the outer error handler for logging.
156
+ throw runAllError
169
157
  }
170
158
  }
159
+
160
+ module.exports = lintStaged
@@ -3,8 +3,8 @@
3
3
  const cliTruncate = require('cli-truncate')
4
4
  const debug = require('debug')('lint-staged:make-cmd-tasks')
5
5
 
6
+ const { configurationError } = require('./messages')
6
7
  const resolveTaskFn = require('./resolveTaskFn')
7
- const { createError } = require('./validateConfig')
8
8
 
9
9
  const STDOUT_COLUMNS_DEFAULT = 80
10
10
 
@@ -51,7 +51,7 @@ const makeCmdTasks = async ({ commands, files, gitDir, renderer, shell, verbose
51
51
  // Do the validation here instead of `validateConfig` to skip evaluating the function multiple times
52
52
  if (isFn && typeof command !== 'string') {
53
53
  throw new Error(
54
- createError(
54
+ configurationError(
55
55
  '[Function]',
56
56
  'Function task should return a string or an array of strings',
57
57
  resolved
package/lib/messages.js CHANGED
@@ -2,11 +2,26 @@
2
2
 
3
3
  const chalk = require('chalk')
4
4
  const { error, info, warning } = require('log-symbols')
5
+ const format = require('stringify-object')
6
+
7
+ const configurationError = (opt, helpMsg, value) =>
8
+ `${chalk.redBright(`${error} Validation Error:`)}
9
+
10
+ Invalid value for '${chalk.bold(opt)}': ${chalk.bold(
11
+ format(value, { inlineCharacterLimit: Number.POSITIVE_INFINITY })
12
+ )}
13
+
14
+ ${helpMsg}`
5
15
 
6
16
  const NOT_GIT_REPO = chalk.redBright(`${error} Current directory is not a git directory!`)
7
17
 
8
18
  const FAILED_GET_STAGED_FILES = chalk.redBright(`${error} Failed to get staged files!`)
9
19
 
20
+ const incorrectBraces = (before, after) => `${warning} ${chalk.yellow(
21
+ `Detected incorrect braces with only single value: \`${before}\`. Reformatted as: \`${after}\``
22
+ )}
23
+ `
24
+
10
25
  const NO_STAGED_FILES = `${info} No staged files found.`
11
26
 
12
27
  const NO_TASKS = `${info} No staged files match any configured task.`
@@ -27,6 +42,14 @@ const SKIPPED_GIT_ERROR = 'Skipped because of previous git error.'
27
42
 
28
43
  const GIT_ERROR = `\n ${error} ${chalk.red(`lint-staged failed due to a git error.`)}`
29
44
 
45
+ const invalidOption = (name, value, message) => `${chalk.redBright(`${error} Validation Error:`)}
46
+
47
+ Invalid value for option '${chalk.bold(name)}': ${chalk.bold(value)}
48
+
49
+ ${message}
50
+
51
+ See https://github.com/okonet/lint-staged#command-line-flags`
52
+
30
53
  const PREVENTED_EMPTY_COMMIT = `
31
54
  ${warning} ${chalk.yellow(`lint-staged prevented an empty git commit.
32
55
  Use the --allow-empty option to continue, or check your task configuration`)}
@@ -42,16 +65,19 @@ const RESTORE_STASH_EXAMPLE = ` Any lost modifications can be restored from a g
42
65
  const CONFIG_STDIN_ERROR = 'Error: Could not read config from stdin.'
43
66
 
44
67
  module.exports = {
45
- NOT_GIT_REPO,
68
+ CONFIG_STDIN_ERROR,
69
+ configurationError,
70
+ DEPRECATED_GIT_ADD,
46
71
  FAILED_GET_STAGED_FILES,
72
+ GIT_ERROR,
73
+ incorrectBraces,
74
+ invalidOption,
47
75
  NO_STAGED_FILES,
48
76
  NO_TASKS,
49
- skippingBackup,
50
- DEPRECATED_GIT_ADD,
51
- TASK_ERROR,
52
- SKIPPED_GIT_ERROR,
53
- GIT_ERROR,
77
+ NOT_GIT_REPO,
54
78
  PREVENTED_EMPTY_COMMIT,
55
79
  RESTORE_STASH_EXAMPLE,
56
- CONFIG_STDIN_ERROR,
80
+ SKIPPED_GIT_ERROR,
81
+ skippingBackup,
82
+ TASK_ERROR,
57
83
  }
package/lib/runAll.js CHANGED
@@ -147,7 +147,8 @@ const runAll = async (
147
147
  matchedFiles.add(file)
148
148
  })
149
149
 
150
- hasDeprecatedGitAdd = subTasks.some((subTask) => subTask.command === 'git add')
150
+ hasDeprecatedGitAdd =
151
+ hasDeprecatedGitAdd || subTasks.some((subTask) => subTask.command === 'git add')
151
152
 
152
153
  chunkListrTasks.push({
153
154
  title: `Running tasks for ${task.pattern}`,
package/lib/symbols.js CHANGED
@@ -1,11 +1,13 @@
1
1
  'use strict'
2
2
 
3
3
  const ApplyEmptyCommitError = Symbol('ApplyEmptyCommitError')
4
+ const ConfigNotFoundError = new Error('Config could not be found')
4
5
  const GetBackupStashError = Symbol('GetBackupStashError')
5
6
  const GetStagedFilesError = Symbol('GetStagedFilesError')
6
7
  const GitError = Symbol('GitError')
7
8
  const GitRepoError = Symbol('GitRepoError')
8
9
  const HideUnstagedChangesError = Symbol('HideUnstagedChangesError')
10
+ const InvalidOptionsError = new Error('Invalid Options')
9
11
  const RestoreMergeStatusError = Symbol('RestoreMergeStatusError')
10
12
  const RestoreOriginalStateError = Symbol('RestoreOriginalStateError')
11
13
  const RestoreUnstagedChangesError = Symbol('RestoreUnstagedChangesError')
@@ -13,10 +15,12 @@ const TaskError = Symbol('TaskError')
13
15
 
14
16
  module.exports = {
15
17
  ApplyEmptyCommitError,
18
+ ConfigNotFoundError,
16
19
  GetBackupStashError,
17
20
  GetStagedFilesError,
18
21
  GitError,
19
22
  GitRepoError,
23
+ InvalidOptionsError,
20
24
  HideUnstagedChangesError,
21
25
  RestoreMergeStatusError,
22
26
  RestoreOriginalStateError,
@@ -0,0 +1,71 @@
1
+ const { incorrectBraces } = require('./messages')
2
+
3
+ /**
4
+ * A correctly-formed brace expansion must contain unquoted opening and closing braces,
5
+ * and at least one unquoted comma or a valid sequence expression.
6
+ * Any incorrectly formed brace expansion is left unchanged.
7
+ *
8
+ * @see https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html
9
+ *
10
+ * Lint-staged uses `micromatch` for brace expansion, and its behavior is to treat
11
+ * invalid brace expansions as literal strings, which means they (typically) do not match
12
+ * anything.
13
+ *
14
+ * This RegExp tries to match most cases of invalid brace expansions, so that they can be
15
+ * detected, warned about, and re-formatted by removing the braces and thus hopefully
16
+ * matching the files as intended by the user. The only real fix is to remove the incorrect
17
+ * braces from user configuration, but this is left to the user (after seeing the warning).
18
+ *
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
22
+ * - file_{1..10}.css // expanded as file_1.css, file_2.css, …, file_10.css
23
+ *
24
+ * @example <caption>Globs with incorrect brace expansions</caption>
25
+ * - *.{js} // should just be *.js
26
+ * - *.{js,{ts}} // should just be *.{js,ts}
27
+ * - *.\{js\} // escaped braces, so they're treated literally
28
+ * - *.${js} // dollar-sign inhibits expansion, so treated literally
29
+ * - *.{js\,ts} // the comma is escaped, so treated literally
30
+ */
31
+ const BRACES_REGEXP = /(?<![\\$])({)(?:(?!(?<!\\),|\.\.|\{|\}).)*?(?<!\\)(})/g
32
+
33
+ /**
34
+ * @param {string} pattern
35
+ * @returns {string}
36
+ */
37
+ const withoutIncorrectBraces = (pattern) => {
38
+ let output = `${pattern}`
39
+ let match = null
40
+
41
+ while ((match = BRACES_REGEXP.exec(pattern))) {
42
+ const fullMatch = match[0]
43
+ const withoutBraces = fullMatch.replace(/{/, '').replace(/}/, '')
44
+ output = output.replace(fullMatch, withoutBraces)
45
+ }
46
+
47
+ return output
48
+ }
49
+
50
+ /**
51
+ * Validate and remove incorrect brace expansions from glob pattern.
52
+ * For example `*.{js}` is incorrect because it doesn't contain a `,` or `..`,
53
+ * and will be reformatted as `*.js`.
54
+ *
55
+ * @param {string} pattern the glob pattern
56
+ * @param {*} logger
57
+ * @returns {string}
58
+ */
59
+ const validateBraces = (pattern, logger) => {
60
+ const fixedPattern = withoutIncorrectBraces(pattern)
61
+
62
+ if (fixedPattern !== pattern) {
63
+ logger.warn(incorrectBraces(pattern, fixedPattern))
64
+ }
65
+
66
+ return fixedPattern
67
+ }
68
+
69
+ module.exports = validateBraces
70
+
71
+ module.exports.BRACES_REGEXP = BRACES_REGEXP
@@ -2,11 +2,11 @@
2
2
 
3
3
  'use strict'
4
4
 
5
- const chalk = require('chalk')
6
- const format = require('stringify-object')
7
-
8
5
  const debug = require('debug')('lint-staged:cfg')
9
6
 
7
+ const { configurationError } = require('./messages')
8
+ const validateBraces = require('./validateBraces')
9
+
10
10
  const TEST_DEPRECATED_KEYS = new Map([
11
11
  ['concurrent', (key) => typeof key === 'boolean'],
12
12
  ['chunkSize', (key) => typeof key === 'number'],
@@ -18,76 +18,91 @@ const TEST_DEPRECATED_KEYS = new Map([
18
18
  ['relative', (key) => typeof key === 'boolean'],
19
19
  ])
20
20
 
21
- const formatError = (helpMsg) => `● Validation Error:
22
-
23
- ${helpMsg}
24
-
25
- Please refer to https://github.com/okonet/lint-staged#configuration for more information...`
26
-
27
- const createError = (opt, helpMsg, value) =>
28
- formatError(`Invalid value for '${chalk.bold(opt)}'.
29
-
30
- ${helpMsg}.
31
-
32
- Configured value is: ${chalk.bold(
33
- format(value, { inlineCharacterLimit: Number.POSITIVE_INFINITY })
34
- )}`)
35
-
36
21
  /**
37
22
  * Runs config validation. Throws error if the config is not valid.
38
23
  * @param config {Object}
39
24
  * @returns config {Object}
40
25
  */
41
- module.exports = function validateConfig(config) {
26
+ const validateConfig = (config, logger) => {
42
27
  debug('Validating config')
43
28
 
44
- const errors = []
29
+ if (!config || (typeof config !== 'object' && typeof config !== 'function')) {
30
+ throw new Error('Configuration should be an object or a function!')
31
+ }
45
32
 
46
- if (!config || typeof config !== 'object') {
47
- errors.push('Configuration should be an object!')
48
- } else {
49
- const entries = Object.entries(config)
33
+ /**
34
+ * Function configurations receive all staged files as their argument.
35
+ * They are not further validated here to make sure the function gets
36
+ * evaluated only once.
37
+ *
38
+ * @see makeCmdTasks
39
+ */
40
+ if (typeof config === 'function') {
41
+ return { '*': config }
42
+ }
50
43
 
51
- if (entries.length === 0) {
52
- errors.push('Configuration should not be empty!')
53
- }
44
+ if (Object.entries(config).length === 0) {
45
+ throw new Error('Configuration should not be empty!')
46
+ }
54
47
 
55
- entries.forEach(([pattern, task]) => {
56
- if (TEST_DEPRECATED_KEYS.has(pattern)) {
57
- const testFn = TEST_DEPRECATED_KEYS.get(pattern)
58
- if (testFn(task)) {
59
- errors.push(
60
- createError(
61
- pattern,
62
- 'Advanced configuration has been deprecated. For more info, please visit: https://github.com/okonet/lint-staged',
63
- task
64
- )
65
- )
66
- }
67
- }
48
+ const errors = []
68
49
 
69
- if (
70
- (!Array.isArray(task) ||
71
- task.some((item) => typeof item !== 'string' && typeof item !== 'function')) &&
72
- typeof task !== 'string' &&
73
- typeof task !== 'function'
74
- ) {
50
+ /**
51
+ * Create a new validated config because the keys (patterns) might change.
52
+ * Since the Object.reduce method already loops through each entry in the config,
53
+ * it can be used for validating the values at the same time.
54
+ */
55
+ const validatedConfig = Object.entries(config).reduce((collection, [pattern, task]) => {
56
+ /** Versions < 9 had more complex configuration options that are no longer supported. */
57
+ if (TEST_DEPRECATED_KEYS.has(pattern)) {
58
+ const testFn = TEST_DEPRECATED_KEYS.get(pattern)
59
+ if (testFn(task)) {
75
60
  errors.push(
76
- createError(
77
- pattern,
78
- 'Should be a string, a function, or an array of strings and functions',
79
- task
80
- )
61
+ configurationError(pattern, 'Advanced configuration has been deprecated.', task)
81
62
  )
82
63
  }
83
- })
84
- }
64
+
65
+ /** Return early for deprecated keys to skip validating their (deprecated) values */
66
+ return collection
67
+ }
68
+
69
+ if (
70
+ (!Array.isArray(task) ||
71
+ task.some((item) => typeof item !== 'string' && typeof item !== 'function')) &&
72
+ typeof task !== 'string' &&
73
+ typeof task !== 'function'
74
+ ) {
75
+ errors.push(
76
+ configurationError(
77
+ pattern,
78
+ 'Should be a string, a function, or an array of strings and functions.',
79
+ task
80
+ )
81
+ )
82
+ }
83
+
84
+ /**
85
+ * A typical configuration error is using invalid brace expansion, like `*.{js}`.
86
+ * These are automatically fixed and warned about.
87
+ */
88
+ const fixedPattern = validateBraces(pattern, logger)
89
+
90
+ return { ...collection, [fixedPattern]: task }
91
+ }, {})
85
92
 
86
93
  if (errors.length) {
87
- throw new Error(errors.join('\n'))
94
+ const message = errors.join('\n\n')
95
+
96
+ logger.error(`Could not parse lint-staged config.
97
+
98
+ ${message}
99
+
100
+ See https://github.com/okonet/lint-staged#configuration.`)
101
+
102
+ throw new Error(message)
88
103
  }
89
104
 
90
- return config
105
+ return validatedConfig
91
106
  }
92
107
 
93
- module.exports.createError = createError
108
+ module.exports = validateConfig
@@ -0,0 +1,31 @@
1
+ const { promises: fs, constants } = require('fs')
2
+
3
+ const { invalidOption } = require('./messages')
4
+ const { InvalidOptionsError } = require('./symbols')
5
+
6
+ const debug = require('debug')('lint-staged:options')
7
+
8
+ /**
9
+ * Validate lint-staged options, either from the Node.js API or the command line flags.
10
+ * @param {*} options
11
+ * @param {boolean|string} [options.shell] - Skip parsing of tasks for better shell support
12
+ *
13
+ * @throws {InvalidOptionsError}
14
+ */
15
+ const validateOptions = async (options = {}, logger) => {
16
+ debug('Validating options...')
17
+
18
+ /** Ensure the passed shell option is executable */
19
+ if (typeof options.shell === 'string') {
20
+ try {
21
+ await fs.access(options.shell, constants.X_OK)
22
+ } catch (error) {
23
+ logger.error(invalidOption('shell', options.shell, error.message))
24
+ throw InvalidOptionsError
25
+ }
26
+ }
27
+
28
+ debug('Validated options!')
29
+ }
30
+
31
+ module.exports = validateOptions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lint-staged",
3
- "version": "11.0.0",
3
+ "version": "11.1.2",
4
4
  "description": "Lint files staged by git",
5
5
  "license": "MIT",
6
6
  "repository": "https://github.com/okonet/lint-staged",
@@ -32,7 +32,6 @@
32
32
  "commander": "^7.2.0",
33
33
  "cosmiconfig": "^7.0.0",
34
34
  "debug": "^4.3.1",
35
- "dedent": "^0.7.0",
36
35
  "enquirer": "^2.3.6",
37
36
  "execa": "^5.0.0",
38
37
  "listr2": "^3.8.2",
@@ -1,7 +0,0 @@
1
- module.exports = function formatConfig(config) {
2
- if (typeof config === 'function') {
3
- return { '*': config }
4
- }
5
-
6
- return config
7
- }