var isValidSemver = require('semver/functions/valid') var cleanSemver = require('semver/functions/clean') var validateLicense = require('validate-npm-package-license') var hostedGitInfo = require('hosted-git-info') var isBuiltinModule = require('is-core-module') var depTypes = ['dependencies', 'devDependencies', 'optionalDependencies'] var extractDescription = require('./extract_description') var url = require('url') var typos = require('./typos.json') var isEmail = str => str.includes('@') && (str.indexOf('@') < str.lastIndexOf('.')) 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 (Object.prototype.hasOwnProperty.call(data, 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 (filtered) { if (!filtered || typeof filtered !== 'string') { this.warn('nonStringBundleDependency', filtered) return false } else { if (!data.dependencies) { data.dependencies = {} } if (!Object.prototype.hasOwnProperty.call(data.dependencies, filtered)) { this.warn('nonDependencyBundleDependency', filtered) data.dependencies[filtered] = '*' } return true } }, this) } }, fixDependencies: function (data, 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 (!isValidSemver(data.version, loose)) { throw new Error('Invalid version: "' + data.version + '"') } data.version = cleanSemver(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) { if (typeof data.bugs === 'string') { if (isEmail(data.bugs)) { data.bugs = { email: data.bugs } /* eslint-disable-next-line node/no-deprecated-api */ } 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) { /* eslint-disable-next-line node/no-deprecated-api */ 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' && isEmail(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 } /* eslint-disable-next-line node/no-deprecated-api */ if (!url.parse(data.homepage).protocol) { data.homepage = 'http://' + data.homepage } }, fixLicenseField: function (data) { const license = data.license || data.licence if (!license) { return this.warn('missingLicense') } if ( typeof (license) !== 'string' || license.length < 1 || license.trim() === '' ) { return this.warn('invalidLicense') } if (!validateLicense(license).validForNewPackages) { return 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 wrappedUrl = u ? (' (' + u + ')') : '' var e = person.email || person.mail var wrappedEmail = e ? (' <' + e + '>') : '' return name + wrappedEmail + wrappedUrl } function parsePerson (person) { if (typeof person !== 'string') { return person } var matchedName = person.match(/^([^(<]+)/) var matchedUrl = person.match(/\(([^()]+)\)/) var matchedEmail = person.match(/<([^<>]+)>/) var obj = {} if (matchedName && matchedName[0].trim()) { obj.name = matchedName[0].trim() } if (matchedEmail) { obj.email = matchedEmail[1] } if (matchedUrl) { obj.url = matchedUrl[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] } }) }