|
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