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

org.scalajs.linker.backend.closure.ClosureLinkerBackend.scala Maven / Gradle / Ivy

The newest version!
/*
 * Scala.js (https://www.scala-js.org/)
 *
 * Copyright EPFL.
 *
 * Licensed under Apache License 2.0
 * (https://www.apache.org/licenses/LICENSE-2.0).
 *
 * See the NOTICE file distributed with this work for
 * additional information regarding copyright ownership.
 */

package org.scalajs.linker.backend.closure

import scala.concurrent._

import java.io.{ByteArrayOutputStream, Writer}
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import java.util.{Arrays, HashSet}

import com.google.javascript.jscomp.{
  SourceFile => ClosureSource,
  Compiler => ClosureCompiler,
  CompilerOptions => ClosureOptions,
  _
}

import org.scalajs.logging.Logger

import org.scalajs.linker.interface._
import org.scalajs.linker.interface.unstable.OutputPatternsImpl
import org.scalajs.linker.backend._
import org.scalajs.linker.backend.emitter.Emitter
import org.scalajs.linker.backend.javascript.{Trees => js}
import org.scalajs.linker.standard._
import org.scalajs.linker.standard.ModuleSet.ModuleID

/** The Closure backend of the Scala.js linker.
 *
 *  Runs a the Google Closure Compiler in advanced mode on the emitted code.
 *  Use this for production builds.
 */
final class ClosureLinkerBackend(config: LinkerBackendImpl.Config)
    extends LinkerBackendImpl(config) {

  import config.commonConfig.coreSpec._

  require(!esFeatures.allowBigIntsForLongs,
      s"Cannot use features $esFeatures with the Closure Compiler " +
      "because they allow to use BigInts")

  require(moduleKind != ModuleKind.ESModule,
      s"Cannot use module kind $moduleKind with the Closure Compiler")

  require(!targetIsWebAssembly,
      s"A JavaScript backend cannot be used with CoreSpec targeting WebAssembly")

  private[this] val emitter = {
    // Note that we do not transfer `minify` -- Closure will do its own thing anyway
    val emitterConfig = Emitter.Config(config.commonConfig.coreSpec)
      .withJSHeader(config.jsHeader)
      .withOptimizeBracketSelects(false)
      .withTrackAllGlobalRefs(true)
      .withInternalModulePattern(m => OutputPatternsImpl.moduleName(config.outputPatterns, m.id))

    // Do not pre-print trees: We do not want the printed form.
    val prePrinter = Emitter.PrePrinter.Off

    new Emitter(emitterConfig, prePrinter)
  }

  val symbolRequirements: SymbolRequirement = emitter.symbolRequirements

  override def injectedIRFiles: Seq[IRFile] = emitter.injectedIRFiles

  private val languageMode: ClosureOptions.LanguageMode = {
    import ClosureOptions.LanguageMode._

    esFeatures.esVersion match {
      case ESVersion.ES5_1  => ECMASCRIPT5_STRICT
      case ESVersion.ES2015 => ECMASCRIPT_2015
      case ESVersion.ES2016 => ECMASCRIPT_2016
      case ESVersion.ES2017 => ECMASCRIPT_2017
      case ESVersion.ES2018 => ECMASCRIPT_2018
      case ESVersion.ES2019 => ECMASCRIPT_2019
      case ESVersion.ES2020 => ECMASCRIPT_2020
      case ESVersion.ES2021 => ECMASCRIPT_2021

      case _ =>
        throw new AssertionError(s"Unknown ES version ${esFeatures.esVersion}")
    }
  }

  /** Emit the given [[standard.ModuleSet ModuleSet]] to the target output.
   *
   *  @param moduleSet [[standard.ModuleSet ModuleSet]] to emit
   *  @param output Directory to write to
   */
  def emit(moduleSet: ModuleSet, output: OutputDirectory, logger: Logger)(
      implicit ec: ExecutionContext): Future[Report] = {
    verifyModuleSet(moduleSet)

    require(moduleSet.modules.size <= 1,
        "Cannot use multiple modules with the Closure Compiler")

    // Run Emitter even with 0 modules, to keep its internal state consistent.
    val emitterResult = logger.time("Emitter") {
      emitter.emit(moduleSet, logger)
    }

    val gccResult = for {
      sjsModule <- moduleSet.modules.headOption
    } yield {
      val closureChunk = logger.time("Closure: Create trees)") {
        val (trees, _) = emitterResult.body(sjsModule.id)
        buildChunk(trees)
      }

      logger.time("Closure: Compiler pass") {
        val options = closureOptions(sjsModule.id)

        val externs = Arrays.asList(
            ClosureSource.fromCode("ScalaJSExterns.js",
                ClosureLinkerBackend.ScalaJSExterns),
            ClosureSource.fromCode("ScalaJSGlobalRefs.js",
                makeExternsForGlobalRefs(emitterResult.globalRefs)),
            ClosureSource.fromCode("ScalaJSExportExterns.js",
                makeExternsForExports(emitterResult.topLevelVarDecls, sjsModule)))

        compile(externs, closureChunk, options, logger)
      }
    }

    logger.timeFuture("Closure: Write result") {
      writeResult(moduleSet, emitterResult.header, emitterResult.footer, gccResult, output)
    }
  }

  private def buildChunk(topLevelTrees: List[js.Tree]): JSChunk = {
    val root = ClosureAstTransformer.transformScript(topLevelTrees,
        languageMode.toFeatureSet(), config.relativizeSourceMapBase)

    val chunk = new JSChunk("Scala.js")
    chunk.add(new CompilerInput(new SyntheticAst(root)))
    chunk
  }

  private def compile(externs: java.util.List[ClosureSource], chunk: JSChunk,
      options: ClosureOptions, logger: Logger) = {
    val compiler = new ClosureCompiler
    compiler.setErrorManager(new SortingErrorManager(new HashSet(Arrays.asList(
        new LoggerErrorReportGenerator(logger)))))

    val result =
      compiler.compileModules(externs, Arrays.asList(chunk), options)

    if (!result.success) {
      throw new LinkingException(
          "There were errors when applying the Google Closure Compiler")
    }

    (compiler.toSource + "\n", compiler.getSourceMap())
  }

  /** Constructs an externs file listing all the global refs.
   */
  private def makeExternsForGlobalRefs(globalRefs: Set[String]): String =
    globalRefs.map("var " + _ + ";\n").mkString

  /** Constructs an externs file listing all exported properties in a linking
   *  unit.
   *
   *  This is necessary to avoid name clashes with renamed properties (#2491).
   */
  private def makeExternsForExports(topLevelVarDeclarations: List[String],
      sjsModule: ModuleSet.Module): String = {
    import org.scalajs.ir.Trees._
    import org.scalajs.linker.backend.javascript.Trees.Ident.isValidJSIdentifierName

    def exportName(memberDef: MemberDef): Option[String] = memberDef match {
      case JSMethodDef(_, StringLiteral(name), _, _, _)   => Some(name)
      case JSPropertyDef(_, StringLiteral(name), _, _) => Some(name)
      case _                                           => None
    }

    val exportedPropertyNames = for {
      classDef <- sjsModule.classDefs
      member <- classDef.exportedMembers
      name <- exportName(member)
      if isValidJSIdentifierName(name)
    } yield {
      name
    }

    val content = new java.lang.StringBuilder
    for (topLevelVarDecl <- topLevelVarDeclarations)
      content.append(s"var $topLevelVarDecl;\n")
    for (exportedPropertyName <- exportedPropertyNames.distinct)
      content.append(s"Object.prototype.$exportedPropertyName = 0;\n")

    content.toString()
  }

  private def writeResult(moduleSet: ModuleSet, header: String, footer: String,
      gccResult: Option[(String, SourceMap)], output: OutputDirectory)(
      implicit ec: ExecutionContext): Future[Report] = {
    /* `gccResult` is an Option, because we might have no module at all.
     * We call `.get` in the write methods to fail if we get a called anyways.
     */

    val writer = new OutputWriter(output, config, skipContentCheck = false) {
      private def writeCode(writer: Writer): Unit = {
        val code = gccResult.get._1
        writer.write(header)
        writer.write(code)
        writer.write(footer)
      }

      protected def writeModuleWithoutSourceMap(moduleID: ModuleID, force: Boolean): Option[ByteBuffer] = {
        val jsFileWriter = new ByteArrayOutputStream()
        val jsFileStrWriter = new java.io.OutputStreamWriter(jsFileWriter, StandardCharsets.UTF_8)
        writeCode(jsFileStrWriter)
        jsFileStrWriter.flush()
        Some(ByteBuffer.wrap(jsFileWriter.toByteArray()))
      }

      protected def writeModuleWithSourceMap(moduleID: ModuleID, force: Boolean): Option[(ByteBuffer, ByteBuffer)] = {
        val jsFileURI = OutputPatternsImpl.jsFileURI(config.outputPatterns, moduleID.id)
        val sourceMapURI = OutputPatternsImpl.sourceMapURI(config.outputPatterns, moduleID.id)

        val jsFileWriter = new ByteArrayOutputStream()
        val jsFileStrWriter = new java.io.OutputStreamWriter(jsFileWriter, StandardCharsets.UTF_8)
        writeCode(jsFileStrWriter)
        jsFileStrWriter.write("//# sourceMappingURL=" + sourceMapURI + "\n")
        jsFileStrWriter.flush()

        val sourceMapWriter = new ByteArrayOutputStream()
        val sourceMapStrWriter = new java.io.OutputStreamWriter(sourceMapWriter, StandardCharsets.UTF_8)
        val sourceMap = gccResult.get._2
        sourceMap.setWrapperPrefix(header)
        sourceMap.appendTo(sourceMapStrWriter, jsFileURI)
        sourceMapStrWriter.flush()

        Some((ByteBuffer.wrap(jsFileWriter.toByteArray()), ByteBuffer.wrap(sourceMapWriter.toByteArray())))
      }
    }

    writer.write(moduleSet)
  }

  private def closureOptions(moduleID: ModuleID) = {
    val options = new ClosureOptions
    options.setPrettyPrint(config.prettyPrint)
    CompilationLevel.ADVANCED_OPTIMIZATIONS.setOptionsForCompilationLevel(options)

    options.setLanguage(languageMode)
    options.setWarningLevel(DiagnosticGroups.GLOBAL_THIS, CheckLevel.OFF)
    options.setWarningLevel(DiagnosticGroups.DUPLICATE_VARS, CheckLevel.OFF)
    options.setWarningLevel(DiagnosticGroups.CHECK_REGEXP, CheckLevel.OFF)
    options.setWarningLevel(DiagnosticGroups.CHECK_TYPES, CheckLevel.OFF)
    options.setWarningLevel(DiagnosticGroups.CHECK_USELESS_CODE, CheckLevel.OFF)

    if (config.sourceMap) {
      val sourceMapFileName =
        OutputPatternsImpl.sourceMapFile(config.outputPatterns, moduleID.id)

      options.setSourceMapDetailLevel(SourceMap.DetailLevel.ALL)
      options.setSourceMapOutputPath(sourceMapFileName)
    }

    options
  }
}

private object ClosureLinkerBackend {
  /** Minimal set of externs to compile Scala.js-emitted code with Closure.
   *
   *  These must be externs in all cases because they are generated outside of
   *  global ref tracking and CoreJSLib.
   *
   *  * `constructor` is generated for classes
   *  * `toString` is used by [[java.lang.Object#toString]]
   *  * `$classData` needs to be protected from renaming because it must not
   *    be renamed to something short and ubiquitous, otherwise
   *    `$isScalaJSObject` and `$is_` functions cease to function properly.
   *  * `length` is generated by [[ArrayLength org.scalajs.ir.ArrayLength]]
   *  * `call` is generated for super calls
   *  * `apply` is generated when desugaring `...spread` arguments
   *  * `NaN`, `Infinity` and `undefined` need to be in externs for
   *    Closure not to crash in cases where it constant-folds an expression into
   *    one of these (this was confirmed to us as intended by Closure devs).
   */
  private val ScalaJSExterns = """
    var Object;
    Object.prototype.constructor;
    Object.prototype.toString;
    Object.prototype.$classData;
    var Array;
    Array.prototype.length;
    var Function;
    Function.prototype.call;
    Function.prototype.apply;
    var NaN = 0.0/0.0, Infinity = 1.0/0.0, undefined = void 0;
    """
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy