Skip to content

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

Merged
merged 1 commit into from
Jul 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ language: scala
scala:
- 2.10.7
- 2.11.12
- 2.12.4
- 2.12.6
jdk:
- oraclejdk8
env:
Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ inThisBuild(Seq(
version := "1.0.0-SNAPSHOT",
organization := "org.scala-js",

crossScalaVersions := Seq("2.12.4", "2.10.7", "2.11.12"),
crossScalaVersions := Seq("2.12.6", "2.10.7", "2.11.12"),
scalaVersion := crossScalaVersions.value.head,
scalacOptions ++= Seq("-deprecation", "-feature", "-Xfatal-warnings"),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member Author

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 considered NonFatal by 2.10.x, really.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed :-/

}
}

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);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure this should be integrated inside ComSupport.scala upstream.

If I don't do this, I get an IllegalStateException because the message for JSEndPoints.detectFrameworks is delivered before TestAdapterBridge.start() gets to setup the handler. Indeed, the messages are delivered during the constructor of TestAdapterBridge, through the constructor of JSRPC.

Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Member Author

Choose a reason for hiding this comment

The 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 start(), so the network messages haven't had a chance to be processed by a JS event handler, so they're not yet in the message buffer in JS. They're still in a C++ buffer somewhere. In the jsdom env, there is a come back to the event loop before this code is executed.

It took me a while to figure it out ^^

Copy link
Contributor

Choose a reason for hiding this comment

The 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 ComSupport.

| });
| },
| 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],
Expand Down
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()
}
}

}
Loading