From 9403c961b6cebe491a9d5feb9f495a13da305ad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Wed, 4 Jul 2018 15:22:39 +0200 Subject: [PATCH] Upgrade to Scala.js 1.0.0-M5. --- .travis.yml | 2 +- build.sbt | 2 +- .../jsenv/jsdomnodejs/JSDOMNodeJSEnv.scala | 286 +++++++++++------- .../jsdomnodejs/JSDOMNodeJSEnvTest.scala | 43 ++- .../jsenv/jsdomnodejs/JSDOMNodeJSSuite.scala | 15 + project/build.properties | 2 +- project/plugins.sbt | 4 +- 7 files changed, 236 insertions(+), 118 deletions(-) create mode 100644 jsdom-nodejs-env/src/test/scala/org/scalajs/jsenv/jsdomnodejs/JSDOMNodeJSSuite.scala diff --git a/.travis.yml b/.travis.yml index 10ba93c..3587cd4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: scala scala: - 2.10.7 - 2.11.12 - - 2.12.4 + - 2.12.6 jdk: - oraclejdk8 env: diff --git a/build.sbt b/build.sbt index 01bb3a3..35fd313 100644 --- a/build.sbt +++ b/build.sbt @@ -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"), diff --git a/jsdom-nodejs-env/src/main/scala/org/scalajs/jsenv/jsdomnodejs/JSDOMNodeJSEnv.scala b/jsdom-nodejs-env/src/main/scala/org/scalajs/jsenv/jsdomnodejs/JSDOMNodeJSEnv.scala index 9b7c972..1cf6090 100644 --- a/jsdom-nodejs-env/src/main/scala/org/scalajs/jsenv/jsdomnodejs/JSDOMNodeJSEnv.scala +++ b/jsdom-nodejs-env/src/main/scala/org/scalajs/jsenv/jsdomnodejs/JSDOMNodeJSEnv.scala @@ -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); + | }); + | }, + | 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], diff --git a/jsdom-nodejs-env/src/test/scala/org/scalajs/jsenv/jsdomnodejs/JSDOMNodeJSEnvTest.scala b/jsdom-nodejs-env/src/test/scala/org/scalajs/jsenv/jsdomnodejs/JSDOMNodeJSEnvTest.scala index 0957c6e..cd7a722 100644 --- a/jsdom-nodejs-env/src/test/scala/org/scalajs/jsenv/jsdomnodejs/JSDOMNodeJSEnvTest.scala +++ b/jsdom-nodejs-env/src/test/scala/org/scalajs/jsenv/jsdomnodejs/JSDOMNodeJSEnvTest.scala @@ -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() + } } } diff --git a/jsdom-nodejs-env/src/test/scala/org/scalajs/jsenv/jsdomnodejs/JSDOMNodeJSSuite.scala b/jsdom-nodejs-env/src/test/scala/org/scalajs/jsenv/jsdomnodejs/JSDOMNodeJSSuite.scala new file mode 100644 index 0000000..b06fa90 --- /dev/null +++ b/jsdom-nodejs-env/src/test/scala/org/scalajs/jsenv/jsdomnodejs/JSDOMNodeJSSuite.scala @@ -0,0 +1,15 @@ +package org.scalajs.jsenv.jsdomnodejs + +import org.scalajs.jsenv.test._ + +import org.junit.runner.RunWith + +@RunWith(classOf[JSEnvSuiteRunner]) +class JSDOMNodeJSSuite extends JSEnvSuite(JSDOMNodeJSSuite.Config) + +object JSDOMNodeJSSuite { + val Config = { + JSEnvSuiteConfig(new JSDOMNodeJSEnv) + .withTerminateVMJSCode("__ScalaJSEnv.exitFunction(0)") + } +} diff --git a/project/build.properties b/project/build.properties index c091b86..133a8f1 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.16 +sbt.version=0.13.17 diff --git a/project/plugins.sbt b/project/plugins.sbt index 2b5298d..de146d8 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,6 +1,6 @@ -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.0.0-M3") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.0.0-M5") -libraryDependencies += "org.scala-js" %% "scalajs-env-nodejs" % "1.0.0-M3" +libraryDependencies += "org.scala-js" %% "scalajs-env-nodejs" % "1.0.0-M5" unmanagedSourceDirectories in Compile += baseDirectory.value.getParentFile / "jsdom-nodejs-env/src/main/scala"