index.js 3.6 KB

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