fixer.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. var isValidSemver = require('semver/functions/valid')
  2. var cleanSemver = require('semver/functions/clean')
  3. var validateLicense = require('validate-npm-package-license')
  4. var hostedGitInfo = require('hosted-git-info')
  5. var isBuiltinModule = require('is-core-module')
  6. var depTypes = ['dependencies', 'devDependencies', 'optionalDependencies']
  7. var extractDescription = require('./extract_description')
  8. var url = require('url')
  9. var typos = require('./typos.json')
  10. var isEmail = str => str.includes('@') && (str.indexOf('@') < str.lastIndexOf('.'))
  11. module.exports = {
  12. // default warning function
  13. warn: function () {},
  14. fixRepositoryField: function (data) {
  15. if (data.repositories) {
  16. this.warn('repositories')
  17. data.repository = data.repositories[0]
  18. }
  19. if (!data.repository) {
  20. return this.warn('missingRepository')
  21. }
  22. if (typeof data.repository === 'string') {
  23. data.repository = {
  24. type: 'git',
  25. url: data.repository,
  26. }
  27. }
  28. var r = data.repository.url || ''
  29. if (r) {
  30. var hosted = hostedGitInfo.fromUrl(r)
  31. if (hosted) {
  32. r = data.repository.url
  33. = hosted.getDefaultRepresentation() === 'shortcut' ? hosted.https() : hosted.toString()
  34. }
  35. }
  36. if (r.match(/github.com\/[^/]+\/[^/]+\.git\.git$/)) {
  37. this.warn('brokenGitUrl', r)
  38. }
  39. },
  40. fixTypos: function (data) {
  41. Object.keys(typos.topLevel).forEach(function (d) {
  42. if (Object.prototype.hasOwnProperty.call(data, d)) {
  43. this.warn('typo', d, typos.topLevel[d])
  44. }
  45. }, this)
  46. },
  47. fixScriptsField: function (data) {
  48. if (!data.scripts) {
  49. return
  50. }
  51. if (typeof data.scripts !== 'object') {
  52. this.warn('nonObjectScripts')
  53. delete data.scripts
  54. return
  55. }
  56. Object.keys(data.scripts).forEach(function (k) {
  57. if (typeof data.scripts[k] !== 'string') {
  58. this.warn('nonStringScript')
  59. delete data.scripts[k]
  60. } else if (typos.script[k] && !data.scripts[typos.script[k]]) {
  61. this.warn('typo', k, typos.script[k], 'scripts')
  62. }
  63. }, this)
  64. },
  65. fixFilesField: function (data) {
  66. var files = data.files
  67. if (files && !Array.isArray(files)) {
  68. this.warn('nonArrayFiles')
  69. delete data.files
  70. } else if (data.files) {
  71. data.files = data.files.filter(function (file) {
  72. if (!file || typeof file !== 'string') {
  73. this.warn('invalidFilename', file)
  74. return false
  75. } else {
  76. return true
  77. }
  78. }, this)
  79. }
  80. },
  81. fixBinField: function (data) {
  82. if (!data.bin) {
  83. return
  84. }
  85. if (typeof data.bin === 'string') {
  86. var b = {}
  87. var match
  88. if (match = data.name.match(/^@[^/]+[/](.*)$/)) {
  89. b[match[1]] = data.bin
  90. } else {
  91. b[data.name] = data.bin
  92. }
  93. data.bin = b
  94. }
  95. },
  96. fixManField: function (data) {
  97. if (!data.man) {
  98. return
  99. }
  100. if (typeof data.man === 'string') {
  101. data.man = [data.man]
  102. }
  103. },
  104. fixBundleDependenciesField: function (data) {
  105. var bdd = 'bundledDependencies'
  106. var bd = 'bundleDependencies'
  107. if (data[bdd] && !data[bd]) {
  108. data[bd] = data[bdd]
  109. delete data[bdd]
  110. }
  111. if (data[bd] && !Array.isArray(data[bd])) {
  112. this.warn('nonArrayBundleDependencies')
  113. delete data[bd]
  114. } else if (data[bd]) {
  115. data[bd] = data[bd].filter(function (filtered) {
  116. if (!filtered || typeof filtered !== 'string') {
  117. this.warn('nonStringBundleDependency', filtered)
  118. return false
  119. } else {
  120. if (!data.dependencies) {
  121. data.dependencies = {}
  122. }
  123. if (!Object.prototype.hasOwnProperty.call(data.dependencies, filtered)) {
  124. this.warn('nonDependencyBundleDependency', filtered)
  125. data.dependencies[filtered] = '*'
  126. }
  127. return true
  128. }
  129. }, this)
  130. }
  131. },
  132. fixDependencies: function (data, strict) {
  133. objectifyDeps(data, this.warn)
  134. addOptionalDepsToDeps(data, this.warn)
  135. this.fixBundleDependenciesField(data)
  136. ;['dependencies', 'devDependencies'].forEach(function (deps) {
  137. if (!(deps in data)) {
  138. return
  139. }
  140. if (!data[deps] || typeof data[deps] !== 'object') {
  141. this.warn('nonObjectDependencies', deps)
  142. delete data[deps]
  143. return
  144. }
  145. Object.keys(data[deps]).forEach(function (d) {
  146. var r = data[deps][d]
  147. if (typeof r !== 'string') {
  148. this.warn('nonStringDependency', d, JSON.stringify(r))
  149. delete data[deps][d]
  150. }
  151. var hosted = hostedGitInfo.fromUrl(data[deps][d])
  152. if (hosted) {
  153. data[deps][d] = hosted.toString()
  154. }
  155. }, this)
  156. }, this)
  157. },
  158. fixModulesField: function (data) {
  159. if (data.modules) {
  160. this.warn('deprecatedModules')
  161. delete data.modules
  162. }
  163. },
  164. fixKeywordsField: function (data) {
  165. if (typeof data.keywords === 'string') {
  166. data.keywords = data.keywords.split(/,\s+/)
  167. }
  168. if (data.keywords && !Array.isArray(data.keywords)) {
  169. delete data.keywords
  170. this.warn('nonArrayKeywords')
  171. } else if (data.keywords) {
  172. data.keywords = data.keywords.filter(function (kw) {
  173. if (typeof kw !== 'string' || !kw) {
  174. this.warn('nonStringKeyword')
  175. return false
  176. } else {
  177. return true
  178. }
  179. }, this)
  180. }
  181. },
  182. fixVersionField: function (data, strict) {
  183. // allow "loose" semver 1.0 versions in non-strict mode
  184. // enforce strict semver 2.0 compliance in strict mode
  185. var loose = !strict
  186. if (!data.version) {
  187. data.version = ''
  188. return true
  189. }
  190. if (!isValidSemver(data.version, loose)) {
  191. throw new Error('Invalid version: "' + data.version + '"')
  192. }
  193. data.version = cleanSemver(data.version, loose)
  194. return true
  195. },
  196. fixPeople: function (data) {
  197. modifyPeople(data, unParsePerson)
  198. modifyPeople(data, parsePerson)
  199. },
  200. fixNameField: function (data, options) {
  201. if (typeof options === 'boolean') {
  202. options = { strict: options }
  203. } else if (typeof options === 'undefined') {
  204. options = {}
  205. }
  206. var strict = options.strict
  207. if (!data.name && !strict) {
  208. data.name = ''
  209. return
  210. }
  211. if (typeof data.name !== 'string') {
  212. throw new Error('name field must be a string.')
  213. }
  214. if (!strict) {
  215. data.name = data.name.trim()
  216. }
  217. ensureValidName(data.name, strict, options.allowLegacyCase)
  218. if (isBuiltinModule(data.name)) {
  219. this.warn('conflictingName', data.name)
  220. }
  221. },
  222. fixDescriptionField: function (data) {
  223. if (data.description && typeof data.description !== 'string') {
  224. this.warn('nonStringDescription')
  225. delete data.description
  226. }
  227. if (data.readme && !data.description) {
  228. data.description = extractDescription(data.readme)
  229. }
  230. if (data.description === undefined) {
  231. delete data.description
  232. }
  233. if (!data.description) {
  234. this.warn('missingDescription')
  235. }
  236. },
  237. fixReadmeField: function (data) {
  238. if (!data.readme) {
  239. this.warn('missingReadme')
  240. data.readme = 'ERROR: No README data found!'
  241. }
  242. },
  243. fixBugsField: function (data) {
  244. if (!data.bugs && data.repository && data.repository.url) {
  245. var hosted = hostedGitInfo.fromUrl(data.repository.url)
  246. if (hosted && hosted.bugs()) {
  247. data.bugs = { url: hosted.bugs() }
  248. }
  249. } else if (data.bugs) {
  250. if (typeof data.bugs === 'string') {
  251. if (isEmail(data.bugs)) {
  252. data.bugs = { email: data.bugs }
  253. /* eslint-disable-next-line node/no-deprecated-api */
  254. } else if (url.parse(data.bugs).protocol) {
  255. data.bugs = { url: data.bugs }
  256. } else {
  257. this.warn('nonEmailUrlBugsString')
  258. }
  259. } else {
  260. bugsTypos(data.bugs, this.warn)
  261. var oldBugs = data.bugs
  262. data.bugs = {}
  263. if (oldBugs.url) {
  264. /* eslint-disable-next-line node/no-deprecated-api */
  265. if (typeof (oldBugs.url) === 'string' && url.parse(oldBugs.url).protocol) {
  266. data.bugs.url = oldBugs.url
  267. } else {
  268. this.warn('nonUrlBugsUrlField')
  269. }
  270. }
  271. if (oldBugs.email) {
  272. if (typeof (oldBugs.email) === 'string' && isEmail(oldBugs.email)) {
  273. data.bugs.email = oldBugs.email
  274. } else {
  275. this.warn('nonEmailBugsEmailField')
  276. }
  277. }
  278. }
  279. if (!data.bugs.email && !data.bugs.url) {
  280. delete data.bugs
  281. this.warn('emptyNormalizedBugs')
  282. }
  283. }
  284. },
  285. fixHomepageField: function (data) {
  286. if (!data.homepage && data.repository && data.repository.url) {
  287. var hosted = hostedGitInfo.fromUrl(data.repository.url)
  288. if (hosted && hosted.docs()) {
  289. data.homepage = hosted.docs()
  290. }
  291. }
  292. if (!data.homepage) {
  293. return
  294. }
  295. if (typeof data.homepage !== 'string') {
  296. this.warn('nonUrlHomepage')
  297. return delete data.homepage
  298. }
  299. /* eslint-disable-next-line node/no-deprecated-api */
  300. if (!url.parse(data.homepage).protocol) {
  301. data.homepage = 'http://' + data.homepage
  302. }
  303. },
  304. fixLicenseField: function (data) {
  305. const license = data.license || data.licence
  306. if (!license) {
  307. return this.warn('missingLicense')
  308. }
  309. if (
  310. typeof (license) !== 'string' ||
  311. license.length < 1 ||
  312. license.trim() === ''
  313. ) {
  314. return this.warn('invalidLicense')
  315. }
  316. if (!validateLicense(license).validForNewPackages) {
  317. return this.warn('invalidLicense')
  318. }
  319. },
  320. }
  321. function isValidScopedPackageName (spec) {
  322. if (spec.charAt(0) !== '@') {
  323. return false
  324. }
  325. var rest = spec.slice(1).split('/')
  326. if (rest.length !== 2) {
  327. return false
  328. }
  329. return rest[0] && rest[1] &&
  330. rest[0] === encodeURIComponent(rest[0]) &&
  331. rest[1] === encodeURIComponent(rest[1])
  332. }
  333. function isCorrectlyEncodedName (spec) {
  334. return !spec.match(/[/@\s+%:]/) &&
  335. spec === encodeURIComponent(spec)
  336. }
  337. function ensureValidName (name, strict, allowLegacyCase) {
  338. if (name.charAt(0) === '.' ||
  339. !(isValidScopedPackageName(name) || isCorrectlyEncodedName(name)) ||
  340. (strict && (!allowLegacyCase) && name !== name.toLowerCase()) ||
  341. name.toLowerCase() === 'node_modules' ||
  342. name.toLowerCase() === 'favicon.ico') {
  343. throw new Error('Invalid name: ' + JSON.stringify(name))
  344. }
  345. }
  346. function modifyPeople (data, fn) {
  347. if (data.author) {
  348. data.author = fn(data.author)
  349. }['maintainers', 'contributors'].forEach(function (set) {
  350. if (!Array.isArray(data[set])) {
  351. return
  352. }
  353. data[set] = data[set].map(fn)
  354. })
  355. return data
  356. }
  357. function unParsePerson (person) {
  358. if (typeof person === 'string') {
  359. return person
  360. }
  361. var name = person.name || ''
  362. var u = person.url || person.web
  363. var wrappedUrl = u ? (' (' + u + ')') : ''
  364. var e = person.email || person.mail
  365. var wrappedEmail = e ? (' <' + e + '>') : ''
  366. return name + wrappedEmail + wrappedUrl
  367. }
  368. function parsePerson (person) {
  369. if (typeof person !== 'string') {
  370. return person
  371. }
  372. var matchedName = person.match(/^([^(<]+)/)
  373. var matchedUrl = person.match(/\(([^()]+)\)/)
  374. var matchedEmail = person.match(/<([^<>]+)>/)
  375. var obj = {}
  376. if (matchedName && matchedName[0].trim()) {
  377. obj.name = matchedName[0].trim()
  378. }
  379. if (matchedEmail) {
  380. obj.email = matchedEmail[1]
  381. }
  382. if (matchedUrl) {
  383. obj.url = matchedUrl[1]
  384. }
  385. return obj
  386. }
  387. function addOptionalDepsToDeps (data, warn) {
  388. var o = data.optionalDependencies
  389. if (!o) {
  390. return
  391. }
  392. var d = data.dependencies || {}
  393. Object.keys(o).forEach(function (k) {
  394. d[k] = o[k]
  395. })
  396. data.dependencies = d
  397. }
  398. function depObjectify (deps, type, warn) {
  399. if (!deps) {
  400. return {}
  401. }
  402. if (typeof deps === 'string') {
  403. deps = deps.trim().split(/[\n\r\s\t ,]+/)
  404. }
  405. if (!Array.isArray(deps)) {
  406. return deps
  407. }
  408. warn('deprecatedArrayDependencies', type)
  409. var o = {}
  410. deps.filter(function (d) {
  411. return typeof d === 'string'
  412. }).forEach(function (d) {
  413. d = d.trim().split(/(:?[@\s><=])/)
  414. var dn = d.shift()
  415. var dv = d.join('')
  416. dv = dv.trim()
  417. dv = dv.replace(/^@/, '')
  418. o[dn] = dv
  419. })
  420. return o
  421. }
  422. function objectifyDeps (data, warn) {
  423. depTypes.forEach(function (type) {
  424. if (!data[type]) {
  425. return
  426. }
  427. data[type] = depObjectify(data[type], type, warn)
  428. })
  429. }
  430. function bugsTypos (bugs, warn) {
  431. if (!bugs) {
  432. return
  433. }
  434. Object.keys(bugs).forEach(function (k) {
  435. if (typos.bugs[k]) {
  436. warn('typo', k, typos.bugs[k], 'bugs')
  437. bugs[typos.bugs[k]] = bugs[k]
  438. delete bugs[k]
  439. }
  440. })
  441. }