codekingpro/portable-devtools
114k
1// to GET CONTENTS for folder at PATH (which may be a PACKAGE):
2// - if PACKAGE, read path/package.json
3// - if bins in ../node_modules/.bin, add those to result
4// - if depth >= maxDepth, add PATH to result, and finish
5// - readdir(PATH, with file types)
6// - add all FILEs in PATH to result
7// - if PARENT:
8// - if depth < maxDepth, add GET CONTENTS of all DIRs in PATH
9// - else, add all DIRs in PATH
10// - if no parent
11// - if no bundled deps,
12// - if depth < maxDepth, add GET CONTENTS of DIRs in path except
13// node_modules
14// - else, add all DIRs in path other than node_modules
15// - if has bundled deps,
16// - get list of bundled deps
17// - add GET CONTENTS of bundled deps, PACKAGE=true, depth + 1
18
19const bundled = require('npm-bundled')
20const { readFile, readdir, stat } = require('fs/promises')
21const { resolve, basename, dirname } = require('path')
22const normalizePackageBin = require('npm-normalize-package-bin')
23
24const readPackage = ({ path, packageJsonCache }) => packageJsonCache.has(path)
25 ? Promise.resolve(packageJsonCache.get(path))
26 : readFile(path).then(json => {
27 const pkg = normalizePackageBin(JSON.parse(json))
28 packageJsonCache.set(path, pkg)
29 return pkg
30 }).catch(() => null)
31
32// just normalize bundle deps and bin, that's all we care about here.
33const normalized = Symbol('package data has been normalized')
34const rpj = ({ path, packageJsonCache }) => readPackage({ path, packageJsonCache })
35 .then(pkg => {
36 if (!pkg || pkg[normalized]) {
37 return pkg
38 }
39 if (pkg.bundledDependencies && !pkg.bundleDependencies) {
40 pkg.bundleDependencies = pkg.bundledDependencies
41 delete pkg.bundledDependencies
42 }
43 const bd = pkg.bundleDependencies
44 if (bd === true) {
45 pkg.bundleDependencies = [
46 ...Object.keys(pkg.dependencies || {}),
47 ...Object.keys(pkg.optionalDependencies || {}),
48 ]
49 }
50 if (typeof bd === 'object' && !Array.isArray(bd)) {
51 pkg.bundleDependencies = Object.keys(bd)
52 }
53 pkg[normalized] = true
54 return pkg
55 })
56
57const pkgContents = async ({
58 path,
59 depth = 1,
60 currentDepth = 0,
61 pkg = null,
62 result = null,
63 packageJsonCache = null,
64}) => {
65 if (!result) {
66 result = new Set()
67 }
68
69 if (!packageJsonCache) {
70 packageJsonCache = new Map()
71 }
72
73 if (pkg === true) {
74 return rpj({ path: path + '/package.json', packageJsonCache })
75 .then(p => pkgContents({
76 path,
77 depth,
78 currentDepth,
79 pkg: p,
80 result,
81 packageJsonCache,
82 }))
83 }
84
85 if (pkg) {
86 // add all bins to result if they exist
87 if (pkg.bin) {
88 const dir = dirname(path)
89 const scope = basename(dir)
90 const nm = /^@.+/.test(scope) ? dirname(dir) : dir
91
92 const binFiles = []
93 Object.keys(pkg.bin).forEach(b => {
94 const base = resolve(nm, '.bin', b)
95 binFiles.push(base, base + '.cmd', base + '.ps1')
96 })
97
98 const bins = await Promise.all(
99 binFiles.map(b => stat(b).then(() => b).catch(() => null))
100 )
101 bins.filter(b => b).forEach(b => result.add(b))
102 }
103 }
104
105 if (currentDepth >= depth) {
106 result.add(path)
107 return result
108 }
109
110 // we'll need bundle list later, so get that now in parallel
111 const [dirEntries, bundleDeps] = await Promise.all([
112 readdir(path, { withFileTypes: true }),
113 currentDepth === 0 && pkg && pkg.bundleDependencies
114 ? bundled({ path, packageJsonCache }) : null,
115 ]).catch(() => [])
116
117 // not a thing, probably a missing folder
118 if (!dirEntries) {
119 return result
120 }
121
122 // empty folder, just add the folder itself to the result
123 if (!dirEntries.length && !bundleDeps && currentDepth !== 0) {
124 result.add(path)
125 return result
126 }
127
128 const recursePromises = []
129
130 for (const entry of dirEntries) {
131 const p = resolve(path, entry.name)
132 if (entry.isDirectory() === false) {
133 result.add(p)
134 continue
135 }
136
137 if (currentDepth !== 0 || entry.name !== 'node_modules') {
138 if (currentDepth < depth - 1) {
139 recursePromises.push(pkgContents({
140 path: p,
141 packageJsonCache,
142 depth,
143 currentDepth: currentDepth + 1,
144 result,
145 }))
146 } else {
147 result.add(p)
148 }
149 continue
150 }
151 }
152
153 if (bundleDeps) {
154 // bundle deps are all folders
155 // we always recurse to get pkg bins, but if currentDepth is too high,
156 // it'll return early before walking their contents.
157 recursePromises.push(...bundleDeps.map(dep => {
158 const p = resolve(path, 'node_modules', dep)
159 return pkgContents({
160 path: p,
161 packageJsonCache,
162 pkg: true,
163 depth,
164 currentDepth: currentDepth + 1,
165 result,
166 })
167 }))
168 }
169
170 if (recursePromises.length) {
171 await Promise.all(recursePromises)
172 }
173
174 return result
175}
176
177module.exports = ({ path, ...opts }) => pkgContents({
178 path: resolve(path),
179 ...opts,
180 pkg: true,
181}).then(results => [...results])
182 