From 569b1c5d34f12142d10e1afa8dfc48d83ea5f63f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADt=20=C5=A0est=C3=A1k?= Date: Thu, 21 Sep 2023 16:20:30 +0200 Subject: [PATCH 1/5] Update SBT + Scala + libs --- Dockerfile | 11 +-- README.md | 6 +- build.sbt | 90 +++++++++++-------- .../src/main/scala/com/example/Moment.scala | 41 ++++++--- client/src/main/scala/com/v6ak/zbdb/App.scala | 8 +- .../com/v6ak/zbdb/BestParticipantData.scala | 1 + .../main/scala/com/v6ak/zbdb/Bootstrap.scala | 8 ++ .../main/scala/com/v6ak/zbdb/HtmlUtils.scala | 5 +- .../src/main/scala/com/v6ak/zbdb/Parser.scala | 5 +- .../scala/com/v6ak/zbdb/PartTimeInfo.scala | 1 + .../scala/com/v6ak/zbdb/PlotRenderer.scala | 6 +- .../main/scala/com/v6ak/zbdb/Renderer.scala | 5 +- pack.sh | 9 +- project/build.properties | 2 +- project/plugins.sbt | 27 +++--- server/app/controllers/Application.scala | 13 +-- server/conf/routes | 4 +- 17 files changed, 146 insertions(+), 96 deletions(-) create mode 100644 client/src/main/scala/com/v6ak/zbdb/Bootstrap.scala diff --git a/Dockerfile b/Dockerfile index 36dc7f8..fb58410 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,24 +3,25 @@ # This Dockerfile has one required ARG to determine which base image # to use for the JDK to install. -# We need JDK 8, as it does not compile with JDK 11 for some unknown reason. +# JDK 8 is the LTS with the longest support at the time of writing. ARG OPENJDK_TAG=8 # First stage just determines SBT version. If build.properties changes without changing the SBT version, it just rebuilds the first stage without rebuilding the second stage. FROM amazoncorretto:${OPENJDK_TAG} AS sbt-version COPY project/build.properties / -#RUN sed -n -e 's/^sbt\.version=//p' /build.properties > /version -RUN echo 1.1.1 > /version +RUN sed -n -e 's/^sbt\.version=//p' /build.properties > /version # Second stage creates final image with the right SBT version FROM amazoncorretto:${OPENJDK_TAG} +RUN yum install -y rsync lftp zip unzip which +ARG NODE_VERSION=16 +RUN yum install https://rpm.nodesource.com/pub_${NODE_VERSION}.x/nodistro/repo/nodesource-release-nodistro-1.noarch.rpm -y +RUN yum install nodejs npm -y --setopt=nodesource-nodejs.module_hotfixes=1 COPY --from=sbt-version /version /sbt-version RUN \ curl -L -o sbt-$(cat /sbt-version).rpm https://scala.jfrog.io/ui/api/v1/download\?repoKey=rpm\&path=%252Fsbt-$(cat /sbt-version).rpm && \ echo SBT launcher downloaded && \ rpm -i sbt-$(cat /sbt-version).rpm && \ rm sbt-$(cat /sbt-version).rpm && \ - sbt sbtVersion && \ mkdir /project WORKDIR /project -RUN yum install -y rsync lftp nodejs zip unzip which diff --git a/README.md b/README.md index 6c90d9c..dd2a49b 100644 --- a/README.md +++ b/README.md @@ -5,18 +5,18 @@ Kompiluje se do statického JS, běží celé v prohlížeči. Troška informac ## Běh lokálně 1. Nainstaluj si SBT (nebo použij Dockerové prostředí) -2. activator ~run +2. `sbt "project server" ~run` 3. Otevři http://localhost:9000/2016/statistiky/ (případně jiný rok) ## Přidání ročníku 1. Uprav project/PageGenerator.scala -2. Pokud běží Activator, restartuj ho nebo použij příkaz reload. +2. Pokud běží SBT, restartuj ho nebo použij příkaz reload. ## Export na web a. Pouze pro Linux/MacOS: `./pack.sh` vygeneruje pack.zip -b. Kdekoliv: `activator stage` vygeneruje soubor server/target/scala-$scalaVersion/zbdb-stats-server_$scalaVersion-$version-web-assets.jar, ve kterém je adresář public. +b. Kdekoliv: `sbt stage` vygeneruje soubor server/target/scala-$scalaVersion/zbdb-stats-server_sjs${scalaJsVersion}_$scalaVersion-$version-web-assets.jar, ve kterém je adresář public. ## Verze formátu diff --git a/build.sbt b/build.sbt index 46d887a..f3c9132 100644 --- a/build.sbt +++ b/build.sbt @@ -1,6 +1,9 @@ +import scala.sys.process._ +import java.io.IOException + val appVersion= "1.0" -lazy val scalaV = "2.11.8" +lazy val scalaV = "2.13.12" val jqueryName: String = "jquery/2.1.4/jquery.js" @@ -34,22 +37,40 @@ def write(file: File, content: String) = { file } +def download(out: File, source: URL) = { + if(out.exists) { + println(s"Source $source is already downloaded at $out.") + } else { + val tmpFile = file(out + ".tmp") + println(s"Downloading $source…") + tmpFile.getParentFile.mkdirs() + source #> tmpFile !; + if(! tmpFile.renameTo(out)) { + throw new IOException(s"Renaming $tmpFile to $out failed!") + } + println(s"Downloaded $source as $out") + } + out +} + +// Generates other assets than client JS, plus contains a server for development purposes lazy val server = (project in file("server")).settings( version := appVersion, name := "zbdb-stats-server", scalaVersion := scalaV, scalaJSProjects := Seq(client), - pipelineStages in Assets := Seq(scalaJSPipeline), + scalaJSStage := FullOptStage, + Assets / pipelineStages := Seq(scalaJSPipeline), pipelineStages := Seq(concat, removeLibs, filter, digest, simpleUrlUpdate/*, digest*/, removeUnversionedAssets, gzip, moveLibs), - includeFilter in digest := "*", - excludeFilter in digest := "*.html" || "*.csv" || "*.json" || "*.json.new" || + digest / includeFilter := "*", + digest / excludeFilter := "*.html" || "*.csv" || "*.json" || "*.json.new" || // When sbt-simple-url-update updates path for glyphicons-halflings-regular.woff, it garbles the path for glyphicons-halflings-regular.woff2. "glyphicons-halflings-regular.woff", - includeFilter in filter := "*.less" || "*.note" || "*.source" || "*.css" || "*.js", - excludeFilter in filter := "main.js" || "main.min.js" || "main.css" || "main.min.css", + filter / excludeFilter := "*.less" || "*.note" || "*.source" || "*.css" - "main.min.css" || "*.js" - "main.min.js", + filter / includeFilter := "*.css" || "*.html" || "*.js" || "*.csv" || "*.svg" || "*.woff" || "*.ttf" || "*.eot" || "*.woff2" || "*.json.new", genHtmlDir := target.value / "web" / "html" / "main", - resourceDirectories in Assets += genHtmlDir.value, - resourceGenerators in Assets += Def.task { + Assets / resourceDirectories += genHtmlDir.value, + Assets / resourceGenerators += Def.task { val yearHtmlFiles = for(year <- PageGenerator.Years) yield { write( file = genHtmlDir.value / s"${year.year}" / PublicDirName / s"index.html", @@ -59,14 +80,12 @@ lazy val server = (project in file("server")).settings( val allYearsListJsonFile = write(genHtmlDir.value / "years.json.new", PageGenerator.allYearsJsonString) yearHtmlFiles :+ allYearsListJsonFile }.taskValue, - resourceGenerators in Assets += Def.task { + Assets / resourceGenerators += Def.task { for(year <- PageGenerator.Years if year.dataSource.csvDownloadUrl startsWith "https://") yield { - val out = genHtmlDir.value / s"${year.year}" / PublicDirName / s"${year.year}.csv" - IO.download( - new java.net.URL(year.dataSource.csvDownloadUrl), - out + download( + out = genHtmlDir.value / s"${year.year}" / PublicDirName / s"${year.year}.csv", + source = url(year.dataSource.csvDownloadUrl) ) - out } }.taskValue, removeUnversionedAssets := { mappings: Seq[PathMapping] => @@ -81,40 +100,43 @@ lazy val server = (project in file("server")).settings( case (file, name) => (file, PublicDirName + "/" + name) } }, - includeFilter in simpleUrlUpdate := "*.css" || "*.js" || "*.html", + simpleUrlUpdate / includeFilter := "*.css" || "*.js" || "*.html", // triggers scalaJSPipeline when using compile or continuous compilation - compile in Compile <<= (compile in Compile) dependsOn scalaJSPipeline, + Compile / compile := ((Compile / compile) dependsOn scalaJSPipeline).value, Concat.groups := Seq( - "main.min.js" -> group(Seq("zbdb-stats-client-jsdeps.min.js", "zbdb-stats-client-opt.js", "zbdb-stats-client-launcher.js")) + "main.min.js" -> group(Seq("zbdb-stats-client-jsdeps.min.js", "zbdb-stats-client-opt/main.js")) ), LessKeys.cleancss := true, LessKeys.compress := true, libraryDependencies ++= Seq( - "com.vmunier" %% "scalajs-scripts" % "1.0.0", + "com.vmunier" %% "scalajs-scripts" % "1.2.0", + guice, bootstrap, jqPlot, specs2 % Test ), - // Compile the project before generating Eclipse files, so that generated .scala or .class files for views and routes are present - EclipseKeys.preTasks := Seq(compile in Compile) -).enablePlugins(PlayScala)//.dependsOn(sharedJvm) +).enablePlugins(PlayScala, JSDependenciesPlugin, SbtWeb)//.dependsOn(sharedJvm) +def toPathMapping(f: File): PathMapping = f -> f.getName + +// Generates client JS; other assets are generated by the server subproject lazy val client = (project in file("client")).settings( name := "zbdb-stats-client", version := appVersion, + scalaJSStage := FullOptStage, scalaVersion := scalaV, - persistLauncher := true, - persistLauncher in Test := false, - scalaJSSemantics ~= (_.withRuntimeClassName { linkedClass => - val fullName = linkedClass.fullName - if (fullName.endsWith("Exception")) fullName - else "" - }), + scalaJSUseMainModuleInitializer := true, + Test / scalaJSUseMainModuleInitializer := false, libraryDependencies ++= Seq( - "org.scala-js" %%% "scalajs-dom" % "0.9.1", - "com.lihaoyi" %%% "scalatags" % "0.5.2", - "com.github.marklister" %%% "product-collections" % "1.4.2" + "org.scala-js" %%% "scalajs-dom" % "2.7.0", + "com.lihaoyi" %%% "scalatags" % "0.12.0", + "com.nrinaudo" %%% "kantan.csv" % "0.7.0", ), + + // Some magic required for compatibility with running the website directly from SBT (using Play) + Compile / fastLinkJS / jsMappings += toPathMapping((Compile / packageJSDependencies).value), + Compile / fullLinkJS / jsMappings += toPathMapping((Compile / packageMinifiedJSDependencies).value), + jsDependencies ++= Seq( bootstrap / "bootstrap.min.js", "org.webjars" % "momentjs" % "2.10.6" / "min/moment.min.js", @@ -130,8 +152,8 @@ lazy val client = (project in file("client")).settings( jqPlot / "jqplot.pointLabels.min.js" dependsOn "jquery.jqplot.min.js", jqPlot / "jqplot.highlighter.min.js" dependsOn "jquery.jqplot.min.js", "org.webjars.bower" % "console-polyfill" % "0.2.2" / "console-polyfill/0.2.2/index.js" - ) -).enablePlugins(ScalaJSPlugin, ScalaJSWeb)//.dependsOn(sharedJs) + ), +).enablePlugins(ScalaJSPlugin, JSDependenciesPlugin, ScalaJSWeb)//.dependsOn(sharedJs) /*lazy val shared = (crossProject.crossType(CrossType.Pure) in file("shared")). settings(scalaVersion := scalaV). @@ -145,5 +167,3 @@ name := "zbdb-stats" version := appVersion -// loads the server project at sbt startup -onLoad in Global := (Command.process("project server", _: State)) compose (onLoad in Global).value diff --git a/client/src/main/scala/com/example/Moment.scala b/client/src/main/scala/com/example/Moment.scala index 43b2e94..9867490 100644 --- a/client/src/main/scala/com/example/Moment.scala +++ b/client/src/main/scala/com/example/Moment.scala @@ -2,9 +2,10 @@ package com.example import com.example.moment.Moment -import scalajs.js +import scala.scalajs.js +import scala.scalajs.js.annotation._ -@js.native class MomentSingleton extends js.Any{ +@js.native trait MomentSingleton extends js.Any { //def moment(): Moment = js.native def utc(): Moment = js.native def utc(x: Number): Moment = js.native @@ -18,9 +19,9 @@ import scalajs.js def tz(time: String, tz: String): Moment = js.native } -package object moment extends scalajs.js.GlobalScope { - - +@js.native +@JSGlobalScope +object MomentJsGlobal extends js.Any { def moment(moment: Moment): Moment = js.native def moment(dateString: String): Moment = js.native def moment(dateString: String, format: String): Moment = js.native @@ -32,12 +33,24 @@ package object moment extends scalajs.js.GlobalScope { } +package object moment { + @inline def moment(moment: Moment): Moment = MomentJsGlobal.moment(moment: Moment) + @inline def moment(dateString: String): Moment = MomentJsGlobal.moment(dateString: String) + @inline def moment(dateString: String, format: String): Moment = MomentJsGlobal.moment(dateString: String, format: String) + @inline def moment(dateString: String, format: String, locale: String): Moment = MomentJsGlobal.moment(dateString: String, format: String, locale: String) + @inline def moment(dateString: String, format: String, strict: Boolean): Moment = MomentJsGlobal.moment(dateString: String, format: String, strict: Boolean) + @inline def moment(dateString: String, format: String, locale: String, strict: Boolean): Moment = MomentJsGlobal.moment(dateString: String, format: String, locale: String, strict: Boolean) + @inline def moment: MomentSingleton = MomentJsGlobal.moment +} + package moment{ + + import scala.scalajs.js.Date @js.native -class Moment extends js.Any { +trait Moment extends js.Any { def add(time: Int, units: String): Moment = js.native //def plus(time: Int, units: String): Moment = js.native @@ -52,9 +65,9 @@ class Moment extends js.Any { def minutes(v: Int): Moment = js.native def seconds(): Int = js.native def minus(other: Moment): Int = js.native - def -(other: Moment): Int = js.native + @JSOperator def -(other: Moment): Int = js.native def plus(other: Int): Moment = js.native - def +(other: Int): Moment = js.native + @JSOperator def +(other: Int): Moment = js.native def date(): Int = js.native def date(v: Int): Moment = js.native @@ -66,11 +79,6 @@ class Moment extends js.Any { def isAfter(other: Moment): Boolean = js.native def isAfter(other: Moment, precision: String): Boolean = js.native - def >=(other: Moment):Boolean = isSame(other) || (this isAfter other) - def <=(other: Moment):Boolean = isSame(other) || (this isBefore other) - def >(other: Moment): Boolean = this isAfter other - def <(other: Moment): Boolean = this isBefore other - def isSame(other: Moment): Boolean = js.native //def clone(): Moment = js.native @@ -87,10 +95,15 @@ object Moment { class RichMoment(val moment: Moment) extends AnyVal{ + def >=(other: Moment):Boolean = moment.isSame(other) || (moment isAfter other) + def <=(other: Moment):Boolean = moment.isSame(other) || (moment isBefore other) + def >(other: Moment): Boolean = moment isAfter other + def <(other: Moment): Boolean = moment isBefore other + def hoursAndMinutes = f"${moment.hours()}%d:${moment.minutes()}%02d" } object RichMoment{ - implicit def toRichMoment(moment: Moment) = new RichMoment(moment) + implicit def toRichMoment(moment: Moment): RichMoment = new RichMoment(moment) } \ No newline at end of file diff --git a/client/src/main/scala/com/v6ak/zbdb/App.scala b/client/src/main/scala/com/v6ak/zbdb/App.scala index 49f7172..a22859d 100644 --- a/client/src/main/scala/com/v6ak/zbdb/App.scala +++ b/client/src/main/scala/com/v6ak/zbdb/App.scala @@ -4,16 +4,16 @@ import com.example.moment._ import org.scalajs.dom import org.scalajs.dom.ext.Ajax -import scala.scalajs.js.{JSApp, JSON} +import scala.scalajs.js.JSON import scala.scalajs.js.annotation.JSExport import scala.util.{Failure, Success} -import scala.concurrent.ExecutionContext.Implicits.global +import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue import scala.scalajs.js -object App extends JSApp { +object App { @JSExport - override def main(): Unit = { + def main(args: Array[String]): Unit = { dom.window.onload = { _: Any => val body = dom.window.document.body val fileName = body.getAttribute("data-file") diff --git a/client/src/main/scala/com/v6ak/zbdb/BestParticipantData.scala b/client/src/main/scala/com/v6ak/zbdb/BestParticipantData.scala index cd275f6..a8c83d7 100644 --- a/client/src/main/scala/com/v6ak/zbdb/BestParticipantData.scala +++ b/client/src/main/scala/com/v6ak/zbdb/BestParticipantData.scala @@ -3,6 +3,7 @@ package com.v6ak.zbdb import com.example.moment.Moment import com.v6ak.scalajs.time.TimeInterval import com.v6ak.zbdb.PartTimeInfo.Finished +import com.example.RichMoment._ object BestParticipantData{ val Empty = BestParticipantData(None, None, None) diff --git a/client/src/main/scala/com/v6ak/zbdb/Bootstrap.scala b/client/src/main/scala/com/v6ak/zbdb/Bootstrap.scala new file mode 100644 index 0000000..afe8118 --- /dev/null +++ b/client/src/main/scala/com/v6ak/zbdb/Bootstrap.scala @@ -0,0 +1,8 @@ +package com.v6ak.zbdb + +import scalatags.JsDom.all._ + +object Bootstrap { + val toggle = data.toggle + val dismiss = data.dismiss +} diff --git a/client/src/main/scala/com/v6ak/zbdb/HtmlUtils.scala b/client/src/main/scala/com/v6ak/zbdb/HtmlUtils.scala index a49bbe4..be1b8fc 100644 --- a/client/src/main/scala/com/v6ak/zbdb/HtmlUtils.scala +++ b/client/src/main/scala/com/v6ak/zbdb/HtmlUtils.scala @@ -4,19 +4,20 @@ import org.scalajs.dom import scala.scalajs.js import scalatags.JsDom.all._ +import Bootstrap._ object HtmlUtils { val EmptyHtml: Frag = "" def dropdownGroup(mods: Modifier*)(buttons: Frag*) = div(cls:="btn-group")( - button(cls:="btn btn-normal dropdown-toggle", "data-toggle".attr := "dropdown", "aria-haspopup".attr := "true", "aria-expanded".attr := "false")(mods: _*), + button(cls:="btn btn-normal dropdown-toggle", toggle := "dropdown", aria.haspopup := "true", aria.expanded := "false")(mods: _*), ul(cls:="dropdown-menu")(buttons.map(li(_)) : _*) ) def modal(title: Frag) = { val modalBodyId = IdGenerator.newId() - val modalHeader = div(`class`:="modal-header")(button(`type`:="button", `class`:="close", "data-dismiss".attr := "modal")(span("aria-hidden".attr := "true")("×")))(h4(`class`:="modal-title")(title)) + val modalHeader = div(`class`:="modal-header")(button(`type`:="button", `class`:="close", dismiss := "modal")(span(aria.hidden := "true")("×")))(h4(`class`:="modal-title")(title)) val modalBody = div(`class`:="modal-body", id := modalBodyId) val modalFooter = div(`class`:="modal-footer") val modalDialog = div(`class`:="modal-dialog modal-xxl")(div(`class`:="modal-content")(modalHeader, modalBody, modalFooter)) diff --git a/client/src/main/scala/com/v6ak/zbdb/Parser.scala b/client/src/main/scala/com/v6ak/zbdb/Parser.scala index c8a4911..e66f05e 100644 --- a/client/src/main/scala/com/v6ak/zbdb/Parser.scala +++ b/client/src/main/scala/com/v6ak/zbdb/Parser.scala @@ -3,7 +3,6 @@ package com.v6ak.zbdb import java.io.StringReader import com.example.moment._ -import com.github.marklister.collections.io.CSVReader import com.v6ak.scalajs.time.TimeInterval import com.v6ak.zbdb.PartTimeInfo.Finished import org.scalajs.dom @@ -97,7 +96,9 @@ object Parser{ } def parse(csvData: String, startTime: Moment, totalEndTime: Moment, maxHourDelta: Int, formatVersion: FormatVersion) = { - val fullDataTable = new CSVReader(new StringReader(csvData.trim)).map(_.clone()).toIndexedSeq.map(_.toIndexedSeq) + import kantan.csv._ + import kantan.csv.ops._ + val fullDataTable: IndexedSeq[IndexedSeq[String]] = csvData.trim.unsafeReadCsv[IndexedSeq, IndexedSeq[String]](rfc) val Seq(header1, header2, header3, dataWithTail @ _*) = fullDataTable.drop(formatVersion.headSize) val (dataTable, footer) = formatVersion.tail.split(dataWithTail.dropWhile(_.head == "").toIndexedSeq) footer.foreach{fl => diff --git a/client/src/main/scala/com/v6ak/zbdb/PartTimeInfo.scala b/client/src/main/scala/com/v6ak/zbdb/PartTimeInfo.scala index ecfe823..f6efdff 100644 --- a/client/src/main/scala/com/v6ak/zbdb/PartTimeInfo.scala +++ b/client/src/main/scala/com/v6ak/zbdb/PartTimeInfo.scala @@ -2,6 +2,7 @@ package com.v6ak.zbdb import com.example.moment.Moment import com.v6ak.scalajs.time.TimeInterval +import com.example.RichMoment._ abstract sealed class PartTimeInfo{ def endTimeOption: Option[Moment] diff --git a/client/src/main/scala/com/v6ak/zbdb/PlotRenderer.scala b/client/src/main/scala/com/v6ak/zbdb/PlotRenderer.scala index 2c8115a..12b9b05 100644 --- a/client/src/main/scala/com/v6ak/zbdb/PlotRenderer.scala +++ b/client/src/main/scala/com/v6ak/zbdb/PlotRenderer.scala @@ -7,8 +7,10 @@ import org.scalajs.dom import scala.collection.immutable import scala.scalajs.js import scala.scalajs.js.Dictionary +import scala.scalajs.js +import scala.scalajs.js.annotation._ -@js.native object `$` extends js.Object{ +@JSGlobal @js.native object `$` extends js.Object{ @js.native object jqplot extends js.Object{ @js.native class LinearAxisRenderer extends js.Object{ def createTicks(plot: js.Dynamic): Unit = js.native @@ -148,7 +150,7 @@ final class PlotRenderer(participantTable: ParticipantTable) { } private def computeCumulativeMortality(rows: Seq[Participant]) = { - val mortalityMap = rows.map(_.partTimes.size).groupBy(identity).mapValues(_.size).map(identity) + val mortalityMap = rows.map(_.partTimes.size).groupBy(identity).mapValues(_.size).map(identity).toMap val mortalitySeq = (0 to mortalityMap.keys.max).map(mortalityMap.getOrElse(_, 0)) mortalitySeq.scan(0)(_ + _).tail } diff --git a/client/src/main/scala/com/v6ak/zbdb/Renderer.scala b/client/src/main/scala/com/v6ak/zbdb/Renderer.scala index f96b2a2..64b6171 100644 --- a/client/src/main/scala/com/v6ak/zbdb/Renderer.scala +++ b/client/src/main/scala/com/v6ak/zbdb/Renderer.scala @@ -34,6 +34,7 @@ final class Renderer private(participantTable: ParticipantTable, processingError private val plotRenderer = new PlotRenderer(participantTable) import Renderer._ + import Bootstrap._ import participantTable._ import plotRenderer._ @@ -129,7 +130,7 @@ final class Renderer private(participantTable: ParticipantTable, processingError dom.console.warn(s"It seems that nobody has reached part #$i") BestParticipantData.Empty } - def moreButton(c: String) = button(cls := s"btn btn-default btn-xs dropdown-toggle $c", `type` := "button", "data-toggle".attr := "dropdown")(span(cls:="caret"))//("⠇") + def moreButton(c: String) = button(cls := s"btn btn-default btn-xs dropdown-toggle $c", `type` := "button", toggle := "dropdown")(span(cls:="caret"))//("⠇") val firstCell = if (i == 0) TableHeadCell("Start") else TableHeadCell.Empty Seq[Column[Participant]]( Column.rich(firstCell, TableHeadCell("|=>"))((r: Participant) => @@ -265,7 +266,7 @@ final class Renderer private(participantTable: ParticipantTable, processingError name, Seq(row), plot.generator, - Seq(span(`class`:=s"glyphicon glyphicon-${plot.glyphiconName}", "aria-hidden".attr := "true")) + Seq(span(`class`:=s"glyphicon glyphicon-${plot.glyphiconName}", aria.hidden := "true")) )(title := name) ) diff --git a/pack.sh b/pack.sh index 2ba9278..0a968f0 100755 --- a/pack.sh +++ b/pack.sh @@ -5,21 +5,22 @@ set -u set -e set -o pipefail -"$(which sbt activator | head -n1)" dist +sbt "project server" dist version=$( - "$(which sbt activator | head -n1)" version | tail -n1 | sed 's/^.* //' | grep -oE '[0-9.A-Z-]+' | head -n2 | tail -n1 + sbt version | tail -n1 | sed 's/^.* //' | grep -oE '[0-9.A-Z-]+' | head -n2 | tail -n1 ) scalaVersion=$( - "$(which sbt activator | head -n1)" scalaVersion | tail -n1 | sed 's/^.* //' | grep -oE '[0-9.A-Z-]+' | head -n2 | tail -n1 | sed 's/^\([^.]*.[^.]*\).*/\1/' + sbt server/scalaVersion | tail -n1 | sed 's/^.* //' | grep -oE '[0-9.A-Z-]+' | head -n2 | tail -n1 | sed 's/^\([^.]*.[^.]*\).*/\1/' ) +scalaJsVersion=1 # TODO: extract if [ -e target/assets ]; then rm -r target/assets fi mkdir -p target mkdir target/assets -unzip -d target/assets "./server/target/scala-$scalaVersion/zbdb-stats-server_$scalaVersion-$version-web-assets.jar" +unzip -d target/assets "./server/target/scala-$scalaVersion/zbdb-stats-server_sjs${scalaJsVersion}_$scalaVersion-$version-web-assets.jar" if [ -e pack.zip ]; then diff --git a/project/build.properties b/project/build.properties index 8932b7d..c82d99b 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1,4 +1,4 @@ #Activator-generated Properties #Mon Sep 28 19:56:15 CEST 2015 template.uuid=e17acfbb-1ff5-41f5-b8cf-2c40be6a8340 -sbt.version=0.13.11 +sbt.version=1.9.6 diff --git a/project/plugins.sbt b/project/plugins.sbt index d020bbd..14eef35 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -8,24 +8,29 @@ resolvers += Resolver.url("heroku-sbt-plugin-releases", url("https://dl.bintray.com/heroku/sbt-plugins/"))(Resolver.ivyStylePatterns) // Sbt plugins -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.6") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.20") -addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.3") +addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.3") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.12") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.13.2") -addSbtPlugin("com.vmunier" % "sbt-web-scalajs" % "1.0.1") +addSbtPlugin("com.vmunier" % "sbt-web-scalajs" % "1.2.0") -addSbtPlugin("com.typesafe.sbt" % "sbt-gzip" % "1.0.0") +addSbtPlugin("com.typesafe.sbt" % "sbt-gzip" % "1.0.2") -addSbtPlugin("com.typesafe.sbt" % "sbt-digest" % "1.1.1") +addSbtPlugin("com.typesafe.sbt" % "sbt-digest" % "1.1.4") -addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.1.0") +addSbtPlugin("com.typesafe.sbt" % "sbt-less" % "1.1.2") -addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "4.0.0") +addSbtPlugin("net.ground5hark.sbt" % "sbt-concat" % "0.2.0") -addSbtPlugin("net.ground5hark.sbt" % "sbt-concat" % "0.1.9") +// WARNING: when switching between fork, check the logic of includeFilter and excludeFilter. +// There are multiple forks with a different logic of includeFilter and excludeFilter. +// When you aren't careful, it can lead to missing/extra files in the export. +addSbtPlugin("com.github.karelcemus" % "sbt-filter" % "1.1.0") -addSbtPlugin("com.slidingautonomy.sbt" % "sbt-filter" % "1.0.1") +addSbtPlugin("org.github.ngbinh" % "sbt-simple-url-update" % "1.0.4") -addSbtPlugin("org.neolin.sbt" % "sbt-simple-url-update" % "1.0.0") \ No newline at end of file +addSbtPlugin("org.scala-js" % "sbt-jsdependencies" % "1.0.2") + +dependencyOverrides += "org.scala-lang.modules" %% "scala-xml" % "2.1.0" diff --git a/server/app/controllers/Application.scala b/server/app/controllers/Application.scala index 389d6a8..192e5e5 100644 --- a/server/app/controllers/Application.scala +++ b/server/app/controllers/Application.scala @@ -7,17 +7,12 @@ import play.api.libs.json.JsString import play.api.mvc._ import play.twirl.api.Txt -class Application @Inject() ()(implicit environment: Environment) extends Controller { - - def yearStats(year: Int) = Assets.versioned(path = "/public", file = Assets.Asset(s"$year/statistiky/index.html")) +class Application @Inject() (cc: ControllerComponents)(implicit environment: Environment) + extends AbstractController(cc) +{ def yearStatsRedirect(year: Int) = Action{ - Redirect(routes.Application.yearStats(year)) - } - - def staticFile(year: Int, file: String) = { - println(s"$year/statistiky/$file") - Assets.versioned(path = "/public", file = Assets.Asset(s"$year/statistiky/$file")) + Redirect(routes.Assets.at(s"$year/statistiky/index.html")) } def mainJs() = Action { diff --git a/server/conf/routes b/server/conf/routes index 8f9dfa6..e9170ec 100644 --- a/server/conf/routes +++ b/server/conf/routes @@ -6,10 +6,10 @@ GET /statistiky/main.min.js controllers.Application.mainJs() # Simulate index.html -GET /:year/statistiky/ controllers.Application.yearStats(year: Int) +GET /:year/statistiky/ controllers.Application.yearStatsRedirect(year: Int) GET /:year/statistiky controllers.Application.yearStatsRedirect(year: Int) # Map static resources from the /public folder to the /URL path GET /statistiky/*file controllers.Assets.versioned(path="/public", file: Asset) # Map year-specific static resources -GET /:year/statistiky/*file controllers.Application.staticFile(year: Int, file) +GET /*file controllers.Assets.at(path="/public", file) From 298baa52bca1db3bb9f5189c43fd7a5e26bb426b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADt=20=C5=A0est=C3=A1k?= Date: Fri, 22 Sep 2023 21:49:36 +0200 Subject: [PATCH 2/5] Use JS-based regexes. It fixes year 2015 (which contains a NBSP, which wasn't matched by \s regex). Additionally, it should decrease the output JS size --- .../com/v6ak/scalajs/regex/JsPattern.scala | 22 +++++++++++++++++++ .../com/v6ak/scalajs/time/TimeInterval.scala | 4 +++- .../src/main/scala/com/v6ak/zbdb/Parser.scala | 10 ++++----- 3 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 client/src/main/scala/com/v6ak/scalajs/regex/JsPattern.scala diff --git a/client/src/main/scala/com/v6ak/scalajs/regex/JsPattern.scala b/client/src/main/scala/com/v6ak/scalajs/regex/JsPattern.scala new file mode 100644 index 0000000..8888a37 --- /dev/null +++ b/client/src/main/scala/com/v6ak/scalajs/regex/JsPattern.scala @@ -0,0 +1,22 @@ +package com.v6ak.scalajs.regex + +import scala.scalajs.js +import scala.language.implicitConversions + + +class JsPattern(val regex: js.RegExp) extends AnyVal { + def unapplySeq(s: String): Option[Seq[String]] = regex.exec(s) match { + case null => None + case parts => Some(parts.toSeq.drop(1).asInstanceOf[Seq[String]]) + } +} + +object JsPattern { + + class JsPatternFactory(val s: String) extends AnyVal { + @inline def jsr: JsPattern = new JsPattern(new js.RegExp(s)) + } + + @inline implicit def wrapString(s: String): JsPatternFactory = new JsPatternFactory(s) + +} diff --git a/client/src/main/scala/com/v6ak/scalajs/time/TimeInterval.scala b/client/src/main/scala/com/v6ak/scalajs/time/TimeInterval.scala index 9400f50..ed9e91f 100644 --- a/client/src/main/scala/com/v6ak/scalajs/time/TimeInterval.scala +++ b/client/src/main/scala/com/v6ak/scalajs/time/TimeInterval.scala @@ -1,5 +1,7 @@ package com.v6ak.scalajs.time +import com.v6ak.scalajs.regex.JsPattern.wrapString + case class TimeInterval(totalMinutes: Int) extends AnyVal{ def hours = totalMinutes/60 def minutes = totalMinutes%60 @@ -8,7 +10,7 @@ case class TimeInterval(totalMinutes: Int) extends AnyVal{ } object TimeInterval{ - private val TimeIntervalRegex = """^([0-9]+):([0-9]+)$""".r + private val TimeIntervalRegex = """^([0-9]+):([0-9]+)$""".jsr def parse(s: String) = s match { case TimeIntervalRegex(hs, ms) => TimeInterval(hs.toInt*60 + ms.toInt) diff --git a/client/src/main/scala/com/v6ak/zbdb/Parser.scala b/client/src/main/scala/com/v6ak/zbdb/Parser.scala index e66f05e..a618805 100644 --- a/client/src/main/scala/com/v6ak/zbdb/Parser.scala +++ b/client/src/main/scala/com/v6ak/zbdb/Parser.scala @@ -1,7 +1,5 @@ package com.v6ak.zbdb -import java.io.StringReader - import com.example.moment._ import com.v6ak.scalajs.time.TimeInterval import com.v6ak.zbdb.PartTimeInfo.Finished @@ -10,7 +8,7 @@ import org.scalajs.dom import scala.collection.immutable import scala.scalajs.js import scala.util.Try -import scala.util.matching.Regex +import com.v6ak.scalajs.regex.JsPattern._ object Parser{ @@ -18,14 +16,14 @@ object Parser{ private val StrictMode = true - private val TrackLengthRegex = """^([0-9]+(?:,[0-9]+)?)\s?(?:km)?$""".r + private val TrackLengthRegex = """^([0-9]+(?:,[0-9]+)?)\s?(?:km)?$""".jsr private def parseTrackLength(s: String) = s match { case TrackLengthRegex(tl) => BigDecimal(tl.replace(',', '.')) case other => sys.error(s"Unknown track length: $s") } - private val TimeRegexp = """^([0-9]+):([0-9]+)$""".r + private val TimeRegexp = """^([0-9]+):([0-9]+)$""".jsr private def strictCheck(f: => Unit): Unit = { if(StrictMode){ @@ -72,7 +70,7 @@ object Parser{ case e: Throwable => throw CellsParsingException(data, e) } - private val Empty = """^(?:x|X|)$""".r + private val Empty = """^(?:x|X|)$""".jsr private def parseTimeInfo(data: Seq[String], prevTimeOption: Option[Moment], maxHourDelta: Int): Option[PartTimeInfo] = guard(data){ data match { From 870070f6c812ce019f2c55a44b6e95c74a15886e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADt=20=C5=A0est=C3=A1k?= Date: Thu, 21 Sep 2023 16:21:35 +0200 Subject: [PATCH 3/5] PageGenerator: Code style --- project/PageGenerator.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project/PageGenerator.scala b/project/PageGenerator.scala index fcb2af0..075290c 100644 --- a/project/PageGenerator.scala +++ b/project/PageGenerator.scala @@ -80,7 +80,7 @@ object PageGenerator{ startTime = "2021-09-17 17:05", endTime = "2021-09-18 20:05", dataSource= NewGoogleSpreadsheetDataSource("2PACX-1vSo5X1l36As8yB9-XRquV9UGIAcptWSzm7P7bEIoj93WVcEhwYumOKOG6h3O147IASNhAJrVwd-CKDq") ), -Year( + Year( year = 2022, formatVersion = 2021, startTime = "2022-09-16 17:30", endTime = "2022-09-17 20:00", dataSource= NewGoogleSpreadsheetDataSource("2PACX-1vQEmRVRc1DBm9PZoRU-4oKu0p6gTWqv6lYbvvrDwGT-umiXtB4Xy13NEcFeanZ37PTw2UrN8TYaHK15") @@ -89,7 +89,7 @@ Year( year = 2023, formatVersion = 2021, startTime = "2023-09-15 17:30", endTime = "2023-09-16 18:10", dataSource= NewGoogleSpreadsheetDataSource("2PACX-1vTzrUrHEarwmtqap2WZQRMJvO7UVy6rGln2xuZv5kWa_slIM6c_-p7BasSUkipAJs86iIOwWDtJlrlb") - ) + ), ) val YearLinks = LegacyYears ++ Years.map(y => y.year -> s"../../${y.year}/statistiky/") From 3830a7dda732b35d977dbe2bd73091172bab5ce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADt=20=C5=A0est=C3=A1k?= Date: Thu, 21 Sep 2023 21:36:38 +0200 Subject: [PATCH 4/5] Fix undefined behavior with substring with empty location.search --- client/src/main/scala/com/v6ak/zbdb/App.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/main/scala/com/v6ak/zbdb/App.scala b/client/src/main/scala/com/v6ak/zbdb/App.scala index a22859d..094d31a 100644 --- a/client/src/main/scala/com/v6ak/zbdb/App.scala +++ b/client/src/main/scala/com/v6ak/zbdb/App.scala @@ -26,7 +26,7 @@ object App { dom.console.log("startTime", startTime.toString) dom.console.log("endTime", endTime.toString) dom.console.log("fileName", fileName) - val params = dom.window.location.search.substring(1).split("&").map{paramString => + val params = dom.window.location.search.drop(1).split("&").map{paramString => paramString.split("=", 2) match { case Array(name, value) => (name, value) case Array(name) => (name, "") From cd592d828ca0fd7779303d5902d73144c1a06b15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADt=20=C5=A0est=C3=A1k?= Date: Fri, 22 Sep 2023 08:47:29 +0200 Subject: [PATCH 5/5] [CI] Test with recent Java/NPM versions --- .github/workflows/build.yml | 16 +++++++++++++++- Dockerfile | 20 ++++++++++++++++---- dockerenv-github.sh | 4 +++- 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2e32f56..ea8d68b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,4 +23,18 @@ jobs: uses: actions/upload-artifact@v2 with: name: site - path: pack.zip \ No newline at end of file + path: pack.zip + + test-bleeding-edge-java-and-npm: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: build image + run: docker build . --target bleeding-edge + - name: build website + run: env DOCKER_TAG=bleeding-edge ./dockerenv-github.sh ./pack.sh + - name: Upload artifact + uses: actions/upload-artifact@v2 + with: + name: site + path: pack-for-verification.zip diff --git a/Dockerfile b/Dockerfile index fb58410..575e069 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,5 @@ # Adopted from mozilla/sbt -# This Dockerfile has one required ARG to determine which base image -# to use for the JDK to install. - # JDK 8 is the LTS with the longest support at the time of writing. ARG OPENJDK_TAG=8 @@ -12,7 +9,7 @@ COPY project/build.properties / RUN sed -n -e 's/^sbt\.version=//p' /build.properties > /version # Second stage creates final image with the right SBT version -FROM amazoncorretto:${OPENJDK_TAG} +FROM amazoncorretto:${OPENJDK_TAG} AS conservative RUN yum install -y rsync lftp zip unzip which ARG NODE_VERSION=16 RUN yum install https://rpm.nodesource.com/pub_${NODE_VERSION}.x/nodistro/repo/nodesource-release-nodistro-1.noarch.rpm -y @@ -25,3 +22,18 @@ RUN \ rm sbt-$(cat /sbt-version).rpm && \ mkdir /project WORKDIR /project + +# This stage can be used for testing whether the build is compatible with fresh versions of JDK and NodeJS/NPM +FROM fedora:latest AS bleeding-edge +RUN dnf install -y rsync lftp zip unzip which nodejs npm java-latest-openjdk-devel +COPY --from=sbt-version /version /sbt-version +RUN \ + curl -L -o sbt-$(cat /sbt-version).rpm https://scala.jfrog.io/ui/api/v1/download\?repoKey=rpm\&path=%252Fsbt-$(cat /sbt-version).rpm && \ + echo SBT launcher downloaded && \ + rpm -i sbt-$(cat /sbt-version).rpm && \ + rm sbt-$(cat /sbt-version).rpm && \ + mkdir /project +WORKDIR /project + + +FROM conservative AS default diff --git a/dockerenv-github.sh b/dockerenv-github.sh index 2a3a66f..b5be403 100755 --- a/dockerenv-github.sh +++ b/dockerenv-github.sh @@ -4,6 +4,8 @@ set -u set -e set -o pipefail +TAG=${DOCKER_TAG:-conservative} + exec docker run \ -v "$HOME"/.ivycache:/root/.ivy2 \ -v "$HOME"/.sbtcache:/root/.sbt \ @@ -12,5 +14,5 @@ exec docker run \ -v "$HOME"/.ssh/zbdb-id_rsa.pub:/root/.ssh/id_rsa.pub:ro \ -v "$(pwd)":/project \ -p 9000:9000 \ - "$(docker build -q .)" \ + "$(docker build -q . --target $TAG)" \ "$@"