Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions workspaces/arborist/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ to a package folder, which may have children in `node_modules`.
`integrity` values for all modules in this tree. Only relevant on `root`
nodes.

Use `node.meta.commit()` or `node.meta.toJSON()` to obtain lockfile data
in the selected lockfile format. Version 3 output contains `packages`
rather than the legacy `dependencies` representation.

* `node.children` Map of packages located in the node's `node_modules`
folder.
* `node.package` The contents of this node's `package.json` file.
Expand Down
6 changes: 6 additions & 0 deletions workspaces/arborist/lib/shrinkwrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -1173,6 +1173,12 @@ class Shrinkwrap {
}
}

// v3+ still needs the root metadata above, but not legacy descendants.
if (node === this.tree && this.lockfileVersion >= 3) {
delete lock.dependencies
return lock
}

// now we walk the children, putting them in the 'dependencies' object
const { children } = node.target
if (!children.size) {
Expand Down
35 changes: 0 additions & 35 deletions workspaces/arborist/tap-snapshots/test/shrinkwrap.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@
'use strict'
exports[`test/shrinkwrap.js TAP a yarn.lock entry with file: resolved > lockfile 1`] = `
Object {
"dependencies": Object {
"mkdirp": Object {
"integrity": "sha512-N2REVrJ/X/jGPfit2d7zea2J1pf7EAR5chIUcfHffAZ7gmlam5U65sAm76+o4ntQbSRdTjYf7qZz3chuHlwXEA==",
"version": "file:mkdirp",
},
},
"lockfileVersion": 3,
"name": "root",
"packages": Object {
Expand Down Expand Up @@ -50,13 +44,6 @@ exports[`test/shrinkwrap.js TAP a yarn.lock entry with file: resolved > yarn.loc

exports[`test/shrinkwrap.js TAP a yarn.lock entry with integrity mismatch > lockfile 1`] = `
Object {
"dependencies": Object {
"mkdirp": Object {
"integrity": "sha512-N2REVrJ/X/jGPfit2d7zea2J1pf7EAR5chIUcfHffAZ7gmlam5U65sAm76+o4ntQbSRdTjYf7qZz3chuHlwXEA==",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.2.tgz",
"version": "1.0.2",
},
},
"lockfileVersion": 3,
"name": "root",
"packages": Object {
Expand Down Expand Up @@ -92,12 +79,6 @@ mkdirp@^1.0.2:

exports[`test/shrinkwrap.js TAP a yarn.lock entry with no integrity > lockfile 1`] = `
Object {
"dependencies": Object {
"mkdirp": Object {
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.2.tgz",
"version": "1.0.2",
},
},
"lockfileVersion": 3,
"name": "root",
"packages": Object {
Expand Down Expand Up @@ -131,12 +112,6 @@ mkdirp@^1.0.2:

exports[`test/shrinkwrap.js TAP a yarn.lock entry with no resolved > lockfile 1`] = `
Object {
"dependencies": Object {
"mkdirp": Object {
"integrity": "sha512-N2REVrJ/X/jGPfit2d7zea2J1pf7EAR5chIUcfHffAZ7gmlam5U65sAm76+o4ntQbSRdTjYf7qZz3chuHlwXEA==",
"version": "1.0.2",
},
},
"lockfileVersion": 3,
"name": "root",
"packages": Object {
Expand Down Expand Up @@ -170,11 +145,6 @@ mkdirp@^1.0.2:

exports[`test/shrinkwrap.js TAP a yarn.lock entry with version mismatch > lockfile 1`] = `
Object {
"dependencies": Object {
"mkdirp": Object {
"version": "1.0.4",
},
},
"lockfileVersion": 3,
"name": "root",
"packages": Object {
Expand Down Expand Up @@ -206,11 +176,6 @@ mkdirp@^1.0.2:

exports[`test/shrinkwrap.js TAP a yarn.lock with no entries > lockfile 1`] = `
Object {
"dependencies": Object {
"mkdirp": Object {
"version": "1.0.4",
},
},
"lockfileVersion": 3,
"name": "root",
"packages": Object {
Expand Down
143 changes: 143 additions & 0 deletions workspaces/arborist/test/shrinkwrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,137 @@ t.test('load and change lockfileVersion', async t => {
t.equal(v3Data.dependencies, undefined, 'v3 data does not have dependencies')
})

t.test('v3 skips legacy descendants while v1 and v2 retain them', async t => {
const path = t.testdir({})
const results = new Map()
for (const lockfileVersion of [1, 2, 3]) {
const meta = await Shrinkwrap.reset({ path, lockfileVersion })
const root = new Node({
path,
meta,
pkg: { name: 'root', version: '1.0.0', dependencies: { child: '1.0.0' } },
})
const child = new Node({
parent: root,
pkg: { name: 'child', version: '1.0.0' },
})
let legacyBundleChecks = 0
Object.defineProperty(child, 'inDepBundle', {
get: () => {
legacyBundleChecks++
return false
},
})

const result = meta.commit()
results.set(lockfileVersion, result)
t.equal(legacyBundleChecks === 0, lockfileVersion === 3,
`v${lockfileVersion} only generates legacy descendants when needed`)
t.match(result, { name: 'root', version: '1.0.0', requires: true, lockfileVersion })
}
t.strictSame(results.get(2), {
...results.get(1),
...results.get(3),
lockfileVersion: 2,
}, 'v2 retains exactly the combined v1 and v3 output')
t.equal(results.get(3).dependencies, undefined, 'v3 omits legacy dependencies')
})

t.test('v3 discards stale legacy metadata after updating a v2 tree', async t => {
const pkg = { name: 'root', version: '1.0.0', dependencies: { foo: '^1.0.0' } }
const oldFoo = {
version: '1.0.0',
resolved: 'https://registry.npmjs.org/foo/-/foo-1.0.0.tgz',
}
const path = t.testdir({
'package.json': JSON.stringify(pkg),
'package-lock.json': JSON.stringify({
...pkg,
lockfileVersion: 2,
requires: true,
packages: { '': pkg, 'node_modules/foo': oldFoo },
dependencies: { foo: oldFoo },
}),
})
const tree = await new Arborist({ path, lockfileVersion: 3 }).loadVirtual()
const meta = tree.meta
meta.add(tree)
const foo = tree.children.get('foo')
foo.package = { ...foo.package, version: '1.1.0' }
foo.resolved = 'https://registry.npmjs.org/foo/-/foo-1.1.0.tgz'
t.equal(meta.data.dependencies.foo.version, '1.0.0', 'input has the old legacy entry')

const result = meta.commit()
t.equal(result.packages['node_modules/foo'].version, '1.1.0')
t.equal(meta.data.dependencies, undefined, 'obsolete legacy data is discarded')
t.equal(meta.get(foo.path).version, '1.1.0', 'modern metadata remains available')
delete meta.data.packages[foo.location]
t.strictSame(meta.get(foo.path), {}, 'missing modern metadata cannot resurrect the old version')
})

t.test('legacy output still terminates at cyclic links', async t => {
const path = t.testdir({})
const meta = await Shrinkwrap.reset({ path, lockfileVersion: 2 })
const root = new Node({
path,
meta,
pkg: { name: 'root', dependencies: { loop: 'file:.' } },
})
new Link({ name: 'loop', parent: root, target: root })

const result = meta.commit()
t.equal(result.dependencies.loop.version, 'file:')
t.equal(result.dependencies.loop.dependencies, undefined, 'cycle is not traversed again')
})

t.test('v3 conversion retains legacy input metadata before committing a tree', async t => {
for (const lockfileVersion of [1, 2]) {
await t.test(`v${lockfileVersion} input`, async t => {
const foo = {
version: '1.0.0',
resolved: 'https://registry.npmjs.org/foo/-/foo-1.0.0.tgz',
}
const path = t.testdir({
'package.json': JSON.stringify({ name: 'root', dependencies: { foo: '^1.0.0' } }),
'package-lock.json': JSON.stringify({
lockfileVersion,
requires: true,
...(lockfileVersion === 2 ? { packages: {} } : {}),
dependencies: { foo },
}),
})
const meta = await Shrinkwrap.load({ path, lockfileVersion: 3 })
t.equal(meta.get('node_modules/foo').version, '1.0.0')
t.equal(meta.get('node_modules/foo').resolved, foo.resolved)
t.strictSame(meta.data.dependencies.foo, foo, 'legacy input remains available')
})
}
})

t.test('v3 does not recover omitted inert nodes from legacy metadata', async t => {
const path = t.testdir({})
const meta = await Shrinkwrap.reset({ path, lockfileVersion: 3 })
const root = new Node({
path,
meta,
pkg: { name: 'root', optionalDependencies: { foo: '*' } },
})
const foo = new Node({
parent: root,
pkg: { name: 'foo', version: '1.0.0' },
resolved: 'https://registry.npmjs.org/foo/-/foo-1.0.0.tgz',
})
meta.commit()
meta.get(foo.path)
foo.package = { name: 'foo' }
foo.inert = true
meta.get(foo.path)

const result = meta.commit()
t.equal(result.packages[foo.location], undefined, 'inert node is omitted')
t.strictSame(meta.get(foo.path), {}, 'legacy data cannot restore the omitted metadata')
})

t.test('load and then reset gets empty lockfile', async t => {
const sw = await Shrinkwrap.load({ path: fixture })
sw.reset()
Expand Down Expand Up @@ -573,6 +704,18 @@ t.test('construct metadata from node and package data', t => {
t.matchSnapshot(meta.get(peer.location), 'a peer dep')
t.matchSnapshot(meta.get(peerdep.location), 'a peer meta-dep')
t.matchSnapshot(meta.commit(), 'data calculated from nodes themselves')
for (const lockfileVersion of [1, 2]) {
meta.lockfileVersion = lockfileVersion
const { dependencies } = meta.commit()
t.match(dependencies.d.dependencies['git-dep'], {
version: git.resolved,
from: 'git-dep@github:foo/bar',
}, `v${lockfileVersion} preserves git metadata`)
t.equal(dependencies.d.dependencies.bundled.version, 'npm:not-bundled-tho@1.2.3',
`v${lockfileVersion} preserves aliases`)
t.match(dependencies.devo, { version: '1.2.3', devOptional: true },
`v${lockfileVersion} preserves mixed dependency flags`)
}
t.end()
})

Expand Down
Loading