lint-staged 11.1.1 → 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
@@ -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
 
@@ -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')
@@ -13,9 +12,7 @@ const {
13
12
  ConfigNotFoundError,
14
13
  GetBackupStashError,
15
14
  GitError,
16
- InvalidOptionsError,
17
15
  } = require('./symbols')
18
- const formatConfig = require('./formatConfig')
19
16
  const validateConfig = require('./validateConfig')
20
17
  const validateOptions = require('./validateOptions')
21
18
 
@@ -85,105 +82,78 @@ const lintStaged = async (
85
82
  } = {},
86
83
  logger = console
87
84
  ) => {
88
- try {
89
- await validateOptions({ shell }, logger)
85
+ await validateOptions({ shell }, logger)
90
86
 
91
- debugLog('Loading config using `cosmiconfig`')
87
+ debugLog('Loading config using `cosmiconfig`')
92
88
 
93
- const resolved = configObject
94
- ? { config: configObject, filepath: '(input)' }
95
- : await loadConfig(configPath)
89
+ const resolved = configObject
90
+ ? { config: configObject, filepath: '(input)' }
91
+ : await loadConfig(configPath)
96
92
 
97
- if (resolved == null) {
98
- throw ConfigNotFoundError
99
- }
93
+ if (resolved == null) {
94
+ logger.error(`${ConfigNotFoundError.message}.`)
95
+ throw ConfigNotFoundError
96
+ }
100
97
 
101
- debugLog('Successfully loaded config from `%s`:\n%O', resolved.filepath, resolved.config)
102
-
103
- // resolved.config is the parsed configuration object
104
- // resolved.filepath is the path to the config file that was found
105
- const formattedConfig = formatConfig(resolved.config)
106
- const config = validateConfig(formattedConfig)
107
-
108
- if (debug) {
109
- // Log using logger to be able to test through `consolemock`.
110
- logger.log('Running lint-staged with the following config:')
111
- logger.log(stringifyObject(config, { indent: ' ' }))
112
- } else {
113
- // We might not be in debug mode but `DEBUG=lint-staged*` could have
114
- // been set.
115
- debugLog('lint-staged config:\n%O', config)
116
- }
98
+ debugLog('Successfully loaded config from `%s`:\n%O', resolved.filepath, resolved.config)
117
99
 
118
- // Unset GIT_LITERAL_PATHSPECS to not mess with path interpretation
119
- debugLog('Unset GIT_LITERAL_PATHSPECS (was `%s`)', process.env.GIT_LITERAL_PATHSPECS)
120
- delete process.env.GIT_LITERAL_PATHSPECS
121
-
122
- try {
123
- const ctx = await runAll(
124
- {
125
- allowEmpty,
126
- concurrent,
127
- config,
128
- cwd,
129
- debug,
130
- maxArgLength,
131
- quiet,
132
- relative,
133
- shell,
134
- stash,
135
- verbose,
136
- },
137
- logger
138
- )
139
- debugLog('Tasks were executed successfully!')
140
- printTaskOutput(ctx, logger)
141
- return true
142
- } catch (runAllError) {
143
- if (runAllError && runAllError.ctx && runAllError.ctx.errors) {
144
- const { ctx } = runAllError
145
- if (ctx.errors.has(ApplyEmptyCommitError)) {
146
- logger.warn(PREVENTED_EMPTY_COMMIT)
147
- } else if (ctx.errors.has(GitError) && !ctx.errors.has(GetBackupStashError)) {
148
- logger.error(GIT_ERROR)
149
- if (ctx.shouldBackup) {
150
- // No sense to show this if the backup stash itself is missing.
151
- logger.error(RESTORE_STASH_EXAMPLE)
152
- }
153
- }
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)
154
103
 
155
- printTaskOutput(ctx, logger)
156
- return false
157
- }
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
+ }
158
113
 
159
- // Probably a compilation error in the config js file. Pass it up to the outer error handler for logging.
160
- throw runAllError
161
- }
162
- } catch (lintStagedError) {
163
- /** throw early because `validateOptions` options contains own logging */
164
- if (lintStagedError === InvalidOptionsError) {
165
- throw InvalidOptionsError
166
- }
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
167
117
 
168
- /** @todo move logging to `validateConfig` and remove this try/catch block */
169
- if (lintStagedError === ConfigNotFoundError) {
170
- logger.error(`${lintStagedError.message}.`)
171
- } else {
172
- // It was probably a parsing error
173
- logger.error(dedent`
174
- Could not parse lint-staged config.
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
+ }
149
+ }
175
150
 
176
- ${lintStagedError}
177
- `)
151
+ printTaskOutput(ctx, logger)
152
+ return false
178
153
  }
179
- logger.error() // empty line
180
- // Print helpful message for all errors
181
- logger.error(dedent`
182
- Please make sure you have created it correctly.
183
- See https://github.com/okonet/lint-staged#configuration.
184
- `)
185
-
186
- 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
187
157
  }
188
158
  }
189
159
 
@@ -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.`
@@ -29,7 +44,7 @@ const GIT_ERROR = `\n ${error} ${chalk.red(`lint-staged failed due to a git err
29
44
 
30
45
  const invalidOption = (name, value, message) => `${chalk.redBright(`${error} Validation Error:`)}
31
46
 
32
- Invalid value for option ${chalk.bold(name)}: ${chalk.bold(value)}
47
+ Invalid value for option '${chalk.bold(name)}': ${chalk.bold(value)}
33
48
 
34
49
  ${message}
35
50
 
@@ -51,9 +66,11 @@ const CONFIG_STDIN_ERROR = 'Error: Could not read config from stdin.'
51
66
 
52
67
  module.exports = {
53
68
  CONFIG_STDIN_ERROR,
69
+ configurationError,
54
70
  DEPRECATED_GIT_ADD,
55
71
  FAILED_GET_STAGED_FILES,
56
72
  GIT_ERROR,
73
+ incorrectBraces,
57
74
  invalidOption,
58
75
  NO_STAGED_FILES,
59
76
  NO_TASKS,
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lint-staged",
3
- "version": "11.1.1",
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
- }