419 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			419 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | var semver = require("semver") | ||
|  | var validateLicense = require('validate-npm-package-license'); | ||
|  | var hostedGitInfo = require("hosted-git-info") | ||
|  | var isBuiltinModule = require("resolve").isCore | ||
|  | var depTypes = ["dependencies","devDependencies","optionalDependencies"] | ||
|  | var extractDescription = require("./extract_description") | ||
|  | var url = require("url") | ||
|  | var typos = require("./typos.json") | ||
|  | 
 | ||
|  | var fixer = module.exports = { | ||
|  |   // default warning function
 | ||
|  |   warn: function() {}, | ||
|  | 
 | ||
|  |   fixRepositoryField: function(data) { | ||
|  |     if (data.repositories) { | ||
|  |       this.warn("repositories"); | ||
|  |       data.repository = data.repositories[0] | ||
|  |     } | ||
|  |     if (!data.repository) return this.warn("missingRepository") | ||
|  |     if (typeof data.repository === "string") { | ||
|  |       data.repository = { | ||
|  |         type: "git", | ||
|  |         url: data.repository | ||
|  |       } | ||
|  |     } | ||
|  |     var r = data.repository.url || "" | ||
|  |     if (r) { | ||
|  |       var hosted = hostedGitInfo.fromUrl(r) | ||
|  |       if (hosted) { | ||
|  |         r = data.repository.url | ||
|  |           = hosted.getDefaultRepresentation() == "shortcut" ? hosted.https() : hosted.toString() | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     if (r.match(/github.com\/[^\/]+\/[^\/]+\.git\.git$/)) { | ||
|  |       this.warn("brokenGitUrl", r) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  | , fixTypos: function(data) { | ||
|  |     Object.keys(typos.topLevel).forEach(function (d) { | ||
|  |       if (data.hasOwnProperty(d)) { | ||
|  |         this.warn("typo", d, typos.topLevel[d]) | ||
|  |       } | ||
|  |     }, this) | ||
|  |   } | ||
|  | 
 | ||
|  | , fixScriptsField: function(data) { | ||
|  |     if (!data.scripts) return | ||
|  |     if (typeof data.scripts !== "object") { | ||
|  |       this.warn("nonObjectScripts") | ||
|  |       delete data.scripts | ||
|  |       return | ||
|  |     } | ||
|  |     Object.keys(data.scripts).forEach(function (k) { | ||
|  |       if (typeof data.scripts[k] !== "string") { | ||
|  |         this.warn("nonStringScript") | ||
|  |         delete data.scripts[k] | ||
|  |       } else if (typos.script[k] && !data.scripts[typos.script[k]]) { | ||
|  |         this.warn("typo", k, typos.script[k], "scripts") | ||
|  |       } | ||
|  |     }, this) | ||
|  |   } | ||
|  | 
 | ||
|  | , fixFilesField: function(data) { | ||
|  |     var files = data.files | ||
|  |     if (files && !Array.isArray(files)) { | ||
|  |       this.warn("nonArrayFiles") | ||
|  |       delete data.files | ||
|  |     } else if (data.files) { | ||
|  |       data.files = data.files.filter(function(file) { | ||
|  |         if (!file || typeof file !== "string") { | ||
|  |           this.warn("invalidFilename", file) | ||
|  |           return false | ||
|  |         } else { | ||
|  |           return true | ||
|  |         } | ||
|  |       }, this) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  | , fixBinField: function(data) { | ||
|  |     if (!data.bin) return; | ||
|  |     if (typeof data.bin === "string") { | ||
|  |       var b = {} | ||
|  |       var match | ||
|  |       if (match = data.name.match(/^@[^/]+[/](.*)$/)) { | ||
|  |         b[match[1]] = data.bin | ||
|  |       } else { | ||
|  |         b[data.name] = data.bin | ||
|  |       } | ||
|  |       data.bin = b | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  | , fixManField: function(data) { | ||
|  |     if (!data.man) return; | ||
|  |     if (typeof data.man === "string") { | ||
|  |       data.man = [ data.man ] | ||
|  |     } | ||
|  |   } | ||
|  | , fixBundleDependenciesField: function(data) { | ||
|  |     var bdd = "bundledDependencies" | ||
|  |     var bd = "bundleDependencies" | ||
|  |     if (data[bdd] && !data[bd]) { | ||
|  |       data[bd] = data[bdd] | ||
|  |       delete data[bdd] | ||
|  |     } | ||
|  |     if (data[bd] && !Array.isArray(data[bd])) { | ||
|  |       this.warn("nonArrayBundleDependencies") | ||
|  |       delete data[bd] | ||
|  |     } else if (data[bd]) { | ||
|  |       data[bd] = data[bd].filter(function(bd) { | ||
|  |         if (!bd || typeof bd !== 'string') { | ||
|  |           this.warn("nonStringBundleDependency", bd) | ||
|  |           return false | ||
|  |         } else { | ||
|  |           if (!data.dependencies) { | ||
|  |             data.dependencies = {} | ||
|  |           } | ||
|  |           if (!data.dependencies.hasOwnProperty(bd)) { | ||
|  |             this.warn("nonDependencyBundleDependency", bd) | ||
|  |             data.dependencies[bd] = "*" | ||
|  |           } | ||
|  |           return true | ||
|  |         } | ||
|  |       }, this) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  | , fixDependencies: function(data, strict) { | ||
|  |     var loose = !strict | ||
|  |     objectifyDeps(data, this.warn) | ||
|  |     addOptionalDepsToDeps(data, this.warn) | ||
|  |     this.fixBundleDependenciesField(data) | ||
|  | 
 | ||
|  |     ;['dependencies','devDependencies'].forEach(function(deps) { | ||
|  |       if (!(deps in data)) return | ||
|  |       if (!data[deps] || typeof data[deps] !== "object") { | ||
|  |         this.warn("nonObjectDependencies", deps) | ||
|  |         delete data[deps] | ||
|  |         return | ||
|  |       } | ||
|  |       Object.keys(data[deps]).forEach(function (d) { | ||
|  |         var r = data[deps][d] | ||
|  |         if (typeof r !== 'string') { | ||
|  |           this.warn("nonStringDependency", d, JSON.stringify(r)) | ||
|  |           delete data[deps][d] | ||
|  |         } | ||
|  |         var hosted = hostedGitInfo.fromUrl(data[deps][d]) | ||
|  |         if (hosted) data[deps][d] = hosted.toString() | ||
|  |       }, this) | ||
|  |     }, this) | ||
|  |   } | ||
|  | 
 | ||
|  | , fixModulesField: function (data) { | ||
|  |     if (data.modules) { | ||
|  |       this.warn("deprecatedModules") | ||
|  |       delete data.modules | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  | , fixKeywordsField: function (data) { | ||
|  |     if (typeof data.keywords === "string") { | ||
|  |       data.keywords = data.keywords.split(/,\s+/) | ||
|  |     } | ||
|  |     if (data.keywords && !Array.isArray(data.keywords)) { | ||
|  |       delete data.keywords | ||
|  |       this.warn("nonArrayKeywords") | ||
|  |     } else if (data.keywords) { | ||
|  |       data.keywords = data.keywords.filter(function(kw) { | ||
|  |         if (typeof kw !== "string" || !kw) { | ||
|  |           this.warn("nonStringKeyword"); | ||
|  |           return false | ||
|  |         } else { | ||
|  |           return true | ||
|  |         } | ||
|  |       }, this) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  | , fixVersionField: function(data, strict) { | ||
|  |     // allow "loose" semver 1.0 versions in non-strict mode
 | ||
|  |     // enforce strict semver 2.0 compliance in strict mode
 | ||
|  |     var loose = !strict | ||
|  |     if (!data.version) { | ||
|  |       data.version = "" | ||
|  |       return true | ||
|  |     } | ||
|  |     if (!semver.valid(data.version, loose)) { | ||
|  |       throw new Error('Invalid version: "'+ data.version + '"') | ||
|  |     } | ||
|  |     data.version = semver.clean(data.version, loose) | ||
|  |     return true | ||
|  |   } | ||
|  | 
 | ||
|  | , fixPeople: function(data) { | ||
|  |     modifyPeople(data, unParsePerson) | ||
|  |     modifyPeople(data, parsePerson) | ||
|  |   } | ||
|  | 
 | ||
|  | , fixNameField: function(data, options) { | ||
|  |     if (typeof options === "boolean") options = {strict: options} | ||
|  |     else if (typeof options === "undefined") options = {} | ||
|  |     var strict = options.strict | ||
|  |     if (!data.name && !strict) { | ||
|  |       data.name = "" | ||
|  |       return | ||
|  |     } | ||
|  |     if (typeof data.name !== "string") { | ||
|  |       throw new Error("name field must be a string.") | ||
|  |     } | ||
|  |     if (!strict) | ||
|  |       data.name = data.name.trim() | ||
|  |     ensureValidName(data.name, strict, options.allowLegacyCase) | ||
|  |     if (isBuiltinModule(data.name)) | ||
|  |       this.warn("conflictingName", data.name) | ||
|  |   } | ||
|  | 
 | ||
|  | 
 | ||
|  | , fixDescriptionField: function (data) { | ||
|  |     if (data.description && typeof data.description !== 'string') { | ||
|  |       this.warn("nonStringDescription") | ||
|  |       delete data.description | ||
|  |     } | ||
|  |     if (data.readme && !data.description) | ||
|  |       data.description = extractDescription(data.readme) | ||
|  |       if(data.description === undefined) delete data.description; | ||
|  |     if (!data.description) this.warn("missingDescription") | ||
|  |   } | ||
|  | 
 | ||
|  | , fixReadmeField: function (data) { | ||
|  |     if (!data.readme) { | ||
|  |       this.warn("missingReadme") | ||
|  |       data.readme = "ERROR: No README data found!" | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  | , fixBugsField: function(data) { | ||
|  |     if (!data.bugs && data.repository && data.repository.url) { | ||
|  |       var hosted = hostedGitInfo.fromUrl(data.repository.url) | ||
|  |       if(hosted && hosted.bugs()) { | ||
|  |         data.bugs = {url: hosted.bugs()} | ||
|  |       } | ||
|  |     } | ||
|  |     else if(data.bugs) { | ||
|  |       var emailRe = /^.+@.*\..+$/ | ||
|  |       if(typeof data.bugs == "string") { | ||
|  |         if(emailRe.test(data.bugs)) | ||
|  |           data.bugs = {email:data.bugs} | ||
|  |         else if(url.parse(data.bugs).protocol) | ||
|  |           data.bugs = {url: data.bugs} | ||
|  |         else | ||
|  |           this.warn("nonEmailUrlBugsString") | ||
|  |       } | ||
|  |       else { | ||
|  |         bugsTypos(data.bugs, this.warn) | ||
|  |         var oldBugs = data.bugs | ||
|  |         data.bugs = {} | ||
|  |         if(oldBugs.url) { | ||
|  |           if(typeof(oldBugs.url) == "string" && url.parse(oldBugs.url).protocol) | ||
|  |             data.bugs.url = oldBugs.url | ||
|  |           else | ||
|  |             this.warn("nonUrlBugsUrlField") | ||
|  |         } | ||
|  |         if(oldBugs.email) { | ||
|  |           if(typeof(oldBugs.email) == "string" && emailRe.test(oldBugs.email)) | ||
|  |             data.bugs.email = oldBugs.email | ||
|  |           else | ||
|  |             this.warn("nonEmailBugsEmailField") | ||
|  |         } | ||
|  |       } | ||
|  |       if(!data.bugs.email && !data.bugs.url) { | ||
|  |         delete data.bugs | ||
|  |         this.warn("emptyNormalizedBugs") | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  | , fixHomepageField: function(data) { | ||
|  |     if (!data.homepage && data.repository && data.repository.url) { | ||
|  |       var hosted = hostedGitInfo.fromUrl(data.repository.url) | ||
|  |       if (hosted && hosted.docs()) data.homepage = hosted.docs() | ||
|  |     } | ||
|  |     if (!data.homepage) return | ||
|  | 
 | ||
|  |     if(typeof data.homepage !== "string") { | ||
|  |       this.warn("nonUrlHomepage") | ||
|  |       return delete data.homepage | ||
|  |     } | ||
|  |     if(!url.parse(data.homepage).protocol) { | ||
|  |       data.homepage = "http://" + data.homepage | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  | , fixLicenseField: function(data) { | ||
|  |     if (!data.license) { | ||
|  |       return this.warn("missingLicense") | ||
|  |     } else{ | ||
|  |       if ( | ||
|  |         typeof(data.license) !== 'string' || | ||
|  |         data.license.length < 1 || | ||
|  |         data.license.trim() === '' | ||
|  |       ) { | ||
|  |         this.warn("invalidLicense") | ||
|  |       } else { | ||
|  |         if (!validateLicense(data.license).validForNewPackages) | ||
|  |           this.warn("invalidLicense") | ||
|  |       } | ||
|  |     } | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function isValidScopedPackageName(spec) { | ||
|  |   if (spec.charAt(0) !== '@') return false | ||
|  | 
 | ||
|  |   var rest = spec.slice(1).split('/') | ||
|  |   if (rest.length !== 2) return false | ||
|  | 
 | ||
|  |   return rest[0] && rest[1] && | ||
|  |     rest[0] === encodeURIComponent(rest[0]) && | ||
|  |     rest[1] === encodeURIComponent(rest[1]) | ||
|  | } | ||
|  | 
 | ||
|  | function isCorrectlyEncodedName(spec) { | ||
|  |   return !spec.match(/[\/@\s\+%:]/) && | ||
|  |     spec === encodeURIComponent(spec) | ||
|  | } | ||
|  | 
 | ||
|  | function ensureValidName (name, strict, allowLegacyCase) { | ||
|  |   if (name.charAt(0) === "." || | ||
|  |       !(isValidScopedPackageName(name) || isCorrectlyEncodedName(name)) || | ||
|  |       (strict && (!allowLegacyCase) && name !== name.toLowerCase()) || | ||
|  |       name.toLowerCase() === "node_modules" || | ||
|  |       name.toLowerCase() === "favicon.ico") { | ||
|  |         throw new Error("Invalid name: " + JSON.stringify(name)) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function modifyPeople (data, fn) { | ||
|  |   if (data.author) data.author = fn(data.author) | ||
|  |   ;["maintainers", "contributors"].forEach(function (set) { | ||
|  |     if (!Array.isArray(data[set])) return; | ||
|  |     data[set] = data[set].map(fn) | ||
|  |   }) | ||
|  |   return data | ||
|  | } | ||
|  | 
 | ||
|  | function unParsePerson (person) { | ||
|  |   if (typeof person === "string") return person | ||
|  |   var name = person.name || "" | ||
|  |   var u = person.url || person.web | ||
|  |   var url = u ? (" ("+u+")") : "" | ||
|  |   var e = person.email || person.mail | ||
|  |   var email = e ? (" <"+e+">") : "" | ||
|  |   return name+email+url | ||
|  | } | ||
|  | 
 | ||
|  | function parsePerson (person) { | ||
|  |   if (typeof person !== "string") return person | ||
|  |   var name = person.match(/^([^\(<]+)/) | ||
|  |   var url = person.match(/\(([^\)]+)\)/) | ||
|  |   var email = person.match(/<([^>]+)>/) | ||
|  |   var obj = {} | ||
|  |   if (name && name[0].trim()) obj.name = name[0].trim() | ||
|  |   if (email) obj.email = email[1]; | ||
|  |   if (url) obj.url = url[1]; | ||
|  |   return obj | ||
|  | } | ||
|  | 
 | ||
|  | function addOptionalDepsToDeps (data, warn) { | ||
|  |   var o = data.optionalDependencies | ||
|  |   if (!o) return; | ||
|  |   var d = data.dependencies || {} | ||
|  |   Object.keys(o).forEach(function (k) { | ||
|  |     d[k] = o[k] | ||
|  |   }) | ||
|  |   data.dependencies = d | ||
|  | } | ||
|  | 
 | ||
|  | function depObjectify (deps, type, warn) { | ||
|  |   if (!deps) return {} | ||
|  |   if (typeof deps === "string") { | ||
|  |     deps = deps.trim().split(/[\n\r\s\t ,]+/) | ||
|  |   } | ||
|  |   if (!Array.isArray(deps)) return deps | ||
|  |   warn("deprecatedArrayDependencies", type) | ||
|  |   var o = {} | ||
|  |   deps.filter(function (d) { | ||
|  |     return typeof d === "string" | ||
|  |   }).forEach(function(d) { | ||
|  |     d = d.trim().split(/(:?[@\s><=])/) | ||
|  |     var dn = d.shift() | ||
|  |     var dv = d.join("") | ||
|  |     dv = dv.trim() | ||
|  |     dv = dv.replace(/^@/, "") | ||
|  |     o[dn] = dv | ||
|  |   }) | ||
|  |   return o | ||
|  | } | ||
|  | 
 | ||
|  | function objectifyDeps (data, warn) { | ||
|  |   depTypes.forEach(function (type) { | ||
|  |     if (!data[type]) return; | ||
|  |     data[type] = depObjectify(data[type], type, warn) | ||
|  |   }) | ||
|  | } | ||
|  | 
 | ||
|  | function bugsTypos(bugs, warn) { | ||
|  |   if (!bugs) return | ||
|  |   Object.keys(bugs).forEach(function (k) { | ||
|  |     if (typos.bugs[k]) { | ||
|  |       warn("typo", k, typos.bugs[k], "bugs") | ||
|  |       bugs[typos.bugs[k]] = bugs[k] | ||
|  |       delete bugs[k] | ||
|  |     } | ||
|  |   }) | ||
|  | } |