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