Newer
Older
support / project / VitePlugin.scala
import sbt._
import Keys._
import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
import org.scalajs.sbtplugin.ScalaJSPlugin
import scala.sys.process._
import sbt.nio.file.FileTreeView

class ViteDevServer() {
  private var worker: Option[Worker] = None

  def start(workDir: File, logger: Logger, globalLogger: Logger): Unit =
    this.synchronized {
      stop()
      worker = Some(new Worker(workDir, logger, globalLogger))
    }

  def stop(): Unit = this.synchronized {
    worker.foreach { w =>
      w.stop()
      worker = None
    }
  }

  private class Worker(
      workDir: File,
      logger: Logger,
      globalLogger: Logger
  ) {
    logger.info("Starting vite dev server")
    val command = Seq("yarn", "dev")
    val process = Process(command, workDir).run(
      ProcessLogger(globalLogger.info(_), globalLogger.error(_))
    )

    def stop(): Unit = {
      logger.info("Stopping vite dev server")
      process.destroy()
    }
  }

  override def finalize() = stop()
}

object VitePlugin extends AutoPlugin {
  override lazy val requires = ScalaJSPlugin
  override lazy val trigger = noTrigger

  object autoImport {
    lazy val viteBuild = taskKey[File]("Vite build")
    lazy val viteMonitoredFiles =
      taskKey[Seq[File]]("Files monitored for vite build")
    lazy val startViteDev = taskKey[Unit]("Start vite dev mode")
    lazy val stopViteDev = taskKey[Unit]("Stop vite dev mode")
  }

  import autoImport._

  private val viteDist =
    SettingKey[File]("viteDist", "Vite dist directory", KeyRanks.Invisible)

  private val viteDevServer = SettingKey[ViteDevServer](
    "viteDevServer",
    "Global vite dev server",
    KeyRanks.Invisible
  )

  override def projectSettings = Seq(
    viteDist := target.value / "vite",
    viteDevServer := new ViteDevServer(),
    startViteDev := {
      val workDir = baseDirectory.value
      val log = streams.value.log
      val globalLog = state.value.globalLogging.full
      val server = viteDevServer.value
      server.start(workDir, log, globalLog)
    },
    stopViteDev := {
      viteDevServer.value.stop()
    },
    viteMonitoredFiles := {
      val baseGlob = baseDirectory.value.toGlob
      def baseFiles(pattern: String): Glob = baseGlob / pattern
      val viteConfigs =
        FileTreeView.default.list(
          List(baseFiles("*.json"), baseFiles("*.js"), baseFiles("*.html"))
        )
      val linkerDirectory =
        (Compile / fullLinkJS / scalaJSLinkerOutputDirectory).value
      val viteInputs = FileTreeView.default.list(
        linkerDirectory.toGlob / "*.js"
      )
      (viteConfigs ++ viteInputs).map(_._1.toFile)
    },
    viteBuild := {
      val s = streams.value
      val dist = viteDist.value
      val files = viteMonitoredFiles.value
      // We depend on fullLinkJS
      val _ = (Compile / fullLinkJS).value
      def doBuild() = Process(
        "yarn" :: "build" :: "--outDir" :: dist.toString :: Nil,
        baseDirectory.value
      ) ! s.log
      val cachedFun = FileFunction.cached(s.cacheDirectory / "vite") { _ =>
        doBuild()
        Set(dist)
      }
      cachedFun(files.toSet).head
    },
    (onLoad in Global) := {
      (onLoad in Global).value.compose(
        _.addExitHook {
          viteDevServer.value.stop()
        }
      )
    }
  )
}