Skip to content

Commit

Permalink
Electron plugin (#72)
Browse files Browse the repository at this point in the history
  • Loading branch information
ptrdom authored Feb 3, 2024
1 parent 68e9af3 commit 04b0178
Show file tree
Hide file tree
Showing 30 changed files with 1,362 additions and 139 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,6 @@ jobs:
- name: Setup pnpm
run: corepack prepare [email protected] --activate
- name: Run tests
run: sbt scalafmtSbtCheck scalafmtCheckAll test scriptedSequentialPerModule
uses: coactions/setup-xvfb@v1
with:
run: sbt scalafmtSbtCheck scalafmtCheckAll test scriptedSequentialPerModule
64 changes: 42 additions & 22 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ lazy val `scalajs-esbuild` = (project in file("."))
.settings(publish / skip := true)
.aggregate(
`sbt-scalajs-esbuild`,
`sbt-scalajs-esbuild-electron`,
`sbt-scalajs-esbuild-web`,
`sbt-web-scalajs-esbuild`
)
Expand All @@ -33,6 +34,31 @@ lazy val commonSettings = Seq(
scriptedBufferLog := false
)

def shadingSettings = Seq(
// workaround for https://github.com/coursier/sbt-shading/issues/39
packagedArtifacts := {
val sbtV = (pluginCrossBuild / sbtBinaryVersion).value
val scalaV = scalaBinaryVersion.value
val packagedArtifactsV = packagedArtifacts.value
val nameV = name.value

val (legacyArtifact, legacyFile) = packagedArtifactsV
.find { case (a, _) =>
a.`type` == "jar" && a.name == nameV
}
.getOrElse(sys.error("Legacy jar not found"))
val mavenArtifact =
legacyArtifact.withName(nameV + s"_${scalaV}_$sbtV")
val mavenFile = new File(
legacyFile.getParentFile,
legacyFile.name.replace(legacyArtifact.name, mavenArtifact.name)
)
IO.copyFile(legacyFile, mavenFile)

packagedArtifactsV + (mavenArtifact -> mavenFile)
}
)

lazy val `sbt-scalajs-esbuild` =
project
.in(file("sbt-scalajs-esbuild"))
Expand Down Expand Up @@ -65,28 +91,7 @@ lazy val `sbt-scalajs-esbuild-web` = project
val () = scriptedDependencies.value
val () = (`sbt-scalajs-esbuild` / publishLocal).value
},
// workaround for https://github.com/coursier/sbt-shading/issues/39
packagedArtifacts := {
val sbtV = (pluginCrossBuild / sbtBinaryVersion).value
val scalaV = scalaBinaryVersion.value
val packagedArtifactsV = packagedArtifacts.value
val nameV = name.value

val (legacyArtifact, legacyFile) = packagedArtifactsV
.find { case (a, _) =>
a.`type` == "jar" && a.name == nameV
}
.getOrElse(sys.error("Legacy jar not found"))
val mavenArtifact =
legacyArtifact.withName(nameV + s"_${scalaV}_$sbtV")
val mavenFile = new File(
legacyFile.getParentFile,
legacyFile.name.replace(legacyArtifact.name, mavenArtifact.name)
)
IO.copyFile(legacyFile, mavenFile)

packagedArtifactsV + (mavenArtifact -> mavenFile)
}
shadingSettings
)
.dependsOn(`sbt-scalajs-esbuild`)

Expand All @@ -104,6 +109,21 @@ lazy val `sbt-web-scalajs-esbuild` =
)
.dependsOn(`sbt-scalajs-esbuild-web`)

lazy val `sbt-scalajs-esbuild-electron` =
project
.in(file("sbt-scalajs-esbuild-electron"))
.enablePlugins(SbtPlugin)
.settings(commonSettings)
.settings(
scriptedDependencies := {
val () = scriptedDependencies.value
val () = (`sbt-scalajs-esbuild` / publishLocal).value
},
shadingSettings
)
.dependsOn(`sbt-scalajs-esbuild-web`)

// workaround for https://github.com/sbt/sbt/issues/7431
TaskKey[Unit]("scriptedSequentialPerModule") := {
Def.taskDyn {
val projects: Seq[ProjectReference] = `scalajs-esbuild`.aggregate
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package scalajsesbuild

class EsbuildElectronProcessConfiguration(
val mainModuleID: String,
val preloadModuleIDs: Set[String],
val rendererModuleIDs: Set[String]
) {
override def toString: String = {
s"EsbuildElectronProcessConfiguration($mainModuleID,$preloadModuleIDs,$rendererModuleIDs)"
}
}

object EsbuildElectronProcessConfiguration {
def main(moduleID: String): EsbuildElectronProcessConfiguration =
new EsbuildElectronProcessConfiguration(moduleID, Set.empty, Set.empty)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
package scalajsesbuild

import org.scalajs.linker.interface.ModuleInitializer
import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport.ModuleKind
import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport.jsEnvInput
import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport.scalaJSLinkerConfig
import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport.scalaJSModuleInitializers
import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport.scalaJSStage
import org.scalajs.sbtplugin.Stage
import sbt.*
import sbt.AutoPlugin
import sbt.Keys.*
import scalajsesbuild.ScalaJSEsbuildPlugin.autoImport.esbuildBundle
import scalajsesbuild.ScalaJSEsbuildPlugin.autoImport.esbuildBundleScript
import scalajsesbuild.ScalaJSEsbuildPlugin.autoImport.esbuildInstall
import scalajsesbuild.ScalaJSEsbuildPlugin.autoImport.esbuildScalaJSModuleConfigurations
import scalajsesbuild.ScalaJSEsbuildWebPlugin.autoImport.esbuildBundleHtmlEntryPoints
import scalajsesbuild.ScalaJSEsbuildWebPlugin.autoImport.esbuildServeScript
import scalajsesbuild.ScalaJSEsbuildWebPlugin.autoImport.esbuildServeStart
import scalajsesbuild.electron.*

import scala.sys.process.Process

object ScalaJSEsbuildElectronPlugin extends AutoPlugin {

override def requires = ScalaJSEsbuildWebPlugin

object autoImport {
// TODO need to replace this with electron process config - not needed for bundling, but needed for serving
val esbuildElectronProcessConfiguration
: TaskKey[EsbuildElectronProcessConfiguration] = taskKey(
"Configuration linking Scala.js modules to Electron process module components"
)
}

import autoImport.*

override lazy val projectSettings: Seq[Setting[?]] =
Seq(
scalaJSLinkerConfig ~= {
_.withModuleKind(
ModuleKind.ESModule
) // TODO try using CommonJSModule - need to figure out how to disable Closure compiler
},
esbuildElectronProcessConfiguration := {
val modules: Seq[ModuleInitializer] = scalaJSModuleInitializers.value
(modules.headOption, modules.tail.isEmpty) match {
case (Some(module), true) =>
EsbuildElectronProcessConfiguration.main(module.moduleID)
case _ =>
sys.error(
"Unable to automatically derive `esbuildElectronProcessConfiguration`, the settings needs to be provided manually"
)
}
}
) ++ inConfig(Compile)(perConfigSettings) ++
inConfig(Test)(perConfigSettings)

private lazy val perConfigSettings: Seq[Setting[?]] = Seq(
esbuildScalaJSModuleConfigurations := Map.empty,
jsEnvInput := jsEnvInputTask.value,
run := Def.taskDyn {
val stageTask = scalaJSStage.value.stageTask
Def.task {
(stageTask / esbuildBundle).value

val stageTaskReport = stageTask.value.data
val mainModule = resolveMainModule(stageTaskReport)

val targetDirectory = (esbuildInstall / crossTarget).value
val outputDirectory =
(stageTask / esbuildBundle / crossTarget).value
val path =
targetDirectory
.relativize(new File(outputDirectory, mainModule.jsFileName))
.getOrElse(
sys.error(
s"Target directory [$targetDirectory] must be parent directory of output directory [$outputDirectory]"
)
)

val exitValue = Process(
"node" :: "./node_modules/electron/cli" :: path.toString :: Nil,
targetDirectory
).run(streams.value.log)
.exitValue()
if (exitValue != 0) {
sys.error(s"Nonzero exit value: $exitValue")
}
}
}.value
) ++
perScalaJSStageSettings(Stage.FastOpt) ++
perScalaJSStageSettings(Stage.FullOpt)

private def perScalaJSStageSettings(stage: Stage): Seq[Setting[?]] = {
val stageTask = stage.stageTask

Seq(
stageTask / esbuildBundleScript := {
val configurationV = configuration.value
val stageTaskReport = stageTask.value.data
val electronProcessConfiguration =
esbuildElectronProcessConfiguration.value
val (
mainModuleEntryPoint,
preloadModuleEntryPoints,
rendererModuleEntryPoints
) = extractEntryPointsByProcess(
stageTaskReport,
electronProcessConfiguration
)
val nodeEntryPointsJsArray =
(preloadModuleEntryPoints + mainModuleEntryPoint)
.map("'" + _ + "'")
.mkString("[", ",", "]")
val rendererModuleEntryPointsJsArray = rendererModuleEntryPoints
.map("'" + _ + "'")
.mkString("[", ",", "]")
val targetDirectory = (esbuildInstall / crossTarget).value
val outputDirectory =
(stageTask / esbuildBundle / crossTarget).value
val relativeOutputDirectory =
targetDirectory
.relativize(outputDirectory)
.getOrElse(
sys.error(
s"Target directory [$targetDirectory] must be parent directory of output directory [$outputDirectory]"
)
)
val relativeOutputDirectoryJs = s"'$relativeOutputDirectory'"
val htmlEntryPoints = esbuildBundleHtmlEntryPoints.value
require(
!htmlEntryPoints.forall(_.isAbsolute),
"HTML entry point paths must be relative"
)
val htmlEntryPointsJsArray =
htmlEntryPoints.map("'" + _ + "'").mkString("[", ",", "]")

val minify = if (configurationV == Test) {
false
} else {
true
}

// language=JS
s"""
|${EsbuildScripts.esbuildOptions}
|
|${EsbuildScripts.bundle}
|
|${EsbuildWebScripts.htmlTransform}
|
|${EsbuildWebScripts.transformHtmlEntryPoints}
|
|bundle(
| ${EsbuildScalaJSModuleConfiguration.EsbuildPlatform.Node.jsValue},
| $nodeEntryPointsJsArray,
| $relativeOutputDirectoryJs,
| null,
| false,
| $minify,
| 'sbt-scalajs-esbuild-node-bundle-meta.json',
| {external: ['electron']}
|);
|
|const metaFilePromise = bundle(
| ${EsbuildScalaJSModuleConfiguration.EsbuildPlatform.Browser.jsValue},
| $rendererModuleEntryPointsJsArray,
| $relativeOutputDirectoryJs,
| 'assets',
| false,
| $minify,
| 'sbt-scalajs-esbuild-renderer-bundle-meta.json'
|);
|
|metaFilePromise
| .then((metaFile) => {
| transformHtmlEntryPoints(
| $htmlEntryPointsJsArray,
| $relativeOutputDirectoryJs,
| metaFile
| );
| });
|""".stripMargin
},
stageTask / esbuildServeScript := {
val configurationV = configuration.value
val stageTaskReport = stageTask.value.data
val electronProcessConfiguration =
esbuildElectronProcessConfiguration.value
val (
mainModuleEntryPoint,
preloadModuleEntryPoints,
rendererModuleEntryPoints
) = extractEntryPointsByProcess(
stageTaskReport,
electronProcessConfiguration
)
val mainModuleEntryPointJs = s"'$mainModuleEntryPoint'"
val preloadModuleEntryPointsJs =
preloadModuleEntryPoints
.map("'" + _ + "'")
.mkString("[", ",", "]")
val rendererModuleEntryPointsJsArray = rendererModuleEntryPoints
.map("'" + _ + "'")
.mkString("[", ",", "]")
val targetDirectory = (esbuildInstall / crossTarget).value
val nodeRelativeOutputDirectoryJs = {
val outputDirectory =
(stageTask / esbuildBundle / crossTarget).value
val nodeRelativeOutputDirectory = targetDirectory
.relativize(outputDirectory)
.getOrElse(
sys.error(
s"Target directory [$targetDirectory] must be parent directory of node output directory [$outputDirectory]"
)
)
s"'$nodeRelativeOutputDirectory'"
}
val rendererRelativeOutputDirectoryJs = {
val outputDirectory =
(stageTask / esbuildServeStart / crossTarget).value
val relativeOutputDirectory =
targetDirectory
.relativize(outputDirectory)
.getOrElse(
sys.error(
s"Target directory [$targetDirectory] must be parent directory of renderer output directory [$outputDirectory]"
)
)
s"'$relativeOutputDirectory'"
}
val htmlEntryPoints = esbuildBundleHtmlEntryPoints.value
require(
!htmlEntryPoints.forall(_.isAbsolute),
"HTML entry point paths must be relative"
)
val htmlEntryPointsJsArray =
htmlEntryPoints.map("'" + _ + "'").mkString("[", ",", "]")

// language=JS
s"""
|${EsbuildScripts.esbuildOptions}
|
|${EsbuildScripts.bundle}
|
|${EsbuildWebScripts.htmlTransform}
|
|${EsbuildWebScripts.esbuildLiveReload}
|
|${EsbuildWebScripts.serve}
|
|${electron.Scripts.electronServe}
|
|serve(
| $rendererModuleEntryPointsJsArray,
| $rendererRelativeOutputDirectoryJs,
| 'assets',
| 'sbt-scalajs-esbuild-renderer-bundle-meta.json',
| 8001,
| 8000,
| $htmlEntryPointsJsArray
|)
| .then((reloadEventEmitter) => {
| electronServe(
| reloadEventEmitter,
| 8000,
| $mainModuleEntryPointJs,
| $preloadModuleEntryPointsJs,
| $nodeRelativeOutputDirectoryJs
| );
| });
|""".stripMargin
}
)
}
}
Loading

0 comments on commit 04b0178

Please sign in to comment.