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")
10 var fixer = module.exports = {
11 // default warning function
14 fixRepositoryField: function(data) {
15 if (data.repositories) {
16 this.warn("repositories");
17 data.repository = data.repositories[0]
19 if (!data.repository) return this.warn("missingRepository")
20 if (typeof data.repository === "string") {
26 var r = data.repository.url || ""
28 var hosted = hostedGitInfo.fromUrl(r)
30 r = data.repository.url
31 = hosted.getDefaultRepresentation() == "shortcut" ? hosted.https() : hosted.toString()
35 if (r.match(/github.com\/[^\/]+\/[^\/]+\.git\.git$/)) {
36 this.warn("brokenGitUrl", r)
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])
48 , fixScriptsField: function(data) {
49 if (!data.scripts) return
50 if (typeof data.scripts !== "object") {
51 this.warn("nonObjectScripts")
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")
65 , fixFilesField: function(data) {
66 var files = data.files
67 if (files && !Array.isArray(files)) {
68 this.warn("nonArrayFiles")
70 } else if (data.files) {
71 data.files = data.files.filter(function(file) {
72 if (!file || typeof file !== "string") {
73 this.warn("invalidFilename", file)
82 , fixBinField: function(data) {
83 if (!data.bin) return;
84 if (typeof data.bin === "string") {
87 if (match = data.name.match(/^@[^/]+[/](.*)$/)) {
88 b[match[1]] = data.bin
90 b[data.name] = data.bin
96 , fixManField: function(data) {
97 if (!data.man) return;
98 if (typeof data.man === "string") {
99 data.man = [ data.man ]
102 , fixBundleDependenciesField: function(data) {
103 var bdd = "bundledDependencies"
104 var bd = "bundleDependencies"
105 if (data[bdd] && !data[bd]) {
109 if (data[bd] && !Array.isArray(data[bd])) {
110 this.warn("nonArrayBundleDependencies")
112 } else if (data[bd]) {
113 data[bd] = data[bd].filter(function(bd) {
114 if (!bd || typeof bd !== 'string') {
115 this.warn("nonStringBundleDependency", bd)
118 if (!data.dependencies) {
119 data.dependencies = {}
121 if (!data.dependencies.hasOwnProperty(bd)) {
122 this.warn("nonDependencyBundleDependency", bd)
123 data.dependencies[bd] = "*"
131 , fixDependencies: function(data, strict) {
133 objectifyDeps(data, this.warn)
134 addOptionalDepsToDeps(data, this.warn)
135 this.fixBundleDependenciesField(data)
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)
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))
150 var hosted = hostedGitInfo.fromUrl(data[deps][d])
151 if (hosted) data[deps][d] = hosted.toString()
156 , fixModulesField: function (data) {
158 this.warn("deprecatedModules")
163 , fixKeywordsField: function (data) {
164 if (typeof data.keywords === "string") {
165 data.keywords = data.keywords.split(/,\s+/)
167 if (data.keywords && !Array.isArray(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");
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
190 if (!semver.valid(data.version, loose)) {
191 throw new Error('Invalid version: "'+ data.version + '"')
193 data.version = semver.clean(data.version, loose)
197 , fixPeople: function(data) {
198 modifyPeople(data, unParsePerson)
199 modifyPeople(data, parsePerson)
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) {
210 if (typeof data.name !== "string") {
211 throw new Error("name field must be a string.")
214 data.name = data.name.trim()
215 ensureValidName(data.name, strict, options.allowLegacyCase)
216 if (isBuiltinModule(data.name))
217 this.warn("conflictingName", data.name)
221 , fixDescriptionField: function (data) {
222 if (data.description && typeof data.description !== 'string') {
223 this.warn("nonStringDescription")
224 delete data.description
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")
232 , fixReadmeField: function (data) {
234 this.warn("missingReadme")
235 data.readme = "ERROR: No README data found!"
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()}
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}
254 this.warn("nonEmailUrlBugsString")
257 bugsTypos(data.bugs, this.warn)
258 var oldBugs = data.bugs
261 if(typeof(oldBugs.url) == "string" && url.parse(oldBugs.url).protocol)
262 data.bugs.url = oldBugs.url
264 this.warn("nonUrlBugsUrlField")
267 if(typeof(oldBugs.email) == "string" && emailRe.test(oldBugs.email))
268 data.bugs.email = oldBugs.email
270 this.warn("nonEmailBugsEmailField")
273 if(!data.bugs.email && !data.bugs.url) {
275 this.warn("emptyNormalizedBugs")
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()
285 if (!data.homepage) return
287 if(typeof data.homepage !== "string") {
288 this.warn("nonUrlHomepage")
289 return delete data.homepage
291 if(!url.parse(data.homepage).protocol) {
292 this.warn("missingProtocolHomepage")
293 data.homepage = "http://" + data.homepage
297 , fixLicenseField: function(data) {
299 return this.warn("missingLicense")
302 typeof(data.license) !== 'string' ||
303 data.license.length < 1
305 this.warn("invalidLicense")
307 if (!validateLicense(data.license).validForNewPackages)
308 this.warn("invalidLicense")
314 function isValidScopedPackageName(spec) {
315 if (spec.charAt(0) !== '@') return false
317 var rest = spec.slice(1).split('/')
318 if (rest.length !== 2) return false
320 return rest[0] && rest[1] &&
321 rest[0] === encodeURIComponent(rest[0]) &&
322 rest[1] === encodeURIComponent(rest[1])
325 function isCorrectlyEncodedName(spec) {
326 return !spec.match(/[\/@\s\+%:]/) &&
327 spec === encodeURIComponent(spec)
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))
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)
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
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(/<([^>]+)>/)
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];
371 function addOptionalDepsToDeps (data, warn) {
372 var o = data.optionalDependencies
374 var d = data.dependencies || {}
375 Object.keys(o).forEach(function (k) {
378 data.dependencies = d
381 function depObjectify (deps, type, warn) {
383 if (typeof deps === "string") {
384 deps = deps.trim().split(/[\n\r\s\t ,]+/)
386 if (!Array.isArray(deps)) return deps
387 warn("deprecatedArrayDependencies", type)
389 deps.filter(function (d) {
390 return typeof d === "string"
391 }).forEach(function(d) {
392 d = d.trim().split(/(:?[@\s><=])/)
396 dv = dv.replace(/^@/, "")
402 function objectifyDeps (data, warn) {
403 depTypes.forEach(function (type) {
404 if (!data[type]) return;
405 data[type] = depObjectify(data[type], type, warn)
409 function bugsTypos(bugs, warn) {
411 Object.keys(bugs).forEach(function (k) {
413 warn("typo", k, typos.bugs[k], "bugs")
414 bugs[typos.bugs[k]] = bugs[k]