Skip to content
This repository has been archived by the owner on Jan 20, 2022. It is now read-only.

Isaacs/links to root causing unreportable eresolve #259

Closed
wants to merge 17 commits into from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 7 additions & 1 deletion bin/lib/options.js
Expand Up @@ -33,7 +33,13 @@ for (const arg of process.argv.slice(2)) {
options.omit.push(arg.substr('--omit='.length))
} else if (/^--before=/.test(arg))
options.before = new Date(arg.substr('--before='.length))
else if (/^--[^=]+=/.test(arg)) {
else if (/^-w.+/.test(arg)) {
options.workspaces = options.workspaces || []
options.workspaces.push(arg.replace(/^-w/, ''))
} else if (/^--workspace=/.test(arg)) {
options.workspaces = options.workspaces || []
options.workspaces.push(arg.replace(/^--workspace=/, ''))
} else if (/^--[^=]+=/.test(arg)) {
const [key, ...v] = arg.replace(/^--/, '').split('=')
const val = v.join('=')
options[key] = val === 'false' ? false : val === 'true' ? true : val
Expand Down
123 changes: 93 additions & 30 deletions lib/arborist/build-ideal-tree.js
Expand Up @@ -44,12 +44,14 @@ const _currentDep = Symbol('currentDep')
const _updateAll = Symbol('updateAll')
const _mutateTree = Symbol('mutateTree')
const _flagsSuspect = Symbol.for('flagsSuspect')
const _workspaces = Symbol.for('workspaces')
const _prune = Symbol('prune')
const _preferDedupe = Symbol('preferDedupe')
const _legacyBundling = Symbol('legacyBundling')
const _parseSettings = Symbol('parseSettings')
const _initTree = Symbol('initTree')
const _applyUserRequests = Symbol('applyUserRequests')
const _applyUserRequestsToNode = Symbol('applyUserRequestsToNode')
const _inflateAncientLockfile = Symbol('inflateAncientLockfile')
const _buildDeps = Symbol('buildDeps')
const _buildDepStep = Symbol('buildDepStep')
Expand Down Expand Up @@ -109,7 +111,7 @@ const _peerSetSource = Symbol.for('peerSetSource')

// used by Reify mixin
const _force = Symbol.for('force')
const _explicitRequests = Symbol.for('explicitRequests')
const _explicitRequests = Symbol('explicitRequests')
const _global = Symbol.for('global')
const _idealTreePrune = Symbol.for('idealTreePrune')

Expand All @@ -130,8 +132,10 @@ module.exports = cls => class IdealTreeBuilder extends cls {
force = false,
packageLock = true,
strictPeerDeps = false,
workspaces = [],
} = options

this[_workspaces] = workspaces || []
this[_force] = !!force
this[_strictPeerDeps] = !!strictPeerDeps

Expand All @@ -143,6 +147,9 @@ module.exports = cls => class IdealTreeBuilder extends cls {
this[_globalStyle] = this[_global] || globalStyle
this[_follow] = !!follow

if (this[_workspaces].length && this[_global])
throw new Error('Cannot operate on workspaces in global mode')

this[_explicitRequests] = new Set()
this[_preferDedupe] = false
this[_legacyBundling] = false
Expand All @@ -157,6 +164,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
this[_manifests] = new Map()
this[_peerConflict] = null
this[_edgesOverridden] = new Set()
this[_resolvedAdd] = []

// a map of each module in a peer set to the thing that depended on
// that set of peers in the first place. Use a WeakMap so that we
Expand Down Expand Up @@ -204,8 +212,8 @@ module.exports = cls => class IdealTreeBuilder extends cls {

try {
await this[_initTree]()
await this[_applyUserRequests](options)
await this[_inflateAncientLockfile]()
await this[_applyUserRequests](options)
await this[_buildDeps]()
await this[_fixDepFlags]()
await this[_pruneFailedOptional]()
Expand Down Expand Up @@ -266,6 +274,7 @@ module.exports = cls => class IdealTreeBuilder extends cls {
this[_preferDedupe] = !!options.preferDedupe
this[_legacyBundling] = !!options.legacyBundling
this[_updateNames] = update.names

this[_updateAll] = update.all
// we prune by default unless explicitly set to boolean false
this[_prune] = options.prune !== false
Expand Down Expand Up @@ -387,6 +396,42 @@ module.exports = cls => class IdealTreeBuilder extends cls {
async [_applyUserRequests] (options) {
process.emit('time', 'idealTree:userRequests')
const tree = this.idealTree.target || this.idealTree

if (!this[_workspaces].length) {
return this[_applyUserRequestsToNode](tree, options).then(() =>
process.emit('timeEnd', 'idealTree:userRequests'))
}

const wsMap = tree.workspaces
if (!wsMap) {
this.log.warn('idealTree', 'Workspace filter set, but no workspaces present')
return
}

const promises = []
for (const name of this[_workspaces]) {
const path = wsMap.get(name)
if (!path) {
this.log.warn('idealTree', `Workspace ${name} in filter set, but not in workspaces`)
continue
}
const loc = relpath(tree.realpath, path)
const node = tree.inventory.get(loc)

/* istanbul ignore if - should be impossible */
if (!node) {
this.log.warn('idealTree', `Workspace ${name} in filter set, but no workspace folder present`)
continue
}

promises.push(this[_applyUserRequestsToNode](node, options))
}

return Promise.all(promises).then(() =>
process.emit('timeEnd', 'idealTree:userRequests'))
}

async [_applyUserRequestsToNode] (tree, options) {
// If we have a list of package names to update, and we know it's
// going to update them wherever they are, add any paths into those
// named nodes to the buildIdealTree queue.
Expand All @@ -395,38 +440,49 @@ module.exports = cls => class IdealTreeBuilder extends cls {

// global updates only update the globalTop nodes, but we need to know
// that they're there, and not reinstall the world unnecessarily.
const globalExplicitUpdateNames = []
if (this[_global] && (this[_updateAll] || this[_updateNames].length)) {
const nm = resolve(this.path, 'node_modules')
for (const name of await readdir(nm).catch(() => [])) {
if (this[_updateNames].includes(name))
this[_explicitRequests].add(name)
tree.package.dependencies = tree.package.dependencies || {}
if (this[_updateAll] || this[_updateNames].includes(name))
const updateName = this[_updateNames].includes(name)
if (this[_updateAll] || updateName) {
if (updateName)
globalExplicitUpdateNames.push(name)
tree.package.dependencies[name] = '*'
}
}
}

if (this.auditReport && this.auditReport.size > 0)
this[_queueVulnDependents](options)

if (options.rm && options.rm.length) {
addRmPkgDeps.rm(tree.package, options.rm)
for (const name of options.rm)
this[_explicitRequests].add(name)
const { add, rm } = options

if (rm && rm.length) {
addRmPkgDeps.rm(tree.package, rm)
for (const name of rm)
this[_explicitRequests].add({ from: tree, name, action: 'DELETE' })
}

if (options.add)
await this[_add](options)
if (add && add.length)
await this[_add](tree, options)

// triggers a refresh of all edgesOut
if (options.add && options.add.length || options.rm && options.rm.length || this[_global])
// triggers a refresh of all edgesOut. this has to be done BEFORE
// adding the edges to explicitRequests, because the package setter
// resets all edgesOut.
if (add && add.length || rm && rm.length || this[_global])
tree.package = tree.package
process.emit('timeEnd', 'idealTree:userRequests')

for (const spec of this[_resolvedAdd])
this[_explicitRequests].add(tree.edgesOut.get(spec.name))
for (const name of globalExplicitUpdateNames)
this[_explicitRequests].add(tree.edgesOut.get(name))
}

// This returns a promise because we might not have the name yet,
// and need to call pacote.manifest to find the name.
[_add] ({add, saveType = null, saveBundle = false}) {
[_add] (tree, {add, saveType = null, saveBundle = false}) {
// get the name for each of the specs in the list.
// ie, doing `foo@bar` we just return foo
// but if it's a url or git, we don't know the name until we
Expand All @@ -438,19 +494,16 @@ module.exports = cls => class IdealTreeBuilder extends cls {
.then(add => this[_updateFilePath](add))
.then(add => this[_followSymlinkPath](add))
})).then(add => {
this[_resolvedAdd] = add
this[_resolvedAdd].push(...add)
// now add is a list of spec objects with names.
// find a home for each of them!
const tree = this.idealTree.target || this.idealTree
addRmPkgDeps.add({
pkg: tree.package,
add,
saveBundle,
saveType,
path: this.path,
})
for (const spec of add)
this[_explicitRequests].add(spec.name)
})
}

Expand Down Expand Up @@ -991,7 +1044,7 @@ This is a one-time fix-up, please be patient...
// if it's peerOptional and not explicitly requested.
if (!edge.to) {
return edge.type !== 'peerOptional' ||
this[_explicitRequests].has(edge.name)
this[_explicitRequests].has(edge)
}

// If the edge has an error, there's a problem.
Expand All @@ -1007,7 +1060,7 @@ This is a one-time fix-up, please be patient...
return true

// If the user has explicitly asked to install this package, it's a problem.
if (node.isProjectRoot && this[_explicitRequests].has(edge.name))
if (node.isProjectRoot && this[_explicitRequests].has(edge))
return true

// No problems!
Expand Down Expand Up @@ -1131,7 +1184,7 @@ This is a one-time fix-up, please be patient...
continue

// problem
this[_failPeerConflict](edge)
this[_failPeerConflict](edge, parentEdge)
}
}

Expand All @@ -1147,17 +1200,17 @@ This is a one-time fix-up, please be patient...
continue

// ok, it's the root, or we're in unforced strict mode, so this is bad
this[_failPeerConflict](edge)
this[_failPeerConflict](edge, parentEdge)
}
return node
}

[_failPeerConflict] (edge) {
const expl = this[_explainPeerConflict](edge)
[_failPeerConflict] (edge, currentEdge) {
const expl = this[_explainPeerConflict](edge, currentEdge)
throw Object.assign(new Error('unable to resolve dependency tree'), expl)
}

[_explainPeerConflict] (edge) {
[_explainPeerConflict] (edge, currentEdge) {
const node = edge.from
const curNode = node.resolve(edge.name)
const pc = this[_peerConflict] || { peer: null, current: null }
Expand All @@ -1166,6 +1219,10 @@ This is a one-time fix-up, please be patient...
return {
code: 'ERESOLVE',
current,
// it SHOULD be impossible to get here without a current node in place,
// but this at least gives us something report on when bugs creep into
// the tree handling logic.
currentEdge: currentEdge ? currentEdge.explain() : null,
edge: edge.explain(),
peerConflict,
strictPeerDeps: this[_strictPeerDeps],
Expand All @@ -1190,7 +1247,7 @@ This is a one-time fix-up, please be patient...
[_placeDep] (dep, node, edge, peerEntryEdge = null, peerPath = []) {
if (edge.to &&
!edge.error &&
!this[_explicitRequests].has(edge.name) &&
!this[_explicitRequests].has(edge) &&
!this[_updateNames].includes(edge.name) &&
!this[_isVulnerable](edge.to))
return []
Expand Down Expand Up @@ -1480,9 +1537,15 @@ This is a one-time fix-up, please be patient...
if (target.children.has(edge.name)) {
const current = target.children.get(edge.name)

// same thing = keep
if (dep.matches(current))
return KEEP
// same thing = keep, UNLESS the current doesn't satisfy and new
// one does satisfy. This can happen if it's a link to a matching target
// at a different location, which satisfies a version dep, but not a
// file: dep. If neither of them satisfy, then we can replace it,
// because presumably it's better for a peer or something.
if (dep.matches(current)) {
if (current.satisfies(edge) || !dep.satisfies(edge))
return KEEP
}

const { version: curVer } = current
const { version: newVer } = dep
Expand Down
30 changes: 28 additions & 2 deletions lib/arborist/load-actual.js
Expand Up @@ -32,6 +32,7 @@ const _loadActual = Symbol('loadActual')
const _loadActualVirtually = Symbol('loadActualVirtually')
const _loadActualActually = Symbol('loadActualActually')
const _loadWorkspaces = Symbol.for('loadWorkspaces')
const _loadWorkspaceTargets = Symbol('loadWorkspaceTargets')
const _actualTreePromise = Symbol('actualTreePromise')
const _actualTree = Symbol('actualTree')
const _transplant = Symbol('transplant')
Expand Down Expand Up @@ -150,18 +151,22 @@ module.exports = cls => class ActualLoader extends cls {
await new this.constructor({...this.options}).loadVirtual({
root: this[_actualTree],
})
await this[_loadWorkspaces](this[_actualTree])
if (this[_actualTree].workspaces && this[_actualTree].workspaces.size)
calcDepFlags(this[_actualTree], !root)
this[_transplant](root)
return this[_actualTree]
}

async [_loadActualActually] ({ root, ignoreMissing, global }) {
await this[_loadFSTree](this[_actualTree])
await this[_loadWorkspaces](this[_actualTree])
await this[_loadWorkspaceTargets](this[_actualTree])
if (!ignoreMissing)
await this[_findMissingEdges]()
this[_findFSParents]()
this[_transplant](root)

await this[_loadWorkspaces](this[_actualTree])
if (global) {
// need to depend on the children, or else all of them
// will end up being flagged as extraneous, since the
Expand All @@ -178,16 +183,37 @@ module.exports = cls => class ActualLoader extends cls {
return this[_actualTree]
}

// if there are workspace targets without Link nodes created, load
// the targets, so that we know what they are.
async [_loadWorkspaceTargets] (tree) {
if (!tree.workspaces || !tree.workspaces.size)
return

const promises = []
for (const path of tree.workspaces.values()) {
if (!this[_cache].has(path)) {
const p = this[_loadFSNode]({ path, root: this[_actualTree] })
.then(node => this[_loadFSTree](node))
promises.push(p)
}
}
await Promise.all(promises)
}

[_transplant] (root) {
if (!root || root === this[_actualTree])
return

this[_actualTree][_changePath](root.path)
for (const node of this[_actualTree].children.values()) {
if (!this[_transplantFilter](node))
node.parent = null
node.root = null
}

root.replace(this[_actualTree])
for (const node of this[_actualTree].fsChildren)
node.root = this[_transplantFilter](node) ? root : null

this[_actualTree] = root
}

Expand Down