From 3b52696fa3e8876f8b883b6bdcecb377da6f4dcf Mon Sep 17 00:00:00 2001 From: Simon Parten Date: Wed, 3 Apr 2024 15:28:45 +0200 Subject: [PATCH] Setup a propoer pipline? --- .github/workflows/ci.yml | 14 +++ .gitignore | 3 +- htmlGen.test.scala | 26 ----- justfile | 22 ++-- live.serverJvm.scala => live.server.scala | 86 +++++++++------- live.server.test.scala | 116 ++++++++++++++++++++++ project.scala | 7 +- readme.md | 21 ++++ 8 files changed, 217 insertions(+), 78 deletions(-) delete mode 100644 htmlGen.test.scala rename live.serverJvm.scala => live.server.scala (77%) create mode 100644 live.server.test.scala create mode 100644 readme.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b5b78f..97a4b77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,7 +8,21 @@ on: pull_request: jobs: + + test: + if: github.event_name == 'push' || github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: coursier/cache-action@v6.4 + - uses: VirtusLab/scala-cli-setup@main + - uses: taiki-e/install-action@just + - run: just gha + publish: + needs: test if: github.event_name == 'push' runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index 08d27c2..a23a448 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .metals .bsp .vscode -native \ No newline at end of file +native +testDir \ No newline at end of file diff --git a/htmlGen.test.scala b/htmlGen.test.scala deleted file mode 100644 index d994b19..0000000 --- a/htmlGen.test.scala +++ /dev/null @@ -1,26 +0,0 @@ -import fs2.io.file.Path - -class HtmlGenTest extends munit.FunSuite: - test("makeHeader should return the correct header") { - val modules = List( - (Path("/path/to/module1"), "1h") - ) - - val expectedHeader = - """ - - - - -
- -""".replaceAll("\\s", "") - - val actualHeader = makeHeader(modules) - - println(expectedHeader.replaceAll("\\s", "")) - println(actualHeader.render.replaceAll("\\s", "")) - - assert(actualHeader.render.replaceAll("\\s", "") == expectedHeader) - } -end HtmlGenTest diff --git a/justfile b/justfile index 9c61951..c43c1e9 100644 --- a/justfile +++ b/justfile @@ -3,26 +3,22 @@ default: just --list setupIde: - scala-cli setup-ide . + scala-cli setup-ide . --exclude testDir --exclude native package: scala-cli package . - test: - scala-cli test project.scala htmlGen.scala htmlGen.test.scala - -packageWatcher: - scala-cli package file.nativewatcher.scala project.native.scala -f - -serveNative: - scala-cli run file.nativewatcher.scala project.native.scala - -procT: - scala-cli run process.scala project.scala + scala-cli test . --exclude testDir --exclude native jvmWatch: scala-cli run project.scala file.watcher.scala jvmServe: - scala-cli run project.scala live.serverJvm.scala htmlGen.scala file.hasher.scala -- "/Users/simon/Code/helloScalaJs/out" \ No newline at end of file + scala-cli run project.scala live.serverJvm.scala htmlGen.scala file.hasher.scala -- "/Users/simon/Code/helloScalaJs/out" + +setupPlaywright: + cs launch com.microsoft.playwright:playwright:1.41.1 -M "com.microsoft.playwright.CLI" -- install --with-deps + +gha: + setupPlaywright test \ No newline at end of file diff --git a/live.serverJvm.scala b/live.server.scala similarity index 77% rename from live.serverJvm.scala rename to live.server.scala index 06863cb..0c43f56 100644 --- a/live.serverJvm.scala +++ b/live.server.scala @@ -33,7 +33,7 @@ import fs2.io.file.Files import fs2.io.Watcher.Event import org.http4s.ServerSentEvent import _root_.io.circe.Encoder -import Main2.seedMapOnStart + import cats.syntax.strong import fs2.concurrent.Topic @@ -41,35 +41,41 @@ sealed trait FrontendEvent(val typ: String) derives Encoder.AsObject case class KeepAlive(override val typ: String = "keepAlive") extends FrontendEvent(typ) derives Encoder.AsObject case class PageRefresh(override val typ: String = "pageRefresh") extends FrontendEvent(typ) derives Encoder.AsObject -object Main2 extends IOApp: - - import cats.effect.Concurrent - - val refreshTopic = Topic[IO, String].toResource - - def buildRunner(refreshTopic: Topic[IO, String]) = ProcessBuilder( - "just", - "packageW" - ).withWorkingDirectory(fs2.io.file.Path("/Users/simon/Code/helloScalaJs")) - .spawn[IO] - .use { p => - // p.stderr.through(fs2.io.stdout).compile.drain >> - p.stderr - .through(text.utf8.decode) - .debug() - .chunks - .evalMap(aChunk => - if aChunk.toString.contains("node ./") then - // IO.println("emit") >> - refreshTopic.publish1("hi") - else IO.unit - ) - .compile - .drain - } - .background +object LiveServer extends IOApp: + + private val refreshTopic = Topic[IO, String].toResource + + private def buildRunner(refreshTopic: Topic[IO, String], workDir: fs2.io.file.Path, outDir: fs2.io.file.Path) = + ProcessBuilder( + "scala-cli", + "--power", + "package", + "--js", + ".", + "-o", + outDir.toString(), + "-f", + "-w" + ).withWorkingDirectory(workDir) + .spawn[IO] + .use { p => + // p.stderr.through(fs2.io.stdout).compile.drain >> + p.stderr + .through(text.utf8.decode) + .debug() + .chunks + .evalMap(aChunk => + if aChunk.toString.contains("node ./") then + // IO.println("emit") >> + refreshTopic.publish1("hi") + else IO.unit + ) + .compile + .drain + } + .background - def seedMapOnStart(stringPath: String, mr: MapRef[IO, String, Option[String]]) = + private def seedMapOnStart(stringPath: String, mr: MapRef[IO, String, Option[String]]) = val asFs2 = fs2.io.file.Path(stringPath) fs2.io.file .Files[IO] @@ -92,7 +98,7 @@ object Main2 extends IOApp: end seedMapOnStart - def fileWatcher( + private def fileWatcher( stringPath: fs2.io.file.Path, mr: MapRef[IO, String, Option[String]] ): ResourceIO[IO[OutcomeIO[Unit]]] = @@ -135,7 +141,7 @@ object Main2 extends IOApp: .background end fileWatcher - def routes( + private def routes( stringPath: String, refreshTopic: Topic[IO, String] ): Resource[IO, (HttpApp[IO], MapRef[IO, String, Option[String]], Ref[IO, Map[String, String]])] = @@ -169,7 +175,7 @@ object Main2 extends IOApp: ) end routes - def buildServer(httpApp: HttpApp[IO]) = EmberServerBuilder + private def buildServer(httpApp: HttpApp[IO]) = EmberServerBuilder .default[IO] .withHttp2 .withHost(host"localhost") @@ -178,20 +184,26 @@ object Main2 extends IOApp: .withShutdownTimeout(10.milli) .build + /* + args.head is the base directory + args.tail.head is the output directory + */ override def run(args: List[String]): IO[ExitCode] = - val serveDir = args.head + println("args || " + args.mkString(",")) + val baseDir = args.head + val outDir = args.tail.head val server = for _ <- IO.println("Start dev server ").toResource refreshPub <- refreshTopic - _ <- buildRunner(refreshPub) - routes <- routes(serveDir, refreshPub) + _ <- buildRunner(refreshPub, fs2.io.file.Path(baseDir), fs2.io.file.Path(outDir)) + routes <- routes(outDir.toString(), refreshPub) (app, mr, ref) = routes _ <- seedMapOnStart(args.head, mr) - _ <- fileWatcher(fs2.io.file.Path(serveDir), mr) + _ <- fileWatcher(fs2.io.file.Path(baseDir), mr) server <- buildServer(app) yield server server.use(_ => IO.never).as(ExitCode.Success) end run -end Main2 +end LiveServer diff --git a/live.server.test.scala b/live.server.test.scala new file mode 100644 index 0000000..d9e2120 --- /dev/null +++ b/live.server.test.scala @@ -0,0 +1,116 @@ +import scala.compiletime.uninitialized +import com.microsoft.playwright.* +import com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat +import com.sun.net.httpserver.*; +import java.net.InetSocketAddress; +import com.microsoft.playwright.Page.InputValueOptions +import com.sun.net.httpserver.SimpleFileServer +import java.nio.file.Paths +import com.microsoft.playwright.impl.driver.Driver +import scala.concurrent.Future + +import cats.effect.unsafe.implicits.global + +/* +Run +cs launch com.microsoft.playwright:playwright:1.41.1 -M "com.microsoft.playwright.CLI" -- install --with-deps +before this test, to make sure that the driver bundles are downloaded. + */ +class PlaywrightTest extends munit.FunSuite: + + val port = 8080 + var pw: Playwright = uninitialized + var browser: Browser = uninitialized + var page: Page = uninitialized + + val testDir = os.pwd / "testDir" + val outDir = testDir / ".out" + + override def beforeAll(): Unit = + + pw = Playwright.create() + browser = pw.chromium().launch(); + page = browser.newPage(); + end beforeAll + + test("incremental") { + + if os.exists(testDir) then os.remove.all(os.pwd / "testDir") + os.makeDir.all(outDir) + + os.write.over(testDir / "hello.scala", helloWorldCode("Hello")) + os.write.over(testDir / ".out" / "styles.less", "") + + LiveServer + .run( + List( + testDir.toString, + outDir.toString + ) + ) + .unsafeToFuture() + + Thread.sleep(500) // give the thing time to start. + + page.navigate(s"http://localhost:8085/index.html") + assertThat(page.locator("h1")).containsText("HelloWorld"); + + os.write.over(testDir / "hello.scala", helloWorldCode("Bye")) + assertThat(page.locator("h1")).containsText("ByeWorld"); + + os.write.append(testDir / ".out" / "styles.less", "h1 { color: red; }") + assertThat(page.locator("h1")).hasCSS("color", "rgb(255, 0, 0)") + + } + + override def afterAll(): Unit = + super.afterAll() + pw.close() + // os.remove.all(testDir) + + end afterAll + +end PlaywrightTest + +def helloWorldCode(greet: String) = s""" +//> using scala 3.3.3 +//> 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( + "$greet", + 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 + ) + ) + ) +""" diff --git a/project.scala b/project.scala index 6b881db..3a7a095 100644 --- a/project.scala +++ b/project.scala @@ -1,6 +1,8 @@ //> using scala 3.4.1 //> using platform jvm +//> using exclude testDir + //> using dep org.http4s::http4s-ember-server::0.23.26 //> using dep org.http4s::http4s-dsl::0.23.26 //> using dep org.http4s::http4s-scalatags::0.25.2 @@ -10,7 +12,10 @@ //> using dep co.fs2::fs2-io::3.10.2 //> using dep com.lihaoyi::scalatags::0.12.0 -//> using dep org.scalameta::munit::1.0.0-M11 +//> using test.dep org.scalameta::munit::1.0.0-M11 +//> using test.dep com.microsoft.playwright:playwright:1.41.1 +//> using test.dep com.microsoft.playwright:driver-bundle:1.41.1 +//> using test.dep com.lihaoyi::os-lib:0.9.3 //> using publish.repository central-s01 //> using publish.organization io.github.quafadas diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..26623a0 --- /dev/null +++ b/readme.md @@ -0,0 +1,21 @@ +# An experiment in a dev server for scala JS + +Try and break the dependance on node / npm completely whilst retaining a sane developer experience for browser based scala-js development. + +## Goals +- Live reload / link on change +- Hot application of style (no page reload) +- Proxy server (TODO) + +## Contraints + +- Scala cli to build frontend +- ESModule output (only) +- Third party ESModules via import map rather than npm +- Styles through LESS + +## Quickstart + +```sh + +``` \ No newline at end of file