codekingpro/portable-devtools
115k
1// Originally normalize-package-data
2
3const { URL } = require('node:url')
4const hostedGitInfo = require('hosted-git-info')
5const validateLicense = require('./license.js')
6
7const 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
29const isEmail = str => str.includes('@') && (str.indexOf('@') < str.lastIndexOf('.'))
30
31// Extracts description from contents of a readme file in markdown format
32function 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
48function 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
73function 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 } else if (URL.canParse(data.bugs)) {
127 data.bugs = { url: data.bugs }
128 } else {
129 changes?.push(`Bug string field must be url, email, or {email,url}`)
130 }
131 } else {
132 for (const k in data.bugs) {
133 if (['web', 'name'].includes(k)) {
134 changes?.push(`bugs['${k}'] should probably be bugs['url'].`)
135 data.bugs.url = data.bugs[k]
136 delete data.bugs[k]
137 }
138 }
139 const oldBugs = data.bugs
140 data.bugs = {}
141 if (oldBugs.url) {
142 if (URL.canParse(oldBugs.url)) {
143 data.bugs.url = oldBugs.url
144 } else {
145 changes?.push('bugs.url field must be a string url. Deleted.')
146 }
147 }
148 if (oldBugs.email) {
149 if (typeof (oldBugs.email) === 'string' && isEmail(oldBugs.email)) {
150 data.bugs.email = oldBugs.email
151 } else {
152 changes?.push('bugs.email field must be a string email. Deleted.')
153 }
154 }
155 }
156 if (!data.bugs.email && !data.bugs.url) {
157 delete data.bugs
158 changes?.push('Normalized value of bugs field is an empty object. Deleted.')
159 }
160 }
161 // fixKeywordsField
162 if (typeof data.keywords === 'string') {
163 data.keywords = data.keywords.split(/,\s+/)
164 }
165 if (data.keywords && !Array.isArray(data.keywords)) {
166 delete data.keywords
167 changes?.push(`keywords should be an array of strings`)
168 } else if (data.keywords) {
169 data.keywords = data.keywords.filter(function (kw) {
170 if (typeof kw !== 'string' || !kw) {
171 changes?.push(`keywords should be an array of strings`)
172 return false
173 } else {
174 return true
175 }
176 })
177 }
178 // fixBundleDependenciesField
179 const bdd = 'bundledDependencies'
180 const bd = 'bundleDependencies'
181 if (data[bdd] && !data[bd]) {
182 data[bd] = data[bdd]
183 delete data[bdd]
184 }
185 if (data[bd] && !Array.isArray(data[bd])) {
186 changes?.push(`Invalid 'bundleDependencies' list. Must be array of package names`)
187 delete data[bd]
188 } else if (data[bd]) {
189 data[bd] = data[bd].filter(function (filtered) {
190 if (!filtered || typeof filtered !== 'string') {
191 changes?.push(`Invalid bundleDependencies member: ${filtered}`)
192 return false
193 } else {
194 if (!data.dependencies) {
195 data.dependencies = {}
196 }
197 if (!Object.prototype.hasOwnProperty.call(data.dependencies, filtered)) {
198 changes?.push(`Non-dependency in bundleDependencies: ${filtered}`)
199 data.dependencies[filtered] = '*'
200 }
201 return true
202 }
203 })
204 }
205 // fixHomepageField
206 if (!data.homepage && data.repository && data.repository.url) {
207 const hosted = hostedGitInfo.fromUrl(data.repository.url)
208 if (hosted) {
209 data.homepage = hosted.docs()
210 }
211 }
212 if (data.homepage) {
213 if (typeof data.homepage !== 'string') {
214 changes?.push('homepage field must be a string url. Deleted.')
215 delete data.homepage
216 } else {
217 if (!URL.canParse(data.homepage)) {
218 data.homepage = 'http://' + data.homepage
219 }
220 }
221 }
222 // fixReadmeField
223 if (!data.readme) {
224 changes?.push('No README data')
225 data.readme = 'ERROR: No README data found!'
226 }
227 // fixLicenseField
228 const license = data.license || data.licence
229 if (!license) {
230 changes?.push('No license field.')
231 } else if (typeof (license) !== 'string' || license.length < 1 || license.trim() === '') {
232 changes?.push('license should be a valid SPDX license expression')
233 } else if (!validateLicense(license)) {
234 changes?.push('license should be a valid SPDX license expression')
235 }
236 // fixPeople
237 if (data.author) {
238 data.author = stringifyPerson(data.author)
239 }
240 ['maintainers', 'contributors'].forEach(function (set) {
241 if (!Array.isArray(data[set])) {
242 return
243 }
244 data[set] = data[set].map(stringifyPerson)
245 })
246 // fixTypos
247 for (const d in typos) {
248 if (Object.prototype.hasOwnProperty.call(data, d)) {
249 changes?.push(`${d} should probably be ${typos[d]}.`)
250 }
251 }
252}
253
254module.exports = { normalizeData }
255 