module.exports = readJson readJson.processJson = processJson readJson.unParsePeople = unParsePeople readJson.parsePeople = parsePeople readJson.clearCache = clearCache var fs = require("graceful-fs") , semver = require("semver") , path = require("path") , log = require("./log.js") , npm = require("../npm.js") , cache = {} , timers = {} , loadPackageDefaults = require("./load-package-defaults.js") function readJson (jsonFile, opts, cb) { if (typeof cb !== "function") cb = opts, opts = {} if (cache.hasOwnProperty(jsonFile)) { log.verbose(jsonFile, "from cache") return cb(null, cache[jsonFile]) } opts.file = jsonFile if (!opts.tag) { var parsedPath = jsonFile.indexOf(npm.dir) === 0 && jsonFile.match( /\/([^\/]+)\/([^\/]+)\/package\/package\.json$/) if (parsedPath && semver.valid(parsedPath[2])) { // this is a package.json in some installed package. // infer the opts.tag so that linked packages behave right. opts.tag = parsedPath[2] } } var wscript = null , contributors = null , serverjs = null if (opts.wscript != null) { wscript = opts.wscript next() } else fs.readFile( path.join(path.dirname(jsonFile), "wscript") , function (er, data) { if (er) opts.wscript = false else opts.wscript = !!(data.toString().match(/(^|\n)def build\b/) && data.toString().match(/(^|\n)def configure\b/)) wscript = opts.wscript next() }) if (opts.contributors != null) { contributors = opts.contributors next() } else fs.readFile( path.join(path.dirname(jsonFile), "AUTHORS") , function (er, data) { if (er) opts.contributors = false else { data = data.toString().split(/\r?\n/).map(function (l) { l = l.trim().split("#").shift() return l }).filter(function (l) { return l }) opts.contributors = data } contributors = opts.contributors next() }) if (opts.serverjs != null) { serverjs = opts.serverjs next() } else fs.stat( path.join(path.dirname(jsonFile), "server.js") , function (er, st) { if (er) opts.serverjs = false else opts.serverjs = st.isFile() serverjs = opts.serverjs next() }) function next () { if (wscript === null || contributors === null || serverjs === null) { return } fs.readFile(jsonFile, processJson(opts, function (er, data) { if (er) return cb(er) var doLoad = !(jsonFile.indexOf(npm.cache) === 0 && path.basename(path.dirname(jsonFile)) !== "package") if (!doLoad) return cb(er, data) loadPackageDefaults(data, path.dirname(jsonFile), cb) })) } } function processJson (opts, cb) { if (typeof cb !== "function") cb = opts, opts = {} if (typeof cb !== "function") { var thing = cb, cb = null return P(null, thing) } else return P function P (er, thing) { if (er) { if (cb) return cb(er, thing) throw er } if (typeof thing === "object" && !Buffer.isBuffer(thing)) { return processObject(opts, cb)(er, thing) } else { return processJsonString(opts, cb)(er, thing) } } } function processJsonString (opts, cb) { return function (er, jsonString) { jsonString += "" if (er) return cb(er, jsonString) var json try { json = JSON.parse(jsonString) } catch (ex) { if (opts.file && opts.file.indexOf(npm.dir) === 0) { try { json = require("vm").runInNewContext("(\n"+jsonString+"\n)") log.error(opts.file, "Error parsing json") log.error(ex, "parse error ") } catch (ex2) { return jsonParseFail(ex, opts.file, cb) } } else { return jsonParseFail(ex, opts.file, cb) } } return processObject(opts, cb)(er, json) }} function jsonParseFail (ex, file, cb) { var e = new Error( "Failed to parse json\n"+ex.message) e.errno = npm.EJSONPARSE e.file = file if (cb) return cb(e) throw e } // a warning for deprecated or likely-incorrect fields var typoWarned = {} function typoWarn (json) { if (typoWarned[json._id]) return typoWarned[json._id] = true if (json.modules) { log.warn("package.json: 'modules' object is deprecated", json._id) delete json.modules } // http://registry.npmjs.org/-/fields var typos = { "dependancies": "dependencies" , "dependecies": "dependencies" , "depdenencies": "dependencies" , "devEependencies": "devDependencies" , "depends": "dependencies" , "devDependences": "devDependencies" , "devDepenencies": "devDependencies" , "devdependencies": "devDependencies" , "repostitory": "repository" , "prefereGlobal": "preferGlobal" , "hompage": "homepage" , "hampage": "homepage" // XXX maybe not a typo, just delicious? , "autohr": "author" , "autor": "author" , "contributers": "contributors" , "publicationConfig": "publishConfig" } Object.keys(typos).forEach(function (d) { if (json.hasOwnProperty(d)) { log.warn( "package.json: '" + d + "' should probably be '" + typos[d] + "'", json._id) } }) // bugs typos var bugsTypos = { "web": "url" , "name": "url" } if (typeof json.bugs === "object") { Object.keys(bugsTypos).forEach(function (d) { if (json.bugs.hasOwnProperty(d)) { log.warn( "package.json: bugs['" + d + "'] should probably be " + "bugs['" + bugsTypos[d] + "']", json._id) } }) } // script typos var scriptTypos = { "server": "start" } if (json.scripts) Object.keys(scriptTypos).forEach(function (d) { if (json.scripts.hasOwnProperty(d)) { log.warn( "package.json: scripts['" + d + "'] should probably be " + "scripts['" + scriptTypos[d] + "']", json._id) } }) } function processObject (opts, cb) { return function (er, json) { // json._npmJsonOpts = opts // log.warn(json, "processing json") if (npm.config.get("username")) { json._npmUser = { name: npm.config.get("username") , email: npm.config.get("email") } } // slashes would be a security risk. // anything else will just fail harmlessly. if (!json.name) { var e = new Error("No 'name' field found in package.json") if (cb) return cb(e) throw e } json.name = json.name.trim() if (json.name.charAt(0) === "." || json.name.match(/[\/@\s\+%:]/)) { var msg = "Invalid name: " + JSON.stringify(json.name) + " may not start with '.' or contain %/@+: or whitespace" , e = new Error(msg) if (cb) return cb(e) throw e } if (json.name.toLowerCase() === "node_modules") { var msg = "Invalid package name: node_modules" , e = new Error(msg) if (cb) return cb(e) throw e } if (json.name.toLowerCase() === "favicon.ico") { var msg = "Sorry, favicon.ico is a picture, not a package." , e = new Error(msg) if (cb) return cb(e) throw e } if (json.repostories) { var msg = "'repositories' (plural) No longer supported.\n" + "Please pick one, and put it in the 'repository' field." , e = new Error(msg) // uncomment once this is no longer an issue. // if (cb) return cb(e) // throw e log.error(msg, "incorrect json: "+json.name) json.repostory = json.repositories[0] delete json.repositories } if (json.repository) { if (typeof json.repository === "string") { json.repository = { type : "git" , url : json.repository } } var repo = json.repository.url || "" repo = repo.replace(/^(https?|git):\/\/[^\@]+\@github.com/ ,'$1://github.com') if (json.repository.type === "git" && ( repo.match(/^https?:\/\/github.com/) || repo.match(/github.com\/[^\/]+\/[^\/]+\/?$/) && !repo.match(/\.git$/) )) { repo = repo.replace(/^https?:\/\/github.com/, 'git://github.com') if (!repo.match(/\.git$/)) { repo = repo.replace(/\/?$/, '.git') } } if (repo.match(/github\.com\/[^\/]+\/[^\/]+\/?$/) && repo.match(/\.git\.git$/)) { log.warn(repo, "Probably broken git url") } json.repository.url = repo } var files = json.files if (files && !Array.isArray(files)) { log.warn(files, "Invalid 'files' member. See 'npm help json'") delete json.files } var kw = json.keywords if (typeof kw === "string") { kw = kw.split(/,\s+/) json.keywords = kw } json._id = json.name+"@"+json.version var tag = opts.tag if (tag) json.version = tag var scripts = json.scripts || {} // if it has a wscript, then build it. if (opts.wscript && !json.prebuilt) { log.verbose([json.prebuilt, opts], "has wscript") if (!scripts.install && !scripts.preinstall) { // don't fail if it was unexpected, just try. scripts.preinstall = "node-waf clean || (exit 0); node-waf configure build" json.scripts = scripts } } // if it has an AUTHORS, then credit them if (opts.contributors && Array.isArray(opts.contributors) && opts.contributors.length) { json.contributors = opts.contributors } // if it has a server.js, then start it. if (opts.serverjs && !scripts.start) { scripts.start = "node server.js" json.scripts = scripts } if (!(semver.valid(json.version))) { var m if (!json.version) { m = "'version' field missing\n" } else { m = "Invalid 'version' field: "+json.version+"\n" } m += "'version' Must be X.Y.Z, with an optional trailing tag.\n" + "See the section on 'version' in `npm help json`" var e = new Error(m) if (cb) return cb(e) throw e } json.version = semver.clean(json.version) if (json.bin && typeof json.bin === "string") { var b = {} b[ json.name ] = json.bin json.bin = b } if (json.bundledDependencies && !json.bundleDependencies) { json.bundleDependencies = json.bundledDependencies delete json.bundledDependencies } if (json.bundleDependencies && !Array.isArray(json.bundleDependencies)) { var e = new Error("bundleDependencies must be an array.\n" +"See `npm help json`") if (cb) return cb(e) throw e } if (json["dev-dependencies"] && !json.devDependencies) { json.devDependencies = json["dev-dependencies"] delete json["dev-dependencies"] } ;["dependencies", "devDependencies"].forEach(function (d) { json[d] = json[d] ? depObjectify(json[d]) : {} }) if (opts.dev || npm.config.get("dev") || npm.config.get("npat")) { // log.warn(json._id, "Adding devdeps") Object.keys(json.devDependencies || {}).forEach(function (d) { json.dependencies[d] = json.devDependencies[d] }) // log.warn(json.dependencies, "Added devdeps") } typoWarn(json) json = testEngine(json) json = parsePeople(unParsePeople(json)) if ( json.bugs ) json.bugs = parsePerson(unParsePerson(json.bugs)) json._npmVersion = npm.version json._nodeVersion = process.version if (opts.file) { log.verbose(opts.file, "caching") cache[opts.file] = json // arbitrary var keys = Object.keys(cache) , l = keys.length if (l > 10000) for (var i = 0; i < l - 5000; i ++) { delete cache[keys[i]] } } if (cb) cb(null,json) return json }} function depObjectify (deps) { if (!Array.isArray(deps)) return deps var o = {} deps.forEach(function (d) { d = d.trim().split(/(:?[@\s><=])/) o[d.shift()] = d.join("").trim().replace(/^@/, "") }) return o } function testEngine (json) { // if engines is empty, then assume that node is allowed. if ( !json.engines || Array.isArray(json.engines) && !json.engines.length || typeof json.engines === "object" && !Object.keys(json.engines).length ) { json.engines = { "node" : "*" } } if (typeof json.engines === "string") { if (semver.validRange(json.engines) !== null) { json.engines = { "node" : json.engines } } else json.engines = [ json.engines ] } var nodeVer = npm.config.get("node-version") , ok = false if (nodeVer) nodeVer = nodeVer.replace(/\+$/, '') if (Array.isArray(json.engines)) { // Packages/1.0 commonjs style, with an array. // hack it to just hang a "node" member with the version range, // then do the npm-style check below. for (var i = 0, l = json.engines.length; i < l; i ++) { var e = json.engines[i].trim() if (e.substr(0, 4) === "node") { json.engines.node = e.substr(4) } else if (e.substr(0, 3) === "npm") { json.engines.npm = e.substr(3) } } } if (json.engines.node === "") json.engines.node = "*" if (json.engines.node && null === semver.validRange(json.engines.node)) { log.warn( json.engines.node , "Invalid range in engines.node. Please see `npm help json`" ) } if (nodeVer) { json._engineSupported = semver.satisfies( nodeVer , json.engines.node || "null" ) } if (json.engines.hasOwnProperty("npm") && json._engineSupported) { json._engineSupported = semver.satisfies(npm.version, json.engines.npm) } return json } function unParsePeople (json) { return parsePeople(json, true) } function parsePeople (json, un) { var fn = un ? unParsePerson : parsePerson if (json.author) json.author = fn(json.author) ;["maintainers", "contributors"].forEach(function (set) { if (Array.isArray(json[set])) json[set] = json[set].map(fn) }) return json } function unParsePerson (person) { if (typeof person === "string") return person var name = person.name || "" , u = person.url || person.web , url = u ? (" ("+u+")") : "" , e = person.email || person.mail , email = e ? (" <"+e+">") : "" return name+email+url } function parsePerson (person) { if (typeof person !== "string") return person var name = person.match(/^([^\(<]+)/) , url = person.match(/\(([^\)]+)\)/) , email = person.match(/<([^>]+)>/) , 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 } function clearCache (prefix) { if (!prefix) { cache = {} return } Object.keys(cache).forEach(function (c) { if (c.indexOf(prefix) === 0) delete cache[c] }) }