417 lines
12 KiB
JavaScript
417 lines
12 KiB
JavaScript
var semver = require("semver")
|
|
var validateLicense = require('validate-npm-package-license');
|
|
var hostedGitInfo = require("hosted-git-info")
|
|
var isBuiltinModule = require("is-builtin-module")
|
|
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
|
|
) {
|
|
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]
|
|
}
|
|
})
|
|
}
|