mirror of https://github.com/lukechilds/node.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
570 lines
16 KiB
570 lines
16 KiB
|
|
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("npmlog")
|
|
, 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("json from cache", jsonFile)
|
|
return cb(null, cache[jsonFile])
|
|
}
|
|
log.verbose("read json", jsonFile)
|
|
|
|
opts.file = jsonFile
|
|
|
|
var wscript = null
|
|
, contributors = null
|
|
, serverjs = null
|
|
, gypfile = null
|
|
|
|
if (opts.gypfile !== null && opts.gypfile !== undefined) {
|
|
gypfile = opts.gypfile
|
|
next()
|
|
} else {
|
|
var pkgdir = path.dirname(jsonFile)
|
|
|
|
function hasGyp (has) {
|
|
gypfile = opts.gypfile = has
|
|
next()
|
|
}
|
|
|
|
fs.readdir(pkgdir, function (er, gf) {
|
|
// this would be weird.
|
|
if (er) return hasGyp(false)
|
|
|
|
// see if there are any *.gyp files in there.
|
|
gf = gf.filter(function (f) {
|
|
return f.match(/\.gyp$/)
|
|
})
|
|
gf = gf[0]
|
|
return hasGyp(!!gf)
|
|
})
|
|
}
|
|
|
|
if (opts.wscript !== null && opts.wscript !== undefined) {
|
|
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 && opts.contributors !== undefined) {
|
|
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 && opts.serverjs !== undefined) {
|
|
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 ||
|
|
gypfile === null ||
|
|
serverjs === null) {
|
|
return
|
|
}
|
|
|
|
// XXX this api here is insane. being internal is no excuse.
|
|
// please refactor.
|
|
var thenLoad = 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)
|
|
})
|
|
|
|
fs.readFile(jsonFile, function (er, data) {
|
|
if (er && er.code === "ENOENT") {
|
|
// single-file module, maybe?
|
|
// check index.js for a /**package { ... } **/ section.
|
|
var indexFile = path.resolve(path.dirname(jsonFile), "index.js")
|
|
return fs.readFile(indexFile, function (er2, data) {
|
|
// if this doesn't work, then die with the original error.
|
|
if (er2) return cb(er)
|
|
data = parseIndex(data)
|
|
if (!data) return cb(er)
|
|
thenLoad(null, data)
|
|
})
|
|
}
|
|
thenLoad(er, data)
|
|
})
|
|
}
|
|
}
|
|
|
|
// sync. no io.
|
|
// /**package { "name": "foo", "version": "1.2.3", ... } **/
|
|
function parseIndex (data) {
|
|
data = data.toString()
|
|
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, "")
|
|
return data
|
|
}
|
|
|
|
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) {
|
|
if (er) return cb(er, jsonString)
|
|
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("Error parsing json", opts.file, ex)
|
|
} 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.code = "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.verbose("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"
|
|
, "dev-dependencies": "devDependencies"
|
|
, "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( json._id, "package.json: '" + d + "' should probably be '"
|
|
+ typos[d] + "'" )
|
|
}
|
|
})
|
|
|
|
// bugs typos
|
|
var bugsTypos = { "web": "url"
|
|
, "name": "url"
|
|
}
|
|
|
|
if (typeof json.bugs === "object") {
|
|
// just go ahead and correct these.
|
|
Object.keys(bugsTypos).forEach(function (d) {
|
|
if (json.bugs.hasOwnProperty(d)) {
|
|
json.bugs[ bugsTypos[d] ] = json.bugs[d]
|
|
delete json.bugs[d]
|
|
}
|
|
})
|
|
}
|
|
|
|
// script typos
|
|
var scriptTypos = { "server": "start" }
|
|
if (json.scripts) Object.keys(scriptTypos).forEach(function (d) {
|
|
if (json.scripts.hasOwnProperty(d)) {
|
|
log.warn( json._id
|
|
, "package.json: scripts['" + d + "'] should probably be "
|
|
+ "scripts['" + scriptTypos[d] + "']" )
|
|
}
|
|
})
|
|
}
|
|
|
|
|
|
function processObject (opts, cb) { return function (er, json) {
|
|
// json._npmJsonOpts = opts
|
|
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("json", "incorrect json: "+json.name, msg)
|
|
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(json._id, "Probably broken git url", repo)
|
|
}
|
|
json.repository.url = repo
|
|
}
|
|
|
|
var files = json.files
|
|
if (files && !Array.isArray(files)) {
|
|
log.warn(json._id, "Invalid 'files' member. See 'npm help json'", files)
|
|
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 scripts = json.scripts || {}
|
|
|
|
// if it has a bindings.gyp, then build with node-gyp
|
|
if (opts.gypfile && !json.prebuilt) {
|
|
log.verbose(json._id, "has bindings.gyp", [json.prebuilt, opts])
|
|
if (!scripts.install && !scripts.preinstall) {
|
|
scripts.install = "node-gyp rebuild"
|
|
json.scripts = scripts
|
|
}
|
|
}
|
|
|
|
// if it has a wscript, then build it.
|
|
if (opts.wscript && !json.prebuilt) {
|
|
log.verbose(json._id, "has wscript", [json.prebuilt, opts])
|
|
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"
|
|
, "optionalDependencies"
|
|
].forEach(function (d) {
|
|
json[d] = json.hasOwnProperty(d)
|
|
? depObjectify(json[d], d, json._id)
|
|
: {}
|
|
})
|
|
|
|
// always merge optionals into deps
|
|
Object.keys(json.optionalDependencies).forEach(function (d) {
|
|
json.dependencies[d] = json.optionalDependencies[d]
|
|
})
|
|
|
|
if (opts.dev
|
|
|| npm.config.get("dev")
|
|
|| npm.config.get("npat")) {
|
|
Object.keys(json.devDependencies || {}).forEach(function (d) {
|
|
json.dependencies[d] = json.devDependencies[d]
|
|
})
|
|
}
|
|
|
|
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("caching json", opts.file)
|
|
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
|
|
}}
|
|
|
|
var depObjectifyWarn = {}
|
|
function depObjectify (deps, d, id) {
|
|
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><=])/)
|
|
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._id
|
|
, "Invalid range in engines.node. Please see `npm help json`"
|
|
, json.engines.node )
|
|
}
|
|
|
|
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]
|
|
})
|
|
}
|
|
|