Skip to content

Commit 399886c

Browse files
committed
Add scalajs-vite-mill.md
- content is copied from scalajs-vite.md to help in diff
1 parent d9d183d commit 399886c

File tree

1 file changed

+359
-0
lines changed

1 file changed

+359
-0
lines changed

doc/tutorial/scalajs-vite-mill.md

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
---
2+
layout: doc
3+
title: Getting Started with Scala.js and Vite
4+
---
5+
6+
In this first tutorial, we learn how to get started with Scala.js and [Vite](https://vitejs.dev/).
7+
We use Vite to provide live-reloading of the Scala.js application in the browser for development.
8+
We also configure it to build a minimal bundle for production.
9+
10+
Going through this tutorial will make sure you understand the basic building blocks.
11+
If you prefer to skip this step and directly write Scala.js code, you may jump to [Getting Started with Scala.js and Laminar](./laminar.html).
12+
13+
If you prefer to look at the end result for this tutorial directly, checkout [the scalajs-vite-end-state branch](https://github.com/sjrd/scalajs-sbt-vite-laminar-chartjs-example/tree/scalajs-vite-end-state) instead of creating everything from scratch.
14+
15+
## Prerequisites
16+
17+
Make sure to install [the prerequisites](./index.html#prerequisites) before continuing further.
18+
19+
## Vite template
20+
21+
We bootstrap our setup using the vanilla Vite template.
22+
Navigate to a directory where you store projects, and run the command
23+
24+
{% highlight shell %}
25+
$ npm create [email protected]
26+
{% endhighlight %}
27+
28+
Choose a project name (we choose `livechart`).
29+
Select the "Vanilla" framework and the "JavaScript" variant.
30+
Our output gives:
31+
32+
{% highlight shell %}
33+
$ npm create [email protected]
34+
Need to install the following packages:
35+
36+
Ok to proceed? (y)
37+
✔ Project name: … livechart
38+
✔ Select a framework: › Vanilla
39+
✔ Select a variant: › JavaScript
40+
41+
Scaffolding project in .../livechart...
42+
43+
Done. Now run:
44+
45+
cd livechart
46+
npm install
47+
npm run dev
48+
{% endhighlight %}
49+
50+
As instructed, we follow up with
51+
52+
{% highlight shell %}
53+
$ cd livechart
54+
$ npm install
55+
[...]
56+
$ npm run dev
57+
58+
VITE v4.1.4 ready in 156 ms
59+
60+
➜ Local: http://localhost:5173/
61+
➜ Network: use --host to expose
62+
➜ press h to show help
63+
{% endhighlight %}
64+
65+
Open the provided URL to see the running JavaScript-based hello world.
66+
67+
### Exploring the template
68+
69+
In the generated folder, we find the following relevant files:
70+
71+
* `index.html`: the main web page; it contains a `<script type=module src="/main.js">` referencing the main JavaScript entry point.
72+
* `main.js`: the main JavaScript entry point; it sets up some DOM elements, and sets up a counter for a button.
73+
* `counter.js`: it implements a counter functionality for a button.
74+
* `package.json`: the config file for `npm`, the JavaScript package manager and build orchestrator.
75+
76+
Remarkably, there is no `vite.config.js` file, which would be the configuration for Vite itself.
77+
Vite gives a decent experience out of the box, without any configuration.
78+
79+
### Live changes
80+
81+
One of the main selling points of Vite is its ability to automatically refresh the browser upon file changes.
82+
Open the file `main.js` and change the content of the `<h1>` element:
83+
84+
{% highlight diff %}
85+
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript" target="_blank">
86+
<img src="${javascriptLogo}" class="logo vanilla" alt="JavaScript logo" />
87+
</a>
88+
- <h1>Hello Vite!</h1>
89+
+ <h1>Hello Scala.js!</h1>
90+
<div class="card">
91+
<button id="counter" type="button"></button>
92+
</div>
93+
{% endhighlight %}
94+
95+
Observe that the page automatically and instantaneously refreshes to show the changes.
96+
97+
## Introducing Scala.js
98+
99+
We use [sbt](https://www.scala-sbt.org/) as a build tool for Scala and Scala.js.
100+
We set it up as follows.
101+
102+
In the subdirectory `livechart/project/`, we add two files: `build.properties` and `plugins.sbt`.
103+
104+
* `project/build.properties`: set the version of sbt
105+
106+
{% highlight plaintext %}
107+
sbt.version=1.10.0
108+
{% endhighlight %}
109+
110+
* `project/plugins.sbt`: declare sbt plugins; in this case, only sbt-scalajs
111+
112+
{% highlight scala %}
113+
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "{{ site.versions.scalaJS }}")
114+
{% endhighlight %}
115+
116+
At the root of our `livechart/` project, we add one file: `build.sbt`.
117+
118+
* `build.sbt`: the main sbt build
119+
120+
{% highlight scala %}
121+
import org.scalajs.linker.interface.ModuleSplitStyle
122+
123+
lazy val livechart = project.in(file("."))
124+
.enablePlugins(ScalaJSPlugin) // Enable the Scala.js plugin in this project
125+
.settings(
126+
scalaVersion := "3.3.3",
127+
128+
// Tell Scala.js that this is an application with a main method
129+
scalaJSUseMainModuleInitializer := true,
130+
131+
/* Configure Scala.js to emit modules in the optimal way to
132+
* connect to Vite's incremental reload.
133+
* - emit ECMAScript modules
134+
* - emit as many small modules as possible for classes in the "livechart" package
135+
* - emit as few (large) modules as possible for all other classes
136+
* (in particular, for the standard library)
137+
*/
138+
scalaJSLinkerConfig ~= {
139+
_.withModuleKind(ModuleKind.ESModule)
140+
.withModuleSplitStyle(
141+
ModuleSplitStyle.SmallModulesFor(List("livechart")))
142+
},
143+
144+
/* Depend on the scalajs-dom library.
145+
* It provides static types for the browser DOM APIs.
146+
*/
147+
libraryDependencies += "org.scala-js" %%% "scalajs-dom" % "{{ site.versions.scalaJSDOM }}",
148+
)
149+
{% endhighlight %}
150+
151+
Finally, we write the following content in the file `src/main/scala/livechart/LiveChart.scala`:
152+
153+
{% highlight scala %}
154+
package livechart
155+
156+
import scala.scalajs.js
157+
import scala.scalajs.js.annotation.*
158+
159+
import org.scalajs.dom
160+
161+
// import javascriptLogo from "/javascript.svg"
162+
@js.native @JSImport("/javascript.svg", JSImport.Default)
163+
val javascriptLogo: String = js.native
164+
165+
@main
166+
def LiveChart(): Unit =
167+
dom.document.querySelector("#app").innerHTML = s"""
168+
<div>
169+
<a href="https://vitejs.dev" target="_blank">
170+
<img src="/vite.svg" class="logo" alt="Vite logo" />
171+
</a>
172+
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript" target="_blank">
173+
<img src="$javascriptLogo" class="logo vanilla" alt="JavaScript logo" />
174+
</a>
175+
<h1>Hello Scala.js!</h1>
176+
<div class="card">
177+
<button id="counter" type="button"></button>
178+
</div>
179+
<p class="read-the-docs">
180+
Click on the Vite logo to learn more
181+
</p>
182+
</div>
183+
"""
184+
185+
setupCounter(dom.document.getElementById("counter"))
186+
end LiveChart
187+
188+
def setupCounter(element: dom.Element): Unit =
189+
var counter = 0
190+
191+
def setCounter(count: Int): Unit =
192+
counter = count
193+
element.innerHTML = s"count is $counter"
194+
195+
element.addEventListener("click", e => setCounter(counter + 1))
196+
setCounter(0)
197+
end setupCounter
198+
{% endhighlight %}
199+
200+
Note that the above is not idiomatic Scala, but rather a direct translation of the Vite template code into Scala.js.
201+
We will see in the next tutorial how to use Laminar to write it more idiomatically.
202+
203+
For the most part, the Scala.js version uses straightforward Scala syntax corresponding to the original JavaScript code.
204+
The definition of `javascriptLogo` deserves some explanation.
205+
206+
We translated it from the JavaScript import
207+
208+
{% highlight javascript %}
209+
import javascriptLogo from "/javascript.svg"
210+
{% endhighlight %}
211+
212+
which is actually a shorthand for
213+
214+
{% highlight javascript %}
215+
import { default as javascriptLogo } from "/javascript.svg"
216+
{% endhighlight %}
217+
218+
Many bundlers, Vite included, treat `import`s with asset files such as `.svg` as pseudo-modules whose `default` import is the *file path* to the corresponding asset in the processed bundle.
219+
Further down, we use it as the value for the `src` attribute an `<img>` tag.
220+
Read more about this mechanism [in the Vite documentation on static asset handling](https://vitejs.dev/guide/assets.html).
221+
222+
The translation in Scala.js reads as
223+
224+
{% highlight scala %}
225+
@js.native @JSImport("/javascript.svg", JSImport.Default)
226+
val javascriptLogo: String = js.native
227+
{% endhighlight %}
228+
229+
The `@js.native` annotation tells Scala.js that `javascriptLogo` is provided externally by JavaScript.
230+
The `@JSImport("/javascript.svg", JSImport.Default)` is the translation of the `default` import from the `/javascript.svg` pseudo-module.
231+
Since it represents a file path, we declare `javascriptLogo` as a `String`.
232+
233+
The `= js.native` is a Scala.js idiosyncrasy: we need a concrete value to satisfy the Scala typechecker.
234+
In an ideal world, it would not be required.
235+
236+
We can now build the Scala.js project by opening a new console, and entering sbt:
237+
238+
{% highlight shell %}
239+
$ sbt
240+
[...]
241+
sbt:livechart> ~fastLinkJS
242+
{% endhighlight %}
243+
244+
The `fastLinkJS` task produces the `.js` outputs from the Scala.js command.
245+
The `~` prefix instructs sbt to re-run that task every time a source file changes.
246+
247+
There is one thing left to change: replace the hand-written JavaScript code with our Scala.js application.
248+
We use the `@scala-js/vite-plugin-scalajs` plugin to link Vite and Scala.js with minimal configuration.
249+
We install it in the dev-dependencies with:
250+
251+
{% highlight shell %}
252+
$ npm install -D @scala-js/vite-plugin-scalajs@1.0.0
253+
{% endhighlight %}
254+
255+
and instruct Vite to use it with the following configuration in a new file `vite.config.js`:
256+
257+
{% highlight javascript %}
258+
import { defineConfig } from "vite";
259+
import scalaJSPlugin from "@scala-js/vite-plugin-scalajs";
260+
261+
export default defineConfig({
262+
plugins: [scalaJSPlugin()],
263+
});
264+
{% endhighlight %}
265+
266+
Finally, open the file `main.js`, remove almost everything to leave only the following two lines:
267+
268+
{% highlight javascript %}
269+
import './style.css'
270+
import 'scalajs:main.js'
271+
{% endhighlight %}
272+
273+
When we `import` a URI starting with `scalajs:`, `vite-plugin-scalajs` resolves it to point to the output directory of Scala.js' `fastLinkJS` task.
274+
275+
You may have to stop and restart the `npm run dev` process, so that Vite picks up the newly created configuration file.
276+
Vite will refresh the browser with our updated "Hello Scala.js!" message.
277+
278+
## Live changes with Scala.js
279+
280+
Earlier, we noticed how changing the JavaScript files caused Vite to immediately refresh the browser.
281+
Is that also the case if we change the Scala source files?
282+
283+
Indeed it is.
284+
Let us change the message to
285+
286+
{% highlight diff %}
287+
<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript" target="_blank">
288+
<img src="/javascript.svg" class="logo vanilla" alt="JavaScript logo" />
289+
</a>
290+
- <h1>Hello Scala.js!</h1>
291+
+ <h1>Hello Scala.js and Vite!</h1>
292+
<div class="card">
293+
<button id="counter" type="button"></button>
294+
</div>
295+
{% endhighlight %}
296+
297+
Once we save, we notice that the browser refreshes with the updated message.
298+
299+
There are two things happening behind the scenes:
300+
301+
1. The `~fastLinkJS` task in sbt notices that a `.scala` file has changed, and therefore rebuilds the `.js` output.
302+
1. The `npm run dev` process with Vite notices that a `.js` file imported from `/main.js` has changed, and triggers a refresh with the updated files.
303+
304+
All these steps are *incremental*.
305+
When we change a single Scala file, only that one gets recompiled by the Scala incremental compiler.
306+
Then, only the affected small `.js` modules produced by `fastLinkJS` are regenerated.
307+
Finally, Vite only reloads those small `.js` files that were touched.
308+
This ensures that the development cycle remains as short as possible.
309+
310+
## Production build
311+
312+
The `fastLinkJS` task of sbt and the `npm run dev` task of Vite are optimized for incremental development.
313+
For production, we want to perform more optimizations on the Scala.js side and bundle minimized files with `npm run build`.
314+
We stop Vite with `Ctrl+C` and launch the following instead:
315+
316+
{% highlight shell %}
317+
$ npm run build
318+
319+
320+
> vite build
321+
322+
vite v4.1.4 building for production...
323+
[info] welcome to sbt 1.8.0 (Temurin Java 1.8.0_362)
324+
[...]
325+
[info] Full optimizing .../livechart/target/scala-3.2.2/livechart-opt
326+
.../livechart/target/scala-3.2.2/livechart-opt
327+
✓ 11 modules transformed.
328+
dist/index.html 0.45 kB
329+
dist/assets/javascript-8dac5379.svg 1.00 kB
330+
dist/assets/index-48a8825f.css 1.24 kB │ gzip: 0.65 kB
331+
dist/assets/index-3c83baa6.js 28.84 kB │ gzip: 6.97 kB
332+
{% endhighlight %}
333+
334+
Since the built website uses an ECMAScript module, we need to serve it through an HTTP server to visualize it.
335+
We can use Vite's `preview` mode for that purpose, as we can run it without any additional dependency:
336+
337+
{% highlight shell %}
338+
$ npm run preview
339+
340+
341+
> vite preview
342+
343+
➜ Local: http://localhost:4173/
344+
➜ Network: use --host to expose
345+
{% endhighlight %}
346+
347+
Navigate to the mentioned URL to see your website.
348+
349+
## Conclusion
350+
351+
In this tutorial, we saw how to configure Scala.js with Vite from the ground up using `@scala-js/vite-plugin-scalajs`.
352+
We used sbt as our build tool, but the same effect can be achieved with any other Scala build tool, such as [Mill](https://com-lihaoyi.github.io/mill/) or [scala-cli](https://scala-cli.virtuslab.org/).
353+
354+
Our setup features the following properties:
355+
356+
* Development mode with live reloading: changing Scala source files automatically triggers recompilation and browser refresh.
357+
* Production mode taking the fully optimized output of Scala.js and producing a unique `.js` file.
358+
359+
In our [next tutorial about Laminar](./laminar.html), we will learn how to write UIs in idiomatic Scala code.

0 commit comments

Comments
 (0)