diff --git a/lib/formatConfig.js b/lib/formatConfig.js
deleted file mode 100644
index 376e67a8d..000000000
--- a/lib/formatConfig.js
+++ /dev/null
@@ -1,7 +0,0 @@
-module.exports = function formatConfig(config) {
- if (typeof config === 'function') {
- return { '*': config }
- }
-
- return config
-}
diff --git a/lib/generateTasks.js b/lib/generateTasks.js
index acce66364..f5c8d6cf4 100644
--- a/lib/generateTasks.js
+++ b/lib/generateTasks.js
@@ -16,39 +16,35 @@ const debug = require('debug')('lint-staged:gen-tasks')
* @param {boolean} [options.files] - Staged filepaths
* @param {boolean} [options.relative] - Whether filepaths to should be relative to gitDir
*/
-module.exports = function generateTasks({
- config,
- cwd = process.cwd(),
- gitDir,
- files,
- relative = false,
-}) {
+const generateTasks = ({ config, cwd = process.cwd(), gitDir, files, relative = false }) => {
debug('Generating linter tasks')
const absoluteFiles = files.map((file) => normalize(path.resolve(gitDir, file)))
const relativeFiles = absoluteFiles.map((file) => normalize(path.relative(cwd, file)))
- return Object.entries(config).map(([pattern, commands]) => {
+ return Object.entries(config).map(([rawPattern, commands]) => {
+ let pattern = rawPattern
+
const isParentDirPattern = pattern.startsWith('../')
- const fileList = micromatch(
- relativeFiles
- // Only worry about children of the CWD unless the pattern explicitly
- // specifies that it concerns a parent directory.
- .filter((file) => {
- if (isParentDirPattern) return true
- return !file.startsWith('..') && !path.isAbsolute(file)
- }),
- pattern,
- {
- cwd,
- dot: true,
- // If pattern doesn't look like a path, enable `matchBase` to
- // match against filenames in every directory. This makes `*.js`
- // match both `test.js` and `subdirectory/test.js`.
- matchBase: !pattern.includes('/'),
- }
- ).map((file) => normalize(relative ? file : path.resolve(cwd, file)))
+ // Only worry about children of the CWD unless the pattern explicitly
+ // specifies that it concerns a parent directory.
+ const filteredFiles = relativeFiles.filter((file) => {
+ if (isParentDirPattern) return true
+ return !file.startsWith('..') && !path.isAbsolute(file)
+ })
+
+ const matches = micromatch(filteredFiles, pattern, {
+ cwd,
+ dot: true,
+ // If the pattern doesn't look like a path, enable `matchBase` to
+ // match against filenames in every directory. This makes `*.js`
+ // match both `test.js` and `subdirectory/test.js`.
+ matchBase: !pattern.includes('/'),
+ strictBrackets: true,
+ })
+
+ const fileList = matches.map((file) => normalize(relative ? file : path.resolve(cwd, file)))
const task = { pattern, commands, fileList }
debug('Generated task: \n%O', task)
@@ -56,3 +52,5 @@ module.exports = function generateTasks({
return task
})
}
+
+module.exports = generateTasks
diff --git a/lib/index.js b/lib/index.js
index a1907eff5..12ae1a19f 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -1,6 +1,5 @@
'use strict'
-const dedent = require('dedent')
const { cosmiconfig } = require('cosmiconfig')
const debugLog = require('debug')('lint-staged')
const stringifyObject = require('stringify-object')
@@ -13,9 +12,7 @@ const {
ConfigNotFoundError,
GetBackupStashError,
GitError,
- InvalidOptionsError,
} = require('./symbols')
-const formatConfig = require('./formatConfig')
const validateConfig = require('./validateConfig')
const validateOptions = require('./validateOptions')
@@ -85,105 +82,78 @@ const lintStaged = async (
} = {},
logger = console
) => {
- try {
- await validateOptions({ shell }, logger)
+ await validateOptions({ shell }, logger)
- debugLog('Loading config using `cosmiconfig`')
+ debugLog('Loading config using `cosmiconfig`')
- const resolved = configObject
- ? { config: configObject, filepath: '(input)' }
- : await loadConfig(configPath)
+ const resolved = configObject
+ ? { config: configObject, filepath: '(input)' }
+ : await loadConfig(configPath)
- if (resolved == null) {
- throw ConfigNotFoundError
- }
+ if (resolved == null) {
+ logger.error(`${ConfigNotFoundError.message}.`)
+ throw ConfigNotFoundError
+ }
- debugLog('Successfully loaded config from `%s`:\n%O', resolved.filepath, resolved.config)
-
- // resolved.config is the parsed configuration object
- // resolved.filepath is the path to the config file that was found
- const formattedConfig = formatConfig(resolved.config)
- const config = validateConfig(formattedConfig)
-
- if (debug) {
- // Log using logger to be able to test through `consolemock`.
- logger.log('Running lint-staged with the following config:')
- logger.log(stringifyObject(config, { indent: ' ' }))
- } else {
- // We might not be in debug mode but `DEBUG=lint-staged*` could have
- // been set.
- debugLog('lint-staged config:\n%O', config)
- }
+ debugLog('Successfully loaded config from `%s`:\n%O', resolved.filepath, resolved.config)
- // Unset GIT_LITERAL_PATHSPECS to not mess with path interpretation
- debugLog('Unset GIT_LITERAL_PATHSPECS (was `%s`)', process.env.GIT_LITERAL_PATHSPECS)
- delete process.env.GIT_LITERAL_PATHSPECS
-
- try {
- const ctx = await runAll(
- {
- allowEmpty,
- concurrent,
- config,
- cwd,
- debug,
- maxArgLength,
- quiet,
- relative,
- shell,
- stash,
- verbose,
- },
- logger
- )
- debugLog('Tasks were executed successfully!')
- printTaskOutput(ctx, logger)
- return true
- } catch (runAllError) {
- if (runAllError && runAllError.ctx && runAllError.ctx.errors) {
- const { ctx } = runAllError
- if (ctx.errors.has(ApplyEmptyCommitError)) {
- logger.warn(PREVENTED_EMPTY_COMMIT)
- } else if (ctx.errors.has(GitError) && !ctx.errors.has(GetBackupStashError)) {
- logger.error(GIT_ERROR)
- if (ctx.shouldBackup) {
- // No sense to show this if the backup stash itself is missing.
- logger.error(RESTORE_STASH_EXAMPLE)
- }
- }
+ // resolved.config is the parsed configuration object
+ // resolved.filepath is the path to the config file that was found
+ const config = validateConfig(resolved.config, logger)
- printTaskOutput(ctx, logger)
- return false
- }
+ if (debug) {
+ // Log using logger to be able to test through `consolemock`.
+ logger.log('Running lint-staged with the following config:')
+ logger.log(stringifyObject(config, { indent: ' ' }))
+ } else {
+ // We might not be in debug mode but `DEBUG=lint-staged*` could have
+ // been set.
+ debugLog('lint-staged config:\n%O', config)
+ }
- // Probably a compilation error in the config js file. Pass it up to the outer error handler for logging.
- throw runAllError
- }
- } catch (lintStagedError) {
- /** throw early because `validateOptions` options contains own logging */
- if (lintStagedError === InvalidOptionsError) {
- throw InvalidOptionsError
- }
+ // Unset GIT_LITERAL_PATHSPECS to not mess with path interpretation
+ debugLog('Unset GIT_LITERAL_PATHSPECS (was `%s`)', process.env.GIT_LITERAL_PATHSPECS)
+ delete process.env.GIT_LITERAL_PATHSPECS
- /** @todo move logging to `validateConfig` and remove this try/catch block */
- if (lintStagedError === ConfigNotFoundError) {
- logger.error(`${lintStagedError.message}.`)
- } else {
- // It was probably a parsing error
- logger.error(dedent`
- Could not parse lint-staged config.
+ try {
+ const ctx = await runAll(
+ {
+ allowEmpty,
+ concurrent,
+ config,
+ cwd,
+ debug,
+ maxArgLength,
+ quiet,
+ relative,
+ shell,
+ stash,
+ verbose,
+ },
+ logger
+ )
+ debugLog('Tasks were executed successfully!')
+ printTaskOutput(ctx, logger)
+ return true
+ } catch (runAllError) {
+ if (runAllError && runAllError.ctx && runAllError.ctx.errors) {
+ const { ctx } = runAllError
+ if (ctx.errors.has(ApplyEmptyCommitError)) {
+ logger.warn(PREVENTED_EMPTY_COMMIT)
+ } else if (ctx.errors.has(GitError) && !ctx.errors.has(GetBackupStashError)) {
+ logger.error(GIT_ERROR)
+ if (ctx.shouldBackup) {
+ // No sense to show this if the backup stash itself is missing.
+ logger.error(RESTORE_STASH_EXAMPLE)
+ }
+ }
- ${lintStagedError}
- `)
+ printTaskOutput(ctx, logger)
+ return false
}
- logger.error() // empty line
- // Print helpful message for all errors
- logger.error(dedent`
- Please make sure you have created it correctly.
- See https://github.com/okonet/lint-staged#configuration.
- `)
-
- throw lintStagedError
+
+ // Probably a compilation error in the config js file. Pass it up to the outer error handler for logging.
+ throw runAllError
}
}
diff --git a/lib/makeCmdTasks.js b/lib/makeCmdTasks.js
index 1316906a6..d2706a978 100644
--- a/lib/makeCmdTasks.js
+++ b/lib/makeCmdTasks.js
@@ -3,8 +3,8 @@
const cliTruncate = require('cli-truncate')
const debug = require('debug')('lint-staged:make-cmd-tasks')
+const { configurationError } = require('./messages')
const resolveTaskFn = require('./resolveTaskFn')
-const { createError } = require('./validateConfig')
const STDOUT_COLUMNS_DEFAULT = 80
@@ -51,7 +51,7 @@ const makeCmdTasks = async ({ commands, files, gitDir, renderer, shell, verbose
// Do the validation here instead of `validateConfig` to skip evaluating the function multiple times
if (isFn && typeof command !== 'string') {
throw new Error(
- createError(
+ configurationError(
'[Function]',
'Function task should return a string or an array of strings',
resolved
diff --git a/lib/messages.js b/lib/messages.js
index aa5b5b265..3a4f56d96 100644
--- a/lib/messages.js
+++ b/lib/messages.js
@@ -2,11 +2,26 @@
const chalk = require('chalk')
const { error, info, warning } = require('log-symbols')
+const format = require('stringify-object')
+
+const configurationError = (opt, helpMsg, value) =>
+ `${chalk.redBright(`${error} Validation Error:`)}
+
+ Invalid value for '${chalk.bold(opt)}': ${chalk.bold(
+ format(value, { inlineCharacterLimit: Number.POSITIVE_INFINITY })
+ )}
+
+ ${helpMsg}`
const NOT_GIT_REPO = chalk.redBright(`${error} Current directory is not a git directory!`)
const FAILED_GET_STAGED_FILES = chalk.redBright(`${error} Failed to get staged files!`)
+const incorrectBraces = (before, after) => `${warning} ${chalk.yellow(
+ `Detected incorrect braces with only single value: \`${before}\`. Reformatted as: \`${after}\``
+)}
+`
+
const NO_STAGED_FILES = `${info} No staged files found.`
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
const invalidOption = (name, value, message) => `${chalk.redBright(`${error} Validation Error:`)}
- Invalid value for option ${chalk.bold(name)}: ${chalk.bold(value)}
+ Invalid value for option '${chalk.bold(name)}': ${chalk.bold(value)}
${message}
@@ -51,9 +66,11 @@ const CONFIG_STDIN_ERROR = 'Error: Could not read config from stdin.'
module.exports = {
CONFIG_STDIN_ERROR,
+ configurationError,
DEPRECATED_GIT_ADD,
FAILED_GET_STAGED_FILES,
GIT_ERROR,
+ incorrectBraces,
invalidOption,
NO_STAGED_FILES,
NO_TASKS,
diff --git a/lib/validateBraces.js b/lib/validateBraces.js
new file mode 100644
index 000000000..4d1e4868f
--- /dev/null
+++ b/lib/validateBraces.js
@@ -0,0 +1,71 @@
+const { incorrectBraces } = require('./messages')
+
+/**
+ * A correctly-formed brace expansion must contain unquoted opening and closing braces,
+ * and at least one unquoted comma or a valid sequence expression.
+ * Any incorrectly formed brace expansion is left unchanged.
+ *
+ * @see https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html
+ *
+ * Lint-staged uses `micromatch` for brace expansion, and its behavior is to treat
+ * invalid brace expansions as literal strings, which means they (typically) do not match
+ * anything.
+ *
+ * This RegExp tries to match most cases of invalid brace expansions, so that they can be
+ * detected, warned about, and re-formatted by removing the braces and thus hopefully
+ * matching the files as intended by the user. The only real fix is to remove the incorrect
+ * braces from user configuration, but this is left to the user (after seeing the warning).
+ *
+ * @example
Globs with brace expansions
+ * - *.{js,tx} // expanded as *.js, *.ts
+ * - *.{{j,t}s,css} // expanded as *.js, *.ts, *.css
+ * - file_{1..10}.css // expanded as file_1.css, file_2.css, …, file_10.css
+ *
+ * @example Globs with incorrect brace expansions
+ * - *.{js} // should just be *.js
+ * - *.{js,{ts}} // should just be *.{js,ts}
+ * - *.\{js\} // escaped braces, so they're treated literally
+ * - *.${js} // dollar-sign inhibits expansion, so treated literally
+ * - *.{js\,ts} // the comma is escaped, so treated literally
+ */
+const BRACES_REGEXP = /(? {
+ let output = `${pattern}`
+ let match = null
+
+ while ((match = BRACES_REGEXP.exec(pattern))) {
+ const fullMatch = match[0]
+ const withoutBraces = fullMatch.replace(/{/, '').replace(/}/, '')
+ output = output.replace(fullMatch, withoutBraces)
+ }
+
+ return output
+}
+
+/**
+ * Validate and remove incorrect brace expansions from glob pattern.
+ * For example `*.{js}` is incorrect because it doesn't contain a `,` or `..`,
+ * and will be reformatted as `*.js`.
+ *
+ * @param {string} pattern the glob pattern
+ * @param {*} logger
+ * @returns {string}
+ */
+const validateBraces = (pattern, logger) => {
+ const fixedPattern = withoutIncorrectBraces(pattern)
+
+ if (fixedPattern !== pattern) {
+ logger.warn(incorrectBraces(pattern, fixedPattern))
+ }
+
+ return fixedPattern
+}
+
+module.exports = validateBraces
+
+module.exports.BRACES_REGEXP = BRACES_REGEXP
diff --git a/lib/validateConfig.js b/lib/validateConfig.js
index 0e34ef930..a5c46facb 100644
--- a/lib/validateConfig.js
+++ b/lib/validateConfig.js
@@ -2,11 +2,11 @@
'use strict'
-const chalk = require('chalk')
-const format = require('stringify-object')
-
const debug = require('debug')('lint-staged:cfg')
+const { configurationError } = require('./messages')
+const validateBraces = require('./validateBraces')
+
const TEST_DEPRECATED_KEYS = new Map([
['concurrent', (key) => typeof key === 'boolean'],
['chunkSize', (key) => typeof key === 'number'],
@@ -18,76 +18,91 @@ const TEST_DEPRECATED_KEYS = new Map([
['relative', (key) => typeof key === 'boolean'],
])
-const formatError = (helpMsg) => `● Validation Error:
-
- ${helpMsg}
-
-Please refer to https://github.com/okonet/lint-staged#configuration for more information...`
-
-const createError = (opt, helpMsg, value) =>
- formatError(`Invalid value for '${chalk.bold(opt)}'.
-
- ${helpMsg}.
-
- Configured value is: ${chalk.bold(
- format(value, { inlineCharacterLimit: Number.POSITIVE_INFINITY })
- )}`)
-
/**
* Runs config validation. Throws error if the config is not valid.
* @param config {Object}
* @returns config {Object}
*/
-module.exports = function validateConfig(config) {
+const validateConfig = (config, logger) => {
debug('Validating config')
- const errors = []
+ if (!config || (typeof config !== 'object' && typeof config !== 'function')) {
+ throw new Error('Configuration should be an object or a function!')
+ }
- if (!config || typeof config !== 'object') {
- errors.push('Configuration should be an object!')
- } else {
- const entries = Object.entries(config)
+ /**
+ * Function configurations receive all staged files as their argument.
+ * They are not further validated here to make sure the function gets
+ * evaluated only once.
+ *
+ * @see makeCmdTasks
+ */
+ if (typeof config === 'function') {
+ return { '*': config }
+ }
- if (entries.length === 0) {
- errors.push('Configuration should not be empty!')
- }
+ if (Object.entries(config).length === 0) {
+ throw new Error('Configuration should not be empty!')
+ }
- entries.forEach(([pattern, task]) => {
- if (TEST_DEPRECATED_KEYS.has(pattern)) {
- const testFn = TEST_DEPRECATED_KEYS.get(pattern)
- if (testFn(task)) {
- errors.push(
- createError(
- pattern,
- 'Advanced configuration has been deprecated. For more info, please visit: https://github.com/okonet/lint-staged',
- task
- )
- )
- }
- }
+ const errors = []
- if (
- (!Array.isArray(task) ||
- task.some((item) => typeof item !== 'string' && typeof item !== 'function')) &&
- typeof task !== 'string' &&
- typeof task !== 'function'
- ) {
+ /**
+ * Create a new validated config because the keys (patterns) might change.
+ * Since the Object.reduce method already loops through each entry in the config,
+ * it can be used for validating the values at the same time.
+ */
+ const validatedConfig = Object.entries(config).reduce((collection, [pattern, task]) => {
+ /** Versions < 9 had more complex configuration options that are no longer supported. */
+ if (TEST_DEPRECATED_KEYS.has(pattern)) {
+ const testFn = TEST_DEPRECATED_KEYS.get(pattern)
+ if (testFn(task)) {
errors.push(
- createError(
- pattern,
- 'Should be a string, a function, or an array of strings and functions',
- task
- )
+ configurationError(pattern, 'Advanced configuration has been deprecated.', task)
)
}
- })
- }
+
+ /** Return early for deprecated keys to skip validating their (deprecated) values */
+ return collection
+ }
+
+ if (
+ (!Array.isArray(task) ||
+ task.some((item) => typeof item !== 'string' && typeof item !== 'function')) &&
+ typeof task !== 'string' &&
+ typeof task !== 'function'
+ ) {
+ errors.push(
+ configurationError(
+ pattern,
+ 'Should be a string, a function, or an array of strings and functions.',
+ task
+ )
+ )
+ }
+
+ /**
+ * A typical configuration error is using invalid brace expansion, like `*.{js}`.
+ * These are automatically fixed and warned about.
+ */
+ const fixedPattern = validateBraces(pattern, logger)
+
+ return { ...collection, [fixedPattern]: task }
+ }, {})
if (errors.length) {
- throw new Error(errors.join('\n'))
+ const message = errors.join('\n\n')
+
+ logger.error(`Could not parse lint-staged config.
+
+${message}
+
+See https://github.com/okonet/lint-staged#configuration.`)
+
+ throw new Error(message)
}
- return config
+ return validatedConfig
}
-module.exports.createError = createError
+module.exports = validateConfig
diff --git a/package.json b/package.json
index dba20462d..d90e45abc 100644
--- a/package.json
+++ b/package.json
@@ -32,7 +32,6 @@
"commander": "^7.2.0",
"cosmiconfig": "^7.0.0",
"debug": "^4.3.1",
- "dedent": "^0.7.0",
"enquirer": "^2.3.6",
"execa": "^5.0.0",
"listr2": "^3.8.2",
diff --git a/test/__snapshots__/index2.spec.js.snap b/test/__snapshots__/index2.spec.js.snap
deleted file mode 100644
index 0a25f136a..000000000
--- a/test/__snapshots__/index2.spec.js.snap
+++ /dev/null
@@ -1,11 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`lintStaged should catch errors from js function config 2`] = `
-"
-ERROR Could not parse lint-staged config.
-
-Error: failed config
-ERROR
-ERROR Please make sure you have created it correctly.
-See https://github.com/okonet/lint-staged#configuration."
-`;
diff --git a/test/__snapshots__/validateConfig.spec.js.snap b/test/__snapshots__/validateConfig.spec.js.snap
index c58ec222b..50d62fe66 100644
--- a/test/__snapshots__/validateConfig.spec.js.snap
+++ b/test/__snapshots__/validateConfig.spec.js.snap
@@ -1,154 +1,116 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`validateConfig should not throw and should print nothing for function config 1`] = `""`;
+exports[`validateConfig should throw and should print validation errors for invalid config 1`] = `
+"× Validation Error:
-exports[`validateConfig should not throw and should print nothing for function task 1`] = `""`;
+ Invalid value for 'foo': false
-exports[`validateConfig should not throw and should print nothing for valid config 1`] = `""`;
+ Should be a string, a function, or an array of strings and functions."
+`;
-exports[`validateConfig should not throw when config contains deprecated key but with valid task 1`] = `""`;
+exports[`validateConfig should throw and should print validation errors for invalid config 1 1`] = `"Configuration should be an object or a function!"`;
-exports[`validateConfig should throw and should print validation errors for invalid config 1`] = `
-"● Validation Error:
+exports[`validateConfig should throw when detecting deprecated advanced configuration 1`] = `
+"× Validation Error:
- Invalid value for 'foo'.
+ Invalid value for 'chunkSize': 10
- Should be a string, a function, or an array of strings and functions.
-
- Configured value is: false
+ Advanced configuration has been deprecated.
-Please refer to https://github.com/okonet/lint-staged#configuration for more information..."
-`;
+× Validation Error:
-exports[`validateConfig should throw and should print validation errors for invalid config 1 1`] = `"Configuration should be an object!"`;
+ Invalid value for 'concurrent': false
-exports[`validateConfig should throw when detecting deprecated advanced configuration 1`] = `
-"● Validation Error:
+ Advanced configuration has been deprecated.
- Invalid value for 'chunkSize'.
+× Validation Error:
- Advanced configuration has been deprecated. For more info, please visit: https://github.com/okonet/lint-staged.
-
- Configured value is: 10
+ Invalid value for 'globOptions': {matchBase: false}
-Please refer to https://github.com/okonet/lint-staged#configuration for more information...
-● Validation Error:
+ Advanced configuration has been deprecated.
- Invalid value for 'chunkSize'.
+× Validation Error:
- Should be a string, a function, or an array of strings and functions.
-
- Configured value is: 10
+ Invalid value for 'ignore': ['test.js']
-Please refer to https://github.com/okonet/lint-staged#configuration for more information...
-● Validation Error:
+ Advanced configuration has been deprecated.
- Invalid value for 'concurrent'.
+× Validation Error:
- Advanced configuration has been deprecated. For more info, please visit: https://github.com/okonet/lint-staged.
-
- Configured value is: false
+ Invalid value for 'linters': {'*.js': ['eslint']}
-Please refer to https://github.com/okonet/lint-staged#configuration for more information...
-● Validation Error:
+ Advanced configuration has been deprecated.
- Invalid value for 'concurrent'.
+× Validation Error:
- Should be a string, a function, or an array of strings and functions.
-
- Configured value is: false
+ Invalid value for 'relative': true
-Please refer to https://github.com/okonet/lint-staged#configuration for more information...
-● Validation Error:
+ Advanced configuration has been deprecated.
- Invalid value for 'globOptions'.
+× Validation Error:
- Advanced configuration has been deprecated. For more info, please visit: https://github.com/okonet/lint-staged.
-
- Configured value is: {matchBase: false}
+ Invalid value for 'renderer': 'silent'
-Please refer to https://github.com/okonet/lint-staged#configuration for more information...
-● Validation Error:
+ Advanced configuration has been deprecated.
- Invalid value for 'globOptions'.
+× Validation Error:
- Should be a string, a function, or an array of strings and functions.
-
- Configured value is: {matchBase: false}
+ Invalid value for 'subTaskConcurrency': 10
-Please refer to https://github.com/okonet/lint-staged#configuration for more information...
-● Validation Error:
+ Advanced configuration has been deprecated."
+`;
- Invalid value for 'ignore'.
+exports[`validateConfig should throw when detecting deprecated advanced configuration 2`] = `
+"
+ERROR Could not parse lint-staged config.
- Advanced configuration has been deprecated. For more info, please visit: https://github.com/okonet/lint-staged.
-
- Configured value is: ['test.js']
+× Validation Error:
-Please refer to https://github.com/okonet/lint-staged#configuration for more information...
-● Validation Error:
+ Invalid value for 'chunkSize': 10
- Invalid value for 'linters'.
+ Advanced configuration has been deprecated.
- Advanced configuration has been deprecated. For more info, please visit: https://github.com/okonet/lint-staged.
-
- Configured value is: {'*.js': ['eslint']}
+× Validation Error:
-Please refer to https://github.com/okonet/lint-staged#configuration for more information...
-● Validation Error:
+ Invalid value for 'concurrent': false
- Invalid value for 'linters'.
+ Advanced configuration has been deprecated.
- Should be a string, a function, or an array of strings and functions.
-
- Configured value is: {'*.js': ['eslint']}
+× Validation Error:
-Please refer to https://github.com/okonet/lint-staged#configuration for more information...
-● Validation Error:
+ Invalid value for 'globOptions': {matchBase: false}
- Invalid value for 'relative'.
+ Advanced configuration has been deprecated.
- Advanced configuration has been deprecated. For more info, please visit: https://github.com/okonet/lint-staged.
-
- Configured value is: true
+× Validation Error:
-Please refer to https://github.com/okonet/lint-staged#configuration for more information...
-● Validation Error:
+ Invalid value for 'ignore': ['test.js']
- Invalid value for 'relative'.
+ Advanced configuration has been deprecated.
- Should be a string, a function, or an array of strings and functions.
-
- Configured value is: true
+× Validation Error:
-Please refer to https://github.com/okonet/lint-staged#configuration for more information...
-● Validation Error:
+ Invalid value for 'linters': {'*.js': ['eslint']}
- Invalid value for 'renderer'.
+ Advanced configuration has been deprecated.
- Advanced configuration has been deprecated. For more info, please visit: https://github.com/okonet/lint-staged.
-
- Configured value is: 'silent'
+× Validation Error:
-Please refer to https://github.com/okonet/lint-staged#configuration for more information...
-● Validation Error:
+ Invalid value for 'relative': true
- Invalid value for 'subTaskConcurrency'.
+ Advanced configuration has been deprecated.
- Advanced configuration has been deprecated. For more info, please visit: https://github.com/okonet/lint-staged.
-
- Configured value is: 10
+× Validation Error:
-Please refer to https://github.com/okonet/lint-staged#configuration for more information...
-● Validation Error:
+ Invalid value for 'renderer': 'silent'
- Invalid value for 'subTaskConcurrency'.
+ Advanced configuration has been deprecated.
- Should be a string, a function, or an array of strings and functions.
-
- Configured value is: 10
+× Validation Error:
-Please refer to https://github.com/okonet/lint-staged#configuration for more information..."
-`;
+ Invalid value for 'subTaskConcurrency': 10
-exports[`validateConfig should throw when detecting deprecated advanced configuration 2`] = `""`;
+ Advanced configuration has been deprecated.
+
+See https://github.com/okonet/lint-staged#configuration."
+`;
diff --git a/test/formatConfig.spec.js b/test/formatConfig.spec.js
deleted file mode 100644
index 903ce56d2..000000000
--- a/test/formatConfig.spec.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import formatConfig from '../lib/formatConfig'
-
-describe('formatConfig', () => {
- it('Object config should return as is', () => {
- const simpleConfig = {
- '*.js': ['eslint --fix', 'git add'],
- }
- expect(formatConfig(simpleConfig)).toEqual(simpleConfig)
- })
-
- it('Function config should be converted to object', () => {
- const functionConfig = (stagedFiles) => [`eslint --fix ${stagedFiles}', 'git add`]
- expect(formatConfig(functionConfig)).toEqual({
- '*': functionConfig,
- })
- })
-})
diff --git a/test/generateTasks.spec.js b/test/generateTasks.spec.js
index d2f9540f0..fd068dad2 100644
--- a/test/generateTasks.spec.js
+++ b/test/generateTasks.spec.js
@@ -1,5 +1,5 @@
-import os from 'os'
import normalize from 'normalize-path'
+import os from 'os'
import path from 'path'
import generateTasks from '../lib/generateTasks'
@@ -15,7 +15,7 @@ const files = [
'.hidden/test.js',
'test.css',
- 'deeper/test.css',
+ 'deeper/test1.css',
'deeper/test2.css',
'even/deeper/test.css',
'.hidden/test.css',
@@ -145,7 +145,7 @@ describe('generateTasks', () => {
`${gitDir}/even/deeper/test.js`,
`${gitDir}/.hidden/test.js`,
`${gitDir}/test.css`,
- `${gitDir}/deeper/test.css`,
+ `${gitDir}/deeper/test1.css`,
`${gitDir}/deeper/test2.css`,
`${gitDir}/even/deeper/test.css`,
`${gitDir}/.hidden/test.css`,
@@ -153,6 +153,25 @@ describe('generateTasks', () => {
})
})
+ it('should match pattern "test{1..2}.css"', async () => {
+ const result = await generateTasks({
+ config: {
+ 'test{1..2}.css': 'lint',
+ },
+ cwd,
+ gitDir,
+ files,
+ })
+
+ const linter = result.find((item) => item.pattern === 'test{1..2}.css')
+
+ expect(linter).toEqual({
+ pattern: 'test{1..2}.css',
+ commands: 'lint',
+ fileList: [`${gitDir}/deeper/test1.css`, `${gitDir}/deeper/test2.css`].map(normalizePath),
+ })
+ })
+
it('should not match files in parent directory by default', async () => {
const result = await generateTasks({
config,
@@ -196,7 +215,7 @@ describe('generateTasks', () => {
'even/deeper/test.js',
'.hidden/test.js',
'test.css',
- 'deeper/test.css',
+ 'deeper/test1.css',
'deeper/test2.css',
'even/deeper/test.css',
'.hidden/test.css',
diff --git a/test/index.spec.js b/test/index.spec.js
index 6a98a629a..047e528e3 100644
--- a/test/index.spec.js
+++ b/test/index.spec.js
@@ -70,15 +70,7 @@ describe('lintStaged', () => {
`[Error: Configuration should not be empty!]`
)
- expect(mockedConsole.printHistory()).toMatchInlineSnapshot(`
- "
- ERROR Could not parse lint-staged config.
-
- Error: Configuration should not be empty!
- ERROR
- ERROR Please make sure you have created it correctly.
- See https://github.com/okonet/lint-staged#configuration."
- `)
+ expect(mockedConsole.printHistory()).toMatchInlineSnapshot(`""`)
console = previousConsole
})
@@ -133,15 +125,7 @@ describe('lintStaged', () => {
`[Error: Configuration should not be empty!]`
)
- expect(logger.printHistory()).toMatchInlineSnapshot(`
- "
- ERROR Could not parse lint-staged config.
-
- Error: Configuration should not be empty!
- ERROR
- ERROR Please make sure you have created it correctly.
- See https://github.com/okonet/lint-staged#configuration."
- `)
+ expect(logger.printHistory()).toMatchInlineSnapshot(`""`)
})
it('should load config file when specified', async () => {
@@ -224,10 +208,7 @@ describe('lintStaged', () => {
expect(logger.printHistory()).toMatchInlineSnapshot(`
"
- ERROR Config could not be found.
- ERROR
- ERROR Please make sure you have created it correctly.
- See https://github.com/okonet/lint-staged#configuration."
+ ERROR Config could not be found."
`)
})
@@ -239,8 +220,8 @@ describe('lintStaged', () => {
// Serialize Windows, Linux and MacOS paths consistently
expect.addSnapshotSerializer(
replaceSerializer(
- /Error: ENOENT: no such file or directory, open '([^']+)'/,
- `Error: ENOENT: no such file or directory, open '${nonExistentConfig}'`
+ /ENOENT: no such file or directory, open '([^']+)'/,
+ `ENOENT: no such file or directory, open '${nonExistentConfig}'`
)
)
@@ -248,14 +229,6 @@ describe('lintStaged', () => {
lintStaged({ configPath: nonExistentConfig, quiet: true }, logger)
).rejects.toThrowError()
- expect(logger.printHistory()).toMatchInlineSnapshot(`
-
- ERROR Could not parse lint-staged config.
-
- Error: ENOENT: no such file or directory, open 'fake-config-file.yml'
- ERROR
- ERROR Please make sure you have created it correctly.
- See https://github.com/okonet/lint-staged#configuration.
- `)
+ expect(logger.printHistory()).toMatchInlineSnapshot(`""`)
})
})
diff --git a/test/index2.spec.js b/test/index2.spec.js
index bdeafcbfe..625623a6b 100644
--- a/test/index2.spec.js
+++ b/test/index2.spec.js
@@ -77,6 +77,7 @@ describe('lintStaged', () => {
await expect(lintStaged({ config }, logger)).rejects.toThrowErrorMatchingInlineSnapshot(
`"failed config"`
)
- expect(logger.printHistory()).toMatchSnapshot()
+
+ expect(logger.printHistory()).toMatchInlineSnapshot(`""`)
})
})
diff --git a/test/makeCmdTasks.spec.js b/test/makeCmdTasks.spec.js
index bdb7a1522..d215c224c 100644
--- a/test/makeCmdTasks.spec.js
+++ b/test/makeCmdTasks.spec.js
@@ -109,16 +109,12 @@ describe('makeCmdTasks', () => {
it("should throw when function task doesn't return string | string[]", async () => {
await expect(makeCmdTasks({ commands: () => null, gitDir, files: ['test.js'] })).rejects
.toThrowErrorMatchingInlineSnapshot(`
-"● Validation Error:
+ "× Validation Error:
- Invalid value for '[Function]'.
+ Invalid value for '[Function]': null
- Function task should return a string or an array of strings.
-
- Configured value is: null
-
-Please refer to https://github.com/okonet/lint-staged#configuration for more information..."
-`)
+ Function task should return a string or an array of strings"
+ `)
})
it('should truncate task title', async () => {
diff --git a/test/validateBraces.spec.js b/test/validateBraces.spec.js
new file mode 100644
index 000000000..11168abdb
--- /dev/null
+++ b/test/validateBraces.spec.js
@@ -0,0 +1,124 @@
+import makeConsoleMock from 'consolemock'
+
+import validateBraces, { BRACES_REGEXP } from '../lib/validateBraces'
+
+describe('BRACES_REGEXP', () => {
+ it(`should match '*.{js}'`, () => {
+ expect('*.{js}'.match(BRACES_REGEXP)).toBeTruthy()
+ })
+
+ it(`should match 'file_{10}'`, () => {
+ expect('file_{test}'.match(BRACES_REGEXP)).toBeTruthy()
+ })
+
+ it(`should match '*.{spec\\.js}'`, () => {
+ expect('*.{spec\\.js}'.match(BRACES_REGEXP)).toBeTruthy()
+ })
+
+ it(`should match '*.{js\\,ts}'`, () => {
+ expect('*.{js\\,ts}'.match(BRACES_REGEXP)).toBeTruthy()
+ })
+
+ it("should not match '*.${js}'", () => {
+ expect('*.${js}'.match(BRACES_REGEXP)).not.toBeTruthy()
+ })
+
+ it(`should not match '.{js,ts}'`, () => {
+ expect('.{js,ts}'.match(BRACES_REGEXP)).not.toBeTruthy()
+ })
+
+ it(`should not match 'file_{1..10}'`, () => {
+ expect('file_{1..10}'.match(BRACES_REGEXP)).not.toBeTruthy()
+ })
+
+ it(`should not match '*.\\{js\\}'`, () => {
+ expect('*.\\{js\\}'.match(BRACES_REGEXP)).not.toBeTruthy()
+ })
+
+ it(`should not match '*.\\{js}'`, () => {
+ expect('*.\\{js}'.match(BRACES_REGEXP)).not.toBeTruthy()
+ })
+
+ it(`should not match '*.{js\\}'`, () => {
+ expect('*.{js\\}'.match(BRACES_REGEXP)).not.toBeTruthy()
+ })
+})
+
+describe('validateBraces', () => {
+ it('should warn about `*.{js}` and return fixed pattern', () => {
+ const logger = makeConsoleMock()
+
+ const fixedBraces = validateBraces('*.{js}', logger)
+
+ expect(fixedBraces).toEqual('*.js')
+ expect(logger.printHistory()).toMatchInlineSnapshot(`
+ "
+ WARN ‼ Detected incorrect braces with only single value: \`*.{js}\`. Reformatted as: \`*.js\`
+ "
+ `)
+ })
+
+ it('should warn about `*.{ts}{x}` and return fixed pattern', () => {
+ const logger = makeConsoleMock()
+
+ const fixedBraces = validateBraces('*.{ts}{x}', logger)
+
+ expect(fixedBraces).toEqual('*.tsx')
+ expect(logger.printHistory()).toMatchInlineSnapshot(`
+ "
+ WARN ‼ Detected incorrect braces with only single value: \`*.{ts}{x}\`. Reformatted as: \`*.tsx\`
+ "
+ `)
+ })
+
+ it('should warn about `*.{js,{ts}}` and return fixed pattern', () => {
+ const logger = makeConsoleMock()
+
+ const fixedBraces = validateBraces('*.{js,{ts}}', logger)
+
+ expect(fixedBraces).toEqual('*.{js,ts}')
+ expect(logger.printHistory()).toMatchInlineSnapshot(`
+ "
+ WARN ‼ Detected incorrect braces with only single value: \`*.{js,{ts}}\`. Reformatted as: \`*.{js,ts}\`
+ "
+ `)
+ })
+
+ /**
+ * @todo This isn't correctly detected even though the outer braces are invalid.
+ */
+ it.skip('should warn about `*.{{js,ts}}` and return fixed pattern', () => {
+ const logger = makeConsoleMock()
+
+ const fixedBraces = validateBraces('*.{{js,ts}}', logger)
+
+ expect(fixedBraces).toEqual('*.{js,ts}')
+ expect(logger.printHistory()).toMatchInlineSnapshot(`
+ "
+ WARN ‼ Detected incorrect braces with only single value: \`*.{{js,ts}}\`. Reformatted as: \`*.{js,ts}\`
+ "
+ `)
+ })
+
+ it('should warn about `*.{{js,ts},{css}}` and return fixed pattern', () => {
+ const logger = makeConsoleMock()
+
+ const fixedBraces = validateBraces('*.{{js,ts},{css}}', logger)
+
+ expect(fixedBraces).toEqual('*.{{js,ts},css}')
+ expect(logger.printHistory()).toMatchInlineSnapshot(`
+ "
+ WARN ‼ Detected incorrect braces with only single value: \`*.{{js,ts},{css}}\`. Reformatted as: \`*.{{js,ts},css}\`
+ "
+ `)
+ })
+
+ it('should not warn about `*.\\{js\\}` and return the same pattern', () => {
+ const logger = makeConsoleMock()
+
+ const fixedBraces = validateBraces('*.\\{js\\}', logger)
+
+ expect(fixedBraces).toEqual('*.\\{js\\}')
+ expect(logger.printHistory()).toMatchInlineSnapshot(`""`)
+ })
+})
diff --git a/test/validateConfig.spec.js b/test/validateConfig.spec.js
index bf6d4a0f2..66376e9d6 100644
--- a/test/validateConfig.spec.js
+++ b/test/validateConfig.spec.js
@@ -2,59 +2,56 @@ import makeConsoleMock from 'consolemock'
import validateConfig from '../lib/validateConfig'
-import formatConfig from '../lib/formatConfig'
-
describe('validateConfig', () => {
- const originalConsole = global.console
- beforeAll(() => {
- global.console = makeConsoleMock()
- })
+ let logger
beforeEach(() => {
- global.console.clearHistory()
- })
-
- afterAll(() => {
- global.console = originalConsole
+ logger = makeConsoleMock()
})
it('should throw and should print validation errors for invalid config 1', () => {
const invalidConfig = 'test'
- expect(() => validateConfig(invalidConfig)).toThrowErrorMatchingSnapshot()
+
+ expect(() => validateConfig(invalidConfig, logger)).toThrowErrorMatchingSnapshot()
})
it('should throw and should print validation errors for invalid config', () => {
const invalidConfig = {
foo: false,
}
- expect(() => validateConfig(invalidConfig)).toThrowErrorMatchingSnapshot()
+
+ expect(() => validateConfig(invalidConfig, logger)).toThrowErrorMatchingSnapshot()
+ })
+
+ it('should wrap function config into object', () => {
+ const functionConfig = (stagedFiles) => [`eslint --fix ${stagedFiles}', 'git add`]
+
+ expect(validateConfig(functionConfig, logger)).toEqual({
+ '*': functionConfig,
+ })
+ expect(logger.printHistory()).toEqual('')
})
it('should not throw and should print nothing for valid config', () => {
const validSimpleConfig = {
'*.js': ['eslint --fix', 'git add'],
}
- expect(() => validateConfig(validSimpleConfig)).not.toThrow()
- expect(console.printHistory()).toMatchSnapshot()
- })
- it('should not throw and should print nothing for function config', () => {
- const functionConfig = (stagedFiles) => [`eslint ${stagedFiles.join(' ')}`]
- expect(() => validateConfig(formatConfig(functionConfig))).not.toThrow()
- expect(console.printHistory()).toMatchSnapshot()
+ expect(() => validateConfig(validSimpleConfig, logger)).not.toThrow()
+ expect(logger.printHistory()).toEqual('')
})
it('should not throw and should print nothing for function task', () => {
- expect(() =>
- validateConfig({
- '*.js': (filenames) => {
- const files = filenames.join(' ')
- return `eslint --fix ${files} && git add ${files}`
- },
- '*.css': [(filenames) => filenames.map((filename) => `eslint --fix ${filename}`)],
- })
- ).not.toThrow()
- expect(console.printHistory()).toMatchSnapshot()
+ const functionTask = {
+ '*.js': (filenames) => {
+ const files = filenames.join(' ')
+ return `eslint --fix ${files} && git add ${files}`
+ },
+ '*.css': [(filenames) => filenames.map((filename) => `eslint --fix ${filename}`)],
+ }
+
+ expect(() => validateConfig(functionTask, logger)).not.toThrow()
+ expect(logger.printHistory()).toEqual('')
})
it('should throw when detecting deprecated advanced configuration', () => {
@@ -71,15 +68,16 @@ describe('validateConfig', () => {
subTaskConcurrency: 10,
}
- expect(() => validateConfig(advancedConfig)).toThrowErrorMatchingSnapshot()
- expect(console.printHistory()).toMatchSnapshot()
+ expect(() => validateConfig(advancedConfig, logger)).toThrowErrorMatchingSnapshot()
+ expect(logger.printHistory()).toMatchSnapshot()
})
it('should not throw when config contains deprecated key but with valid task', () => {
const stillValidConfig = {
concurrent: 'my command',
}
- expect(() => validateConfig(stillValidConfig)).not.toThrow()
- expect(console.printHistory()).toMatchSnapshot()
+
+ expect(() => validateConfig(stillValidConfig, logger)).not.toThrow()
+ expect(logger.printHistory()).toEqual('')
})
})
diff --git a/test/validateOptions.spec.js b/test/validateOptions.spec.js
index a888e4d8e..45acc6d72 100644
--- a/test/validateOptions.spec.js
+++ b/test/validateOptions.spec.js
@@ -41,7 +41,7 @@ describe('validateOptions', () => {
const logger = makeConsoleMock()
- mockAccess.mockImplementationOnce(() => new Promise.reject())
+ mockAccess.mockImplementationOnce(() => Promise.reject(new Error('Failed')))
await expect(validateOptions({ shell: '/bin/sh' }, logger)).rejects.toThrowError(
InvalidOptionsError
@@ -55,9 +55,9 @@ describe('validateOptions', () => {
"
ERROR × Validation Error:
- Invalid value for option shell: /bin/sh
+ Invalid value for option 'shell': /bin/sh
- Promise.reject is not a constructor
+ Failed
See https://github.com/okonet/lint-staged#command-line-flags"
`)
diff --git a/yarn.lock b/yarn.lock
index dad17a326..f86131fd9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2490,11 +2490,6 @@ decode-uri-component@^0.2.0:
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
-dedent@^0.7.0:
- version "0.7.0"
- resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
- integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=
-
deep-is@^0.1.3, deep-is@~0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"