summaryrefslogtreecommitdiff
path: root/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala
diff options
context:
space:
mode:
Diffstat (limited to 'examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala')
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala306
1 files changed, 306 insertions, 0 deletions
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala
new file mode 100644
index 0000000..dfabe23
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala
@@ -0,0 +1,306 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin.env.nodejs
+
+import scala.scalajs.sbtplugin.env._
+import scala.scalajs.sbtplugin.JSUtils.toJSstr
+
+import scala.scalajs.tools.io._
+import scala.scalajs.tools.classpath._
+import scala.scalajs.tools.env._
+import scala.scalajs.tools.jsdep._
+import scala.scalajs.tools.logging._
+
+import scala.scalajs.sbtplugin.JSUtils._
+
+import java.io.{ Console => _, _ }
+import java.net._
+
+import scala.io.Source
+
+class NodeJSEnv(
+ nodejsPath: String = "node",
+ addArgs: Seq[String] = Seq.empty,
+ addEnv: Map[String, String] = Map.empty
+) extends ExternalJSEnv(addArgs, addEnv) with ComJSEnv {
+
+ protected def vmName: String = "node.js"
+ protected def executable: String = nodejsPath
+
+ override def jsRunner(classpath: CompleteClasspath, code: VirtualJSFile,
+ logger: Logger, console: JSConsole): JSRunner = {
+ new NodeRunner(classpath, code, logger, console)
+ }
+
+ override def asyncRunner(classpath: CompleteClasspath, code: VirtualJSFile,
+ logger: Logger, console: JSConsole): AsyncJSRunner = {
+ new AsyncNodeRunner(classpath, code, logger, console)
+ }
+
+ override def comRunner(classpath: CompleteClasspath, code: VirtualJSFile,
+ logger: Logger, console: JSConsole): ComJSRunner = {
+ new ComNodeRunner(classpath, code, logger, console)
+ }
+
+ protected class NodeRunner(classpath: CompleteClasspath,
+ code: VirtualJSFile, logger: Logger, console: JSConsole
+ ) extends ExtRunner(classpath, code, logger, console)
+ with AbstractNodeRunner
+
+ protected class AsyncNodeRunner(classpath: CompleteClasspath,
+ code: VirtualJSFile, logger: Logger, console: JSConsole
+ ) extends AsyncExtRunner(classpath, code, logger, console)
+ with AbstractNodeRunner
+
+ protected class ComNodeRunner(classpath: CompleteClasspath,
+ code: VirtualJSFile, logger: Logger, console: JSConsole
+ ) extends AsyncNodeRunner(classpath, code, logger, console)
+ with ComJSRunner {
+
+ /** Retry-timeout to wait for the JS VM to connect */
+ private final val acceptTimeout = 1000
+
+ private[this] val serverSocket =
+ new ServerSocket(0, 0, InetAddress.getByName(null)) // Loopback address
+ private[this] var comSocket: Socket = _
+ private[this] var jvm2js: DataOutputStream = _
+ private[this] var js2jvm: DataInputStream = _
+
+ private def comSetup = new MemVirtualJSFile("comSetup.js").withContent(
+ s"""
+ (function() {
+ // The socket for communication
+ var socket = null;
+ // The callback where received messages go
+ var recvCallback = null;
+
+ // Buffers received data
+ var inBuffer = new Buffer(0);
+
+ function onData(data) {
+ inBuffer = Buffer.concat([inBuffer, data]);
+ tryReadMsg();
+ }
+
+ function tryReadMsg() {
+ if (inBuffer.length < 4) return;
+ var msgLen = inBuffer.readInt32BE(0);
+ var byteLen = 4 + msgLen * 2;
+
+ if (inBuffer.length < byteLen) return;
+ var res = "";
+
+ for (var i = 0; i < msgLen; ++i)
+ res += String.fromCharCode(inBuffer.readInt16BE(4 + i * 2));
+
+ inBuffer = inBuffer.slice(byteLen);
+
+ recvCallback(res);
+ }
+
+ global.scalajsCom = {
+ init: function(recvCB) {
+ if (socket !== null) throw new Error("Com already open");
+
+ var net = require('net');
+ recvCallback = recvCB;
+ socket = net.connect(${serverSocket.getLocalPort});
+ socket.on('data', onData);
+ },
+ send: function(msg) {
+ if (socket === null) throw new Error("Com not open");
+
+ var len = msg.length;
+ var buf = new Buffer(4 + len * 2);
+ buf.writeInt32BE(len, 0);
+ for (var i = 0; i < len; ++i)
+ buf.writeInt16BE(msg.charCodeAt(i), 4 + i * 2);
+ socket.write(buf);
+ },
+ close: function() {
+ if (socket === null) throw new Error("Com not open");
+ socket.end();
+ }
+ }
+ }).call(this);
+ """
+ )
+
+ def send(msg: String): Unit = {
+ if (awaitConnection()) {
+ jvm2js.writeInt(msg.length)
+ jvm2js.writeChars(msg)
+ jvm2js.flush()
+ }
+ }
+
+ def receive(): String = {
+ if (!awaitConnection())
+ throw new ComJSEnv.ComClosedException
+ try {
+ val len = js2jvm.readInt()
+ val carr = Array.fill(len)(js2jvm.readChar())
+ String.valueOf(carr)
+ } catch {
+ case e: EOFException =>
+ throw new ComJSEnv.ComClosedException
+ }
+ }
+
+ def close(): Unit = {
+ serverSocket.close()
+ if (jvm2js != null)
+ jvm2js.close()
+ if (js2jvm != null)
+ js2jvm.close()
+ if (comSocket != null)
+ comSocket.close()
+ }
+
+ override def stop(): Unit = {
+ close()
+ super.stop()
+ }
+
+ /** Waits until the JS VM has established a connection or terminates
+ * @return true if the connection was established
+ */
+ private def awaitConnection(): Boolean = {
+ serverSocket.setSoTimeout(acceptTimeout)
+ while (comSocket == null && isRunning) {
+ try {
+ comSocket = serverSocket.accept()
+ jvm2js = new DataOutputStream(
+ new BufferedOutputStream(comSocket.getOutputStream()))
+ js2jvm = new DataInputStream(
+ new BufferedInputStream(comSocket.getInputStream()))
+ } catch {
+ case to: SocketTimeoutException =>
+ }
+ }
+
+ comSocket != null
+ }
+
+ override protected def initFiles(): Seq[VirtualJSFile] =
+ super.initFiles :+ comSetup
+
+ override protected def finalize(): Unit = close()
+ }
+
+ protected trait AbstractNodeRunner extends AbstractExtRunner {
+
+ protected[this] val libCache = new VirtualFileMaterializer(true)
+
+ /** File(s) to automatically install source-map-support.
+ * Is used by [[initFiles]], override to change/disable.
+ */
+ protected def installSourceMap(): Seq[VirtualJSFile] = Seq(
+ new MemVirtualJSFile("sourceMapSupport.js").withContent(
+ """
+ try {
+ require('source-map-support').install();
+ } catch (e) {}
+ """
+ )
+ )
+
+ /** File(s) to hack console.log to prevent if from changing `%%` to `%`.
+ * Is used by [[initFiles]], override to change/disable.
+ */
+ protected def fixPercentConsole(): Seq[VirtualJSFile] = Seq(
+ new MemVirtualJSFile("nodeConsoleHack.js").withContent(
+ """
+ // Hack console log to duplicate double % signs
+ (function() {
+ var oldLog = console.log;
+ var newLog = function() {
+ var args = arguments;
+ if (args.length >= 1 && args[0] !== void 0 && args[0] !== null) {
+ args[0] = args[0].toString().replace(/%/g, "%%");
+ }
+ oldLog.apply(console, args);
+ };
+ console.log = newLog;
+ })();
+ """
+ )
+ )
+
+ /** File(s) to define `__ScalaJSEnv`. Defines `exitFunction`.
+ * Is used by [[initFiles]], override to change/disable.
+ */
+ protected def runtimeEnv(): Seq[VirtualJSFile] = Seq(
+ new MemVirtualJSFile("scalaJSEnvInfo.js").withContent(
+ """
+ __ScalaJSEnv = {
+ exitFunction: function(status) { process.exit(status); }
+ };
+ """
+ )
+ )
+
+ /** Concatenates results from [[installSourceMap]], [[fixPercentConsole]] and
+ * [[runtimeEnv]] (in this order).
+ */
+ override protected def initFiles(): Seq[VirtualJSFile] =
+ installSourceMap() ++ fixPercentConsole() ++ runtimeEnv()
+
+ /** Libraries are loaded via require in Node.js */
+ override protected def getLibJSFiles(): Seq[VirtualJSFile] = {
+ initFiles() ++
+ classpath.jsLibs.map(requireLibrary) :+
+ classpath.scalaJSCode
+ }
+
+ /** Rewrites a library virtual file to a require statement if possible */
+ protected def requireLibrary(dep: ResolvedJSDependency): VirtualJSFile = {
+ dep.info.commonJSName.fold(dep.lib) { varname =>
+ val fname = dep.lib.name
+ libCache.materialize(dep.lib)
+ new MemVirtualJSFile(s"require-$fname").withContent(
+ s"""$varname = require(${toJSstr(fname)});"""
+ )
+ }
+ }
+
+ // Send code to Stdin
+ override protected def sendVMStdin(out: OutputStream): Unit = {
+ sendJS(getJSFiles(), out)
+ }
+
+ /** write a single JS file to a writer using an include fct if appropriate
+ * uses `require` if the file exists on the filesystem
+ */
+ override protected def writeJSFile(file: VirtualJSFile,
+ writer: Writer): Unit = {
+ file match {
+ case file: FileVirtualJSFile =>
+ val fname = toJSstr(file.file.getAbsolutePath)
+ writer.write(s"require($fname);\n")
+ case _ =>
+ super.writeJSFile(file, writer)
+ }
+ }
+
+ // Node.js specific (system) environment
+ override protected def getVMEnv(): Map[String, String] = {
+ val baseNodePath = sys.env.get("NODE_PATH").filter(_.nonEmpty)
+ val nodePath = libCache.cacheDir.getAbsolutePath +
+ baseNodePath.fold("")(p => File.pathSeparator + p)
+
+ sys.env ++ Seq(
+ "NODE_MODULE_CONTEXTS" -> "0",
+ "NODE_PATH" -> nodePath
+ ) ++ additionalEnv
+ }
+ }
+
+}