123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- // Originally normalize-package-data
- const url = require('node:url')
- const hostedGitInfo = require('hosted-git-info')
- const validateLicense = require('validate-npm-package-license')
- const typos = {
- dependancies: 'dependencies',
- dependecies: 'dependencies',
- depdenencies: 'dependencies',
- devEependencies: 'devDependencies',
- depends: 'dependencies',
- 'dev-dependencies': 'devDependencies',
- devDependences: 'devDependencies',
- devDepenencies: 'devDependencies',
- devdependencies: 'devDependencies',
- repostitory: 'repository',
- repo: 'repository',
- prefereGlobal: 'preferGlobal',
- hompage: 'homepage',
- hampage: 'homepage',
- autohr: 'author',
- autor: 'author',
- contributers: 'contributors',
- publicationConfig: 'publishConfig',
- script: 'scripts',
- }
- const isEmail = str => str.includes('@') && (str.indexOf('@') < str.lastIndexOf('.'))
- // Extracts description from contents of a readme file in markdown format
- function extractDescription (description) {
- // the first block of text before the first heading that isn't the first line heading
- const lines = description.trim().split('\n')
- let start = 0
- // skip initial empty lines and lines that start with #
- while (lines[start]?.trim().match(/^(#|$)/)) {
- start++
- }
- let end = start + 1
- // keep going till we get to the end or an empty line
- while (end < lines.length && lines[end].trim()) {
- end++
- }
- return lines.slice(start, end).join(' ').trim()
- }
- function stringifyPerson (person) {
- if (typeof person !== 'string') {
- const name = person.name || ''
- const u = person.url || person.web
- const wrappedUrl = u ? (' (' + u + ')') : ''
- const e = person.email || person.mail
- const wrappedEmail = e ? (' <' + e + '>') : ''
- person = name + wrappedEmail + wrappedUrl
- }
- const matchedName = person.match(/^([^(<]+)/)
- const matchedUrl = person.match(/\(([^()]+)\)/)
- const matchedEmail = person.match(/<([^<>]+)>/)
- const parsed = {}
- if (matchedName?.[0].trim()) {
- parsed.name = matchedName[0].trim()
- }
- if (matchedEmail) {
- parsed.email = matchedEmail[1]
- }
- if (matchedUrl) {
- parsed.url = matchedUrl[1]
- }
- return parsed
- }
- function normalizeData (data, changes) {
- // fixDescriptionField
- if (data.description && typeof data.description !== 'string') {
- changes?.push(`'description' field should be a string`)
- delete data.description
- }
- if (data.readme && !data.description && data.readme !== 'ERROR: No README data found!') {
- data.description = extractDescription(data.readme)
- }
- if (data.description === undefined) {
- delete data.description
- }
- if (!data.description) {
- changes?.push('No description')
- }
- // fixModulesField
- if (data.modules) {
- changes?.push(`modules field is deprecated`)
- delete data.modules
- }
- // fixFilesField
- const files = data.files
- if (files && !Array.isArray(files)) {
- changes?.push(`Invalid 'files' member`)
- delete data.files
- } else if (data.files) {
- data.files = data.files.filter(function (file) {
- if (!file || typeof file !== 'string') {
- changes?.push(`Invalid filename in 'files' list: ${file}`)
- return false
- } else {
- return true
- }
- })
- }
- // fixManField
- if (data.man && typeof data.man === 'string') {
- data.man = [data.man]
- }
- // fixBugsField
- if (!data.bugs && data.repository?.url) {
- const 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 {
- changes?.push(`Bug string field must be url, email, or {email,url}`)
- }
- } else {
- for (const k in data.bugs) {
- if (['web', 'name'].includes(k)) {
- changes?.push(`bugs['${k}'] should probably be bugs['url'].`)
- data.bugs.url = data.bugs[k]
- delete data.bugs[k]
- }
- }
- const 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 {
- changes?.push('bugs.url field must be a string url. Deleted.')
- }
- }
- if (oldBugs.email) {
- if (typeof (oldBugs.email) === 'string' && isEmail(oldBugs.email)) {
- data.bugs.email = oldBugs.email
- } else {
- changes?.push('bugs.email field must be a string email. Deleted.')
- }
- }
- }
- if (!data.bugs.email && !data.bugs.url) {
- delete data.bugs
- changes?.push('Normalized value of bugs field is an empty object. Deleted.')
- }
- }
- // fixKeywordsField
- if (typeof data.keywords === 'string') {
- data.keywords = data.keywords.split(/,\s+/)
- }
- if (data.keywords && !Array.isArray(data.keywords)) {
- delete data.keywords
- changes?.push(`keywords should be an array of strings`)
- } else if (data.keywords) {
- data.keywords = data.keywords.filter(function (kw) {
- if (typeof kw !== 'string' || !kw) {
- changes?.push(`keywords should be an array of strings`)
- return false
- } else {
- return true
- }
- })
- }
- // fixBundleDependenciesField
- 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])) {
- changes?.push(`Invalid 'bundleDependencies' list. Must be array of package names`)
- delete data[bd]
- } else if (data[bd]) {
- data[bd] = data[bd].filter(function (filtered) {
- if (!filtered || typeof filtered !== 'string') {
- changes?.push(`Invalid bundleDependencies member: ${filtered}`)
- return false
- } else {
- if (!data.dependencies) {
- data.dependencies = {}
- }
- if (!Object.prototype.hasOwnProperty.call(data.dependencies, filtered)) {
- changes?.push(`Non-dependency in bundleDependencies: ${filtered}`)
- data.dependencies[filtered] = '*'
- }
- return true
- }
- })
- }
- // fixHomepageField
- if (!data.homepage && data.repository && data.repository.url) {
- const hosted = hostedGitInfo.fromUrl(data.repository.url)
- if (hosted) {
- data.homepage = hosted.docs()
- }
- }
- if (data.homepage) {
- if (typeof data.homepage !== 'string') {
- changes?.push('homepage field must be a string url. Deleted.')
- delete data.homepage
- } else {
- /* eslint-disable-next-line node/no-deprecated-api */
- if (!url.parse(data.homepage).protocol) {
- data.homepage = 'http://' + data.homepage
- }
- }
- }
- // fixReadmeField
- if (!data.readme) {
- changes?.push('No README data')
- data.readme = 'ERROR: No README data found!'
- }
- // fixLicenseField
- const license = data.license || data.licence
- if (!license) {
- changes?.push('No license field.')
- } else if (typeof (license) !== 'string' || license.length < 1 || license.trim() === '') {
- changes?.push('license should be a valid SPDX license expression')
- } else if (!validateLicense(license).validForNewPackages) {
- changes?.push('license should be a valid SPDX license expression')
- }
- // fixPeople
- if (data.author) {
- data.author = stringifyPerson(data.author)
- }
- ['maintainers', 'contributors'].forEach(function (set) {
- if (!Array.isArray(data[set])) {
- return
- }
- data[set] = data[set].map(stringifyPerson)
- })
- // fixTypos
- for (const d in typos) {
- if (Object.prototype.hasOwnProperty.call(data, d)) {
- changes?.push(`${d} should probably be ${typos[d]}.`)
- }
- }
- }
- module.exports = { normalizeData }
|