index.js 3.6 KB

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