summaryrefslogtreecommitdiff
path: root/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin
diff options
context:
space:
mode:
Diffstat (limited to 'examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin')
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/AbstractJSDeps.scala81
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/Implicits.scala34
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/JSUtils.scala35
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/LoggerJSConsole.scala18
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/OptimizerOptions.scala74
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/ScalaJSCrossVersion.scala48
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/ScalaJSPlugin.scala179
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/ScalaJSPluginInternal.scala598
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/Stage.scala18
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala200
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/VirtualFileMaterializer.scala67
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala306
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/JettyWebsocketManager.scala126
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/PhantomJSEnv.scala466
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/PhantomJettyClassLoader.scala63
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/WebsocketListener.scala10
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/WebsocketManager.scala10
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/LazyScalaJSScope.scala96
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/RhinoJSEnv.scala303
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/ScalaJSCoreLib.scala173
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/package.scala42
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/impl/DependencyBuilders.scala99
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/Events.scala35
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/JSClasspathLoader.scala15
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/SbtTestLoggerAccWrapper.scala22
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestException.scala9
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestFramework.scala52
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestOutputConsole.scala190
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestRunner.scala37
-rw-r--r--examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestTask.scala110
30 files changed, 3516 insertions, 0 deletions
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/AbstractJSDeps.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/AbstractJSDeps.scala
new file mode 100644
index 0000000..9eb7f69
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/AbstractJSDeps.scala
@@ -0,0 +1,81 @@
+package scala.scalajs.sbtplugin
+
+import sbt._
+
+import StringUtilities.nonEmpty
+
+import scala.scalajs.tools.jsdep.JSDependency
+
+/** Something JavaScript related a project may depend on. Either a JavaScript
+ * module/library, or the DOM at runtime. */
+sealed trait AbstractJSDep {
+ def configurations: Option[String]
+
+ protected def withConfigs(configs: Option[String]): AbstractJSDep
+
+ def %(configurations: Configuration): AbstractJSDep = %(configurations.name)
+ def %(configurations: String): AbstractJSDep = {
+ require(this.configurations.isEmpty,
+ "Configurations already specified for jsModule " + this)
+ nonEmpty(configurations, "Configurations")
+ withConfigs(Some(configurations))
+ }
+
+}
+
+/** A JavaScript module/library a Scala.js project may depend on */
+sealed trait JSModuleID extends AbstractJSDep {
+ def jsDep: JSDependency
+
+ protected def withJSDep(jsDep: JSDependency): JSModuleID
+
+ def commonJSName(name: String): JSModuleID =
+ withJSDep(jsDep = jsDep.commonJSName(name))
+
+ def dependsOn(names: String*): JSModuleID =
+ withJSDep(jsDep = jsDep.dependsOn(names: _*))
+}
+
+/** A JavaScript module that resides inside a jar (probably webjar) */
+final case class JarJSModuleID(
+ module: ModuleID,
+ jsDep: JSDependency) extends JSModuleID {
+
+ def configurations: Option[String] = module.configurations
+
+ protected def withConfigs(configs: Option[String]): JSModuleID =
+ copy(module = module.copy(configurations = configs))
+ protected def withJSDep(jsDep: JSDependency): JSModuleID =
+ copy(jsDep = jsDep)
+}
+
+object JarJSModuleID {
+ def apply(module: ModuleID, name: String): JarJSModuleID =
+ JarJSModuleID(module, new JSDependency(name, Nil))
+}
+
+/** A JavaScript module that we depend on, but is provided externally or
+ * by the project itself */
+final case class ProvidedJSModuleID(
+ jsDep: JSDependency,
+ configurations: Option[String]) extends JSModuleID {
+
+ protected def withConfigs(configs: Option[String]): JSModuleID =
+ copy(configurations = configs)
+ protected def withJSDep(jsDep: JSDependency): JSModuleID =
+ copy(jsDep = jsDep)
+}
+
+object ProvidedJSModuleID {
+ def apply(name: String, configurations: Option[String]): ProvidedJSModuleID =
+ ProvidedJSModuleID(new JSDependency(name, Nil), configurations)
+}
+
+sealed case class RuntimeDOM(
+ configurations: Option[String]) extends AbstractJSDep {
+
+ protected def withConfigs(configs: Option[String]): RuntimeDOM =
+ copy(configurations = configs)
+}
+
+object RuntimeDOM extends RuntimeDOM(None)
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/Implicits.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/Implicits.scala
new file mode 100644
index 0000000..0c1559f
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/Implicits.scala
@@ -0,0 +1,34 @@
+package scala.scalajs.sbtplugin
+
+import scala.language.implicitConversions
+
+import scala.scalajs.tools.logging._
+import sbt.{Logger => SbtLogger, Level => SbtLevel}
+
+object Implicits {
+ private class SbtLoggerWrapper(underlying: SbtLogger) extends Logger {
+ def log(level: Level, message: => String): Unit =
+ underlying.log(level, message)
+ def success(message: => String): Unit =
+ underlying.success(message)
+ def trace(t: => Throwable): Unit =
+ underlying.trace(t)
+ }
+
+ implicit def sbtLogger2ToolsLogger(logger: SbtLogger): Logger =
+ new SbtLoggerWrapper(logger)
+
+ implicit def sbtLevel2ToolsLevel(level: SbtLevel.Value): Level = level match {
+ case SbtLevel.Error => Level.Error
+ case SbtLevel.Warn => Level.Warn
+ case SbtLevel.Info => Level.Info
+ case SbtLevel.Debug => Level.Debug
+ }
+
+ implicit def toolsLevel2sbtLevel(level: Level): SbtLevel.Value = level match {
+ case Level.Error => SbtLevel.Error
+ case Level.Warn => SbtLevel.Warn
+ case Level.Info => SbtLevel.Info
+ case Level.Debug => SbtLevel.Debug
+ }
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/JSUtils.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/JSUtils.scala
new file mode 100644
index 0000000..a59f105
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/JSUtils.scala
@@ -0,0 +1,35 @@
+package scala.scalajs.sbtplugin
+
+object JSUtils {
+ def listToJS(xs: List[String]): String =
+ xs.map(toJSstr _).mkString("[",",","]")
+
+ /** (almost) stolen from scala.scalajs.compiler.JSPrinters */
+ def toJSstr(str: String): String = {
+ /* Note that Java and JavaScript happen to use the same encoding for
+ * Unicode, namely UTF-16, which means that 1 char from Java always equals
+ * 1 char in JavaScript. */
+ val builder = new StringBuilder()
+ builder.append('"')
+ str foreach {
+ case '\\' => builder.append("\\\\")
+ case '"' => builder.append("\\\"")
+ case '\u0007' => builder.append("\\a")
+ case '\u0008' => builder.append("\\b")
+ case '\u0009' => builder.append("\\t")
+ case '\u000A' => builder.append("\\n")
+ case '\u000B' => builder.append("\\v")
+ case '\u000C' => builder.append("\\f")
+ case '\u000D' => builder.append("\\r")
+ case c =>
+ if (c >= 32 && c <= 126) builder.append(c.toChar) // ASCII printable characters
+ else builder.append(f"\\u$c%04x")
+ }
+ builder.append('"')
+ builder.result()
+ }
+
+ def dot2bracket(name: String): String = {
+ name.split('.').map(s => s"""[${toJSstr(s)}]""").mkString
+ }
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/LoggerJSConsole.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/LoggerJSConsole.scala
new file mode 100644
index 0000000..ecfb546
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/LoggerJSConsole.scala
@@ -0,0 +1,18 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin
+
+import sbt.Logger
+import scala.scalajs.tools.env.JSConsole
+
+/** A proxy for a Logger that looks like a Mozilla console object */
+class LoggerJSConsole(logger: Logger) extends JSConsole {
+ def log(msg: Any): Unit = logger.info(msg.toString)
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/OptimizerOptions.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/OptimizerOptions.scala
new file mode 100644
index 0000000..25d6178
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/OptimizerOptions.scala
@@ -0,0 +1,74 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin
+
+import OptimizerOptions._
+
+/** Various options for the Scala.js optimizer tool chain
+ *
+ * This is not a case class and does have a private constructor so that we
+ * can add fields in a binary-compatible manner.
+ *
+ * Use [[OptimizerOptions.apply]] and the `with` methods to create a configured
+ * instance.
+ */
+final class OptimizerOptions private (
+ /** Whether to parallelize the optimizer (currently fastOptJS only) **/
+ val parallel: Boolean = true,
+ /** Whether to run the optimizer in batch (i.e. non-incremental) mode */
+ val batchMode: Boolean = false,
+ /** Whether to run the Scala.js optimizer */
+ val disableOptimizer: Boolean = false,
+ /** Whether to pretty-print in fullOptJS */
+ val prettyPrintFullOptJS: Boolean = false,
+ /** Perform expensive checks of the sanity of the Scala.js IR */
+ val checkScalaJSIR: Boolean = false
+) {
+
+ def withParallel(parallel: Boolean): OptimizerOptions = {
+ new OptimizerOptions(parallel, batchMode,
+ disableOptimizer, prettyPrintFullOptJS, checkScalaJSIR)
+ }
+
+ def withBatchMode(batchMode: Boolean): OptimizerOptions = {
+ new OptimizerOptions(parallel, batchMode,
+ disableOptimizer, prettyPrintFullOptJS, checkScalaJSIR)
+ }
+
+ def withDisableOptimizer(disableOptimizer: Boolean): OptimizerOptions = {
+ new OptimizerOptions(parallel, batchMode,
+ disableOptimizer, prettyPrintFullOptJS, checkScalaJSIR)
+ }
+
+ def withPrettyPrintFullOptJS(prettyPrintFullOptJS: Boolean): OptimizerOptions = {
+ new OptimizerOptions(parallel, batchMode,
+ disableOptimizer, prettyPrintFullOptJS, checkScalaJSIR)
+ }
+
+ def withCheckScalaJSIR(checkScalaJSIR: Boolean): OptimizerOptions = {
+ new OptimizerOptions(parallel, batchMode,
+ disableOptimizer, prettyPrintFullOptJS, checkScalaJSIR)
+ }
+
+ override def toString: String = {
+ s"""OptimizerOptions(
+ | parallel = $parallel
+ | batchMode = $batchMode
+ | disableOptimizer = $disableOptimizer
+ | prettyPrintFullOptJS = $prettyPrintFullOptJS
+ | checkScalaJSIR = $checkScalaJSIR
+ |)""".stripMargin
+ }
+
+}
+
+object OptimizerOptions {
+ def apply() = new OptimizerOptions()
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/ScalaJSCrossVersion.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/ScalaJSCrossVersion.scala
new file mode 100644
index 0000000..d813622
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/ScalaJSCrossVersion.scala
@@ -0,0 +1,48 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin
+
+import sbt._
+
+import scala.scalajs.ir.ScalaJSVersions
+
+object ScalaJSCrossVersion {
+ private val scalaJSVersionUnmapped: String => String =
+ _ => s"sjs$currentBinaryVersion"
+
+ private val scalaJSVersionMap: String => String =
+ version => s"sjs${currentBinaryVersion}_$version"
+
+ private final val ReleaseVersion =
+ raw"""(\d+)\.(\d+)\.(\d+)""".r
+ private final val MinorSnapshotVersion =
+ raw"""(\d+)\.(\d+)\.([1-9]\d*)-SNAPSHOT""".r
+
+ val currentBinaryVersion = binaryScalaJSVersion(ScalaJSVersions.current)
+
+ def binaryScalaJSVersion(full: String): String = full match {
+ case ReleaseVersion(major, minor, release) => s"$major.$minor"
+ case MinorSnapshotVersion(major, minor, _) => s"$major.$minor"
+ case _ => full
+ }
+
+ def scalaJSMapped(cross: CrossVersion): CrossVersion = cross match {
+ case CrossVersion.Disabled =>
+ CrossVersion.binaryMapped(scalaJSVersionUnmapped)
+ case cross: CrossVersion.Binary =>
+ CrossVersion.binaryMapped(cross.remapVersion andThen scalaJSVersionMap)
+ case cross: CrossVersion.Full =>
+ CrossVersion.fullMapped(cross.remapVersion andThen scalaJSVersionMap)
+ }
+
+ def binary: CrossVersion = scalaJSMapped(CrossVersion.binary)
+
+ def full: CrossVersion = scalaJSMapped(CrossVersion.full)
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/ScalaJSPlugin.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/ScalaJSPlugin.scala
new file mode 100644
index 0000000..b33e2fb
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/ScalaJSPlugin.scala
@@ -0,0 +1,179 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin
+
+import sbt._
+
+import scala.scalajs.tools.sem.Semantics
+import scala.scalajs.tools.classpath._
+import scala.scalajs.tools.io.VirtualJSFile
+import scala.scalajs.tools.env.{JSEnv, JSConsole}
+import scala.scalajs.tools.optimizer.ScalaJSOptimizer
+
+import scala.scalajs.ir.ScalaJSVersions
+
+import scala.scalajs.sbtplugin.env.nodejs.NodeJSEnv
+import scala.scalajs.sbtplugin.env.phantomjs.PhantomJSEnv
+
+object ScalaJSPlugin extends AutoPlugin {
+ override def requires = plugins.JvmPlugin
+
+ object autoImport extends impl.DependencyBuilders {
+ import KeyRanks._
+
+ // Some constants
+ val scalaJSVersion = ScalaJSVersions.current
+ val scalaJSIsSnapshotVersion = ScalaJSVersions.currentIsSnapshot
+ val scalaJSBinaryVersion = ScalaJSCrossVersion.currentBinaryVersion
+
+ // Stage values
+ val PreLinkStage = Stage.PreLink
+ val FastOptStage = Stage.FastOpt
+ val FullOptStage = Stage.FullOpt
+
+ // Factory methods for JSEnvs
+
+ /**
+ * Creates a [[Def.Initialize]] for a NodeJSEnv. Use this to explicitly
+ * specify in your build that you would like to run with Node.js:
+ *
+ * {{{
+ * postLinkJSEnv := NodeJSEnv().value
+ * }}}
+ *
+ * Note that the resulting [[Setting]] is not scoped at all, but must be
+ * scoped in a project that has the ScalaJSPlugin enabled to work properly.
+ * Therefore, either put the upper line in your project settings (common
+ * case) or scope it manually, using [[Project.inScope]].
+ */
+ def NodeJSEnv(
+ executable: String = "node",
+ args: Seq[String] = Seq.empty,
+ env: Map[String, String] = Map.empty
+ ): Def.Initialize[Task[NodeJSEnv]] = Def.task {
+ new NodeJSEnv(executable, args, env)
+ }
+
+ /**
+ * Creates a [[Def.Initialize]] for a PhantomJSEnv. Use this to explicitly
+ * specify in your build that you would like to run with PhantomJS:
+ *
+ * {{{
+ * postLinkJSEnv := PhantomJSEnv().value
+ * }}}
+ *
+ * Note that the resulting [[Setting]] is not scoped at all, but must be
+ * scoped in a project that has the ScalaJSPlugin enabled to work properly.
+ * Therefore, either put the upper line in your project settings (common
+ * case) or scope it manually, using [[Project.inScope]].
+ */
+ def PhantomJSEnv(
+ executable: String = "phantomjs",
+ args: Seq[String] = Seq.empty,
+ env: Map[String, String] = Map.empty,
+ autoExit: Boolean = true
+ ): Def.Initialize[Task[PhantomJSEnv]] = Def.task {
+ val loader = scalaJSPhantomJSClassLoader.value
+ new PhantomJSEnv(executable, args, env, autoExit, loader)
+ }
+
+ // All our public-facing keys
+
+ val fastOptJS = TaskKey[Attributed[File]]("fastOptJS",
+ "Quickly link all compiled JavaScript into a single file", APlusTask)
+ val fullOptJS = TaskKey[Attributed[File]]("fullOptJS",
+ "Link all compiled JavaScript into a single file and fully optimize", APlusTask)
+
+ val scalaJSStage = SettingKey[Stage]("scalaJSStage",
+ "The optimization stage at which run and test are executed", APlusSetting)
+
+ val packageScalaJSLauncher = TaskKey[Attributed[File]]("packageScalaJSLauncher",
+ "Writes the persistent launcher file. Fails if the mainClass is ambigous", CTask)
+
+ val packageJSDependencies = TaskKey[File]("packageJSDependencies",
+ "Packages all dependencies of the preLink classpath in a single file. " +
+ "Set skip in packageJSDependencies := false to run automatically", AMinusTask)
+
+ val jsDependencyManifest = TaskKey[File]("jsDependencyManifest",
+ "Writes the JS_DEPENDENCIES file.", DTask)
+
+ val scalaJSPreLinkClasspath = TaskKey[IRClasspath]("scalaJSPreLinkClasspath",
+ "Completely resolved classpath just after compilation", DTask)
+
+ val scalaJSExecClasspath = TaskKey[CompleteClasspath]("scalaJSExecClasspath",
+ "The classpath used for running and testing", DTask)
+
+ val scalaJSLauncher = TaskKey[Attributed[VirtualJSFile]]("scalaJSLauncher",
+ "Code used to run. (Attributed with used class name)", DTask)
+
+ val scalaJSConsole = TaskKey[JSConsole]("scalaJSConsole",
+ "The JS console used by the Scala.js runner/tester", DTask)
+
+ val preLinkJSEnv = TaskKey[JSEnv]("preLinkJSEnv",
+ "The jsEnv used to execute before linking (packaging / optimizing) Scala.js files", BSetting)
+ val postLinkJSEnv = TaskKey[JSEnv]("postLinkJSEnv",
+ "The jsEnv used to execute after linking (packaging / optimizing) Scala.js files", AMinusSetting)
+
+ val jsEnv = TaskKey[JSEnv]("jsEnv",
+ "A JVM-like environment where Scala.js files can be run and tested", DTask)
+
+ val requiresDOM = SettingKey[Boolean]("requiresDOM",
+ "Whether this projects needs the DOM. Overrides anything inherited through dependencies.", AMinusSetting)
+
+ val scalaJSTestFramework = SettingKey[String]("scalaJSTestFramework",
+ "The Scala.js class that is used as a test framework, for example a class that wraps Jasmine", ASetting)
+
+ val relativeSourceMaps = SettingKey[Boolean]("relativeSourceMaps",
+ "Make the referenced paths on source maps relative to target path", BPlusSetting)
+
+ val emitSourceMaps = SettingKey[Boolean]("emitSourceMaps",
+ "Whether package and optimize stages should emit source maps at all", BPlusSetting)
+
+ val jsDependencies = SettingKey[Seq[AbstractJSDep]]("jsDependencies",
+ "JavaScript libraries this project depends upon. Also used to depend on the DOM.", APlusSetting)
+
+ val scalaJSSemantics = SettingKey[Semantics]("scalaJSSemantics",
+ "Configurable semantics of Scala.js.", BPlusSetting)
+
+ val jsDependencyFilter = SettingKey[PartialClasspath.DependencyFilter]("jsDependencyFilter",
+ "The filter applied to the raw JavaScript dependencies before execution", CSetting)
+
+ val checkScalaJSSemantics = SettingKey[Boolean]("checkScalaJSSemantics",
+ "Whether to check that the current semantics meet compliance " +
+ "requirements of dependencies.", CSetting)
+
+ val persistLauncher = SettingKey[Boolean]("persistLauncher",
+ "Tell optimize/package tasks to write the laucher file to disk. " +
+ "If this is set, your project may only have a single mainClass or you must explicitly set it", AMinusSetting)
+
+ val scalaJSOptimizerOptions = SettingKey[OptimizerOptions]("scalaJSOptimizerOptions",
+ "All kinds of options for the Scala.js optimizer stages", DSetting)
+
+ /** Class loader for PhantomJSEnv. Used to load jetty8. */
+ val scalaJSPhantomJSClassLoader = TaskKey[ClassLoader]("scalaJSPhantomJSClassLoader",
+ "Private class loader to load jetty8 without polluting classpath. Only use this " +
+ "as the `jettyClassLoader` argument of the PhantomJSEnv",
+ KeyRanks.Invisible)
+ }
+
+ import autoImport._
+ import ScalaJSPluginInternal._
+
+ override def globalSettings: Seq[Setting[_]] = {
+ super.globalSettings ++ Seq(
+ scalaJSStage := Stage.PreLink
+ )
+ }
+
+ override def projectSettings: Seq[Setting[_]] = (
+ scalaJSAbstractSettings ++
+ scalaJSEcosystemSettings
+ )
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/ScalaJSPluginInternal.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/ScalaJSPluginInternal.scala
new file mode 100644
index 0000000..fe97f0b
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/ScalaJSPluginInternal.scala
@@ -0,0 +1,598 @@
+package scala.scalajs.sbtplugin
+
+import sbt._
+import sbt.inc.{IncOptions, ClassfileManager}
+import Keys._
+
+import Implicits._
+import JSUtils._
+
+import scala.scalajs.tools.sem.Semantics
+import scala.scalajs.tools.io.{IO => toolsIO, _}
+import scala.scalajs.tools.classpath._
+import scala.scalajs.tools.classpath.builder._
+import scala.scalajs.tools.jsdep._
+import scala.scalajs.tools.optimizer.{
+ ScalaJSOptimizer,
+ ScalaJSClosureOptimizer,
+ IncOptimizer,
+ ParIncOptimizer
+}
+import scala.scalajs.tools.corelib.CoreJSLibs
+
+import scala.scalajs.tools.env._
+import scala.scalajs.sbtplugin.env.rhino.RhinoJSEnv
+import scala.scalajs.sbtplugin.env.nodejs.NodeJSEnv
+import scala.scalajs.sbtplugin.env.phantomjs.{PhantomJSEnv, PhantomJettyClassLoader}
+
+import scala.scalajs.ir.ScalaJSVersions
+
+import scala.scalajs.sbtplugin.testing.{TestFramework, JSClasspathLoader}
+
+import scala.util.Try
+
+import java.nio.charset.Charset
+import java.net.URLClassLoader
+
+/** Contains settings used by ScalaJSPlugin that should not be automatically
+ * be in the *.sbt file's scope.
+ */
+object ScalaJSPluginInternal {
+
+ import ScalaJSPlugin.autoImport._
+
+ /** Dummy setting to ensure we do not fork in Scala.js run & test. */
+ val scalaJSEnsureUnforked = SettingKey[Boolean]("ensureUnforked",
+ "Scala.js internal: Fails if fork is true.", KeyRanks.Invisible)
+
+ /** Dummy setting to persist Scala.js optimizer */
+ val scalaJSOptimizer = SettingKey[ScalaJSOptimizer]("scalaJSOptimizer",
+ "Scala.js internal: Setting to persist the optimizer", KeyRanks.Invisible)
+
+ /** Internal task to calculate whether a project requests the DOM
+ * (through jsDependencies or requiresDOM) */
+ val scalaJSRequestsDOM = TaskKey[Boolean]("scalaJSRequestsDOM",
+ "Scala.js internal: Whether a project really wants the DOM. " +
+ "Calculated using requiresDOM and jsDependencies", KeyRanks.Invisible)
+
+ /** Default post link environment */
+ val scalaJSDefaultPostLinkJSEnv = TaskKey[JSEnv]("scalaJSDefaultPostLinkJSEnv",
+ "Scala.js internal: Default for postLinkJSEnv", KeyRanks.Invisible)
+
+ /** Lookup key for CompleteClasspath in attribute maps */
+ val scalaJSCompleteClasspath =
+ AttributeKey[CompleteClasspath]("scalaJSCompleteClasspath")
+
+ /** Patches the IncOptions so that .sjsir files are pruned as needed.
+ *
+ * This complicated logic patches the ClassfileManager factory of the given
+ * IncOptions with one that is aware of .sjsir files emitted by the Scala.js
+ * compiler. This makes sure that, when a .class file must be deleted, the
+ * corresponding .sjsir file are also deleted.
+ */
+ def scalaJSPatchIncOptions(incOptions: IncOptions): IncOptions = {
+ val inheritedNewClassfileManager = incOptions.newClassfileManager
+ val newClassfileManager = () => new ClassfileManager {
+ private[this] val inherited = inheritedNewClassfileManager()
+
+ def delete(classes: Iterable[File]): Unit = {
+ inherited.delete(classes flatMap { classFile =>
+ val scalaJSFiles = if (classFile.getPath endsWith ".class") {
+ val f = FileVirtualFile.withExtension(classFile, ".class", ".sjsir")
+ if (f.exists) List(f)
+ else Nil
+ } else Nil
+ classFile :: scalaJSFiles
+ })
+ }
+
+ def generated(classes: Iterable[File]): Unit = inherited.generated(classes)
+ def complete(success: Boolean): Unit = inherited.complete(success)
+ }
+ incOptions.copy(newClassfileManager = newClassfileManager)
+ }
+
+ private def scalaJSOptimizerSetting(key: TaskKey[_]): Setting[_] = (
+ scalaJSOptimizer in key := {
+ val semantics = (scalaJSSemantics in key).value
+ if ((scalaJSOptimizerOptions in key).value.parallel)
+ new ScalaJSOptimizer(semantics, new ParIncOptimizer(_))
+ else
+ new ScalaJSOptimizer(semantics, new IncOptimizer(_))
+ }
+ )
+
+ val scalaJSConfigSettings: Seq[Setting[_]] = Seq(
+ incOptions ~= scalaJSPatchIncOptions
+ ) ++ Seq(
+
+ scalaJSPreLinkClasspath := {
+ val cp = fullClasspath.value
+ val pcp = PartialClasspathBuilder.build(Attributed.data(cp).toList)
+ val ccp = pcp.resolve(jsDependencyFilter.value)
+
+ if (checkScalaJSSemantics.value)
+ ccp.checkCompliance(scalaJSSemantics.value)
+
+ ccp
+ },
+
+ artifactPath in fastOptJS :=
+ ((crossTarget in fastOptJS).value /
+ ((moduleName in fastOptJS).value + "-fastopt.js")),
+
+ scalaJSOptimizerSetting(fastOptJS),
+
+ fastOptJS := {
+ val s = streams.value
+ val output = (artifactPath in fastOptJS).value
+ val taskCache =
+ WritableFileVirtualTextFile(s.cacheDirectory / "fastopt-js")
+
+ IO.createDirectory(output.getParentFile)
+
+ val relSourceMapBase =
+ if ((relativeSourceMaps in fastOptJS).value)
+ Some(output.getParentFile.toURI())
+ else None
+
+ val opts = (scalaJSOptimizerOptions in fastOptJS).value
+
+ import ScalaJSOptimizer._
+ val outCP = (scalaJSOptimizer in fastOptJS).value.optimizeCP(
+ Inputs(input = (scalaJSPreLinkClasspath in fastOptJS).value),
+ OutputConfig(
+ output = WritableFileVirtualJSFile(output),
+ cache = Some(taskCache),
+ wantSourceMap = (emitSourceMaps in fastOptJS).value,
+ relativizeSourceMapBase = relSourceMapBase,
+ checkIR = opts.checkScalaJSIR,
+ disableOptimizer = opts.disableOptimizer,
+ batchMode = opts.batchMode),
+ s.log)
+
+ Attributed.blank(output).put(scalaJSCompleteClasspath, outCP)
+ },
+ fastOptJS <<=
+ fastOptJS.dependsOn(packageJSDependencies, packageScalaJSLauncher),
+
+ artifactPath in fullOptJS :=
+ ((crossTarget in fullOptJS).value /
+ ((moduleName in fullOptJS).value + "-opt.js")),
+
+ scalaJSSemantics in fullOptJS :=
+ (scalaJSSemantics in fastOptJS).value.optimized,
+
+ scalaJSOptimizerSetting(fullOptJS),
+
+ fullOptJS := {
+ val s = streams.value
+ val output = (artifactPath in fullOptJS).value
+ val taskCache =
+ WritableFileVirtualTextFile(s.cacheDirectory / "fullopt-js")
+
+ IO.createDirectory(output.getParentFile)
+
+ val relSourceMapBase =
+ if ((relativeSourceMaps in fullOptJS).value)
+ Some(output.getParentFile.toURI())
+ else None
+
+ val opts = (scalaJSOptimizerOptions in fullOptJS).value
+
+ val semantics = (scalaJSSemantics in fullOptJS).value
+
+ import ScalaJSClosureOptimizer._
+ val outCP = new ScalaJSClosureOptimizer(semantics).optimizeCP(
+ (scalaJSOptimizer in fullOptJS).value,
+ Inputs(ScalaJSOptimizer.Inputs(
+ input = (scalaJSPreLinkClasspath in fullOptJS).value)),
+ OutputConfig(
+ output = WritableFileVirtualJSFile(output),
+ cache = Some(taskCache),
+ wantSourceMap = (emitSourceMaps in fullOptJS).value,
+ relativizeSourceMapBase = relSourceMapBase,
+ checkIR = opts.checkScalaJSIR,
+ disableOptimizer = opts.disableOptimizer,
+ batchMode = opts.batchMode,
+ prettyPrint = opts.prettyPrintFullOptJS),
+ s.log)
+
+ Attributed.blank(output).put(scalaJSCompleteClasspath, outCP)
+ },
+
+ artifactPath in packageScalaJSLauncher :=
+ ((crossTarget in packageScalaJSLauncher).value /
+ ((moduleName in packageScalaJSLauncher).value + "-launcher.js")),
+
+ skip in packageScalaJSLauncher := !persistLauncher.value,
+
+ packageScalaJSLauncher <<= Def.taskDyn {
+ if ((skip in packageScalaJSLauncher).value)
+ Def.task(Attributed.blank((artifactPath in packageScalaJSLauncher).value))
+ else Def.task {
+ mainClass.value map { mainCl =>
+ val file = (artifactPath in packageScalaJSLauncher).value
+ IO.write(file, launcherContent(mainCl), Charset.forName("UTF-8"))
+
+ // Attach the name of the main class used, (ab?)using the name key
+ Attributed(file)(AttributeMap.empty.put(name.key, mainCl))
+ } getOrElse {
+ sys.error("Cannot write launcher file, since there is no or multiple mainClasses")
+ }
+ }
+ },
+
+ artifactPath in packageJSDependencies :=
+ ((crossTarget in packageJSDependencies).value /
+ ((moduleName in packageJSDependencies).value + "-jsdeps.js")),
+
+ packageJSDependencies <<= Def.taskDyn {
+ if ((skip in packageJSDependencies).value)
+ Def.task((artifactPath in packageJSDependencies).value)
+ else Def.task {
+ val cp = scalaJSPreLinkClasspath.value
+ val output = (artifactPath in packageJSDependencies).value
+ val taskCache = WritableFileVirtualJSFile(
+ streams.value.cacheDirectory / "package-js-deps")
+
+ IO.createDirectory(output.getParentFile)
+
+ val outFile = WritableFileVirtualTextFile(output)
+ CacheUtils.cached(cp.version, outFile, Some(taskCache)) {
+ toolsIO.concatFiles(outFile, cp.jsLibs.map(_.lib))
+ }
+
+ output
+ }
+ },
+
+ jsDependencyManifest := {
+ val myModule = thisProject.value.id
+ val config = configuration.value.name
+
+ // Collect all libraries
+ val jsDeps = jsDependencies.value.collect {
+ case dep: JSModuleID if dep.configurations.forall(_ == config) =>
+ dep.jsDep
+ }
+
+ val requiresDOM = jsDependencies.value.exists {
+ case RuntimeDOM(configurations) =>
+ configurations.forall(_ == config)
+ case _ => false
+ }
+
+ val compliantSemantics = scalaJSSemantics.value.compliants
+
+ val manifest = new JSDependencyManifest(new Origin(myModule, config),
+ jsDeps.toList, requiresDOM, compliantSemantics)
+
+ // Write dependency file to class directory
+ val targetDir = classDirectory.value
+ IO.createDirectory(targetDir)
+
+ val file = targetDir / JSDependencyManifest.ManifestFileName
+ val vfile = WritableFileVirtualTextFile(file)
+
+ // Prevent writing if unnecessary to not invalidate dependencies
+ val needWrite = !vfile.exists || {
+ Try {
+ val readManifest = JSDependencyManifest.read(vfile)
+ readManifest != manifest
+ } getOrElse true
+ }
+
+ if (needWrite)
+ JSDependencyManifest.write(manifest, vfile)
+
+ file
+ },
+
+ products <<= products.dependsOn(jsDependencyManifest),
+
+ console <<= console.dependsOn(Def.task(
+ streams.value.log.warn("Scala REPL doesn't work with Scala.js. You " +
+ "are running a JVM REPL. JavaScript things won't work.")
+ )),
+
+ // Give tasks ability to check we are not forking at build reading time
+ scalaJSEnsureUnforked := {
+ if (fork.value)
+ sys.error("Scala.js cannot be run in a forked JVM")
+ else
+ true
+ },
+
+ scalaJSRequestsDOM :=
+ requiresDOM.?.value.getOrElse(scalaJSExecClasspath.value.requiresDOM),
+
+ // Default jsEnv
+ jsEnv <<= Def.taskDyn {
+ scalaJSStage.value match {
+ case Stage.PreLink =>
+ Def.task {
+ preLinkJSEnv.?.value.getOrElse {
+ new RhinoJSEnv(scalaJSSemantics.value,
+ withDOM = scalaJSRequestsDOM.value)
+ }
+ }
+ case Stage.FastOpt | Stage.FullOpt =>
+ Def.task(scalaJSDefaultPostLinkJSEnv.value)
+ }
+ },
+
+ // Wire jsEnv and sources for other stages
+ scalaJSDefaultPostLinkJSEnv := postLinkJSEnv.?.value.getOrElse {
+ if (scalaJSRequestsDOM.value)
+ new PhantomJSEnv(jettyClassLoader = scalaJSPhantomJSClassLoader.value)
+ else
+ new NodeJSEnv
+ },
+
+ scalaJSExecClasspath <<= Def.taskDyn {
+ scalaJSStage.value match {
+ case Stage.PreLink =>
+ Def.task { scalaJSPreLinkClasspath.value }
+ case Stage.FastOpt =>
+ Def.task { fastOptJS.value.get(scalaJSCompleteClasspath).get }
+ case Stage.FullOpt =>
+ Def.task { fullOptJS.value.get(scalaJSCompleteClasspath).get }
+ }
+ }
+ )
+
+ /** Run a class in a given environment using a given launcher */
+ private def jsRun(env: JSEnv, cp: CompleteClasspath, mainCl: String,
+ launcher: VirtualJSFile, jsConsole: JSConsole, log: Logger) = {
+
+ log.info("Running " + mainCl)
+ log.debug(s"with JSEnv of type ${env.getClass()}")
+ log.debug(s"with classpath of type ${cp.getClass}")
+
+ // Actually run code
+ env.jsRunner(cp, launcher, log, jsConsole).run()
+ }
+
+ private def launcherContent(mainCl: String) = {
+ // If we are running in Node.js, we need to bracket select on
+ // global rather than this
+ """((typeof global === "object" && global &&
+ global["Object"] === Object) ? global : this)""" +
+ s"${dot2bracket(mainCl)}().main();\n"
+ }
+
+ private def memLauncher(mainCl: String) = {
+ new MemVirtualJSFile("Generated launcher file")
+ .withContent(launcherContent(mainCl))
+ }
+
+ // These settings will be filtered by the stage dummy tasks
+ val scalaJSRunSettings = Seq(
+ mainClass in scalaJSLauncher := (mainClass in run).value,
+ scalaJSLauncher <<= Def.taskDyn {
+ if (persistLauncher.value)
+ Def.task(packageScalaJSLauncher.value.map(FileVirtualJSFile))
+ else Def.task {
+ (mainClass in scalaJSLauncher).value map { mainClass =>
+ val memLaunch = memLauncher(mainClass)
+ Attributed[VirtualJSFile](memLaunch)(
+ AttributeMap.empty.put(name.key, mainClass))
+ } getOrElse {
+ sys.error("No main class detected.")
+ }
+ }
+ },
+
+ /* We do currently not discover objects containing a
+ *
+ * def main(args: Array[String]): Unit
+ *
+ * Support will be added again, as soon as we can run them
+ * reliably (e.g. without implicitly requiring that an exported
+ *
+ * def main(): Unit
+ *
+ * exists alongside.
+ */
+ discoveredMainClasses := {
+ import xsbt.api.{Discovered, Discovery}
+
+ val jsApp = "scala.scalajs.js.JSApp"
+
+ def isJSApp(discovered: Discovered) =
+ discovered.isModule && discovered.baseClasses.contains(jsApp)
+
+ Discovery(Set(jsApp), Set.empty)(Tests.allDefs(compile.value)) collect {
+ case (definition, discovered) if isJSApp(discovered) =>
+ definition.name
+ }
+ },
+
+ run <<= Def.inputTask {
+ // use assert to prevent warning about pure expr in stat pos
+ assert(scalaJSEnsureUnforked.value)
+
+ val launch = scalaJSLauncher.value
+ val className = launch.get(name.key).getOrElse("<unknown class>")
+ jsRun(jsEnv.value, scalaJSExecClasspath.value, className,
+ launch.data, scalaJSConsole.value, streams.value.log)
+ },
+
+ runMain <<= {
+ // Implicits for parsing
+ import sbinary.DefaultProtocol.StringFormat
+ import Cache.seqFormat
+
+ val parser = Defaults.loadForParser(discoveredMainClasses)((s, names) =>
+ Defaults.runMainParser(s, names getOrElse Nil))
+
+ Def.inputTask {
+ // use assert to prevent warning about pure expr in stat pos
+ assert(scalaJSEnsureUnforked.value)
+
+ val mainCl = parser.parsed._1
+ jsRun(jsEnv.value, scalaJSExecClasspath.value, mainCl,
+ memLauncher(mainCl), scalaJSConsole.value, streams.value.log)
+ }
+ }
+ )
+
+ val scalaJSCompileSettings = (
+ scalaJSConfigSettings ++
+ scalaJSRunSettings
+ )
+
+ val scalaJSTestFrameworkSettings = Seq(
+ // Copied from Defaults, but scoped. We need a JVM loader in
+ // loadedTestFrameworks to find out whether the framework exists.
+ testLoader in loadedTestFrameworks := {
+ TestFramework.createTestLoader(
+ Attributed.data(fullClasspath.value),
+ scalaInstance.value,
+ IO.createUniqueDirectory(taskTemporaryDirectory.value))
+ },
+
+ loadedTestFrameworks := {
+ // use assert to prevent warning about pure expr in stat pos
+ assert(scalaJSEnsureUnforked.value)
+
+ val loader = (testLoader in loadedTestFrameworks).value
+ val isTestFrameworkDefined = try {
+ Class.forName(scalaJSTestFramework.value, false, loader)
+ true
+ } catch {
+ case _: ClassNotFoundException => false
+ }
+ if (isTestFrameworkDefined) {
+ loadedTestFrameworks.value.updated(
+ sbt.TestFramework(classOf[TestFramework].getName),
+ new TestFramework(
+ environment = jsEnv.value,
+ jsConsole = scalaJSConsole.value,
+ testFramework = scalaJSTestFramework.value)
+ )
+ } else {
+ loadedTestFrameworks.value
+ }
+ },
+
+ // Pseudo loader to pass classpath to test framework
+ testLoader := JSClasspathLoader(scalaJSExecClasspath.value)
+ )
+
+ val scalaJSTestBuildSettings = (
+ scalaJSConfigSettings
+ ) ++ (
+ Seq(fastOptJS, fullOptJS, packageScalaJSLauncher,
+ packageJSDependencies) map { packageJSTask =>
+ moduleName in packageJSTask := moduleName.value + "-test"
+ }
+ )
+
+ val scalaJSTestSettings = (
+ scalaJSTestBuildSettings ++
+ scalaJSTestFrameworkSettings
+ )
+
+ val scalaJSDependenciesSettings = Seq(
+ // add all the webjars your jsDependencies depend upon
+ libraryDependencies ++= jsDependencies.value.collect {
+ case JarJSModuleID(module, _) => module
+ }
+ )
+
+ val scalaJSDefaultBuildConfigs = (
+ inConfig(Compile)(scalaJSConfigSettings) ++ // build settings for Compile
+ inConfig(Test)(scalaJSTestBuildSettings) ++
+ scalaJSDependenciesSettings
+ )
+
+ val scalaJSDefaultConfigs = (
+ inConfig(Compile)(scalaJSCompileSettings) ++
+ inConfig(Test)(scalaJSTestSettings) ++
+ scalaJSDependenciesSettings
+ )
+
+ val phantomJSJettyModules = Seq(
+ "org.eclipse.jetty" % "jetty-websocket" % "8.1.16.v20140903",
+ "org.eclipse.jetty" % "jetty-server" % "8.1.16.v20140903"
+ )
+
+ val scalaJSProjectBaseSettings = Seq(
+ relativeSourceMaps := false,
+ persistLauncher := false,
+
+ skip in packageJSDependencies := true,
+
+ scalaJSTestFramework := "org.scalajs.jasminetest.JasmineTestFramework",
+
+ emitSourceMaps := true,
+
+ scalaJSOptimizerOptions := OptimizerOptions(),
+
+ jsDependencies := Seq(),
+ jsDependencyFilter := identity,
+
+ scalaJSSemantics := Semantics.Defaults,
+ checkScalaJSSemantics := true,
+
+ scalaJSConsole := ConsoleJSConsole,
+
+ clean <<= clean.dependsOn(Def.task {
+ // have clean reset incremental optimizer state
+ (scalaJSOptimizer in (Compile, fastOptJS)).value.clean()
+ (scalaJSOptimizer in (Test, fastOptJS)).value.clean()
+ }),
+
+ /* Depend on jetty artifacts in dummy configuration to be able to inject
+ * them into the PhantomJS runner if necessary.
+ * See scalaJSPhantomJSClassLoader
+ */
+ ivyConfigurations += config("phantom-js-jetty").hide,
+ libraryDependencies ++= phantomJSJettyModules.map(_ % "phantom-js-jetty"),
+ scalaJSPhantomJSClassLoader := {
+ val report = update.value
+ val jars = report.select(configurationFilter("phantom-js-jetty"))
+
+ val jettyLoader =
+ new URLClassLoader(jars.map(_.toURI.toURL).toArray, null)
+
+ new PhantomJettyClassLoader(jettyLoader, getClass.getClassLoader)
+ }
+ )
+
+ val scalaJSAbstractSettings: Seq[Setting[_]] = (
+ scalaJSProjectBaseSettings ++
+ scalaJSDefaultConfigs
+ )
+
+ val scalaJSAbstractBuildSettings: Seq[Setting[_]] = (
+ scalaJSProjectBaseSettings ++
+ scalaJSDefaultBuildConfigs
+ )
+
+ val scalaJSReleasesResolver = Resolver.url("scala-js-releases",
+ url("http://dl.bintray.com/content/scala-js/scala-js-releases"))(
+ Resolver.ivyStylePatterns)
+ val scalaJSSnapshotsResolver = Resolver.url("scala-js-snapshots",
+ url("http://repo.scala-js.org/repo/snapshots/"))(
+ Resolver.ivyStylePatterns)
+
+ val scalaJSEcosystemSettings = Seq(
+ // the resolver to find the compiler and library (and others)
+ resolvers ++= Seq(scalaJSReleasesResolver, scalaJSSnapshotsResolver),
+
+ // you will need the Scala.js compiler plugin
+ autoCompilerPlugins := true,
+ addCompilerPlugin(
+ "org.scala-lang.modules.scalajs" % "scalajs-compiler" % scalaJSVersion cross CrossVersion.full),
+
+ // and of course the Scala.js library
+ libraryDependencies += "org.scala-lang.modules.scalajs" %% "scalajs-library" % scalaJSVersion,
+
+ // and you will want to be cross-compiled on the Scala.js binary version
+ crossVersion := ScalaJSCrossVersion.binary
+ )
+
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/Stage.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/Stage.scala
new file mode 100644
index 0000000..7f7b916
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/Stage.scala
@@ -0,0 +1,18 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin
+
+sealed trait Stage
+
+object Stage {
+ case object PreLink extends Stage
+ case object FullOpt extends Stage
+ case object FastOpt extends Stage
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala
new file mode 100644
index 0000000..e0aa557
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/ExternalJSEnv.scala
@@ -0,0 +1,200 @@
+package scala.scalajs.sbtplugin.env
+
+import scala.scalajs.tools.io._
+import scala.scalajs.tools.classpath._
+import scala.scalajs.tools.env._
+import scala.scalajs.tools.logging._
+
+import scala.scalajs.sbtplugin.JSUtils._
+
+import java.io.{ Console => _, _ }
+import scala.io.Source
+
+import scala.concurrent.{Future, Promise}
+import scala.util.Try
+
+abstract class ExternalJSEnv(
+ final protected val additionalArgs: Seq[String],
+ final protected val additionalEnv: Map[String, String]) extends AsyncJSEnv {
+
+ /** Printable name of this VM */
+ protected def vmName: String
+
+ /** Command to execute (on shell) for this VM */
+ protected def executable: String
+
+ protected class AbstractExtRunner(protected val classpath: CompleteClasspath,
+ protected val code: VirtualJSFile, protected val logger: Logger,
+ protected val console: JSConsole) {
+
+ /** JS files used to setup VM */
+ protected def initFiles(): Seq[VirtualJSFile] = Nil
+
+ /** Sends required data to VM Stdin (can throw) */
+ protected def sendVMStdin(out: OutputStream): Unit = {}
+
+ /** VM arguments excluding executable. Override to adapt.
+ * Overrider is responsible to add additionalArgs.
+ */
+ protected def getVMArgs(): Seq[String] = additionalArgs
+
+ /** VM environment. Override to adapt.
+ *
+ * Default is `sys.env` and [[additionalEnv]]
+ */
+ protected def getVMEnv(): Map[String, String] =
+ sys.env ++ additionalEnv
+
+ /** Get files that are a library (i.e. that do not run anything) */
+ protected def getLibJSFiles(): Seq[VirtualJSFile] =
+ initFiles() ++ classpath.allCode
+
+ /** Get all files that are passed to VM (libraries and code) */
+ protected def getJSFiles(): Seq[VirtualJSFile] =
+ getLibJSFiles() :+ code
+
+ /** write a single JS file to a writer using an include fct if appropriate */
+ protected def writeJSFile(file: VirtualJSFile, writer: Writer): Unit = {
+ // The only platform-independent way to do this in JS is to dump the file.
+ writer.write(file.content)
+ writer.write('\n')
+ }
+
+ /** Pipe stdin and stdout from/to VM */
+ final protected def pipeVMData(vmInst: Process): Unit = {
+ // Send stdin to VM.
+ val out = vmInst.getOutputStream()
+ try { sendVMStdin(out) }
+ finally { out.close() }
+
+ // Pipe stdout to console
+ pipeToConsole(vmInst.getInputStream(), console)
+
+ // We are probably done (stdin is closed). Report any errors
+ val errSrc = Source.fromInputStream(vmInst.getErrorStream(), "UTF-8")
+ try { errSrc.getLines.foreach(err => logger.error(err)) }
+ finally { errSrc.close }
+ }
+
+ /** Wait for the VM to terminate, verify exit code */
+ final protected def waitForVM(vmInst: Process): Unit = {
+ // Make sure we are done.
+ vmInst.waitFor()
+
+ // Get return value and return
+ val retVal = vmInst.exitValue
+ if (retVal != 0)
+ sys.error(s"$vmName exited with code $retVal")
+ }
+
+ protected def startVM(): Process = {
+ val vmArgs = getVMArgs()
+ val vmEnv = getVMEnv()
+
+ val allArgs = executable +: vmArgs
+ val pBuilder = new ProcessBuilder(allArgs: _*)
+
+ pBuilder.environment().clear()
+ for ((name, value) <- vmEnv)
+ pBuilder.environment().put(name, value)
+
+ pBuilder.start()
+ }
+
+ /** send a bunch of JS files to an output stream */
+ final protected def sendJS(files: Seq[VirtualJSFile],
+ out: OutputStream): Unit = {
+ val writer = new BufferedWriter(new OutputStreamWriter(out, "UTF-8"))
+ try sendJS(files, writer)
+ finally writer.close()
+ }
+
+ /** send a bunch of JS files to a writer */
+ final protected def sendJS(files: Seq[VirtualJSFile], out: Writer): Unit =
+ files.foreach { writeJSFile(_, out) }
+
+ /** pipe lines from input stream to JSConsole */
+ final protected def pipeToConsole(in: InputStream, console: JSConsole) = {
+ val source = Source.fromInputStream(in, "UTF-8")
+ try { source.getLines.foreach(console.log _) }
+ finally { source.close() }
+ }
+
+ }
+
+ protected class ExtRunner(classpath: CompleteClasspath, code: VirtualJSFile,
+ logger: Logger, console: JSConsole
+ ) extends AbstractExtRunner(classpath, code, logger, console)
+ with JSRunner {
+
+ def run(): Unit = {
+ val vmInst = startVM()
+
+ pipeVMData(vmInst)
+ waitForVM(vmInst)
+ }
+ }
+
+ protected class AsyncExtRunner(classpath: CompleteClasspath,
+ code: VirtualJSFile, logger: Logger, console: JSConsole
+ ) extends AbstractExtRunner(classpath, code, logger, console)
+ with AsyncJSRunner {
+
+ private[this] var vmInst: Process = null
+ private[this] var ioThreadEx: Throwable = null
+ private[this] val promise = Promise[Unit]
+
+ private[this] val thread = new Thread {
+ override def run(): Unit = {
+ // This thread should not be interrupted, so it is safe to use Trys
+ val pipeResult = Try(pipeVMData(vmInst))
+ val vmComplete = Try(waitForVM(vmInst))
+
+ // Store IO exception
+ pipeResult recover {
+ case e => ioThreadEx = e
+ }
+
+ // Chain Try's the other way: We want VM failure first, then IO failure
+ promise.complete(pipeResult orElse vmComplete)
+ }
+ }
+
+ def start(): Future[Unit] = {
+ require(vmInst == null, "start() may only be called once")
+ vmInst = startVM()
+ thread.start()
+ promise.future
+ }
+
+ def stop(): Unit = {
+ require(vmInst != null, "start() must have been called")
+ vmInst.destroy()
+ }
+
+ def isRunning(): Boolean = {
+ require(vmInst != null, "start() must have been called")
+ // Emulate JDK 8 Process.isAlive
+ try {
+ vmInst.exitValue()
+ false
+ } catch {
+ case e: IllegalThreadStateException =>
+ true
+ }
+ }
+
+ def await(): Unit = {
+ require(vmInst != null, "start() must have been called")
+ thread.join()
+ waitForVM(vmInst)
+
+ // At this point, the VM itself didn't fail. We need to check if
+ // anything bad happened while piping the data from the VM
+
+ if (ioThreadEx != null)
+ throw ioThreadEx
+ }
+ }
+
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/VirtualFileMaterializer.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/VirtualFileMaterializer.scala
new file mode 100644
index 0000000..fca1c47
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/VirtualFileMaterializer.scala
@@ -0,0 +1,67 @@
+package scala.scalajs.sbtplugin.env
+
+import scala.scalajs.tools.io.{IO => _, _}
+
+import sbt.IO
+
+import java.io.File
+
+/** A helper class to temporarily store virtual files to the filesystem.
+ *
+ * Can be used with tools that require real files.
+ * @param singleDir if true, forces files to be copied into
+ * [[cacheDir]]. Useful to setup include directories for
+ * example.
+ */
+final class VirtualFileMaterializer(singleDir: Boolean = false) {
+
+ val cacheDir = {
+ val dir = IO.createTemporaryDirectory
+ dir.deleteOnExit()
+ dir
+ }
+
+ /** Create a target file to write/copy to. Will also call
+ * deleteOnExit on the file.
+ */
+ private def trgFile(name: String): File = {
+ val f = new File(cacheDir, name)
+ f.deleteOnExit()
+ f
+ }
+
+ private def materializeFileVF(vf: FileVirtualFile): File = {
+ if (!singleDir) vf.file
+ else {
+ val trg = trgFile(vf.name)
+ IO.copyFile(vf.file, trg)
+ trg
+ }
+ }
+
+ def materialize(vf: VirtualTextFile): File = vf match {
+ case vf: FileVirtualFile => materializeFileVF(vf)
+ case _ =>
+ val trg = trgFile(vf.name)
+ IO.write(trg, vf.content)
+ trg
+ }
+
+ def materialize(vf: VirtualBinaryFile): File = vf match {
+ case vf: FileVirtualFile => materializeFileVF(vf)
+ case _ =>
+ val trg = trgFile(vf.name)
+ IO.write(trg, vf.content)
+ trg
+ }
+
+ /** Removes the cache directory. Any operation on this
+ * VirtualFileMaterializer is invalid after [[close]] has been
+ * called.
+ */
+ def close(): Unit = {
+ cacheDir.listFiles().foreach(_.delete)
+ cacheDir.delete()
+ }
+
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala
new file mode 100644
index 0000000..dfabe23
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/nodejs/NodeJSEnv.scala
@@ -0,0 +1,306 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin.env.nodejs
+
+import scala.scalajs.sbtplugin.env._
+import scala.scalajs.sbtplugin.JSUtils.toJSstr
+
+import scala.scalajs.tools.io._
+import scala.scalajs.tools.classpath._
+import scala.scalajs.tools.env._
+import scala.scalajs.tools.jsdep._
+import scala.scalajs.tools.logging._
+
+import scala.scalajs.sbtplugin.JSUtils._
+
+import java.io.{ Console => _, _ }
+import java.net._
+
+import scala.io.Source
+
+class NodeJSEnv(
+ nodejsPath: String = "node",
+ addArgs: Seq[String] = Seq.empty,
+ addEnv: Map[String, String] = Map.empty
+) extends ExternalJSEnv(addArgs, addEnv) with ComJSEnv {
+
+ protected def vmName: String = "node.js"
+ protected def executable: String = nodejsPath
+
+ override def jsRunner(classpath: CompleteClasspath, code: VirtualJSFile,
+ logger: Logger, console: JSConsole): JSRunner = {
+ new NodeRunner(classpath, code, logger, console)
+ }
+
+ override def asyncRunner(classpath: CompleteClasspath, code: VirtualJSFile,
+ logger: Logger, console: JSConsole): AsyncJSRunner = {
+ new AsyncNodeRunner(classpath, code, logger, console)
+ }
+
+ override def comRunner(classpath: CompleteClasspath, code: VirtualJSFile,
+ logger: Logger, console: JSConsole): ComJSRunner = {
+ new ComNodeRunner(classpath, code, logger, console)
+ }
+
+ protected class NodeRunner(classpath: CompleteClasspath,
+ code: VirtualJSFile, logger: Logger, console: JSConsole
+ ) extends ExtRunner(classpath, code, logger, console)
+ with AbstractNodeRunner
+
+ protected class AsyncNodeRunner(classpath: CompleteClasspath,
+ code: VirtualJSFile, logger: Logger, console: JSConsole
+ ) extends AsyncExtRunner(classpath, code, logger, console)
+ with AbstractNodeRunner
+
+ protected class ComNodeRunner(classpath: CompleteClasspath,
+ code: VirtualJSFile, logger: Logger, console: JSConsole
+ ) extends AsyncNodeRunner(classpath, code, logger, console)
+ with ComJSRunner {
+
+ /** Retry-timeout to wait for the JS VM to connect */
+ private final val acceptTimeout = 1000
+
+ private[this] val serverSocket =
+ new ServerSocket(0, 0, InetAddress.getByName(null)) // Loopback address
+ private[this] var comSocket: Socket = _
+ private[this] var jvm2js: DataOutputStream = _
+ private[this] var js2jvm: DataInputStream = _
+
+ private def comSetup = new MemVirtualJSFile("comSetup.js").withContent(
+ s"""
+ (function() {
+ // The socket for communication
+ var socket = null;
+ // The callback where received messages go
+ var recvCallback = null;
+
+ // Buffers received data
+ var inBuffer = new Buffer(0);
+
+ function onData(data) {
+ inBuffer = Buffer.concat([inBuffer, data]);
+ tryReadMsg();
+ }
+
+ function tryReadMsg() {
+ if (inBuffer.length < 4) return;
+ var msgLen = inBuffer.readInt32BE(0);
+ var byteLen = 4 + msgLen * 2;
+
+ if (inBuffer.length < byteLen) return;
+ var res = "";
+
+ for (var i = 0; i < msgLen; ++i)
+ res += String.fromCharCode(inBuffer.readInt16BE(4 + i * 2));
+
+ inBuffer = inBuffer.slice(byteLen);
+
+ recvCallback(res);
+ }
+
+ global.scalajsCom = {
+ init: function(recvCB) {
+ if (socket !== null) throw new Error("Com already open");
+
+ var net = require('net');
+ recvCallback = recvCB;
+ socket = net.connect(${serverSocket.getLocalPort});
+ socket.on('data', onData);
+ },
+ send: function(msg) {
+ if (socket === null) throw new Error("Com not open");
+
+ var len = msg.length;
+ var buf = new Buffer(4 + len * 2);
+ buf.writeInt32BE(len, 0);
+ for (var i = 0; i < len; ++i)
+ buf.writeInt16BE(msg.charCodeAt(i), 4 + i * 2);
+ socket.write(buf);
+ },
+ close: function() {
+ if (socket === null) throw new Error("Com not open");
+ socket.end();
+ }
+ }
+ }).call(this);
+ """
+ )
+
+ def send(msg: String): Unit = {
+ if (awaitConnection()) {
+ jvm2js.writeInt(msg.length)
+ jvm2js.writeChars(msg)
+ jvm2js.flush()
+ }
+ }
+
+ def receive(): String = {
+ if (!awaitConnection())
+ throw new ComJSEnv.ComClosedException
+ try {
+ val len = js2jvm.readInt()
+ val carr = Array.fill(len)(js2jvm.readChar())
+ String.valueOf(carr)
+ } catch {
+ case e: EOFException =>
+ throw new ComJSEnv.ComClosedException
+ }
+ }
+
+ def close(): Unit = {
+ serverSocket.close()
+ if (jvm2js != null)
+ jvm2js.close()
+ if (js2jvm != null)
+ js2jvm.close()
+ if (comSocket != null)
+ comSocket.close()
+ }
+
+ override def stop(): Unit = {
+ close()
+ super.stop()
+ }
+
+ /** Waits until the JS VM has established a connection or terminates
+ * @return true if the connection was established
+ */
+ private def awaitConnection(): Boolean = {
+ serverSocket.setSoTimeout(acceptTimeout)
+ while (comSocket == null && isRunning) {
+ try {
+ comSocket = serverSocket.accept()
+ jvm2js = new DataOutputStream(
+ new BufferedOutputStream(comSocket.getOutputStream()))
+ js2jvm = new DataInputStream(
+ new BufferedInputStream(comSocket.getInputStream()))
+ } catch {
+ case to: SocketTimeoutException =>
+ }
+ }
+
+ comSocket != null
+ }
+
+ override protected def initFiles(): Seq[VirtualJSFile] =
+ super.initFiles :+ comSetup
+
+ override protected def finalize(): Unit = close()
+ }
+
+ protected trait AbstractNodeRunner extends AbstractExtRunner {
+
+ protected[this] val libCache = new VirtualFileMaterializer(true)
+
+ /** File(s) to automatically install source-map-support.
+ * Is used by [[initFiles]], override to change/disable.
+ */
+ protected def installSourceMap(): Seq[VirtualJSFile] = Seq(
+ new MemVirtualJSFile("sourceMapSupport.js").withContent(
+ """
+ try {
+ require('source-map-support').install();
+ } catch (e) {}
+ """
+ )
+ )
+
+ /** File(s) to hack console.log to prevent if from changing `%%` to `%`.
+ * Is used by [[initFiles]], override to change/disable.
+ */
+ protected def fixPercentConsole(): Seq[VirtualJSFile] = Seq(
+ new MemVirtualJSFile("nodeConsoleHack.js").withContent(
+ """
+ // Hack console log to duplicate double % signs
+ (function() {
+ var oldLog = console.log;
+ var newLog = function() {
+ var args = arguments;
+ if (args.length >= 1 && args[0] !== void 0 && args[0] !== null) {
+ args[0] = args[0].toString().replace(/%/g, "%%");
+ }
+ oldLog.apply(console, args);
+ };
+ console.log = newLog;
+ })();
+ """
+ )
+ )
+
+ /** File(s) to define `__ScalaJSEnv`. Defines `exitFunction`.
+ * Is used by [[initFiles]], override to change/disable.
+ */
+ protected def runtimeEnv(): Seq[VirtualJSFile] = Seq(
+ new MemVirtualJSFile("scalaJSEnvInfo.js").withContent(
+ """
+ __ScalaJSEnv = {
+ exitFunction: function(status) { process.exit(status); }
+ };
+ """
+ )
+ )
+
+ /** Concatenates results from [[installSourceMap]], [[fixPercentConsole]] and
+ * [[runtimeEnv]] (in this order).
+ */
+ override protected def initFiles(): Seq[VirtualJSFile] =
+ installSourceMap() ++ fixPercentConsole() ++ runtimeEnv()
+
+ /** Libraries are loaded via require in Node.js */
+ override protected def getLibJSFiles(): Seq[VirtualJSFile] = {
+ initFiles() ++
+ classpath.jsLibs.map(requireLibrary) :+
+ classpath.scalaJSCode
+ }
+
+ /** Rewrites a library virtual file to a require statement if possible */
+ protected def requireLibrary(dep: ResolvedJSDependency): VirtualJSFile = {
+ dep.info.commonJSName.fold(dep.lib) { varname =>
+ val fname = dep.lib.name
+ libCache.materialize(dep.lib)
+ new MemVirtualJSFile(s"require-$fname").withContent(
+ s"""$varname = require(${toJSstr(fname)});"""
+ )
+ }
+ }
+
+ // Send code to Stdin
+ override protected def sendVMStdin(out: OutputStream): Unit = {
+ sendJS(getJSFiles(), out)
+ }
+
+ /** write a single JS file to a writer using an include fct if appropriate
+ * uses `require` if the file exists on the filesystem
+ */
+ override protected def writeJSFile(file: VirtualJSFile,
+ writer: Writer): Unit = {
+ file match {
+ case file: FileVirtualJSFile =>
+ val fname = toJSstr(file.file.getAbsolutePath)
+ writer.write(s"require($fname);\n")
+ case _ =>
+ super.writeJSFile(file, writer)
+ }
+ }
+
+ // Node.js specific (system) environment
+ override protected def getVMEnv(): Map[String, String] = {
+ val baseNodePath = sys.env.get("NODE_PATH").filter(_.nonEmpty)
+ val nodePath = libCache.cacheDir.getAbsolutePath +
+ baseNodePath.fold("")(p => File.pathSeparator + p)
+
+ sys.env ++ Seq(
+ "NODE_MODULE_CONTEXTS" -> "0",
+ "NODE_PATH" -> nodePath
+ ) ++ additionalEnv
+ }
+ }
+
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/JettyWebsocketManager.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/JettyWebsocketManager.scala
new file mode 100644
index 0000000..3dec79c
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/JettyWebsocketManager.scala
@@ -0,0 +1,126 @@
+package scala.scalajs.sbtplugin.env.phantomjs
+
+import javax.servlet.http.HttpServletRequest
+
+import org.eclipse.jetty.server.Server
+import org.eclipse.jetty.server.nio.SelectChannelConnector
+import org.eclipse.jetty.websocket.{WebSocket, WebSocketHandler}
+import org.eclipse.jetty.util.component.{LifeCycle, AbstractLifeCycle}
+import org.eclipse.jetty.util.log
+
+private[phantomjs] final class JettyWebsocketManager(
+ wsListener: WebsocketListener) extends WebsocketManager { thisMgr =>
+
+ private[this] var webSocketConn: WebSocket.Connection = null
+ private[this] var closed = false
+
+ // We can just set the logger here, since we are supposed to be protected by
+ // the private ClassLoader that loads us reflectively.
+ log.Log.setLog(new WSLogger("root"))
+
+ private[this] val connector = new SelectChannelConnector
+
+ connector.setHost("localhost")
+ connector.setPort(0)
+
+ private[this] val server = new Server()
+
+ server.addConnector(connector)
+ server.setHandler(new WebSocketHandler {
+ // Support Hixie 76 for Phantom.js
+ getWebSocketFactory().setMinVersion(-1)
+
+ override def doWebSocketConnect(
+ request: HttpServletRequest, protocol: String): WebSocket =
+ new ComWebSocketListener
+ })
+
+ server.addLifeCycleListener(new AbstractLifeCycle.AbstractLifeCycleListener {
+ override def lifeCycleStarted(event: LifeCycle): Unit = {
+ if (event.isRunning())
+ wsListener.onRunning()
+ }
+ })
+
+ private class ComWebSocketListener extends WebSocket.OnTextMessage {
+ override def onOpen(connection: WebSocket.Connection): Unit = {
+ thisMgr.synchronized {
+ if (isConnected)
+ throw new IllegalStateException("Client connected twice")
+ webSocketConn = connection
+ }
+ wsListener.onOpen()
+ }
+
+ override def onClose(statusCode: Int, reason: String): Unit = {
+ thisMgr.synchronized {
+ webSocketConn = null
+ closed = true
+ }
+ wsListener.onClose()
+ server.stop()
+
+ if (statusCode != 1000) {
+ throw new Exception("Abnormal closing of connection. " +
+ s"Code: $statusCode, Reason: $reason")
+ }
+ }
+
+ override def onMessage(message: String): Unit =
+ wsListener.onMessage(message)
+ }
+
+ private class WSLogger(fullName: String) extends log.AbstractLogger {
+ private[this] var debugEnabled = false
+
+ def debug(msg: String, args: Object*): Unit =
+ if (debugEnabled) log("DEBUG", msg, args)
+
+ def debug(msg: String, thrown: Throwable): Unit =
+ if (debugEnabled) log("DEBUG", msg, thrown)
+
+ def debug(thrown: Throwable): Unit =
+ if (debugEnabled) log("DEBUG", thrown)
+
+ def getName(): String = fullName
+
+ def ignore(ignored: Throwable): Unit = ()
+
+ def info(msg: String, args: Object*): Unit = log("INFO", msg, args)
+ def info(msg: String, thrown: Throwable): Unit = log("INFO", msg, thrown)
+ def info(thrown: Throwable): Unit = log("INFO", thrown)
+
+ def warn(msg: String, args: Object*): Unit = log("WARN", msg, args)
+ def warn(msg: String, thrown: Throwable): Unit = log("WARN", msg, thrown)
+ def warn(thrown: Throwable): Unit = log("WARN", thrown)
+
+ def isDebugEnabled(): Boolean = debugEnabled
+ def setDebugEnabled(enabled: Boolean): Unit = debugEnabled = enabled
+
+ private def log(lvl: String, msg: String, args: Object*): Unit =
+ wsListener.log(s"$lvl: $msg " + args.mkString(", "))
+
+ private def log(lvl: String, msg: String, thrown: Throwable): Unit =
+ wsListener.log(s"$lvl: $msg $thrown\n{$thrown.getStackStrace}")
+
+ private def log(lvl: String, thrown: Throwable): Unit =
+ wsListener.log(s"$lvl: $thrown\n{$thrown.getStackStrace}")
+
+ protected def newLogger(fullName: String) = new WSLogger(fullName)
+ }
+
+ def start(): Unit = server.start()
+
+ def stop(): Unit = server.stop()
+
+ def isConnected: Boolean = webSocketConn != null && !closed
+ def isClosed: Boolean = closed
+
+ def localPort: Int = connector.getLocalPort()
+
+ def sendMessage(msg: String) = synchronized {
+ if (webSocketConn != null)
+ webSocketConn.sendMessage(msg)
+ }
+
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/PhantomJSEnv.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/PhantomJSEnv.scala
new file mode 100644
index 0000000..7bb47d2
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/PhantomJSEnv.scala
@@ -0,0 +1,466 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin.env.phantomjs
+
+import scala.scalajs.sbtplugin.env._
+
+import scala.scalajs.tools.io._
+import scala.scalajs.tools.classpath._
+import scala.scalajs.tools.env._
+import scala.scalajs.tools.logging._
+
+import scala.scalajs.sbtplugin.JSUtils._
+
+import java.io.{ Console => _, _ }
+import java.net._
+
+import scala.io.Source
+import scala.collection.mutable
+import scala.annotation.tailrec
+
+class PhantomJSEnv(
+ phantomjsPath: String = "phantomjs",
+ addArgs: Seq[String] = Seq.empty,
+ addEnv: Map[String, String] = Map.empty,
+ val autoExit: Boolean = true,
+ jettyClassLoader: ClassLoader = getClass().getClassLoader()
+) extends ExternalJSEnv(addArgs, addEnv) with ComJSEnv {
+
+ import PhantomJSEnv._
+
+ protected def vmName: String = "PhantomJS"
+ protected def executable: String = phantomjsPath
+
+ override def jsRunner(classpath: CompleteClasspath, code: VirtualJSFile,
+ logger: Logger, console: JSConsole): JSRunner = {
+ new PhantomRunner(classpath, code, logger, console)
+ }
+
+ override def asyncRunner(classpath: CompleteClasspath, code: VirtualJSFile,
+ logger: Logger, console: JSConsole): AsyncJSRunner = {
+ new AsyncPhantomRunner(classpath, code, logger, console)
+ }
+
+ override def comRunner(classpath: CompleteClasspath, code: VirtualJSFile,
+ logger: Logger, console: JSConsole): ComJSRunner = {
+ new ComPhantomRunner(classpath, code, logger, console)
+ }
+
+ protected class PhantomRunner(classpath: CompleteClasspath,
+ code: VirtualJSFile, logger: Logger, console: JSConsole
+ ) extends ExtRunner(classpath, code, logger, console)
+ with AbstractPhantomRunner
+
+ protected class AsyncPhantomRunner(classpath: CompleteClasspath,
+ code: VirtualJSFile, logger: Logger, console: JSConsole
+ ) extends AsyncExtRunner(classpath, code, logger, console)
+ with AbstractPhantomRunner
+
+ protected class ComPhantomRunner(classpath: CompleteClasspath,
+ code: VirtualJSFile, logger: Logger, console: JSConsole
+ ) extends AsyncPhantomRunner(classpath, code, logger, console)
+ with ComJSRunner with WebsocketListener {
+
+ private def loadMgr() = {
+ val clazz = jettyClassLoader.loadClass(
+ "scala.scalajs.sbtplugin.env.phantomjs.JettyWebsocketManager")
+
+ val ctors = clazz.getConstructors()
+ assert(ctors.length == 1, "JettyWebsocketManager may only have one ctor")
+
+ val mgr = ctors.head.newInstance(this)
+
+ mgr.asInstanceOf[WebsocketManager]
+ }
+
+ val mgr: WebsocketManager = loadMgr()
+
+ def onRunning(): Unit = synchronized(notifyAll())
+ def onOpen(): Unit = synchronized(notifyAll())
+ def onClose(): Unit = synchronized(notifyAll())
+
+ def onMessage(msg: String): Unit = synchronized {
+ recvBuf.enqueue(msg)
+ notifyAll()
+ }
+
+ def log(msg: String): Unit = logger.debug(s"PhantomJS WS Jetty: $msg")
+
+ private[this] val recvBuf = mutable.Queue.empty[String]
+
+ mgr.start()
+
+ /** The websocket server starts asynchronously, but we need the port it is
+ * running on. This method waits until the port is non-negative and
+ * returns its value.
+ */
+ private def waitForPort(): Int = {
+ while (mgr.localPort < 0)
+ wait()
+ mgr.localPort
+ }
+
+ private def comSetup = {
+ def maybeExit(code: Int) =
+ if (autoExit)
+ s"window.callPhantom({ action: 'exit', returnValue: $code });"
+ else
+ ""
+
+ val serverPort = waitForPort()
+
+ val code = s"""
+ |(function() {
+ | var MaxPayloadSize = $MaxCharPayloadSize;
+ |
+ | // The socket for communication
+ | var websocket = null;
+ |
+ | // Buffer for messages sent before socket is open
+ | var outMsgBuf = null;
+ |
+ | function sendImpl(msg) {
+ | var frags = (msg.length / MaxPayloadSize) | 0;
+ |
+ | for (var i = 0; i < frags; ++i) {
+ | var payload = msg.substring(
+ | i * MaxPayloadSize, (i + 1) * MaxPayloadSize);
+ | websocket.send("1" + payload);
+ | }
+ |
+ | websocket.send("0" + msg.substring(frags * MaxPayloadSize));
+ | }
+ |
+ | function recvImpl(recvCB) {
+ | var recvBuf = "";
+ |
+ | return function(evt) {
+ | var newData = recvBuf + evt.data.substring(1);
+ | if (evt.data.charAt(0) == "0") {
+ | recvBuf = "";
+ | recvCB(newData);
+ | } else if (evt.data.charAt(0) == "1") {
+ | recvBuf = newData;
+ | } else {
+ | throw new Error("Bad fragmentation flag in " + evt.data);
+ | }
+ | };
+ | }
+ |
+ | window.scalajsCom = {
+ | init: function(recvCB) {
+ | if (websocket !== null) throw new Error("Com already open");
+ |
+ | outMsgBuf = [];
+ |
+ | websocket = new WebSocket("ws://localhost:$serverPort");
+ |
+ | websocket.onopen = function(evt) {
+ | for (var i = 0; i < outMsgBuf.length; ++i)
+ | sendImpl(outMsgBuf[i]);
+ | outMsgBuf = null;
+ | };
+ | websocket.onclose = function(evt) {
+ | websocket = null;
+ | ${maybeExit(0)}
+ | };
+ | websocket.onmessage = recvImpl(recvCB);
+ | websocket.onerror = function(evt) {
+ | websocket = null;
+ | throw new Error("Websocket failed: " + evt);
+ | };
+ |
+ | // Take over responsibility to auto exit
+ | window.callPhantom({
+ | action: 'setAutoExit',
+ | autoExit: false
+ | });
+ | },
+ | send: function(msg) {
+ | if (websocket === null)
+ | return; // we are closed already. ignore message
+ |
+ | if (outMsgBuf !== null)
+ | outMsgBuf.push(msg);
+ | else
+ | sendImpl(msg);
+ | },
+ | close: function() {
+ | if (websocket === null)
+ | return; // we are closed already. all is well.
+ |
+ | if (outMsgBuf !== null)
+ | // Reschedule ourselves to give onopen a chance to kick in
+ | window.setTimeout(window.scalajsCom.close, 10);
+ | else
+ | websocket.close();
+ | }
+ | }
+ |}).call(this);""".stripMargin
+
+ new MemVirtualJSFile("comSetup.js").withContent(code)
+ }
+
+ def send(msg: String): Unit = synchronized {
+ if (awaitConnection()) {
+ val fragParts = msg.length / MaxCharPayloadSize
+
+ for (i <- 0 until fragParts) {
+ val payload = msg.substring(
+ i * MaxCharPayloadSize, (i + 1) * MaxCharPayloadSize)
+ mgr.sendMessage("1" + payload)
+ }
+
+ mgr.sendMessage("0" + msg.substring(fragParts * MaxCharPayloadSize))
+ }
+ }
+
+ def receive(): String = synchronized {
+ if (recvBuf.isEmpty && !awaitConnection())
+ throw new ComJSEnv.ComClosedException
+
+ @tailrec
+ def loop(acc: String): String = {
+ val frag = receiveFrag()
+ val newAcc = acc + frag.substring(1)
+
+ if (frag(0) == '0')
+ newAcc
+ else if (frag(0) == '1')
+ loop(newAcc)
+ else
+ throw new AssertionError("Bad fragmentation flag in " + frag)
+ }
+
+ loop("")
+ }
+
+ private def receiveFrag(): String = {
+ while (recvBuf.isEmpty && !mgr.isClosed)
+ wait()
+
+ if (recvBuf.isEmpty)
+ throw new ComJSEnv.ComClosedException
+ else
+ recvBuf.dequeue()
+ }
+
+ def close(): Unit = mgr.stop()
+
+ override def stop(): Unit = {
+ close()
+ super.stop()
+ }
+
+ /** Waits until the JS VM has established a connection, or the VM
+ * terminated. Returns true if a connection was established.
+ */
+ private def awaitConnection(): Boolean = {
+ while (!mgr.isConnected && !mgr.isClosed && isRunning)
+ wait(200) // We sleep-wait for isRunning
+
+ mgr.isConnected
+ }
+
+ override protected def initFiles(): Seq[VirtualJSFile] =
+ super.initFiles :+ comSetup
+ }
+
+ protected trait AbstractPhantomRunner extends AbstractExtRunner {
+
+ override protected def getVMArgs() =
+ // Add launcher file to arguments
+ additionalArgs :+ createTmpLauncherFile().getAbsolutePath
+
+ /** In phantom.js, we include JS using HTML */
+ override protected def writeJSFile(file: VirtualJSFile, writer: Writer) = {
+ file match {
+ case file: FileVirtualJSFile =>
+ val fname = htmlEscape(file.file.getAbsolutePath)
+ writer.write(
+ s"""<script type="text/javascript" src="$fname"></script>""" + "\n")
+ case _ =>
+ writer.write("""<script type="text/javascript">""" + "\n")
+ writer.write(s"// Virtual File: ${file.path}\n")
+ writer.write(file.content)
+ writer.write("</script>\n")
+ }
+ }
+
+ /**
+ * PhantomJS doesn't support Function.prototype.bind. We polyfill it.
+ * https://github.com/ariya/phantomjs/issues/10522
+ */
+ override protected def initFiles(): Seq[VirtualJSFile] = Seq(
+ new MemVirtualJSFile("bindPolyfill.js").withContent(
+ """
+ |// Polyfill for Function.bind from Facebook react:
+ |// https://github.com/facebook/react/blob/3dc10749080a460e48bee46d769763ec7191ac76/src/test/phantomjs-shims.js
+ |// Originally licensed under Apache 2.0
+ |(function() {
+ |
+ | var Ap = Array.prototype;
+ | var slice = Ap.slice;
+ | var Fp = Function.prototype;
+ |
+ | if (!Fp.bind) {
+ | // PhantomJS doesn't support Function.prototype.bind natively, so
+ | // polyfill it whenever this module is required.
+ | Fp.bind = function(context) {
+ | var func = this;
+ | var args = slice.call(arguments, 1);
+ |
+ | function bound() {
+ | var invokedAsConstructor = func.prototype && (this instanceof func);
+ | return func.apply(
+ | // Ignore the context parameter when invoking the bound function
+ | // as a constructor. Note that this includes not only constructor
+ | // invocations using the new keyword but also calls to base class
+ | // constructors such as BaseClass.call(this, ...) or super(...).
+ | !invokedAsConstructor && context || this,
+ | args.concat(slice.call(arguments))
+ | );
+ | }
+ |
+ | // The bound function must share the .prototype of the unbound
+ | // function so that any object created by one constructor will count
+ | // as an instance of both constructors.
+ | bound.prototype = func.prototype;
+ |
+ | return bound;
+ | };
+ | }
+ |
+ |})();
+ |""".stripMargin
+ ),
+ new MemVirtualJSFile("scalaJSEnvInfo.js").withContent(
+ """
+ |__ScalaJSEnv = {
+ | exitFunction: function(status) {
+ | window.callPhantom({
+ | action: 'exit',
+ | returnValue: status | 0
+ | });
+ | }
+ |};
+ """.stripMargin
+ )
+ )
+
+ protected def writeWebpageLauncher(out: Writer): Unit = {
+ out.write("<html>\n<head>\n<title>Phantom.js Launcher</title>\n")
+ sendJS(getLibJSFiles(), out)
+ writeCodeLauncher(code, out)
+ out.write("</head>\n<body></body>\n</html>\n")
+ }
+
+ protected def createTmpLauncherFile(): File = {
+ val webF = createTmpWebpage()
+
+ val launcherTmpF = File.createTempFile("phantomjs-launcher", ".js")
+ launcherTmpF.deleteOnExit()
+
+ val out = new FileWriter(launcherTmpF)
+
+ try {
+ out.write(
+ s"""// Scala.js Phantom.js launcher
+ |var page = require('webpage').create();
+ |var url = ${toJSstr(webF.getAbsolutePath)};
+ |var autoExit = $autoExit;
+ |page.onConsoleMessage = function(msg) {
+ | console.log(msg);
+ |};
+ |page.onError = function(msg, trace) {
+ | console.error(msg);
+ | if (trace && trace.length) {
+ | console.error('');
+ | trace.forEach(function(t) {
+ | console.error(' ' + t.file + ':' + t.line + (t.function ? ' (in function "' + t.function +'")' : ''));
+ | });
+ | }
+ |
+ | phantom.exit(2);
+ |};
+ |page.onCallback = function(data) {
+ | if (!data.action) {
+ | console.error('Called callback without action');
+ | phantom.exit(3);
+ | } else if (data.action === 'exit') {
+ | phantom.exit(data.returnValue || 0);
+ | } else if (data.action === 'setAutoExit') {
+ | if (typeof(data.autoExit) === 'boolean')
+ | autoExit = data.autoExit;
+ | else
+ | autoExit = true;
+ | } else {
+ | console.error('Unknown callback action ' + data.action);
+ | phantom.exit(4);
+ | }
+ |};
+ |page.open(url, function (status) {
+ | if (autoExit || status !== 'success')
+ | phantom.exit(status !== 'success');
+ |});
+ |""".stripMargin)
+ } finally {
+ out.close()
+ }
+
+ logger.debug(
+ "PhantomJS using launcher at: " + launcherTmpF.getAbsolutePath())
+
+ launcherTmpF
+ }
+
+ protected def createTmpWebpage(): File = {
+ val webTmpF = File.createTempFile("phantomjs-launcher-webpage", ".html")
+ webTmpF.deleteOnExit()
+
+ val out = new BufferedWriter(new FileWriter(webTmpF))
+ try {
+ writeWebpageLauncher(out)
+ } finally {
+ out.close()
+ }
+
+ logger.debug(
+ "PhantomJS using webpage launcher at: " + webTmpF.getAbsolutePath())
+
+ webTmpF
+ }
+
+ protected def writeCodeLauncher(code: VirtualJSFile, out: Writer): Unit = {
+ out.write("""<script type="text/javascript">""" + "\n")
+ out.write("// Phantom.js code launcher\n")
+ out.write(s"// Origin: ${code.path}\n")
+ out.write("window.addEventListener('load', function() {\n")
+ out.write(code.content)
+ out.write("}, false);\n")
+ out.write("</script>\n")
+ }
+ }
+
+ protected def htmlEscape(str: String): String = str.flatMap {
+ case '<' => "&lt;"
+ case '>' => "&gt;"
+ case '"' => "&quot;"
+ case '&' => "&amp;"
+ case c => c :: Nil
+ }
+
+}
+
+object PhantomJSEnv {
+ private final val MaxByteMessageSize = 32768 // 32 KB
+ private final val MaxCharMessageSize = MaxByteMessageSize / 2 // 2B per char
+ private final val MaxCharPayloadSize = MaxCharMessageSize - 1 // frag flag
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/PhantomJettyClassLoader.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/PhantomJettyClassLoader.scala
new file mode 100644
index 0000000..02c229b
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/PhantomJettyClassLoader.scala
@@ -0,0 +1,63 @@
+package scala.scalajs.sbtplugin.env.phantomjs
+
+import scala.scalajs.tools.io.IO
+
+/** A special [[ClassLoader]] to load the Jetty 8 dependency of [[PhantomJSEnv]]
+ * in a private space.
+ *
+ * It loads everything that belongs to [[JettyWebsocketManager]] itself (while
+ * retrieving the requested class file from its parent.
+ * For all other classes, it first tries to load them from [[jettyLoader]],
+ * which should only contain the Jetty 8 classpath.
+ * If this fails, it delegates to its parent.
+ *
+ * The rationale is, that [[JettyWebsocketManager]] and its dependees can use
+ * the classes on the Jetty 8 classpath, while they remain hidden from the rest
+ * of the Java world. This allows to load another version of Jetty in the same
+ * JVM for the rest of the project.
+ */
+private[sbtplugin] class PhantomJettyClassLoader(jettyLoader: ClassLoader,
+ parent: ClassLoader) extends ClassLoader(parent) {
+
+ def this(loader: ClassLoader) =
+ this(loader, ClassLoader.getSystemClassLoader())
+
+ /** Classes needed to bridge private jetty classpath and public PhantomJS
+ * Basically everything defined in JettyWebsocketManager.
+ */
+ private val bridgeClasses = Set(
+ "scala.scalajs.sbtplugin.env.phantomjs.JettyWebsocketManager",
+ "scala.scalajs.sbtplugin.env.phantomjs.JettyWebsocketManager$WSLogger",
+ "scala.scalajs.sbtplugin.env.phantomjs.JettyWebsocketManager$ComWebSocketListener",
+ "scala.scalajs.sbtplugin.env.phantomjs.JettyWebsocketManager$$anon$1",
+ "scala.scalajs.sbtplugin.env.phantomjs.JettyWebsocketManager$$anon$2"
+ )
+
+ override protected def loadClass(name: String, resolve: Boolean): Class[_] = {
+ if (bridgeClasses.contains(name)) {
+ // Load bridgeClasses manually since they must be associated to this
+ // class loader, rather than the parent class loader in order to find the
+ // jetty classes
+
+ // First check if we have loaded it already
+ Option(findLoadedClass(name)) getOrElse {
+ val wsManager =
+ parent.getResourceAsStream(name.replace('.', '/') + ".class")
+
+ if (wsManager == null) {
+ throw new ClassNotFoundException(name)
+ } else {
+ val buf = IO.readInputStreamToByteArray(wsManager)
+ defineClass(name, buf, 0, buf.length)
+ }
+ }
+ } else {
+ try {
+ jettyLoader.loadClass(name)
+ } catch {
+ case _: ClassNotFoundException =>
+ super.loadClass(name, resolve)
+ }
+ }
+ }
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/WebsocketListener.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/WebsocketListener.scala
new file mode 100644
index 0000000..4faac64
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/WebsocketListener.scala
@@ -0,0 +1,10 @@
+package scala.scalajs.sbtplugin.env.phantomjs
+
+private[phantomjs] trait WebsocketListener {
+ def onRunning(): Unit
+ def onOpen(): Unit
+ def onClose(): Unit
+ def onMessage(msg: String): Unit
+
+ def log(msg: String): Unit
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/WebsocketManager.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/WebsocketManager.scala
new file mode 100644
index 0000000..a466841
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/phantomjs/WebsocketManager.scala
@@ -0,0 +1,10 @@
+package scala.scalajs.sbtplugin.env.phantomjs
+
+private[phantomjs] trait WebsocketManager {
+ def start(): Unit
+ def stop(): Unit
+ def sendMessage(msg: String): Unit
+ def localPort: Int
+ def isConnected: Boolean
+ def isClosed: Boolean
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/LazyScalaJSScope.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/LazyScalaJSScope.scala
new file mode 100644
index 0000000..d4cdaee
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/LazyScalaJSScope.scala
@@ -0,0 +1,96 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin.env.rhino
+
+import scala.collection.mutable
+
+import org.mozilla.javascript.Scriptable
+
+/** A proxy for a ScalaJS "scope" field that loads scripts lazily
+ *
+ * E.g., ScalaJS.c, which is a scope with the Scala.js classes, can be
+ * turned to a LazyScalaJSScope. Upon first access to a field of ScalaJS.c,
+ * say ScalaJS.c.scala_Option, the script defining that particular
+ * field will be loaded.
+ * This is possible because the relative path to the script can be derived
+ * from the name of the property being accessed.
+ *
+ * It is immensely useful, because it allows to load lazily only the scripts
+ * that are actually needed.
+ */
+class LazyScalaJSScope(
+ coreLib: ScalaJSCoreLib,
+ globalScope: Scriptable,
+ base: Scriptable,
+ isModule: Boolean = false,
+ isTraitImpl: Boolean = false) extends Scriptable {
+
+ private val fields = mutable.HashMap.empty[String, Any]
+ private var prototype: Scriptable = _
+ private var parentScope: Scriptable = _
+
+ {
+ // Pre-fill fields with the properties of `base`
+ for (id <- base.getIds()) {
+ (id.asInstanceOf[Any]: @unchecked) match {
+ case name: String => put(name, this, base.get(name, base))
+ case index: Int => put(index, this, base.get(index, base))
+ }
+ }
+ }
+
+ private def load(name: String): Unit =
+ coreLib.load(globalScope, propNameToEncodedName(name))
+
+ private def propNameToEncodedName(name: String): String = {
+ if (isTraitImpl) name.split("__")(0)
+ else if (isModule) name + "$"
+ else name
+ }
+
+ override def getClassName() = "LazyScalaJSScope"
+
+ override def get(name: String, start: Scriptable) = {
+ fields.getOrElse(name, {
+ load(name)
+ fields.getOrElse(name, Scriptable.NOT_FOUND)
+ }).asInstanceOf[AnyRef]
+ }
+ override def get(index: Int, start: Scriptable) =
+ get(index.toString, start)
+
+ override def has(name: String, start: Scriptable) =
+ fields.contains(name)
+ override def has(index: Int, start: Scriptable) =
+ has(index.toString, start)
+
+ override def put(name: String, start: Scriptable, value: Any) = {
+ fields(name) = value
+ }
+ override def put(index: Int, start: Scriptable, value: Any) =
+ put(index.toString, start, value)
+
+ override def delete(name: String) = ()
+ override def delete(index: Int) = ()
+
+ override def getPrototype() = prototype
+ override def setPrototype(value: Scriptable) = prototype = value
+
+ override def getParentScope() = parentScope
+ override def setParentScope(value: Scriptable) = parentScope = value
+
+ override def getIds() = fields.keys.toArray
+
+ override def getDefaultValue(hint: java.lang.Class[_]) = {
+ base.getDefaultValue(hint)
+ }
+
+ override def hasInstance(instance: Scriptable) = false
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/RhinoJSEnv.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/RhinoJSEnv.scala
new file mode 100644
index 0000000..cd35ff6
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/RhinoJSEnv.scala
@@ -0,0 +1,303 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin.env.rhino
+
+import scala.scalajs.tools.sem.Semantics
+import scala.scalajs.tools.io._
+import scala.scalajs.tools.classpath._
+import scala.scalajs.tools.env._
+import scala.scalajs.tools.logging._
+
+import scala.io.Source
+
+import scala.collection.mutable
+
+import scala.concurrent.{Future, Promise, Await}
+import scala.concurrent.duration.Duration
+
+import org.mozilla.javascript._
+
+class RhinoJSEnv(semantics: Semantics,
+ withDOM: Boolean = false) extends ComJSEnv {
+
+ import RhinoJSEnv._
+
+ /** Executes code in an environment where the Scala.js library is set up to
+ * load its classes lazily.
+ *
+ * Other .js scripts in the inputs are executed eagerly before the provided
+ * `code` is called.
+ */
+ override def jsRunner(classpath: CompleteClasspath, code: VirtualJSFile,
+ logger: Logger, console: JSConsole): JSRunner = {
+ new Runner(classpath, code, logger, console)
+ }
+
+ private class Runner(classpath: CompleteClasspath, code: VirtualJSFile,
+ logger: Logger, console: JSConsole) extends JSRunner {
+ def run(): Unit = internalRunJS(classpath, code, logger, console, None)
+ }
+
+ override def asyncRunner(classpath: CompleteClasspath, code: VirtualJSFile,
+ logger: Logger, console: JSConsole): AsyncJSRunner = {
+ new AsyncRunner(classpath, code, logger, console)
+ }
+
+ private class AsyncRunner(classpath: CompleteClasspath, code: VirtualJSFile,
+ logger: Logger, console: JSConsole) extends AsyncJSRunner {
+
+ private[this] val promise = Promise[Unit]
+
+ private[this] val thread = new Thread {
+ override def run(): Unit = {
+ try {
+ internalRunJS(classpath, code, logger, console, optChannel)
+ promise.success(())
+ } catch {
+ case t: Throwable =>
+ promise.failure(t)
+ }
+ }
+ }
+
+ def start(): Future[Unit] = {
+ thread.start()
+ promise.future
+ }
+
+ def stop(): Unit = thread.interrupt()
+
+ def isRunning(): Boolean = !promise.isCompleted
+
+ def await(): Unit = Await.result(promise.future, Duration.Inf)
+
+ protected def optChannel(): Option[Channel] = None
+ }
+
+ override def comRunner(classpath: CompleteClasspath, code: VirtualJSFile,
+ logger: Logger, console: JSConsole): ComJSRunner = {
+ new ComRunner(classpath, code, logger, console)
+ }
+
+ private class ComRunner(classpath: CompleteClasspath, code: VirtualJSFile,
+ logger: Logger, console: JSConsole)
+ extends AsyncRunner(classpath, code, logger, console) with ComJSRunner {
+
+ private[this] val channel = new Channel
+
+ override protected def optChannel(): Option[Channel] = Some(channel)
+
+ def send(msg: String): Unit = {
+ try {
+ channel.sendToJS(msg)
+ } catch {
+ case _: ChannelClosedException =>
+ throw new ComJSEnv.ComClosedException
+ }
+ }
+
+ def receive(): String = {
+ try {
+ channel.recvJVM()
+ } catch {
+ case _: ChannelClosedException =>
+ throw new ComJSEnv.ComClosedException
+ }
+ }
+
+ def close(): Unit = channel.close()
+
+ override def stop(): Unit = {
+ close()
+ super.stop()
+ }
+
+ }
+
+ private def internalRunJS(classpath: CompleteClasspath, code: VirtualJSFile,
+ logger: Logger, console: JSConsole, optChannel: Option[Channel]): Unit = {
+
+ val context = Context.enter()
+ try {
+ val scope = context.initStandardObjects()
+
+ if (withDOM) {
+ // Fetch env.rhino.js from webjar
+ val name = "env.rhino.js"
+ val path = "/META-INF/resources/webjars/envjs/1.2/" + name
+ val resource = getClass.getResource(path)
+ assert(resource != null, s"need $name as resource")
+
+ // Rhino can't optimize envjs
+ context.setOptimizationLevel(-1)
+
+ // Don't print envjs header
+ scope.addFunction("print", args => ())
+
+ // Pipe file to Rhino
+ val reader = Source.fromURL(resource).bufferedReader
+ context.evaluateReader(scope, reader, name, 1, null);
+
+ // No need to actually define print here: It is captured by envjs to
+ // implement console.log, which we'll override in the next statement
+ }
+
+ // Make sure Rhino does not do its magic for JVM top-level packages (#364)
+ val PackagesObject =
+ ScriptableObject.getProperty(scope, "Packages").asInstanceOf[Scriptable]
+ val topLevelPackageIds = ScriptableObject.getPropertyIds(PackagesObject)
+ for (id <- topLevelPackageIds) (id: Any) match {
+ case name: String => ScriptableObject.deleteProperty(scope, name)
+ case index: Int => ScriptableObject.deleteProperty(scope, index)
+ case _ => // should not happen, I think, but with Rhino you never know
+ }
+
+ // Setup console.log
+ val jsconsole = context.newObject(scope)
+ jsconsole.addFunction("log", _.foreach(console.log _))
+ ScriptableObject.putProperty(scope, "console", jsconsole)
+
+ // Optionally setup scalaJSCom
+ var recvCallback: Option[String => Unit] = None
+ for (channel <- optChannel) {
+ val comObj = context.newObject(scope)
+
+ comObj.addFunction("send", s =>
+ channel.sendToJVM(Context.toString(s(0))))
+
+ comObj.addFunction("init", s => s(0) match {
+ case f: Function =>
+ val cb: String => Unit =
+ msg => f.call(context, scope, scope, Array(msg))
+ recvCallback = Some(cb)
+ case _ =>
+ sys.error("First argument to init must be a function")
+ })
+
+ comObj.addFunction("close", _ => {
+ // Tell JVM side we won't send anything
+ channel.close()
+ // Internally register that we're done
+ recvCallback = None
+ })
+
+ ScriptableObject.putProperty(scope, "scalajsCom", comObj)
+ }
+
+ try {
+ // Make the classpath available. Either through lazy loading or by
+ // simply inserting
+ classpath match {
+ case cp: IRClasspath =>
+ // Setup lazy loading classpath and source mapper
+ val optLoader = if (cp.scalaJSIR.nonEmpty) {
+ val loader = new ScalaJSCoreLib(semantics, cp)
+
+ // Setup sourceMapper
+ val scalaJSenv = context.newObject(scope)
+
+ scalaJSenv.addFunction("sourceMapper", args => {
+ val trace = Context.toObject(args(0), scope)
+ loader.mapStackTrace(trace, context, scope)
+ })
+
+ ScriptableObject.putProperty(scope, "__ScalaJSEnv", scalaJSenv)
+
+ Some(loader)
+ } else {
+ None
+ }
+
+ // Load JS libraries
+ cp.jsLibs.foreach(dep => context.evaluateFile(scope, dep.lib))
+
+ optLoader.foreach(_.insertInto(context, scope))
+ case cp =>
+ cp.allCode.foreach(context.evaluateFile(scope, _))
+ }
+
+ context.evaluateFile(scope, code)
+
+ // Callback the com channel if necessary (if comCallback = None, channel
+ // wasn't initialized on the client)
+ for ((channel, callback) <- optChannel zip recvCallback) {
+ try {
+ while (recvCallback.isDefined)
+ callback(channel.recvJS())
+ } catch {
+ case _: ChannelClosedException =>
+ // the JVM side closed the connection
+ }
+ }
+
+ // Enusre the channel is closed to release JVM side
+ optChannel.foreach(_.close)
+
+ } catch {
+ case e: RhinoException =>
+ // Trace here, since we want to be in the context to trace.
+ logger.trace(e)
+ sys.error(s"Exception while running JS code: ${e.getMessage}")
+ }
+ } finally {
+ Context.exit()
+ }
+ }
+
+}
+
+object RhinoJSEnv {
+
+ /** Communication channel between the Rhino thread and the rest of the JVM */
+ private class Channel {
+ private[this] var _closed = false
+ private[this] val js2jvm = mutable.Queue.empty[String]
+ private[this] val jvm2js = mutable.Queue.empty[String]
+
+ def sendToJS(msg: String): Unit = synchronized {
+ jvm2js.enqueue(msg)
+ notify()
+ }
+
+ def sendToJVM(msg: String): Unit = synchronized {
+ js2jvm.enqueue(msg)
+ notify()
+ }
+
+ def recvJVM(): String = synchronized {
+ while (js2jvm.isEmpty && ensureOpen())
+ wait()
+
+ js2jvm.dequeue()
+ }
+
+ def recvJS(): String = synchronized {
+ while (jvm2js.isEmpty && ensureOpen())
+ wait()
+
+ jvm2js.dequeue()
+ }
+
+ def close(): Unit = synchronized {
+ _closed = true
+ notify()
+ }
+
+ /** Throws if the channel is closed and returns true */
+ private def ensureOpen(): Boolean = {
+ if (_closed)
+ throw new ChannelClosedException
+ true
+ }
+ }
+
+ private class ChannelClosedException extends Exception
+
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/ScalaJSCoreLib.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/ScalaJSCoreLib.scala
new file mode 100644
index 0000000..e937e5b
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/ScalaJSCoreLib.scala
@@ -0,0 +1,173 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin.env.rhino
+
+import scala.collection.mutable
+
+import org.mozilla.javascript.{Context, Scriptable}
+
+import scala.scalajs.ir
+
+import scala.scalajs.tools.sem.Semantics
+import scala.scalajs.tools.javascript.{Printers, ScalaJSClassEmitter}
+import scala.scalajs.tools.io._
+import scala.scalajs.tools.classpath._
+import scala.scalajs.tools.corelib._
+
+class ScalaJSCoreLib(semantics: Semantics, classpath: IRClasspath) {
+ import ScalaJSCoreLib._
+
+ private val (providers, exportedSymbols) = {
+ val providers = mutable.Map.empty[String, VirtualScalaJSIRFile]
+ val exportedSymbols = mutable.ListBuffer.empty[String]
+
+ for (irFile <- classpath.scalaJSIR) {
+ val info = irFile.roughInfo
+ providers += info.encodedName -> irFile
+ if (info.isExported)
+ exportedSymbols += info.encodedName
+ }
+
+ (providers, exportedSymbols)
+ }
+
+ def insertInto(context: Context, scope: Scriptable) = {
+ CoreJSLibs.libs(semantics).foreach(context.evaluateFile(scope, _))
+ lazifyScalaJSFields(scope)
+
+ // Make sure exported symbols are loaded
+ val ScalaJS = Context.toObject(scope.get("ScalaJS", scope), scope)
+ val c = Context.toObject(ScalaJS.get("c", ScalaJS), scope)
+ for (encodedName <- exportedSymbols)
+ c.get(encodedName, c)
+ }
+
+ /** Source maps the given stack trace (where possible) */
+ def mapStackTrace(stackTrace: Scriptable,
+ context: Context, scope: Scriptable): Scriptable = {
+ val count = Context.toNumber(stackTrace.get("length", stackTrace)).toInt
+
+ // Maps file -> max line (0-based)
+ val neededMaps = mutable.Map.empty[String, Int]
+
+ // Collect required line counts
+ for (i <- 0 until count) {
+ val elem = Context.toObject(stackTrace.get(i, stackTrace), scope)
+ val fileName = Context.toString(elem.get("fileName", elem))
+
+ if (fileName.endsWith(PseudoFileSuffix) &&
+ providers.contains(fileName.stripSuffix(PseudoFileSuffix))) {
+
+ val curMaxLine = neededMaps.getOrElse(fileName, -1)
+ val reqLine = Context.toNumber(elem.get("lineNumber", elem)).toInt - 1
+
+ if (reqLine > curMaxLine)
+ neededMaps.put(fileName, reqLine)
+ }
+ }
+
+ // Map required files
+ val maps =
+ for ((fileName, maxLine) <- neededMaps)
+ yield (fileName, getSourceMapper(fileName, maxLine))
+
+ // Create new stack trace to return
+ val res = context.newArray(scope, count)
+
+ for (i <- 0 until count) {
+ val elem = Context.toObject(stackTrace.get(i, stackTrace), scope)
+ val fileName = Context.toString(elem.get("fileName", elem))
+ val line = Context.toNumber(elem.get("lineNumber", elem)).toInt - 1
+
+ val pos = maps.get(fileName).fold(ir.Position.NoPosition)(_(line))
+
+ val newElem =
+ if (pos.isDefined) newPosElem(scope, context, elem, pos)
+ else elem
+
+ res.put(i, res, newElem)
+ }
+
+ res
+ }
+
+ private def getSourceMapper(fileName: String, untilLine: Int) = {
+ val irFile = providers(fileName.stripSuffix(PseudoFileSuffix))
+ val mapper = new Printers.ReverseSourceMapPrinter(untilLine)
+ val classDef = irFile.tree
+ val desugared = new ScalaJSClassEmitter(semantics).genClassDef(classDef)
+ mapper.reverseSourceMap(desugared)
+ mapper
+ }
+
+ private def newPosElem(scope: Scriptable, context: Context,
+ origElem: Scriptable, pos: ir.Position): Scriptable = {
+ assert(pos.isDefined)
+
+ val elem = context.newObject(scope)
+
+ elem.put("declaringClass", elem, origElem.get("declaringClass", origElem))
+ elem.put("methodName", elem, origElem.get("methodName", origElem))
+ elem.put("fileName", elem, pos.source.toString)
+ elem.put("lineNumber", elem, pos.line + 1)
+ elem.put("columnNumber", elem, pos.column + 1)
+
+ elem
+ }
+
+ private val scalaJSLazyFields = Seq(
+ Info("d"),
+ Info("c"),
+ Info("h"),
+ Info("i", isTraitImpl = true),
+ Info("n", isModule = true),
+ Info("m", isModule = true),
+ Info("is"),
+ Info("as"),
+ Info("isArrayOf"),
+ Info("asArrayOf"))
+
+ private def lazifyScalaJSFields(scope: Scriptable) = {
+ val ScalaJS = Context.toObject(scope.get("ScalaJS", scope), scope)
+
+ def makeLazyScalaJSScope(base: Scriptable, isModule: Boolean, isTraitImpl: Boolean) =
+ new LazyScalaJSScope(this, scope, base, isModule, isTraitImpl)
+
+ for (Info(name, isModule, isTraitImpl) <- scalaJSLazyFields) {
+ val base = ScalaJS.get(name, ScalaJS).asInstanceOf[Scriptable]
+ val lazified = makeLazyScalaJSScope(base, isModule, isTraitImpl)
+ ScalaJS.put(name, ScalaJS, lazified)
+ }
+ }
+
+ private[rhino] def load(scope: Scriptable, encodedName: String): Unit = {
+ providers.get(encodedName) foreach { irFile =>
+ val codeWriter = new java.io.StringWriter
+ val printer = new Printers.JSTreePrinter(codeWriter)
+ val classDef = irFile.tree
+ val desugared = new ScalaJSClassEmitter(semantics).genClassDef(classDef)
+ printer.printTopLevelTree(desugared)
+ printer.complete()
+ val ctx = Context.getCurrentContext()
+ val fakeFileName = encodedName + PseudoFileSuffix
+ ctx.evaluateString(scope, codeWriter.toString(),
+ fakeFileName, 1, null)
+ }
+ }
+}
+
+object ScalaJSCoreLib {
+ private case class Info(name: String,
+ isModule: Boolean = false, isTraitImpl: Boolean = false)
+
+ private val EncodedNameLine = raw""""encodedName": *"([^"]+)"""".r.unanchored
+
+ private final val PseudoFileSuffix = ".sjsir"
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/package.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/package.scala
new file mode 100644
index 0000000..926fbb2
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/env/rhino/package.scala
@@ -0,0 +1,42 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin.env
+
+import org.mozilla.javascript._
+
+import scala.scalajs.tools.io._
+
+package object rhino {
+
+ implicit class ContextOps(val self: Context) extends AnyVal {
+ def evaluateFile(scope: Scriptable, file: VirtualJSFile,
+ securityDomain: AnyRef = null): Any = {
+ self.evaluateString(scope, file.content, file.path, 1, securityDomain)
+ }
+ }
+
+ implicit class ScriptableObjectOps(val self: Scriptable) {
+ def addFunction(name: String, function: Array[AnyRef] => Any) = {
+ val rhinoFunction =
+ new BaseFunction {
+ ScriptRuntime.setFunctionProtoAndParent(this, self)
+ override def call(context: Context, scope: Scriptable,
+ thisObj: Scriptable, args: Array[AnyRef]): AnyRef = {
+ function(args) match {
+ case () => Undefined.instance
+ case r => r.asInstanceOf[AnyRef]
+ }
+ }
+ }
+
+ ScriptableObject.putProperty(self, name, rhinoFunction)
+ }
+ }
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/impl/DependencyBuilders.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/impl/DependencyBuilders.scala
new file mode 100644
index 0000000..32ffb94
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/impl/DependencyBuilders.scala
@@ -0,0 +1,99 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin
+package impl
+
+import scala.language.implicitConversions
+import scala.language.experimental.macros
+
+import sbt._
+
+import StringUtilities.nonEmpty
+
+trait DependencyBuilders {
+ final implicit def toScalaJSGroupID(groupID: String): ScalaJSGroupID = {
+ nonEmpty(groupID, "Group ID")
+ new ScalaJSGroupID(groupID)
+ }
+
+ /** Builder to allow for stuff like:
+ *
+ * ProvidedJS / "foo.js"
+ * ProvidedJS / "foo.js" % "test"
+ *
+ */
+ object ProvidedJS {
+ def /(name: String): ProvidedJSModuleID = ProvidedJSModuleID(name, None)
+ }
+
+ /** Builder to allow for stuff like:
+ *
+ * "org.webjars" % "jquery" % "1.10.2" / "jquery.js"
+ * "org.webjars" % "jquery" % "1.10.2" / "jquery.js" % "test"
+ *
+ */
+ implicit class JSModuleIDBuilder(module: ModuleID) {
+ def /(name: String): JarJSModuleID = JarJSModuleID(module, name)
+ }
+}
+
+final class ScalaJSGroupID private[sbtplugin] (private val groupID: String) {
+ def %%%(artifactID: String): CrossGroupArtifactID =
+ macro ScalaJSGroupID.auto_impl
+
+ def %%%!(artifactID: String): CrossGroupArtifactID =
+ ScalaJSGroupID.withCross(this, artifactID, ScalaJSCrossVersion.binary)
+}
+
+object ScalaJSGroupID {
+ import scala.reflect.macros.Context
+
+ /** Internal. Used by the macro implementing [[ScalaJSGroupID.%%%]]. Use:
+ * {{{
+ * ("a" % artifactID % revision).cross(cross)
+ * }}}
+ * instead.
+ */
+ def withCross(groupID: ScalaJSGroupID, artifactID: String,
+ cross: CrossVersion): CrossGroupArtifactID = {
+ nonEmpty(artifactID, "Artifact ID")
+ new CrossGroupArtifactID(groupID.groupID, artifactID, cross)
+ }
+
+ def auto_impl(c: Context { type PrefixType = ScalaJSGroupID })(
+ artifactID: c.Expr[String]): c.Expr[CrossGroupArtifactID] = {
+ import c.universe._
+
+ // Hack to work around bug in sbt macros (wrong way of collecting local
+ // definitions)
+ val keysSym = rootMirror.staticModule(
+ "_root_.scala.scalajs.sbtplugin.ScalaJSPlugin.autoImport")
+ val keys = c.Expr[ScalaJSPlugin.autoImport.type](Ident(keysSym))
+
+ reify {
+ val cross = {
+ if (keys.splice.jsDependencies.?.value.isDefined)
+ ScalaJSCrossVersion.binary
+ else
+ CrossVersion.binary
+ }
+ ScalaJSGroupID.withCross(c.prefix.splice, artifactID.splice, cross)
+ }
+ }
+
+}
+
+final class CrossGroupArtifactID(groupID: String,
+ artifactID: String, crossVersion: CrossVersion) {
+ def %(revision: String): ModuleID = {
+ nonEmpty(revision, "Revision")
+ ModuleID(groupID, artifactID, revision).cross(crossVersion)
+ }
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/Events.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/Events.scala
new file mode 100644
index 0000000..f13c195
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/Events.scala
@@ -0,0 +1,35 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin.testing
+
+import sbt.testing.{Event => SbtEvent, _}
+
+class Events(taskDef: TaskDef) {
+
+ abstract class Event(val status: Status,
+ val throwable: OptionalThrowable = new OptionalThrowable) extends SbtEvent {
+ val fullyQualifiedName = taskDef.fullyQualifiedName
+ val fingerprint = taskDef.fingerprint
+ val selector = taskDef.selectors.headOption.getOrElse(new SuiteSelector)
+ val duration = -1L
+ }
+
+ case class Error(exception: Throwable) extends Event(
+ Status.Error, new OptionalThrowable(exception))
+
+ case class Failure(exception: Throwable) extends Event(
+ Status.Failure, new OptionalThrowable(exception))
+
+ case object Succeeded extends Event(Status.Success)
+ case object Skipped extends Event(Status.Skipped)
+ case object Pending extends Event(Status.Pending)
+ case object Ignored extends Event(Status.Ignored)
+ case object Canceled extends Event(Status.Canceled)
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/JSClasspathLoader.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/JSClasspathLoader.scala
new file mode 100644
index 0000000..bfe0ffc
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/JSClasspathLoader.scala
@@ -0,0 +1,15 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin.testing
+
+import scala.scalajs.tools.classpath.CompleteClasspath
+
+/** A dummy ClassLoader to pass on Scala.js ClasspathContents to tests */
+final case class JSClasspathLoader(cp: CompleteClasspath) extends ClassLoader
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/SbtTestLoggerAccWrapper.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/SbtTestLoggerAccWrapper.scala
new file mode 100644
index 0000000..dfebe00
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/SbtTestLoggerAccWrapper.scala
@@ -0,0 +1,22 @@
+package scala.scalajs.sbtplugin.testing
+
+import scala.scalajs.tools.logging._
+import sbt.testing.{ Logger => SbtTestLogger }
+
+class SbtTestLoggerAccWrapper(logger: Seq[SbtTestLogger]) extends Logger {
+
+ import scala.scalajs.sbtplugin.Implicits._
+ import Level._
+
+ def log(level: Level, message: => String): Unit = level match {
+ case Error => logger.foreach(_.error(message))
+ case Warn => logger.foreach(_.warn(message))
+ case Info => logger.foreach(_.info(message))
+ case Debug => logger.foreach(_.debug(message))
+ }
+
+ def success(message: => String): Unit = logger.foreach(_.info(message))
+
+ def trace(t: => Throwable): Unit = logger.foreach(_.trace(t))
+
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestException.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestException.scala
new file mode 100644
index 0000000..b4cb09b
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestException.scala
@@ -0,0 +1,9 @@
+package scala.scalajs.sbtplugin.testing
+
+/** Dummy Exception to wrap stack traces passed to SBT */
+class TestException(
+ message: String,
+ stackTrace: Array[StackTraceElement]
+) extends Exception(message) {
+ override def getStackTrace = stackTrace
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestFramework.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestFramework.scala
new file mode 100644
index 0000000..ab43bfe
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestFramework.scala
@@ -0,0 +1,52 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin.testing
+
+import scala.scalajs.tools.env._
+import scala.scalajs.tools.classpath._
+
+import sbt._
+import sbt.testing._
+import sbt.classpath.ClasspathFilter
+
+import java.net.URLClassLoader
+
+class TestFramework(
+ environment: JSEnv,
+ jsConsole: JSConsole,
+ testFramework: String) extends Framework {
+
+ val name = "Scala.js Test Framework"
+
+ lazy val fingerprints = Array[Fingerprint](f1)
+
+ private val f1 = new SubclassFingerprint {
+ val isModule = true
+ val superclassName = "scala.scalajs.testbridge.Test"
+ val requireNoArgConstructor = true
+ }
+
+ def runner(args: Array[String], remoteArgs: Array[String],
+ testClassLoader: ClassLoader): Runner = {
+
+ val jsClasspath = extractClasspath(testClassLoader)
+ new TestRunner(environment, jsClasspath, jsConsole,
+ testFramework, args, remoteArgs)
+ }
+
+ /** extract classpath from ClassLoader (which must be a JSClasspathLoader) */
+ private def extractClasspath(cl: ClassLoader) = cl match {
+ case cl: JSClasspathLoader => cl.cp
+ case _ =>
+ sys.error("The Scala.js framework only works with a class loader of " +
+ s"type JSClasspathLoader (${cl.getClass} given)")
+ }
+
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestOutputConsole.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestOutputConsole.scala
new file mode 100644
index 0000000..9aad956
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestOutputConsole.scala
@@ -0,0 +1,190 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin.testing
+
+import sbt.testing.Logger
+import sbt.testing.EventHandler
+
+import scala.scalajs.tools.env.JSConsole
+import scala.scalajs.tools.sourcemap.SourceMapper
+import scala.scalajs.tools.classpath.{CompleteClasspath, IRClasspath}
+
+import scala.collection.mutable.ArrayBuffer
+
+import scala.util.Try
+
+import java.util.regex._
+
+/** This parses the messages sent from the test bridge and forwards
+ * the calls to SBT. It also buffers all log messages and allows to
+ * pipe them to multiple loggers in a synchronized fashion. This
+ * ensures that log messages aren't interleaved due to parallelism.
+ */
+class TestOutputConsole(
+ base: JSConsole,
+ handler: EventHandler,
+ events: Events,
+ classpath: CompleteClasspath,
+ noSourceMap: Boolean) extends JSConsole {
+
+ import TestOutputConsole._
+ import events._
+
+ private val traceBuf = new ArrayBuffer[StackTraceElement]
+ private val logBuffer = new ArrayBuffer[LogElement]
+
+ /* See #727: source mapping does not work with CompleteIRClasspath, so
+ * don't bother to try.
+ */
+ private val ignoreSourceMapping =
+ noSourceMap || classpath.isInstanceOf[IRClasspath]
+
+ private lazy val sourceMapper = new SourceMapper(classpath)
+
+ override def log(msg: Any): Unit = {
+ val data = msg.toString
+ val sepPos = data.indexOf("|")
+
+ if (sepPos == -1)
+ log(_.error, s"Malformed message: $data")
+ else {
+ val op = data.substring(0, sepPos)
+ val message = unescape(data.substring(sepPos + 1))
+
+ op match {
+ case "console-log" =>
+ base.log(message)
+ case "error" =>
+ val trace = getTrace()
+ logWithEvent(_.error,
+ messageWithStack(message, trace),
+ Error(new TestException(message, trace))
+ )
+ case "failure" =>
+ val trace = getTrace()
+ logWithEvent(_.error,
+ messageWithStack(message, trace),
+ Failure(new TestException(message, trace))
+ )
+ case "succeeded" =>
+ noTrace()
+ logWithEvent(_.info, message, Succeeded)
+ case "skipped" =>
+ noTrace()
+ logWithEvent(_.info, message, Skipped)
+ case "pending" =>
+ noTrace()
+ logWithEvent(_.info, message, Pending)
+ case "ignored" =>
+ noTrace()
+ logWithEvent(_.info, message, Ignored)
+ case "canceled" =>
+ noTrace()
+ logWithEvent(_.info, message, Canceled)
+ case "error-log" =>
+ noTrace()
+ log(_.error, message)
+ case "info" =>
+ noTrace()
+ log(_.info, message)
+ case "warn" =>
+ noTrace()
+ log(_.warn, message)
+ case "trace" =>
+ val Array(className, methodName, fileName,
+ lineNumberStr, columnNumberStr) = message.split('|')
+
+ def tryParse(num: String, name: String) = Try(num.toInt).getOrElse {
+ log(_.warn, s"Couldn't parse $name number in StackTrace: $num")
+ -1
+ }
+
+ val lineNumber = tryParse(lineNumberStr, "line")
+ val columnNumber = tryParse(columnNumberStr, "column")
+
+ val ste =
+ new StackTraceElement(className, methodName, fileName, lineNumber)
+
+ if (ignoreSourceMapping)
+ traceBuf += ste
+ else
+ traceBuf += sourceMapper.map(ste, columnNumber)
+ case _ =>
+ noTrace()
+ log(_.error, s"Unknown op: $op. Originating log message: $data")
+ }
+ }
+ }
+
+ private def noTrace() = {
+ if (traceBuf.nonEmpty)
+ log(_.warn, s"Discarding ${traceBuf.size} stack elements")
+ traceBuf.clear()
+ }
+
+ private def getTrace() = {
+ val res = traceBuf.toArray
+ traceBuf.clear()
+ res
+ }
+
+ private def messageWithStack(message: String, stack: Array[StackTraceElement]): String =
+ message + stack.mkString("\n", "\n", "")
+
+ private def log(method: LogMethod, message: String): Unit =
+ logBuffer.append(LogElement(method, message))
+
+ private def logWithEvent(method: LogMethod,
+ message: String, event: Event): Unit = {
+ handler handle event
+ log(method, message)
+ }
+
+ def pipeLogsTo(loggers: Array[Logger]): Unit = {
+ TestOutputConsole.synchronized {
+ for {
+ LogElement(method, message) <- logBuffer
+ logger <- loggers
+ } method(logger) {
+ if (logger.ansiCodesSupported) message
+ else removeColors(message)
+ }
+ }
+ }
+
+ def allLogs: List[LogElement] = logBuffer.toList
+
+ private val colorPattern = raw"\033\[\d{1,2}m"
+
+ private def removeColors(message: String): String =
+ message.replaceAll(colorPattern, "")
+
+ private val unEscPat = Pattern.compile("(\\\\\\\\|\\\\n|\\\\r)")
+ private def unescape(message: String): String = {
+ val m = unEscPat.matcher(message)
+ val res = new StringBuffer()
+ while (m.find()) {
+ val repl = m.group() match {
+ case "\\\\" => "\\\\"
+ case "\\n" => "\n"
+ case "\\r" => "\r"
+ }
+ m.appendReplacement(res, repl);
+ }
+ m.appendTail(res);
+ res.toString
+ }
+
+}
+
+object TestOutputConsole {
+ type LogMethod = Logger => (String => Unit)
+ case class LogElement(method: LogMethod, message: String)
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestRunner.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestRunner.scala
new file mode 100644
index 0000000..e5ca2a2
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestRunner.scala
@@ -0,0 +1,37 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin.testing
+
+import sbt.testing._
+
+import scala.scalajs.tools.env._
+import scala.scalajs.tools.classpath._
+
+class TestRunner(
+ environment: JSEnv,
+ classpath: CompleteClasspath,
+ jsConsole: JSConsole,
+ testFramework: String,
+ val args: Array[String],
+ val remoteArgs: Array[String]) extends Runner {
+
+ def tasks(taskDefs: Array[TaskDef]): Array[Task] = if (_done) {
+ throw new IllegalStateException("Done has already been called")
+ } else {
+ taskDefs.map(TestTask(environment, classpath, jsConsole, testFramework, args))
+ }
+
+ def done(): String = {
+ _done = true
+ ""
+ }
+
+ private var _done = false
+}
diff --git a/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestTask.scala b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestTask.scala
new file mode 100644
index 0000000..b1cabb9
--- /dev/null
+++ b/examples/scala-js/sbt-plugin/src/main/scala/scala/scalajs/sbtplugin/testing/TestTask.scala
@@ -0,0 +1,110 @@
+/* __ *\
+** ________ ___ / / ___ __ ____ Scala.js sbt plugin **
+** / __/ __// _ | / / / _ | __ / // __/ (c) 2013, LAMP/EPFL **
+** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ **
+** /____/\___/_/ |_/____/_/ | |__/ /____/ **
+** |/____/ **
+\* */
+
+
+package scala.scalajs.sbtplugin.testing
+
+import sbt.testing._
+
+import scala.scalajs.tools.io._
+import scala.scalajs.tools.classpath._
+import scala.scalajs.tools.env._
+
+import scala.scalajs.sbtplugin.JSUtils._
+
+import scala.annotation.tailrec
+import scala.util.control.NonFatal
+
+class TestTask(
+ env: JSEnv,
+ classpath: CompleteClasspath,
+ jsConsole: JSConsole,
+ testFramework: String,
+ args: Array[String],
+ val taskDef: TaskDef) extends Task {
+
+ import TestTask._
+
+ val tags = Array.empty[String]
+ val options = readArgs(args.toList)
+
+ def execute(eventHandler: EventHandler,
+ loggers: Array[Logger]): Array[Task] = {
+
+ val runnerFile = testRunnerFile(options.frameworkArgs)
+ val testConsole = new TestOutputConsole(jsConsole, eventHandler,
+ new Events(taskDef), classpath, options.noSourceMap)
+ val logger = new SbtTestLoggerAccWrapper(loggers)
+
+ // Actually execute test
+ env.jsRunner(classpath, runnerFile, logger, testConsole).run()
+
+ testConsole.pipeLogsTo(loggers)
+
+ Array.empty
+ }
+
+ private def testRunnerFile(args: List[String]) = {
+ val testKey = taskDef.fullyQualifiedName
+
+ // Note that taskDef does also have the selector, fingerprint and
+ // explicitlySpecified value we could pass to the framework. However, we
+ // believe that these are only moderately useful. Therefore, we'll silently
+ // ignore them.
+
+ val jsArgArray = listToJS(args)
+ new MemVirtualJSFile("Generated test launcher file").
+ withContent(s"""this${dot2bracket(testFramework)}().safeRunTest(
+ | scala.scalajs.testbridge.internal.ConsoleTestOutput(),
+ | $jsArgArray,
+ | this${dot2bracket(testKey)});""".stripMargin)
+ }
+
+
+}
+
+object TestTask {
+
+ def apply(environment: JSEnv, classpath: CompleteClasspath,
+ jsConsole: JSConsole, testFramework: String, args: Array[String]
+ )(taskDef: TaskDef) =
+ new TestTask(environment, classpath, jsConsole,
+ testFramework, args, taskDef)
+
+ case class ArgOptions(
+ noSourceMap: Boolean,
+ frameworkArgs: List[String]
+ )
+
+ private def readArgs(args0: List[String]) = {
+ // State for each option
+ var noSourceMap = false
+
+ def mkOptions(frameworkArgs: List[String]) =
+ ArgOptions(noSourceMap, frameworkArgs)
+
+ @tailrec
+ def read0(args: List[String]): ArgOptions = args match {
+ case "-no-source-map" :: xs =>
+ noSourceMap = true
+ read0(xs)
+
+ // Explicitly end our argument list
+ case "--" :: xs =>
+ mkOptions(xs)
+
+ // Unknown argument
+ case xs =>
+ mkOptions(xs)
+
+ }
+
+ read0(args0)
+ }
+
+}