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