Skip to content

Commit 4f3ddbb

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 d6bc684 commit 4f3ddbb

File tree

8 files changed

+185
-9
lines changed

8 files changed

+185
-9
lines changed

docs/lib/content/commands/npm-query.md

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -133,19 +133,32 @@ npm query ":type(git)" | jq 'map(.name)' | xargs -I {} npm why {}
133133
},
134134
...
135135
```
136-
### Package lock only mode
137136

138-
If package-lock-only is enabled, only the information in the package
139-
lock (or shrinkwrap) is loaded. This means that information from the
140-
package.json files of your dependencies will not be included in the
141-
result set (e.g. description, homepage, engines).
137+
### Expecting a certain number of results
138+
139+
One common use of `npm query` is to make sure there is only one version of
140+
a certain dependency in your tree. This is especially common for
141+
ecosystems like that rely on `typescript` where having state split
142+
across two different but identically-named packages causes bugs. You
143+
can use the `--expect-results` or `--expect-result-count` in your setup
144+
to ensure that npm will exit with an exit code if your tree doesn't look
145+
like you want it to.
146+
147+
148+
```sh
149+
$ npm query '#react' --expect-result-count=1
150+
```
151+
152+
Perhaps you want to quickly check if there are any production
153+
dependencies that could be updated:
154+
155+
```sh
156+
$ npm query ':root>:outdated(in-range).prod' --no-expect-results
157+
```
142158

143159
### Package lock only mode
144160

145-
If package-lock-only is enabled, only the information in the package
146-
lock (or shrinkwrap) is loaded. This means that information from the
147-
package.json files of your dependencies will not be included in the
148-
result set (e.g. description, homepage, engines).
161+
If package-lock-only is enabled, only the information in the package lock (or shrinkwrap) is loaded. This means that information from the package.json files of your dependencies will not be included in the result set (e.g. description, homepage, engines).
149162

150163
### Configuration
151164

lib/base-command.js

Lines changed: 19 additions & 0 deletions
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

Lines changed: 3 additions & 0 deletions
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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ 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-results": null,
54+
"expect-result-count": null,
5355
"fetch-retries": 2,
5456
"fetch-retry-factor": 10,
5557
"fetch-retry-maxtimeout": 60000,
@@ -207,6 +209,8 @@ diff-unified = 3
207209
dry-run = false
208210
editor = "{EDITOR}"
209211
engine-strict = false
212+
expect-result-count = null
213+
expect-results = null
210214
fetch-retries = 2
211215
fetch-retry-factor = 10
212216
fetch-retry-maxtimeout = 60000

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,25 @@ This can be overridden by setting the \`--force\` flag.
537537
538538
539539
540+
#### \`expect-result-count\`
541+
542+
* Default: null
543+
* Type: null or Number
544+
545+
Tells to expect a specific number of results from the command.
546+
547+
This config can not be used with: \`expect-results\`
548+
549+
#### \`expect-results\`
550+
551+
* Default: null
552+
* Type: null or Boolean
553+
554+
Tells npm whether or not to expect results from the command. Can be either
555+
true (expect some results) or false (expect no results).
556+
557+
This config can not be used with: \`expect-result-count\`
558+
540559
#### \`fetch-retries\`
541560
542561
* Default: 2
@@ -2074,6 +2093,8 @@ Array [
20742093
"dry-run",
20752094
"editor",
20762095
"engine-strict",
2096+
"expect-results",
2097+
"expect-result-count",
20772098
"fetch-retries",
20782099
"fetch-retry-factor",
20792100
"fetch-retry-maxtimeout",
@@ -2325,6 +2346,8 @@ Array [
23252346

23262347
exports[`test/lib/docs.js TAP config > keys that are not flattened 1`] = `
23272348
Array [
2349+
"expect-results",
2350+
"expect-result-count",
23282351
"init-author-email",
23292352
"init-author-name",
23302353
"init-author-url",
@@ -3869,6 +3892,7 @@ Options:
38693892
[-g|--global]
38703893
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
38713894
[-ws|--workspaces] [--include-workspace-root] [--package-lock-only]
3895+
[--expect-results|--expect-result-count <count>]
38723896
38733897
Run "npm help query" for more info
38743898
@@ -3881,6 +3905,8 @@ npm query <selector>
38813905
#### \`workspaces\`
38823906
#### \`include-workspace-root\`
38833907
#### \`package-lock-only\`
3908+
#### \`expect-results\`
3909+
#### \`expect-result-count\`
38843910
`
38853911

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

test/lib/commands/query.js

Lines changed: 83 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: {
@@ -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

Lines changed: 20 additions & 0 deletions
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,

workspaces/config/tap-snapshots/test/type-description.js.test.cjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,14 @@ Object {
139139
"engine-strict": Array [
140140
"boolean value (true or false)",
141141
],
142+
"expect-result-count": Array [
143+
null,
144+
"numeric value",
145+
],
146+
"expect-results": Array [
147+
null,
148+
"boolean value (true or false)",
149+
],
142150
"fetch-retries": Array [
143151
"numeric value",
144152
],

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy