// Do not rely on package._fields, so that we don't throw // false failures if a tree is generated by other clients. // Only relies on child.resolved, which MAY come from // client-specific package.json meta _fields, but most of // the time will be pulled out of a lockfile const semver = require('semver') const npa = require('npm-package-arg') const { relative } = require('path') const fromPath = require('./from-path.js') const depValid = (child, requested, requestor) => { // NB: we don't do much to verify 'tag' type requests. // Just verify that we got a remote resolution. Presumably, it // came from a registry and was tagged at some point. if (typeof requested === 'string') { try { // tarball/dir must have resolved to the same tgz on disk, but for // file: deps that depend on other files/dirs, we must resolve the // location based on the *requestor* file/dir, not where it ends up. // '' is equivalent to '*' requested = npa.resolve(child.name, requested || '*', fromPath(requestor, requestor.edgesOut.get(child.name))) } catch (er) { // Not invalid because the child doesn't match, but because // the spec itself is not supported. Nothing would match, // so the edge is definitely not valid and never can be. er.dependency = child.name er.requested = requested requestor.errors.push(er) return false } } // if the lockfile is super old, or hand-modified, // then it's possible to hit this state. if (!requested) { const er = new Error('Invalid dependency specifier') er.dependency = child.name er.requested = requested requestor.errors.push(er) return false } switch (requested.type) { case 'range': if (requested.fetchSpec === '*') { return true } // fallthrough case 'version': // if it's a version or a range other than '*', semver it return semver.satisfies(child.version, requested.fetchSpec, true) case 'directory': return linkValid(child, requested, requestor) case 'file': return tarballValid(child, requested, requestor) case 'alias': // check that the alias target is valid return depValid(child, requested.subSpec, requestor) case 'tag': // if it's a tag, we just verify that it has a tarball resolution // presumably, it came from the registry and was tagged at some point return child.resolved && npa(child.resolved).type === 'remote' case 'remote': // verify that we got it from the desired location return child.resolved === requested.fetchSpec case 'git': { // if it's a git type, verify that they're the same repo // // if it specifies a definite commit, then it must have the // same commit to be considered the same repo // // if it has a #semver: specifier, verify that the // version in the package is in the semver range const resRepo = npa(child.resolved || '') const resHost = resRepo.hosted const reqHost = requested.hosted const reqCommit = /^[a-fA-F0-9]{40}$/.test(requested.gitCommittish || '') const nc = { noCommittish: !reqCommit } const sameRepo = resHost ? reqHost && reqHost.ssh(nc) === resHost.ssh(nc) : resRepo.fetchSpec === requested.fetchSpec return !sameRepo ? false : !requested.gitRange ? true : semver.satisfies(child.package.version, requested.gitRange, { loose: true, }) } default: // unpossible, just being cautious break } const er = new Error('Unsupported dependency type') er.dependency = child.name er.requested = requested requestor.errors.push(er) return false } const linkValid = (child, requested, requestor) => { const isLink = !!child.isLink // if we're installing links and the node is a link, then it's invalid because we want // a real node to be there if (requestor.installLinks) { return !isLink } // directory must be a link to the specified folder return isLink && relative(child.realpath, requested.fetchSpec) === '' } const tarballValid = (child, requested, requestor) => { if (child.isLink) { return false } if (child.resolved) { return child.resolved.replace(/\\/g, '/') === `file:${requested.fetchSpec.replace(/\\/g, '/')}` } // if we have a legacy mutated package.json file. we can't be 100% // sure that it resolved to the same file, but if it was the same // request, that's a pretty good indicator of sameness. if (child.package._requested) { return child.package._requested.saveSpec === requested.saveSpec } // ok, we're probably dealing with some legacy cruft here, not much // we can do at this point unfortunately. return false } module.exports = (child, requested, accept, requestor) => depValid(child, requested, requestor) || (typeof accept === 'string' ? depValid(child, accept, requestor) : false)