1

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?

3
  • 1
    The error means that scalajs-library is missing from the classpath that you give to the linker. In addition to your own directories with sjsir files, you need to provide the jars of all your transitive dependencies. Commented Mar 11, 2022 at 2:42
  • I see, sorry I actually thought that it is a JVM bytecode jar. My bad, will update my classpath discovery and try again Commented Mar 11, 2022 at 3:26
  • OK I posted my answer @sjrd does it make sense to package the posted code as a runtime linking API? Commented Mar 13, 2022 at 9:11

1 Answer 1

0

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.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.