From ae285b39191f3a0c4edfb045a334057bef4567b5 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Sun, 13 Jun 2021 10:16:22 +1200 Subject: [PATCH] feat(ls): support `--package-lock-only` flag This enables using the virtual tree instead of node_modules. PR-URL: https://github.com/npm/cli/pull/3408 Credit: @G-Rath Close: #3408 Reviewed-by: @isaacs --- lib/ls.js | 13 +- lib/utils/config/definitions.js | 10 +- .../test/lib/load-all-commands.js.test.cjs | 4 +- .../lib/utils/config/describe-all.js.test.cjs | 10 +- .../test/lib/utils/npm-usage.js.test.cjs | 4 +- test/lib/ls.js | 786 ++++++++++++++++++ 6 files changed, 816 insertions(+), 11 deletions(-) diff --git a/lib/ls.js b/lib/ls.js index 319439fcc8f48..b425bd620b38e 100644 --- a/lib/ls.js +++ b/lib/ls.js @@ -50,6 +50,7 @@ class LS extends ArboristWorkspaceCmd { 'depth', 'omit', 'link', + 'package-lock-only', '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 packageLockOnly = this.npm.config.get('package-lock-only') 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, packageLockOnly }) // 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, packageLockOnly }) { + const tree = await ( + packageLockOnly + ? 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..b6f7c84317c6a 100644 --- a/lib/utils/config/definitions.js +++ b/lib/utils/config/definitions.js @@ -1333,8 +1333,14 @@ define('package-lock-only', { default: false, type: Boolean, description: ` - If set to true, it will update only the \`package-lock.json\`, instead of - checking \`node_modules\` and downloading dependencies. + If set to true, the current operation will only use the \`package-lock.json\`, + ignoring \`node_modules\`. + + For \`update\` this means only the \`package-lock.json\` will be updated, + instead of checking \`node_modules\` and downloading dependencies. + + For \`list\` this means the output will be based on the tree described by the + \`package-lock.json\`, rather than the contents of \`node_modules\`. `, flatten, }) 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 ff6a5fb321e94..70902ba10cf33 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] +[--package-lock-only] [--unicode] [-w|--workspace [-w|--workspace ...]] [-ws|--workspaces] @@ -588,7 +588,7 @@ npm ls [[<@scope>/] ...] Options: [-a|--all] [--json] [-l|--long] [-p|--parseable] [-g|--global] [--depth ] [--omit [--omit ...]] [--link] -[--unicode] +[--package-lock-only] [--unicode] [-w|--workspace [-w|--workspace ...]] [-ws|--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..b7bcca8d539a1 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 @@ -781,8 +781,14 @@ package-locks disabled use \`npm prune\`. * Default: false * Type: Boolean -If set to true, it will update only the \`package-lock.json\`, instead of -checking \`node_modules\` and downloading dependencies. +If set to true, the current operation will only use the \`package-lock.json\`, +ignoring \`node_modules\`. + +For \`update\` this means only the \`package-lock.json\` will be updated, +instead of checking \`node_modules\` and downloading dependencies. + +For \`list\` this means the output will be based on the tree described by the +\`package-lock.json\`, rather than the contents of \`node_modules\`. #### \`parseable\` 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 e5ae7d827a6ca..dc10b43739b15 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] + [--package-lock-only] [--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] + [--package-lock-only] [--unicode] [-w|--workspace [-w|--workspace ...]] [-ws|--workspaces] diff --git a/test/lib/ls.js b/test/lib/ls.js index ecdede809df20..582416f4aa2d2 100644 --- a/test/lib/ls.js +++ b/test/lib/ls.js @@ -107,6 +107,7 @@ const config = { only: null, parseable: false, production: false, + 'package-lock-only': false, } const flatOptions = { } @@ -4152,3 +4153,788 @@ t.test('ls --json', (t) => { t.end() }) + +t.test('ls --package-lock-only', (t) => { + config['package-lock-only'] = true + t.test('ls --package-lock-only --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---package-lock-only-ls---package-lock-only---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---package-lock-only-ls---package-lock-only---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() +})