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