Instead of fastOptJS
/ fullOptJS
, use fastLinkJS
/ fullLinkJS
and do any or all of the following:
import org.scalajs.linker.interface.ModuleSplitStyle
scalaJSLinkerConfig ~= (_.withModuleSplitStyle(ModuleSplitStyle.SmallestModules))
Set the moduleID
for your top level exports and/or module
initializers explicitly. The default moduleID
is "main"
.
@JSExportTopLevel(name = "startAdmin", moduleID = "admin")
def startAdmin(): Unit = ???
import org.scalajs.linker.interface.ModuleInitializer
scalaJSModuleInitializers in Compile += {
ModuleInitializer.mainMethod("my.app.admin.Main", "main")
.withModuleID("admin")
}
Everything with the same moduleID
will go into the same entry point module.
With module splitting, the Scala.js linker splits its output into
multiple JavaScript modules (i.e. files). Both ES6 modules
(ModuleKind.ESModule
) and CommonJS modules
(ModuleKind.CommonJSModule
) are supported.
There are several reasons to split JavaScript output into multiple files:
- Share code between different parts of an application (e.g. user/admin interfaces).
- Create smaller files to minimize changes for incremental downstream tooling.
- Load parts of a large app progressively (not supported yet, see #4201).
The Scala.js linker can split a full Scala.js application automatically based on:
- The entry points (top level exports and module initializers)
- The split style (fewest modules or smallest modules)
Scala.js generated code has two different kinds of entry points:
- Top level exports: Definitions to be called from external JS code.
- Module initializers: Code that gets executed when a module is imported.
The Scala.js linker determines how to group entry points into
different (public) modules by using their assigned moduleID
. The
default moduleID
is "main"
.
The moduleID
of a top level export can be specified using the
moduleID
parameter. The moduleID
of a ModuleInitializer
can be
specified by the withModuleID
method.
Example:
Say you have the following App.scala
and build.sbt
:
package my.app
import scala.collection.mutable
import scala.scalajs.js.annotation._
// Separate objects to allow for splitting.
object AppA {
@JSExportTopLevel(name = "start", moduleID = "a")
def a(): Unit = println("hello from a")
}
object AppB {
private val x = mutable.Set.empty[String]
@JSExportTopLevel(name = "start", moduleID = "b")
def b(): Unit = {
println("hello from b")
println(x)
}
def main(): Unit = x.add("something")
}
import org.scalajs.linker.interface.ModuleInitializer
scalaJSModuleInitializers in Compile += {
ModuleInitializer.mainMethod("my.app.AppB", "main").withModuleID("b")
}
This would generate two public modules a.js
/ b.js
. a.js
will
export a method named start
that calls AppA.a
. b.js
will export a
method named start
that calls AppB.b
. Further, importing b.js
will call AppB.main
.
Note that there is no public module main.js
, because there is no
entry point using the default moduleID
.
So far, we have seen how public modules can be configured. Based on the public modules, the Scala.js linker generates internal modules for the shared code between the public modules. Unlike public modules, internal modules may not be imported by user code. Doing so is undefined behavior and subject to change at any time.
The linker genereates internal modules automatically based on the
dependency graph of the code and moduleSplitStyle
. You can change it
as follows:
import org.scalajs.linker.interface.ModuleSplitStyle
scalaJSLinkerConfig ~= (_.withModuleSplitStyle(ModuleSplitStyle.SmallestModules))
There are currently two module split styles:
Create as few modules as possible without including unnecessary code. This is the default.
In the example above, this would generate:
a.js
: public module, containingAppA
and the export ofstart
.b.js
: public module, containingAppB
,mutable.Set
, the export ofstart
and the call toAppB.main
a-b.js
: internal module, Scala.js core and the implementation ofprintln
.
This also works for more than two public modules, creating intermediate shared (internal) modules as necessary.
Create modules that are as small as possible. The smallest unit of splitting is a Scala class.
Using this mode typically results in an internal module per class with the exception of classes that have circular dependencies: These are put into the same module to avoid a circular module dependency graph.
In the example above, this would generate:
a.js
: public module, containing the export ofstart
.b.js
: public module, containing the export ofstart
and the call toAppB.main
- many internal small modules (~50 for this example), approximately one per class.
With module splitting, the set of files created by the linker is not known at invocation time. To support this new requirement, the linker output is configured as follows:
- A directory where all files go:
scalaJSLinkerOutputDirectory
- Patterns for output file names:
outputPatterns
onscalaJSLinkerConfig
.
Both of these have reasonable defaults and usually do not need to be
changed. The exception is file extensions: If you need to produce
*.mjs
files for Node.js, use:
import org.scalajs.linker.interface.OutputPatterns
scalaJSLinkerConfig ~= (_.withOutputPatterns(OutputPatterns.fromJSFile("%s.mjs")))
In order to make sense of the files in the directory, linking returns
a Report
listing the public modules and their file names.
Since the fastOptJS
/ fullOptJS
keys/tasks assume that linking will
produce a single file, we had to introduce two new keys/tasks for
linking: fastLinkJS
/ fullLinkJS
. These tasks return the Report
instead of an individual File
.
In order to ensure backwards compatibility, the fastOptJS
/
fullOptJS
tasks now invoke fastLinkJS
/ fullLinkJS
respectively
and copy the produced files to their target location. However, this
only works if the linker produced a single public module. So with
actual module splitting, fastOptJS
/ fullOptJS
will fail.
The run
and test
tasks now depend on fastLinkJS
/ fullLinkJS
(depending on the scalaJSStage
) and load the public module with
moduleID="main"
(they fail if no such module exists). This does
not change their behavior for existing builds but allows running and
testing with module splitting enabled.