Skip to content

Commit

Permalink
Refactor webpack stats error handling to fail build on error
Browse files Browse the repository at this point in the history
  • Loading branch information
ptrdom committed May 14, 2022
1 parent a52c3be commit 36f94ae
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 69 deletions.
53 changes: 36 additions & 17 deletions sbt-scalajs-bundler/src/main/scala/scalajsbundler/BundlerFile.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -88,14 +85,36 @@ 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 (notExisting, existing) = stats.resolveAllAssets(targetDir)
.foldLeft((List.empty[File], List.empty[File])) {
case ((notExisting, existing), asset) =>
if (asset.exists()) {
(notExisting, existing :+ asset)
} else {
(notExisting :+ asset, existing)
}
}
if (notExisting.nonEmpty) {
throw new RuntimeException(
s"Webpack failed to create application assets:\n" +
s"${notExisting.map(asset => s"${asset.getAbsolutePath}").mkString("\n")}")
} else {
existing
}
}

/**
* Returns an application bundle from a set of cached files
Expand Down
94 changes: 46 additions & 48 deletions sbt-scalajs-bundler/src/main/scala/scalajsbundler/Webpack.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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 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")
}
}
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
}
}
)
}

/**
Expand All @@ -279,11 +273,15 @@ 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)
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"))
)
}

}
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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, _ => ())
}

Expand Down

0 comments on commit 36f94ae

Please sign in to comment.