summaryrefslogtreecommitdiff
path: root/examples/scala-js/tools/jvm/src/main/scala/scala/scalajs/tools/optimizer/ScalaJSClosureOptimizer.scala
blob: 146b2b8ae7d3a0283ce9419885bcdf5dd2b524d4 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
/*                     __                                               *\
**     ________ ___   / /  ___      __ ____  Scala.js tools             **
**    / __/ __// _ | / /  / _ | __ / // __/  (c) 2013-2014, LAMP/EPFL   **
**  __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \    http://scala-js.org/       **
** /____/\___/_/ |_/____/_/ | |__/ /____/                               **
**                          |/____/                                     **
\*                                                                      */


package scala.scalajs.tools.optimizer

import scala.scalajs.tools.sem.Semantics
import scala.scalajs.tools.classpath._
import scala.scalajs.tools.logging._
import scala.scalajs.tools.io._
import scala.scalajs.tools.corelib.CoreJSLibs

import com.google.javascript.jscomp.{
  SourceFile => ClosureSource,
  Compiler => ClosureCompiler,
  CompilerOptions => ClosureOptions,
  _
}
import scala.collection.JavaConverters._
import scala.collection.immutable.{Seq, Traversable}

import java.net.URI

/** Scala.js Closure optimizer: does advanced optimizations with Closure. */
class ScalaJSClosureOptimizer(semantics: Semantics) {
  import ScalaJSClosureOptimizer._

  private def toClosureSource(file: VirtualJSFile) =
    ClosureSource.fromReader(file.toURI.toString(), file.reader)

  private def toClosureInput(file: VirtualJSFile) =
    new CompilerInput(toClosureSource(file))

  /** Fully optimizes an [[IRClasspath]] by asking the ScalaJSOptimizer to
   *  emit a closure AST and then compiling this AST directly
   */
  def optimizeCP(optimizer: ScalaJSOptimizer,
      inputs: Inputs[ScalaJSOptimizer.Inputs[IRClasspath]],
      outCfg: OutputConfig, logger: Logger): LinkedClasspath = {

    val cp = inputs.input.input

    CacheUtils.cached(cp.version, outCfg.output, outCfg.cache) {
      logger.info(s"Full Optimizing ${outCfg.output.path}")

      val irFastOptInput =
        inputs.input.copy(input = inputs.input.input.scalaJSIR)
      val irFullOptInput = inputs.copy(input = irFastOptInput)

      optimizeIR(optimizer, irFullOptInput, outCfg, logger)
    }

    new LinkedClasspath(cp.jsLibs, outCfg.output, cp.requiresDOM, cp.version)
  }

  def optimizeIR(optimizer: ScalaJSOptimizer,
      inputs: Inputs[ScalaJSOptimizer.Inputs[Traversable[VirtualScalaJSIRFile]]],
      outCfg: OutputConfig, logger: Logger): Unit = {

    // Build Closure IR via ScalaJSOptimizer
    val builder = new ClosureAstBuilder(outCfg.relativizeSourceMapBase)

    optimizer.optimizeIR(inputs.input, outCfg, builder, logger)

    // Build a Closure JSModule which includes the core libs
    val module = new JSModule("Scala.js")

    for (lib <- CoreJSLibs.libs(semantics))
      module.add(toClosureInput(lib))

    val ast = builder.closureAST
    module.add(new CompilerInput(ast, ast.getInputId(), false))

    for (export <- inputs.additionalExports)
      module.add(toClosureInput(export))

    // Compile the module
    val closureExterns =
      (ScalaJSExternsFile +: inputs.additionalExterns).map(toClosureSource)

    val options = closureOptions(outCfg)
    val compiler = closureCompiler(logger)

    val result = GenIncOptimizer.logTime(logger, "Closure Compiler pass") {
      compiler.compileModules(
          closureExterns.asJava, List(module).asJava, options)
    }

    GenIncOptimizer.logTime(logger, "Write Closure result") {
      writeResult(result, compiler, outCfg.output)
    }
  }

  private def writeResult(result: Result, compiler: ClosureCompiler,
      output: WritableVirtualJSFile): Unit = {

    val outputContent = if (result.errors.nonEmpty) ""
    else "(function(){'use strict';" + compiler.toSource + "}).call(this);\n"

    val sourceMap = Option(compiler.getSourceMap())

    // Write optimized code
    val w = output.contentWriter
    try {
      w.write(outputContent)
      if (sourceMap.isDefined)
        w.write("//# sourceMappingURL=" + output.name + ".map\n")
    } finally w.close()

    // Write source map (if available)
    sourceMap.foreach { sm =>
      val w = output.sourceMapWriter
      try sm.appendTo(w, output.name)
      finally w.close()
    }
  }

  private def closureOptions(optConfig: OptimizerConfig,
      noSourceMap: Boolean = false) = {

    val options = new ClosureOptions
    options.prettyPrint = optConfig.prettyPrint
    CompilationLevel.ADVANCED_OPTIMIZATIONS.setOptionsForCompilationLevel(options)
    options.setLanguageIn(ClosureOptions.LanguageMode.ECMASCRIPT5)
    options.setCheckGlobalThisLevel(CheckLevel.OFF)

    if (!noSourceMap && optConfig.wantSourceMap) {
      options.setSourceMapOutputPath(optConfig.output.name + ".map")
      options.setSourceMapDetailLevel(SourceMap.DetailLevel.ALL)
    }

    options
  }

  private def closureCompiler(logger: Logger) = {
    val compiler = new ClosureCompiler
    compiler.setErrorManager(new LoggerErrorManager(logger))
    compiler
  }
}

object ScalaJSClosureOptimizer {
  /** Inputs of the Scala.js Closure optimizer. */
  final case class Inputs[T](
      /** Input to optimize (classpath or file-list) */
      input: T,
      /** Additional externs files to be given to Closure. */
      additionalExterns: Seq[VirtualJSFile] = Nil,
      /** Additional exports to be given to Closure.
       *  These files are just appended to the classpath, given to Closure,
       *  but not used in the Scala.js optimizer pass when direct optimizing
       */
      additionalExports: Seq[VirtualJSFile] = Nil
  )

  /** Configuration the closure part of the optimizer needs.
   *  See [[OutputConfig]] for a description of the fields.
   */
  trait OptimizerConfig {
    val output: WritableVirtualJSFile
    val cache: Option[WritableVirtualTextFile]
    val wantSourceMap: Boolean
    val prettyPrint: Boolean
    val relativizeSourceMapBase: Option[URI]
  }

  /** Configuration for the output of the Scala.js Closure optimizer */
  final case class OutputConfig(
      /** Writer for the output */
      output: WritableVirtualJSFile,
      /** Cache file */
      cache: Option[WritableVirtualTextFile] = None,
      /** If true, performs expensive checks of the IR for the used parts. */
      checkIR: Boolean = false,
      /** If true, the optimizer removes trees that have not been used in the
       *  last run from the cache. Otherwise, all trees that has been used once,
       *  are kept in memory. */
      unCache: Boolean = true,
      /** If true, no optimizations are performed */
      disableOptimizer: Boolean = false,
      /** If true, nothing is performed incrementally */
      batchMode: Boolean = false,
      /** Ask to produce source map for the output */
      wantSourceMap: Boolean = false,
      /** Pretty-print the output. */
      prettyPrint: Boolean = false,
      /** Base path to relativize paths in the source map */
      relativizeSourceMapBase: Option[URI] = None
  ) extends OptimizerConfig with ScalaJSOptimizer.OptimizerConfig

  /** Minimal set of externs to compile Scala.js-emitted code with Closure. */
  val ScalaJSExterns = """
    /** @constructor */
    function Object() {}
    Object.protoype.toString = function() {};
    /** @constructor */
    function Array() {}
    Array.prototype.length = 0;
    /** @constructor */
    function Function() {}
    Function.prototype.constructor = function() {};
    Function.prototype.call = function() {};
    Function.prototype.apply = function() {};
    var global = {};
    var __ScalaJSEnv = {};
    """

  val ScalaJSExternsFile = new MemVirtualJSFile("ScalaJSExterns.js").
    withContent(ScalaJSExterns)

}