Skip to content

Commit

Permalink
feat: add --expect-entries to npm query
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
wraithgar committed Feb 22, 2024
1 parent d6bc684 commit 4f3ddbb
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 9 deletions.
31 changes: 22 additions & 9 deletions docs/lib/content/commands/npm-query.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,19 +133,32 @@ npm query ":type(git)" | jq 'map(.name)' | xargs -I {} npm why {}
},
...
```
### Package lock only mode

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).
### Expecting a certain number of results

One common use of `npm query` is to make sure there is only one version of
a certain dependency in your tree. This is especially common for
ecosystems like that rely on `typescript` where having state split
across two different but identically-named packages causes bugs. You
can use the `--expect-results` or `--expect-result-count` in your setup
to ensure that npm will exit with an exit code if your tree doesn't look
like you want it to.


```sh
$ npm query '#react' --expect-result-count=1
```

Perhaps you want to quickly check if there are any production
dependencies that could be updated:

```sh
$ npm query ':root>:outdated(in-range).prod' --no-expect-results
```

### Package lock only mode

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).
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).

### Configuration

Expand Down
19 changes: 19 additions & 0 deletions lib/base-command.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { relative } = require('path')
const { definitions } = require('@npmcli/config/lib/definitions')
const getWorkspaces = require('./workspaces/get-workspaces.js')
const { aliases: cmdAliases } = require('./utils/cmd-list')
const log = require('./utils/log-shim.js')

class BaseCommand {
static workspaces = false
Expand Down Expand Up @@ -142,6 +143,24 @@ class BaseCommand {
return this.exec(args)
}

// Compare the number of entries with what was expected
checkExpected (entries) {
if (!this.npm.config.isDefault('expect-results')) {
const expected = this.npm.config.get('expect-results')
if (!!entries !== !!expected) {
log.warn(this.name, `Expected ${expected ? '' : 'no '}results, got ${entries}`)
process.exitCode = 1
}
} else if (!this.npm.config.isDefault('expect-result-count')) {
const expected = this.npm.config.get('expect-result-count')
if (expected !== entries) {
/* eslint-disable-next-line max-len */
log.warn(this.name, `Expected ${expected} result${expected === 1 ? '' : 's'}, got ${entries}`)
process.exitCode = 1
}
}
}

async setWorkspaces () {
const includeWorkspaceRoot = this.isArboristCmd
? false
Expand Down
3 changes: 3 additions & 0 deletions lib/commands/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class Query extends BaseCommand {
'workspaces',
'include-workspace-root',
'package-lock-only',
'expect-results',
]

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

this.checkExpected(this.#response.length)
this.npm.output(this.parsedResponse)
}

Expand All @@ -104,6 +106,7 @@ class Query extends BaseCommand {
}
this.buildResponse(items)
}
this.checkExpected(this.#response.length)
this.npm.output(this.parsedResponse)
}

Expand Down
4 changes: 4 additions & 0 deletions tap-snapshots/test/lib/commands/config.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna
"dry-run": false,
"editor": "{EDITOR}",
"engine-strict": false,
"expect-results": null,
"expect-result-count": null,
"fetch-retries": 2,
"fetch-retry-factor": 10,
"fetch-retry-maxtimeout": 60000,
Expand Down Expand Up @@ -207,6 +209,8 @@ diff-unified = 3
dry-run = false
editor = "{EDITOR}"
engine-strict = false
expect-result-count = null
expect-results = null
fetch-retries = 2
fetch-retry-factor = 10
fetch-retry-maxtimeout = 60000
Expand Down
26 changes: 26 additions & 0 deletions tap-snapshots/test/lib/docs.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,25 @@ This can be overridden by setting the \`--force\` flag.
#### \`expect-result-count\`
* Default: null
* Type: null or Number
Tells to expect a specific number of results from the command.
This config can not be used with: \`expect-results\`
#### \`expect-results\`
* Default: null
* Type: null or Boolean
Tells npm whether or not to expect results from the command. Can be either
true (expect some results) or false (expect no results).
This config can not be used with: \`expect-result-count\`
#### \`fetch-retries\`
* Default: 2
Expand Down Expand Up @@ -2074,6 +2093,8 @@ Array [
"dry-run",
"editor",
"engine-strict",
"expect-results",
"expect-result-count",
"fetch-retries",
"fetch-retry-factor",
"fetch-retry-maxtimeout",
Expand Down Expand Up @@ -2325,6 +2346,8 @@ Array [

exports[`test/lib/docs.js TAP config > keys that are not flattened 1`] = `
Array [
"expect-results",
"expect-result-count",
"init-author-email",
"init-author-name",
"init-author-url",
Expand Down Expand Up @@ -3869,6 +3892,7 @@ Options:
[-g|--global]
[-w|--workspace <workspace-name> [-w|--workspace <workspace-name> ...]]
[-ws|--workspaces] [--include-workspace-root] [--package-lock-only]
[--expect-results|--expect-result-count <count>]
Run "npm help query" for more info
Expand All @@ -3881,6 +3905,8 @@ npm query <selector>
#### \`workspaces\`
#### \`include-workspace-root\`
#### \`package-lock-only\`
#### \`expect-results\`
#### \`expect-result-count\`
`

exports[`test/lib/docs.js TAP usage rebuild > must match snapshot 1`] = `
Expand Down
83 changes: 83 additions & 0 deletions test/lib/commands/query.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ t.test('recursive tree', async t => {
await npm.exec('query', ['*'])
t.matchSnapshot(joinedOutput(), 'should return everything in the tree, accounting for recursion')
})

t.test('workspace query', async t => {
const { npm, joinedOutput } = await loadMockNpm(t, {
config: {
Expand Down Expand Up @@ -237,3 +238,85 @@ t.test('package-lock-only', t => {
})
t.end()
})

t.test('expect entries', t => {
const { exitCode } = process
t.afterEach(() => process.exitCode = exitCode)
const prefixDir = {
node_modules: {
a: { name: 'a', version: '1.0.0' },
},
'package.json': JSON.stringify({
name: 'project',
dependencies: { a: '^1.0.0' },
}),
}
t.test('false, has entries', async t => {
const { logs, npm, joinedOutput } = await loadMockNpm(t, {
prefixDir,
})
npm.config.set('expect-results', false)
await npm.exec('query', ['#a'])
t.not(joinedOutput(), '[]', 'has entries')
t.same(logs.warn, [['query', 'Expected no results, got 1']])
t.ok(process.exitCode, 'exits with code')
})
t.test('false, no entries', async t => {
const { npm, joinedOutput } = await loadMockNpm(t, {
prefixDir,
})
npm.config.set('expect-results', false)
await npm.exec('query', ['#b'])
t.equal(joinedOutput(), '[]', 'does not have entries')
t.notOk(process.exitCode, 'exits without code')
})
t.test('true, has entries', async t => {
const { npm, joinedOutput } = await loadMockNpm(t, {
prefixDir,
})
npm.config.set('expect-results', true)
await npm.exec('query', ['#a'])
t.not(joinedOutput(), '[]', 'has entries')
t.notOk(process.exitCode, 'exits without code')
})
t.test('true, no entries', async t => {
const { logs, npm, joinedOutput } = await loadMockNpm(t, {
prefixDir,
})
npm.config.set('expect-results', true)
await npm.exec('query', ['#b'])
t.equal(joinedOutput(), '[]', 'does not have entries')
t.same(logs.warn, [['query', 'Expected results, got 0']])
t.ok(process.exitCode, 'exits with code')
})
t.test('count, matches', async t => {
const { npm, joinedOutput } = await loadMockNpm(t, {
prefixDir,
})
npm.config.set('expect-result-count', 1)
await npm.exec('query', ['#a'])
t.not(joinedOutput(), '[]', 'has entries')
t.notOk(process.exitCode, 'exits without code')
})
t.test('count 1, does not match', async t => {
const { logs, npm, joinedOutput } = await loadMockNpm(t, {
prefixDir,
})
npm.config.set('expect-result-count', 1)
await npm.exec('query', ['#b'])
t.equal(joinedOutput(), '[]', 'does not have entries')
t.same(logs.warn, [['query', 'Expected 1 result, got 0']])
t.ok(process.exitCode, 'exits with code')
})
t.test('count 3, does not match', async t => {
const { logs, npm, joinedOutput } = await loadMockNpm(t, {
prefixDir,
})
npm.config.set('expect-result-count', 3)
await npm.exec('query', ['#b'])
t.equal(joinedOutput(), '[]', 'does not have entries')
t.same(logs.warn, [['query', 'Expected 3 results, got 0']])
t.ok(process.exitCode, 'exits with code')
})
t.end()
})
20 changes: 20 additions & 0 deletions workspaces/config/lib/definitions/definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,26 @@ define('engine-strict', {
flatten,
})

define('expect-results', {
default: null,
type: [null, Boolean],
exclusive: ['expect-result-count'],
description: `
Tells npm whether or not to expect results from the command.
Can be either true (expect some results) or false (expect no results).
`,
})

define('expect-result-count', {
default: null,
type: [null, Number],
hint: '<count>',
exclusive: ['expect-results'],
description: `
Tells to expect a specific number of results from the command.
`,
})

define('fetch-retries', {
default: 2,
type: Number,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ Object {
"engine-strict": Array [
"boolean value (true or false)",
],
"expect-result-count": Array [
null,
"numeric value",
],
"expect-results": Array [
null,
"boolean value (true or false)",
],
"fetch-retries": Array [
"numeric value",
],
Expand Down

0 comments on commit 4f3ddbb

Please sign in to comment.