diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..dbece55 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,41 @@ +sudo: false +language: scala +scala: + - 2.10.6 + - 2.11.11 + - 2.12.2 +jdk: + - oraclejdk8 +env: + - JSDOM_VERSION=9.12.0 + - JSDOM_VERSION=10.0.0 +install: + # The default ivy resolution takes way too much time, and times out Travis builds. + # We use coursier instead. + - mkdir -p $HOME/.sbt/0.13/plugins/ + - echo 'addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-RC3")' > $HOME/.sbt/0.13/plugins/coursier.sbt + # We need a recent version of Node.js for jsdom + - nvm install 6 + - nvm use 6 + - node --version + # Of course we need jsdom + - npm install jsdom@$JSDOM_VERSION + # While there is no published version of Scala.js 1.x, we have to locally build a snapshot. + - git clone https://github.com/scala-js/scala-js.git + - cd scala-js + - git checkout d25fa8bba708977c68c093fdbc50958368f9602f + - sbt ++$TRAVIS_SCALA_VERSION compiler/publishLocal jUnitPlugin/publishLocal library/publishLocal testInterface/publishLocal jUnitRuntime/publishLocal ir/publishLocal tools/publishLocal jsEnvs/publishLocal jsEnvsTestKit/publishLocal nodeJSEnv/publishLocal + - sbt ++2.10.6 ir/publishLocal tools/publishLocal jsEnvs/publishLocal nodeJSEnv/publishLocal testAdapter/publishLocal sbtPlugin/publishLocal + - cd .. +script: + - sbt ++$TRAVIS_SCALA_VERSION scalajs-env-jsdom-nodejs/test scalajs-env-jsdom-nodejs/doc + - sbt ++$TRAVIS_SCALA_VERSION test-project/run test-project/test +cache: + directories: + - $HOME/.ivy2/cache + - $HOME/.sbt + - $HOME/.coursier/cache +before_cache: + - find $HOME/.ivy2/cache -name "ivydata-*.properties" -print -delete + - find $HOME/.sbt -name "*.lock" -print -delete + - rm $HOME/.sbt/0.13/plugins/coursier.sbt diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..511eb75 --- /dev/null +++ b/build.sbt @@ -0,0 +1,88 @@ +inThisBuild(Seq( + version := "0.1.0-SNAPSHOT", + organization := "org.scala-js", + + crossScalaVersions := Seq("2.10.6", "2.11.11", "2.12.2"), + scalaVersion := "2.11.11", + scalacOptions ++= Seq("-deprecation", "-feature", "-Xfatal-warnings"), + + homepage := Some(url("https://www.scala-js.org/")), + licenses += ("BSD New", + url("https://github.com/scala-js/scala-js-env-jsdom-nodejs/blob/master/LICENSE")), + scmInfo := Some(ScmInfo( + url("https://github.com/scala-js/scala-js-env-jsdom-nodejs"), + "scm:git:git@github.com:scala-js/scala-js-env-jsdom-nodejs.git", + Some("scm:git:git@github.com:scala-js/scala-js-env-jsdom-nodejs.git"))) +)) + +val commonSettings = Def.settings( + // Scaladoc linking + apiURL := { + val name = moduleName.value + val v = version.value + Some(url(s"https://www.scala-js.org/api/$name/$v/")) + }, + autoAPIMappings := true, + + publishMavenStyle := true, + publishTo := { + val nexus = "https://oss.sonatype.org/" + if (isSnapshot.value) + Some("snapshots" at nexus + "content/repositories/snapshots") + else + Some("releases" at nexus + "service/local/staging/deploy/maven2") + }, + pomExtra := ( + + + sjrd + Sébastien Doeraene + https://github.com/sjrd/ + + + gzm0 + Tobias Schlatter + https://github.com/gzm0/ + + + nicolasstucki + Nicolas Stucki + https://github.com/nicolasstucki/ + + + ), + pomIncludeRepository := { _ => false } +) + +lazy val root: Project = project.in(file(".")). + settings( + publishArtifact in Compile := false, + publish := {}, + publishLocal := {}, + + clean := clean.dependsOn( + clean in `scalajs-env-jsdom-nodejs`, + clean in `test-project` + ).value + ) + +lazy val `scalajs-env-jsdom-nodejs`: Project = project.in(file("jsdom-nodejs-env")). + settings( + commonSettings, + + libraryDependencies ++= Seq( + "org.scala-js" %% "scalajs-js-envs" % scalaJSVersion, + "org.scala-js" %% "scalajs-nodejs-env" % scalaJSVersion, + + "com.novocode" % "junit-interface" % "0.11" % "test", + "org.scala-js" %% "scalajs-js-envs-test-kit" % scalaJSVersion % "test" + ) + ) + +lazy val `test-project`: Project = project. + enablePlugins(ScalaJSPlugin). + enablePlugins(ScalaJSJUnitPlugin). + settings( + scalaJSUseMainModuleInitializer := true, + jsEnv := new org.scalajs.jsenv.jsdomnodejs.JSDOMNodeJSEnv() + ) 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 new file mode 100644 index 0000000..ac6fbab --- /dev/null +++ b/jsdom-nodejs-env/src/main/scala/org/scalajs/jsenv/jsdomnodejs/JSDOMNodeJSEnv.scala @@ -0,0 +1,185 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js JS envs ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2017, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + +package org.scalajs.jsenv.jsdomnodejs + +import scala.collection.immutable + +import java.io.OutputStream + +import org.scalajs.core.tools.io._ +import org.scalajs.jsenv._ +import org.scalajs.jsenv.nodejs.AbstractNodeJSEnv + +import org.scalajs.core.ir.Utils.escapeJS + +class JSDOMNodeJSEnv(config: JSDOMNodeJSEnv.Config) extends AbstractNodeJSEnv { + + def this() = this(JSDOMNodeJSEnv.Config()) + + protected def vmName: String = "Node.js with JSDOM" + + protected def executable: String = config.executable + + override protected def args: immutable.Seq[String] = config.args + + override protected def env: Map[String, String] = config.env + + // TODO We might want to make this configurable - not sure why it isn't + override protected def wantSourceMap: Boolean = false + + override def jsRunner(files: Seq[VirtualJSFile]): JSRunner = + new DOMNodeRunner(files) + + override def asyncRunner(files: Seq[VirtualJSFile]): AsyncJSRunner = + new AsyncDOMNodeRunner(files) + + override def comRunner(files: Seq[VirtualJSFile]): ComJSRunner = + new ComDOMNodeRunner(files) + + protected class DOMNodeRunner(files: Seq[VirtualJSFile]) + extends ExtRunner(files) with AbstractDOMNodeRunner + + protected class AsyncDOMNodeRunner(files: Seq[VirtualJSFile]) + extends AsyncExtRunner(files) with AbstractDOMNodeRunner + + protected class ComDOMNodeRunner(files: Seq[VirtualJSFile]) + extends AsyncDOMNodeRunner(files) with NodeComJSRunner + + protected trait AbstractDOMNodeRunner extends AbstractNodeRunner { + + 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 + } + Seq(new MemVirtualJSFile("codeWithJSDOMContext.js").withContent(jsDOMCode)) + } + + /** 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() + + /** 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. + */ + sendJS(getJSFiles(), out) + } + } +} + +object JSDOMNodeJSEnv { + final class Config private ( + val executable: String, + val args: List[String], + val env: Map[String, String] + ) { + private def this() = { + this( + executable = "node", + args = Nil, + env = Map.empty + ) + } + + def withExecutable(executable: String): Config = + copy(executable = executable) + + def withArgs(args: List[String]): Config = + copy(args = args) + + def withEnv(env: Map[String, String]): Config = + copy(env = env) + + private def copy( + executable: String = executable, + args: List[String] = args, + env: Map[String, String] = env + ): Config = { + new Config(executable, args, env) + } + } + + object Config { + /** Returns a default configuration for a [[JSDOMNodeJSEnv]]. + * + * The defaults are: + * + * - `executable`: `"node"` + * - `args`: `Nil` + * - `env`: `Map.empty` + */ + def apply(): Config = new Config() + } +} 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 new file mode 100644 index 0000000..0957c6e --- /dev/null +++ b/jsdom-nodejs-env/src/test/scala/org/scalajs/jsenv/jsdomnodejs/JSDOMNodeJSEnvTest.scala @@ -0,0 +1,23 @@ +package org.scalajs.jsenv.jsdomnodejs + +import org.scalajs.jsenv.test._ + +import org.junit.Test +import org.junit.Assert._ + +class JSDOMNodeJSEnvTest extends TimeoutComTests { + + protected def newJSEnv: JSDOMNodeJSEnv = new JSDOMNodeJSEnv() + + @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 + } + +} diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..64317fd --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=0.13.15 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..ab8517d --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,6 @@ +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.0.0-SNAPSHOT") + +libraryDependencies += "org.scala-js" %% "scalajs-nodejs-env" % "1.0.0-SNAPSHOT" + +unmanagedSourceDirectories in Compile += + baseDirectory.value.getParentFile / "jsdom-nodejs-env/src/main/scala" diff --git a/test-project/src/main/scala/testproject/Lib.scala b/test-project/src/main/scala/testproject/Lib.scala new file mode 100644 index 0000000..e6d0913 --- /dev/null +++ b/test-project/src/main/scala/testproject/Lib.scala @@ -0,0 +1,19 @@ +package testproject + +import scala.scalajs.js + +object Lib { + + val document: js.Dynamic = js.Dynamic.global.document + + def getElementsByTagName(name: String): js.Array[js.Dynamic] = + document.getElementsByTagName(name).asInstanceOf[js.Array[js.Dynamic]] + + /** appends a

with the message to the document */ + def appendDocument(msg: String): Unit = { + val elem = document.createElement("p") + elem.appendChild(document.createTextNode(msg)) + document.body.appendChild(elem) + } + +} diff --git a/test-project/src/main/scala/testproject/TestApp.scala b/test-project/src/main/scala/testproject/TestApp.scala new file mode 100644 index 0000000..9c17e1f --- /dev/null +++ b/test-project/src/main/scala/testproject/TestApp.scala @@ -0,0 +1,12 @@ +package testproject + +object TestApp { + + def main(args: Array[String]): Unit = { + Lib.appendDocument("Hello World") + Lib.appendDocument("Still Here!") + + println(Lib.getElementsByTagName("p").head.innerHTML) + } + +} diff --git a/test-project/src/test/scala/testproject/LibTest.scala b/test-project/src/test/scala/testproject/LibTest.scala new file mode 100644 index 0000000..1bbe022 --- /dev/null +++ b/test-project/src/test/scala/testproject/LibTest.scala @@ -0,0 +1,16 @@ +package testproject + +import scala.scalajs.js + +import org.junit.Test +import org.junit.Assert._ + +class LibTest { + @Test def dummy_library_should_append_an_element(): Unit = { + def count = Lib.getElementsByTagName("p").length + + val oldCount = count + Lib.appendDocument("foo") + assertEquals(1, count - oldCount) + } +}