Skip to content

Commit a814cc3

Browse files
committed
Add DOMJSEnv based on 'jsdom' in Node.js.
1 parent eac5177 commit a814cc3

File tree

4 files changed

+331
-290
lines changed

4 files changed

+331
-290
lines changed

js-envs/src/main/scala/org/scalajs/jsenv/ExternalJSEnv.scala

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ abstract class ExternalJSEnv(
2929

3030
protected class AbstractExtRunner(
3131
protected val libs: Seq[ResolvedJSDependency],
32-
protected val code: VirtualJSFile) {
32+
protected val code: VirtualJSFile) extends JSInitFiles {
3333

3434
private[this] var _logger: Logger = _
3535
private[this] var _console: JSConsole = _
@@ -43,9 +43,6 @@ abstract class ExternalJSEnv(
4343
_console = console
4444
}
4545

46-
/** JS files used to setup VM */
47-
protected def initFiles(): Seq[VirtualJSFile] = Nil
48-
4946
/** Custom initialization scripts, defined by the environment. */
5047
final protected def customInitFiles(): Seq[VirtualJSFile] =
5148
ExternalJSEnv.this.customInitFiles()
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.scalajs.jsenv
2+
3+
import org.scalajs.core.tools.io.VirtualJSFile
4+
5+
trait JSInitFiles {
6+
/** JS files used to setup VM */
7+
protected def initFiles(): Seq[VirtualJSFile] = Nil
8+
}
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
/* __ *\
2+
** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
3+
** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
4+
** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
5+
** /____/\___/_/ |_/____/_/ | |__/ /____/ **
6+
** |/____/ **
7+
\* */
8+
9+
10+
package org.scalajs.jsenv.nodejs
11+
12+
import java.io.{Console => _, _}
13+
import java.net._
14+
15+
import org.scalajs.core.ir.Utils.escapeJS
16+
import org.scalajs.core.tools.io._
17+
import org.scalajs.core.tools.jsdep.ResolvedJSDependency
18+
import org.scalajs.core.tools.logging.NullLogger
19+
import org.scalajs.jsenv._
20+
import org.scalajs.jsenv.Utils.OptDeadline
21+
22+
import scala.concurrent.TimeoutException
23+
import scala.concurrent.duration._
24+
25+
abstract class AbstractNodeJSEnv(nodejsPath: String, addArgs: Seq[String],
26+
addEnv: Map[String, String], val sourceMap: Boolean)
27+
extends ExternalJSEnv(addArgs, addEnv) with ComJSEnv {
28+
29+
/** True, if the installed node executable supports source mapping.
30+
*
31+
* Do `npm install source-map-support` if you need source maps.
32+
*/
33+
lazy val hasSourceMapSupport: Boolean = {
34+
val code = new MemVirtualJSFile("source-map-support-probe.js")
35+
.withContent("""require('source-map-support').install();""")
36+
37+
try {
38+
jsRunner(code).run(NullLogger, NullJSConsole)
39+
true
40+
} catch {
41+
case t: ExternalJSEnv.NonZeroExitException =>
42+
false
43+
}
44+
}
45+
46+
protected def executable: String = nodejsPath
47+
48+
/** Retry-timeout to wait for the JS VM to connect */
49+
protected val acceptTimeout = 5000
50+
51+
protected trait AbstractNodeRunner extends AbstractExtRunner with JSInitFiles {
52+
53+
protected[this] val libCache = new VirtualFileMaterializer(true)
54+
55+
/** File(s) to automatically install source-map-support.
56+
* Is used by [[initFiles]], override to change/disable.
57+
*/
58+
protected def installSourceMap(): Seq[VirtualJSFile] = {
59+
if (sourceMap) Seq(
60+
new MemVirtualJSFile("sourceMapSupport.js").withContent(
61+
"""
62+
|try {
63+
| require('source-map-support').install();
64+
|} catch (e) {}
65+
""".stripMargin
66+
)
67+
) else Seq()
68+
}
69+
70+
/** File(s) to hack console.log to prevent if from changing `%%` to `%`.
71+
* Is used by [[initFiles]], override to change/disable.
72+
*/
73+
protected def fixPercentConsole(): Seq[VirtualJSFile] = Seq(
74+
new MemVirtualJSFile("nodeConsoleHack.js").withContent(
75+
"""
76+
|// Hack console log to duplicate double % signs
77+
|(function() {
78+
| function startsWithAnyOf(s, prefixes) {
79+
| for (var i = 0; i < prefixes.length; i++) {
80+
| // ES5 does not have .startsWith() on strings
81+
| if (s.substring(0, prefixes[i].length) === prefixes[i])
82+
| return true;
83+
| }
84+
| return false;
85+
| }
86+
| var nodeWillDeduplicateEvenForOneArgument = startsWithAnyOf(
87+
| process.version, ["v0.", "v1.", "v2.0."]);
88+
| var oldLog = console.log;
89+
| var newLog = function() {
90+
| var args = arguments;
91+
| if (args.length >= 1 && args[0] !== void 0 && args[0] !== null) {
92+
| var argStr = args[0].toString();
93+
| if (args.length > 1 || nodeWillDeduplicateEvenForOneArgument)
94+
| argStr = argStr.replace(/%/g, "%%");
95+
| args[0] = argStr;
96+
| }
97+
| oldLog.apply(console, args);
98+
| };
99+
| console.log = newLog;
100+
|})();
101+
""".stripMargin
102+
)
103+
)
104+
105+
106+
/** File(s) to define `__ScalaJSEnv`. Defines `exitFunction`.
107+
* Is used by [[initFiles]], override to change/disable.
108+
*/
109+
protected def runtimeEnv(): Seq[VirtualJSFile] = Seq(
110+
new MemVirtualJSFile("scalaJSEnvInfo.js").withContent(
111+
"""
112+
|__ScalaJSEnv = {
113+
| exitFunction: function(status) { process.exit(status); }
114+
|};
115+
""".stripMargin
116+
)
117+
)
118+
119+
override protected def initFiles(): Seq[VirtualJSFile] =
120+
installSourceMap() ++ fixPercentConsole() ++ runtimeEnv()
121+
122+
/** write a single JS file to a writer using an include fct if appropriate
123+
* uses `require` if the file exists on the filesystem
124+
*/
125+
override protected def writeJSFile(file: VirtualJSFile,
126+
writer: Writer): Unit = {
127+
file match {
128+
case file: FileVirtualJSFile =>
129+
val fname = file.file.getAbsolutePath
130+
writer.write(s"""require("${escapeJS(fname)}");\n""")
131+
case _ =>
132+
super.writeJSFile(file, writer)
133+
}
134+
}
135+
136+
// Node.js specific (system) environment
137+
override protected def getVMEnv(): Map[String, String] = {
138+
val baseNodePath = sys.env.get("NODE_PATH").filter(_.nonEmpty)
139+
val nodePath = libCache.cacheDir.getAbsolutePath +
140+
baseNodePath.fold("")(p => File.pathSeparator + p)
141+
142+
sys.env ++ Seq(
143+
"NODE_MODULE_CONTEXTS" -> "0",
144+
"NODE_PATH" -> nodePath
145+
) ++ additionalEnv
146+
}
147+
}
148+
149+
protected trait NodeComJSRunner extends ComJSRunner with JSInitFiles {
150+
151+
private[this] val serverSocket =
152+
new ServerSocket(0, 0, InetAddress.getByName(null)) // Loopback address
153+
private var comSocket: Socket = _
154+
private var jvm2js: DataOutputStream = _
155+
private var js2jvm: DataInputStream = _
156+
157+
abstract override protected def initFiles(): Seq[VirtualJSFile] =
158+
super.initFiles :+ comSetup
159+
160+
private def comSetup(): VirtualJSFile = {
161+
new MemVirtualJSFile("comSetup.js").withContent(
162+
s"""
163+
|(function() {
164+
| // The socket for communication
165+
| var socket = null;
166+
| // The callback where received messages go
167+
| var recvCallback = null;
168+
|
169+
| // Buffers received data
170+
| var inBuffer = new Buffer(0);
171+
|
172+
| function onData(data) {
173+
| inBuffer = Buffer.concat([inBuffer, data]);
174+
| tryReadMsg();
175+
| }
176+
|
177+
| function tryReadMsg() {
178+
| while (inBuffer.length >= 4) {
179+
| var msgLen = inBuffer.readInt32BE(0);
180+
| var byteLen = 4 + msgLen * 2;
181+
|
182+
| if (inBuffer.length < byteLen) return;
183+
| var res = "";
184+
|
185+
| for (var i = 0; i < msgLen; ++i)
186+
| res += String.fromCharCode(inBuffer.readInt16BE(4 + i * 2));
187+
|
188+
| inBuffer = inBuffer.slice(byteLen);
189+
|
190+
| recvCallback(res);
191+
| }
192+
| }
193+
|
194+
| global.scalajsCom = {
195+
| init: function(recvCB) {
196+
| if (socket !== null) throw new Error("Com already open");
197+
|
198+
| var net = require('net');
199+
| recvCallback = recvCB;
200+
| socket = net.connect(${serverSocket.getLocalPort});
201+
| socket.on('data', onData);
202+
| socket.on('error', function(err) {
203+
| // Whatever happens, this closes the Com
204+
| socket.end();
205+
|
206+
| // Expected errors:
207+
| // - EPIPE on write: JVM closes
208+
| // - ECONNREFUSED on connect: JVM closes before JS opens
209+
| var expected = (
210+
| err.syscall === "write" && err.code === "EPIPE" ||
211+
| err.syscall === "connect" && err.code === "ECONNREFUSED"
212+
| );
213+
|
214+
| if (!expected) {
215+
| console.error("Scala.js Com failed: " + err);
216+
| // We must terminate with an error
217+
| process.exit(-1);
218+
| }
219+
| });
220+
| },
221+
| send: function(msg) {
222+
| if (socket === null) throw new Error("Com not open");
223+
|
224+
| var len = msg.length;
225+
| var buf = new Buffer(4 + len * 2);
226+
| buf.writeInt32BE(len, 0);
227+
| for (var i = 0; i < len; ++i)
228+
| buf.writeUInt16BE(msg.charCodeAt(i), 4 + i * 2);
229+
| socket.write(buf);
230+
| },
231+
| close: function() {
232+
| if (socket === null) throw new Error("Com not open");
233+
| socket.end();
234+
| }
235+
| }
236+
|}).call(this);
237+
""".stripMargin)
238+
}
239+
240+
def send(msg: String): Unit = {
241+
if (awaitConnection()) {
242+
jvm2js.writeInt(msg.length)
243+
jvm2js.writeChars(msg)
244+
jvm2js.flush()
245+
}
246+
}
247+
248+
def receive(timeout: Duration): String = {
249+
if (!awaitConnection())
250+
throw new ComJSEnv.ComClosedException("Node.js isn't connected")
251+
252+
js2jvm.mark(Int.MaxValue)
253+
val savedSoTimeout = comSocket.getSoTimeout()
254+
try {
255+
val optDeadline = OptDeadline(timeout)
256+
257+
comSocket.setSoTimeout((optDeadline.millisLeft min Int.MaxValue).toInt)
258+
val len = js2jvm.readInt()
259+
val carr = Array.fill(len) {
260+
comSocket.setSoTimeout((optDeadline.millisLeft min Int.MaxValue).toInt)
261+
js2jvm.readChar()
262+
}
263+
264+
js2jvm.mark(0)
265+
String.valueOf(carr)
266+
} catch {
267+
case e: EOFException =>
268+
throw new ComJSEnv.ComClosedException(e)
269+
case e: SocketTimeoutException =>
270+
js2jvm.reset()
271+
throw new TimeoutException("Timeout expired")
272+
} finally {
273+
comSocket.setSoTimeout(savedSoTimeout)
274+
}
275+
}
276+
277+
def close(): Unit = {
278+
serverSocket.close()
279+
if (jvm2js != null)
280+
jvm2js.close()
281+
if (js2jvm != null)
282+
js2jvm.close()
283+
if (comSocket != null)
284+
comSocket.close()
285+
}
286+
287+
/** Waits until the JS VM has established a connection or terminates
288+
*
289+
* @return true if the connection was established
290+
*/
291+
private def awaitConnection(): Boolean = {
292+
serverSocket.setSoTimeout(acceptTimeout)
293+
while (comSocket == null && isRunning) {
294+
try {
295+
comSocket = serverSocket.accept()
296+
jvm2js = new DataOutputStream(
297+
new BufferedOutputStream(comSocket.getOutputStream()))
298+
js2jvm = new DataInputStream(
299+
new BufferedInputStream(comSocket.getInputStream()))
300+
} catch {
301+
case to: SocketTimeoutException =>
302+
}
303+
}
304+
305+
comSocket != null
306+
}
307+
308+
override protected def finalize(): Unit = close()
309+
}
310+
}

0 commit comments

Comments
 (0)