diff options
Diffstat (limited to 'deps/npm/lib/utils/read-json.js')
-rw-r--r-- | deps/npm/lib/utils/read-json.js | 496 |
1 files changed, 496 insertions, 0 deletions
diff --git a/deps/npm/lib/utils/read-json.js b/deps/npm/lib/utils/read-json.js new file mode 100644 index 0000000000..e71128c1b1 --- /dev/null +++ b/deps/npm/lib/utils/read-json.js @@ -0,0 +1,496 @@ + +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 || true; 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] + }) +} |