Skip to content

Commit 1d7de6a

Browse files
committed
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.
1 parent d04111d commit 1d7de6a

File tree

5 files changed

+127
-0
lines changed

5 files changed

+127
-0
lines changed

lib/base-command.js

+19
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const { relative } = require('path')
55
const { definitions } = require('@npmcli/config/lib/definitions')
66
const getWorkspaces = require('./workspaces/get-workspaces.js')
77
const { aliases: cmdAliases } = require('./utils/cmd-list')
8+
const log = require('./utils/log-shim.js')
89

910
class BaseCommand {
1011
static workspaces = false
@@ -142,6 +143,24 @@ class BaseCommand {
142143
return this.exec(args)
143144
}
144145

146+
// Compare the number of entries with what was expected
147+
checkExpected (entries) {
148+
if (!this.npm.config.isDefault('expect-results')) {
149+
const expected = this.npm.config.get('expect-results')
150+
if (!!entries !== !!expected) {
151+
log.warn(this.name, `Expected ${expected ? '' : 'no '}results, got ${entries}`)
152+
process.exitCode = 1
153+
}
154+
} else if (!this.npm.config.isDefault('expect-result-count')) {
155+
const expected = this.npm.config.get('expect-result-count')
156+
if (expected !== entries) {
157+
/* eslint-disable-next-line max-len */
158+
log.warn(this.name, `Expected ${expected} result${expected === 1 ? '' : 's'}, got ${entries}`)
159+
process.exitCode = 1
160+
}
161+
}
162+
}
163+
145164
async setWorkspaces () {
146165
const includeWorkspaceRoot = this.isArboristCmd
147166
? false

lib/commands/query.js

+3
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class Query extends BaseCommand {
5050
'workspaces',
5151
'include-workspace-root',
5252
'package-lock-only',
53+
'expect-results',
5354
]
5455

5556
get parsedResponse () {
@@ -81,6 +82,7 @@ class Query extends BaseCommand {
8182
const items = await tree.querySelectorAll(args[0], this.npm.flatOptions)
8283
this.buildResponse(items)
8384

85+
this.checkExpected(this.#response.length)
8486
this.npm.output(this.parsedResponse)
8587
}
8688

@@ -104,6 +106,7 @@ class Query extends BaseCommand {
104106
}
105107
this.buildResponse(items)
106108
}
109+
this.checkExpected(this.#response.length)
107110
this.npm.output(this.parsedResponse)
108111
}
109112

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

+2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna
5050
"dry-run": false,
5151
"editor": "{EDITOR}",
5252
"engine-strict": false,
53+
"expect-entries": null,
5354
"fetch-retries": 2,
5455
"fetch-retry-factor": 10,
5556
"fetch-retry-maxtimeout": 60000,
@@ -207,6 +208,7 @@ diff-unified = 3
207208
dry-run = false
208209
editor = "{EDITOR}"
209210
engine-strict = false
211+
expect-entries = null
210212
fetch-retries = 2
211213
fetch-retry-factor = 10
212214
fetch-retry-maxtimeout = 60000

test/lib/commands/query.js

+83
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: {
@@ -237,3 +238,85 @@ t.test('package-lock-only', t => {
237238
})
238239
t.end()
239240
})
241+
242+
t.test('expect entries', t => {
243+
const { exitCode } = process
244+
t.afterEach(() => process.exitCode = exitCode)
245+
const prefixDir = {
246+
node_modules: {
247+
a: { name: 'a', version: '1.0.0' },
248+
},
249+
'package.json': JSON.stringify({
250+
name: 'project',
251+
dependencies: { a: '^1.0.0' },
252+
}),
253+
}
254+
t.test('false, has entries', async t => {
255+
const { logs, npm, joinedOutput } = await loadMockNpm(t, {
256+
prefixDir,
257+
})
258+
npm.config.set('expect-results', false)
259+
await npm.exec('query', ['#a'])
260+
t.not(joinedOutput(), '[]', 'has entries')
261+
t.same(logs.warn, [['query', 'Expected no results, got 1']])
262+
t.ok(process.exitCode, 'exits with code')
263+
})
264+
t.test('false, no entries', async t => {
265+
const { npm, joinedOutput } = await loadMockNpm(t, {
266+
prefixDir,
267+
})
268+
npm.config.set('expect-results', false)
269+
await npm.exec('query', ['#b'])
270+
t.equal(joinedOutput(), '[]', 'does not have entries')
271+
t.notOk(process.exitCode, 'exits without code')
272+
})
273+
t.test('true, has entries', async t => {
274+
const { npm, joinedOutput } = await loadMockNpm(t, {
275+
prefixDir,
276+
})
277+
npm.config.set('expect-results', true)
278+
await npm.exec('query', ['#a'])
279+
t.not(joinedOutput(), '[]', 'has entries')
280+
t.notOk(process.exitCode, 'exits without code')
281+
})
282+
t.test('true, no entries', async t => {
283+
const { logs, npm, joinedOutput } = await loadMockNpm(t, {
284+
prefixDir,
285+
})
286+
npm.config.set('expect-results', true)
287+
await npm.exec('query', ['#b'])
288+
t.equal(joinedOutput(), '[]', 'does not have entries')
289+
t.same(logs.warn, [['query', 'Expected results, got 0']])
290+
t.ok(process.exitCode, 'exits with code')
291+
})
292+
t.test('count, matches', async t => {
293+
const { npm, joinedOutput } = await loadMockNpm(t, {
294+
prefixDir,
295+
})
296+
npm.config.set('expect-result-count', 1)
297+
await npm.exec('query', ['#a'])
298+
t.not(joinedOutput(), '[]', 'has entries')
299+
t.notOk(process.exitCode, 'exits without code')
300+
})
301+
t.test('count 1, does not match', async t => {
302+
const { logs, npm, joinedOutput } = await loadMockNpm(t, {
303+
prefixDir,
304+
})
305+
npm.config.set('expect-result-count', 1)
306+
await npm.exec('query', ['#b'])
307+
t.equal(joinedOutput(), '[]', 'does not have entries')
308+
t.same(logs.warn, [['query', 'Expected 1 result, got 0']])
309+
t.ok(process.exitCode, 'exits with code')
310+
})
311+
t.test('count 3, does not match', async t => {
312+
const { logs, npm, joinedOutput } = await loadMockNpm(t, {
313+
prefixDir,
314+
})
315+
npm.config.set('expect-result-count', 3)
316+
await npm.exec('query', ['#b'])
317+
t.equal(joinedOutput(), '[]', 'does not have entries')
318+
t.same(logs.warn, [['query', 'Expected 3 results, got 0']])
319+
t.ok(process.exitCode, 'exits with code')
320+
})
321+
t.end()
322+
})

workspaces/config/lib/definitions/definitions.js

+20
Original file line numberDiff line numberDiff line change
@@ -665,6 +665,26 @@ define('engine-strict', {
665665
flatten,
666666
})
667667

668+
define('expect-results', {
669+
default: null,
670+
type: [null, Boolean],
671+
exclusive: ['expect-result-count'],
672+
description: `
673+
Tells npm whether or not to expect results from the command.
674+
Can be either true (expect some results) or false (expect no results).
675+
`,
676+
})
677+
678+
define('expect-result-count', {
679+
default: null,
680+
type: [null, Number],
681+
hint: '<count>',
682+
exclusive: ['expect-results'],
683+
description: `
684+
Tells to expect a specific number of results from the command.
685+
`,
686+
})
687+
668688
define('fetch-retries', {
669689
default: 2,
670690
type: Number,

0 commit comments

Comments
 (0)