I'm trying to defer ScalaJS Linking to runtime, which allows multi-stage compilation to be more flexible and less dependent on sbt.

The setup looks like this:

  1. Instead of using scalajs-sbt plugin, I chose to invoke scalajs-compiler directly as a scala compiler plugin:
        scalaCompilerPlugins("org.scala-js:scalajs-compiler_${vs.scalaV}:${vs.scalaJSV}")

This can successfully generate the "sjsir" files under project output directory, but no further.

  1. Use the solution in this post:

Build / Compile latest SalaJS (1.3+) using gradle on a windows machine?

"Linking scala.js yourself" to invoke the linker on all the compiled sjsir files to produce js files, this is my implementation:

in compile-time & runtime dependencies, add scalajs basics and scalajs-linker:

        bothImpl("org.scala-js:scalajs-library_${vs.scalaBinaryV}:${vs.scalaJSV}")
        bothImpl("org.scala-js:scalajs-linker_${vs.scalaBinaryV}:${vs.scalaJSV}")
        bothImpl("org.scala-js:scalajs-dom_${vs.scalaJSSuffix}:2.1.0")

Write the following code:


import org.scalajs.linker.interface.{Report, StandardConfig}
import org.scalajs.linker.{PathIRContainer, PathOutputDirectory, StandardImpl}
import org.scalajs.logging.{Level, ScalaConsoleLogger}

import java.nio.file.{Path, Paths}
import java.util.Collections
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, ExecutionContext}

object JSLinker {

  implicit def gec = ExecutionContext.global

  def link(classpath: Seq[Path], outputDir: Path): Report = {
    val logger = new ScalaConsoleLogger(Level.Warn)
    val linkerConfig = StandardConfig() // look at the API of this, lots of options.
    val linker = StandardImpl.linker(linkerConfig)

    // Same as scalaJSModuleInitializers in sbt, add if needed.
    val moduleInitializers = Seq()

    val cache = StandardImpl.irFileCache().newCache
    val result = PathIRContainer
      .fromClasspath(classpath)
      .map(_._1)
      .flatMap(cache.cached _)
      .flatMap(linker.link(_, moduleInitializers, PathOutputDirectory(outputDir), logger))

    Await.result(result, Duration.Inf)
  }

  def linkClasses(outputDir: Path = Paths.get("./")): Report = {

    import scala.jdk.CollectionConverters._

    val cl = Thread.currentThread().getContextClassLoader

    val resources = cl.getResources("")

    val rList = Collections.list(resources).asScala.toSeq.map { v =>
      Paths.get(v.toURI)
    }

    link(rList, outputDir)
  }

  lazy val linkOnce = {

    linkClasses()
  }
}

The resources detection was successful, all roots containing sjsir are detected:

rList = {$colon$colon@1629} "::" size = 4
 0 = {UnixPath@1917} "/home/peng/git-scaffold/scaffold-gradle-kts/build/classes/scala/test"
 1 = {UnixPath@1918} "/home/peng/git-scaffold/scaffold-gradle-kts/build/classes/scala/testFixtures"
 2 = {UnixPath@1919} "/home/peng/git-scaffold/scaffold-gradle-kts/build/classes/scala/main"
 3 = {UnixPath@1920} "/home/peng/git-scaffold/scaffold-gradle-kts/build/resources/main"

But linking still fails:


Fatal error: java.lang.Object is missing
  called from core module analyzer


There were linking errors
org.scalajs.linker.interface.LinkingException: There were linking errors
    at org.scalajs.linker.frontend.BaseLinker.reportErrors$1(BaseLinker.scala:91)
    at org.scalajs.linker.frontend.BaseLinker.$anonfun$analyze$5(BaseLinker.scala:100)
    at scala.concurrent.impl.Promise$Transformation.run$$$capture(Promise.scala:467)
    at scala.concurrent.impl.Promise$Transformation.run(Promise.scala)
    at java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1402)
    at java.util.concurrent.ForkJoinTask.doExec$$$capture(ForkJoinTask.java:289)
    at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java)
    at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
    at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
    at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:175)

I wonder what this error message entails. Clearly java.lang.Object is not compiled into sjsir. Does this error message make sense? How do I fix it?

1

There are 1 best solutions below

0
On

Thanks to @sjrd I now have the correct runtime compilation stack. There are 2 problems in my old settings:

  1. It turns out that cl.getResources("") is indeed not able to infer all classpath, so I switch to system property java.class.path, which contains classpaths of all dependencies

  2. moduleInitializers has to be manually set to point to a main method, which will be invoked when the js function is called.

After correcting them, the compilation class becomes:

import org.scalajs.linker.interface.{ModuleInitializer, Report, StandardConfig}
import org.scalajs.linker.{PathIRContainer, PathOutputDirectory, StandardImpl}
import org.scalajs.logging.{Level, ScalaConsoleLogger}

import java.nio.file.{Files, Path, Paths}
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, ExecutionContext, ExecutionContextExecutor}

object JSLinker {

  implicit def gec: ExecutionContextExecutor = ExecutionContext.global

  val logger = new ScalaConsoleLogger(Level.Info) // TODO: cannot be lazy val, why?

  lazy val linkerConf: StandardConfig = {
    StandardConfig()
  } // look at the API of this, lots of options.

  def link(classpath: Seq[Path], outputDir: Path): Report = {
    val linker = StandardImpl.linker(linkerConf)

    // Same as scalaJSModuleInitializers in sbt, add if needed.
    val moduleInitializers = Seq(
      ModuleInitializer.mainMethodWithArgs(SlinkyHelloWorld.getClass.getName.stripSuffix("$"), "main")
    )

    Files.createDirectories(outputDir)

    val cache = StandardImpl.irFileCache().newCache
    val result = PathIRContainer
      .fromClasspath(classpath)
      .map(_._1)
      .flatMap(cache.cached _)
      .flatMap { v =>
        linker.link(v, moduleInitializers, PathOutputDirectory(outputDir), logger)
      }

    Await.result(result, Duration.Inf)
  }

  def linkClasses(outputDir: Path = Paths.get("./ui/build/js")): Report = {

    val rList = getClassPaths

    link(rList, outputDir)
  }

  def getClassPaths: Seq[Path] = {

    val str = System.getProperty("java.class.path")
    val paths = str.split(':').map { v =>
      Paths.get(v)
    }

    paths

  }

  lazy val linkOnce: Report = {

    val report = linkClasses()
    logger.info(
      s"""
         |=== [Linked] ===
         |${report.toString()}
         |""".stripMargin
    )
    report
  }
}

This is all it takes to convert sjsir artefacts to a single main.js file.