Advantages
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 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!.
-
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: ${{ secrets.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.