You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

fixer.js 12KB

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