|
8 | 8 |
|
9 | 9 | package org.scalajs.jsenv.jsdomnodejs |
10 | 10 |
|
| 11 | +import scala.annotation.tailrec |
| 12 | + |
11 | 13 | import scala.collection.immutable |
| 14 | +import scala.util.control.NonFatal |
12 | 15 |
|
13 | | -import java.io.OutputStream |
| 16 | +import java.io._ |
| 17 | +import java.nio.file.{Files, StandardCopyOption} |
| 18 | +import java.net.URI |
14 | 19 |
|
15 | 20 | import org.scalajs.io._ |
16 | 21 | import org.scalajs.io.JSUtils.escapeJS |
17 | 22 |
|
18 | 23 | import org.scalajs.jsenv._ |
19 | | -import org.scalajs.jsenv.nodejs.AbstractNodeJSEnv |
| 24 | +import org.scalajs.jsenv.nodejs._ |
20 | 25 |
|
21 | | -class JSDOMNodeJSEnv(config: JSDOMNodeJSEnv.Config) extends AbstractNodeJSEnv { |
| 26 | +class JSDOMNodeJSEnv(config: JSDOMNodeJSEnv.Config) extends JSEnv { |
22 | 27 |
|
23 | 28 | def this() = this(JSDOMNodeJSEnv.Config()) |
24 | 29 |
|
25 | | - protected def vmName: String = "Node.js with JSDOM" |
| 30 | + val name: String = "Node.js with JSDOM" |
| 31 | + |
| 32 | + def start(input: Input, runConfig: RunConfig): JSRun = { |
| 33 | + JSDOMNodeJSEnv.validator.validate(runConfig) |
| 34 | + try { |
| 35 | + internalStart(initFiles ++ codeWithJSDOMContext(input), runConfig) |
| 36 | + } catch { |
| 37 | + case NonFatal(t) => |
| 38 | + JSRun.failed(t) |
| 39 | + |
| 40 | + case t: NotImplementedError => |
| 41 | + /* In Scala 2.10.x, NotImplementedError was considered fatal. |
| 42 | + * We need this case for the conformance tests to pass on 2.10. |
| 43 | + */ |
| 44 | + JSRun.failed(t) |
| 45 | + } |
| 46 | + } |
26 | 47 |
|
27 | | - protected def executable: String = config.executable |
| 48 | + def startWithCom(input: Input, runConfig: RunConfig, |
| 49 | + onMessage: String => Unit): JSComRun = { |
| 50 | + JSDOMNodeJSEnv.validator.validate(runConfig) |
| 51 | + try { |
| 52 | + ComRun.start(runConfig, onMessage) { comLoader => |
| 53 | + val files = initFiles ::: (comLoader :: codeWithJSDOMContext(input)) |
| 54 | + internalStart(files, runConfig) |
| 55 | + } |
| 56 | + } catch { |
| 57 | + case t: NotImplementedError => |
| 58 | + /* In Scala 2.10.x, NotImplementedError was considered fatal. |
| 59 | + * We need this case for the conformance tests to pass on 2.10. |
| 60 | + * Non-fatal exceptions are already handled by ComRun.start(). |
| 61 | + */ |
| 62 | + JSComRun.failed(t) |
| 63 | + } |
| 64 | + } |
28 | 65 |
|
29 | | - override protected def args: immutable.Seq[String] = config.args |
| 66 | + private def internalStart(files: List[VirtualBinaryFile], |
| 67 | + runConfig: RunConfig): JSRun = { |
| 68 | + val command = config.executable :: config.args |
| 69 | + val externalConfig = ExternalJSRun.Config() |
| 70 | + .withEnv(env) |
| 71 | + .withRunConfig(runConfig) |
| 72 | + ExternalJSRun.start(command, externalConfig)(JSDOMNodeJSEnv.write(files)) |
| 73 | + } |
30 | 74 |
|
31 | | - override protected def env: Map[String, String] = config.env |
| 75 | + private def initFiles: List[VirtualBinaryFile] = |
| 76 | + List(JSDOMNodeJSEnv.runtimeEnv, Support.fixPercentConsole) |
32 | 77 |
|
33 | | - // TODO We might want to make this configurable - not sure why it isn't |
34 | | - override protected def wantSourceMap: Boolean = false |
| 78 | + private def env: Map[String, String] = |
| 79 | + Map("NODE_MODULE_CONTEXTS" -> "0") ++ config.env |
35 | 80 |
|
36 | | - override def jsRunner(files: Seq[VirtualJSFile]): JSRunner = |
37 | | - new DOMNodeRunner(files) |
| 81 | + private def scriptFiles(input: Input): List[VirtualBinaryFile] = input match { |
| 82 | + case Input.ScriptsToLoad(scripts) => scripts |
| 83 | + case _ => throw new UnsupportedInputException(input) |
| 84 | + } |
38 | 85 |
|
39 | | - override def asyncRunner(files: Seq[VirtualJSFile]): AsyncJSRunner = |
40 | | - new AsyncDOMNodeRunner(files) |
| 86 | + private def codeWithJSDOMContext(input: Input): List[VirtualBinaryFile] = { |
| 87 | + val scriptsURIs = scriptFiles(input).map(JSDOMNodeJSEnv.materialize(_)) |
| 88 | + val scriptsURIsAsJSStrings = |
| 89 | + scriptsURIs.map(uri => '"' + escapeJS(uri.toASCIIString) + '"') |
| 90 | + val jsDOMCode = { |
| 91 | + s""" |
| 92 | + |(function () { |
| 93 | + | var jsdom; |
| 94 | + | try { |
| 95 | + | jsdom = require("jsdom/lib/old-api.js"); // jsdom >= 10.x |
| 96 | + | } catch (e) { |
| 97 | + | jsdom = require("jsdom"); // jsdom <= 9.x |
| 98 | + | } |
| 99 | + | |
| 100 | + | var virtualConsole = jsdom.createVirtualConsole() |
| 101 | + | .sendTo(console, { omitJsdomErrors: true }); |
| 102 | + | virtualConsole.on("jsdomError", function (error) { |
| 103 | + | /* This inelegant if + console.error is the only way I found |
| 104 | + | * to make sure the stack trace of the original error is |
| 105 | + | * printed out. |
| 106 | + | */ |
| 107 | + | if (error.detail && error.detail.stack) |
| 108 | + | console.error(error.detail.stack); |
| 109 | + | |
| 110 | + | // Throw the error anew to make sure the whole execution fails |
| 111 | + | throw error; |
| 112 | + | }); |
| 113 | + | |
| 114 | + | /* Work around the fast that scalajsCom.init() should delay already |
| 115 | + | * received messages to the next tick. Here we cannot tell whether |
| 116 | + | * the receive callback is called for already received messages or |
| 117 | + | * not, so we dealy *all* messages to the next tick. |
| 118 | + | */ |
| 119 | + | var scalajsCom = global.scalajsCom; |
| 120 | + | var scalajsComWrapper = scalajsCom === (void 0) ? scalajsCom : ({ |
| 121 | + | init: function(recvCB) { |
| 122 | + | scalajsCom.init(function(msg) { |
| 123 | + | process.nextTick(recvCB, msg); |
| 124 | + | }); |
| 125 | + | }, |
| 126 | + | send: function(msg) { |
| 127 | + | scalajsCom.send(msg); |
| 128 | + | } |
| 129 | + | }); |
| 130 | + | |
| 131 | + | jsdom.env({ |
| 132 | + | html: "", |
| 133 | + | url: "http://localhost/", |
| 134 | + | virtualConsole: virtualConsole, |
| 135 | + | created: function (error, window) { |
| 136 | + | if (error == null) { |
| 137 | + | window["__ScalaJSEnv"] = __ScalaJSEnv; |
| 138 | + | window["scalajsCom"] = scalajsComWrapper; |
| 139 | + | } else { |
| 140 | + | throw error; |
| 141 | + | } |
| 142 | + | }, |
| 143 | + | scripts: [${scriptsURIsAsJSStrings.mkString(", ")}] |
| 144 | + | }); |
| 145 | + |})(); |
| 146 | + |""".stripMargin |
| 147 | + } |
| 148 | + List(MemVirtualBinaryFile.fromStringUTF8("codeWithJSDOMContext.js", jsDOMCode)) |
| 149 | + } |
| 150 | +} |
41 | 151 |
|
42 | | - override def comRunner(files: Seq[VirtualJSFile]): ComJSRunner = |
43 | | - new ComDOMNodeRunner(files) |
| 152 | +object JSDOMNodeJSEnv { |
| 153 | + private lazy val validator = ExternalJSRun.supports(RunConfig.Validator()) |
44 | 154 |
|
45 | | - protected class DOMNodeRunner(files: Seq[VirtualJSFile]) |
46 | | - extends ExtRunner(files) with AbstractDOMNodeRunner |
| 155 | + private lazy val runtimeEnv = { |
| 156 | + MemVirtualBinaryFile.fromStringUTF8("scalaJSEnvInfo.js", |
| 157 | + """ |
| 158 | + |__ScalaJSEnv = { |
| 159 | + | exitFunction: function(status) { process.exit(status); } |
| 160 | + |}; |
| 161 | + """.stripMargin |
| 162 | + ) |
| 163 | + } |
47 | 164 |
|
48 | | - protected class AsyncDOMNodeRunner(files: Seq[VirtualJSFile]) |
49 | | - extends AsyncExtRunner(files) with AbstractDOMNodeRunner |
| 165 | + // Copied from NodeJSEnv.scala upstream |
| 166 | + private def write(files: List[VirtualBinaryFile])(out: OutputStream): Unit = { |
| 167 | + val p = new PrintStream(out, false, "UTF8") |
| 168 | + try { |
| 169 | + files.foreach { |
| 170 | + case file: FileVirtualBinaryFile => |
| 171 | + val fname = file.file.getAbsolutePath |
| 172 | + p.println(s"""require("${escapeJS(fname)}");""") |
| 173 | + case f => |
| 174 | + val in = f.inputStream |
| 175 | + try { |
| 176 | + val buf = new Array[Byte](4096) |
50 | 177 |
|
51 | | - protected class ComDOMNodeRunner(files: Seq[VirtualJSFile]) |
52 | | - extends AsyncDOMNodeRunner(files) with NodeComJSRunner |
| 178 | + @tailrec |
| 179 | + def loop(): Unit = { |
| 180 | + val read = in.read(buf) |
| 181 | + if (read != -1) { |
| 182 | + p.write(buf, 0, read) |
| 183 | + loop() |
| 184 | + } |
| 185 | + } |
53 | 186 |
|
54 | | - protected trait AbstractDOMNodeRunner extends AbstractNodeRunner { |
| 187 | + loop() |
| 188 | + } finally { |
| 189 | + in.close() |
| 190 | + } |
55 | 191 |
|
56 | | - protected def codeWithJSDOMContext(): Seq[VirtualJSFile] = { |
57 | | - val scriptsPaths = getScriptsJSFiles().map { |
58 | | - case file: FileVirtualFile => file.path |
59 | | - case file => libCache.materialize(file).getAbsolutePath |
60 | | - } |
61 | | - val scriptsURIs = |
62 | | - scriptsPaths.map(path => new java.io.File(path).toURI.toASCIIString) |
63 | | - val scriptsURIsAsJSStrings = scriptsURIs.map('"' + escapeJS(_) + '"') |
64 | | - val jsDOMCode = { |
65 | | - s""" |
66 | | - |(function () { |
67 | | - | var jsdom; |
68 | | - | try { |
69 | | - | jsdom = require("jsdom/lib/old-api.js"); // jsdom >= 10.x |
70 | | - | } catch (e) { |
71 | | - | jsdom = require("jsdom"); // jsdom <= 9.x |
72 | | - | } |
73 | | - | |
74 | | - | var virtualConsole = jsdom.createVirtualConsole() |
75 | | - | .sendTo(console, { omitJsdomErrors: true }); |
76 | | - | virtualConsole.on("jsdomError", function (error) { |
77 | | - | /* This inelegant if + console.error is the only way I found |
78 | | - | * to make sure the stack trace of the original error is |
79 | | - | * printed out. |
80 | | - | */ |
81 | | - | if (error.detail && error.detail.stack) |
82 | | - | console.error(error.detail.stack); |
83 | | - | |
84 | | - | // Throw the error anew to make sure the whole execution fails |
85 | | - | throw error; |
86 | | - | }); |
87 | | - | |
88 | | - | jsdom.env({ |
89 | | - | html: "", |
90 | | - | url: "http://localhost/", |
91 | | - | virtualConsole: virtualConsole, |
92 | | - | created: function (error, window) { |
93 | | - | if (error == null) { |
94 | | - | window["__ScalaJSEnv"] = __ScalaJSEnv; |
95 | | - | window["scalajsCom"] = global.scalajsCom; |
96 | | - | } else { |
97 | | - | throw error; |
98 | | - | } |
99 | | - | }, |
100 | | - | scripts: [${scriptsURIsAsJSStrings.mkString(", ")}] |
101 | | - | }); |
102 | | - |})(); |
103 | | - |""".stripMargin |
| 192 | + p.println() |
104 | 193 | } |
105 | | - Seq(new MemVirtualJSFile("codeWithJSDOMContext.js").withContent(jsDOMCode)) |
| 194 | + } finally { |
| 195 | + p.close() |
106 | 196 | } |
| 197 | + } |
107 | 198 |
|
108 | | - /** All the JS files that are passed to the VM. |
109 | | - * |
110 | | - * This method can overridden to provide custom behavior in subclasses. |
111 | | - * |
112 | | - * This method is overridden in `JSDOMNodeJSEnv` so that user-provided |
113 | | - * JS files (excluding "init" files) are executed as *scripts* within the |
114 | | - * jsdom environment, rather than being directly executed by the VM. |
115 | | - * |
116 | | - * The value returned by this method in `JSDOMNodeJSEnv` is |
117 | | - * `initFiles() ++ customInitFiles() ++ codeWithJSDOMContext()`. |
118 | | - */ |
119 | | - override protected def getJSFiles(): Seq[VirtualJSFile] = |
120 | | - initFiles() ++ customInitFiles() ++ codeWithJSDOMContext() |
| 199 | + // tmpSuffixRE and tmpFile copied from HTMLRunnerBuilder.scala in Scala.js |
121 | 200 |
|
122 | | - /** JS files to be loaded via scripts in the jsdom environment. |
123 | | - * |
124 | | - * This method can be overridden to provide a different list of scripts. |
125 | | - * |
126 | | - * The default value in `JSDOMNodeJSEnv` is `files`. |
127 | | - */ |
128 | | - protected def getScriptsJSFiles(): Seq[VirtualJSFile] = |
129 | | - files |
130 | | - |
131 | | - // Send code to Stdin |
132 | | - override protected def sendVMStdin(out: OutputStream): Unit = { |
133 | | - /* Do not factor this method out into AbstractNodeRunner or when mixin in |
134 | | - * the traits it would use AbstractExtRunner.sendVMStdin due to |
135 | | - * linearization order. |
| 201 | + private val tmpSuffixRE = """[a-zA-Z0-9-_.]*$""".r |
| 202 | + |
| 203 | + private def tmpFile(path: String, in: InputStream): URI = { |
| 204 | + try { |
| 205 | + /* - createTempFile requires a prefix of at least 3 chars |
| 206 | + * - we use a safe part of the path as suffix so the extension stays (some |
| 207 | + * browsers need that) and there is a clue which file it came from. |
136 | 208 | */ |
137 | | - sendJS(getJSFiles(), out) |
| 209 | + val suffix = tmpSuffixRE.findFirstIn(path).orNull |
| 210 | + |
| 211 | + val f = File.createTempFile("tmp-", suffix) |
| 212 | + f.deleteOnExit() |
| 213 | + Files.copy(in, f.toPath(), StandardCopyOption.REPLACE_EXISTING) |
| 214 | + f.toURI() |
| 215 | + } finally { |
| 216 | + in.close() |
| 217 | + } |
| 218 | + } |
| 219 | + |
| 220 | + private def materialize(file: VirtualBinaryFile): URI = { |
| 221 | + file match { |
| 222 | + case file: FileVirtualFile => file.file.toURI |
| 223 | + case file => tmpFile(file.path, file.inputStream) |
138 | 224 | } |
139 | 225 | } |
140 | | -} |
141 | 226 |
|
142 | | -object JSDOMNodeJSEnv { |
143 | 227 | final class Config private ( |
144 | 228 | val executable: String, |
145 | 229 | val args: List[String], |
|
0 commit comments