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"