Mein SBT-Setup für Scala 3 und Java EE 11
Der Build ist kein Nebenthema. Er ist ein Teil der Architektur. Wenn ein Projekt fachlich sauber gedacht ist, aber der Build zufällig zusammengewachsen ist, entsteht an einer sehr zentralen Stelle Unruhe. Deshalb behandle ich mein SBT-Setup nicht als technische Pflichtübung, sondern als strukturelle Grundlage.
Dieses Projekt ist dafür ein gutes Beispiel, weil es nicht nur ein JVM-Backend ist. Es verbindet Scala 3, Scala.js, SSR, Vite und ein paketierbares Backend. Genau an solchen Stellen kippen Builds oft in zwei Extreme: entweder in wilde Copy-Paste-Konfiguration oder in hochabstrakte Build-Metaprogrammierung, die später niemand mehr anfassen möchte.
Mein Ziel ist etwas dazwischen: wenige wiederverwendbare Bausteine, aber explizite Projekte.
ThisBuild / version := "0.1.1-SNAPSHOT"
ThisBuild / scalaVersion := "3.8.3"
ThisBuild / serverConnectionType := ConnectionType.Tcp
val repositoryDir = file(".").getCanonicalFile
val jfx2Version = "2.2.6-SNAPSHOT"Das hier ist unspektakulär, aber wichtig. Ein Build sollte seine globalen Prämissen klar benennen. Version, Scala-Version und zentrale Konstanten gehören nicht versteckt, sondern sichtbar an den Anfang.
Danach teile ich nur das heraus, was wirklich gemeinsamer Baustein ist. Zum Beispiel die Scala.js-Linker-Konfiguration.
lazy val commonJsSettings = Seq(
scalaJSLinkerConfig ~= (
_.withModuleKind(ModuleKind.ESModule)
.withESFeatures(_.withESVersion(ESVersion.ES2021))
)
)Der Punkt ist nicht nur Wiederverwendung. Der Punkt ist, dass die gemeinsame technische Linie an einer Stelle definiert ist und danach von den beteiligten Projekten bewusst übernommen wird. Das ist etwas anderes als „irgendwo läuft schon irgendwas“.
Die Projektstruktur selbst bleibt dagegen konkret:
lazy val frontend =
Project(id = "simplicity-blog-frontend", base = file("frontend"))
.enablePlugins(ScalaJSPlugin)
.settings(
scalaJSUseMainModuleInitializer := false,
libraryDependencies += "com.anjunar" %%% "scalajs-jfx2-core" % jfx2Version,
libraryDependencies += "com.anjunar" %%% "scalajs-jfx2-controls" % jfx2Version,
libraryDependencies += "com.anjunar" %%% "scalajs-jfx2-ssr" % jfx2Version
)
.settings(commonJsSettings)Und auf der Serverseite:
lazy val system = Project(id = "simplicity-blog-system", base = file("system"))
lazy val domain = Project(id = "simplicity-blog-domain", base = file("domain"))
.dependsOn(system)
lazy val rest = Project(id = "simplicity-blog-rest", base = file("rest"))
.dependsOn(domain)
lazy val application =
Project(id = "simplicity-blog-backend", base = file("application"))
.dependsOn(rest)Das ist kein komplizierter Multi-Module-Build. Aber er zeigt eine wichtige Entscheidung: Die Modulgrenzen im Build folgen der Architektur und nicht bloß dem Ordnerbaum. system, domain, rest, application, frontend sind keine beliebigen Namen. Sie machen sichtbar, wie das System gedacht ist.
Besonders wichtig wird das dort, wo Packaging und Laufzeit zusammenkommen. Ich mag es nicht, wenn Build, Deployment und Runtime als voneinander getrennte Welten behandelt werden. Deshalb ist auch das Paketieren im Build selbst explizit modelliert.
lazy val frontendBuild = taskKey[File]("Builds the frontend with Vite")
lazy val appPackage =
taskKey[File]("Builds a distributable application directory")Und später:
val frontendServerBundle = (frontend / Compile / fullOptJS).value.data
val frontendDist = frontendBuild.value
IO.copyDirectory(
frontendDist,
frontendDir,
overwrite = true,
preserveLastModified = true
)
IO.copyFile(
frontendServerBundle,
ssrDir / "main.js",
preserveLastModified = true
)Genau hier trennt sich für mich ein sauberer Build von einem bloßen Startskript-Orchester. Der Build weiß, welche Artefakte am Ende zusammengehören. Er kennt Frontend, Backend und SSR als Teile eines Gesamtsystems. Das macht die Auslieferung robuster, weil die Architektur bis zum Packaging durchgehalten wird.
Warum SBT und nicht etwas anderes? Weil SBT in Scala-Projekten genau dort stark ist, wo ich Präzision will: bei Modellen, Abhängigkeiten, Task-Flüssen und Projektbeziehungen. Es ist nicht immer charmant. Aber es ist ehrlich genug, um Build-Logik nicht hinter hübschen Schaltern zu verstecken.
Mein SBT-Setup ist also nicht deswegen gut, weil es besonders clever wäre. Es ist gut, wenn es lesbar bleibt, die Modulgrenzen unterstützt und die Packaging-Realität des Systems korrekt ausdrückt. Ein Build sollte nicht beeindrucken. Er sollte tragen.
Das ist die eigentliche Idee: weniger Build-Magie, mehr Build-Form.
Noch keine Kommentare.