1const { readFile, lstat, readdir } = require('fs/promises') 2const parse = require('json-parse-even-better-errors') 3const normalizePackageBin = require('npm-normalize-package-bin') 4const { resolve, dirname, join, relative } = require('path') 5 6const rpj = path => readFile(path, 'utf8') 7 .then(data => readBinDir(path, normalize(stripUnderscores(parse(data))))) 8 .catch(er => { 9 er.path = path 10 throw er 11 }) 12 13// load the directories.bin folder as a 'bin' object 14const readBinDir = async (path, data) => { 15 if (data.bin) { 16 return data 17 } 18 19 const m = data.directories && data.directories.bin 20 if (!m || typeof m !== 'string') { 21 return data 22 } 23 24 // cut off any monkey business, like setting directories.bin 25 // to ../../../etc/passwd or /etc/passwd or something like that. 26 const root = dirname(path) 27 const dir = join('.', join('/', m)) 28 data.bin = await walkBinDir(root, dir, {}) 29 return data 30} 31 32const walkBinDir = async (root, dir, obj) => { 33 const entries = await readdir(resolve(root, dir)).catch(() => []) 34 for (const entry of entries) { 35 if (entry.charAt(0) === '.') { 36 continue 37 } 38 const f = resolve(root, dir, entry) 39 // ignore stat errors, weird file types, symlinks, etc. 40 const st = await lstat(f).catch(() => null) 41 if (!st) { 42 continue 43 } else if (st.isFile()) { 44 obj[entry] = relative(root, f) 45 } else if (st.isDirectory()) { 46 await walkBinDir(root, join(dir, entry), obj) 47 } 48 } 49 return obj 50} 51 52// do not preserve _fields set in files, they are sus 53const stripUnderscores = data => { 54 for (const key of Object.keys(data).filter(k => /^_/.test(k))) { 55 delete data[key] 56 } 57 return data 58} 59 60const normalize = data => { 61 addId(data) 62 fixBundled(data) 63 pruneRepeatedOptionals(data) 64 fixScripts(data) 65 fixFunding(data) 66 normalizePackageBin(data) 67 return data 68} 69 70rpj.normalize = normalize 71 72const addId = data => { 73 if (data.name && data.version) { 74 data._id = `${data.name}@${data.version}` 75 } 76 return data 77} 78 79// it was once common practice to list deps both in optionalDependencies 80// and in dependencies, to support npm versions that did not know abbout 81// optionalDependencies. This is no longer a relevant need, so duplicating 82// the deps in two places is unnecessary and excessive. 83const pruneRepeatedOptionals = data => { 84 const od = data.optionalDependencies 85 const dd = data.dependencies || {} 86 if (od && typeof od === 'object') { 87 for (const name of Object.keys(od)) { 88 delete dd[name] 89 } 90 } 91 if (Object.keys(dd).length === 0) { 92 delete data.dependencies 93 } 94 return data 95} 96 97const fixBundled = data => { 98 const bdd = data.bundledDependencies 99 const bd = data.bundleDependencies === undefined ? bdd 100 : data.bundleDependencies 101 102 if (bd === false) { 103 data.bundleDependencies = [] 104 } else if (bd === true) { 105 data.bundleDependencies = Object.keys(data.dependencies || {}) 106 } else if (bd && typeof bd === 'object') { 107 if (!Array.isArray(bd)) { 108 data.bundleDependencies = Object.keys(bd) 109 } else { 110 data.bundleDependencies = bd 111 } 112 } else { 113 delete data.bundleDependencies 114 } 115 116 delete data.bundledDependencies 117 return data 118} 119 120const fixScripts = data => { 121 if (!data.scripts || typeof data.scripts !== 'object') { 122 delete data.scripts 123 return data 124 } 125 126 for (const [name, script] of Object.entries(data.scripts)) { 127 if (typeof script !== 'string') { 128 delete data.scripts[name] 129 } 130 } 131 return data 132} 133 134const fixFunding = data => { 135 if (data.funding && typeof data.funding === 'string') { 136 data.funding = { url: data.funding } 137 } 138 return data 139} 140 141module.exports = rpj 142