You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
418 lines
12 KiB
418 lines
12 KiB
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] |
|
} |
|
}) |
|
}
|
|
|