-
Notifications
You must be signed in to change notification settings - Fork 10
Upgrade to Scala.js 1.0.0-M5. #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,7 +3,7 @@ language: scala | |
scala: | ||
- 2.10.7 | ||
- 2.11.12 | ||
- 2.12.4 | ||
- 2.12.6 | ||
jdk: | ||
- oraclejdk8 | ||
env: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,138 +8,222 @@ | |
|
||
package org.scalajs.jsenv.jsdomnodejs | ||
|
||
import scala.annotation.tailrec | ||
|
||
import scala.collection.immutable | ||
import scala.util.control.NonFatal | ||
|
||
import java.io.OutputStream | ||
import java.io._ | ||
import java.nio.file.{Files, StandardCopyOption} | ||
import java.net.URI | ||
|
||
import org.scalajs.io._ | ||
import org.scalajs.io.JSUtils.escapeJS | ||
|
||
import org.scalajs.jsenv._ | ||
import org.scalajs.jsenv.nodejs.AbstractNodeJSEnv | ||
import org.scalajs.jsenv.nodejs._ | ||
|
||
class JSDOMNodeJSEnv(config: JSDOMNodeJSEnv.Config) extends AbstractNodeJSEnv { | ||
class JSDOMNodeJSEnv(config: JSDOMNodeJSEnv.Config) extends JSEnv { | ||
|
||
def this() = this(JSDOMNodeJSEnv.Config()) | ||
|
||
protected def vmName: String = "Node.js with JSDOM" | ||
val name: String = "Node.js with JSDOM" | ||
|
||
def start(input: Input, runConfig: RunConfig): JSRun = { | ||
JSDOMNodeJSEnv.validator.validate(runConfig) | ||
try { | ||
internalStart(initFiles ++ codeWithJSDOMContext(input), runConfig) | ||
} catch { | ||
case NonFatal(t) => | ||
JSRun.failed(t) | ||
|
||
case t: NotImplementedError => | ||
/* In Scala 2.10.x, NotImplementedError was considered fatal. | ||
* We need this case for the conformance tests to pass on 2.10. | ||
*/ | ||
JSRun.failed(t) | ||
} | ||
} | ||
|
||
protected def executable: String = config.executable | ||
def startWithCom(input: Input, runConfig: RunConfig, | ||
onMessage: String => Unit): JSComRun = { | ||
JSDOMNodeJSEnv.validator.validate(runConfig) | ||
try { | ||
ComRun.start(runConfig, onMessage) { comLoader => | ||
val files = initFiles ::: (comLoader :: codeWithJSDOMContext(input)) | ||
internalStart(files, runConfig) | ||
} | ||
} catch { | ||
case t: NotImplementedError => | ||
/* In Scala 2.10.x, NotImplementedError was considered fatal. | ||
* We need this case for the conformance tests to pass on 2.10. | ||
* Non-fatal exceptions are already handled by ComRun.start(). | ||
*/ | ||
JSComRun.failed(t) | ||
} | ||
} | ||
|
||
override protected def args: immutable.Seq[String] = config.args | ||
private def internalStart(files: List[VirtualBinaryFile], | ||
runConfig: RunConfig): JSRun = { | ||
val command = config.executable :: config.args | ||
val externalConfig = ExternalJSRun.Config() | ||
.withEnv(env) | ||
.withRunConfig(runConfig) | ||
ExternalJSRun.start(command, externalConfig)(JSDOMNodeJSEnv.write(files)) | ||
} | ||
|
||
override protected def env: Map[String, String] = config.env | ||
private def initFiles: List[VirtualBinaryFile] = | ||
List(JSDOMNodeJSEnv.runtimeEnv, Support.fixPercentConsole) | ||
|
||
// TODO We might want to make this configurable - not sure why it isn't | ||
override protected def wantSourceMap: Boolean = false | ||
private def env: Map[String, String] = | ||
Map("NODE_MODULE_CONTEXTS" -> "0") ++ config.env | ||
|
||
override def jsRunner(files: Seq[VirtualJSFile]): JSRunner = | ||
new DOMNodeRunner(files) | ||
private def scriptFiles(input: Input): List[VirtualBinaryFile] = input match { | ||
case Input.ScriptsToLoad(scripts) => scripts | ||
case _ => throw new UnsupportedInputException(input) | ||
} | ||
|
||
override def asyncRunner(files: Seq[VirtualJSFile]): AsyncJSRunner = | ||
new AsyncDOMNodeRunner(files) | ||
private def codeWithJSDOMContext(input: Input): List[VirtualBinaryFile] = { | ||
val scriptsURIs = scriptFiles(input).map(JSDOMNodeJSEnv.materialize(_)) | ||
val scriptsURIsAsJSStrings = | ||
scriptsURIs.map(uri => '"' + escapeJS(uri.toASCIIString) + '"') | ||
val jsDOMCode = { | ||
s""" | ||
|(function () { | ||
| var jsdom; | ||
| try { | ||
| jsdom = require("jsdom/lib/old-api.js"); // jsdom >= 10.x | ||
| } catch (e) { | ||
| jsdom = require("jsdom"); // jsdom <= 9.x | ||
| } | ||
| | ||
| var virtualConsole = jsdom.createVirtualConsole() | ||
| .sendTo(console, { omitJsdomErrors: true }); | ||
| virtualConsole.on("jsdomError", function (error) { | ||
| /* This inelegant if + console.error is the only way I found | ||
| * to make sure the stack trace of the original error is | ||
| * printed out. | ||
| */ | ||
| if (error.detail && error.detail.stack) | ||
| console.error(error.detail.stack); | ||
| | ||
| // Throw the error anew to make sure the whole execution fails | ||
| throw error; | ||
| }); | ||
| | ||
| /* Work around the fast that scalajsCom.init() should delay already | ||
| * received messages to the next tick. Here we cannot tell whether | ||
| * the receive callback is called for already received messages or | ||
| * not, so we dealy *all* messages to the next tick. | ||
| */ | ||
| var scalajsCom = global.scalajsCom; | ||
| var scalajsComWrapper = scalajsCom === (void 0) ? scalajsCom : ({ | ||
| init: function(recvCB) { | ||
| scalajsCom.init(function(msg) { | ||
| process.nextTick(recvCB, msg); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm pretty sure this should be integrated inside If I don't do this, I get an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm... Yes, but why does this not happen in the base env :-S There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because in the base Node.js env, there is no return to the event loop between the startup and It took me a while to figure it out ^^ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, ah, I see. Fair enough. So yes, it should definitely be moved to |
||
| }); | ||
| }, | ||
| send: function(msg) { | ||
| scalajsCom.send(msg); | ||
| } | ||
| }); | ||
| | ||
| jsdom.env({ | ||
| html: "", | ||
| url: "http://localhost/", | ||
| virtualConsole: virtualConsole, | ||
| created: function (error, window) { | ||
| if (error == null) { | ||
| window["__ScalaJSEnv"] = __ScalaJSEnv; | ||
| window["scalajsCom"] = scalajsComWrapper; | ||
| } else { | ||
| throw error; | ||
| } | ||
| }, | ||
| scripts: [${scriptsURIsAsJSStrings.mkString(", ")}] | ||
| }); | ||
|})(); | ||
|""".stripMargin | ||
} | ||
List(MemVirtualBinaryFile.fromStringUTF8("codeWithJSDOMContext.js", jsDOMCode)) | ||
} | ||
} | ||
|
||
override def comRunner(files: Seq[VirtualJSFile]): ComJSRunner = | ||
new ComDOMNodeRunner(files) | ||
object JSDOMNodeJSEnv { | ||
private lazy val validator = ExternalJSRun.supports(RunConfig.Validator()) | ||
|
||
protected class DOMNodeRunner(files: Seq[VirtualJSFile]) | ||
extends ExtRunner(files) with AbstractDOMNodeRunner | ||
private lazy val runtimeEnv = { | ||
MemVirtualBinaryFile.fromStringUTF8("scalaJSEnvInfo.js", | ||
""" | ||
|__ScalaJSEnv = { | ||
| exitFunction: function(status) { process.exit(status); } | ||
|}; | ||
""".stripMargin | ||
) | ||
} | ||
|
||
protected class AsyncDOMNodeRunner(files: Seq[VirtualJSFile]) | ||
extends AsyncExtRunner(files) with AbstractDOMNodeRunner | ||
// Copied from NodeJSEnv.scala upstream | ||
private def write(files: List[VirtualBinaryFile])(out: OutputStream): Unit = { | ||
val p = new PrintStream(out, false, "UTF8") | ||
try { | ||
files.foreach { | ||
case file: FileVirtualBinaryFile => | ||
val fname = file.file.getAbsolutePath | ||
p.println(s"""require("${escapeJS(fname)}");""") | ||
case f => | ||
val in = f.inputStream | ||
try { | ||
val buf = new Array[Byte](4096) | ||
|
||
protected class ComDOMNodeRunner(files: Seq[VirtualJSFile]) | ||
extends AsyncDOMNodeRunner(files) with NodeComJSRunner | ||
@tailrec | ||
def loop(): Unit = { | ||
val read = in.read(buf) | ||
if (read != -1) { | ||
p.write(buf, 0, read) | ||
loop() | ||
} | ||
} | ||
|
||
protected trait AbstractDOMNodeRunner extends AbstractNodeRunner { | ||
loop() | ||
} finally { | ||
in.close() | ||
} | ||
|
||
protected def codeWithJSDOMContext(): Seq[VirtualJSFile] = { | ||
val scriptsPaths = getScriptsJSFiles().map { | ||
case file: FileVirtualFile => file.path | ||
case file => libCache.materialize(file).getAbsolutePath | ||
} | ||
val scriptsURIs = | ||
scriptsPaths.map(path => new java.io.File(path).toURI.toASCIIString) | ||
val scriptsURIsAsJSStrings = scriptsURIs.map('"' + escapeJS(_) + '"') | ||
val jsDOMCode = { | ||
s""" | ||
|(function () { | ||
| var jsdom; | ||
| try { | ||
| jsdom = require("jsdom/lib/old-api.js"); // jsdom >= 10.x | ||
| } catch (e) { | ||
| jsdom = require("jsdom"); // jsdom <= 9.x | ||
| } | ||
| | ||
| var virtualConsole = jsdom.createVirtualConsole() | ||
| .sendTo(console, { omitJsdomErrors: true }); | ||
| virtualConsole.on("jsdomError", function (error) { | ||
| /* This inelegant if + console.error is the only way I found | ||
| * to make sure the stack trace of the original error is | ||
| * printed out. | ||
| */ | ||
| if (error.detail && error.detail.stack) | ||
| console.error(error.detail.stack); | ||
| | ||
| // Throw the error anew to make sure the whole execution fails | ||
| throw error; | ||
| }); | ||
| | ||
| jsdom.env({ | ||
| html: "", | ||
| url: "http://localhost/", | ||
| virtualConsole: virtualConsole, | ||
| created: function (error, window) { | ||
| if (error == null) { | ||
| window["__ScalaJSEnv"] = __ScalaJSEnv; | ||
| window["scalajsCom"] = global.scalajsCom; | ||
| } else { | ||
| throw error; | ||
| } | ||
| }, | ||
| scripts: [${scriptsURIsAsJSStrings.mkString(", ")}] | ||
| }); | ||
|})(); | ||
|""".stripMargin | ||
p.println() | ||
} | ||
Seq(new MemVirtualJSFile("codeWithJSDOMContext.js").withContent(jsDOMCode)) | ||
} finally { | ||
p.close() | ||
} | ||
} | ||
|
||
/** All the JS files that are passed to the VM. | ||
* | ||
* This method can overridden to provide custom behavior in subclasses. | ||
* | ||
* This method is overridden in `JSDOMNodeJSEnv` so that user-provided | ||
* JS files (excluding "init" files) are executed as *scripts* within the | ||
* jsdom environment, rather than being directly executed by the VM. | ||
* | ||
* The value returned by this method in `JSDOMNodeJSEnv` is | ||
* `initFiles() ++ customInitFiles() ++ codeWithJSDOMContext()`. | ||
*/ | ||
override protected def getJSFiles(): Seq[VirtualJSFile] = | ||
initFiles() ++ customInitFiles() ++ codeWithJSDOMContext() | ||
// tmpSuffixRE and tmpFile copied from HTMLRunnerBuilder.scala in Scala.js | ||
|
||
/** JS files to be loaded via scripts in the jsdom environment. | ||
* | ||
* This method can be overridden to provide a different list of scripts. | ||
* | ||
* The default value in `JSDOMNodeJSEnv` is `files`. | ||
*/ | ||
protected def getScriptsJSFiles(): Seq[VirtualJSFile] = | ||
files | ||
|
||
// Send code to Stdin | ||
override protected def sendVMStdin(out: OutputStream): Unit = { | ||
/* Do not factor this method out into AbstractNodeRunner or when mixin in | ||
* the traits it would use AbstractExtRunner.sendVMStdin due to | ||
* linearization order. | ||
private val tmpSuffixRE = """[a-zA-Z0-9-_.]*$""".r | ||
|
||
private def tmpFile(path: String, in: InputStream): URI = { | ||
try { | ||
/* - createTempFile requires a prefix of at least 3 chars | ||
* - we use a safe part of the path as suffix so the extension stays (some | ||
* browsers need that) and there is a clue which file it came from. | ||
*/ | ||
sendJS(getJSFiles(), out) | ||
val suffix = tmpSuffixRE.findFirstIn(path).orNull | ||
|
||
val f = File.createTempFile("tmp-", suffix) | ||
f.deleteOnExit() | ||
Files.copy(in, f.toPath(), StandardCopyOption.REPLACE_EXISTING) | ||
f.toURI() | ||
} finally { | ||
in.close() | ||
} | ||
} | ||
|
||
private def materialize(file: VirtualBinaryFile): URI = { | ||
file match { | ||
case file: FileVirtualFile => file.file.toURI | ||
case file => tmpFile(file.path, file.inputStream) | ||
} | ||
} | ||
} | ||
|
||
object JSDOMNodeJSEnv { | ||
final class Config private ( | ||
val executable: String, | ||
val args: List[String], | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,42 @@ | ||
package org.scalajs.jsenv.jsdomnodejs | ||
|
||
import org.scalajs.jsenv.test._ | ||
import scala.concurrent.Await | ||
|
||
import org.junit.Test | ||
import org.junit.Assert._ | ||
|
||
class JSDOMNodeJSEnvTest extends TimeoutComTests { | ||
import org.scalajs.io._ | ||
|
||
protected def newJSEnv: JSDOMNodeJSEnv = new JSDOMNodeJSEnv() | ||
import org.scalajs.jsenv._ | ||
|
||
class JSDOMNodeJSEnvTest { | ||
|
||
private val TestRunConfig = { | ||
RunConfig() | ||
.withInheritOut(false) | ||
.withOnOutputStream((_, _) => ()) // ignore stdout | ||
} | ||
|
||
private val config = JSDOMNodeJSSuite.Config | ||
|
||
@Test | ||
def historyAPI: Unit = { | ||
"""|console.log(window.location.href); | ||
|window.history.pushState({}, "", "/foo"); | ||
|console.log(window.location.href); | ||
""".stripMargin hasOutput | ||
"""|http://localhost/ | ||
|http://localhost/foo | ||
|""".stripMargin | ||
def historyAPIWithoutTestKit: Unit = { | ||
assertRunSucceeds( | ||
""" | ||
|console.log(window.location.href); | ||
|window.history.pushState({}, "", "/foo"); | ||
|console.log(window.location.href); | ||
""".stripMargin) | ||
} | ||
|
||
private def assertRunSucceeds(inputStr: String): Unit = { | ||
val inputFile = MemVirtualBinaryFile.fromStringUTF8("test.js", inputStr) | ||
val input = Input.ScriptsToLoad(List(inputFile)) | ||
val run = config.jsEnv.start(input, TestRunConfig) | ||
try { | ||
Await.result(run.future, config.awaitTimeout) | ||
} finally { | ||
run.close() | ||
} | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given scala/scala@68f62d7, perhaps the conformance tests should use a different exception than
NotImplementedError
. Anything else that would be consideredNonFatal
by 2.10.x, really.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed :-/