Skip to content

Commit d8b836c

Browse files
committed
feat: Remove support for npm scripts (#390)
- Update `findBin` to only resolve binaries installed locally or globally. Also add simple caching. - Check pkg scripts when `findBin` fails and print useful message to aid users in migration to newer version config. - Update readme. - Remove dependency `app-root-path`. - Update tests. BREAKING CHANGE: Remove implicit support for running npm scripts. Consider example `lint-staged` config: { "name": "My project", "version": "0.1.0", "scripts": { "my-custom-script": "linter --arg1 --arg2", "precommit": "lint-staged" }, "lint-staged": { "*.js": ["my-custom-script", "git add"] } } The list of commands should be changed to the following: "*.js": ["npm run my-custom-script --", "git add"]
1 parent 5a333a0 commit d8b836c

17 files changed

+244
-190
lines changed

README.md

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Run linters against staged git files and don't let :poop: slip into your code ba
66

77
Linting makes more sense when running before committing your code. By doing that you can ensure no errors are going into 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.
88

9-
This project contains a script that will run arbitrary npm and shell tasks with a list of staged files as an argument, filtered by a specified glob pattern.
9+
This project contains a script that will run arbitrary shell tasks with a list of staged files as an argument, filtered by a specified glob pattern.
1010

1111
## Related blogs posts
1212

@@ -96,11 +96,8 @@ Should be an object where each value is a command to run and its key is a glob p
9696
#### `package.json` example:
9797
```json
9898
{
99-
"scripts": {
100-
"my-task": "your-command",
101-
},
10299
"lint-staged": {
103-
"*": "my-task"
100+
"*": "your-cmd"
104101
}
105102
}
106103
```
@@ -109,15 +106,15 @@ Should be an object where each value is a command to run and its key is a glob p
109106

110107
```json
111108
{
112-
"*": "my-task"
109+
"*": "your-cmd"
113110
}
114111
```
115112

116-
This config will execute `npm run my-task` with the list of currently staged files passed as arguments.
113+
This config will execute `your-cmd` with the list of currently staged files passed as arguments.
117114

118115
So, considering you did `git add file1.ext file2.ext`, lint-staged will run the following command:
119116

120-
`npm run my-task -- file1.ext file2.ext`
117+
`your-cmd file1.ext file2.ext`
121118

122119
### Advanced config format
123120
To set options and keep lint-staged extensible, advanced format can be used. This should hold linters object in `linters` property.
@@ -164,11 +161,11 @@ Also see [How to use `lint-staged` in a multi package monorepo?](#how-to-use-lin
164161

165162
## What commands are supported?
166163

167-
Supported are both local npm scripts (`npm run-script`), or any executables installed locally or globally via `npm` as well as any executable from your $PATH.
164+
Supported are any executables installed locally or globally via `npm` as well as any executable from your $PATH.
168165

169166
> Using globally installed scripts is discouraged, since lint-staged may not work for someone who doesn’t have it installed.
170167
171-
`lint-staged` is using [npm-which](https://github.com/timoxley/npm-which) to locate locally installed scripts, so you don't need to add `{ "eslint": "eslint" }` to the `scripts` section of your `package.json`. So in your `.lintstagedrc` you can write:
168+
`lint-staged` is using [npm-which](https://github.com/timoxley/npm-which) to locate locally installed scripts. So in your `.lintstagedrc` you can write:
172169

173170
```json
174171
{
@@ -210,13 +207,14 @@ All examples assuming you’ve already set up lint-staged and husky in the `pack
210207
"name": "My project",
211208
"version": "0.1.0",
212209
"scripts": {
210+
"my-custom-script": "linter --arg1 --arg2",
213211
"precommit": "lint-staged"
214212
},
215213
"lint-staged": {}
216214
}
217215
```
218216

219-
*Note we don’t pass a path as an argument for the runners. This is important since lint-staged will do this for you. Please don’t reuse your tasks with paths from package.json.*
217+
*Note we don’t pass a path as an argument for the runners. This is important since lint-staged will do this for you.*
220218

221219
### ESLint with default parameters for `*.js` and `*.jsx` running as a pre-commit hook
222220

@@ -236,6 +234,23 @@ All examples assuming you’ve already set up lint-staged and husky in the `pack
236234

237235
This will run `eslint --fix` and automatically add changes to the commit. Please note, that it doesn’t work well with committing hunks (`git add -p`).
238236

237+
### Reuse npm script
238+
239+
If you wish to reuse a npm script defined in your package.json:
240+
241+
```json
242+
{
243+
"*.js": ["npm run my-custom-script --", "git add"]
244+
}
245+
```
246+
247+
The following is equivalent:
248+
249+
```json
250+
{
251+
"*.js": ["linter --arg1 --arg2", "git add"]
252+
}
253+
```
239254

240255
### Automatically fix code style with `prettier` for javascript + flow or typescript
241256

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"test:watch": "jest --watch"
2828
},
2929
"dependencies": {
30-
"app-root-path": "^2.0.0",
30+
"app-root-path": "^2.0.1",
3131
"chalk": "^2.1.0",
3232
"commander": "^2.11.0",
3333
"cosmiconfig": "^4.0.0",

src/checkPkgScripts.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
'use strict'
2+
3+
const chalk = require('chalk')
4+
const dedent = require('dedent')
5+
const has = require('lodash/has')
6+
7+
const warn = msg => {
8+
console.warn(chalk.yellowBright.bold(msg))
9+
}
10+
11+
/**
12+
* Checks if the given command or binary name is present in the package.json scripts. This would be
13+
* called if and when resolving a binary fails in `findBin`.
14+
*
15+
* @param {Object} pkg package.json
16+
* @param {string} cmd
17+
* @param {string} binName
18+
* @param {Array<string>} args
19+
* @throws {Error} If a script is found in the pkg for the given `cmd` or `binName`.
20+
*/
21+
module.exports = function checkPkgScripts(pkg, cmd, binName, args) {
22+
if (pkg && pkg.scripts) {
23+
let scriptName
24+
let script
25+
if (has(pkg.scripts, cmd)) {
26+
scriptName = cmd
27+
script = pkg.scripts[cmd]
28+
} else if (has(pkg.scripts, binName)) {
29+
scriptName = binName
30+
script = pkg.scripts[binName]
31+
} else {
32+
return
33+
}
34+
35+
const argsStr = args && args.length ? args.join(' ') : ''
36+
warn(dedent`
37+
\`lint-staged\` no longer supports running scripts defined in package.json.
38+
39+
The same behavior can be achieved by changing the command to any of the following:
40+
- \`npm run ${scriptName} -- ${argsStr}\`
41+
- \`${script} ${argsStr}\`
42+
`)
43+
throw new Error(`Could not resolve binary for \`${cmd}\``)
44+
}
45+
}

src/findBin.js

Lines changed: 24 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,47 @@
11
'use strict'
22

3+
const appRoot = require('app-root-path')
34
const npmWhich = require('npm-which')(process.cwd())
5+
const checkPkgScripts = require('./checkPkgScripts')
46

7+
// Find and load the package.json at the root of the project.
8+
const pkg = require(appRoot.resolve('package.json')) // eslint-disable-line import/no-dynamic-require
59
const debug = require('debug')('lint-staged:find-bin')
610

7-
module.exports = function findBin(cmd, scripts, debugMode) {
11+
const cache = new Map()
12+
13+
module.exports = function findBin(cmd) {
814
debug('Resolving binary for command `%s`', cmd)
9-
const npmArgs = (bin, args) =>
10-
// We always add `--` even if args are not defined. This is required
11-
// because we pass filenames later.
12-
['run', debugMode ? undefined : '--silent', bin, '--']
13-
// args could be undefined but we filter that out.
14-
.concat(args)
15-
.filter(arg => arg !== undefined)
1615

1716
/*
18-
* If package.json has script with cmd defined we want it to be executed
19-
* first. For finding the bin from scripts defined in package.json, there
20-
* are 2 possibilities. It's a command which does not have any arguments
21-
* passed to it in the lint-staged config. Or it could be something like
22-
* `kcd-scripts` which has arguments such as `format`, `lint` passed to it.
23-
* But we always cannot assume that the cmd, which has a space in it, is of
24-
* the format `bin argument` because it is legal to define a package.json
25-
* script with space in it's name. So we do 2 types of lookup. First a naive
26-
* lookup which just looks for the scripts entry with cmd. Then we split on
27-
* space, parse the bin and args, and try again.
28-
*
29-
* Related:
30-
* - https://github.com/kentcdodds/kcd-scripts/pull/3
31-
* - https://github.com/okonet/lint-staged/issues/294
17+
* Try to locate the binary in node_modules/.bin and if this fails, in
18+
* $PATH.
3219
*
33-
* Example:
20+
* This allows to use linters installed for the project:
3421
*
35-
* "scripts": {
36-
* "my cmd": "echo deal-wth-it",
37-
* "demo-bin": "node index.js"
38-
* },
39-
* "lint-staged": {
40-
* "*.js": ["my cmd", "demo-bin hello"]
41-
* }
22+
* "lint-staged": {
23+
* "*.js": "eslint"
24+
* }
4225
*/
43-
if (scripts[cmd] !== undefined) {
44-
// Support for scripts from package.json
45-
debug('`%s` resolved to npm script - `%s`', cmd, scripts[cmd])
46-
return { bin: 'npm', args: npmArgs(cmd) }
47-
}
48-
4926
const parts = cmd.split(' ')
50-
let bin = parts[0]
27+
const binName = parts[0]
5128
const args = parts.splice(1)
5229

53-
if (scripts[bin] !== undefined) {
54-
debug('`%s` resolved to npm script - `%s`', bin, scripts[bin])
55-
return { bin: 'npm', args: npmArgs(bin, args) }
30+
if (cache.has(binName)) {
31+
debug('Resolving binary for `%s` from cache', binName)
32+
return { bin: cache.get(binName), args }
5633
}
5734

58-
/*
59-
* If cmd wasn't found in package.json scripts
60-
* we'll try to locate the binary in node_modules/.bin
61-
* and if this fails in $PATH.
62-
*
63-
* This is useful for shorter configs like:
64-
*
65-
* "lint-staged": {
66-
* "*.js": "eslint"
67-
* }
68-
*
69-
* without adding
70-
*
71-
* "scripts" {
72-
* "eslint": "eslint"
73-
* }
74-
*/
75-
7635
try {
7736
/* npm-which tries to resolve the bin in local node_modules/.bin */
7837
/* and if this fails it look in $PATH */
79-
bin = npmWhich.sync(bin)
38+
const bin = npmWhich.sync(binName)
39+
debug('Binary for `%s` resolved to `%s`', cmd, bin)
40+
cache.set(binName, bin)
41+
return { bin, args }
8042
} catch (err) {
81-
throw new Error(`${bin} could not be found. Try \`npm install ${bin}\`.`)
43+
// throw helpful error if matching script is present in package.json
44+
checkPkgScripts(pkg, cmd, binName, args)
45+
throw new Error(`${binName} could not be found. Try \`npm install ${binName}\`.`)
8246
}
83-
84-
debug('Binary for `%s` resolved to `%s`', cmd, bin)
85-
return { bin, args }
8647
}

src/index.js

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
'use strict'
44

5-
const appRoot = require('app-root-path')
65
const dedent = require('dedent')
76
const cosmiconfig = require('cosmiconfig')
87
const stringifyObject = require('stringify-object')
@@ -13,9 +12,6 @@ const runAll = require('./runAll')
1312

1413
const debug = require('debug')('lint-staged')
1514

16-
// Find the right package.json at the root of the project
17-
const packageJson = require(appRoot.resolve('package.json'))
18-
1915
// Force colors for packages that depend on https://www.npmjs.com/package/supports-color
2016
// but do this only in TTY mode
2117
if (process.stdout.isTTY) {
@@ -56,10 +52,7 @@ module.exports = function lintStaged(injectedLogger, configPath, debugMode) {
5652
debug('Normalized config:\n%O', config)
5753
}
5854

59-
const scripts = packageJson.scripts || {}
60-
debug('Loaded scripts from package.json:\n%O', scripts)
61-
62-
runAll(scripts, config, debugMode)
55+
runAll(config)
6356
.then(() => {
6457
debug('linters were executed successfully!')
6558
// No errors, exiting with 0

src/runAll.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,10 @@ const debug = require('debug')('lint-staged:run')
1212

1313
/**
1414
* Executes all tasks and either resolves or rejects the promise
15-
* @param scripts
1615
* @param config {Object}
1716
* @returns {Promise}
1817
*/
19-
module.exports = function runAll(scripts, config, debugMode) {
18+
module.exports = function runAll(config) {
2019
debug('Running all linter scripts')
2120
// Config validation
2221
if (!config || !has(config, 'concurrent') || !has(config, 'renderer')) {
@@ -37,7 +36,7 @@ module.exports = function runAll(scripts, config, debugMode) {
3736
const tasks = generateTasks(config, filenames).map(task => ({
3837
title: `Running tasks for ${task.pattern}`,
3938
task: () =>
40-
new Listr(runScript(task.commands, task.fileList, scripts, config, debugMode), {
39+
new Listr(runScript(task.commands, task.fileList, config), {
4140
// In sub-tasks we don't want to run concurrently
4241
// and we want to abort on errors
4342
dateFormat: false,

src/runScript.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const resolveGitDir = require('./resolveGitDir')
1212

1313
const debug = require('debug')('lint-staged:run-script')
1414

15-
module.exports = function runScript(commands, pathsToLint, scripts, config, debugMode) {
15+
module.exports = function runScript(commands, pathsToLint, config) {
1616
debug('Running script with commands %o', commands)
1717

1818
const normalizedConfig = getConfig(config)
@@ -28,7 +28,7 @@ module.exports = function runScript(commands, pathsToLint, scripts, config, debu
2828
title: linter,
2929
task: () => {
3030
try {
31-
const res = findBin(linter, scripts, debugMode)
31+
const res = findBin(linter)
3232

3333
// Only use gitDir as CWD if we are using the git binary
3434
// e.g `npm` should run tasks in the actual CWD

test/__mocks__/npm-which.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
const mockFn = jest.fn(path => {
2+
if (path.includes('missing')) {
3+
throw new Error(`not found: ${path}`)
4+
}
5+
return path
6+
})
7+
18
module.exports = function npmWhich() {
29
return {
3-
sync(path) {
4-
if (path.includes('missing')) {
5-
throw new Error(`not found: ${path}`)
6-
}
7-
return path
8-
}
10+
sync: mockFn
911
}
1012
}
13+
14+
module.exports.mockFn = mockFn
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`checkPkgScripts throws if the binary name is defined in script 1`] = `
4+
"
5+
WARN \`lint-staged\` no longer supports running scripts defined in package.json.
6+
7+
The same behavior can be achieved by changing the command to any of the following:
8+
- \`npm run lint -- --fix\`
9+
- \`eslint . --fix\`"
10+
`;
11+
12+
exports[`checkPkgScripts throws if the cmd is defined in script 1`] = `
13+
"
14+
WARN \`lint-staged\` no longer supports running scripts defined in package.json.
15+
16+
The same behavior can be achieved by changing the command to any of the following:
17+
- \`npm run lint -- \`
18+
- \`eslint . \`"
19+
`;
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`findBin should throw a helpful error if the cmd is present in pkg scripts 1`] = `
4+
"
5+
WARN \`lint-staged\` no longer supports running scripts defined in package.json.
6+
7+
The same behavior can be achieved by changing the command to any of the following:
8+
- \`npm run lint -- \`
9+
- \`eslint . \`"
10+
`;

0 commit comments

Comments
 (0)