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