codekingpro/portable-devtools
114k
1// Do not rely on package._fields, so that we don't throw
2// false failures if a tree is generated by other clients.
3// Only relies on child.resolved, which MAY come from
4// client-specific package.json meta _fields, but most of
5// the time will be pulled out of a lockfile
6
7const semver = require('semver')
8const npa = require('npm-package-arg')
9const { relative } = require('node:path')
10const fromPath = require('./from-path.js')
11
12const depValid = (child, requested, requestor) => {
13 // NB: we don't do much to verify 'tag' type requests.
14 // Just verify that we got a remote resolution. Presumably, it
15 // came from a registry and was tagged at some point.
16
17 if (typeof requested === 'string') {
18 try {
19 // tarball/dir must have resolved to the same tgz on disk, but for
20 // file: deps that depend on other files/dirs, we must resolve the
21 // location based on the *requestor* file/dir, not where it ends up.
22 // '' is equivalent to '*'
23 requested = npa.resolve(child.name, requested || '*', fromPath(requestor, requestor.edgesOut.get(child.name)))
24 } catch (er) {
25 // Not invalid because the child doesn't match, but because
26 // the spec itself is not supported. Nothing would match,
27 // so the edge is definitely not valid and never can be.
28 er.dependency = child.name
29 er.requested = requested
30 requestor.errors.push(er)
31 return false
32 }
33 }
34
35 // if the lockfile is super old, or hand-modified,
36 // then it's possible to hit this state.
37 if (!requested) {
38 const er = new Error('Invalid dependency specifier')
39 er.dependency = child.name
40 er.requested = requested
41 requestor.errors.push(er)
42 return false
43 }
44
45 switch (requested.type) {
46 case 'range':
47 if (requested.fetchSpec === '*') {
48 return true
49 }
50 // fallthrough
51 case 'version':
52 // if it's a version or a range other than '*', semver it
53 return semver.satisfies(child.version, requested.fetchSpec, true)
54
55 case 'directory':
56 return linkValid(child, requested, requestor)
57
58 case 'file':
59 return tarballValid(child, requested, requestor)
60
61 case 'alias':
62 // check that the alias target is valid
63 return depValid(child, requested.subSpec, requestor)
64
65 case 'tag':
66 // if it's a tag, we just verify that it has a tarball resolution
67 // presumably, it came from the registry and was tagged at some point
68 return child.resolved && npa(child.resolved).type === 'remote'
69
70 case 'remote':
71 // verify that we got it from the desired location
72 return child.resolved === requested.fetchSpec
73
74 case 'git': {
75 // if it's a git type, verify that they're the same repo
76 //
77 // if it specifies a definite commit, then it must have the
78 // same commit to be considered the same repo
79 //
80 // if it has a #semver:<range> specifier, verify that the
81 // version in the package is in the semver range
82 const resRepo = npa(child.resolved || '')
83 const resHost = resRepo.hosted
84 const reqHost = requested.hosted
85 const reqCommit = /^[a-fA-F0-9]{40,64}$/.test(requested.gitCommittish || '')
86 const nc = { noCommittish: !reqCommit }
87 if (!resHost) {
88 if (resRepo.fetchSpec !== requested.fetchSpec) {
89 return false
90 }
91 } else {
92 if (reqHost?.ssh(nc) !== resHost.ssh(nc)) {
93 return false
94 }
95 }
96 if (!requested.gitRange) {
97 return true
98 }
99 return semver.satisfies(child.package.version, requested.gitRange, {
100 loose: true,
101 })
102 }
103
104 default: // impossible, just being cautious
105 break
106 }
107
108 const er = new Error('Unsupported dependency type')
109 er.dependency = child.name
110 er.requested = requested
111 requestor.errors.push(er)
112 return false
113}
114
115const linkValid = (child, requested, requestor) => {
116 const isLink = !!child.isLink
117 // if we're installing links and the node is a link, then it's invalid because we want
118 // a real node to be there. Except for workspaces. They are always links.
119 if (requestor.installLinks && !child.isWorkspace) {
120 return !isLink
121 }
122
123 // directory must be a link to the specified folder
124 return isLink && relative(child.realpath, requested.fetchSpec) === ''
125}
126
127const tarballValid = (child, requested) => {
128 if (child.isLink) {
129 return false
130 }
131
132 if (child.resolved) {
133 return child.resolved.replace(/\\/g, '/') === `file:${requested.fetchSpec.replace(/\\/g, '/')}`
134 }
135
136 // if we have a legacy mutated package.json file. we can't be 100%
137 // sure that it resolved to the same file, but if it was the same
138 // request, that's a pretty good indicator of sameness.
139 if (child.package._requested) {
140 return child.package._requested.saveSpec === requested.saveSpec
141 }
142
143 // ok, we're probably dealing with some legacy cruft here, not much
144 // we can do at this point unfortunately.
145 return false
146}
147
148module.exports = (child, requested, accept, requestor) =>
149 depValid(child, requested, requestor) ||
150 (typeof accept === 'string' ? depValid(child, accept, requestor) : false)
151 