diff options
Diffstat (limited to 'examples/scala-js/tools/shared/src/main/scala/scala/scalajs/tools/jsdep')
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") + +} |