1 | 'use strict'
|
2 |
|
3 | const path = require('path')
|
4 | const fs = require('fs')
|
5 | const { promisify } = require('util')
|
6 | const readPackage = require('read-package-json')
|
7 | const builtins = require('module').builtinModules
|
8 | const resolveModule = require('resolve')
|
9 | const debug = require('debug')('dependency-check')
|
10 | const isRelative = require('is-relative')
|
11 | const globby = require('globby')
|
12 | const micromatch = require('micromatch')
|
13 | const pkgUp = require('pkg-up')
|
14 |
|
15 | const promisedFsAccess = promisify(fs.access)
|
16 | const promisedReadPackage = promisify(readPackage)
|
17 |
|
18 | async 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 |
|
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 |
|
45 | async 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 |
|
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 |
|
71 | async function resolveEntryTarget (targetPath) {
|
72 |
|
73 |
|
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 |
|
95 | module.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 |
|
125 | module.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 |
|
138 | module.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 |
|
151 | function getDetective (name) {
|
152 | try {
|
153 | return name
|
154 | ? (typeof name === 'string' ? require(name) : name)
|
155 | : require('detective')
|
156 | } catch (e) {}
|
157 | }
|
158 |
|
159 | function noopDetective () {
|
160 | return []
|
161 | }
|
162 |
|
163 | function getExtensions (extensions, detective) {
|
164 |
|
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 |
|
182 |
|
183 |
|
184 | if (result['.js'] === noopDetective) {
|
185 | result['.js'] = getDetective(detective)
|
186 | }
|
187 |
|
188 | return result
|
189 | }
|
190 |
|
191 | function 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 |
|
213 | function isNotRelative (file) {
|
214 | return isRelative(file) && file[0] !== '.'
|
215 | }
|
216 |
|
217 | function joinAndResolvePath (basePath, targetPath) {
|
218 | return path.resolve(path.join(basePath, targetPath))
|
219 | }
|
220 |
|
221 | async 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 |
|
231 | try {
|
232 | await promisedFsAccess(mainPath)
|
233 | paths.push(mainPath)
|
234 | } catch (err) {}
|
235 |
|
236 |
|
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 |
|
250 | async 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 |
|
265 | async 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 |
|
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 |
|
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 | }
|