Skip to content
This repository was archived by the owner on Jan 20, 2022. It is now read-only.
Closed
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
5 changes: 3 additions & 2 deletions lib/add-rm-pkg-deps.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ const addSingle = ({pkg, spec, saveBundle, saveType, path}) => {

pkg[type] = pkg[type] || {}
if (rawSpec !== '' || pkg[type][name] === undefined) {
// if we're in global mode, file specs are based on cwd, not arb path
pkg[type][name] = specType === 'file' || specType === 'directory'
// file specs are based on arb path, directories specs
// are normalized during build-ideal-tree instead
pkg[type][name] = specType === 'file'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was introduced in #125 as a way to compensate for the wrong parsed links and should not be necessary anymore

? `file:${relpath(path, fetchSpec)}`
: (rawSpec || '*')
}
Expand Down
63 changes: 51 additions & 12 deletions lib/arborist/build-ideal-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const pickManifest = require('npm-pick-manifest')
const mapWorkspaces = require('@npmcli/map-workspaces')
const promiseCallLimit = require('promise-call-limit')
const getPeerSet = require('../peer-set.js')
const realpath = require('../../lib/realpath.js')

const fromPath = require('../from-path.js')
const calcDepFlags = require('../calc-dep-flags.js')
Expand Down Expand Up @@ -79,6 +80,10 @@ const _globalStyle = Symbol('globalStyle')
const _globalRootNode = Symbol('globalRootNode')
const _isVulnerable = Symbol.for('isVulnerable')
const _usePackageLock = Symbol.for('usePackageLock')
const _rpcache = Symbol.for('realpathCache')
const _stcache = Symbol.for('statCache')
const _followSymlinkPath = Symbol('followSymlinkPath')
const _retrieveSpecName = Symbol('retrieveSpecName')

// used for the ERESOLVE error to show the last peer conflict encountered
const _peerConflict = Symbol('peerConflict')
Expand Down Expand Up @@ -130,6 +135,12 @@ module.exports = cls => class IdealTreeBuilder extends cls {
this[_linkNodes] = new Set()
this[_manifests] = new Map()
this[_peerConflict] = null

// caches for cached realpath calls
const cwd = process.cwd()
// assume that the cwd is real enough for our purposes
this[_rpcache] = this[_rpcache]
this[_stcache] = this[_stcache]
}

get explicitRequests () {
Expand Down Expand Up @@ -326,18 +337,11 @@ module.exports = cls => class IdealTreeBuilder extends cls {
return Promise.all(add.map(s => {
// in global mode, `npm i foo.tgz` needs to be resolved from
// the current working dir, NOT /usr/local/lib!
const spec = npa(s, this[_global] ? process.cwd() : this.path)
// if it's just @'' then we reload whatever's there, or get latest
// if it's an explicit tag, we need to install that specific tag version
const isTag = spec.rawSpec && spec.type === 'tag'
return spec.name && !isTag ? spec : pacote.manifest(spec).then(mani => {
// if it's a tag type, then we need to run it down to an actual version
if (isTag)
return npa(`${mani.name}@${mani.version}`)

spec.name = mani.name
return spec
})
const currpath = this[_global] ? process.cwd() : this.path
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@isaacs I decided to leave this here for now, we can revisit it later

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can land on a separate commit, but ought to get it fixed, may as well pull in with this changeset.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eh, let's land it in a sep commit, maybe even better sep PR, let's thing about future us trying to bisect something 😂

const spec = npa(s, currpath)

return this[_retrieveSpecName](spec)
.then(add => this[_followSymlinkPath](add, currpath))
})).then(add => {
this[_resolvedAdd] = add
// now add is a list of spec objects with names.
Expand All @@ -355,6 +359,41 @@ module.exports = cls => class IdealTreeBuilder extends cls {
})
}

async [_retrieveSpecName] (spec) {
// if it's just @'' then we reload whatever's there, or get latest
// if it's an explicit tag, we need to install that specific tag version
const isTag = spec.rawSpec && spec.type === 'tag'

if (spec.name && !isTag)
return spec

const mani = await pacote.manifest(spec)
// if it's a tag type, then we need to run it down to an actual version
if (isTag)
return npa(`${mani.name}@${mani.version}`)

spec.name = mani.name
return spec
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was just a small refactor for readability since when adding the _followSymlinkPath method (that is now below) things got gnarly in this part of the code


async [_followSymlinkPath] (spec, currpath) {
if (spec.type === 'directory') {
const real = await (
realpath(spec.fetchSpec, this[_rpcache], this[_stcache])
// TODO: create synthetic test case to simulate realpath failure
.catch(/* istanbul ignore next */() => null)
)

/* istanbul ignore else - same as the ignored catch above */
if (real) {
const { name } = spec
spec = npa(`file:${relpath(this.path, real)}`, this.path)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the new spec gets npa parsed relative to this.path, so that's what will end up in resolved, in the package.json, etc - also something I want to validate that makes sense in the impl 😊

spec.name = name
}
}
return spec
}

// TODO: provide a way to fix bundled deps by exposing metadata about
// what's in the bundle at each published manifest. Without that, we
// can't possibly fix bundled deps without breaking a ton of other stuff,
Expand Down
4 changes: 2 additions & 2 deletions lib/arborist/load-actual.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ const _findMissingEdges = Symbol('findMissingEdges')
const _findFSParents = Symbol('findFSParents')

const _actualTreeLoaded = Symbol('actualTreeLoaded')
const _rpcache = Symbol('realpathCache')
const _stcache = Symbol('statCache')
const _rpcache = Symbol.for('realpathCache')
const _stcache = Symbol.for('statCache')
const _topNodes = Symbol('linkTargets')
const _cache = Symbol('nodeLoadingCache')
const _loadActual = Symbol('loadActual')
Expand Down
35 changes: 35 additions & 0 deletions tap-snapshots/test-arborist-build-ideal-tree.js-TAP.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,41 @@ Node {
}
`

exports[`test/arborist/build-ideal-tree.js TAP add symlink that points to a symlink > should follow symlinks to find final realpath destination 1`] = `
Node {
"children": Map {
"a" => Link {
"edgesIn": Set {
Edge {
"from": "",
"name": "a",
"spec": "file:../linked-pkg",
"type": "prod",
},
},
"location": "node_modules/a",
"name": "a",
"resolved": "file:../../linked-pkg",
"target": Object {
"name": "a",
"parent": null,
},
},
},
"edgesOut": Map {
"a" => Edge {
"name": "a",
"spec": "file:../linked-pkg",
"to": "node_modules/a",
"type": "prod",
},
},
"location": "",
"name": "my-project",
"resolved": null,
}
`

exports[`test/arborist/build-ideal-tree.js TAP bad shrinkwrap file > bad shrinkwrap 1`] = `
Node {
"children": Map {
Expand Down
63 changes: 55 additions & 8 deletions test/arborist/build-ideal-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -1321,6 +1321,42 @@ t.test('workspaces', t => {
t.end()
})

t.test('add symlink that points to a symlink', t => {
const fixt = t.testdir({
'global-prefix': {
lib: {
node_modules: {
a: t.fixture('symlink', '../../../linked-pkg')
}
}
},
'linked-pkg': {
'package.json': JSON.stringify({
name: 'a',
version: '1.0.0'
})
},
'my-project': {
'package.json': JSON.stringify({})
}
})
const path = resolve(fixt, 'my-project')
const arb = new Arborist({
path,
...OPT,
})
return arb.buildIdealTree({
add: [
'file:../global-prefix/lib/node_modules/a'
]
}).then(tree =>
t.matchSnapshot(
printTree(tree),
'should follow symlinks to find final realpath destination'
)
)
})

// if we get this wrong, it'll spin forever and use up all the memory
t.test('pathologically nested dependency cycle', t =>
t.resolveMatchSnapshot(printIdeal(
Expand Down Expand Up @@ -1349,24 +1385,35 @@ t.test('resolve files from cwd in global mode, Arb path in local mode', t => {
})

t.only('resolve links in global mode', t => {
const cwd = process.cwd()
t.teardown(() => process.chdir(cwd))
const path = t.testdir({
global: {}
global: {},
lib: {
'my-project': {}
},
'linked-dep': {
'package.json': JSON.stringify({
name: 'linked-dep',
version: '1.0.0'
})
}
})
const fixturedir = resolve(fixtures, 'root-bundler')
const fixturedir = resolve(path, 'lib', 'my-project')

const cwd = process.cwd()
t.teardown(() => process.chdir(cwd))
process.chdir(fixturedir)

const arb = new Arborist({
...OPT,
global: true,
path: resolve(path, 'global'),
...OPT,
})
return arb.buildIdealTree({
add: ['file:../sax'],
add: ['file:../../linked-dep'],
global: true,
}).then(tree => {
const resolved = 'file:../../../../fixtures/sax'
t.equal(tree.children.get('sax').resolved, resolved)
const resolved = 'file:../../linked-dep'
t.equal(tree.children.get('linked-dep').resolved, resolved)
})
})

Expand Down