diff --git a/lib/base-command.js b/lib/base-command.js index 0adff8e5d95ea..17e67987d99dc 100644 --- a/lib/base-command.js +++ b/lib/base-command.js @@ -103,6 +103,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 (typeof expected === 'boolean') { + if (Boolean(entries) !== expected) { + process.exitCode = 1 + } + } + // TODO `>5` or `<=6` which will require a custom Config type + } + async cmdExec (args) { const { config } = this.npm diff --git a/lib/commands/query.js b/lib/commands/query.js index b5f4d8e57ddf5..9fb031876b6b7 100644 --- a/lib/commands/query.js +++ b/lib/commands/query.js @@ -49,6 +49,7 @@ class Query extends BaseCommand { 'workspace', 'workspaces', 'include-workspace-root', + 'expect-entries', ] get parsedResponse () { @@ -68,6 +69,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) } @@ -90,6 +92,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 dd3d9946af819..d122fbdc25b6a 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 4170dd9078c86..ae13f95b903ab 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 08d908688fdd7..b51cf7a902517 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", @@ -2390,6 +2400,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", @@ -3770,6 +3781,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 @@ -3781,6 +3793,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 2b9a5b4976323..adbed442a5298 100644 --- a/test/lib/commands/query.js +++ b/test/lib/commands/query.js @@ -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: { @@ -179,3 +180,70 @@ t.test('global', async t => { await npm.exec('query', ['[name=lorem]']) t.matchSnapshot(joinedOutput(), 'should return global package') }) + +t.test('expect entries', t => { + 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, + config: { '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, + config: { '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, + config: { '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, + config: { '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, + config: { '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, + config: { 'expect-entries': 1 }, + }) + await npm.exec('query', ['#b']) + t.equal(joinedOutput(), '[]', 'does not have entries') + t.ok(process.exitCode, 'exits with code') + }) + t.end() +})