summaryrefslogtreecommitdiff
path: root/examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep
diff options
context:
space:
mode:
Diffstat (limited to 'examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep')
-rw-r--r--examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/Exceptions.scala59
-rw-r--r--examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/FlatJSDependency.scala17
-rw-r--r--examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/JSDependency.scala66
-rw-r--r--examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/JSDependencyManifest.scala130
-rw-r--r--examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/Origin.scala28
-rw-r--r--examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/ResolutionInfo.scala21
6 files changed, 321 insertions, 0 deletions
diff --git a/examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/Exceptions.scala b/examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/Exceptions.scala
new file mode 100644
index 0000000..dd7f635
--- /dev/null
+++ b/examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/Exceptions.scala
@@ -0,0 +1,59 @@
+package scala.scalajs.tools.jsdep
+
+abstract class DependencyException(msg: String) extends Exception(msg)
+
+class MissingDependencyException(
+ val originatingLib: FlatJSDependency,
+ val missingLib: String
+) extends DependencyException(
+ s"The JS dependency ${originatingLib.resourceName} declared " +
+ s"from ${originatingLib.origin} has an unmet transitive " +
+ s"dependency $missingLib")
+
+class CyclicDependencyException(
+ val participants: List[ResolutionInfo]
+) extends DependencyException(
+ CyclicDependencyException.mkMsg(participants))
+
+object CyclicDependencyException {
+ private def mkMsg(parts: List[ResolutionInfo]) = {
+ val lookup = parts.map(p => (p.resourceName, p)).toMap
+
+ val msg = new StringBuilder()
+ msg.append("There is a loop in the following JS dependencies:\n")
+
+ def str(info: ResolutionInfo) =
+ s"${info.resourceName} from: ${info.origins.mkString(", ")}"
+
+ for (dep <- parts) {
+ msg.append(s" ${str(dep)} which depends on\n")
+ for (name <- dep.dependencies) {
+ val rdep = lookup(name)
+ msg.append(s" - ${str(rdep)}\n")
+ }
+ }
+
+ msg.toString()
+ }
+}
+
+class ConflictingNameException(
+ val participants: List[FlatJSDependency]
+) extends DependencyException(
+ ConflictingNameException.mkMsg(participants))
+
+object ConflictingNameException {
+ private def mkMsg(parts: List[FlatJSDependency]) = {
+ val resName = parts.head.resourceName
+
+ val msg = new StringBuilder()
+ msg.append(s"Name conflicts in:\n")
+
+ for (p <- parts) {
+ msg.append(p)
+ msg.append('\n')
+ }
+
+ sys.error(msg.toString())
+ }
+}
diff --git a/examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/FlatJSDependency.scala b/examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/FlatJSDependency.scala
new file mode 100644
index 0000000..0c55e88
--- /dev/null
+++ b/examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/FlatJSDependency.scala
@@ -0,0 +1,17 @@
+package scala.scalajs.tools.jsdep
+
+import scala.scalajs.ir.Trees.isValidIdentifier
+
+/** The same as a [[JSDependency]] but containing the origin from the containing
+ * JSDependencyManifest. This class is used for filtering of dependencies.
+ */
+final class FlatJSDependency(
+ val origin: Origin,
+ val resourceName: String,
+ val dependencies: List[String] = Nil,
+ val commonJSName: Option[String] = None) {
+
+ require(commonJSName.forall(isValidIdentifier),
+ "commonJSName must be a valid JavaScript identifier")
+
+}
diff --git a/examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/JSDependency.scala b/examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/JSDependency.scala
new file mode 100644
index 0000000..2e6f8d1
--- /dev/null
+++ b/examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/JSDependency.scala
@@ -0,0 +1,66 @@
+package scala.scalajs.tools.jsdep
+
+import scala.scalajs.tools.json._
+
+import scala.scalajs.ir.Trees.isValidIdentifier
+
+/** Expresses a dependency on a raw JS library and the JS libraries this library
+ * itself depends on.
+ *
+ * Both the [[resourceName]] and each element of [[dependencies]] is the
+ * unqualified filename of the library (e.g. "jquery.js").
+ *
+ * @param resourceName Filename of the JavaScript file to include
+ * (e.g. "jquery.js")
+ * @param dependencies Filenames of JavaScript files that must be included
+ * before this JavaScript file.
+ * @param commonJSName A JavaScript variable name this dependency should be
+ * required in a commonJS environment (n.b. Node.js). Should only be set if
+ * the JavaScript library will register its exports.
+ */
+final class JSDependency(
+ val resourceName: String,
+ val dependencies: List[String] = Nil,
+ val commonJSName: Option[String] = None) {
+
+ require(commonJSName.forall(isValidIdentifier),
+ "commonJSName must be a valid JavaScript identifier")
+
+ def dependsOn(names: String*): JSDependency =
+ copy(dependencies = dependencies ++ names)
+ def commonJSName(name: String): JSDependency =
+ copy(commonJSName = Some(name))
+ def withOrigin(origin: Origin): FlatJSDependency =
+ new FlatJSDependency(origin, resourceName, dependencies, commonJSName)
+
+ private def copy(
+ resourceName: String = this.resourceName,
+ dependencies: List[String] = this.dependencies,
+ commonJSName: Option[String] = this.commonJSName) = {
+ new JSDependency(resourceName, dependencies, commonJSName)
+ }
+}
+
+object JSDependency {
+
+ implicit object JSDepJSONSerializer extends JSONSerializer[JSDependency] {
+ def serialize(x: JSDependency): JSON = {
+ new JSONObjBuilder()
+ .fld("resourceName", x.resourceName)
+ .opt("dependencies",
+ if (x.dependencies.nonEmpty) Some(x.dependencies) else None)
+ .opt("commonJSName", x.commonJSName)
+ .toJSON
+ }
+ }
+
+ implicit object JSDepJSONDeserializer extends JSONDeserializer[JSDependency] {
+ def deserialize(x: JSON): JSDependency = {
+ val obj = new JSONObjExtractor(x)
+ new JSDependency(
+ obj.fld[String] ("resourceName"),
+ obj.opt[List[String]]("dependencies").getOrElse(Nil),
+ obj.opt[String] ("commonJSName"))
+ }
+ }
+}
diff --git a/examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/JSDependencyManifest.scala b/examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/JSDependencyManifest.scala
new file mode 100644
index 0000000..24491b4
--- /dev/null
+++ b/examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/JSDependencyManifest.scala
@@ -0,0 +1,130 @@
+package scala.scalajs.tools.jsdep
+
+import scala.scalajs.tools.json._
+import scala.scalajs.tools.io._
+
+import scala.collection.immutable.{Seq, Traversable}
+
+import java.io.{Reader, Writer}
+
+/** The information written to a "JS_DEPENDENCIES" manifest file. */
+final class JSDependencyManifest(
+ val origin: Origin,
+ val libDeps: List[JSDependency],
+ val requiresDOM: Boolean,
+ val compliantSemantics: List[String]) {
+ def flatten: List[FlatJSDependency] = libDeps.map(_.withOrigin(origin))
+}
+
+object JSDependencyManifest {
+
+ final val ManifestFileName = "JS_DEPENDENCIES"
+
+ def createIncludeList(
+ flatDeps: Traversable[FlatJSDependency]): List[ResolutionInfo] = {
+ val jsDeps = mergeManifests(flatDeps)
+
+ // Verify all dependencies are met
+ for {
+ lib <- flatDeps
+ dep <- lib.dependencies
+ if !jsDeps.contains(dep)
+ } throw new MissingDependencyException(lib, dep)
+
+ // Sort according to dependencies and return
+
+ // Very simple O(n²) topological sort for elements assumed to be distinct
+ // Copied :( from GenJSExports (but different exception)
+ @scala.annotation.tailrec
+ def loop(coll: List[ResolutionInfo],
+ acc: List[ResolutionInfo]): List[ResolutionInfo] = {
+
+ if (coll.isEmpty) acc
+ else if (coll.tail.isEmpty) coll.head :: acc
+ else {
+ val (selected, pending) = coll.partition { x =>
+ coll forall { y => (x eq y) || !y.dependencies.contains(x.resourceName) }
+ }
+
+ if (selected.nonEmpty)
+ loop(pending, selected ::: acc)
+ else
+ throw new CyclicDependencyException(pending)
+ }
+ }
+
+ loop(jsDeps.values.toList, Nil)
+ }
+
+ /** Merges multiple JSDependencyManifests into a map of map:
+ * resourceName -> ResolutionInfo
+ */
+ private def mergeManifests(flatDeps: Traversable[FlatJSDependency]) = {
+ @inline
+ def hasConflict(x: FlatJSDependency, y: FlatJSDependency) = (
+ x.commonJSName.isDefined &&
+ y.commonJSName.isDefined &&
+ (x.resourceName == y.resourceName ^
+ x.commonJSName == y.commonJSName)
+ )
+
+ val conflicts = flatDeps.filter(x =>
+ flatDeps.exists(y => hasConflict(x,y)))
+
+ if (conflicts.nonEmpty)
+ throw new ConflictingNameException(conflicts.toList)
+
+ flatDeps.groupBy(_.resourceName).mapValues { sameName =>
+ new ResolutionInfo(
+ resourceName = sameName.head.resourceName,
+ dependencies = sameName.flatMap(_.dependencies).toSet,
+ origins = sameName.map(_.origin).toList,
+ commonJSName = sameName.flatMap(_.commonJSName).headOption
+ )
+ }
+ }
+
+ implicit object JSDepManJSONSerializer extends JSONSerializer[JSDependencyManifest] {
+ @inline def optList[T](x: List[T]): Option[List[T]] =
+ if (x.nonEmpty) Some(x) else None
+
+ def serialize(x: JSDependencyManifest): JSON = {
+ new JSONObjBuilder()
+ .fld("origin", x.origin)
+ .opt("libDeps", optList(x.libDeps))
+ .opt("requiresDOM", if (x.requiresDOM) Some(true) else None)
+ .opt("compliantSemantics", optList(x.compliantSemantics))
+ .toJSON
+ }
+ }
+
+ implicit object JSDepManJSONDeserializer extends JSONDeserializer[JSDependencyManifest] {
+ def deserialize(x: JSON): JSDependencyManifest = {
+ val obj = new JSONObjExtractor(x)
+ new JSDependencyManifest(
+ obj.fld[Origin] ("origin"),
+ obj.opt[List[JSDependency]]("libDeps").getOrElse(Nil),
+ obj.opt[Boolean] ("requiresDOM").getOrElse(false),
+ obj.opt[List[String]] ("compliantSemantics").getOrElse(Nil))
+ }
+ }
+
+ def write(dep: JSDependencyManifest, output: WritableVirtualTextFile): Unit = {
+ val writer = output.contentWriter
+ try write(dep, writer)
+ finally writer.close()
+ }
+
+ def write(dep: JSDependencyManifest, writer: Writer): Unit =
+ writeJSON(dep.toJSON, writer)
+
+ def read(file: VirtualTextFile): JSDependencyManifest = {
+ val reader = file.reader
+ try read(reader)
+ finally reader.close()
+ }
+
+ def read(reader: Reader): JSDependencyManifest =
+ fromJSON[JSDependencyManifest](readJSON(reader))
+
+}
diff --git a/examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/Origin.scala b/examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/Origin.scala
new file mode 100644
index 0000000..a2c6b2d
--- /dev/null
+++ b/examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/Origin.scala
@@ -0,0 +1,28 @@
+package scala.scalajs.tools.jsdep
+
+import scala.scalajs.tools.json._
+
+/** The place a JSDependency originated from */
+final class Origin(val moduleName: String, val configuration: String) {
+ override def toString(): String = s"$moduleName:$configuration"
+}
+
+object Origin {
+ implicit object OriginJSONSerializer extends JSONSerializer[Origin] {
+ def serialize(x: Origin): JSON = {
+ new JSONObjBuilder()
+ .fld("moduleName", x.moduleName)
+ .fld("configuration", x.configuration)
+ .toJSON
+ }
+ }
+
+ implicit object OriginDeserializer extends JSONDeserializer[Origin] {
+ def deserialize(x: JSON): Origin = {
+ val obj = new JSONObjExtractor(x)
+ new Origin(
+ obj.fld[String]("moduleName"),
+ obj.fld[String]("configuration"))
+ }
+ }
+}
diff --git a/examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/ResolutionInfo.scala b/examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/ResolutionInfo.scala
new file mode 100644
index 0000000..2aa177e
--- /dev/null
+++ b/examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep/ResolutionInfo.scala
@@ -0,0 +1,21 @@
+package scala.scalajs.tools.jsdep
+
+import scala.scalajs.ir.Trees.isValidIdentifier
+
+/** Information about a resolved JSDependency
+ *
+ * @param resourceName Filename of the JavaScript file
+ * @param dependencies Filenames this dependency depends on
+ * @param origins Who declared this dependency
+ * @param commonJSName Variable name in commonJS environments
+ */
+final class ResolutionInfo(
+ val resourceName: String,
+ val dependencies: Set[String],
+ val origins: List[Origin],
+ val commonJSName: Option[String]) {
+
+ require(commonJSName.forall(isValidIdentifier),
+ "commonJSName must be a valid JavaScript identifier")
+
+}