import { fromUrl } from "hosted-git-info"
import * as semver from "semver"
import * as url from "url"
export function normalizePackageData(data: any) {
for (const it of check) {
it(data)
}
}
const depTypes = ["dependencies", "devDependencies", "optionalDependencies"]
const check = [
function (data: any) {
if (data.repositories) {
data.repository = data.repositories[0]
}
if (typeof data.repository === "string") {
data.repository = {
type: "git",
url: data.repository,
}
}
if (data.repository != null && data.repository.url) {
const hosted = fromUrl(data.repository.url)
if (hosted) {
data.repository.url = hosted.getDefaultRepresentation() == "shortcut" ? hosted.https() : hosted.toString()
}
}
},
function (data: any) {
const files = data.files
if (files && !Array.isArray(files)) {
delete data.files
} else if (data.files) {
data.files = data.files.filter(function (file: any) {
return !(!file || typeof file !== "string")
})
}
},
function (data: any) {
if (!data.bin) {
return
}
if (typeof data.bin === "string") {
const b: any = {}
const match = data.name.match(/^@[^/]+[/](.*)$/)
if (match) {
b[match[1]] = data.bin
} else {
b[data.name] = data.bin
}
data.bin = b
}
},
function (data: any) {
if (data.description && typeof data.description !== "string") {
delete data.description
}
if (data.description === undefined) {
delete data.description
}
},
fixBundleDependenciesField,
function fixDependencies(data: any) {
objectifyDeps(data)
fixBundleDependenciesField(data)
for (const deps of ["dependencies", "devDependencies", "optionalDependencies"]) {
if (!(deps in data)) {
continue
}
if (!data[deps] || typeof data[deps] !== "object") {
delete data[deps]
continue
}
Object.keys(data[deps]).forEach(function (d) {
const r = data[deps][d]
if (typeof r !== "string") {
delete data[deps][d]
}
const hosted = fromUrl(data[deps][d])
if (hosted) {
data[deps][d] = hosted.toString()
}
})
}
},
function fixBugsField(data: any) {
if (!data.bugs && data.repository && data.repository.url) {
const hosted = fromUrl(data.repository.url)
if (hosted && hosted.bugs()) {
data.bugs = { url: hosted.bugs() }
}
} else if (data.bugs) {
const 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 {
bugsTypos(data.bugs)
const oldBugs = data.bugs
data.bugs = {}
if (oldBugs.url) {
if (typeof oldBugs.url == "string" && url.parse(oldBugs.url).protocol) {
data.bugs.url = oldBugs.url
}
}
if (oldBugs.email) {
if (typeof oldBugs.email == "string" && emailRe.test(oldBugs.email)) {
data.bugs.email = oldBugs.email
}
}
}
if (!data.bugs.email && !data.bugs.url) {
delete data.bugs
}
}
},
function fixModulesField(data: any) {
if (data.modules) {
delete data.modules
}
},
function fixKeywordsField(data: any) {
if (typeof data.keywords === "string") {
data.keywords = data.keywords.split(/,\s+/)
}
if (data.keywords && !Array.isArray(data.keywords)) {
delete data.keywords
} else if (data.keywords) {
data.keywords = data.keywords.filter(function (kw: any) {
return !(typeof kw !== "string" || !kw)
})
}
},
function fixVersionField(data: any) {
const loose = true
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
},
function fixPeople(data: any) {
modifyPeople(data, unParsePerson)
modifyPeople(data, parsePerson)
},
function fixNameField(data: any) {
if (!data.name) {
data.name = ""
return
}
if (typeof data.name !== "string") {
throw new Error("name field must be a string.")
}
data.name = data.name.trim()
ensureValidName(data.name)
},
function fixHomepageField(data: any) {
if (!data.homepage && data.repository && data.repository.url) {
const hosted = fromUrl(data.repository.url)
if (hosted && hosted.docs()) {
data.homepage = hosted.docs()
}
}
if (!data.homepage) {
return
}
if (typeof data.homepage !== "string") {
delete data.homepage
}
if (!url.parse(data.homepage).protocol) {
data.homepage = `https://${data.homepage}`
}
return
},
]
function fixBundleDependenciesField(data: any) {
const bdd = "bundledDependencies"
const bd = "bundleDependencies"
if (data[bdd] && !data[bd]) {
data[bd] = data[bdd]
delete data[bdd]
}
if (data[bd] && !Array.isArray(data[bd])) {
delete data[bd]
} else if (data[bd]) {
data[bd] = data[bd].filter(function (bd: any) {
if (!bd || typeof bd !== "string") {
return false
} else {
if (!data.dependencies) {
data.dependencies = {}
}
if (!("bd" in data.dependencies)) {
data.dependencies[bd] = "*"
}
return true
}
})
}
}
function isValidScopedPackageName(spec: string): boolean {
if (spec.charAt(0) !== "@") {
return false
}
const rest = spec.slice(1).split("/")
if (rest.length !== 2) {
return false
}
return rest[0] !== "" && rest[1] !== "" && rest[0] != null && rest[1] != null && rest[0] === encodeURIComponent(rest[0]) && rest[1] === encodeURIComponent(rest[1])
}
function isCorrectlyEncodedName(spec: string): boolean {
return !/[/@\s+%:]/.test(spec) && spec === encodeURIComponent(spec)
}
function ensureValidName(name: string): void {
if (
name.charAt(0) === "." ||
!(isValidScopedPackageName(name) || isCorrectlyEncodedName(name)) ||
name.toLowerCase() === "node_modules" ||
name.toLowerCase() === "favicon.ico"
) {
throw new Error("Invalid name: " + JSON.stringify(name))
}
}
function modifyPeople(data: any, fn: any): any {
if (data.author) {
data.author = fn(data.author)
}
for (const set of ["maintainers", "contributors"]) {
if (!Array.isArray(data[set])) {
continue
}
data[set] = data[set].map(fn)
}
return data
}
function unParsePerson(person: any): string {
if (typeof person === "string") {
return person
}
const name = person.name || ""
const u = person.url || person.web
const url = u ? ` (${u})` : ""
const e = person.email || person.mail
const email = e ? ` <${e}>` : ""
return `${name}${email}${url}`
}
function parsePerson(person: any) {
if (typeof person !== "string") {
return person
}
const name = /^([^(<]+)/.exec(person)
const url = /\(([^)]+)\)/.exec(person)
const email = /<([^>]+)>/.exec(person)
const obj: any = {}
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 depObjectify(deps: any): any {
if (!deps) {
return {}
}
if (typeof deps === "string") {
deps = deps.trim().split(/[\n\r\s\t ,]+/)
}
if (!Array.isArray(deps)) {
return deps
}
const o: any = {}
deps
.filter(function (d) {
return typeof d === "string"
})
.forEach(function (d: string) {
const arr: string[] = d.trim().split(/(:?[@\s><=])/)
const dn = arr.shift()
let dv = arr.join("")
dv = dv.trim()
dv = dv.replace(/^@/, "")
o[dn!] = dv
})
return o
}
function objectifyDeps(data: any) {
depTypes.forEach(function (type) {
if (!data[type]) {
return
}
data[type] = depObjectify(data[type])
})
}
const typoBugs: any = { web: "url", name: "url" }
function bugsTypos(bugs: any) {
if (!bugs) {
return
}
Object.keys(bugs).forEach(function (k) {
if (typoBugs[k]) {
bugs[typoBugs[k]] = bugs[k]
delete bugs[k]
}
})
}