1
+
// Originally normalize-package-data
2
+
3
+
const url = require('node:url')
4
+
const hostedGitInfo = require('hosted-git-info')
5
+
const validateLicense = require('validate-npm-package-license')
6
+
7
+
const typos = {
8
+
dependancies: 'dependencies',
9
+
dependecies: 'dependencies',
10
+
depdenencies: 'dependencies',
11
+
devEependencies: 'devDependencies',
12
+
depends: 'dependencies',
13
+
'dev-dependencies': 'devDependencies',
14
+
devDependences: 'devDependencies',
15
+
devDepenencies: 'devDependencies',
16
+
devdependencies: 'devDependencies',
17
+
repostitory: 'repository',
18
+
repo: 'repository',
19
+
prefereGlobal: 'preferGlobal',
20
+
hompage: 'homepage',
21
+
hampage: 'homepage',
22
+
autohr: 'author',
23
+
autor: 'author',
24
+
contributers: 'contributors',
25
+
publicationConfig: 'publishConfig',
26
+
script: 'scripts',
27
+
}
28
+
29
+
const isEmail = str => str.includes('@') && (str.indexOf('@') < str.lastIndexOf('.'))
30
+
31
+
// Extracts description from contents of a readme file in markdown format
32
+
function extractDescription (description) {
33
+
// the first block of text before the first heading that isn't the first line heading
34
+
const lines = description.trim().split('\n')
35
+
let start = 0
36
+
// skip initial empty lines and lines that start with #
37
+
while (lines[start]?.trim().match(/^(#|$)/)) {
38
+
start++
39
+
}
40
+
let end = start + 1
41
+
// keep going till we get to the end or an empty line
42
+
while (end < lines.length && lines[end].trim()) {
43
+
end++
44
+
}
45
+
return lines.slice(start, end).join(' ').trim()
46
+
}
47
+
48
+
function stringifyPerson (person) {
49
+
if (typeof person !== 'string') {
50
+
const name = person.name || ''
51
+
const u = person.url || person.web
52
+
const wrappedUrl = u ? (' (' + u + ')') : ''
53
+
const e = person.email || person.mail
54
+
const wrappedEmail = e ? (' <' + e + '>') : ''
55
+
person = name + wrappedEmail + wrappedUrl
56
+
}
57
+
const matchedName = person.match(/^([^(<]+)/)
58
+
const matchedUrl = person.match(/\(([^()]+)\)/)
59
+
const matchedEmail = person.match(/<([^<>]+)>/)
60
+
const parsed = {}
61
+
if (matchedName?.[0].trim()) {
62
+
parsed.name = matchedName[0].trim()
63
+
}
64
+
if (matchedEmail) {
65
+
parsed.email = matchedEmail[1]
66
+
}
67
+
if (matchedUrl) {
68
+
parsed.url = matchedUrl[1]
69
+
}
70
+
return parsed
71
+
}
72
+
73
+
function normalizeData (data, changes) {
74
+
// fixDescriptionField
75
+
if (data.description && typeof data.description !== 'string') {
76
+
changes?.push(`'description' field should be a string`)
77
+
delete data.description
78
+
}
79
+
if (data.readme && !data.description && data.readme !== 'ERROR: No README data found!') {
80
+
data.description = extractDescription(data.readme)
81
+
}
82
+
if (data.description === undefined) {
83
+
delete data.description
84
+
}
85
+
if (!data.description) {
86
+
changes?.push('No description')
87
+
}
88
+
89
+
// fixModulesField
90
+
if (data.modules) {
91
+
changes?.push(`modules field is deprecated`)
92
+
delete data.modules
93
+
}
94
+
95
+
// fixFilesField
96
+
const files = data.files
97
+
if (files && !Array.isArray(files)) {
98
+
changes?.push(`Invalid 'files' member`)
99
+
delete data.files
100
+
} else if (data.files) {
101
+
data.files = data.files.filter(function (file) {
102
+
if (!file || typeof file !== 'string') {
103
+
changes?.push(`Invalid filename in 'files' list: ${file}`)
104
+
return false
105
+
} else {
106
+
return true
107
+
}
108
+
})
109
+
}
110
+
111
+
// fixManField
112
+
if (data.man && typeof data.man === 'string') {
113
+
data.man = [data.man]
114
+
}
115
+
116
+
// fixBugsField
117
+
if (!data.bugs && data.repository?.url) {
118
+
const hosted = hostedGitInfo.fromUrl(data.repository.url)
119
+
if (hosted && hosted.bugs()) {
120
+
data.bugs = { url: hosted.bugs() }
121
+
}
122
+
} else if (data.bugs) {
123
+
if (typeof data.bugs === 'string') {
124
+
if (isEmail(data.bugs)) {
125
+
data.bugs = { email: data.bugs }
126
+
/* eslint-disable-next-line node/no-deprecated-api */
127
+
} else if (url.parse(data.bugs).protocol) {
128
+
data.bugs = { url: data.bugs }
129
+
} else {
130
+
changes?.push(`Bug string field must be url, email, or {email,url}`)
131
+
}
132
+
} else {
133
+
for (const k in data.bugs) {
134
+
if (['web', 'name'].includes(k)) {
135
+
changes?.push(`bugs['${k}'] should probably be bugs['url'].`)
136
+
data.bugs.url = data.bugs[k]
137
+
delete data.bugs[k]
138
+
}
139
+
}
140
+
const oldBugs = data.bugs
141
+
data.bugs = {}
142
+
if (oldBugs.url) {
143
+
/* eslint-disable-next-line node/no-deprecated-api */
144
+
if (typeof (oldBugs.url) === 'string' && url.parse(oldBugs.url).protocol) {
145
+
data.bugs.url = oldBugs.url
146
+
} else {
147
+
changes?.push('bugs.url field must be a string url. Deleted.')
148
+
}
149
+
}
150
+
if (oldBugs.email) {
151
+
if (typeof (oldBugs.email) === 'string' && isEmail(oldBugs.email)) {
152
+
data.bugs.email = oldBugs.email
153
+
} else {
154
+
changes?.push('bugs.email field must be a string email. Deleted.')
155
+
}
156
+
}
157
+
}
158
+
if (!data.bugs.email && !data.bugs.url) {
159
+
delete data.bugs
160
+
changes?.push('Normalized value of bugs field is an empty object. Deleted.')
161
+
}
162
+
}
163
+
// fixKeywordsField
164
+
if (typeof data.keywords === 'string') {
165
+
data.keywords = data.keywords.split(/,\s+/)
166
+
}
167
+
if (data.keywords && !Array.isArray(data.keywords)) {
168
+
delete data.keywords
169
+
changes?.push(`keywords should be an array of strings`)
170
+
} else if (data.keywords) {
171
+
data.keywords = data.keywords.filter(function (kw) {
172
+
if (typeof kw !== 'string' || !kw) {
173
+
changes?.push(`keywords should be an array of strings`)
174
+
return false
175
+
} else {
176
+
return true
177
+
}
178
+
})
179
+
}
180
+
// fixBundleDependenciesField
181
+
const bdd = 'bundledDependencies'
182
+
const bd = 'bundleDependencies'
183
+
if (data[bdd] && !data[bd]) {
184
+
data[bd] = data[bdd]
185
+
delete data[bdd]
186
+
}
187
+
if (data[bd] && !Array.isArray(data[bd])) {
188
+
changes?.push(`Invalid 'bundleDependencies' list. Must be array of package names`)
189
+
delete data[bd]
190
+
} else if (data[bd]) {
191
+
data[bd] = data[bd].filter(function (filtered) {
192
+
if (!filtered || typeof filtered !== 'string') {
193
+
changes?.push(`Invalid bundleDependencies member: ${filtered}`)
194
+
return false
195
+
} else {
196
+
if (!data.dependencies) {
197
+
data.dependencies = {}
198
+
}
199
+
if (!Object.prototype.hasOwnProperty.call(data.dependencies, filtered)) {
200
+
changes?.push(`Non-dependency in bundleDependencies: ${filtered}`)
201
+
data.dependencies[filtered] = '*'
202
+
}
203
+
return true
204
+
}
205
+
})
206
+
}
207
+
// fixHomepageField
208
+
if (!data.homepage && data.repository && data.repository.url) {
209
+
const hosted = hostedGitInfo.fromUrl(data.repository.url)
210
+
if (hosted) {
211
+
data.homepage = hosted.docs()
212
+
}
213
+
}
214
+
if (data.homepage) {
215
+
if (typeof data.homepage !== 'string') {
216
+
changes?.push('homepage field must be a string url. Deleted.')
217
+
delete data.homepage
218
+
} else {
219
+
/* eslint-disable-next-line node/no-deprecated-api */
220
+
if (!url.parse(data.homepage).protocol) {
221
+
data.homepage = 'http://' + data.homepage
222
+
}
223
+
}
224
+
}
225
+
// fixReadmeField
226
+
if (!data.readme) {
227
+
changes?.push('No README data')
228
+
data.readme = 'ERROR: No README data found!'
229
+
}
230
+
// fixLicenseField
231
+
const license = data.license || data.licence
232
+
if (!license) {
233
+
changes?.push('No license field.')
234
+
} else if (typeof (license) !== 'string' || license.length < 1 || license.trim() === '') {
235
+
changes?.push('license should be a valid SPDX license expression')
236
+
} else if (!validateLicense(license).validForNewPackages) {
237
+
changes?.push('license should be a valid SPDX license expression')
238
+
}
239
+
// fixPeople
240
+
if (data.author) {
241
+
data.author = stringifyPerson(data.author)
242
+
}
243
+
['maintainers', 'contributors'].forEach(function (set) {
244
+
if (!Array.isArray(data[set])) {
245
+
return
246
+
}
247
+
data[set] = data[set].map(stringifyPerson)
248
+
})
249
+
// fixTypos
250
+
for (const d in typos) {
251
+
if (Object.prototype.hasOwnProperty.call(data, d)) {
252
+
changes?.push(`${d} should probably be ${typos[d]}.`)
253
+
}
254
+
}
255
+
}
256
+
257
+
module.exports = { normalizeData }
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4