• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1var semver = require("semver")
2var validateLicense = require('validate-npm-package-license');
3var hostedGitInfo = require("hosted-git-info")
4var isBuiltinModule = require("resolve").isCore
5var depTypes = ["dependencies","devDependencies","optionalDependencies"]
6var extractDescription = require("./extract_description")
7var url = require("url")
8var typos = require("./typos.json")
9
10var 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
314function 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
325function isCorrectlyEncodedName(spec) {
326  return !spec.match(/[\/@\s\+%:]/) &&
327    spec === encodeURIComponent(spec)
328}
329
330function 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
340function 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
349function 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
359function 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
371function 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
381function 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
402function objectifyDeps (data, warn) {
403  depTypes.forEach(function (type) {
404    if (!data[type]) return;
405    data[type] = depObjectify(data[type], type, warn)
406  })
407}
408
409function 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}
419