From e9e548c187691830c17cc2f45c6787e90c4b6b8d Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Tue, 2 Nov 2021 17:29:19 -0700 Subject: [PATCH] fix: shrinkwrap setting incorrect lockfileVersion Fix: #3962 When created from a hidden lockfile, shrinkwrap was always setting the lockfileVersion to 3. The shrinkwrap command also needed to be updated to log the correct message based on the lockfileVersion config instead of the static lockfileVersion. With these fixes, it was possible to log a better message whenever we are changing the lockfileVersion, either from a config or any existing lockfile. Lastly, the tests for shrinkwrap were completely refactored to use the real npm and test all conceivable scenarios, as well as removing usage of `util.promisify` in favor of `fs.promises` now that we've dropped support for Node 10. --- lib/commands/shrinkwrap.js | 37 +- .../test/lib/commands/shrinkwrap.js.test.cjs | 362 +++++++++++++ tap-snapshots/test/lib/shrinkwrap.js.test.cjs | 362 +++++++++++++ test/lib/commands/shrinkwrap.js | 508 +++++++----------- 4 files changed, 946 insertions(+), 323 deletions(-) create mode 100644 tap-snapshots/test/lib/commands/shrinkwrap.js.test.cjs create mode 100644 tap-snapshots/test/lib/shrinkwrap.js.test.cjs diff --git a/lib/commands/shrinkwrap.js b/lib/commands/shrinkwrap.js index 722d26c90dc73..42489a27f5bfb 100644 --- a/lib/commands/shrinkwrap.js +++ b/lib/commands/shrinkwrap.js @@ -1,7 +1,5 @@ const { resolve, basename } = require('path') -const util = require('util') -const fs = require('fs') -const { unlink } = fs.promises || { unlink: util.promisify(fs.unlink) } +const { unlink } = require('fs').promises const Arborist = require('@npmcli/arborist') const log = require('npmlog') @@ -21,7 +19,6 @@ class Shrinkwrap extends BaseCommand { // if has a npm-shrinkwrap.json, nothing to do // if has a package-lock.json, rename to npm-shrinkwrap.json // if has neither, load the actual tree and save that as npm-shrinkwrap.json - // in all cases, re-cast to current lockfile version // // loadVirtual, fall back to loadActual // rename shrinkwrap file type, and tree.meta.save() @@ -40,17 +37,37 @@ class Shrinkwrap extends BaseCommand { const oldFilename = meta.filename const notSW = !newFile && basename(oldFilename) !== 'npm-shrinkwrap.json' + // The computed lockfile version of a hidden lockfile is always 3 + // even if the actual value of the property is a different. + // When shrinkwrap is run with only a hidden lockfile we want to + // set the shrinkwrap lockfile version as whatever was explicitly + // requested with a fallback to the actual value from the hidden + // lockfile. + if (meta.hiddenLockfile) { + meta.lockfileVersion = arb.options.lockfileVersion || + meta.originalLockfileVersion + } meta.hiddenLockfile = false meta.filename = sw await meta.save() - if (newFile) - log.notice('', 'created a lockfile as npm-shrinkwrap.json') - else if (notSW) { + const updatedVersion = meta.originalLockfileVersion !== meta.lockfileVersion + ? meta.lockfileVersion + : null + + if (newFile) { + let message = 'created a lockfile as npm-shrinkwrap.json' + if (updatedVersion) + message += ` with version ${updatedVersion}` + log.notice('', message) + } else if (notSW) { await unlink(oldFilename) - log.notice('', 'package-lock.json has been renamed to npm-shrinkwrap.json') - } else if (meta.originalLockfileVersion !== this.npm.lockfileVersion) - log.notice('', `npm-shrinkwrap.json updated to version ${this.npm.lockfileVersion}`) + let message = 'package-lock.json has been renamed to npm-shrinkwrap.json' + if (updatedVersion) + message += ` and updated to version ${updatedVersion}` + log.notice('', message) + } else if (updatedVersion) + log.notice('', `npm-shrinkwrap.json updated to version ${updatedVersion}`) else log.notice('', 'npm-shrinkwrap.json up to date') } diff --git a/tap-snapshots/test/lib/commands/shrinkwrap.js.test.cjs b/tap-snapshots/test/lib/commands/shrinkwrap.js.test.cjs new file mode 100644 index 0000000000000..9b7b989bbf9e6 --- /dev/null +++ b/tap-snapshots/test/lib/commands/shrinkwrap.js.test.cjs @@ -0,0 +1,362 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/lib/commands/shrinkwrap.js TAP with hidden lockfile ancient > must match snapshot 1`] = ` + localPrefix = { + "node_modules": { + ".package-lock.json": { + "lockfileVersion": 1 + } + } + } + config = {} + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-hidden-lockfile-ancient", + "lockfileVersion": 1, + "requires": true + } + logs = [ + "created a lockfile as npm-shrinkwrap.json" + ] +` + +exports[`test/lib/commands/shrinkwrap.js TAP with hidden lockfile ancient upgrade > must match snapshot 1`] = ` + localPrefix = { + "node_modules": { + ".package-lock.json": { + "lockfileVersion": 1 + } + } + } + config = { + "lockfileVersion": 3 + } + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-hidden-lockfile-ancient-upgrade", + "lockfileVersion": 3, + "requires": true, + "packages": {} + } + logs = [ + "created a lockfile as npm-shrinkwrap.json with version 3" + ] +` + +exports[`test/lib/commands/shrinkwrap.js TAP with hidden lockfile existing > must match snapshot 1`] = ` + localPrefix = { + "node_modules": { + ".package-lock.json": { + "lockfileVersion": 2 + } + } + } + config = {} + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-hidden-lockfile-existing", + "lockfileVersion": 2, + "requires": true, + "packages": {} + } + logs = [ + "created a lockfile as npm-shrinkwrap.json" + ] +` + +exports[`test/lib/commands/shrinkwrap.js TAP with hidden lockfile existing downgrade > must match snapshot 1`] = ` + localPrefix = { + "node_modules": { + ".package-lock.json": { + "lockfileVersion": 2 + } + } + } + config = { + "lockfileVersion": 1 + } + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-hidden-lockfile-existing-downgrade", + "lockfileVersion": 1, + "requires": true + } + logs = [ + "created a lockfile as npm-shrinkwrap.json with version 1" + ] +` + +exports[`test/lib/commands/shrinkwrap.js TAP with hidden lockfile existing upgrade > must match snapshot 1`] = ` + localPrefix = { + "node_modules": { + ".package-lock.json": { + "lockfileVersion": 2 + } + } + } + config = { + "lockfileVersion": 3 + } + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-hidden-lockfile-existing-upgrade", + "lockfileVersion": 3, + "requires": true, + "packages": {} + } + logs = [ + "created a lockfile as npm-shrinkwrap.json with version 3" + ] +` + +exports[`test/lib/commands/shrinkwrap.js TAP with nothing ancient > must match snapshot 1`] = ` + localPrefix = {} + config = {} + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-nothing-ancient", + "lockfileVersion": 2, + "requires": true, + "packages": {} + } + logs = [ + "created a lockfile as npm-shrinkwrap.json with version 2" + ] +` + +exports[`test/lib/commands/shrinkwrap.js TAP with nothing ancient upgrade > must match snapshot 1`] = ` + localPrefix = {} + config = { + "lockfileVersion": 3 + } + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-nothing-ancient-upgrade", + "lockfileVersion": 3, + "requires": true, + "packages": {} + } + logs = [ + "created a lockfile as npm-shrinkwrap.json with version 3" + ] +` + +exports[`test/lib/commands/shrinkwrap.js TAP with npm-shrinkwrap.json ancient > must match snapshot 1`] = ` + localPrefix = { + "npm-shrinkwrap.json": { + "lockfileVersion": 1 + } + } + config = {} + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-ancient", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-ancient" + } + } + } + logs = [ + "npm-shrinkwrap.json updated to version 2" + ] +` + +exports[`test/lib/commands/shrinkwrap.js TAP with npm-shrinkwrap.json ancient upgrade > must match snapshot 1`] = ` + localPrefix = { + "npm-shrinkwrap.json": { + "lockfileVersion": 1 + } + } + config = { + "lockfileVersion": 3 + } + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-ancient-upgrade", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-ancient-upgrade" + } + } + } + logs = [ + "npm-shrinkwrap.json updated to version 3" + ] +` + +exports[`test/lib/commands/shrinkwrap.js TAP with npm-shrinkwrap.json existing > must match snapshot 1`] = ` + localPrefix = { + "npm-shrinkwrap.json": { + "lockfileVersion": 2 + } + } + config = {} + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-existing", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-existing" + } + } + } + logs = [ + "npm-shrinkwrap.json up to date" + ] +` + +exports[`test/lib/commands/shrinkwrap.js TAP with npm-shrinkwrap.json existing downgrade > must match snapshot 1`] = ` + localPrefix = { + "npm-shrinkwrap.json": { + "lockfileVersion": 2 + } + } + config = { + "lockfileVersion": 1 + } + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-existing-downgrade", + "lockfileVersion": 1, + "requires": true + } + logs = [ + "npm-shrinkwrap.json updated to version 1" + ] +` + +exports[`test/lib/commands/shrinkwrap.js TAP with npm-shrinkwrap.json existing upgrade > must match snapshot 1`] = ` + localPrefix = { + "npm-shrinkwrap.json": { + "lockfileVersion": 2 + } + } + config = { + "lockfileVersion": 3 + } + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-existing-upgrade", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-existing-upgrade" + } + } + } + logs = [ + "npm-shrinkwrap.json updated to version 3" + ] +` + +exports[`test/lib/commands/shrinkwrap.js TAP with package-lock.json ancient > must match snapshot 1`] = ` + localPrefix = { + "package-lock.json": { + "lockfileVersion": 1 + } + } + config = {} + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-package-lock.json-ancient", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "tap-testdir-shrinkwrap-with-package-lock.json-ancient" + } + } + } + logs = [ + "package-lock.json has been renamed to npm-shrinkwrap.json and updated to version 2" + ] +` + +exports[`test/lib/commands/shrinkwrap.js TAP with package-lock.json ancient upgrade > must match snapshot 1`] = ` + localPrefix = { + "package-lock.json": { + "lockfileVersion": 1 + } + } + config = { + "lockfileVersion": 3 + } + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-package-lock.json-ancient-upgrade", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tap-testdir-shrinkwrap-with-package-lock.json-ancient-upgrade" + } + } + } + logs = [ + "package-lock.json has been renamed to npm-shrinkwrap.json and updated to version 3" + ] +` + +exports[`test/lib/commands/shrinkwrap.js TAP with package-lock.json existing > must match snapshot 1`] = ` + localPrefix = { + "package-lock.json": { + "lockfileVersion": 2 + } + } + config = {} + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-package-lock.json-existing", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "tap-testdir-shrinkwrap-with-package-lock.json-existing" + } + } + } + logs = [ + "package-lock.json has been renamed to npm-shrinkwrap.json" + ] +` + +exports[`test/lib/commands/shrinkwrap.js TAP with package-lock.json existing downgrade > must match snapshot 1`] = ` + localPrefix = { + "package-lock.json": { + "lockfileVersion": 2 + } + } + config = { + "lockfileVersion": 1 + } + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-package-lock.json-existing-downgrade", + "lockfileVersion": 1, + "requires": true + } + logs = [ + "package-lock.json has been renamed to npm-shrinkwrap.json and updated to version 1" + ] +` + +exports[`test/lib/commands/shrinkwrap.js TAP with package-lock.json existing upgrade > must match snapshot 1`] = ` + localPrefix = { + "package-lock.json": { + "lockfileVersion": 2 + } + } + config = { + "lockfileVersion": 3 + } + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-package-lock.json-existing-upgrade", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tap-testdir-shrinkwrap-with-package-lock.json-existing-upgrade" + } + } + } + logs = [ + "package-lock.json has been renamed to npm-shrinkwrap.json and updated to version 3" + ] +` diff --git a/tap-snapshots/test/lib/shrinkwrap.js.test.cjs b/tap-snapshots/test/lib/shrinkwrap.js.test.cjs new file mode 100644 index 0000000000000..e966d6650c11f --- /dev/null +++ b/tap-snapshots/test/lib/shrinkwrap.js.test.cjs @@ -0,0 +1,362 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/lib/shrinkwrap.js TAP with hidden lockfile ancient > must match snapshot 1`] = ` + localPrefix = { + "node_modules": { + ".package-lock.json": { + "lockfileVersion": 1 + } + } + } + config = {} + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-hidden-lockfile-ancient", + "lockfileVersion": 1, + "requires": true + } + logs = [ + "created a lockfile as npm-shrinkwrap.json" + ] +` + +exports[`test/lib/shrinkwrap.js TAP with hidden lockfile ancient upgrade > must match snapshot 1`] = ` + localPrefix = { + "node_modules": { + ".package-lock.json": { + "lockfileVersion": 1 + } + } + } + config = { + "lockfileVersion": 3 + } + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-hidden-lockfile-ancient-upgrade", + "lockfileVersion": 3, + "requires": true, + "packages": {} + } + logs = [ + "created a lockfile as npm-shrinkwrap.json with version 3" + ] +` + +exports[`test/lib/shrinkwrap.js TAP with hidden lockfile existing > must match snapshot 1`] = ` + localPrefix = { + "node_modules": { + ".package-lock.json": { + "lockfileVersion": 2 + } + } + } + config = {} + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-hidden-lockfile-existing", + "lockfileVersion": 2, + "requires": true, + "packages": {} + } + logs = [ + "created a lockfile as npm-shrinkwrap.json" + ] +` + +exports[`test/lib/shrinkwrap.js TAP with hidden lockfile existing downgrade > must match snapshot 1`] = ` + localPrefix = { + "node_modules": { + ".package-lock.json": { + "lockfileVersion": 2 + } + } + } + config = { + "lockfileVersion": 1 + } + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-hidden-lockfile-existing-downgrade", + "lockfileVersion": 1, + "requires": true + } + logs = [ + "created a lockfile as npm-shrinkwrap.json with version 1" + ] +` + +exports[`test/lib/shrinkwrap.js TAP with hidden lockfile existing upgrade > must match snapshot 1`] = ` + localPrefix = { + "node_modules": { + ".package-lock.json": { + "lockfileVersion": 2 + } + } + } + config = { + "lockfileVersion": 3 + } + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-hidden-lockfile-existing-upgrade", + "lockfileVersion": 3, + "requires": true, + "packages": {} + } + logs = [ + "created a lockfile as npm-shrinkwrap.json with version 3" + ] +` + +exports[`test/lib/shrinkwrap.js TAP with nothing ancient > must match snapshot 1`] = ` + localPrefix = {} + config = {} + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-nothing-ancient", + "lockfileVersion": 2, + "requires": true, + "packages": {} + } + logs = [ + "created a lockfile as npm-shrinkwrap.json with version 2" + ] +` + +exports[`test/lib/shrinkwrap.js TAP with nothing ancient upgrade > must match snapshot 1`] = ` + localPrefix = {} + config = { + "lockfileVersion": 3 + } + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-nothing-ancient-upgrade", + "lockfileVersion": 3, + "requires": true, + "packages": {} + } + logs = [ + "created a lockfile as npm-shrinkwrap.json with version 3" + ] +` + +exports[`test/lib/shrinkwrap.js TAP with npm-shrinkwrap.json ancient > must match snapshot 1`] = ` + localPrefix = { + "npm-shrinkwrap.json": { + "lockfileVersion": 1 + } + } + config = {} + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-ancient", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-ancient" + } + } + } + logs = [ + "npm-shrinkwrap.json updated to version 2" + ] +` + +exports[`test/lib/shrinkwrap.js TAP with npm-shrinkwrap.json ancient upgrade > must match snapshot 1`] = ` + localPrefix = { + "npm-shrinkwrap.json": { + "lockfileVersion": 1 + } + } + config = { + "lockfileVersion": 3 + } + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-ancient-upgrade", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-ancient-upgrade" + } + } + } + logs = [ + "npm-shrinkwrap.json updated to version 3" + ] +` + +exports[`test/lib/shrinkwrap.js TAP with npm-shrinkwrap.json existing > must match snapshot 1`] = ` + localPrefix = { + "npm-shrinkwrap.json": { + "lockfileVersion": 2 + } + } + config = {} + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-existing", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-existing" + } + } + } + logs = [ + "npm-shrinkwrap.json up to date" + ] +` + +exports[`test/lib/shrinkwrap.js TAP with npm-shrinkwrap.json existing downgrade > must match snapshot 1`] = ` + localPrefix = { + "npm-shrinkwrap.json": { + "lockfileVersion": 2 + } + } + config = { + "lockfileVersion": 1 + } + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-existing-downgrade", + "lockfileVersion": 1, + "requires": true + } + logs = [ + "npm-shrinkwrap.json updated to version 1" + ] +` + +exports[`test/lib/shrinkwrap.js TAP with npm-shrinkwrap.json existing upgrade > must match snapshot 1`] = ` + localPrefix = { + "npm-shrinkwrap.json": { + "lockfileVersion": 2 + } + } + config = { + "lockfileVersion": 3 + } + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-existing-upgrade", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-existing-upgrade" + } + } + } + logs = [ + "npm-shrinkwrap.json updated to version 3" + ] +` + +exports[`test/lib/shrinkwrap.js TAP with package-lock.json ancient > must match snapshot 1`] = ` + localPrefix = { + "package-lock.json": { + "lockfileVersion": 1 + } + } + config = {} + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-package-lock.json-ancient", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "tap-testdir-shrinkwrap-with-package-lock.json-ancient" + } + } + } + logs = [ + "package-lock.json has been renamed to npm-shrinkwrap.json and updated to version 2" + ] +` + +exports[`test/lib/shrinkwrap.js TAP with package-lock.json ancient upgrade > must match snapshot 1`] = ` + localPrefix = { + "package-lock.json": { + "lockfileVersion": 1 + } + } + config = { + "lockfileVersion": 3 + } + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-package-lock.json-ancient-upgrade", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tap-testdir-shrinkwrap-with-package-lock.json-ancient-upgrade" + } + } + } + logs = [ + "package-lock.json has been renamed to npm-shrinkwrap.json and updated to version 3" + ] +` + +exports[`test/lib/shrinkwrap.js TAP with package-lock.json existing > must match snapshot 1`] = ` + localPrefix = { + "package-lock.json": { + "lockfileVersion": 2 + } + } + config = {} + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-package-lock.json-existing", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "tap-testdir-shrinkwrap-with-package-lock.json-existing" + } + } + } + logs = [ + "package-lock.json has been renamed to npm-shrinkwrap.json" + ] +` + +exports[`test/lib/shrinkwrap.js TAP with package-lock.json existing downgrade > must match snapshot 1`] = ` + localPrefix = { + "package-lock.json": { + "lockfileVersion": 2 + } + } + config = { + "lockfileVersion": 1 + } + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-package-lock.json-existing-downgrade", + "lockfileVersion": 1, + "requires": true + } + logs = [ + "package-lock.json has been renamed to npm-shrinkwrap.json and updated to version 1" + ] +` + +exports[`test/lib/shrinkwrap.js TAP with package-lock.json existing upgrade > must match snapshot 1`] = ` + localPrefix = { + "package-lock.json": { + "lockfileVersion": 2 + } + } + config = { + "lockfileVersion": 3 + } + npm-shrinkwrap.json = { + "name": "tap-testdir-shrinkwrap-with-package-lock.json-existing-upgrade", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tap-testdir-shrinkwrap-with-package-lock.json-existing-upgrade" + } + } + } + logs = [ + "package-lock.json has been renamed to npm-shrinkwrap.json and updated to version 3" + ] +` diff --git a/test/lib/commands/shrinkwrap.js b/test/lib/commands/shrinkwrap.js index a1638ed5acfa1..32e36da5a65e7 100644 --- a/test/lib/commands/shrinkwrap.js +++ b/test/lib/commands/shrinkwrap.js @@ -1,330 +1,212 @@ const t = require('tap') const fs = require('fs') -const { fake: mockNpm } = require('../../fixtures/mock-npm') - -const config = { - global: false, -} -const flatOptions = { - depth: 0, -} -const npm = mockNpm({ - config, - flatOptions, - lockfileVersion: 2, - globalDir: '', - prefix: '', -}) -const tree = { - meta: { - hiddenLockfile: null, - loadedFromDisk: false, - filename: '', - originalLockfileVersion: 2, - save () {}, - }, -} -const mocks = { - npmlog: { notice () {} }, - '@npmcli/arborist': class { - loadVirtual () { - return tree - } - - loadActual () { - return tree - } - }, - '../../../lib/utils/usage.js': () => 'usage instructions', - '../../../lib/utils/config/definitions.js': {}, -} - -t.afterEach(() => { - npm.prefix = '' - config.global = false - npm.globalDir = '' -}) - -t.test('no args', async t => { - t.plan(4) - - npm.prefix = '/project/a' - - class Arborist { - constructor (args) { - t.same( - args, - { ...flatOptions, path: npm.prefix }, - 'should call arborist constructor with expected args' - ) - } - - async loadVirtual () { - t.ok('should load virtual tree') - return { - ...tree, - meta: { - ...tree.meta, - save () { - t.ok('should save the lockfile') - }, - }, - } - } - } - - const npmlog = { - notice (title, msg) { - t.equal( - msg, - 'created a lockfile as npm-shrinkwrap.json', - 'should log notice msg that file was successfully created' - ) - }, - } - - const Shrinkwrap = t.mock('../../../lib/commands/shrinkwrap.js', { - ...mocks, - npmlog, - '@npmcli/arborist': Arborist, - }) - const shrinkwrap = new Shrinkwrap(npm) - - await shrinkwrap.exec([]) -}) - -t.test('no virtual tree', async t => { - t.plan(4) - - npm.prefix = '/project/a' - - class Arborist { - constructor (args) { - t.same( - args, - { ...flatOptions, path: npm.prefix }, - 'should call arborist constructor with expected args' - ) - } - - async loadVirtual () { - throw new Error('ERR') - } - - async loadActual () { - t.ok('should load actual tree') - return { - ...tree, - meta: { - ...tree.meta, - save () { - t.ok('should save the lockfile') - }, - }, - } - } +const { resolve } = require('path') +const { real: mockNpm } = require('../../fixtures/mock-npm') + +// Attempt to parse json values in snapshots before +// stringifying to remove escaped values +const stringify = (obj) => JSON.stringify(obj, (k, v) => { + try { + return JSON.parse(v) + } catch (_) {} + return v +}, 2) + +t.formatSnapshot = (obj) => [ + `localPrefix = ${stringify(obj.testdir)}`, + `config = ${stringify(obj.config)}`, + `npm-shrinkwrap.json = ${stringify(obj.shrinkwrap)}`, + `logs = ${stringify(obj.logs)}`, +].join('\n').split('\n').map((s) => ` ${s}`).join('\n') + +// Run shrinkwrap against a specified testdir with config items +// and make some assertions that should always be true. Sets +// the results on t.context for use in child tests +const shrinkwrap = async ( + t, + testdir = {}, + config = {}, + mocks = {} +) => { + const { Npm, logs } = mockNpm(t, mocks) + const npm = new Npm() + await npm.load() + + npm.localPrefix = t.testdir(testdir) + if (config.lockfileVersion) + npm.flatOptions.lockfileVersion = config.lockfileVersion + if (config.global) + npm.config.set('global', config.global) + + await npm.exec('shrinkwrap', []) + + const newFile = resolve(npm.localPrefix, 'npm-shrinkwrap.json') + const oldFile = resolve(npm.localPrefix, 'package-lock.json') + const notices = logs + .filter(([title]) => title === 'notice') + .map(([,, msg]) => msg) + const warnings = logs + .filter(([title]) => title === 'warn') + .map(([,, msg]) => msg) + + t.notOk(fs.existsSync(oldFile), 'package-lock is always deleted') + t.same(warnings, [], 'no warnings') + t.teardown(() => delete t.context) + t.context = { + testdir, + config, + shrinkwrap: JSON.parse(fs.readFileSync(newFile)), + logs: notices, } +} - const npmlog = { - notice (title, msg) { - t.equal( - msg, - 'created a lockfile as npm-shrinkwrap.json', - 'should log notice msg that file was successfully created' - ) - }, +// Run shrinkwrap against all combinations of existing and config +// lockfile versions +const shrinkwrapMatrix = async (t, file, assertions) => { + const ancient = JSON.stringify({ lockfileVersion: 1 }) + const existing = JSON.stringify({ lockfileVersion: 2 }) + const upgrade = { lockfileVersion: 3 } + const downgrade = { lockfileVersion: 1 } + + let ancientDir = {} + let existingDir = null + if (file === 'package-lock') { + ancientDir = { 'package-lock.json': ancient } + existingDir = { 'package-lock.json': existing } + } else if (file === 'npm-shrinkwrap') { + ancientDir = { 'npm-shrinkwrap.json': ancient } + existingDir = { 'npm-shrinkwrap.json': existing } + } else if (file === 'hidden-lockfile') { + ancientDir = { node_modules: { '.package-lock.json': ancient } } + existingDir = { node_modules: { '.package-lock.json': existing } } } - const Shrinkwrap = t.mock('../../../lib/commands/shrinkwrap.js', { - ...mocks, - npmlog, - '@npmcli/arborist': Arborist, + await t.test('ancient', async (t) => { + await shrinkwrap(t, ancientDir) + t.match(t.context, assertions.ancient) + t.matchSnapshot(t.context) }) - const shrinkwrap = new Shrinkwrap(npm) - - await shrinkwrap.exec([]) -}) - -t.test('existing package-json file', async t => { - t.plan(5) - - npm.prefix = '/project/a' - - class Arborist { - constructor (args) { - t.same( - args, - { ...flatOptions, path: npm.prefix }, - 'should call arborist constructor with expected args' - ) - } - - async loadVirtual () { - t.ok('should load virtual tree') - return { - ...tree, - meta: { - hiddenLockfile: false, - loadedFromDisk: true, - filename: 'package-lock.json', - save () { - t.ok('should save the lockfile') - }, - }, - } - } - } - - const npmlog = { - notice (title, msg) { - t.equal( - msg, - 'package-lock.json has been renamed to npm-shrinkwrap.json', - 'should log notice msg that file was renamed' - ) - }, - } - - const fs = { - promises: { - unlink (filename) { - t.equal(filename, 'package-lock.json', 'should remove old lockfile') - }, - }, - } - - const Shrinkwrap = t.mock('../../../lib/commands/shrinkwrap.js', { - ...mocks, - fs, - npmlog, - '@npmcli/arborist': Arborist, + await t.test('ancient upgrade', async (t) => { + await shrinkwrap(t, ancientDir, upgrade) + t.match(t.context, assertions.ancientUpgrade) + t.matchSnapshot(t.context) }) - const shrinkwrap = new Shrinkwrap(npm) - - await shrinkwrap.exec([]) -}) - -t.test('update shrinkwrap file version', async t => { - t.plan(4) - npm.prefix = '/project/a' - - class Arborist { - constructor (args) { - t.same( - args, - { ...flatOptions, path: npm.prefix }, - 'should call arborist constructor with expected args' - ) - } - - async loadVirtual () { - t.ok('should load virtual tree') - return { - ...tree, - meta: { - hiddenLockfile: false, - loadedFromDisk: true, - filename: 'npm-shrinkwrap.json', - originalLockfileVersion: 1, - save () { - t.ok('should save the lockfile') - }, - }, - } - } - } - - const npmlog = { - notice (title, msg) { - t.equal( - msg, - 'npm-shrinkwrap.json updated to version 2', - 'should log notice msg that file was updated' - ) - }, - } - - const Shrinkwrap = t.mock('../../../lib/commands/shrinkwrap.js', { - ...mocks, - npmlog, - '@npmcli/arborist': Arborist, - }) - const shrinkwrap = new Shrinkwrap(npm) - - await shrinkwrap.exec([]) -}) - -t.test('update to date shrinkwrap file', async t => { - t.plan(4) - - npm.prefix = '/project/a' - - class Arborist { - constructor (args) { - t.same( - args, - { ...flatOptions, path: npm.prefix }, - 'should call arborist constructor with expected args' - ) - } - - async loadVirtual () { - t.ok('should load virtual tree') - return { - ...tree, - meta: { - hiddenLockfile: false, - loadedFromDisk: true, - filename: 'npm-shrinkwrap.json', - originalLockfileVersion: 2, - save () { - t.ok('should save the lockfile') - }, - }, - } - } - } - - const npmlog = { - notice (title, msg) { - t.equal( - msg, - 'npm-shrinkwrap.json up to date', - 'should log notice msg shrinkwrap up to date' - ) - }, + if (existingDir) { + await t.test('existing', async (t) => { + await shrinkwrap(t, existingDir) + t.match(t.context, assertions.existing) + t.matchSnapshot(t.context) + }) + await t.test('existing upgrade', async (t) => { + await shrinkwrap(t, existingDir, upgrade) + t.match(t.context, assertions.existingUpgrade) + t.matchSnapshot(t.context) + }) + await t.test('existing downgrade', async (t) => { + await shrinkwrap(t, existingDir, downgrade) + t.match(t.context, assertions.existingDowngrade) + t.matchSnapshot(t.context) + }) } +} - const Shrinkwrap = t.mock('../../../lib/commands/shrinkwrap.js', { - ...mocks, - npmlog, - '@npmcli/arborist': Arborist, - }) - const shrinkwrap = new Shrinkwrap(npm) - - await shrinkwrap.exec([]) -}) +const NOTICES = { + CREATED: (v = '') => + [`created a lockfile as npm-shrinkwrap.json${v && ` with version ${v}`}`], + RENAMED: (v = '') => + [`package-lock.json has been renamed to npm-shrinkwrap.json${v && ` and updated to version ${v}`}`], + UPDATED: (v = '') => + [`npm-shrinkwrap.json updated to version ${v}`], + SAME: () => + [`npm-shrinkwrap.json up to date`], +} -t.test('shrinkwrap --global', async t => { - const Shrinkwrap = t.mock('../../../lib/commands/shrinkwrap.js', mocks) +t.test('with nothing', t => shrinkwrapMatrix(t, null, { + ancient: { + shrinkwrap: { lockfileVersion: 2 }, + logs: NOTICES.CREATED(2), + }, + ancientUpgrade: { + shrinkwrap: { lockfileVersion: 3 }, + logs: NOTICES.CREATED(3), + }, +})) - config.global = true - const shrinkwrap = new Shrinkwrap(npm) +t.test('with package-lock.json', t => shrinkwrapMatrix(t, 'package-lock', { + ancient: { + shrinkwrap: { lockfileVersion: 2 }, + logs: NOTICES.RENAMED(2), + }, + ancientUpgrade: { + shrinkwrap: { lockfileVersion: 3 }, + logs: NOTICES.RENAMED(3), + }, + existing: { + shrinkwrap: { lockfileVersion: 2 }, + logs: NOTICES.RENAMED(), + }, + existingUpgrade: { + shrinkwrap: { lockfileVersion: 3 }, + logs: NOTICES.RENAMED(3), + }, + existingDowngrade: { + shrinkwrap: { lockfileVersion: 1 }, + logs: NOTICES.RENAMED(1), + }, +})) - await t.rejects( - shrinkwrap.exec([]), - { code: 'ESHRINKWRAPGLOBAL', message: /does not work for global packages/ }, - 'should throw no global support msg' - ) -}) +t.test('with npm-shrinkwrap.json', t => shrinkwrapMatrix(t, 'npm-shrinkwrap', { + ancient: { + shrinkwrap: { lockfileVersion: 2 }, + logs: NOTICES.UPDATED(2), + }, + ancientUpgrade: { + shrinkwrap: { lockfileVersion: 3 }, + logs: NOTICES.UPDATED(3), + }, + existing: { + shrinkwrap: { lockfileVersion: 2 }, + logs: NOTICES.SAME(), + }, + existingUpgrade: { + shrinkwrap: { lockfileVersion: 3 }, + logs: NOTICES.UPDATED(3), + }, + existingDowngrade: { + shrinkwrap: { lockfileVersion: 1 }, + logs: NOTICES.UPDATED(1), + }, +})) -t.test('works without fs.promises', async t => { - t.doesNotThrow(() => { - const Shrinkwrap = t.mock('../../../lib/commands/shrinkwrap.js', { - ...mocks, - fs: { ...fs, promises: null }, - }) - new Shrinkwrap(npm) +t.test('with hidden lockfile', t => shrinkwrapMatrix(t, 'hidden-lockfile', { + ancient: { + shrinkwrap: { lockfileVersion: 1 }, + logs: NOTICES.CREATED(), + }, + ancientUpgrade: { + shrinkwrap: { lockfileVersion: 3 }, + logs: NOTICES.CREATED(), + }, + existing: { + shrinkwrap: { lockfileVersion: 2 }, + logs: NOTICES.CREATED(), + }, + existingUpgrade: { + shrinkwrap: { lockfileVersion: 3 }, + logs: NOTICES.CREATED(3), + }, + existingDowngrade: { + shrinkwrap: { lockfileVersion: 1 }, + logs: NOTICES.CREATED(1), + }, +})) + +t.test('throws in global mode', async t => { + t.rejects(shrinkwrap(t, {}, { + global: true, + }), { + message: '`npm shrinkwrap` does not work for global packages', + code: 'ESHRINKWRAPGLOBAL', }) })