lint-staged
Advanced tools
Comparing version 12.1.7 to 12.2.0
@@ -19,7 +19,6 @@ import path from 'path' | ||
*/ | ||
export const generateTasks = ({ config, cwd = process.cwd(), gitDir, files, relative = false }) => { | ||
export const generateTasks = ({ config, cwd = process.cwd(), files, relative = false }) => { | ||
debugLog('Generating linter tasks') | ||
const absoluteFiles = files.map((file) => normalize(path.resolve(gitDir, file))) | ||
const relativeFiles = absoluteFiles.map((file) => normalize(path.relative(cwd, file))) | ||
const relativeFiles = files.map((file) => normalize(path.relative(cwd, file))) | ||
@@ -26,0 +25,0 @@ return Object.entries(config).map(([rawPattern, commands]) => { |
@@ -0,14 +1,26 @@ | ||
import path from 'path' | ||
import normalize from 'normalize-path' | ||
import { execGit } from './execGit.js' | ||
export const getStagedFiles = async (options) => { | ||
export const getStagedFiles = async ({ cwd = process.cwd() } = {}) => { | ||
try { | ||
// Docs for --diff-filter option: https://git-scm.com/docs/git-diff#Documentation/git-diff.txt---diff-filterACDMRTUXB82308203 | ||
// Docs for -z option: https://git-scm.com/docs/git-diff#Documentation/git-diff.txt--z | ||
const lines = await execGit( | ||
['diff', '--staged', '--diff-filter=ACMR', '--name-only', '-z'], | ||
options | ||
const lines = await execGit(['diff', '--staged', '--diff-filter=ACMR', '--name-only', '-z'], { | ||
cwd, | ||
}) | ||
if (!lines) return [] | ||
// With `-z`, git prints `fileA\u0000fileB\u0000fileC\u0000` so we need to | ||
// remove the last occurrence of `\u0000` before splitting | ||
return ( | ||
lines | ||
// eslint-disable-next-line no-control-regex | ||
.replace(/\u0000$/, '') | ||
.split('\u0000') | ||
.map((file) => normalize(path.resolve(cwd, file))) | ||
) | ||
// With `-z`, git prints `fileA\u0000fileB\u0000fileC\u0000` so we need to remove the last occurrence of `\u0000` before splitting | ||
// eslint-disable-next-line no-control-regex | ||
return lines ? lines.replace(/\u0000$/, '').split('\u0000') : [] | ||
} catch { | ||
@@ -15,0 +27,0 @@ return null |
import debug from 'debug' | ||
import inspect from 'object-inspect' | ||
import { loadConfig } from './loadConfig.js' | ||
import { PREVENTED_EMPTY_COMMIT, GIT_ERROR, RESTORE_STASH_EXAMPLE } from './messages.js' | ||
import { printTaskOutput } from './printTaskOutput.js' | ||
import { runAll } from './runAll.js' | ||
import { | ||
ApplyEmptyCommitError, | ||
ConfigNotFoundError, | ||
GetBackupStashError, | ||
GitError, | ||
} from './symbols.js' | ||
import { validateConfig } from './validateConfig.js' | ||
import { ApplyEmptyCommitError, GetBackupStashError, GitError } from './symbols.js' | ||
import { validateOptions } from './validateOptions.js' | ||
@@ -61,21 +53,2 @@ | ||
const inputConfig = configObject || (await loadConfig({ configPath, cwd }, logger)) | ||
if (!inputConfig) { | ||
logger.error(`${ConfigNotFoundError.message}.`) | ||
throw ConfigNotFoundError | ||
} | ||
const config = validateConfig(inputConfig, logger) | ||
if (debug) { | ||
// Log using logger to be able to test through `consolemock`. | ||
logger.log('Running lint-staged with the following config:') | ||
logger.log(inspect(config, { indent: 2 })) | ||
} else { | ||
// We might not be in debug mode but `DEBUG=lint-staged*` could have | ||
// been set. | ||
debugLog('lint-staged config:\n%O', config) | ||
} | ||
// Unset GIT_LITERAL_PATHSPECS to not mess with path interpretation | ||
@@ -90,3 +63,4 @@ debugLog('Unset GIT_LITERAL_PATHSPECS (was `%s`)', process.env.GIT_LITERAL_PATHSPECS) | ||
concurrent, | ||
config, | ||
configObject, | ||
configPath, | ||
cwd, | ||
@@ -93,0 +67,0 @@ debug, |
/** @typedef {import('./index').Logger} Logger */ | ||
import { pathToFileURL } from 'url' | ||
import debug from 'debug' | ||
@@ -9,2 +7,3 @@ import { lilconfig } from 'lilconfig' | ||
import { dynamicImport } from './dynamicImport.js' | ||
import { resolveConfig } from './resolveConfig.js' | ||
@@ -32,5 +31,2 @@ | ||
/** exported for tests */ | ||
export const dynamicImport = (path) => import(pathToFileURL(path)).then((module) => module.default) | ||
const jsonParse = (path, content) => JSON.parse(content) | ||
@@ -56,2 +52,4 @@ | ||
const explorer = lilconfig('lint-staged', { searchPlaces, loaders }) | ||
/** | ||
@@ -70,20 +68,20 @@ * @param {object} options | ||
const explorer = lilconfig('lint-staged', { searchPlaces, loaders }) | ||
const result = await (configPath | ||
? explorer.load(resolveConfig(configPath)) | ||
: explorer.search(cwd)) | ||
if (!result) return null | ||
if (!result) return {} | ||
// config is a promise when using the `dynamicImport` loader | ||
const config = await result.config | ||
const filepath = result.filepath | ||
debugLog('Successfully loaded config from `%s`:\n%O', result.filepath, config) | ||
debugLog('Successfully loaded config from `%s`:\n%O', filepath, config) | ||
return config | ||
return { config, filepath } | ||
} catch (error) { | ||
debugLog('Failed to load configuration!') | ||
logger.error(error) | ||
return null | ||
return {} | ||
} | ||
} |
/** @typedef {import('./index').Logger} Logger */ | ||
import path from 'path' | ||
import { dim } from 'colorette' | ||
import debug from 'debug' | ||
import { Listr } from 'listr2' | ||
import normalize from 'normalize-path' | ||
@@ -9,2 +13,3 @@ import { chunkFiles } from './chunkFiles.js' | ||
import { generateTasks } from './generateTasks.js' | ||
import { getConfigGroups } from './getConfigGroups.js' | ||
import { getRenderer } from './getRenderer.js' | ||
@@ -44,6 +49,7 @@ import { getStagedFiles } from './getStagedFiles.js' | ||
* @param {object} options | ||
* @param {Object} [options.allowEmpty] - Allow empty commits when tasks revert all staged changes | ||
* @param {boolean} [options.allowEmpty] - Allow empty commits when tasks revert all staged changes | ||
* @param {boolean | number} [options.concurrent] - The number of tasks to run concurrently, or false to run tasks serially | ||
* @param {Object} [options.config] - Task configuration | ||
* @param {Object} [options.cwd] - Current working directory | ||
* @param {Object} [options.configObject] - Explicit config object from the js API | ||
* @param {string} [options.configPath] - Explicit path to a config file | ||
* @param {string} [options.cwd] - Current working directory | ||
* @param {boolean} [options.debug] - Enable debug mode | ||
@@ -63,3 +69,4 @@ * @param {number} [options.maxArgLength] - Maximum argument string length | ||
concurrent = true, | ||
config, | ||
configObject, | ||
configPath, | ||
cwd = process.cwd(), | ||
@@ -113,5 +120,3 @@ debug = false, | ||
const stagedFileChunks = chunkFiles({ baseDir: gitDir, files, maxArgLength, relative }) | ||
const chunkCount = stagedFileChunks.length | ||
if (chunkCount > 1) debugLog(`Chunked staged files into ${chunkCount} part`, chunkCount) | ||
const configGroups = await getConfigGroups({ configObject, configPath, files }, logger) | ||
@@ -135,39 +140,67 @@ // lint-staged 10 will automatically add modifications to index | ||
for (const [index, files] of stagedFileChunks.entries()) { | ||
const chunkTasks = generateTasks({ config, cwd, gitDir, files, relative }) | ||
const chunkListrTasks = [] | ||
for (const [configPath, { config, files }] of Object.entries(configGroups)) { | ||
const stagedFileChunks = chunkFiles({ baseDir: gitDir, files, maxArgLength, relative }) | ||
for (const task of chunkTasks) { | ||
const subTasks = await makeCmdTasks({ | ||
commands: task.commands, | ||
cwd, | ||
files: task.fileList, | ||
gitDir, | ||
renderer: listrOptions.renderer, | ||
shell, | ||
verbose, | ||
}) | ||
const chunkCount = stagedFileChunks.length | ||
if (chunkCount > 1) { | ||
debugLog('Chunked staged files from `%s` into %d part', configPath, chunkCount) | ||
} | ||
// Add files from task to match set | ||
task.fileList.forEach((file) => { | ||
matchedFiles.add(file) | ||
}) | ||
for (const [index, files] of stagedFileChunks.entries()) { | ||
const relativeConfig = normalize(path.relative(cwd, configPath)) | ||
hasDeprecatedGitAdd = | ||
hasDeprecatedGitAdd || subTasks.some((subTask) => subTask.command === 'git add') | ||
const chunkListrTasks = await Promise.all( | ||
generateTasks({ config, cwd, files, relative }).map((task) => | ||
makeCmdTasks({ | ||
commands: task.commands, | ||
cwd, | ||
files: task.fileList, | ||
gitDir, | ||
renderer: listrOptions.renderer, | ||
shell, | ||
verbose, | ||
}).then((subTasks) => { | ||
// Add files from task to match set | ||
task.fileList.forEach((file) => { | ||
matchedFiles.add(file) | ||
}) | ||
chunkListrTasks.push({ | ||
title: `Running tasks for ${task.pattern}`, | ||
task: async () => | ||
new Listr(subTasks, { | ||
// In sub-tasks we don't want to run concurrently | ||
// and we want to abort on errors | ||
...listrOptions, | ||
concurrent: false, | ||
exitOnError: true, | ||
}), | ||
hasDeprecatedGitAdd = | ||
hasDeprecatedGitAdd || subTasks.some((subTask) => subTask.command === 'git add') | ||
const fileCount = task.fileList.length | ||
return { | ||
title: `${task.pattern}${dim(` — ${fileCount} ${fileCount > 1 ? 'files' : 'file'}`)}`, | ||
task: async () => | ||
new Listr(subTasks, { | ||
// In sub-tasks we don't want to run concurrently | ||
// and we want to abort on errors | ||
...listrOptions, | ||
concurrent: false, | ||
exitOnError: true, | ||
}), | ||
skip: () => { | ||
// Skip task when no files matched | ||
if (fileCount === 0) { | ||
return `${task.pattern}${dim(' — no files')}` | ||
} | ||
return false | ||
}, | ||
} | ||
}) | ||
) | ||
) | ||
listrTasks.push({ | ||
title: | ||
`${relativeConfig}${dim(` — ${files.length} ${files.length > 1 ? 'files' : 'file'}`)}` + | ||
(chunkCount > 1 ? dim(` (chunk ${index + 1}/${chunkCount})...`) : ''), | ||
task: () => new Listr(chunkListrTasks, { ...listrOptions, concurrent, exitOnError: true }), | ||
skip: () => { | ||
// Skip task when no files matched | ||
if (task.fileList.length === 0) { | ||
return `No staged files match ${task.pattern}` | ||
// Skip if the first step (backup) failed | ||
if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR | ||
// Skip chunk when no every task is skipped (due to no matches) | ||
if (chunkListrTasks.every((task) => task.skip())) { | ||
return `${relativeConfig}${dim(' — no tasks to run')}` | ||
} | ||
@@ -178,16 +211,2 @@ return false | ||
} | ||
listrTasks.push({ | ||
// No need to show number of task chunks when there's only one | ||
title: | ||
chunkCount > 1 ? `Running tasks (chunk ${index + 1}/${chunkCount})...` : 'Running tasks...', | ||
task: () => new Listr(chunkListrTasks, { ...listrOptions, concurrent }), | ||
skip: () => { | ||
// Skip if the first step (backup) failed | ||
if (ctx.errors.has(GitError)) return SKIPPED_GIT_ERROR | ||
// Skip chunk when no every task is skipped (due to no matches) | ||
if (chunkListrTasks.every((task) => task.skip())) return 'No tasks to run.' | ||
return false | ||
}, | ||
}) | ||
} | ||
@@ -220,3 +239,3 @@ | ||
{ | ||
title: 'Preparing...', | ||
title: 'Preparing lint-staged...', | ||
task: (ctx) => git.prepare(ctx), | ||
@@ -229,5 +248,9 @@ }, | ||
}, | ||
...listrTasks, | ||
{ | ||
title: 'Applying modifications...', | ||
title: `Running tasks for staged files...`, | ||
task: () => new Listr(listrTasks, { ...listrOptions, concurrent }), | ||
skip: () => listrTasks.every((task) => task.skip()), | ||
}, | ||
{ | ||
title: 'Applying modifications from tasks...', | ||
task: (ctx) => git.applyModifications(ctx), | ||
@@ -249,3 +272,3 @@ skip: applyModificationsSkipped, | ||
{ | ||
title: 'Cleaning up...', | ||
title: 'Cleaning up temporary files...', | ||
task: (ctx) => git.cleanup(ctx), | ||
@@ -252,0 +275,0 @@ enabled: cleanupEnabled, |
@@ -0,2 +1,5 @@ | ||
/** @typedef {import('./index').Logger} Logger */ | ||
import debug from 'debug' | ||
import inspect from 'object-inspect' | ||
@@ -24,7 +27,9 @@ import { configurationError } from './messages.js' | ||
* Runs config validation. Throws error if the config is not valid. | ||
* @param config {Object} | ||
* @returns config {Object} | ||
* @param {Object} config | ||
* @param {string} configPath | ||
* @param {Logger} logger | ||
* @returns {Object} config | ||
*/ | ||
export const validateConfig = (config, logger) => { | ||
debugLog('Validating config') | ||
export const validateConfig = (config, configPath, logger) => { | ||
debugLog('Validating config from `%s`...', configPath) | ||
@@ -107,3 +112,6 @@ if (!config || (typeof config !== 'object' && typeof config !== 'function')) { | ||
debugLog('Validated config from `%s`:', configPath) | ||
debugLog(inspect(config, { indent: 2 })) | ||
return validatedConfig | ||
} |
{ | ||
"name": "lint-staged", | ||
"version": "12.1.7", | ||
"version": "12.2.0", | ||
"description": "Lint files staged by git", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
@@ -5,4 +5,26 @@ # 🚫💩 lint-staged ![GitHub Actions](https://github.com/okonet/lint-staged/workflows/CI/badge.svg) [![npm version](https://badge.fury.io/js/lint-staged.svg)](https://badge.fury.io/js/lint-staged) [![Codecov](https://codecov.io/gh/okonet/lint-staged/branch/master/graph/badge.svg)](https://codecov.io/gh/okonet/lint-staged) | ||
``` | ||
$ git commit | ||
✔ Preparing lint-staged... | ||
❯ Running tasks for staged files... | ||
❯ packages/frontend/.lintstagedrc.json — 1 file | ||
↓ *.js — no files [SKIPPED] | ||
❯ *.{json,md} — 1 file | ||
⠹ prettier --write | ||
↓ packages/backend/.lintstagedrc.json — 2 files | ||
❯ *.js — 2 files | ||
⠼ eslint --fix | ||
↓ *.{json,md} — no files [SKIPPED] | ||
◼ Applying modifications from tasks... | ||
◼ Cleaning up temporary files... | ||
``` | ||
<details> | ||
<summary>See asciinema video</summary> | ||
[![asciicast](https://asciinema.org/a/199934.svg)](https://asciinema.org/a/199934) | ||
</details> | ||
## Why | ||
@@ -120,2 +142,4 @@ | ||
You can also place multiple configuration files in different directories inside a project. For a given staged file, the closest configuration file will always be used. See ["How to use `lint-staged` in a multi-package monorepo?"](#how-to-use-lint-staged-in-a-multi-package-monorepo) for more info and an example. | ||
#### `package.json` example: | ||
@@ -649,9 +673,29 @@ | ||
Starting with v5.0, `lint-staged` automatically resolves the git root **without any** additional configuration. You configure `lint-staged` as you normally would if your project root and git root were the same directory. | ||
Install _lint-staged_ on the monorepo root level, and add separate configuration files in each package. When running, _lint-staged_ will always use the configuration closest to a staged file, so having separate configuration files makes sure linters do not "leak" into other packages. | ||
If you wish to use `lint-staged` in a multi package monorepo, it is recommended to install [`husky`](https://github.com/typicode/husky) in the root package.json. | ||
[`lerna`](https://github.com/lerna/lerna) can be used to execute the `precommit` script in all sub-packages. | ||
For example, in a monorepo with `packages/frontend/.lintstagedrc.json` and `packages/backend/.lintstagedrc.json`, a staged file inside `packages/frontend/` will only match that configuration, and not the one in `packages/backend/`. | ||
Example repo: [sudo-suhas/lint-staged-multi-pkg](https://github.com/sudo-suhas/lint-staged-multi-pkg). | ||
**Note**: _lint-staged_ discovers the closest configuration to each staged file, even if that configuration doesn't include any matching globs. Given these example configurations: | ||
```js | ||
// ./.lintstagedrc.json | ||
{ "*.md": "prettier --write" } | ||
``` | ||
```js | ||
// ./packages/frontend/.lintstagedrc.json | ||
{ "*.js": "eslint --fix" } | ||
``` | ||
When committing `./packages/frontend/README.md`, it **will not run** _prettier_, because the configuration in the `frontend/` directory is closer to the file and doesn't include it. You should treat all _lint-staged_ configuration files as isolated and separated from each other. You can always use JS files to "extend" configurations, for example: | ||
```js | ||
import baseConfig from '../.lintstagedrc.js' | ||
export default { | ||
...baseConfig, | ||
'*.js': 'eslint --fix', | ||
} | ||
``` | ||
</details> | ||
@@ -658,0 +702,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
93754
28
1615
781