From b00d21ebe6ddb622a908a72c26d7c89dbe7879b0 Mon Sep 17 00:00:00 2001 From: Gar Date: Wed, 14 Dec 2022 10:52:05 -0800 Subject: [PATCH] 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. --- lib/base-command.js | 22 ++++++ lib/commands/query.js | 3 + lib/utils/config/definitions.js | 10 +++ .../test/lib/commands/config.js.test.cjs | 2 + tap-snapshots/test/lib/docs.js.test.cjs | 13 ++++ test/lib/commands/query.js | 70 +++++++++++++++++++ 6 files changed, 120 insertions(+) diff --git a/lib/base-command.js b/lib/base-command.js index b57b7474a5efb..a56e2ab5b0753 100644 --- a/lib/base-command.js +++ b/lib/base-command.js @@ -98,6 +98,28 @@ class BaseCommand { }) } + // Compare the number of entries with what was expected + checkExpected (entries) { + const expected = this.npm.config.get('expect-entries') + if (expected === null) { + // By default we do nothing + return + } + if (typeof expected === 'number') { + if (entries !== expected) { + process.exitCode = 1 + } + return + } + // entries is boolean + if (expected === true && !entries) { + process.exitCode = 1 + } else if (expected === false && !!entries) { + process.exitCode = 1 + } + // TODO `>5` or `<=6` which will require a custom Config type + } + async execWorkspaces (args, filters) { throw Object.assign(new Error('This command does not support workspaces.'), { code: 'ENOWORKSPACES', diff --git a/lib/commands/query.js b/lib/commands/query.js index 5f05ab3164d7c..a4eb8435559e2 100644 --- a/lib/commands/query.js +++ b/lib/commands/query.js @@ -48,6 +48,7 @@ class Query extends BaseCommand { 'workspace', 'workspaces', 'include-workspace-root', + 'expect-entries', ] get parsedResponse () { @@ -67,6 +68,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) } @@ -89,6 +91,7 @@ class Query extends BaseCommand { } this.buildResponse(items) } + this.checkExpected(this.#response.length) this.npm.output(this.parsedResponse) } diff --git a/lib/utils/config/definitions.js b/lib/utils/config/definitions.js index 0f401d6572a59..a62207d0161ad 100644 --- a/lib/utils/config/definitions.js +++ b/lib/utils/config/definitions.js @@ -650,6 +650,16 @@ define('engine-strict', { flatten, }) +define('expect-entries', { + default: null, + type: [null, Boolean, Number], + description: ` + Tells npm how many entries to expect from the command. Can be either + true (expect some entries), false (expect no entries), or a number to match + exactly. + `, +}) + define('fetch-retries', { default: 2, type: Number, diff --git a/tap-snapshots/test/lib/commands/config.js.test.cjs b/tap-snapshots/test/lib/commands/config.js.test.cjs index 93a3e9ac4eebf..f89c83d7ff298 100644 --- a/tap-snapshots/test/lib/commands/config.js.test.cjs +++ b/tap-snapshots/test/lib/commands/config.js.test.cjs @@ -48,6 +48,7 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna "dry-run": false, "editor": "{EDITOR}", "engine-strict": false, + "expect-entries": null, "fetch-retries": 2, "fetch-retry-factor": 10, "fetch-retry-maxtimeout": 60000, @@ -201,6 +202,7 @@ diff-unified = 3 dry-run = false editor = "{EDITOR}" engine-strict = false +expect-entries = null fetch-retries = 2 fetch-retry-factor = 10 fetch-retry-maxtimeout = 60000 diff --git a/tap-snapshots/test/lib/docs.js.test.cjs b/tap-snapshots/test/lib/docs.js.test.cjs index 8038dfa30b6c2..4d508cfbe5585 100644 --- a/tap-snapshots/test/lib/docs.js.test.cjs +++ b/tap-snapshots/test/lib/docs.js.test.cjs @@ -891,6 +891,15 @@ Node.js version. This can be overridden by setting the \`--force\` flag. +#### \`expect-entries\` + +* Default: null +* Type: null, Boolean, or Number + +Tells npm how many entries to expect from the command. Can be either true +(expect some entries), false (expect no entries), or a number to match +exactly. + #### \`fetch-retries\` * Default: 2 @@ -2150,6 +2159,7 @@ Array [ "dry-run", "editor", "engine-strict", + "expect-entries", "fetch-retries", "fetch-retry-factor", "fetch-retry-maxtimeout", @@ -2389,6 +2399,7 @@ Array [ exports[`test/lib/docs.js TAP config > keys that are not flattened 1`] = ` Array [ + "expect-entries", "init-author-email", "init-author-name", "init-author-url", @@ -3730,6 +3741,7 @@ Options: [-g|--global] [-w|--workspace [-w|--workspace ...]] [-ws|--workspaces] [--include-workspace-root] +[--no-expect-entries|--expect-entries ] Run "npm help query" for more info @@ -3741,6 +3753,7 @@ npm query #### \`workspace\` #### \`workspaces\` #### \`include-workspace-root\` +#### \`expect-entries\` ` exports[`test/lib/docs.js TAP usage rebuild > must match snapshot 1`] = ` diff --git a/test/lib/commands/query.js b/test/lib/commands/query.js index fb5b4843c34ee..af07327da8314 100644 --- a/test/lib/commands/query.js +++ b/test/lib/commands/query.js @@ -68,6 +68,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: { @@ -197,3 +198,72 @@ t.test('global', async t => { await npm.exec('query', ['[name=lorem]']) t.matchSnapshot(joinedOutput(), 'should return global package') }) + +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 { npm, joinedOutput } = await loadMockNpm(t, { + prefixDir, + }) + npm.config.set('expect-entries', false) + await npm.exec('query', ['#a']) + t.not(joinedOutput(), '[]', 'has entries') + 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-entries', 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-entries', 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 { npm, joinedOutput } = await loadMockNpm(t, { + prefixDir, + }) + npm.config.set('expect-entries', true) + await npm.exec('query', ['#b']) + t.equal(joinedOutput(), '[]', 'does not have entries') + t.ok(process.exitCode, 'exits with code') + }) + t.test('number, matches', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + prefixDir, + }) + npm.config.set('expect-entries', 1) + await npm.exec('query', ['#a']) + t.not(joinedOutput(), '[]', 'has entries') + t.notOk(process.exitCode, 'exits without code') + }) + t.test('number, does not match', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + prefixDir, + }) + npm.config.set('expect-entries', 1) + await npm.exec('query', ['#b']) + t.equal(joinedOutput(), '[]', 'does not have entries') + t.ok(process.exitCode, 'exits with code') + }) + t.end() +})