All Downloads are FREE. Search and download functionalities are using the official Maven repository.

skinny.assets.CoffeeScriptCompiler.scala Maven / Gradle / Ivy

There is a newer version: 2.3.0-RC1
Show newest version
package skinny.assets

import org.mozilla.javascript._
import java.io.{ ByteArrayInputStream, IOException, InputStreamReader }
import skinny.util.LoanPattern._
import skinny.{ SkinnyEnv, ClassPathResourceLoader }

import scala.sys.process._
import org.slf4j.LoggerFactory
import skinny.exception.AssetsPrecompileFailureException

/**
 * CoffeeScript compiler.
 *
 * @see https://github.com/Filirom1/concoct
 */
case class CoffeeScriptCompiler(bare: Boolean = false) {

  private[this] val log = LoggerFactory.getLogger(classOf[CoffeeScriptCompiler])

  private[this] lazy val globalScope: ScriptableObject = {
    val context = Context.enter
    context.setOptimizationLevel(-1)
    val globalScope = context.initStandardObjects

    ClassPathResourceLoader.getClassPathResource("META-INF/skinny-assets/coffee-script.js").map { coffee =>
      using(coffee.stream) { stream =>
        using(new InputStreamReader(stream)) { input =>
          context.evaluateReader(globalScope, input, "coffeeScript", 0, null)
        }
      }
    }

    globalScope
  }

  private[this] def isWindows = System.getProperty("os.name").toLowerCase().indexOf("win") >= 0

  private[this] def coffeeCommand = if (isWindows) "coffee.bat" else "coffee"

  // TODO Scala 2.11 method lines in trait ProcessBuilder is deprecated: Use lineStream instead.
  // lineStream doesn't exist in Scala 2.10
  private[this] def nativeCompilerDescription = Seq(coffeeCommand, "-v").lines.mkString

  private[this] def nativeCompilerCommand = Seq(coffeeCommand, "--compile", "--stdio") ++ (if (bare) Seq("--bare") else Nil)

  def sourceMapsEnabled: Boolean = SkinnyEnv.isDevelopment() || SkinnyEnv.isTest()

  private[this] def nativeCompilerExists: Boolean = {
    try Seq(coffeeCommand, "-v").lines.size > 0
    catch { case e: IOException => false }
  }

  /**
   * Compiles CoffeeScript source code to JavaScript source code.
   *
   * @param coffeeScriptCode coffee code
   * @return js code
   */
  def compile(fullpath: String, coffeeScriptCode: String): String = {
    if (nativeCompilerExists) {
      // Native compiler (npm install -g coffee-script)
      log.debug(s"Native CoffeeScript compiler is found. (version: ${nativeCompilerDescription})")

      val (out, err) = (new StringBuilder, new StringBuilder)
      val processLogger = ProcessLogger(
        (o: String) => out ++= o ++= "\n",
        (e: String) => err ++= e ++= "\n"
      )
      using(new ByteArrayInputStream(coffeeScriptCode.getBytes)) { stdin =>
        val exitCode = (nativeCompilerCommand #< stdin) ! processLogger
        if (exitCode == 0) {
          // create Source Maps file on the same directory
          if (sourceMapsEnabled) {
            fullpath.split("WEB-INF/assets/coffee/") match {
              case Array(dir, path) =>
                sys.process.Process(Seq(coffeeCommand, "--map", path), new java.io.File(dir + "WEB-INF/assets/coffee")).!
                out.toString + s"\n\n//# sourceMappingURL=${fullpath.split("/").last.replaceFirst(".coffee", ".map")}"
              case _ =>
                out.toString
            }
          } else {
            out.toString
          }
        } else {
          val message = s"Failed to compile coffee script code! (exit code: ${exitCode})\n\n${err.toString}"
          log.error(message)
          throw new AssetsPrecompileFailureException(message)
        }
      }
    } else {
      // Compiler on the Rhino JS engine
      val context = Context.enter
      val compileScope = context.newObject(globalScope)
      compileScope.setParentScope(globalScope)
      compileScope.put("coffeeScriptSource", compileScope, coffeeScriptCode)
      val compilerScript = s"CoffeeScript.compile(coffeeScriptSource, {bare: ${bare}});"
      try {
        context.evaluateString(compileScope, compilerScript, "skinny.assets.CoffeeScriptCompiler", 0, null).toString
      } catch {
        case e: JavaScriptException =>
          val message = s"Failed to compile coffee script code! (${e.getMessage})"
          log.error(message)
          throw new AssetsPrecompileFailureException(message)
      }
    }
  }

}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy