Skip to content

Commit c7dc71d

Browse files
wraithgarlukekarrys
authored andcommitted
feat: add --expect-entries to npm query
This will allow users to tell npm whether or not to exit with an exit code depending on if the command had any resulting entries or not, or a specific number of entries.
1 parent 122938c commit c7dc71d

6 files changed

Lines changed: 120 additions & 0 deletions

File tree

lib/base-command.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,28 @@ class BaseCommand {
103103
})
104104
}
105105

106+
// Compare the number of entries with what was expected
107+
checkExpected (entries) {
108+
const expected = this.npm.config.get('expect-entries')
109+
if (expected === null) {
110+
// By default we do nothing
111+
return
112+
}
113+
if (typeof expected === 'number') {
114+
if (entries !== expected) {
115+
process.exitCode = 1
116+
}
117+
return
118+
}
119+
// entries is boolean
120+
if (expected === true && !entries) {
121+
process.exitCode = 1
122+
} else if (expected === false && !!entries) {
123+
process.exitCode = 1
124+
}
125+
// TODO `>5` or `<=6` which will require a custom Config type
126+
}
127+
106128
async cmdExec (args) {
107129
const { config } = this.npm
108130

lib/commands/query.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class Query extends BaseCommand {
4949
'workspace',
5050
'workspaces',
5151
'include-workspace-root',
52+
'expect-entries',
5253
]
5354

5455
get parsedResponse () {
@@ -68,6 +69,7 @@ class Query extends BaseCommand {
6869
const items = await tree.querySelectorAll(args[0], this.npm.flatOptions)
6970
this.buildResponse(items)
7071

72+
this.checkExpected(this.#response.length)
7173
this.npm.output(this.parsedResponse)
7274
}
7375

@@ -90,6 +92,7 @@ class Query extends BaseCommand {
9092
}
9193
this.buildResponse(items)
9294
}
95+
this.checkExpected(this.#response.length)
9396
this.npm.output(this.parsedResponse)
9497
}
9598

lib/utils/config/definitions.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,16 @@ define('engine-strict', {
650650
flatten,
651651
})
652652

653+
define('expect-entries', {
654+
default: null,
655+
type: [null, Boolean, Number],
656+
description: `
657+
Tells npm how many entries to expect from the command. Can be either
658+
true (expect some entries), false (expect no entries), or a number to match
659+
exactly.
660+
`,
661+
})
662+
653663
define('fetch-retries', {
654664
default: 2,
655665
type: Number,

tap-snapshots/test/lib/commands/config.js.test.cjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna
4848
"dry-run": false,
4949
"editor": "{EDITOR}",
5050
"engine-strict": false,
51+
"expect-entries": null,
5152
"fetch-retries": 2,
5253
"fetch-retry-factor": 10,
5354
"fetch-retry-maxtimeout": 60000,
@@ -201,6 +202,7 @@ diff-unified = 3
201202
dry-run = false
202203
editor = "{EDITOR}"
203204
engine-strict = false
205+
expect-entries = null
204206
fetch-retries = 2
205207
fetch-retry-factor = 10
206208
fetch-retry-maxtimeout = 60000

tap-snapshots/test/lib/docs.js.test.cjs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,15 @@ Node.js version.
891891
892892
This can be overridden by setting the \`--force\` flag.
893893
894+
#### \`expect-entries\`
895+
896+
* Default: null
897+
* Type: null, Boolean, or Number
898+
899+
Tells npm how many entries to expect from the command. Can be either true
900+
(expect some entries), false (expect no entries), or a number to match
901+
exactly.
902+
894903
#### \`fetch-retries\`
895904
896905
* Default: 2
@@ -2150,6 +2159,7 @@ Array [
21502159
"dry-run",
21512160
"editor",
21522161
"engine-strict",
2162+
"expect-entries",
21532163
"fetch-retries",
21542164
"fetch-retry-factor",
21552165
"fetch-retry-maxtimeout",
@@ -2390,6 +2400,7 @@ Array [
23902400

23912401
exports[`test/lib/docs.js TAP config > keys that are not flattened 1`] = `
23922402
Array [
2403+
"expect-entries",
23932404
"init-author-email",
23942405
"init-author-name",
23952406
"init-author-url",
@@ -3770,6 +3781,7 @@ Options:
37703781
[-g|--global]
37713782
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
37723783
[-ws|--workspaces] [--include-workspace-root]
3784+
[--no-expect-entries|--expect-entries <expect-entries>]
37733785
37743786
Run "npm help query" for more info
37753787
@@ -3781,6 +3793,7 @@ npm query <selector>
37813793
#### \`workspace\`
37823794
#### \`workspaces\`
37833795
#### \`include-workspace-root\`
3796+
#### \`expect-entries\`
37843797
`
37853798

37863799
exports[`test/lib/docs.js TAP usage rebuild > must match snapshot 1`] = `

test/lib/commands/query.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ t.test('recursive tree', async t => {
6161
await npm.exec('query', ['*'])
6262
t.matchSnapshot(joinedOutput(), 'should return everything in the tree, accounting for recursion')
6363
})
64+
6465
t.test('workspace query', async t => {
6566
const { npm, joinedOutput } = await loadMockNpm(t, {
6667
config: {
@@ -179,3 +180,72 @@ t.test('global', async t => {
179180
await npm.exec('query', ['[name=lorem]'])
180181
t.matchSnapshot(joinedOutput(), 'should return global package')
181182
})
183+
184+
t.test('expect entries', t => {
185+
const { exitCode } = process
186+
t.afterEach(() => process.exitCode = exitCode)
187+
const prefixDir = {
188+
node_modules: {
189+
a: { name: 'a', version: '1.0.0' },
190+
},
191+
'package.json': JSON.stringify({
192+
name: 'project',
193+
dependencies: { a: '^1.0.0' },
194+
}),
195+
}
196+
t.test('false, has entries', async t => {
197+
const { npm, joinedOutput } = await loadMockNpm(t, {
198+
prefixDir,
199+
})
200+
npm.config.set('expect-entries', false)
201+
await npm.exec('query', ['#a'])
202+
t.not(joinedOutput(), '[]', 'has entries')
203+
t.ok(process.exitCode, 'exits with code')
204+
})
205+
t.test('false, no entries', async t => {
206+
const { npm, joinedOutput } = await loadMockNpm(t, {
207+
prefixDir,
208+
})
209+
npm.config.set('expect-entries', false)
210+
await npm.exec('query', ['#b'])
211+
t.equal(joinedOutput(), '[]', 'does not have entries')
212+
t.notOk(process.exitCode, 'exits without code')
213+
})
214+
t.test('true, has entries', async t => {
215+
const { npm, joinedOutput } = await loadMockNpm(t, {
216+
prefixDir,
217+
})
218+
npm.config.set('expect-entries', true)
219+
await npm.exec('query', ['#a'])
220+
t.not(joinedOutput(), '[]', 'has entries')
221+
t.notOk(process.exitCode, 'exits without code')
222+
})
223+
t.test('true, no entries', async t => {
224+
const { npm, joinedOutput } = await loadMockNpm(t, {
225+
prefixDir,
226+
})
227+
npm.config.set('expect-entries', true)
228+
await npm.exec('query', ['#b'])
229+
t.equal(joinedOutput(), '[]', 'does not have entries')
230+
t.ok(process.exitCode, 'exits with code')
231+
})
232+
t.test('number, matches', async t => {
233+
const { npm, joinedOutput } = await loadMockNpm(t, {
234+
prefixDir,
235+
})
236+
npm.config.set('expect-entries', 1)
237+
await npm.exec('query', ['#a'])
238+
t.not(joinedOutput(), '[]', 'has entries')
239+
t.notOk(process.exitCode, 'exits without code')
240+
})
241+
t.test('number, does not match', async t => {
242+
const { npm, joinedOutput } = await loadMockNpm(t, {
243+
prefixDir,
244+
})
245+
npm.config.set('expect-entries', 1)
246+
await npm.exec('query', ['#b'])
247+
t.equal(joinedOutput(), '[]', 'does not have entries')
248+
t.ok(process.exitCode, 'exits with code')
249+
})
250+
t.end()
251+
})

0 commit comments

Comments
 (0)