|
| 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 | + |
| 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 | + |
| 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