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

mill.scalajslib.worker.ScalaJSWorkerImpl.scala Maven / Gradle / Ivy

The newest version!
package mill.scalajslib.worker

import scala.concurrent.Await
import scala.concurrent.duration.Duration
import java.io.File
import java.nio.file.Path
import mill.scalajslib.worker.api._
import mill.scalajslib.worker.jsenv._
import org.scalajs.ir.ScalaJSVersions
import org.scalajs.linker.{PathIRContainer, PathOutputDirectory, PathOutputFile, StandardImpl}
import org.scalajs.linker.interface.{
  ESFeatures => ScalaJSESFeatures,
  ESVersion => ScalaJSESVersion,
  ModuleKind => ScalaJSModuleKind,
  OutputPatterns => ScalaJSOutputPatterns,
  Report => _,
  ModuleSplitStyle => _,
  _
}
import org.scalajs.logging.{Level, Logger}
import org.scalajs.jsenv.{Input, JSEnv, RunConfig}
import org.scalajs.testing.adapter.TestAdapter
import org.scalajs.testing.adapter.{TestAdapterInitializer => TAI}

import scala.collection.mutable
import scala.ref.SoftReference

import com.armanbilge.sjsimportmap.ImportMappedIRFile

class ScalaJSWorkerImpl extends ScalaJSWorkerApi {
  private case class LinkerInput(
      isFullLinkJS: Boolean,
      optimizer: Boolean,
      sourceMap: Boolean,
      moduleKind: ModuleKind,
      esFeatures: ESFeatures,
      moduleSplitStyle: ModuleSplitStyle,
      outputPatterns: OutputPatterns,
      minify: Boolean,
      dest: File,
      experimentalUseWebAssembly: Boolean
  )
  private def minorIsGreaterThanOrEqual(number: Int) = ScalaJSVersions.current match {
    case s"1.$n.$_" if n.toIntOption.exists(_ < number) => false
    case _ => true
  }
  private object ScalaJSLinker {
    private val irFileCache = StandardImpl.irFileCache()
    private val cache = mutable.Map.empty[LinkerInput, SoftReference[(Linker, IRFileCache.Cache)]]
    def reuseOrCreate(input: LinkerInput): (Linker, IRFileCache.Cache) = cache.get(input) match {
      case Some(SoftReference((linker, irFileCacheCache))) => (linker, irFileCacheCache)
      case _ =>
        val newResult = createLinker(input)
        cache.update(input, SoftReference(newResult))
        newResult
    }
    private def createLinker(input: LinkerInput): (Linker, IRFileCache.Cache) = {
      val semantics = input.isFullLinkJS match {
        case true => Semantics.Defaults.optimized
        case false => Semantics.Defaults
      }
      val scalaJSModuleKind = input.moduleKind match {
        case ModuleKind.NoModule => ScalaJSModuleKind.NoModule
        case ModuleKind.CommonJSModule => ScalaJSModuleKind.CommonJSModule
        case ModuleKind.ESModule => ScalaJSModuleKind.ESModule
      }
      @scala.annotation.nowarn("cat=deprecation")
      def withESVersion_1_5_minus(esFeatures: ScalaJSESFeatures): ScalaJSESFeatures = {
        val useECMAScript2015: Boolean = input.esFeatures.esVersion match {
          case ESVersion.ES5_1 => false
          case ESVersion.ES2015 => true
          case v => throw new Exception(
              s"ESVersion $v is not supported with Scala.js < 1.6. Either update Scala.js or use one of ESVersion.ES5_1 or ESVersion.ES2015"
            )
        }
        esFeatures.withUseECMAScript2015(useECMAScript2015)
      }
      def withESVersion_1_6_plus(esFeatures: ScalaJSESFeatures): ScalaJSESFeatures = {
        val scalaJSESVersion: ScalaJSESVersion = input.esFeatures.esVersion match {
          case ESVersion.ES5_1 => ScalaJSESVersion.ES5_1
          case ESVersion.ES2015 => ScalaJSESVersion.ES2015
          case ESVersion.ES2016 => ScalaJSESVersion.ES2016
          case ESVersion.ES2017 => ScalaJSESVersion.ES2017
          case ESVersion.ES2018 => ScalaJSESVersion.ES2018
          case ESVersion.ES2019 => ScalaJSESVersion.ES2019
          case ESVersion.ES2020 => ScalaJSESVersion.ES2020
          case ESVersion.ES2021 => ScalaJSESVersion.ES2021
        }
        esFeatures.withESVersion(scalaJSESVersion)
      }
      var scalaJSESFeatures: ScalaJSESFeatures = ScalaJSESFeatures.Defaults
        .withAllowBigIntsForLongs(input.esFeatures.allowBigIntsForLongs)

      if (minorIsGreaterThanOrEqual(4)) {
        scalaJSESFeatures = scalaJSESFeatures
          .withAvoidClasses(input.esFeatures.avoidClasses)
          .withAvoidLetsAndConsts(input.esFeatures.avoidLetsAndConsts)
      }
      scalaJSESFeatures =
        if (minorIsGreaterThanOrEqual(6)) withESVersion_1_6_plus(scalaJSESFeatures)
        else withESVersion_1_5_minus(scalaJSESFeatures)

      val useClosure = input.isFullLinkJS && input.moduleKind != ModuleKind.ESModule
      val partialConfig = StandardConfig()
        .withOptimizer(input.optimizer)
        .withClosureCompilerIfAvailable(useClosure)
        .withSemantics(semantics)
        .withModuleKind(scalaJSModuleKind)
        .withESFeatures(scalaJSESFeatures)
        .withSourceMap(input.sourceMap)

      def withModuleSplitStyle_1_3_plus(config: StandardConfig): StandardConfig = {
        config.withModuleSplitStyle(
          input.moduleSplitStyle match {
            case ModuleSplitStyle.FewestModules => ScalaJSModuleSplitStyle.FewestModules()
            case ModuleSplitStyle.SmallestModules => ScalaJSModuleSplitStyle.SmallestModules()
            case v @ ModuleSplitStyle.SmallModulesFor(packages) =>
              if (minorIsGreaterThanOrEqual(10)) ScalaJSModuleSplitStyle.SmallModulesFor(packages)
              else throw new Exception(
                s"ModuleSplitStyle $v is not supported with Scala.js < 1.10. Either update Scala.js or use one of ModuleSplitStyle.SmallestModules or ModuleSplitStyle.FewestModules"
              )
          }
        )
      }

      def withModuleSplitStyle_1_2_minus(config: StandardConfig): StandardConfig = {
        input.moduleSplitStyle match {
          case ModuleSplitStyle.FewestModules =>
          case v => throw new Exception(
              s"ModuleSplitStyle $v is not supported with Scala.js < 1.2. Either update Scala.js or use ModuleSplitStyle.FewestModules"
            )
        }
        config
      }

      val withModuleSplitStyle =
        if (minorIsGreaterThanOrEqual(3)) withModuleSplitStyle_1_3_plus(partialConfig)
        else withModuleSplitStyle_1_2_minus(partialConfig)

      val withOutputPatterns =
        if (minorIsGreaterThanOrEqual(3))
          withModuleSplitStyle
            .withOutputPatterns(
              ScalaJSOutputPatterns.Defaults
                .withJSFile(input.outputPatterns.jsFile)
                .withJSFileURI(input.outputPatterns.jsFileURI)
                .withModuleName(input.outputPatterns.moduleName)
                .withSourceMapFile(input.outputPatterns.sourceMapFile)
                .withSourceMapURI(input.outputPatterns.sourceMapURI)
            )
        else withModuleSplitStyle

      val withMinify =
        if (minorIsGreaterThanOrEqual(16)) withOutputPatterns.withMinify(input.minify)
        else withOutputPatterns

      val withWasm =
        (minorIsGreaterThanOrEqual(17), input.experimentalUseWebAssembly) match {
          case (_, false) => withMinify
          case (true, true) => withMinify.withExperimentalUseWebAssembly(true)
          case (false, true) =>
            throw new Exception("Emitting wasm is not supported with Scala.js < 1.17")
        }

      val linker = StandardImpl.clearableLinker(withWasm)
      val irFileCacheCache = irFileCache.newCache
      (linker, irFileCacheCache)
    }
  }
  private val logger = new Logger {
    def log(level: Level, message: => String): Unit = {
      System.err.println(message)
    }
    def trace(t: => Throwable): Unit = {
      t.printStackTrace()
    }
  }
  def link(
      runClasspath: Seq[Path],
      dest: File,
      main: Either[String, String],
      forceOutJs: Boolean,
      testBridgeInit: Boolean,
      isFullLinkJS: Boolean,
      optimizer: Boolean,
      sourceMap: Boolean,
      moduleKind: ModuleKind,
      esFeatures: ESFeatures,
      moduleSplitStyle: ModuleSplitStyle,
      outputPatterns: OutputPatterns,
      minify: Boolean,
      importMap: Seq[ESModuleImportMapping],
      experimentalUseWebAssembly: Boolean
  ): Either[String, Report] = {
    // On Scala.js 1.2- we want to use the legacy mode either way since
    // the new mode is not supported and in tests we always use legacy = false
    val useLegacy = forceOutJs || !minorIsGreaterThanOrEqual(3)
    import scala.concurrent.ExecutionContext.Implicits.global
    val (linker, irFileCacheCache) = ScalaJSLinker.reuseOrCreate(LinkerInput(
      isFullLinkJS = isFullLinkJS,
      optimizer = optimizer,
      sourceMap = sourceMap,
      moduleKind = moduleKind,
      esFeatures = esFeatures,
      moduleSplitStyle = moduleSplitStyle,
      outputPatterns = outputPatterns,
      minify = minify,
      dest = dest,
      experimentalUseWebAssembly = experimentalUseWebAssembly
    ))
    val irContainersAndPathsFuture = PathIRContainer.fromClasspath(runClasspath)
    val testInitializer =
      if (testBridgeInit)
        ModuleInitializer.mainMethod(TAI.ModuleClassName, TAI.MainMethodName) :: Nil
      else Nil
    val moduleInitializers = main match {
      case Right(main) =>
        ModuleInitializer.mainMethodWithArgs(main, "main") ::
          testInitializer
      case _ =>
        testInitializer
    }

    val resultFuture = (for {
      (irContainers, _) <- irContainersAndPathsFuture
      irFiles0 <- irFileCacheCache.cached(irContainers)
      irFiles = if (importMap.isEmpty) {
        irFiles0
      } else {
        if (!minorIsGreaterThanOrEqual(16)) {
          throw new Exception("scalaJSImportMap is not supported with Scala.js < 1.16.")
        }
        val remapFunction = (rawImport: String) => {
          importMap
            .collectFirst {
              case ESModuleImportMapping.Prefix(prefix, replacement)
                  if rawImport.startsWith(prefix) =>
                s"$replacement${rawImport.stripPrefix(prefix)}"
            }
            .getOrElse(rawImport)
        }
        irFiles0.map { ImportMappedIRFile.fromIRFile(_)(remapFunction) }
      }
      report <-
        if (useLegacy) {
          val jsFileName = "out.js"
          val jsFile = new File(dest, jsFileName).toPath()
          var linkerOutput = LinkerOutput(PathOutputFile(jsFile))
            .withJSFileURI(java.net.URI.create(jsFile.getFileName.toString))
          val sourceMapNameOpt = Option.when(sourceMap)(s"${jsFile.getFileName}.map")
          sourceMapNameOpt.foreach { sourceMapName =>
            val sourceMapFile = jsFile.resolveSibling(sourceMapName)
            linkerOutput = linkerOutput
              .withSourceMap(PathOutputFile(sourceMapFile))
              .withSourceMapURI(java.net.URI.create(sourceMapFile.getFileName.toString))
          }
          linker.link(irFiles, moduleInitializers, linkerOutput, logger).map {
            file =>
              Report(
                publicModules = Seq(Report.Module(
                  moduleID = "main",
                  jsFileName = jsFileName,
                  sourceMapName = sourceMapNameOpt,
                  moduleKind = moduleKind
                )),
                dest = dest
              )
          }
        } else {
          val linkerOutput = PathOutputDirectory(dest.toPath())
          linker.link(
            irFiles,
            moduleInitializers,
            linkerOutput,
            logger
          ).map { report =>
            Report(
              publicModules =
                report.publicModules.map(module =>
                  Report.Module(
                    moduleID = module.moduleID,
                    jsFileName = module.jsFileName,
                    sourceMapName = module.sourceMapName,
                    // currently moduleKind is always the input moduleKind
                    moduleKind = moduleKind
                  )
                ),
              dest = dest
            )
          }
        }
    } yield {
      Right(report)
    }).recover {
      case e: org.scalajs.linker.interface.LinkingException =>
        Left(e.getMessage)
    }

    Await.result(resultFuture, Duration.Inf)
  }

  def run(config: JsEnvConfig, report: Report): Unit = {
    val env = jsEnv(config)
    val input = jsEnvInput(report)
    val runConfig0 = RunConfig().withLogger(logger)
    val runConfig =
      if (mill.api.SystemStreams.isOriginal()) runConfig0
      else runConfig0
        .withInheritErr(false)
        .withInheritOut(false)
        .withOnOutputStream { case (Some(processOut), Some(processErr)) =>
          val sources = Seq(
            (processOut, System.out, "spawnSubprocess.stdout", false, () => true),
            (processErr, System.err, "spawnSubprocess.stderr", false, () => true)
          )

          for ((std, dest, name, checkAvailable, runningCheck) <- sources) {
            val t = new Thread(
              new mill.main.client.InputPumper(
                () => std,
                () => dest,
                checkAvailable,
                () => runningCheck()
              ),
              name
            )
            t.setDaemon(true)
            t.start()
          }
        }
    Run.runInterruptible(env, input, runConfig)
  }

  def getFramework(
      config: JsEnvConfig,
      frameworkName: String,
      report: Report
  ): (() => Unit, sbt.testing.Framework) = {
    val env = jsEnv(config)
    val input = jsEnvInput(report)
    val tconfig = TestAdapter.Config().withLogger(logger)

    val adapter = new TestAdapter(env, input, tconfig)

    (
      () => adapter.close(),
      adapter
        .loadFrameworks(List(List(frameworkName)))
        .flatten
        .headOption
        .getOrElse(throw new RuntimeException(
          """|Test framework class was not found. Please check that:
             |- the correct Scala.js dependency of the framework is used (like ivy"group::artifact::version", instead of ivy"group::artifact:version" for JVM Scala. Note the extra : before the version.)
             |- there are no typos in the framework class name.
             |- the framework library is on the classpath
             |""".stripMargin
        ))
    )
  }

  def jsEnv(config: JsEnvConfig): JSEnv = config match {
    case config: JsEnvConfig.NodeJs =>
      NodeJs(config)
    case config: JsEnvConfig.JsDom =>
      JsDom(config)
    case config: JsEnvConfig.ExoegoJsDomNodeJs =>
      ExoegoJsDomNodeJs(config)
    case config: JsEnvConfig.Phantom =>
      Phantom(config)
    case config: JsEnvConfig.Selenium =>
      Selenium(config)
  }

  def jsEnvInput(report: Report): Seq[Input] = {
    val mainModule = report.publicModules.find(_.moduleID == "main").getOrElse(throw new Exception(
      "Cannot determine `jsEnvInput`: Linking result does not have a " +
        "module named `main`.\n" +
        s"Full report:\n$report"
    ))
    val path = new File(report.dest, mainModule.jsFileName).toPath
    val input = mainModule.moduleKind match {
      case ModuleKind.NoModule => Input.Script(path)
      case ModuleKind.ESModule => Input.ESModule(path)
      case ModuleKind.CommonJSModule => Input.CommonJSModule(path)
    }
    Seq(input)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy