diff --git a/sbt-scalajs-bundler/src/main/scala/scalajsbundler/BundlerFile.scala b/sbt-scalajs-bundler/src/main/scala/scalajsbundler/BundlerFile.scala index a2d96a94..ed0b292e 100644 --- a/sbt-scalajs-bundler/src/main/scala/scalajsbundler/BundlerFile.scala +++ b/sbt-scalajs-bundler/src/main/scala/scalajsbundler/BundlerFile.scala @@ -65,15 +65,12 @@ object BundlerFile { /** * Returns the Library identifying the asset produced by scala.js through webpack stats */ - def asLibrary(stats: Option[WebpackStats]): Library = - Library(project, - stats.flatMap { s => - s.resolveAsset(targetDir, project) - }.getOrElse(targetDir.resolve(Library.fileName(project)).toFile), - stats.map { s => - s.resolveAllAssets(targetDir) - }.getOrElse(Nil) - ) + def asLibrary(stats: WebpackStats): Library = + Library( + project, + resolveApplicationBundleFile(stats), + resolveApplicationAssets(stats) + ) /** * Returns library from a set of cached files @@ -88,14 +85,28 @@ object BundlerFile { /** * Returns the Application for this configuration identifying the asset produced by scala.js through webpack stats */ - def asApplicationBundle(stats: Option[WebpackStats]): ApplicationBundle = - ApplicationBundle(project, - stats.flatMap { s => - s.resolveAsset(targetDir, project) - }.getOrElse(targetDir.resolve(ApplicationBundle.fileName(project)).toFile), - stats.map { s => - s.resolveAllAssets(targetDir) - }.getOrElse(Nil)) + def asApplicationBundle(stats: WebpackStats): ApplicationBundle = + ApplicationBundle( + project, + resolveApplicationBundleFile(stats), + resolveApplicationAssets(stats) + ) + + private def resolveApplicationBundleFile(stats: WebpackStats) = { + stats.resolveAsset(targetDir, project) + .getOrElse(throw new RuntimeException("Webpack failed to create application bundle")) + } + + private def resolveApplicationAssets(stats: WebpackStats) = { + val assets = stats.resolveAllAssets(targetDir) + val nonExisting = assets.filter(!_.exists()) + if (nonExisting.nonEmpty) { + throw new RuntimeException( + s"Webpack failed to create application assets:\n" + + s"${nonExisting.map(asset => s"${asset.getAbsolutePath}").mkString("\n")}") + } + assets + } /** * Returns an application bundle from a set of cached files diff --git a/sbt-scalajs-bundler/src/main/scala/scalajsbundler/Stats.scala b/sbt-scalajs-bundler/src/main/scala/scalajsbundler/Stats.scala index 9f0ab4e1..fc805d0c 100644 --- a/sbt-scalajs-bundler/src/main/scala/scalajsbundler/Stats.scala +++ b/sbt-scalajs-bundler/src/main/scala/scalajsbundler/Stats.scala @@ -2,7 +2,6 @@ package scalajsbundler import play.api.libs.json._ -import play.api.libs.functional.syntax._ import sbt.Logger import scala.math.max import java.io.File @@ -50,18 +49,48 @@ object Stats { } - final case class WebpackError(moduleName: String, message: String, loc: String) + sealed trait Issue { + val message: String + val moduleName: Option[String] + val loc: Option[String] + val stack: Option[String] + val details: Option[String] + + def format(): String = { + List( + moduleName.map("in "+_), + Some("Message: "+message), + loc.map("Loc: "+_), + stack.map("Stack: "+_), + details.map("Details: "+_) + ).flatten.mkString("\n") + } + } - final case class WebpackWarning(moduleName: String, message: String) + final case class WebpackError( + message: String, + moduleName: Option[String], + loc: Option[String], + stack: Option[String], + details: Option[String] + ) extends Issue + + final case class WebpackWarning( + message: String, + moduleName: Option[String], + loc: Option[String], + stack: Option[String], + details: Option[String] + ) extends Issue final case class WebpackStats( version: String, hash: String, - time: Long, + time: Option[Long], outputPath: Option[Path], - errors: List[WebpackError], - warnings: List[WebpackWarning], - assets: List[Asset] + errors: List[WebpackError] = Nil, + warnings: List[WebpackWarning] = Nil, + assets: List[Asset] = Nil ) { /** @@ -70,7 +99,12 @@ object Stats { def print(log: Logger): Unit = { import formatting._ // Print base info - List(s"Version: $version", s"Hash: $hash", s"Time: ${time}ms", s"Path: ${outputPath.getOrElse("")}").foreach(x => log.info(x)) + List( + Some(s"Version: $version"), + Some(s"Hash: $hash"), + time.map(time => s"Time: ${time}ms"), + outputPath.map(s"Path: "+_) + ).flatten.foreach(x => log.info(x)) log.info("") // Print the assets assets.map { a => @@ -107,32 +141,10 @@ object Stats { assets.map(a => outputPath.getOrElse(altDir).resolve(a.name).toFile) } - implicit val assetsReads: Reads[Asset] = ( - (JsPath \ "name").read[String] and - (JsPath \ "size").read[Long] and - (JsPath \ "emitted").read[Boolean] and - (JsPath \ "chunkNames").read[List[String]] - )(Asset.apply _) - - implicit val errorReads: Reads[WebpackError] = ( - (JsPath \ "moduleName").read[String] and - (JsPath \ "message").read[String] and - (JsPath \ "loc").read[String] - )(WebpackError.apply _) - - implicit val warningReads: Reads[WebpackWarning] = ( - (JsPath \ "moduleName").read[String] and - (JsPath \ "message").read[String] - )(WebpackWarning.apply _) - - implicit val statsReads: Reads[WebpackStats] = ( - (JsPath \ "version").read[String] and - (JsPath \ "hash").read[String] and - (JsPath \ "time").read[Long] and - (JsPath \ "outputPath").readNullable[String].map(x => x.map(new File(_).toPath)) and // It seems webpack 2 doesn't produce outputPath - (JsPath \ "errors").read[List[WebpackError]] and - (JsPath \ "warnings").read[List[WebpackWarning]] and - (JsPath \ "assets").read[List[Asset]] - )(WebpackStats.apply _) + implicit val assetsReads: Reads[Asset] = Json.reads[Asset] + implicit val errorReads: Reads[WebpackError] = Json.reads[WebpackError] + implicit val warningReads: Reads[WebpackWarning] = Json.reads[WebpackWarning] + implicit val pathReads: Reads[Path] = Reads.StringReads.map(new File(_).toPath) + implicit val statsReads: Reads[WebpackStats] = Json.using[Json.WithDefaultValues].reads[WebpackStats] } diff --git a/sbt-scalajs-bundler/src/main/scala/scalajsbundler/Webpack.scala b/sbt-scalajs-bundler/src/main/scala/scalajsbundler/Webpack.scala index da50694f..3ff82fdb 100644 --- a/sbt-scalajs-bundler/src/main/scala/scalajsbundler/Webpack.scala +++ b/sbt-scalajs-bundler/src/main/scala/scalajsbundler/Webpack.scala @@ -168,12 +168,10 @@ object Webpack { log.info("Bundling the application with its NPM dependencies") val args = extraArgs ++: Seq("--config", configFile.absolutePath) val stats = Webpack.run(nodeArgs: _*)(args: _*)(targetDir, log) - stats.foreach(_.print(log)) + stats.print(log) // Attempt to discover the actual name produced by webpack indexing by chunk name and discarding maps val bundle = generatedWebpackConfigFile.asApplicationBundle(stats) - assert(bundle.file.exists(), "Webpack failed to create application bundle") - assert(bundle.assets.forall(_.exists()), "Webpack failed to create application assets") bundle } @@ -220,55 +218,51 @@ object Webpack { val args = extraArgs ++: Seq("--config", configFile.absolutePath) val stats = Webpack.run(nodeArgs: _*)(args: _*)(generatedWebpackConfigFile.targetDir.toFile, log) - stats.foreach(_.print(log)) + stats.print(log) val library = generatedWebpackConfigFile.asLibrary(stats) - assert(library.file.exists, "Webpack failed to create library file") - assert(library.assets.forall(_.exists), "Webpack failed to create library assets") library } - private def jsonOutput(cmd: Seq[String], logger: Logger)(in: InputStream): Option[WebpackStats] = { - Try { - val parsed = Json.parse(in) - parsed.validate[WebpackStats] match { - case JsError(e) => - logger.error("Error parsing webpack stats output") - // In case of error print the result and return None. it will be ignored upstream - e.foreach { - case (p, v) => logger.error(s"$p: ${v.mkString(",")}") - } - None - case JsSuccess(p, _) => - if (p.warnings.nonEmpty || p.errors.nonEmpty) { - logger.info("") - // Filtering is a workaround for #111 - p.warnings.filterNot(_.message.contains("https://raw.githubusercontent.com")).foreach { warning => - logger.warn(s"WARNING in ${warning.moduleName}") - logger.warn(warning.message) - logger.warn("\n") - } - p.errors.foreach { error => - logger.error(s"ERROR in ${error.moduleName} ${error.loc}") - logger.error(error.message) - logger.error("\n") - } + private def jsonOutput(cmd: Seq[String], logger: Logger)(in: InputStream): Try[WebpackStats] = { + Try(Json.parse(in)) + .fold( + e => Failure( + new RuntimeException( + "Failure on parsing the output of webpack\n" + + "You can try to manually execute the command\n" + + s"${cmd.mkString(" ")}", + e + ) + ), + parsed => { + parsed.validate[WebpackStats] match { + case JsError(e) => + Failure( + new RuntimeException( + "Error parsing webpack stats output\n" + + s"${e.map { case (p, v) => s"$p: ${v.mkString(",")}"}.mkString("\n")}" + ) + ) + case JsSuccess(p, _) => + if (p.warnings.nonEmpty || p.errors.nonEmpty) { + logger.info("") + // Filtering is a workaround for #111 + p.warnings.filterNot(_.message.contains("https://raw.githubusercontent.com")).foreach { warning => + logger.warn(s"WARNING") + logger.warn(warning.format()) + logger.warn("\n") + } + p.errors.foreach { error => + logger.error(s"ERROR") + logger.error(error.format()) + logger.error("\n") + } + } + Success(p) } - Some(p) - } - } match { - case Success(x) => - x - case Failure(e) => - // In same cases errors are not reported on the json output but comes on stdout - // where they cannot be parsed as json. The best we can do here is to suggest - // running the command manually - logger.error(s"Failure on parsing the output of webpack: ${e.getMessage}") - logger.error(s"You can try to manually execute the command") - logger.error(cmd.mkString(" ")) - logger.error("\n") - None - } + } + ) } /** @@ -279,11 +273,16 @@ object Webpack { * @param workingDir Working directory in which the Nodejs will be run (where there is the `node_modules` subdirectory) * @param log Logger */ - def run(nodeArgs: String*)(args: String*)(workingDir: File, log: Logger): Option[WebpackStats] = { + def run(nodeArgs: String*)(args: String*)(workingDir: File, log: Logger): WebpackStats = { val webpackBin = workingDir / "node_modules" / "webpack" / "bin" / "webpack" val params = nodeArgs ++ Seq(webpackBin.absolutePath, "--profile", "--json") ++ args val cmd = "node" +: params - Commands.run(cmd, workingDir, log, jsonOutput(cmd, log)).fold(sys.error, _.flatten) + log.debug(s"Running command [${cmd.mkString(" ")}]") + Commands.run(cmd, workingDir, log, jsonOutput(cmd, log)) + .fold( + sys.error, + _.map(_.get).getOrElse(throw new RuntimeException("Failure on returning webpack stats from command output")) + ) } } diff --git a/sbt-scalajs-bundler/src/main/scala/scalajsbundler/util/Commands.scala b/sbt-scalajs-bundler/src/main/scala/scalajsbundler/util/Commands.scala index 81edd57b..6a1082c9 100644 --- a/sbt-scalajs-bundler/src/main/scala/scalajsbundler/util/Commands.scala +++ b/sbt-scalajs-bundler/src/main/scala/scalajsbundler/util/Commands.scala @@ -1,21 +1,23 @@ package scalajsbundler.util import sbt.Logger -import java.io.{InputStream, File} +import java.io.{File, InputStream} + import scala.sys.process.Process import scala.sys.process.BasicIO import scala.sys.process.ProcessLogger +import scala.util.Try object Commands { - def run[A](cmd: Seq[String], cwd: File, logger: Logger, outputProcess: InputStream => A): Either[String, Option[A]] = { + def run[A](cmd: Seq[String], cwd: File, logger: Logger, outputProcess: InputStream => Try[A]): Either[String, Option[Try[A]]] = { val toErrorLog = (is: InputStream) => { scala.io.Source.fromInputStream(is).getLines.foreach(msg => logger.error(msg)) is.close() } // Unfortunately a var is the only way to capture the result - var result: Option[A] = None + var result: Option[Try[A]] = None def outputCapture(o: InputStream): Unit = { result = Some(outputProcess(o)) o.close() @@ -34,7 +36,7 @@ object Commands { } def run(cmd: Seq[String], cwd: File, logger: Logger): Unit = { - val toInfoLog = (is: InputStream) => scala.io.Source.fromInputStream(is).getLines.foreach(msg => logger.info(msg)) + val toInfoLog = (is: InputStream) => Try(scala.io.Source.fromInputStream(is).getLines.foreach(msg => logger.info(msg))) run(cmd, cwd, logger, toInfoLog).fold(sys.error, _ => ()) } diff --git a/sbt-scalajs-bundler/src/sbt-test/sbt-scalajs-bundler/webpack-stats/build.sbt b/sbt-scalajs-bundler/src/sbt-test/sbt-scalajs-bundler/webpack-stats/build.sbt new file mode 100644 index 00000000..f92a4839 --- /dev/null +++ b/sbt-scalajs-bundler/src/sbt-test/sbt-scalajs-bundler/webpack-stats/build.sbt @@ -0,0 +1,13 @@ +scalaVersion := "2.12.8" + +useYarn := true + +yarnExtraArgs in Compile := Seq("--silent") + +scalaJSUseMainModuleInitializer := true + +webpackConfigFile := Some(baseDirectory.value / "webpack.config.js") + +enablePlugins(ScalaJSBundlerPlugin) + +ivyLoggingLevel in ThisBuild := UpdateLogging.Quiet diff --git a/sbt-scalajs-bundler/src/sbt-test/sbt-scalajs-bundler/webpack-stats/project/plugins.sbt b/sbt-scalajs-bundler/src/sbt-test/sbt-scalajs-bundler/webpack-stats/project/plugins.sbt new file mode 100644 index 00000000..77ac06f1 --- /dev/null +++ b/sbt-scalajs-bundler/src/sbt-test/sbt-scalajs-bundler/webpack-stats/project/plugins.sbt @@ -0,0 +1,8 @@ +val scalaJSVersion = sys.props.getOrElse("scalajs.version", sys.error("'scalajs.version' environment variable is not defined")) +val scalaJSBundlerVersion = sys.props.getOrElse("plugin.version", sys.error("'plugin.version' environment variable is not set")) + +addSbtPlugin("org.scala-js" % "sbt-scalajs" % scalaJSVersion) + +addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % scalaJSBundlerVersion) + +ivyLoggingLevel in ThisBuild := UpdateLogging.Quiet diff --git a/sbt-scalajs-bundler/src/sbt-test/sbt-scalajs-bundler/webpack-stats/src/main/scala/example/Main.scala b/sbt-scalajs-bundler/src/sbt-test/sbt-scalajs-bundler/webpack-stats/src/main/scala/example/Main.scala new file mode 100644 index 00000000..96e71fb6 --- /dev/null +++ b/sbt-scalajs-bundler/src/sbt-test/sbt-scalajs-bundler/webpack-stats/src/main/scala/example/Main.scala @@ -0,0 +1,7 @@ +package example + +object Main { + def main(args: Array[String]): Unit = { + println("webpack-stats main") + } +} diff --git a/sbt-scalajs-bundler/src/sbt-test/sbt-scalajs-bundler/webpack-stats/test b/sbt-scalajs-bundler/src/sbt-test/sbt-scalajs-bundler/webpack-stats/test new file mode 100644 index 00000000..b3fa55b2 --- /dev/null +++ b/sbt-scalajs-bundler/src/sbt-test/sbt-scalajs-bundler/webpack-stats/test @@ -0,0 +1 @@ +> fullOptJS::webpack diff --git a/sbt-scalajs-bundler/src/sbt-test/sbt-scalajs-bundler/webpack-stats/webpack.config.js b/sbt-scalajs-bundler/src/sbt-test/sbt-scalajs-bundler/webpack-stats/webpack.config.js new file mode 100644 index 00000000..6b4b1b8a --- /dev/null +++ b/sbt-scalajs-bundler/src/sbt-test/sbt-scalajs-bundler/webpack-stats/webpack.config.js @@ -0,0 +1,10 @@ +// Load the config generated by scalajs-bundler +var config = require('./scalajs.webpack.config'); + +module.exports = config; + +module.exports.performance = { + maxEntrypointSize: 1 // force warning +}; + +module.exports.output.filename = '[name].[contenthash].bundle.js'