Simon’s Guerilla guide to getting started with Scala JS.

It is assumed that coursier and scala-cli are on the path. i.e. that shell calls to

cs version

and

scala-cli -v

Behave reasonably and print their version in the shell you are using. If so… game on!

  1. mkdir viteless && cd viteless && mkdir out && touch hello.scala
  2. Copy and paste into hello.scala the below.
//> using scala 3.4.2
//> using platform js

//> using dep org.scala-js::scalajs-dom::2.8.0
//> using dep com.raquo::laminar::17.0.0-M6

//> using jsModuleKind es
//> using jsModuleSplitStyleStr smallmodulesfor
//> using jsSmallModuleForPackage webapp

package webapp

import org.scalajs.dom
import org.scalajs.dom.document
import com.raquo.laminar.api.L.{*, given}

@main
def main: Unit =
  renderOnDomContentLoaded(
    dom.document.getElementById("app"),
    interactiveApp
  )

def interactiveApp =
  val hiVar = Var("World")
  div(
    h1(
      s"Hello ",
      child.text <-- hiVar.signal
    ),
    p("This is a simple example of a Laminar app."),
    // https://demo.laminar.dev/app/form/controlled-inputs
    input(
      typ := "text",
      controlled(
        value <-- hiVar.signal,
        onInput.mapToValue --> hiVar.writer
      )
    )
  )

Run the below command in your terminal, adjusting the paths to your project

cs launch io.github.quafadas:live-server-scala-cli-js_3:0.0.12

A browser window should pop up at localhost:3000 with this hello world app.

Final step : change some code - observe browser refresh when linking completes.

That’s the TL;DR…

Goal

Can we replicate the vite “experience” of web development in scalajs using vite… without vite. Or NPM. Or node.

Basically, toss JS tooling in the bin :-).

After some time configuring node. And NPM. And vite. And then doing it all again in CI, I asked…

Wouldn’t it be more fun to… write our own frontend development server?

This is, in a way the natural evolution to this post.

Contraints

“replicating vite” is a big job. I might be stupid, but I ain’t that bloody stupid :-). We aren’t trying to replicate vite, with it’s big plugin ecosystem and support of whatever the latest frontend whizzbangery is these days. We’re trying to replicate vites experience for my scalaJS projects.

I claim that this is less stupid. YMMV.

Funnily enough though, once you break it down, each invidual piece is… not that bad…

Features

  1. insta-style application
  2. proxy requests to backend
  3. open webpage on start
  4. resolve references to JS eco-system
  5. serve website
    1. naively
    2. reloadably-on-change

If all this works, that is our definition of done.

1. Insta Style Application

I style things with LESS. It turns out, that this is built right in.

We will not be needing vite, to save ourselves from a script tag in our html. One down.

2. Proxy requests to backend

We’re in scala, right? A mythical land where just about everyone you trip over is secretly a backend ninja. Someone must have a prox… well hello.

Mostly, I copied and pasted code from there and poked it with a sharp stick until it did what I wanted.

3. open webpage on start

At least make it a challenge…

  def openBrowserWindow(uri: java.net.URI): Unit =
    println(s"opening browser window at $uri")
    if Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE) then
      Desktop.getDesktop().browse(uri)

4. resolve references to JS eco-system

Finally! Something non-trivial…

The big observation here, is that one can resolve ESModules directly out of ES module syntax in Browser. See this post for more detail. ES Modules at link time. This capability is now in SBT, scala-cli and mill.

For the purposes of this excercise, it negates the need for a bundler. Instead, we can rely on the browser resolution of ES Modules. From a strategic perspective - sure we’re giving up vite. But we replace it with a ** Browser **. Vite is good software, sure, but there are leagues, and vite ain’t in the same league as Chrome or Safari. I never looked back.

5. serve website

Naively

There is a very simple approach, which is just serve straight out of javas simple http server.

$JAVA_HOME/bin/jwebserver -d c:/temp/helloScalaJs/out -p 8000

That server starts super fast, and it proves our concept to this point works, because it resolves the modules and we can visit it in browser. It’s here because firstly it’s an easy way to verify the steps to this point, and also, because it’s super useful for unit testing. It’s killer in combination with Playwright.

As part of a hot development loop however, it’s seriously lacking. We need to restart the app on every change - which is not the experience we are looking for.

Hot Reload

Well, we now come to the point. If do things the vite way, then we need to somehow track all the module dependancies, figure out which one has changed or is dependant, reload it and heaven knows what else. Vite seems to setup some heavy duty websocket comms to manage all this.

But at this point, we’re like 100 lines of blogpost in. Time to cheat.

bah

for ((p: Path, h: Hash) <- modules)
      yield link(rel := "modulepreload", href := s"${p}?hash=${h}")

Instead of all that, the proposal is to generate index.html on the fly. We include in the header all the modules generated by the scala JS compiler as modulepreloads - they are all aggressively downloaded by the browser when index.html loads.

Module preloads have some nice properties. If we include, say, a query parameter and match that parameter, then the browser hits it’s cache when it goes to resolve the module a second time. The browser cache is fast. Proposal: file hash. If we change the hash, our (changed) module re-downloads. It means that there’s no “module ping pong” as browser traverses the module graph - it hits the cache every time, because we pre-loaded all the modules.

So reloading? Fast. Very, fast. And the difficult module resolution problems? All dealt with by your friendly neighbourhood browser.

To generate our index.html, our dev server monitors file changes, and updates a MapRef[File, Hash]. We use that MapRef to generate the index.html on demand. It appears natural, to request a page refresh (and a new index.html) when we detect linker success.

The final thing we need to do is include in index.html a script which refreshes the page when it recieves the right event from our dev server.

const sse = new EventSource('/api/v1/sse');
sse.addEventListener('message', (e) => {
  const msg = JSON.parse(e.data)

  if ('KeepAlive' in msg)
      console.log("KeepAlive")

  if ('PageRefresh' in msg)
      location.reload()
});

To trigger a page refresh, we use server sent events.

  case GET -> Root / "api" / "v1" / "sse" =>
    val keepAlive = fs2.Stream.fixedRate[IO](10.seconds).as(KeepAlive())
    Ok(
      keepAlive
        .merge(refreshTopic.subscribe(10).as(PageRefresh()))
        .map(msg => ServerSentEvent(Some(msg.asJson.noSpaces)))
    )

Does it work?

It certainly seems to. See the below screenshot.

screencap

The “fat” scalaJS dependancy gets loaded out of memory in 9.88ms on page regfresh, which means page refresh is essentially instantaneous, once the linker completes.