UNPKG

9.02 kBJavaScriptView Raw
1'use strict'
2
3const path = require('path')
4const fs = require('fs')
5const { promisify } = require('util')
6const readPackage = require('read-package-json')
7const builtins = require('module').builtinModules
8const resolveModule = require('resolve')
9const debug = require('debug')('dependency-check')
10const isRelative = require('is-relative')
11const globby = require('globby')
12const micromatch = require('micromatch')
13const pkgUp = require('pkg-up')
14
15const promisedFsAccess = promisify(fs.access)
16const promisedReadPackage = promisify(readPackage)
17
18async function resolveGlobbedPath (entries, cwd) {
19 if (typeof entries === 'string') entries = [entries]
20
21 debug('globby resolving', entries)
22
23 const resolvedEntries = await globby(entries, {
24 cwd,
25 absolute: true,
26 expandDirectories: false
27 })
28
29 const paths = Object.keys(resolvedEntries.reduce((result, entry) => {
30 // Globby yields unix-style paths.
31 const normalized = path.resolve(entry)
32
33 if (!result[normalized]) {
34 result[normalized] = true
35 }
36
37 return result
38 }, {}))
39
40 debug('globby resolved', paths)
41
42 return paths
43}
44
45async function resolveModuleTarget (targetPath) {
46 let pkgPath, pkg
47
48 try {
49 pkg = await promisedReadPackage(targetPath)
50 pkgPath = targetPath
51 } catch (err) {
52 if (targetPath.endsWith('/package.json') || targetPath === 'package.json') {
53 throw new Error('Failed to read package.json: ' + err.message)
54 }
55
56 if (err && err.code === 'EISDIR') {
57 // We were given a path to a module folder
58 pkgPath = path.join(targetPath, 'package.json')
59 pkg = await promisedReadPackage(pkgPath)
60 }
61 }
62
63 if (!pkg) return undefined
64
65 return {
66 pkgPath,
67 pkg
68 }
69}
70
71async function resolveEntryTarget (targetPath) {
72 // We've been given an entry path pattern as the target rather than a package.json or module folder
73 // We'll resolve those entries and then finds us the package.json from the location of those
74 const targetEntries = await resolveGlobbedPath(targetPath)
75
76 if (!targetEntries[0]) {
77 throw new Error('Failed to find package.json, no file to resolve it from')
78 }
79
80 const pkgPath = await pkgUp({ cwd: path.dirname(targetEntries[0]) })
81
82 if (!pkgPath) {
83 throw new Error('Failed to find a package.json')
84 }
85
86 const pkg = await promisedReadPackage(pkgPath)
87
88 return {
89 pkgPath,
90 pkg,
91 targetEntries
92 }
93}
94
95module.exports = async function ({
96 builtins,
97 detective,
98 entries,
99 extensions,
100 noDefaultEntries,
101 path: targetPath
102}) {
103 if (!targetPath) throw new Error('Requires a path to be set')
104
105 const {
106 pkgPath,
107 pkg,
108 targetEntries
109 } = await resolveModuleTarget(targetPath) || await resolveEntryTarget(targetPath)
110
111 entries = targetEntries ? [...targetEntries, ...entries] : entries
112 extensions = getExtensions(extensions, detective)
113 noDefaultEntries = noDefaultEntries || (targetEntries && targetEntries.length !== 0)
114
115 return parse({
116 builtins,
117 entries,
118 extensions,
119 noDefaultEntries,
120 package: pkg,
121 path: pkgPath
122 })
123}
124
125module.exports.missing = function (pkg, deps, options) {
126 const missing = []
127 const config = configure(pkg, options)
128
129 deps.map(used => {
130 if (!config.allDeps.includes(used) && !micromatch.isMatch(used, config.ignore)) {
131 missing.push(used)
132 }
133 })
134
135 return missing
136}
137
138module.exports.extra = function (pkg, deps, options) {
139 const missing = []
140 const config = configure(pkg, options)
141
142 config.allDeps.map(dep => {
143 if (!deps.includes(dep) && !micromatch.isMatch(dep, config.ignore)) {
144 missing.push(dep)
145 }
146 })
147
148 return missing
149}
150
151function getDetective (name) {
152 try {
153 return name
154 ? (typeof name === 'string' ? require(name) : name)
155 : require('detective')
156 } catch (e) {}
157}
158
159function noopDetective () {
160 return []
161}
162
163function getExtensions (extensions, detective) {
164 // Initialize extensions with node.js default handlers.
165 const result = {
166 '.js': noopDetective,
167 '.node': noopDetective,
168 '.json': noopDetective
169 }
170
171 if (Array.isArray(extensions)) {
172 extensions.forEach(extension => {
173 result[extension] = getDetective(detective)
174 })
175 } else if (typeof extensions === 'object') {
176 Object.keys(extensions).forEach(extension => {
177 result[extension] = getDetective(extensions[extension] || detective)
178 })
179 }
180
181 // Reset the `detective` instance for `.js` when it hasn't been set. This is
182 // done to defer loading detective when not needed and to keep `.js` first in
183 // the order of `Object.keys` (matching node.js behavior).
184 if (result['.js'] === noopDetective) {
185 result['.js'] = getDetective(detective)
186 }
187
188 return result
189}
190
191function configure (pkg, options) {
192 options = options || {}
193
194 let allDeps = Object.keys(pkg.dependencies || {}).concat(Object.keys(pkg.peerDependencies || {}))
195 let ignore = options.ignore || []
196
197 if (typeof ignore === 'string') ignore = [ignore]
198
199 if (!options.excludePeer) {
200 allDeps = allDeps.concat(Object.keys(pkg.peerDependencies || {}))
201 }
202
203 if (!options.excludeDev) {
204 allDeps = allDeps.concat(Object.keys(pkg.devDependencies || {}))
205 }
206
207 return {
208 allDeps,
209 ignore
210 }
211}
212
213function isNotRelative (file) {
214 return isRelative(file) && file[0] !== '.'
215}
216
217function joinAndResolvePath (basePath, targetPath) {
218 return path.resolve(path.join(basePath, targetPath))
219}
220
221async function resolveDefaultEntriesPaths (opts) {
222 const pkgPath = opts.path
223 const pkgDir = path.dirname(pkgPath)
224 const pkg = opts.package
225
226 const mainPath = joinAndResolvePath(pkgDir, pkg.main || 'index.js')
227
228 const paths = []
229
230 // Add the path of the main file
231 try {
232 await promisedFsAccess(mainPath)
233 paths.push(mainPath)
234 } catch (err) {}
235
236 // Add the path of binaries
237 if (pkg.bin) {
238 const binPaths = typeof pkg.bin === 'string'
239 ? [pkg.bin]
240 : Object.values(pkg.bin)
241
242 binPaths.forEach(cmd => {
243 paths.push(joinAndResolvePath(pkgDir, cmd))
244 })
245 }
246
247 return paths
248}
249
250async function resolvePaths (opts) {
251 const [
252 defaultEntries,
253 globbedPaths
254 ] = await Promise.all([
255 !opts.noDefaultEntries ? await resolveDefaultEntriesPaths(opts) : [],
256 opts.entries ? await resolveGlobbedPath(opts.entries, path.dirname(opts.path)) : []
257 ])
258
259 return [
260 ...defaultEntries,
261 ...globbedPaths
262 ]
263}
264
265async function parse (opts) {
266 const pkg = opts.package
267 const extensions = opts.extensions
268
269 const deps = {}
270 const seen = []
271 const core = []
272
273 const paths = await resolvePaths(opts)
274
275 debug('entry paths', paths)
276
277 if (paths.length === 0) return Promise.reject(new Error('No entry paths found'))
278
279 return Promise.all(paths.map(file => resolveDep(file)))
280 .then(allDeps => {
281 const used = {}
282 // merge all deps into one unique list
283 allDeps.forEach(deps => {
284 Object.keys(deps).forEach(dep => {
285 used[dep] = true
286 })
287 })
288
289 if (opts.builtins) return { package: pkg, used: Object.keys(used), builtins: core }
290
291 return { package: pkg, used: Object.keys(used) }
292 })
293
294 function resolveDep (file) {
295 if (isNotRelative(file)) {
296 return Promise.resolve(null)
297 }
298
299 return new Promise((resolve, reject) => {
300 resolveModule(file, {
301 basedir: path.dirname(file),
302 extensions: Object.keys(extensions)
303 }, (err, path) => {
304 if (err) return reject(err)
305 resolve(path)
306 })
307 })
308 .then(path => getDeps(path))
309 }
310
311 function getDeps (file) {
312 const ext = path.extname(file)
313 const detective = extensions[ext] || extensions['.js']
314
315 if (typeof detective !== 'function') {
316 return Promise.reject(new Error('Detective function missing for "' + file + '"'))
317 }
318
319 return new Promise((resolve, reject) => {
320 fs.readFile(file, 'utf8', (err, contents) => {
321 if (err) return reject(err)
322 resolve(contents)
323 })
324 })
325 .then(contents => {
326 const requires = detective(contents)
327 const relatives = []
328 requires.map(req => {
329 const isCore = builtins.includes(req)
330 if (isNotRelative(req) && !isCore) {
331 // require('foo/bar') -> require('foo')
332 if (req[0] !== '@' && req.includes('/')) req = req.split('/')[0]
333 else if (req[0] === '@') req = req.split('/').slice(0, 2).join('/')
334 debug('require("' + req + '")' + ' is a dependency')
335 deps[req] = true
336 } else {
337 if (isCore) {
338 debug('require("' + req + '")' + ' is core')
339 if (!core.includes(req)) {
340 core.push(req)
341 }
342 } else {
343 debug('require("' + req + '")' + ' is relative')
344 req = path.resolve(path.dirname(file), req)
345 if (!seen.includes(req)) {
346 seen.push(req)
347 relatives.push(req)
348 }
349 }
350 }
351 })
352
353 return Promise.all(relatives.map(name => resolveDep(name)))
354 .then(() => deps)
355 })
356 }
357}