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]
 | |
|     }
 | |
|   })
 | |
| }
 |