Here are the key advantages of this approach:
-
Getting started is cca 10 seconds and you are live reloading your changes in browser. No config.
-
There is no bundling misdirection, so source maps work. You can debug scalaJS straight out of VSCode by following the standard VSSCode debugging instructions.
-
Because there's no seperate ecosystem or NPM to configure, configuring build and CI is a lot easier. No
node_modules
to worry about. I found that this simplicity infected everything around it. -
In terms of performance; NPM dependancies are loaded out the CDN. This is slow the first time - but seriously - check the network browser tools when you refresh the page. The second time they are all served out of browser cache - it takes 0ms. Even better, that cache survives application redeployment!. If you pre-load the (fat) "internal-" xxx dependancies scalaJS produces, this combination crushes page load times.
-
You can use the same build tool for both backend and frontend, and share code between them.
-
Unit testing the frontend with the Playwright java client suddenly bursts into life...
- And be because it's all orchestrated in the same process - you can test the styles and the interplay between the application state and styles which is where most of my bugs are.
-
Your build becomes very simple. Here is a mill build.sc file that builds an assembly, including your static resources, frontend app and API. Example project
object backend extends Common with ScalafmtModule with ScalafixModule {
def ivyDeps =
super.ivyDeps() ++ Config.jvmDependencies ++ Config.sharedDependencies
def moduleDeps = Seq(shared.jvm)
def frontendResources = T{PathRef(frontend.fullLinkJS().dest.path)}
def staticAssets = T.source{PathRef(frontend.millSourcePath / "ui")}
def allClasspath = T{localClasspath() ++ Seq(frontendResources()) ++ Seq(staticAssets()) }
override def assembly: T[PathRef] = T{
Assembly.createAssembly(
Agg.from(allClasspath().map(_.path)),
manifest(),
prependShellScript(),
Some(upstreamAssembly2().pathRef.path),
assemblyRules
)
}
}
A dockerfile that uses the assembly:
FROM azul/zulu-openjdk-alpine:17
# See the GHA for building the assembly
COPY "./out/backend/assembly.dest/out.jar" "/app/app.jar"
EXPOSE 8080
ENTRYPOINT [ "java", "-jar", "/app/app.jar" ]
Here is a GHA, that deploys that assembly to fly.io.
name: Deploy to fly.io
on:
push:
branches: [main]
env:
FLY_API_TOKEN: $
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: coursier/setup-action@main
with:
jvm: temurin@17
apps: mill
- uses: superfly/flyctl-actions/setup-flyctl@master
- name: Build application
run: mill show backend.assembly -j 0
- name: Deploy to fly.io
run: flyctl deploy --remote-only
You are live.