Emitting JavaScript modules
Basic Module Setup
By default, the -fastopt.js
and -fullopt.js
files produced by Scala.js are top-level scripts, and their @JSExport
ed stuff are sent to the global scope.
With modern JavaScript toolchains, we typically write modules instead, which import and export things from other modules.
You can configure Scala.js to emit a JavaScript module instead of a top-level script.
Two kinds of modules are supported: CommonJS modules (traditional module system of Node.js) and ECMAScript modules. They are enabled with the following sbt settings:
Note: when using ECMAScript modules, the optimizations performed in fullLinkJS
are limited, because the Google Closure Compiler cannot be used with them.
When emitting a module, @JSExportTopLevel
s are really exported from the Scala.js module.
Moreover, you can use top-level @JSImport
to import native JavaScript stuff from other JavaScript module.
For example, consider the following definitions:
Once compiled under ModuleKind.ESModule
, the resulting module would be equivalent to the following JavaScript module:
With ModuleKind.CommonJSModule
, it would instead be equivalent to:
Module Splitting
When emitting modules, the Scala.js linker is able to split its output into multiple JavaScript modules (i.e. files).
There are several reasons to split the JavaScript output into multiple files:
- Share code between different parts of an application (e.g. user/admin interfaces).
- Load parts of a large app progressively
- Create smaller files to minimize changes for incremental downstream tooling.
The Scala.js linker can split a full Scala.js application automatically based on:
- Entry points (top-level exports and module initializers)
- Dynamic import boundaries (calls to
js.dynamicImport
) - The split style (fewest modules, smallest modules, or a combination thereof)
Entry Points
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 (i.e., main methods).
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
:
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
.
Dynamic Imports
Warning: Dynamic imports in Scala.js 1.4.0 are affected by #4386, see the issue for a workaround.
Dynamic imports allow a Scala.js application to be loaded in multiple steps to reduce initial loading time.
To defer loading of a part of your Scala.js application to a later point in time, use js.dynamicImport
:
Example:
The js.dynamicImport
method has the following signature:
Semantically, it will evaluate body
asynchronously and return a Promise of the result.
More importantly, it acts as a border for the Scala.js linker to split out a module that will be dynamically loaded.
The above program would generate
- a public module
main.js
containingonClick
and its direct dependencies - an internal module
MyApp$$anon$1.js
containingHeavyFeature
- an internal module
main-MyApp$$anon$1.js
containing common dependencies ofmain.js
andMyApp$$anon$1.js
.
Internal modules allow the Scala.js linker to split code internally. Unlike public modules, internal modules may not be imported by user code. Doing so is undefined behavior and subject to change at any time.
In the example above, the js.dynamicImport
is replaced by import("./MyApp$$anon$1.js")
, followed by an invocation of the main entry point in MyApp$$anon$1.js
(the body
passed to js.dynamicImport
).
Therefore, when main.js
is loaded, we do not need to load, nor download MyApp$$anon$1.js
.
It will only be loaded the first time onClick
is actually called.
This reduces the initial download time for users.
Dynamic imports and entry points can be arbitrarily combined.
Module Split Styles
So far, we have seen how public modules and dynamic import boundaries can be defined.
Based on these, the Scala.js linker automatically uses the dependency graph of the code to generate appropriate internal modules.
However, there are still choices involved.
They can be configured with the moduleSplitStyle
:
There are currently three module split styles: FewestModules
, SmallestModules
and SmallModulesFor(packages)
.
FewestModules
Create as few modules as possible
- while respecting dynamic import boundaries and
- without including unnecessary code.
This is the default.
In the entry points 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.
The dynamic import example above already assumes this module split style so a module listing is omitted.
SmallestModules
Create modules that are as small as possible. The smallest unit of splitting is a Scala class (see Splitting Granularity below for more).
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 entry points 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.
In the dynamic import example, this would generate:
main.js
: public module, containing the export ofonClick
.- many internal small modules (~150 for this example), approximately one per class.
Generating many small modules can be useful if the output of Scala.js is further processed by downstream JavaScript bundling tools. In incremental builds, they will not need to reprocess the entire Scala.js-generated .js file, but instead only the small modules that have changed.
SmallModulesFor(packages: List[String])
Create modules that are as small as possible for the classes in the specified packages
(and their subpackages).
For all other classes, create as few modules as possible.
This is a combination of the two other split styles.
The typical usage pattern is to list the application’s packages as argument.
This way, often-changing classes receive independent, small modules, while the stable classes coming from libraries are bundled together as much as possible.
For example, if your application code lives in my.app
, you could configure your module split style as:
Splitting Granularity
Scala.js only splits modules along class boundaries. It is important to be aware of this when structuring your application to avoid unnecessary grouping.
For example, the following structure likely leads to poor splitting (if FeatureN
s are not always used together):
For better splitting, group code that belongs to the same feature:
Linker Output
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 for Node.js, for that, see the next section.
In order to make sense of the files in the directory, the linking tasks (fastLinkJS
/fullLinkJS
) return a Report
listing the public modules and their file names.
ES modules and Node.js
Node.js needs explicit signaling that a module is an ECMAScript module (the default is CommonJS).
There are two ways to achieve this:
- Use the file extension
.mjs
. - Configure it in
package.json
.
For details, see the Node.js packages documentation.
To set the extension used by Scala.js to .mjs
use the following setting:
Note for Scala.js 1.2.x and earlier:
OutputPatterns
was introduced in Scala.js 1.3.0. In earlier versions, the following settings were necessary: