Permalink
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 258 lines (229 sloc) 7.91 KB
#!/usr/bin/env node
// HTTP server for desktop editors
// Reads .tern-project files, wraps a Tern server in an HTTP wrapper
// so that editor plug-ins can talk to it.
var tern = require("../lib/tern");
var fs = require("fs"), path = require("path"), url = require("url");
var glob = require("glob"), minimatch = require("minimatch");
var resolveFrom = require("resolve-from");
var projectFileName = ".tern-project", portFileName = ".tern-port";
var maxIdleTime = 6e4 * 5; // Shut down after five minutes of inactivity
var persistent = process.argv.indexOf("--persistent") > -1;
var stripCRs = process.argv.indexOf("--strip-crs") > -1;
var disableLoadingLocal = process.argv.indexOf("--disable-loading-local") > -1;
var verbose = process.argv.indexOf("--verbose") > -1;
var debug = verbose || process.argv.indexOf("--debug") > -1;
var noPortFile = process.argv.indexOf("--no-port-file") > -1;
var host = "127.0.0.1";
var hostArg = process.argv.indexOf("--host");
if (hostArg > -1) {
host = process.argv[hostArg + 1];
if (host == "null" || host == "any") host = null;
}
var port = 0;
var portArg = process.argv.indexOf("--port");
if (portArg > -1) {
port = Number(process.argv[portArg + 1]);
if (isNaN(port)) port = 0;
}
var ignoreStdin = process.argv.indexOf("--ignore-stdin") > -1;
function findProjectDir() {
var dir = process.cwd();
for (;;) {
try {
if (fs.statSync(path.resolve(dir, projectFileName)).isFile()) return dir;
} catch(e) {}
var shorter = path.dirname(dir);
if (shorter == dir) return null;
dir = shorter;
}
}
var defaultConfig = {
libs: [],
loadEagerly: false,
plugins: {doc_comment: true},
ecmaScript: true,
ecmaVersion: 9,
dependencyBudget: tern.defaultOptions.dependencyBudget
};
var homeDir = process.env.HOME || process.env.USERPROFILE;
if (homeDir && fs.existsSync(path.resolve(homeDir, ".tern-config")))
defaultConfig = readProjectFile(path.resolve(homeDir, ".tern-config"));
function readJSON(fileName) {
var file = fs.readFileSync(fileName, "utf8");
try {
return JSON.parse(file);
} catch (e) {
console.error("Bad JSON in " + fileName + ": " + e.message);
process.exit(1);
}
}
function readProjectFile(fileName) {
var data = readJSON(fileName), name;
for (var option in defaultConfig) {
if (!data.hasOwnProperty(option))
data[option] = defaultConfig[option];
else if (option == "plugins")
for (name in defaultConfig.plugins)
if (!Object.prototype.hasOwnProperty.call(data.plugins, name))
data.plugins[name] = defaultConfig.plugins[name];
}
return data;
}
function findFile(file, projectDir, fallbackDir) {
var local = path.resolve(projectDir, file);
if (!disableLoadingLocal && fs.existsSync(local)) return local;
var shared = path.resolve(fallbackDir, file);
if (fs.existsSync(shared)) return shared;
}
var distDir = path.resolve(__dirname, "..");
function findDefs(projectDir, config) {
var defs = [], src = config.libs.slice();
if (config.ecmaScript && src.indexOf("ecmascript") == -1)
src.unshift("ecmascript");
for (var i = 0; i < src.length; ++i) {
var file = src[i];
if (!/\.json$/.test(file)) file = file + ".json";
var found = findFile(file, projectDir, path.resolve(distDir, "defs"))
|| resolveFrom(projectDir, "tern-" + src[i]);
if (!found) {
try {
found = require.resolve("tern-" + src[i]);
} catch (e) {
process.stderr.write("Failed to find library " + src[i] + ".\n");
continue;
}
}
if (found) defs.push(readJSON(found));
}
return defs;
}
function loadPlugins(projectDir, config) {
var plugins = config.plugins, options = {};
for (var plugin in plugins) {
var val = plugins[plugin];
if (!val) continue;
var found = findFile(plugin + ".js", projectDir, path.resolve(distDir, "plugin"))
|| resolveFrom(projectDir, "tern-" + plugin);
if (!found) {
try {
found = require.resolve("tern-" + plugin);
} catch (e) {
process.stderr.write("Failed to find plugin " + plugin + ".\n");
continue;
}
}
var mod = require(found);
if (mod.hasOwnProperty("initialize")) mod.initialize(distDir);
options[path.basename(plugin)] = val;
}
return options;
}
var projectDir = findProjectDir();
if (projectDir) {
var config = readProjectFile(path.resolve(projectDir, projectFileName));
} else {
projectDir = process.cwd();
var config = defaultConfig;
}
var httpServer = require("http").createServer(function(req, resp) {
clearTimeout(shutdown);
shutdown = setTimeout(doShutdown, maxIdleTime);
var target = url.parse(req.url, true);
if (target.pathname == "/ping") return respondSimple(resp, 200, "pong");
if (target.pathname != "/") return respondSimple(resp, 404, "No service at " + target.pathname);
if (req.method == "POST") {
var body = "";
req.on("data", function (data) { body += data; });
req.on("end", function() { respond(resp, body); });
} else if (req.method == "GET") {
if (target.query.doc) respond(resp, target.query.doc);
else respondSimple(resp, 400, "Missing query document");
}
});
var server = startServer(projectDir, config, httpServer);
function startServer(dir, config, httpServer) {
var defs = findDefs(dir, config);
var plugins = loadPlugins(dir, config);
var server = new tern.Server({
getFile: function(name, c) {
if (config.dontLoad && config.dontLoad.some(function(pat) {return minimatch(name, pat)}))
c(null, "");
else
fs.readFile(path.resolve(dir, name), "utf8", c);
},
normalizeFilename: function(name) {
var pt = path.resolve(dir, name);
try { pt = fs.realPathSync(path.resolve(dir, name), true) }
catch(e) {}
return path.relative(dir, pt);
},
async: true,
defs: defs,
plugins: plugins,
debug: debug,
projectDir: dir,
ecmaVersion: config.ecmaVersion,
dependencyBudget: config.dependencyBudget,
stripCRs: stripCRs,
parent: {httpServer: httpServer}
});
if (config.loadEagerly) config.loadEagerly.forEach(function(pat) {
glob.sync(pat, { cwd: dir }).forEach(function(file) {
server.addFile(file);
});
});
server.flush(function(){});
return server;
}
function doShutdown() {
if (persistent) return;
console.log("Was idle for " + Math.floor(maxIdleTime / 6e4) + " minutes. Shutting down.");
process.exit();
}
var shutdown = setTimeout(doShutdown, maxIdleTime);
if (!ignoreStdin) {
process.stdin.on("end", function() {
console.log("stdin pipe closed, killing tern");
process.exit();
});
process.stdin.resume();
}
httpServer.listen(port, host, function() {
var port = httpServer.address().port;
if (!noPortFile) {
var portFile = path.resolve(projectDir, portFileName);
fs.writeFileSync(portFile, String(port), "utf8");
process.on("exit", function() {
try {
var cur = Number(fs.readFileSync(portFile, "utf8"));
if (cur == port) fs.unlinkSync(portFile);
} catch(e) {}
});
}
process.on("SIGINT", function() {
console.log("SIGINT trapped, killing tern");
process.exit();
});
process.on("SIGTERM", function() {
console.log("SIGTERM trapped, killing tern");
process.exit();
});
console.log("Listening on port " + port);
});
function respondSimple(resp, status, text) {
resp.writeHead(status, {"content-type": "text/plain; charset=utf-8"});
resp.end(text);
if (verbose) console.log("Response: " + status + " " + text);
}
function respond(resp, doc) {
try { var doc = JSON.parse(doc); }
catch(e) { return respondSimple(resp, 400, "JSON parse error: " + e.message); }
if (verbose) console.log("Request: " + JSON.stringify(doc, null, 2));
server.request(doc, function(err, data) {
if (err) return respondSimple(resp, 400, String(err));
resp.writeHead(200, {"content-type": "application/json; charset=utf-8"});
if (verbose) console.log("Response: " + JSON.stringify(data, null, 2) + "\n");
resp.end(JSON.stringify(data));
});
}