Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add --expect-results and --exepect-result-count to npm query #5966

Merged
merged 2 commits into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
6 changes: 3 additions & 3 deletions docs/lib/content/using-npm/dependency-selectors.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ Some examples:
The `:outdated` pseudo selector retrieves data from the registry and returns information about which of your dependencies are outdated. The type parameter may be one of the following:

- `any` (default) a version exists that is greater than the current one
- `in-range` a version exists that is greater than the current one, and satisfies at least one if its dependents
- `out-of-range` a version exists that is greater than the current one, does not satisfy at least one of its dependents
- `in-range` a version exists that is greater than the current one, and satisfies at least one if its parent's dependencies
- `out-of-range` a version exists that is greater than the current one, does not satisfy at least one of its parent's dependencies
- `major` a version exists that is a semver major greater than the current one
- `minor` a version exists that is a semver minor greater than the current one
- `patch` a version exists that is a semver patch greater than the current one
Expand All @@ -99,7 +99,7 @@ In addition to the filtering performed by the pseudo selector, some extra data i
Some examples:

- `:root > :outdated(major)` returns every direct dependency that has a new semver major release
- `.prod:outdated(in-range)` returns production dependencies that have a new release that satisfies at least one of its edges in
- `.prod:outdated(in-range)` returns production dependencies that have a new release that satisfies at least one of its parent's dependencies

#### [Attribute Selectors](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors)

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
}
}
}

wraithgar marked this conversation as resolved.
Show resolved Hide resolved
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
Loading