diff --git a/lib/ls.js b/lib/ls.js index d92b73ddfcdbb..f39249c0e93af 100644 --- a/lib/ls.js +++ b/lib/ls.js @@ -50,6 +50,7 @@ class LS extends ArboristWorkspaceCmd { 'depth', 'omit', 'link', + 'virtual', 'unicode', ...super.params, ] @@ -79,6 +80,7 @@ class LS extends ArboristWorkspaceCmd { const prod = this.npm.config.get('prod') const production = this.npm.config.get('production') const unicode = this.npm.config.get('unicode') + const virtual = this.npm.config.get('virtual') const path = global ? resolve(this.npm.globalDir, '..') : this.npm.prefix @@ -88,7 +90,7 @@ class LS extends ArboristWorkspaceCmd { legacyPeerDeps: false, path, }) - const tree = await this.initTree({arb, args }) + const tree = await this.initTree({arb, args, virtual }) // filters by workspaces nodes when using -w // We only have to filter the first layer of edges, so we don't @@ -216,8 +218,13 @@ class LS extends ArboristWorkspaceCmd { } } - async initTree ({ arb, args }) { - const tree = await arb.loadActual() + async initTree ({ arb, args, virtual }) { + const tree = await ( + virtual + ? arb.loadVirtual() + : arb.loadActual() + ) + tree[_include] = args.length === 0 tree[_depth] = 0 diff --git a/lib/utils/config/definitions.js b/lib/utils/config/definitions.js index ce7702aaa4f79..04dd5e0bd7814 100644 --- a/lib/utils/config/definitions.js +++ b/lib/utils/config/definitions.js @@ -2054,6 +2054,15 @@ define('viewer', { `, }) +define('virtual', { + default: false, + type: Boolean, + description: ` + Operates in "virtual" mode, meaning that the dependency tree is based off + the lockfile rather than the contents of \`node_modules\`. + `, +}) + define('which', { default: null, hint: '', diff --git a/tap-snapshots/test/lib/load-all-commands.js.test.cjs b/tap-snapshots/test/lib/load-all-commands.js.test.cjs index 097123d46a3cc..58dd01182b634 100644 --- a/tap-snapshots/test/lib/load-all-commands.js.test.cjs +++ b/tap-snapshots/test/lib/load-all-commands.js.test.cjs @@ -538,7 +538,7 @@ npm ll [[<@scope>/] ...] Options: [-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global] [--depth ] [--omit [--omit ...]] [--link] -[--unicode] +[--virtual] [--unicode] [-w|--workspace [-w|--workspace ...]] [-ws|--workspaces] @@ -588,7 +588,7 @@ npm ls npm ls [[<@scope>/] ...] Options: [-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global] [--depth ] [--omit [--omit ...]] [--link] -[--unicode] +[--virtual] [--unicode] [-w|--workspace [-w|--workspace ...]] [-ws|--workspaces] diff --git a/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs b/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs index 32443c57af35b..97be57eb5c3a1 100644 --- a/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs +++ b/tap-snapshots/test/lib/utils/config/definitions.js.test.cjs @@ -144,6 +144,7 @@ Array [ "version", "versions", "viewer", + "virtual", "which", "workspace", "workspaces", diff --git a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs index da8cd1794f2ac..82fe02f49a913 100644 --- a/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs +++ b/tap-snapshots/test/lib/utils/config/describe-all.js.test.cjs @@ -1211,6 +1211,14 @@ The program to use to view help content. Set to \`"browser"\` to view html help content in the default web browser. +#### \`virtual\` + +* Default: false +* Type: Boolean + +Operates in "virtual" mode, meaning that the dependency tree is based off +the lockfile rather than the contents of \`node_modules\`. + #### \`which\` * Default: null diff --git a/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs b/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs index 54f6c3d2feb2a..454a4b0d31438 100644 --- a/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs +++ b/tap-snapshots/test/lib/utils/npm-usage.js.test.cjs @@ -639,7 +639,7 @@ All commands: Options: [-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global] [--depth ] [--omit [--omit ...]] [--link] - [--unicode] + [--virtual] [--unicode] [-w|--workspace [-w|--workspace ...]] [-ws|--workspaces] @@ -683,7 +683,7 @@ All commands: Options: [-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global] [--depth ] [--omit [--omit ...]] [--link] - [--unicode] + [--virtual] [--unicode] [-w|--workspace [-w|--workspace ...]] [-ws|--workspaces] diff --git a/test/lib/ls.js b/test/lib/ls.js index ecdede809df20..cac80529fafef 100644 --- a/test/lib/ls.js +++ b/test/lib/ls.js @@ -107,6 +107,7 @@ const config = { only: null, parseable: false, production: false, + virtual: false, } const flatOptions = { } @@ -4152,3 +4153,788 @@ t.test('ls --json', (t) => { t.end() }) + +t.test('ls --virtual', (t) => { + config.virtual = true + t.test('ls --virtual --json', (t) => { + t.beforeEach(cleanUpResult) + config.json = true + config.parseable = false + t.test('no args', (t) => { + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^1.0.0', + chai: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec([], (err) => { + t.error(err, 'npm ls') + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: { + version: '1.0.0', + dependencies: { + dog: { + version: '1.0.0', + }, + }, + }, + chai: { + version: '1.0.0', + }, + }, + }, + 'should output json representation of dependencies structure' + ) + t.end() + }) + }) + + t.test('extraneous deps', (t) => { + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec([], (err) => { + t.error(err) // should not error for extraneous + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: { + version: '1.0.0', + dependencies: { + dog: { + version: '1.0.0', + }, + }, + }, + }, + }, + 'should output json containing no problem info' + ) + t.end() + }) + }) + + t.test('missing deps --long', (t) => { + config.long = true + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^1.0.0', + dog: '^1.0.0', + chai: '^1.0.0', + ipsum: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + ipsum: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec([], (err) => { + t.error(err, 'npm ls') + t.match( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + }, + 'should output json containing no problems info' + ) + config.long = false + t.end() + }) + }) + + t.test('with filter arg', (t) => { + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^1.0.0', + chai: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + ipsum: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec(['chai'], (err) => { + t.error(err, 'npm ls') + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + chai: { + version: '1.0.0', + }, + }, + }, + 'should output json contaning only occurrences of filtered by package' + ) + t.equal( + process.exitCode, + 0, + 'should exit with error code 0' + ) + t.end() + }) + }) + + t.test('with filter arg nested dep', (t) => { + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^1.0.0', + chai: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + ipsum: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec(['dog'], (err) => { + t.error(err, 'npm ls') + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: { + version: '1.0.0', + dependencies: { + dog: { + version: '1.0.0', + }, + }, + }, + }, + }, + 'should output json contaning only occurrences of filtered by package' + ) + t.end() + }) + }) + + t.test('with multiple filter args', (t) => { + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^1.0.0', + chai: '^1.0.0', + ipsum: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + ipsum: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec(['dog@*', 'chai@1.0.0'], (err) => { + t.error(err, 'npm ls') + t.same( + jsonParse(result), + { + version: '1.0.0', + name: 'test-npm-ls', + dependencies: { + foo: { + version: '1.0.0', + dependencies: { + dog: { + version: '1.0.0', + }, + }, + }, + chai: { + version: '1.0.0', + }, + }, + }, + 'should output json contaning only occurrences of multiple filtered packages and their ancestors' + ) + t.end() + }) + }) + + t.test('with missing filter arg', (t) => { + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^1.0.0', + chai: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec(['notadep'], (err) => { + t.error(err, 'npm ls') + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + }, + 'should output json containing no dependencies info' + ) + t.equal( + process.exitCode, + 1, + 'should exit with error code 1' + ) + process.exitCode = 0 + t.end() + }) + }) + + t.test('default --depth value should now be 0', (t) => { + config.all = false + config.depth = undefined + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^1.0.0', + chai: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec([], (err) => { + t.error(err, 'npm ls') + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + }, + }, + 'should output json containing only top-level dependencies' + ) + config.all = true + config.depth = Infinity + t.end() + }) + }) + + t.test('--depth=0', (t) => { + config.all = false + config.depth = 0 + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^1.0.0', + chai: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec([], (err) => { + t.error(err, 'npm ls') + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + }, + }, + 'should output json containing only top-level dependencies' + ) + config.all = true + config.depth = Infinity + t.end() + }) + }) + + t.test('--depth=1', (t) => { + config.all = false + config.depth = 1 + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^1.0.0', + chai: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec([], (err) => { + t.error(err, 'npm ls') + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: { + version: '1.0.0', + dependencies: { + dog: { + version: '1.0.0', + }, + }, + }, + chai: { + version: '1.0.0', + }, + }, + }, + 'should output json containing top-level deps and their deps only' + ) + config.all = true + config.depth = Infinity + t.end() + }) + }) + + t.test('missing/invalid/extraneous', (t) => { + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + foo: '^2.0.0', + ipsum: '^1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + foo: { + version: '1.0.0', + requires: { + dog: '^1.0.0', + }, + }, + dog: { + version: '1.0.0', + }, + chai: { + version: '1.0.0', + }, + }, + }), + }) + ls.exec([], (err) => { + t.match(err, { code: 'ELSPROBLEMS' }, 'should list dep problems') + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + problems: [ + 'invalid: foo@1.0.0 {CWD}/tap-testdir-ls-ls---virtual-ls---virtual---json-missing-invalid-extraneous/node_modules/foo', + 'missing: ipsum@^1.0.0, required by test-npm-ls@1.0.0', + ], + dependencies: { + foo: { + version: '1.0.0', + invalid: true, + problems: [ + 'invalid: foo@1.0.0 {CWD}/tap-testdir-ls-ls---virtual-ls---virtual---json-missing-invalid-extraneous/node_modules/foo', + ], + dependencies: { + dog: { + version: '1.0.0', + }, + }, + }, + ipsum: { + required: '^1.0.0', + missing: true, + problems: [ + 'missing: ipsum@^1.0.0, required by test-npm-ls@1.0.0', + ], + }, + }, + }, + 'should output json containing top-level deps and their deps only' + ) + t.end() + }) + }) + + t.test('from lockfile', (t) => { + npm.prefix = t.testdir({ + 'package-lock.json': JSON.stringify({ + name: 'dedupe-lockfile', + version: '1.0.0', + lockfileVersion: 2, + requires: true, + packages: { + '': { + name: 'dedupe-lockfile', + version: '1.0.0', + dependencies: { + '@isaacs/dedupe-tests-a': '1.0.1', + '@isaacs/dedupe-tests-b': '1||2', + }, + }, + 'node_modules/@isaacs/dedupe-tests-a': { + name: '@isaacs/dedupe-tests-a', + version: '1.0.1', + resolved: 'https://registry.npmjs.org/@isaacs/dedupe-tests-a/-/dedupe-tests-a-1.0.1.tgz', + integrity: 'sha512-8AN9lNCcBt5Xeje7fMEEpp5K3rgcAzIpTtAjYb/YMUYu8SbIVF6wz0WqACDVKvpQOUcSfNHZQNLNmue0QSwXOQ==', + dependencies: { + '@isaacs/dedupe-tests-b': '1', + }, + }, + 'node_modules/@isaacs/dedupe-tests-a/node_modules/@isaacs/dedupe-tests-b': { + name: '@isaacs/dedupe-tests-b', + version: '1.0.0', + resolved: 'https://registry.npmjs.org/@isaacs/dedupe-tests-b/-/dedupe-tests-b-1.0.0.tgz', + integrity: 'sha512-3nmvzIb8QL8OXODzipwoV3U8h9OQD9g9RwOPuSBQqjqSg9JZR1CCFOWNsDUtOfmwY8HFUJV9EAZ124uhqVxq+w==', + }, + 'node_modules/@isaacs/dedupe-tests-b': { + name: '@isaacs/dedupe-tests-b', + version: '2.0.0', + resolved: 'https://registry.npmjs.org/@isaacs/dedupe-tests-b/-/dedupe-tests-b-2.0.0.tgz', + integrity: 'sha512-KTYkpRv9EzlmCg4Gsm/jpclWmRYFCXow8GZKJXjK08sIZBlElTZEa5Bw/UQxIvEfcKmWXczSqItD49Kr8Ax4UA==', + }, + }, + dependencies: { + '@isaacs/dedupe-tests-a': { + version: '1.0.1', + resolved: 'https://registry.npmjs.org/@isaacs/dedupe-tests-a/-/dedupe-tests-a-1.0.1.tgz', + integrity: 'sha512-8AN9lNCcBt5Xeje7fMEEpp5K3rgcAzIpTtAjYb/YMUYu8SbIVF6wz0WqACDVKvpQOUcSfNHZQNLNmue0QSwXOQ==', + requires: { + '@isaacs/dedupe-tests-b': '1', + }, + dependencies: { + '@isaacs/dedupe-tests-b': { + version: '1.0.0', + resolved: 'https://registry.npmjs.org/@isaacs/dedupe-tests-b/-/dedupe-tests-b-1.0.0.tgz', + integrity: 'sha512-3nmvzIb8QL8OXODzipwoV3U8h9OQD9g9RwOPuSBQqjqSg9JZR1CCFOWNsDUtOfmwY8HFUJV9EAZ124uhqVxq+w==', + }, + }, + }, + '@isaacs/dedupe-tests-b': { + version: '2.0.0', + resolved: 'https://registry.npmjs.org/@isaacs/dedupe-tests-b/-/dedupe-tests-b-2.0.0.tgz', + integrity: 'sha512-KTYkpRv9EzlmCg4Gsm/jpclWmRYFCXow8GZKJXjK08sIZBlElTZEa5Bw/UQxIvEfcKmWXczSqItD49Kr8Ax4UA==', + }, + }, + }), + 'package.json': JSON.stringify({ + name: 'dedupe-lockfile', + version: '1.0.0', + dependencies: { + '@isaacs/dedupe-tests-a': '1.0.1', + '@isaacs/dedupe-tests-b': '1||2', + }, + }), + }) + ls.exec([], () => { + t.same( + jsonParse(result), + { + version: '1.0.0', + name: 'dedupe-lockfile', + dependencies: { + '@isaacs/dedupe-tests-a': { + version: '1.0.1', + resolved: 'https://registry.npmjs.org/@isaacs/dedupe-tests-a/-/dedupe-tests-a-1.0.1.tgz', + dependencies: { + '@isaacs/dedupe-tests-b': { + version: '1.0.0', + resolved: 'https://registry.npmjs.org/@isaacs/dedupe-tests-b/-/dedupe-tests-b-1.0.0.tgz', + }, + }, + }, + '@isaacs/dedupe-tests-b': { + version: '2.0.0', + resolved: 'https://registry.npmjs.org/@isaacs/dedupe-tests-b/-/dedupe-tests-b-2.0.0.tgz', + }, + }, + }, + 'should output json containing only prod deps' + ) + t.end() + }) + }) + + t.test('using aliases', (t) => { + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + a: 'npm:b@1.0.0', + }, + }), + 'package-lock.json': JSON.stringify({ + dependencies: { + a: { + version: 'npm:b@1.0.0', + resolved: 'https://localhost:8080/abbrev/-/abbrev-1.0.0.tgz', + }, + }, + }), + }) + ls.exec([], () => { + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + a: { + version: '1.0.0', + resolved: 'https://localhost:8080/abbrev/-/abbrev-1.0.0.tgz', + }, + }, + }, + 'should output json containing aliases' + ) + t.end() + }) + }) + + t.test('resolved points to git ref', (t) => { + config.long = false + npm.prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + abbrev: 'git+https://github.com/isaacs/abbrev-js.git', + }, + }), + 'package-lock.json': JSON.stringify({ + name: 'test-npm-ls', + version: '1.0.0', + lockfileVersion: 2, + requires: true, + dependencies: { + abbrev: { + version: 'git+ssh://git@github.com/isaacs/abbrev-js.git#b8f3a2fc0c3bb8ffd8b0d0072cc6b5a3667e963c', + from: 'abbrev@git+https://github.com/isaacs/abbrev-js.git', + }, + }, + } + ), + }) + ls.exec([], () => { + t.same( + jsonParse(result), + { + name: 'test-npm-ls', + version: '1.0.0', + dependencies: { + abbrev: { + resolved: 'git+ssh://git@github.com/isaacs/abbrev-js.git#b8f3a2fc0c3bb8ffd8b0d0072cc6b5a3667e963c', + }, + }, + }, + 'should output json containing git refs' + ) + t.end() + }) + }) + + t.end() + }) + + t.end() +})