// vim: set softtabstop=16 shiftwidth=16: try { readJson.log = require("npmlog") } catch (er) { readJson.log = { info: function () {}, verbose: function () {}, warn: function () {} } } try { var fs = require("graceful-fs") } catch (er) { var fs = require("fs") } module.exports = readJson var LRU = require("lru-cache") readJson.cache = new LRU({max: 1000}) var path = require("path") var glob = require("glob") var slide = require("slide") var asyncMap = slide.asyncMap var semver = require("semver") // put more stuff on here to customize. readJson.extraSet = [ gypfile, wscript, serverjs, authors, readme, mans, bins, githead ] var typoWarned = {} // http://registry.npmjs.org/-/fields var typos = { "dependancies": "dependencies" , "dependecies": "dependencies" , "depdenencies": "dependencies" , "devEependencies": "devDependencies" , "depends": "dependencies" , "dev-dependencies": "devDependencies" , "devDependences": "devDependencies" , "devDepenencies": "devDependencies" , "devdependencies": "devDependencies" , "repostitory": "repository" , "prefereGlobal": "preferGlobal" , "hompage": "homepage" , "hampage": "homepage" , "autohr": "author" , "autor": "author" , "contributers": "contributors" , "publicationConfig": "publishConfig" } var bugsTypos = { "web": "url", "name": "url" } var scriptTypos = { "server": "start", "tests": "test" } var depTypes = [ "dependencies" , "devDependencies" , "optionalDependencies" ] function readJson (file, cb) { var c = readJson.cache.get(file) if (c) { readJson.log.verbose("from cache", file) cb = cb.bind(null, null, c) return process.nextTick(cb); } readJson.log.verbose("read json", file) cb = (function (orig) { return function (er, data) { if (data) readJson.cache.set(file, data); return orig(er, data) } })(cb) readJson_(file, cb) } function readJson_ (file, cb) { fs.readFile(file, "utf8", function (er, d) { parseJson(file, er, d, cb) }) } function stripBOM(content) { // Remove byte order marker. This catches EF BB BF (the UTF-8 BOM) // because the buffer-to-string conversion in `fs.readFileSync()` // translates it to FEFF, the UTF-16 BOM. if (content.charCodeAt(0) === 0xFEFF) { content = content.slice(1); } return content; } function parseJson (file, er, d, cb) { if (er && er.code === "ENOENT") { indexjs(file, er, cb) return } if (er) return cb(er); try { d = JSON.parse(stripBOM(d)) } catch (er) { d = parseIndex(d) if (!d) return cb(parseError(er, file)); } extras(file, d, cb) } function indexjs (file, er, cb) { if (path.basename(file) === "index.js") { return cb(er); } var index = path.resolve(path.dirname(file), "index.js") fs.readFile(index, "utf8", function (er2, d) { if (er2) return cb(er); d = parseIndex(d) if (!d) return cb(er); extras(file, d, cb) }) } readJson.extras = extras function extras (file, data, cb) { asyncMap(readJson.extraSet, function (fn, cb) { return fn(file, data, cb) }, function (er) { if (er) return cb(er); final(file, data, cb) }) } function gypfile (file, data, cb) { var dir = path.dirname(file) var s = data.scripts || {} if (s.install || s.preinstall) { return cb(null, data); } glob("*.gyp", { cwd: dir }, function (er, files) { if (er) return cb(er); gypfile_(file, data, files, cb) }) } function gypfile_ (file, data, files, cb) { if (!files.length) return cb(null, data); var s = data.scripts || {} s.install = "node-gyp rebuild" data.scripts = s data.gypfile = true return cb(null, data); } function wscript (file, data, cb) { var dir = path.dirname(file) var s = data.scripts || {} if (s.install || s.preinstall) { return cb(null, data); } glob("wscript", { cwd: dir }, function (er, files) { if (er) return cb(er); wscript_(file, data, files, cb) }) } function wscript_ (file, data, files, cb) { if (!files.length || data.gypfile) return cb(null, data); var s = data.scripts || {} s.install = "node-waf clean ; node-waf configure build" data.scripts = s return cb(null, data); } function serverjs (file, data, cb) { var dir = path.dirname(file) var s = data.scripts || {} if (s.start) return cb(null, data) glob("server.js", { cwd: dir }, function (er, files) { if (er) return cb(er); serverjs_(file, data, files, cb) }) } function serverjs_ (file, data, files, cb) { if (!files.length) return cb(null, data); var s = data.scripts || {} s.start = "node server.js" data.scripts = s return cb(null, data) } function authors (file, data, cb) { if (data.contributors) return cb(null, data); var af = path.resolve(path.dirname(file), "AUTHORS") fs.readFile(af, "utf8", function (er, ad) { // ignore error. just checking it. if (er) return cb(null, data); authors_(file, data, ad, cb) }) } function authors_ (file, data, ad, cb) { ad = ad.split(/\r?\n/g).map(function (line) { return line.replace(/^\s*#.*$/, '').trim() }).filter(function (line) { return line }) data.contributors = ad return cb(null, data) } var defDesc = "Unnamed repository; edit this file " + "'description' to name the repository." function gitDescription (file, data, cb) { if (data.description) return cb(null, data); var dir = path.dirname(file) // just cuz it'd be nice if this file mattered... var gitDesc = path.resolve(dir, '.git/description') fs.readFile(gitDesc, 'utf8', function (er, desc) { if (desc) desc = desc.trim() if (!er && desc !== defDesc) data.description = desc return cb(null, data) }) } function readmeDescription (file, data) { if (data.description) return cb(null, data); var d = data.readme if (!d) return; // the first block of text before the first heading // that isn't the first line heading d = d.trim().split('\n') for (var s = 0; d[s] && d[s].trim().match(/^(#|$)/); s ++); var l = d.length for (var e = s + 1; e < l && d[e].trim(); e ++); data.description = d.slice(s, e).join(' ').trim() } function readme (file, data, cb) { if (data.readme) return cb(null, data); var dir = path.dirname(file) var globOpts = { cwd: dir, nocase: true, mark: true } glob("README?(.*)", globOpts, function (er, files) { if (er) return cb(er); // don't accept directories. files = files.filter(function (file) { return !file.match(/\/$/) }) if (!files.length) return cb(); var rm = path.resolve(dir, files[0]) readme_(file, data, rm, cb) }) } function readme_(file, data, rm, cb) { var rmfn = path.basename(rm); fs.readFile(rm, "utf8", function (er, rm) { // maybe not readable, or something. if (er) return cb() data.readme = rm data.readmeFilename = rmfn return cb(er, data) }) } function mans (file, data, cb) { var m = data.directories && data.directories.man if (data.man || !m) return cb(null, data); m = path.resolve(path.dirname(file), m) glob("**/*.[0-9]", { cwd: m }, function (er, mans) { if (er) return cb(er); mans_(file, data, mans, cb) }) } function mans_ (file, data, mans, cb) { var m = data.directories && data.directories.man data.man = mans.map(function (mf) { return path.resolve(path.dirname(file), m, mf) }) return cb(null, data) } function bins (file, data, cb) { if (Array.isArray(data.bin)) { return bins_(file, data, data.bin, cb) } var m = data.directories && data.directories.bin if (data.bin || !m) return cb(null, data); m = path.resolve(path.dirname(file), m) glob("**", { cwd: m }, function (er, bins) { if (er) return cb(er); bins_(file, data, bins, cb) }) } function bins_ (file, data, bins, cb) { var m = data.directories && data.directories.bin || '.' data.bin = bins.reduce(function (acc, mf) { if (mf && mf.charAt(0) !== '.') { var f = path.basename(mf) acc[f] = path.join(m, mf) } return acc }, {}) return cb(null, data) } function githead (file, data, cb) { if (data.gitHead) return cb(null, data); var dir = path.dirname(file) var head = path.resolve(dir, '.git/HEAD') fs.readFile(head, 'utf8', function (er, head) { if (er) return cb(null, data); githead_(file, data, dir, head, cb) }) } function githead_ (file, data, dir, head, cb) { if (!head.match(/^ref: /)) { data.gitHead = head.trim() return cb(null, data) } var headFile = head.replace(/^ref: /, '').trim() headFile = path.resolve(dir, '.git', headFile) fs.readFile(headFile, 'utf8', function (er, head) { if (er || !head) return cb(null, data) head = head.replace(/^ref: /, '').trim() data.gitHead = head return cb(null, data) }) } function final (file, data, cb) { var ret = validName(file, data) if (ret !== true) return cb(ret); ret = validVersion(file, data) if (ret !== true) return cb(ret); data._id = data.name + "@" + data.version typoWarn(file, data) validRepo(file, data) validFiles(file, data) validBin(file, data) validMan(file, data) validBundled(file, data) objectifyDeps(file, data) unParsePeople(file, data) parsePeople(file, data) if (data.description && typeof data.description !== 'string') { warn(file, data, "'description' field should be a string") delete data.description } if (data.readme && !data.description) readmeDescription(file, data) readJson.cache.set(file, data) cb(null, data) } // /**package { "name": "foo", "version": "1.2.3", ... } **/ function parseIndex (data) { data = data.split(/^\/\*\*package(?:\s|$)/m) if (data.length < 2) return null data = data[1] data = data.split(/\*\*\/$/m) if (data.length < 2) return null data = data[0] data = data.replace(/^\s*\*/mg, "") try { return JSON.parse(data) } catch (er) { return null } } function parseError (ex, file) { var e = new Error("Failed to parse json\n"+ex.message) e.code = "EJSONPARSE" e.file = file return e } // a warning for deprecated or likely-incorrect fields function typoWarn (file, data) { if (data.modules) { warn(file, data, "'modules' is deprecated") delete data.modules } Object.keys(typos).forEach(function (d) { checkTypo(file, data, d) }) bugsTypoWarn(file, data) scriptTypoWarn(file, data) noreadmeWarn(file, data) typoWarned[data._id] = true } function noreadmeWarn (file, data) { if (data.readme) return; warn(file, data, "No README.md file found!") data.readme = "ERROR: No README.md file found!" } function checkTypo (file, data, d) { if (!data.hasOwnProperty(d)) return; warn(file, data, "'" + d + "' should probably be '" + typos[d] + "'" ) } function bugsTypoWarn (file, data) { var b = data.bugs if (!b || typeof b !== "object") return Object.keys(b).forEach(function (k) { if (bugsTypos[k]) { b[bugsTypos[k]] = b[k] delete b[k] } }) } function scriptTypoWarn (file, data) { var s = data.scripts if (!s || typeof s !== "object") return Object.keys(s).forEach(function (k) { if (scriptTypos[k]) { scriptWarn_(file, data, k) } }) } function scriptWarn_ (file, data, k) { warn(file, data, "scripts['" + k + "'] should probably " + "be scripts['" + scriptTypos[k] + "']") } function validRepo (file, data) { if (data.repostories) { warnRepositories(file, data) } if (!data.repository) return; if (typeof data.repository === "string") { data.repository = { type: "git", url: data.repository } } var r = data.repository.url || "" // use the non-private urls r = r.replace(/^(https?|git):\/\/[^\@]+\@github.com/, '$1://github.com') r = r.replace(/^https?:\/\/github.com/, 'git://github.com') if (r.match(/github.com\/[^\/]+\/[^\/]+\.git\.git$/)) { warn(file, data, "Probably broken git " + "url: " + r) } } function warnRepostories (file, data) { warn(file, data, "'repositories' (plural) Not supported.\n" + "Please pick one as the 'repository' field"); data.repository = data.repositories[0] } function validFiles (file, data) { var files = data.files if (files && !Array.isArray(files)) { warn(file, data, "Invalid 'files' member") delete data.files } } function validBin (file, data) { if (!data.bin) return; if (typeof data.bin === "string") { var b = {} b[data.name] = data.bin data.bin = b } } function validMan (file, data) { if (!data.man) return; if (typeof data.man === "string") { data.man = [ data.man ] } } function validBundled (file, data) { var bdd = "bundledDependencies" var bd = "bundleDependencies" if (data[bdd] && !data[bd]) { data[bd] = data[bdd] delete data[bdd] } if (data[bd] && !Array.isArray(data[bd])) { warn(file, data, "bundleDependencies " + "must be an array") } } function objectifyDeps (file, data) { depTypes.forEach(function (d) { objectifyDep_(file, data, d) }) var o = data.optionalDependencies if (!o) return; var d = data.dependencies || {} Object.keys(o).forEach(function (k) { d[k] = o[k] }) data.dependencies = d } function objectifyDep_ (file, data, type) { if (!data[type]) return; data[type] = depObjectify(file, data, data[type]) } function depObjectify (file, data, deps) { if (!deps) return {} if (typeof deps === "string") { deps = deps.trim().split(/[\n\r\s\t ,]+/) } if (!Array.isArray(deps)) return deps var o = {} deps.forEach(function (d) { d = d.trim().split(/(:?[@\s><=])/) var dn = d.shift() var dv = d.join("") dv = dv.trim() dv = dv.replace(/^@/, "") o[dn] = dv }) return o } function warn (f, d, m) { if (typoWarned[d._id]) return; readJson.log.warn("package.json", d._id, m) } function validName (file, data) { if (!data.name) { data.name = "" return true } data.name = data.name.trim() if (data.name.charAt(0) === "." || data.name.match(/[\/@\s\+%:]/) || data.name !== encodeURIComponent(data.name) || data.name.toLowerCase() === "node_modules" || data.name.toLowerCase() === "favicon.ico") { var m = "Invalid name: " m += JSON.stringify(data.name) return new Error(m) } return true } function parseKeywords (file, data) { var kw = data.keywords if (typeof kw === "string") { kw = kw.split(/,\s+/) data.keywords = kw } } function validVersion (file, data) { var v = data.version if (!v) { data.version = "" return true } if (!semver.valid(v)) { return new Error("invalid version: "+v) } data.version = semver.clean(data.version) return true } function unParsePeople (file, data) { return parsePeople(file, data, true) } function parsePeople (file, data, un) { var fn = un ? unParsePerson : parsePerson if (data.author) data.author = fn(data.author) ;["maintainers", "contributors"].forEach(function (set) { if (!Array.isArray(data[set])) return; data[set] = data[set].map(fn) }) return data } function unParsePerson (person) { if (typeof person === "string") return person var name = person.name || "" var u = person.url || person.web var url = u ? (" ("+u+")") : "" var e = person.email || person.mail var email = e ? (" <"+e+">") : "" return name+email+url } function parsePerson (person) { if (typeof person !== "string") return person var name = person.match(/^([^\(<]+)/) var url = person.match(/\(([^\)]+)\)/) var email = person.match(/<([^>]+)>/) var obj = {} if (name && name[0].trim()) obj.name = name[0].trim() if (email) obj.email = email[1]; if (url) obj.url = url[1]; return obj }