Skip to content

Commit 87b02c9

Browse files
authored
Rewrite Scala.js linking to enable packaging of projects without a main method (#3992)
1 parent 8c48451 commit 87b02c9

File tree

2 files changed

+65
-39
lines changed

2 files changed

+65
-39
lines changed

modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala

Lines changed: 43 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -720,14 +720,14 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
720720
isFullOpt <- builds.head.options.scalaJsOptions.fullOpt
721721
linkerConfig = builds.head.options.scalaJsOptions.linkerConfig(logger)
722722
linkResult <- linkJs(
723-
builds,
724-
destPath,
725-
mainClass,
723+
builds = builds,
724+
dest = destPath,
725+
mainClassOpt = mainClass,
726726
addTestInitializer = false,
727-
linkerConfig,
728-
isFullOpt,
729-
builds.head.options.scalaJsOptions.noOpt.getOrElse(false),
730-
logger
727+
config = linkerConfig,
728+
fullOpt = isFullOpt,
729+
noOpt = builds.head.options.scalaJsOptions.noOpt.getOrElse(false),
730+
logger = logger
731731
)
732732
} yield linkResult
733733

@@ -1023,48 +1023,52 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers {
10231023
builds.head.options.archiveCache
10241024
)
10251025
}
1026-
val relMainJs = os.rel / "main.js"
1027-
val relSourceMapJs = os.rel / "main.js.map"
1028-
val mainJs = linkingDir / relMainJs
1029-
val sourceMapJs = linkingDir / relSourceMapJs
1030-
1031-
if (os.exists(mainJs))
1032-
if (
1033-
os.walk.stream(linkingDir)
1034-
.filter(_ != mainJs)
1035-
.filter(_ != sourceMapJs)
1036-
.headOption
1037-
.nonEmpty
1038-
) {
1039-
// copy linking dir to dest
1040-
os.copy(
1041-
linkingDir,
1042-
dest,
1043-
createFolders = true,
1044-
replaceExisting = true,
1045-
mergeFolders = true
1046-
)
1026+
os.walk.stream(linkingDir).filter(_.ext == "js").toSeq match {
1027+
case Seq(sourceJs) if os.isFile(sourceJs) && sourceJs.last.endsWith(".js") =>
1028+
// there's just one js file to link, so we copy it directly
10471029
logger.debug(
1048-
s"Scala.js linker generate multiple files for js multi-modules. Copy files to $dest directory."
1030+
s"Scala.js linker generated single file ${sourceJs.last}. Copying it to $dest"
10491031
)
1050-
dest / "main.js"
1051-
}
1052-
else {
1053-
os.copy(mainJs, dest, replaceExisting = true)
1054-
if (builds.head.options.scalaJsOptions.emitSourceMaps && os.exists(sourceMapJs)) {
1032+
val sourceMapJs = os.Path(sourceJs.toString + ".map")
1033+
os.copy(sourceJs, dest, replaceExisting = true)
1034+
if builds.head.options.scalaJsOptions.emitSourceMaps && os.exists(sourceMapJs) then {
1035+
logger.debug(
1036+
s"Source maps emission enabled, copying source map file: ${sourceMapJs.last}"
1037+
)
10551038
val sourceMapDest =
10561039
builds.head.options.scalaJsOptions.sourceMapsDest.getOrElse(os.Path(s"$dest.map"))
10571040
val updatedMainJs = ScalaJsLinker.updateSourceMappingURL(dest)
10581041
os.write.over(dest, updatedMainJs)
10591042
os.copy(sourceMapJs, sourceMapDest, replaceExisting = true)
10601043
logger.message(s"Emitted js source maps to: $sourceMapDest")
10611044
}
1062-
10631045
dest
1064-
}
1065-
else {
1066-
val found = os.walk(linkingDir).map(_.relativeTo(linkingDir))
1067-
value(Left(new ScalaJsLinkingError(relMainJs, found)))
1046+
case _ @Seq(jsSource, _*) =>
1047+
os.copy(
1048+
linkingDir,
1049+
dest,
1050+
createFolders = true,
1051+
replaceExisting = true,
1052+
mergeFolders = true
1053+
)
1054+
logger.debug(
1055+
s"Scala.js linker generated multiple files for js multi-modules. Copied files to $dest directory."
1056+
)
1057+
val jsFileToReturn = os.rel / {
1058+
mainClassOpt match {
1059+
case Some(_) if os.exists(linkingDir / "main.js") => "main.js"
1060+
case Some(mc) if os.exists(linkingDir / s"$mc.js") => s"$mc.js"
1061+
case _ => jsSource.relativeTo(linkingDir)
1062+
}
1063+
}
1064+
dest / jsFileToReturn
1065+
case Nil =>
1066+
logger.debug("Scala.js linker did not generate any .js files.")
1067+
val allFilesInLinkingDir = os.walk(linkingDir).map(_.relativeTo(linkingDir))
1068+
value(Left(new ScalaJsLinkingError(
1069+
expected = if mainClassOpt.nonEmpty then "main.js" else "<module>.js",
1070+
foundFiles = allFilesInLinkingDir
1071+
)))
10681072
}
10691073
}
10701074
}

modules/integration/src/test/scala/scala/cli/integration/PackageTestDefinitions.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1485,4 +1485,26 @@ abstract class PackageTestDefinitions extends ScalaCliSuite with TestScalaVersio
14851485
}
14861486
}
14871487
}
1488+
1489+
if actualScalaVersion.startsWith("3") then
1490+
test("package Scala.js without a main method") {
1491+
val moduleName = "whatever"
1492+
TestInputs(
1493+
os.rel / s"$moduleName.scala" ->
1494+
s"""import scala.scalajs.js.annotation.*
1495+
|
1496+
|object $moduleName {
1497+
| @JSExportTopLevel(name = "handler", moduleID = "$moduleName")
1498+
| def handler(): Unit = {
1499+
| println("Hello world!")
1500+
| }
1501+
|}
1502+
|""".stripMargin
1503+
).fromRoot { root =>
1504+
val res =
1505+
os.proc(TestUtil.cli, "package", ".", "--js", "--power", extraOptions)
1506+
.call(cwd = root, mergeErrIntoOut = true, stderr = os.Pipe)
1507+
expect(res.out.trim().contains(s"$moduleName.js"))
1508+
}
1509+
}
14881510
}

0 commit comments

Comments
 (0)