Skip to content

Commit 1dc3bd6

Browse files
sudo-suhasokonet
authored andcommitted
fix(findBin): Resolve package script with args (#295)
1 parent fd79a31 commit 1dc3bd6

File tree

7 files changed

+88
-66
lines changed

7 files changed

+88
-66
lines changed

src/findBin.js

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,50 @@
22

33
const npmWhich = require('npm-which')(process.cwd())
44

5-
module.exports = function findBin(cmd, packageJson, options) {
5+
module.exports = function findBin(cmd, scripts, options) {
6+
const npmArgs = (bin, args) =>
7+
['run', options && options.verbose ? undefined : '--silent', bin]
8+
// args could be undefined but we filter that out.
9+
.concat(args)
10+
.filter(arg => arg !== undefined)
11+
612
/*
7-
* If package.json has script with cmd defined
8-
* we want it to be executed first
9-
*/
10-
if (packageJson.scripts && packageJson.scripts[cmd] !== undefined) {
13+
* If package.json has script with cmd defined we want it to be executed
14+
* first. For finding the bin from scripts defined in package.json, there
15+
* are 2 possibilities. It's a command which does not have any arguments
16+
* passed to it in the lint-staged config. Or it could be something like
17+
* `kcd-scripts` which has arguments such as `format`, `lint` passed to it.
18+
* But we always cannot assume that the cmd, which has a space in it, is of
19+
* the format `bin argument` because it is legal to define a package.json
20+
* script with space in it's name. So we do 2 types of lookup. First a naive
21+
* lookup which just looks for the scripts entry with cmd. Then we split on
22+
* space, parse the bin and args, and try again.
23+
*
24+
* Related:
25+
* - https://github.com/kentcdodds/kcd-scripts/pull/3
26+
* - https://github.com/okonet/lint-staged/issues/294
27+
*
28+
* Example:
29+
*
30+
* "scripts": {
31+
* "my cmd": "echo deal-wth-it",
32+
* "demo-bin": "node index.js"
33+
* },
34+
* "lint-staged": {
35+
* "*.js": ["my cmd", "demo-bin hello"]
36+
* }
37+
*/
38+
if (scripts[cmd] !== undefined) {
1139
// Support for scripts from package.json
12-
const args = ['run', options && options.verbose ? undefined : '--silent', cmd].filter(Boolean)
40+
return { bin: 'npm', args: npmArgs(cmd) }
41+
}
1342

14-
return { bin: 'npm', args }
43+
const parts = cmd.split(' ')
44+
let bin = parts[0]
45+
const args = parts.splice(1)
46+
47+
if (scripts[bin] !== undefined) {
48+
return { bin: 'npm', args: npmArgs(bin, args) }
1549
}
1650

1751
/*
@@ -32,10 +66,6 @@ module.exports = function findBin(cmd, packageJson, options) {
3266
* }
3367
*/
3468

35-
const parts = cmd.split(' ')
36-
let bin = parts[0]
37-
const args = parts.splice(1)
38-
3969
try {
4070
/* npm-which tries to resolve the bin in local node_modules/.bin */
4171
/* and if this fails it look in $PATH */

src/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ ${stringifyObject(config)}
4444
`)
4545
}
4646

47-
runAll(packageJson, config)
47+
const scripts = packageJson.scripts || {}
48+
49+
runAll(scripts, config)
4850
.then(() => {
4951
// No errors, exiting with 0
5052
process.exitCode = 0

src/runAll.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ const resolveGitDir = require('./resolveGitDir')
99

1010
/**
1111
* Executes all tasks and either resolves or rejects the promise
12-
* @param packageJson
12+
* @param scripts
1313
* @param config {Object}
1414
* @returns {Promise}
1515
*/
16-
module.exports = function runAll(packageJson, config) {
16+
module.exports = function runAll(scripts, config) {
1717
// Config validation
1818
if (!config || !has(config, 'gitDir') || !has(config, 'concurrent') || !has(config, 'renderer')) {
1919
throw new Error('Invalid config provided to runAll! Use getConfig instead.')
@@ -35,7 +35,7 @@ module.exports = function runAll(packageJson, config) {
3535
const tasks = generateTasks(config, filenames).map(task => ({
3636
title: `Running tasks for ${task.pattern}`,
3737
task: () =>
38-
new Listr(runScript(task.commands, task.fileList, packageJson, config), {
38+
new Listr(runScript(task.commands, task.fileList, scripts, config), {
3939
// In sub-tasks we don't want to run concurrently
4040
// and we want to abort on errors
4141
concurrent: false,

src/runScript.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const getConfig = require('./getConfig').getConfig
88
const calcChunkSize = require('./calcChunkSize')
99
const findBin = require('./findBin')
1010

11-
module.exports = function runScript(commands, pathsToLint, packageJson, config) {
11+
module.exports = function runScript(commands, pathsToLint, scripts, config) {
1212
const normalizedConfig = getConfig(config)
1313
const chunkSize = normalizedConfig.chunkSize
1414
const concurrency = normalizedConfig.subTaskConcurrency
@@ -22,7 +22,7 @@ module.exports = function runScript(commands, pathsToLint, packageJson, config)
2222
title: linter,
2323
task: () => {
2424
try {
25-
const res = findBin(linter, packageJson, config)
25+
const res = findBin(linter, scripts, config)
2626

2727
const separatorArgs = /npm(\.exe)?$/i.test(res.bin) ? ['--'] : []
2828

test/findBin.spec.js

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,47 @@ import findBin from '../src/findBin'
22

33
jest.mock('npm-which')
44

5-
const packageJSON = {
6-
scripts: {
7-
test: 'noop'
8-
},
9-
'lint-staged': {}
10-
}
5+
const scripts = { test: 'noop' }
116

127
describe('findBin', () => {
138
it('should favor `npm run` command if exists in both package.json and .bin/', () => {
14-
const packageJSONMock = {
15-
scripts: {
16-
'my-linter': 'my-linter'
17-
}
18-
}
19-
const { bin, args } = findBin('my-linter', packageJSONMock)
9+
const { bin, args } = findBin('my-linter', { 'my-linter': 'my-linter' })
2010
expect(bin).toEqual('npm')
2111
expect(args).toEqual(['run', '--silent', 'my-linter'])
2212
})
2313

2414
it('should return npm run command without --silent in verbose mode', () => {
25-
const packageJSONMock = {
26-
scripts: {
27-
eslint: 'eslint'
28-
}
29-
}
30-
const { bin, args } = findBin('eslint', packageJSONMock, { verbose: true })
15+
const { bin, args } = findBin('eslint', { eslint: 'eslint' }, { verbose: true })
3116
expect(bin).toEqual('npm')
3217
expect(args).toEqual(['run', 'eslint'])
3318
})
3419

20+
it('should resolve cmd defined in scripts with args', () => {
21+
const { bin, args } = findBin('kcd-scripts format', { 'kcd-scripts': 'node index.js' })
22+
expect(bin).toEqual('npm')
23+
expect(args).toEqual(['run', '--silent', 'kcd-scripts', 'format'])
24+
})
25+
26+
it('should resolve cmd defined in scripts with space in name', () => {
27+
const { bin, args } = findBin('my cmd', { 'my cmd': 'echo deal-with-it' })
28+
expect(bin).toEqual('npm')
29+
expect(args).toEqual(['run', '--silent', 'my cmd'])
30+
})
31+
3532
it('should return path to bin if there is no `script` with name in package.json', () => {
36-
const { bin, args } = findBin('my-linter', packageJSON)
33+
const { bin, args } = findBin('my-linter', scripts)
3734
expect(bin).toEqual('my-linter')
3835
expect(args).toEqual([])
3936
})
4037

4138
it('should throw an error if bin not found and there is no entry in scripts section', () => {
4239
expect(() => {
43-
findBin('my-missing-linter', packageJSON)
40+
findBin('my-missing-linter', scripts)
4441
}).toThrow('my-missing-linter could not be found. Try `npm install my-missing-linter`.')
4542
})
4643

4744
it('should parse cmd and add arguments to args', () => {
48-
const { bin, args } = findBin('my-linter task --fix', packageJSON)
45+
const { bin, args } = findBin('my-linter task --fix', scripts)
4946
expect(bin).toEqual('my-linter')
5047
expect(args).toEqual(['task', '--fix'])
5148
})

test/runAll.spec.js

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,42 +8,38 @@ sgfMock.mockImplementation((params, callback) => {
88
callback(null, [])
99
})
1010

11-
const packageJson = {
12-
scripts: {
13-
mytask: 'echo "Running task"'
14-
}
15-
}
11+
const scripts = { mytask: 'echo "Running task"' }
1612

1713
describe('runAll', () => {
1814
afterEach(() => {
1915
sgfMock.mockClear()
2016
})
2117
it('should throw when invalid config is provided', () => {
22-
expect(() => runAll(packageJson, {})).toThrowErrorMatchingSnapshot()
23-
expect(() => runAll(packageJson)).toThrowErrorMatchingSnapshot()
18+
expect(() => runAll(scripts, {})).toThrowErrorMatchingSnapshot()
19+
expect(() => runAll(scripts)).toThrowErrorMatchingSnapshot()
2420
})
2521

2622
it('should not throw when a valid config is provided', () => {
2723
const config = getConfig({
2824
concurrent: false
2925
})
30-
expect(() => runAll(packageJson, config)).not.toThrow()
26+
expect(() => runAll(scripts, config)).not.toThrow()
3127
})
3228

3329
it('should return a promise', () => {
34-
expect(runAll(packageJson, getConfig({}))).toBeInstanceOf(Promise)
30+
expect(runAll(scripts, getConfig({}))).toBeInstanceOf(Promise)
3531
})
3632

3733
it('should resolve the promise with no tasks', () => {
3834
expect.assertions(1)
39-
return expect(runAll(packageJson, getConfig({}))).resolves.toEqual('No tasks to run.')
35+
return expect(runAll(scripts, getConfig({}))).resolves.toEqual('No tasks to run.')
4036
})
4137

4238
it('should reject the promise when staged-git-files errors', () => {
4339
sgfMock.mockImplementation((params, callback) => {
4440
callback('test', undefined)
4541
})
4642
expect.assertions(1)
47-
return expect(runAll(packageJson, getConfig({}))).rejects.toEqual('test')
43+
return expect(runAll(scripts, getConfig({}))).rejects.toEqual('test')
4844
})
4945
})

test/runScript.spec.js

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,9 @@ import runScript from '../src/runScript'
44

55
jest.mock('execa')
66

7-
const packageJSON = {
8-
scripts: {
9-
test: 'noop',
10-
test2: 'noop'
11-
},
12-
'lint-staged': {}
7+
const scripts = {
8+
test: 'noop',
9+
test2: 'noop'
1310
}
1411

1512
describe('runScript', () => {
@@ -18,17 +15,17 @@ describe('runScript', () => {
1815
})
1916

2017
it('should return an array', () => {
21-
expect(runScript('test', ['test.js'], packageJSON)).toBeInstanceOf(Array)
18+
expect(runScript('test', ['test.js'], scripts)).toBeInstanceOf(Array)
2219
})
2320

2421
it('should throw for non-existend script', () => {
2522
expect(() => {
26-
runScript('missing-module', ['test.js'], packageJSON)[0].task()
23+
runScript('missing-module', ['test.js'], scripts)[0].task()
2724
}).toThrow()
2825
})
2926

3027
it('should work with a single command', async () => {
31-
const res = runScript('test', ['test.js'], packageJSON)
28+
const res = runScript('test', ['test.js'], scripts)
3229
expect(res.length).toBe(1)
3330
expect(res[0].title).toBe('test')
3431
expect(res[0].task).toBeInstanceOf(Function)
@@ -38,7 +35,7 @@ describe('runScript', () => {
3835
})
3936

4037
it('should work with multiple commands', async () => {
41-
const res = runScript(['test', 'test2'], ['test.js'], packageJSON)
38+
const res = runScript(['test', 'test2'], ['test.js'], scripts)
4239
expect(res.length).toBe(2)
4340
expect(res[0].title).toBe('test')
4441
expect(res[1].title).toBe('test2')
@@ -56,7 +53,7 @@ describe('runScript', () => {
5653
})
5754

5855
it('should respect chunk size', async () => {
59-
const res = runScript(['test'], ['test1.js', 'test2.js'], packageJSON, {
56+
const res = runScript(['test'], ['test1.js', 'test2.js'], scripts, {
6057
chunkSize: 1
6158
})
6259
const taskPromise = res[0].task()
@@ -68,7 +65,7 @@ describe('runScript', () => {
6865
})
6966

7067
it('should support non npm scripts', async () => {
71-
const res = runScript(['node --arg=true ./myscript.js', 'git add'], ['test.js'], packageJSON)
68+
const res = runScript(['node --arg=true ./myscript.js', 'git add'], ['test.js'], scripts)
7269
expect(res.length).toBe(2)
7370
expect(res[0].title).toBe('node --arg=true ./myscript.js')
7471
expect(res[1].title).toBe('git add')
@@ -89,7 +86,7 @@ describe('runScript', () => {
8986
})
9087

9188
it('should pass cwd to execa if gitDir option is set for non-npm tasks', async () => {
92-
const res = runScript(['test', 'git add'], ['test.js'], packageJSON, { gitDir: '../' })
89+
const res = runScript(['test', 'git add'], ['test.js'], scripts, { gitDir: '../' })
9390
let taskPromise = res[0].task()
9491
expect(taskPromise).toBeInstanceOf(Promise)
9592
await taskPromise
@@ -106,7 +103,7 @@ describe('runScript', () => {
106103
})
107104

108105
it('should not pass `gitDir` as `cwd` to `execa()` if a non-git binary is called', async () => {
109-
const res = runScript(['jest'], ['test.js'], packageJSON, { gitDir: '../' })
106+
const res = runScript(['jest'], ['test.js'], scripts, { gitDir: '../' })
110107
const taskPromise = res[0].task()
111108
expect(taskPromise).toBeInstanceOf(Promise)
112109
await taskPromise
@@ -115,7 +112,7 @@ describe('runScript', () => {
115112
})
116113

117114
it('should use --silent in non-verbose mode', async () => {
118-
const res = runScript('test', ['test.js'], packageJSON, { verbose: false })
115+
const res = runScript('test', ['test.js'], scripts, { verbose: false })
119116
const taskPromise = res[0].task()
120117
expect(taskPromise).toBeInstanceOf(Promise)
121118
await taskPromise
@@ -124,7 +121,7 @@ describe('runScript', () => {
124121
})
125122

126123
it('should not use --silent in verbose mode', async () => {
127-
const res = runScript('test', ['test.js'], packageJSON, { verbose: true })
124+
const res = runScript('test', ['test.js'], scripts, { verbose: true })
128125
const taskPromise = res[0].task()
129126
expect(taskPromise).toBeInstanceOf(Promise)
130127
await taskPromise
@@ -138,7 +135,7 @@ describe('runScript', () => {
138135
linterErr.stderr = ''
139136
mockFn.mockImplementationOnce(() => Promise.reject(linterErr))
140137

141-
const res = runScript('mock-fail-linter', ['test.js'], packageJSON)
138+
const res = runScript('mock-fail-linter', ['test.js'], scripts)
142139
const taskPromise = res[0].task()
143140
try {
144141
await taskPromise

0 commit comments

Comments
 (0)