Skip to content

Commit e0c0001

Browse files
authored
Merge pull request #531 from gzm0/module-splitting
Document module splitting
2 parents 1807422 + 1613d71 commit e0c0001

File tree

1 file changed

+135
-1
lines changed

1 file changed

+135
-1
lines changed

doc/project/module.md

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
---
22
layout: doc
3-
title: Emitting a JavaScript module
3+
title: Emitting JavaScript modules
44
---
55

6+
## Basic Module Setup
7+
68
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.
79
With modern JavaScript toolchains, we typically write *modules* instead, which import and export things from other modules.
810
You can configure Scala.js to emit a JavaScript module instead of a top-level script.
@@ -76,6 +78,138 @@ class Foobaz {
7678
exports.Babar = Foobaz;
7779
{% endhighlight %}
7880

81+
## Module splitting
82+
83+
When emitting modules, the Scala.js linker is able to split its output into multiple JavaScript modules (i.e. files).
84+
85+
There are several reasons to split the JavaScript output into multiple files:
86+
87+
* Share code between different parts of an application (e.g. user/admin interfaces).
88+
* Create smaller files to minimize changes for incremental downstream tooling.
89+
* Load parts of a large app progressively (not supported yet, see [#4201](https://github.com/scala-js/scala-js/issues/4201)).
90+
91+
The Scala.js linker can split a full Scala.js application automatically based on:
92+
93+
* The entry points (top-level exports and module initializers)
94+
* The split style (fewest modules or smallest modules)
95+
96+
### Entry Points
97+
98+
Scala.js-generated code has two different kinds of entry points:
99+
100+
* [Top-level exports]({{ site.production_url }}/doc/interoperability/export-to-javascript.html): Definitions to be called from external JS code.
101+
* [Module initializers](./building.html): Code that gets executed when a module is imported (i.e., main methods).
102+
103+
The Scala.js linker determines how to group entry points into different (public) modules by using their assigned `moduleID`.
104+
The default `moduleID` is `"main"`.
105+
106+
The `moduleID` of a top-level export can be specified using the [`moduleID` parameter]({{ site.production_url }}/api/scalajs-library/latest/scala/scalajs/js/annotation/JSExportTopLevel.html#%3Cinit%3E(name:String,moduleID:String):scala.scalajs.js.annotation.JSExportTopLevel).
107+
The `moduleID` of a `ModuleInitializer` can be specified by the [`withModuleID` method]({{ site.production_url }}/api/scalajs-linker-interface/latest/org/scalajs/linker/interface/ModuleInitializer.html#withModuleID(moduleID:String):org.scalajs.linker.interface.ModuleInitializer).
108+
109+
**Example**:
110+
111+
Say you have the following `App.scala` and `build.sbt`:
112+
113+
{% highlight scala %}
114+
package my.app
115+
116+
import scala.collection.mutable
117+
import scala.scalajs.js.annotation._
118+
119+
// Separate objects to allow for splitting.
120+
121+
object AppA {
122+
@JSExportTopLevel(name = "start", moduleID = "a")
123+
def a(): Unit = println("hello from a")
124+
}
125+
126+
object AppB {
127+
private val x = mutable.Set.empty[String]
128+
129+
@JSExportTopLevel(name = "start", moduleID = "b")
130+
def b(): Unit = {
131+
println("hello from b")
132+
println(x)
133+
}
134+
135+
def main(): Unit = x.add("something")
136+
}
137+
{% endhighlight %}
138+
139+
{% highlight scala %}
140+
import org.scalajs.linker.interface.ModuleInitializer
141+
142+
scalaJSModuleInitializers in Compile += {
143+
ModuleInitializer.mainMethod("my.app.AppB", "main").withModuleID("b")
144+
}
145+
{% endhighlight %}
146+
147+
This would generate two public modules `a.js` / `b.js`.
148+
`a.js` will export a method named `start` that calls `AppA.a`.
149+
`b.js` will export a method named `start` that calls `AppB.b`.
150+
Further, importing `b.js` will call `AppB.main`.
151+
152+
Note that there is no public module `main.js`, because there is no entry point using the default `moduleID`.
153+
154+
### Module Split Styles
155+
156+
So far, we have seen how public modules can be configured.
157+
Based on the public modules, the Scala.js linker generates internal modules for the shared code between the public modules.
158+
Unlike public modules, internal modules may not be imported by user code.
159+
Doing so is undefined behavior and subject to change at any time.
160+
161+
The linker generates internal modules automatically based on the dependency graph of the code and `moduleSplitStyle`.
162+
You can change it as follows:
163+
164+
{% highlight scala %}
165+
import org.scalajs.linker.interface.ModuleSplitStyle
166+
scalaJSLinkerConfig ~= (_.withModuleSplitStyle(ModuleSplitStyle.SmallestModules))
167+
{% endhighlight %}
168+
169+
There are currently two module split styles: `FewestModules` and `SmallestModules`.
170+
171+
#### `FewestModules`
172+
173+
Create as few modules as possible without including unnecessary code.
174+
This is the default.
175+
176+
In the example above, this would generate:
177+
178+
* `a.js`: public module, containing `AppA` and the export of `start`.
179+
* `b.js`: public module, containing `AppB`, `mutable.Set`, the export of `start` and the call to `AppB.main`
180+
* `a-b.js`: internal module, Scala.js core and the implementation of `println`.
181+
182+
This also works for more than two public modules, creating intermediate shared (internal) modules as necessary.
183+
184+
#### `SmallestModules`
185+
186+
Create modules that are as small as possible.
187+
The smallest unit of splitting is a Scala class.
188+
189+
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.
190+
191+
In the example above, this would generate:
192+
193+
* `a.js`: public module, containing the export of `start`.
194+
* `b.js`: public module, containing the export of `start` and the call to `AppB.main`
195+
* many internal small modules (~50 for this example), approximately one per class.
196+
197+
Generating many small modules can be useful if the output of Scala.js is further processed by downstream JavaScript bundling tools.
198+
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.
199+
200+
### Linker Output
201+
202+
With module splitting, the set of files created by the linker is not known at invocation time.
203+
To support this new requirement, the linker output is configured as follows:
204+
205+
* A directory where all files go: `scalaJSLinkerOutputDirectory`
206+
* Patterns for output file names: `outputPatterns` on `scalaJSLinkerConfig`.
207+
208+
Both of these have reasonable defaults and usually do not need to be changed.
209+
The exception is file extensions for Node.js, for that, see the next section.
210+
211+
In order to make sense of the files in the directory, the linking tasks (`fastLinkJS`/`fullLinkJS`) return a [`Report`]({{ site.production_url }}/api/scalajs-linker-interface/latest/org/scalajs/linker/interface/Report.html) listing the public modules and their file names.
212+
79213
## ES modules and Node.js
80214

81215
Node.js needs explicit signaling that a module is an ECMAScript module (the default is CommonJS).

0 commit comments

Comments
 (0)