Initial commit
[yaffs-website] / node_modules / normalize-package-data / lib / fixer.js
1 var semver = require("semver")
2 var validateLicense = require('validate-npm-package-license');
3 var hostedGitInfo = require("hosted-git-info")
4 var isBuiltinModule = require("is-builtin-module")
5 var depTypes = ["dependencies","devDependencies","optionalDependencies"]
6 var extractDescription = require("./extract_description")
7 var url = require("url")
8 var typos = require("./typos")
9
10 var fixer = module.exports = {
11   // default warning function
12   warn: function() {},
13
14   fixRepositoryField: function(data) {
15     if (data.repositories) {
16       this.warn("repositories");
17       data.repository = data.repositories[0]
18     }
19     if (!data.repository) return this.warn("missingRepository")
20     if (typeof data.repository === "string") {
21       data.repository = {
22         type: "git",
23         url: data.repository
24       }
25     }
26     var r = data.repository.url || ""
27     if (r) {
28       var hosted = hostedGitInfo.fromUrl(r)
29       if (hosted) {
30         r = data.repository.url
31           = hosted.getDefaultRepresentation() == "shortcut" ? hosted.https() : hosted.toString()
32       }
33     }
34
35     if (r.match(/github.com\/[^\/]+\/[^\/]+\.git\.git$/)) {
36       this.warn("brokenGitUrl", r)
37     }
38   }
39
40 , fixTypos: function(data) {
41     Object.keys(typos.topLevel).forEach(function (d) {
42       if (data.hasOwnProperty(d)) {
43         this.warn("typo", d, typos.topLevel[d])
44       }
45     }, this)
46   }
47
48 , fixScriptsField: function(data) {
49     if (!data.scripts) return
50     if (typeof data.scripts !== "object") {
51       this.warn("nonObjectScripts")
52       delete data.scripts
53       return
54     }
55     Object.keys(data.scripts).forEach(function (k) {
56       if (typeof data.scripts[k] !== "string") {
57         this.warn("nonStringScript")
58         delete data.scripts[k]
59       } else if (typos.script[k] && !data.scripts[typos.script[k]]) {
60         this.warn("typo", k, typos.script[k], "scripts")
61       }
62     }, this)
63   }
64
65 , fixFilesField: function(data) {
66     var files = data.files
67     if (files && !Array.isArray(files)) {
68       this.warn("nonArrayFiles")
69       delete data.files
70     } else if (data.files) {
71       data.files = data.files.filter(function(file) {
72         if (!file || typeof file !== "string") {
73           this.warn("invalidFilename", file)
74           return false
75         } else {
76           return true
77         }
78       }, this)
79     }
80   }
81
82 , fixBinField: function(data) {
83     if (!data.bin) return;
84     if (typeof data.bin === "string") {
85       var b = {}
86       var match
87       if (match = data.name.match(/^@[^/]+[/](.*)$/)) {
88         b[match[1]] = data.bin
89       } else {
90         b[data.name] = data.bin
91       }
92       data.bin = b
93     }
94   }
95
96 , fixManField: function(data) {
97     if (!data.man) return;
98     if (typeof data.man === "string") {
99       data.man = [ data.man ]
100     }
101   }
102 , fixBundleDependenciesField: function(data) {
103     var bdd = "bundledDependencies"
104     var bd = "bundleDependencies"
105     if (data[bdd] && !data[bd]) {
106       data[bd] = data[bdd]
107       delete data[bdd]
108     }
109     if (data[bd] && !Array.isArray(data[bd])) {
110       this.warn("nonArrayBundleDependencies")
111       delete data[bd]
112     } else if (data[bd]) {
113       data[bd] = data[bd].filter(function(bd) {
114         if (!bd || typeof bd !== 'string') {
115           this.warn("nonStringBundleDependency", bd)
116           return false
117         } else {
118           if (!data.dependencies) {
119             data.dependencies = {}
120           }
121           if (!data.dependencies.hasOwnProperty(bd)) {
122             this.warn("nonDependencyBundleDependency", bd)
123             data.dependencies[bd] = "*"
124           }
125           return true
126         }
127       }, this)
128     }
129   }
130
131 , fixDependencies: function(data, strict) {
132     var loose = !strict
133     objectifyDeps(data, this.warn)
134     addOptionalDepsToDeps(data, this.warn)
135     this.fixBundleDependenciesField(data)
136
137     ;['dependencies','devDependencies'].forEach(function(deps) {
138       if (!(deps in data)) return
139       if (!data[deps] || typeof data[deps] !== "object") {
140         this.warn("nonObjectDependencies", deps)
141         delete data[deps]
142         return
143       }
144       Object.keys(data[deps]).forEach(function (d) {
145         var r = data[deps][d]
146         if (typeof r !== 'string') {
147           this.warn("nonStringDependency", d, JSON.stringify(r))
148           delete data[deps][d]
149         }
150         var hosted = hostedGitInfo.fromUrl(data[deps][d])
151         if (hosted) data[deps][d] = hosted.toString()
152       }, this)
153     }, this)
154   }
155
156 , fixModulesField: function (data) {
157     if (data.modules) {
158       this.warn("deprecatedModules")
159       delete data.modules
160     }
161   }
162
163 , fixKeywordsField: function (data) {
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       this.warn("nonArrayKeywords")
170     } else if (data.keywords) {
171       data.keywords = data.keywords.filter(function(kw) {
172         if (typeof kw !== "string" || !kw) {
173           this.warn("nonStringKeyword");
174           return false
175         } else {
176           return true
177         }
178       }, this)
179     }
180   }
181
182 , fixVersionField: function(data, strict) {
183     // allow "loose" semver 1.0 versions in non-strict mode
184     // enforce strict semver 2.0 compliance in strict mode
185     var loose = !strict
186     if (!data.version) {
187       data.version = ""
188       return true
189     }
190     if (!semver.valid(data.version, loose)) {
191       throw new Error('Invalid version: "'+ data.version + '"')
192     }
193     data.version = semver.clean(data.version, loose)
194     return true
195   }
196
197 , fixPeople: function(data) {
198     modifyPeople(data, unParsePerson)
199     modifyPeople(data, parsePerson)
200   }
201
202 , fixNameField: function(data, options) {
203     if (typeof options === "boolean") options = {strict: options}
204     else if (typeof options === "undefined") options = {}
205     var strict = options.strict
206     if (!data.name && !strict) {
207       data.name = ""
208       return
209     }
210     if (typeof data.name !== "string") {
211       throw new Error("name field must be a string.")
212     }
213     if (!strict)
214       data.name = data.name.trim()
215     ensureValidName(data.name, strict, options.allowLegacyCase)
216     if (isBuiltinModule(data.name))
217       this.warn("conflictingName", data.name)
218   }
219
220
221 , fixDescriptionField: function (data) {
222     if (data.description && typeof data.description !== 'string') {
223       this.warn("nonStringDescription")
224       delete data.description
225     }
226     if (data.readme && !data.description)
227       data.description = extractDescription(data.readme)
228       if(data.description === undefined) delete data.description;
229     if (!data.description) this.warn("missingDescription")
230   }
231
232 , fixReadmeField: function (data) {
233     if (!data.readme) {
234       this.warn("missingReadme")
235       data.readme = "ERROR: No README data found!"
236     }
237   }
238
239 , fixBugsField: function(data) {
240     if (!data.bugs && data.repository && data.repository.url) {
241       var hosted = hostedGitInfo.fromUrl(data.repository.url)
242       if(hosted && hosted.bugs()) {
243         data.bugs = {url: hosted.bugs()}
244       }
245     }
246     else if(data.bugs) {
247       var emailRe = /^.+@.*\..+$/
248       if(typeof data.bugs == "string") {
249         if(emailRe.test(data.bugs))
250           data.bugs = {email:data.bugs}
251         else if(url.parse(data.bugs).protocol)
252           data.bugs = {url: data.bugs}
253         else
254           this.warn("nonEmailUrlBugsString")
255       }
256       else {
257         bugsTypos(data.bugs, this.warn)
258         var oldBugs = data.bugs
259         data.bugs = {}
260         if(oldBugs.url) {
261           if(typeof(oldBugs.url) == "string" && url.parse(oldBugs.url).protocol)
262             data.bugs.url = oldBugs.url
263           else
264             this.warn("nonUrlBugsUrlField")
265         }
266         if(oldBugs.email) {
267           if(typeof(oldBugs.email) == "string" && emailRe.test(oldBugs.email))
268             data.bugs.email = oldBugs.email
269           else
270             this.warn("nonEmailBugsEmailField")
271         }
272       }
273       if(!data.bugs.email && !data.bugs.url) {
274         delete data.bugs
275         this.warn("emptyNormalizedBugs")
276       }
277     }
278   }
279
280 , fixHomepageField: function(data) {
281     if (!data.homepage && data.repository && data.repository.url) {
282       var hosted = hostedGitInfo.fromUrl(data.repository.url)
283       if (hosted && hosted.docs()) data.homepage = hosted.docs()
284     }
285     if (!data.homepage) return
286
287     if(typeof data.homepage !== "string") {
288       this.warn("nonUrlHomepage")
289       return delete data.homepage
290     }
291     if(!url.parse(data.homepage).protocol) {
292       this.warn("missingProtocolHomepage")
293       data.homepage = "http://" + data.homepage
294     }
295   }
296
297 , fixLicenseField: function(data) {
298     if (!data.license) {
299       return this.warn("missingLicense")
300     } else{
301       if (
302         typeof(data.license) !== 'string' ||
303         data.license.length < 1
304       ) {
305         this.warn("invalidLicense")
306       } else {
307         if (!validateLicense(data.license).validForNewPackages)
308           this.warn("invalidLicense")
309       }
310     }
311   }
312 }
313
314 function isValidScopedPackageName(spec) {
315   if (spec.charAt(0) !== '@') return false
316
317   var rest = spec.slice(1).split('/')
318   if (rest.length !== 2) return false
319
320   return rest[0] && rest[1] &&
321     rest[0] === encodeURIComponent(rest[0]) &&
322     rest[1] === encodeURIComponent(rest[1])
323 }
324
325 function isCorrectlyEncodedName(spec) {
326   return !spec.match(/[\/@\s\+%:]/) &&
327     spec === encodeURIComponent(spec)
328 }
329
330 function ensureValidName (name, strict, allowLegacyCase) {
331   if (name.charAt(0) === "." ||
332       !(isValidScopedPackageName(name) || isCorrectlyEncodedName(name)) ||
333       (strict && (!allowLegacyCase) && name !== name.toLowerCase()) ||
334       name.toLowerCase() === "node_modules" ||
335       name.toLowerCase() === "favicon.ico") {
336         throw new Error("Invalid name: " + JSON.stringify(name))
337   }
338 }
339
340 function modifyPeople (data, fn) {
341   if (data.author) data.author = fn(data.author)
342   ;["maintainers", "contributors"].forEach(function (set) {
343     if (!Array.isArray(data[set])) return;
344     data[set] = data[set].map(fn)
345   })
346   return data
347 }
348
349 function unParsePerson (person) {
350   if (typeof person === "string") return person
351   var name = person.name || ""
352   var u = person.url || person.web
353   var url = u ? (" ("+u+")") : ""
354   var e = person.email || person.mail
355   var email = e ? (" <"+e+">") : ""
356   return name+email+url
357 }
358
359 function parsePerson (person) {
360   if (typeof person !== "string") return person
361   var name = person.match(/^([^\(<]+)/)
362   var url = person.match(/\(([^\)]+)\)/)
363   var email = person.match(/<([^>]+)>/)
364   var obj = {}
365   if (name && name[0].trim()) obj.name = name[0].trim()
366   if (email) obj.email = email[1];
367   if (url) obj.url = url[1];
368   return obj
369 }
370
371 function addOptionalDepsToDeps (data, warn) {
372   var o = data.optionalDependencies
373   if (!o) return;
374   var d = data.dependencies || {}
375   Object.keys(o).forEach(function (k) {
376     d[k] = o[k]
377   })
378   data.dependencies = d
379 }
380
381 function depObjectify (deps, type, warn) {
382   if (!deps) return {}
383   if (typeof deps === "string") {
384     deps = deps.trim().split(/[\n\r\s\t ,]+/)
385   }
386   if (!Array.isArray(deps)) return deps
387   warn("deprecatedArrayDependencies", type)
388   var o = {}
389   deps.filter(function (d) {
390     return typeof d === "string"
391   }).forEach(function(d) {
392     d = d.trim().split(/(:?[@\s><=])/)
393     var dn = d.shift()
394     var dv = d.join("")
395     dv = dv.trim()
396     dv = dv.replace(/^@/, "")
397     o[dn] = dv
398   })
399   return o
400 }
401
402 function objectifyDeps (data, warn) {
403   depTypes.forEach(function (type) {
404     if (!data[type]) return;
405     data[type] = depObjectify(data[type], type, warn)
406   })
407 }
408
409 function bugsTypos(bugs, warn) {
410   if (!bugs) return
411   Object.keys(bugs).forEach(function (k) {
412     if (typos.bugs[k]) {
413       warn("typo", k, typos.bugs[k], "bugs")
414       bugs[typos.bugs[k]] = bugs[k]
415       delete bugs[k]
416     }
417   })
418 }