Skip to content

Commit

Permalink
Merge branch 'master' into fix/react-native-metro-warning
Browse files Browse the repository at this point in the history
  • Loading branch information
iiroj authored Feb 13, 2022
2 parents cf1cebb + 9a14e92 commit 82c0dfa
Show file tree
Hide file tree
Showing 33 changed files with 3,261 additions and 3,268 deletions.
2 changes: 1 addition & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
version: 2

updates:
# Production dependencies
- package-ecosystem: 'npm'
directory: '/'
versioning-strategy: 'increase'
schedule:
interval: 'weekly'
day: 'monday'
Expand Down
80 changes: 61 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,30 @@

Run linters against staged git files and don't let :poop: slip into your code base!

```
$ 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

Linting makes more sense when run before committing your code. By doing so you can ensure no errors go into the repository and enforce code style. But running a lint process on a whole project is slow, and linting results can be irrelevant. Ultimately you only want to lint files that will be committed.
Expand Down Expand Up @@ -59,38 +81,36 @@ See [Releases](https://github.com/okonet/lint-staged/releases).

## Command line flags

```bash
```
❯ npx lint-staged --help
Usage: lint-staged [options]
Options:
-V, --version output the version number
--allow-empty allow empty commits when tasks revert all staged changes
(default: false)
--allow-empty allow empty commits when tasks revert all staged changes (default: false)
-p, --concurrent <number|boolean> the number of tasks to run concurrently, or false for serial (default: true)
-c, --config [path] path to configuration file, or - to read from stdin
--cwd [path] run all tasks in specific directory, instead of the current
-d, --debug print additional debug information (default: false)
--no-stash disable the backup stash, and do not revert in case of
errors
-p, --concurrent <parallel tasks> the number of tasks to run concurrently, or false to run
tasks serially (default: true)
--no-stash disable the backup stash, and do not revert in case of errors
-q, --quiet disable lint-staged’s own console output (default: false)
-r, --relative pass relative filepaths to tasks (default: false)
-x, --shell [path] skip parsing of tasks for better shell support (default:
false)
-v, --verbose show task output even when tasks succeed; by default only
failed output is shown (default: false)
-x, --shell [path] skip parsing of tasks for better shell support (default: false)
-v, --verbose show task output even when tasks succeed; by default only failed output is shown
(default: false)
-h, --help display help for command
```

- **`--allow-empty`**: By default, when linter tasks undo all staged changes, lint-staged will exit with an error and abort the commit. Use this flag to allow creating empty git commits.
- **`--concurrent [number|boolean]`**: Controls the concurrency of tasks being run by lint-staged. **NOTE**: This does NOT affect the concurrency of subtasks (they will always be run sequentially). Possible values are:
- `false`: Run all tasks serially
- `true` (default) : _Infinite_ concurrency. Runs as many tasks in parallel as possible.
- `{number}`: Run the specified number of tasks in parallel, where `1` is equivalent to `false`.
- **`--config [path]`**: Manually specify a path to a config file or npm package name. Note: when used, lint-staged won't perform the config file search and will print an error if the specified file cannot be found. If '-' is provided as the filename then the config will be read from stdin, allowing piping in the config like `cat my-config.json | npx lint-staged --config -`.
- **`--cwd [path]`**: By default tasks run in the current working directory. Use the `--cwd some/directory` to override this. The path can be absolute or relative to the current working directory.
- **`--debug`**: Run in debug mode. When set, it does the following:
- uses [debug](https://github.com/visionmedia/debug) internally to log additional information about staged files, commands being executed, location of binaries, etc. Debug logs, which are automatically enabled by passing the flag, can also be enabled by setting the environment variable `$DEBUG` to `lint-staged*`.
- uses [`verbose` renderer](https://github.com/SamVerschueren/listr-verbose-renderer) for `listr`; this causes serial, uncoloured output to the terminal, instead of the default (beautified, dynamic) output.
- **`--concurrent [number | (true/false)]`**: Controls the concurrency of tasks being run by lint-staged. **NOTE**: This does NOT affect the concurrency of subtasks (they will always be run sequentially). Possible values are:
- `false`: Run all tasks serially
- `true` (default) : _Infinite_ concurrency. Runs as many tasks in parallel as possible.
- `{number}`: Run the specified number of tasks in parallel, where `1` is equivalent to `false`.
- **`--no-stash`**: By default a backup stash will be created before running the tasks, and all task modifications will be reverted in case of an error. This option will disable creating the stash, and instead leave all modifications in the index when aborting the commit.
- **`--quiet`**: Supress all CLI output, except from tasks.
- **`--relative`**: Pass filepaths relative to `process.cwd()` (where `lint-staged` runs) to tasks. Default is `false`.
Expand All @@ -116,6 +136,8 @@ Starting with v3.1 you can now use different ways of configuring lint-staged:

Configuration should be an object where each value is a command to run and its key is a glob pattern to use for this command. This package uses [micromatch](https://github.com/micromatch/micromatch) for glob patterns. JavaScript files can also export advanced configuration as a function. See [Using JS configuration files](#using-js-configuration-files) for more info.

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:

```json
Expand Down Expand Up @@ -644,12 +666,32 @@ _Thanks to [this comment](https://youtrack.jetbrains.com/issue/IDEA-135454#comme
<details>
<summary>Click to expand</summary>

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.

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/`.

**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:

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.
```js
import baseConfig from '../.lintstagedrc.js'

Example repo: [sudo-suhas/lint-staged-multi-pkg](https://github.com/sudo-suhas/lint-staged-multi-pkg).
export default {
...baseConfig,
'*.js': 'eslint --fix',
}
```

</details>

Expand Down
23 changes: 13 additions & 10 deletions bin/lint-staged.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,15 @@ const version = packageJson.version
cmdline
.version(version)
.option('--allow-empty', 'allow empty commits when tasks revert all staged changes', false)
.option('-c, --config [path]', 'path to configuration file, or - to read from stdin')
.option('-d, --debug', 'print additional debug information', false)
.option('--no-stash', 'disable the backup stash, and do not revert in case of errors', false)
.option(
'-p, --concurrent <parallel tasks>',
'the number of tasks to run concurrently, or false to run tasks serially',
'-p, --concurrent <number|boolean>',
'the number of tasks to run concurrently, or false for serial',
true
)
.option('-c, --config [path]', 'path to configuration file, or - to read from stdin')
.option('--cwd [path]', 'run all tasks in specific directory, instead of the current')
.option('-d, --debug', 'print additional debug information', false)
.option('--no-stash', 'disable the backup stash, and do not revert in case of errors', false)
.option('-q, --quiet', 'disable lint-staged’s own console output', false)
.option('-r, --relative', 'pass relative filepaths to tasks', false)
.option('-x, --shell [path]', 'skip parsing of tasks for better shell support', false)
Expand All @@ -44,10 +45,13 @@ cmdline
)
.parse(process.argv)

const debugLog = debug('lint-staged:bin')
if (cmdline.debug) {
const cmdlineOptions = cmdline.opts()

if (cmdlineOptions.debug) {
debug.enable('lint-staged*')
}

const debugLog = debug('lint-staged:bin')
debugLog('Running `lint-staged@%s`', version)

/**
Expand All @@ -68,18 +72,17 @@ const getMaxArgLength = () => {
}
}

const cmdlineOptions = cmdline.opts()

const options = {
allowEmpty: !!cmdlineOptions.allowEmpty,
concurrent: JSON.parse(cmdlineOptions.concurrent),
configPath: cmdlineOptions.config,
cwd: cmdlineOptions.cwd,
debug: !!cmdlineOptions.debug,
maxArgLength: getMaxArgLength() / 2,
stash: !!cmdlineOptions.stash, // commander inverts `no-<x>` flags to `!x`
quiet: !!cmdlineOptions.quiet,
relative: !!cmdlineOptions.relative,
shell: cmdlineOptions.shell /* Either a boolean or a string pointing to the shell */,
stash: !!cmdlineOptions.stash, // commander inverts `no-<x>` flags to `!x`
verbose: !!cmdlineOptions.verbose,
}

Expand Down
7 changes: 6 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
const config = {
collectCoverage: true,
collectCoverageFrom: ['lib/**/*.js'],
collectCoverageFrom: [
'lib/**/*.js',
// Avoid ESM import.meta parse error.
// (Can't measure coverage anyway, it's always mocked)
'!lib/resolveConfig.js',
],
moduleDirectories: ['node_modules'],
setupFiles: ['./testSetup.js'],
snapshotSerializers: ['jest-snapshot-serializer-ansi'],
Expand Down
3 changes: 3 additions & 0 deletions lib/dynamicImport.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { pathToFileURL } from 'url'

export const dynamicImport = (path) => import(pathToFileURL(path)).then((module) => module.default)
5 changes: 2 additions & 3 deletions lib/generateTasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@ const debugLog = debug('lint-staged:generateTasks')
* @param {boolean} [options.files] - Staged filepaths
* @param {boolean} [options.relative] - Whether filepaths to should be relative to gitDir
*/
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)))

return Object.entries(config).map(([rawPattern, commands]) => {
let pattern = rawPattern
Expand Down
112 changes: 112 additions & 0 deletions lib/getConfigGroups.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/** @typedef {import('./index').Logger} Logger */

import path from 'path'

import debug from 'debug'
import objectInspect from 'object-inspect'

import { loadConfig } from './loadConfig.js'
import { ConfigNotFoundError } from './symbols.js'
import { validateConfig } from './validateConfig.js'

const debugLog = debug('lint-staged:getConfigGroups')

/**
* Return matched files grouped by their configuration.
*
* @param {object} options
* @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 {string} [options.files] - List of staged files
* @param {Logger} logger
*/
export const getConfigGroups = async (
{ configObject, configPath, cwd, files },
logger = console
) => {
debugLog('Grouping configuration files...')

// Return explicit config object from js API
if (configObject) {
debugLog('Using single direct configuration object...')

const config = validateConfig(configObject, 'config object', logger)
return { '': { config, files } }
}

// Use only explicit config path instead of discovering multiple
if (configPath) {
debugLog('Using single configuration path...')

const { config, filepath } = await loadConfig({ configPath }, logger)

if (!config) {
logger.error(`${ConfigNotFoundError.message}.`)
throw ConfigNotFoundError
}

const validatedConfig = validateConfig(config, filepath, logger)
return { [configPath]: { config: validatedConfig, files } }
}

debugLog('Grouping staged files by their directories...')

// Group files by their base directory
const filesByDir = files.reduce((acc, file) => {
const dir = path.normalize(path.dirname(file))

if (dir in acc) {
acc[dir].push(file)
} else {
acc[dir] = [file]
}

return acc
}, {})

debugLog('Grouped staged files into %d directories:', Object.keys(filesByDir).length)
debugLog(objectInspect(filesByDir, { indent: 2 }))

// Group files by their discovered config
// { '.lintstagedrc.json': { config: {...}, files: [...] } }
const configGroups = {}

debugLog('Searching config files...')

const searchConfig = async (cwd, files = []) => {
const { config, filepath } = await loadConfig({ cwd }, logger)
if (!config) {
debugLog('Found no config from "%s"!', cwd)
return
}

if (filepath in configGroups) {
debugLog('Found existing config "%s" from "%s"!', filepath, cwd)
// Re-use cached config and skip validation
configGroups[filepath].files.push(...files)
} else {
debugLog('Found new config "%s" from "%s"!', filepath, cwd)

const validatedConfig = validateConfig(config, filepath, logger)
configGroups[filepath] = { config: validatedConfig, files }
}
}

// Start by searching from cwd
await searchConfig(cwd)

// Discover configs from the base directory of each file
await Promise.all(Object.entries(filesByDir).map(([dir, files]) => searchConfig(dir, files)))

// Throw if no configurations were found
if (Object.keys(configGroups).length === 0) {
debugLog('Found no config groups!')
logger.error(`${ConfigNotFoundError.message}.`)
throw ConfigNotFoundError
}

debugLog('Grouped staged files into %d groups!', Object.keys(configGroups).length)

return configGroups
}
26 changes: 19 additions & 7 deletions lib/getStagedFiles.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
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 {
return null
}
Expand Down
Loading

0 comments on commit 82c0dfa

Please sign in to comment.