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