Skip to content

Commit

Permalink
Enable remapping ES module imports for scala JS at link time
Browse files Browse the repository at this point in the history
  • Loading branch information
Quafadas committed Feb 16, 2024
1 parent 6b7a100 commit feea21d
Show file tree
Hide file tree
Showing 10 changed files with 221 additions and 11 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -9,5 +9,8 @@ out/
dest/
target/

*/scoverage.coverage

# ignore vim backup files
*.sw[op]

Expand Up @@ -37,44 +37,58 @@ final case class ScalaJsOptions(

@Group(HelpGroup.ScalaJs.toString)
@Tag(tags.should)
jsCheckIr: Option[Boolean] = None,
jsCheckIr: Option[Boolean] = None,

@Group(HelpGroup.ScalaJs.toString)
@HelpMessage("Emit source maps")
@Tag(tags.should)
jsEmitSourceMaps: Boolean = false,

@Group(HelpGroup.ScalaJs.toString)
@HelpMessage("Set the destination path of source maps")
@Tag(tags.should)
jsSourceMapsPath: Option[String] = None,

@Group(HelpGroup.ScalaJs.toString)
@HelpMessage("A file relative to the root directory containing import maps for ES module imports")
@Tag(tags.experimental)
jsEsModuleImportMap: Option[String] = None,

@Group(HelpGroup.ScalaJs.toString)
@Tag(tags.should)
@HelpMessage("Enable jsdom")
jsDom: Option[Boolean] = None,

@Group(HelpGroup.ScalaJs.toString)
@Tag(tags.should)
@HelpMessage("A header that will be added at the top of generated .js files")
jsHeader: Option[String] = None,

@Group(HelpGroup.ScalaJs.toString)
@Tag(tags.implementation)
@HelpMessage("Primitive Longs *may* be compiled as primitive JavaScript bigints")
jsAllowBigIntsForLongs: Option[Boolean] = None,

@Group(HelpGroup.ScalaJs.toString)
@Tag(tags.implementation)
@HelpMessage("Avoid class'es when using functions and prototypes has the same observable semantics.")
jsAvoidClasses: Option[Boolean] = None,

@Group(HelpGroup.ScalaJs.toString)
@Tag(tags.implementation)
@HelpMessage("Avoid lets and consts when using vars has the same observable semantics.")
jsAvoidLetsAndConsts: Option[Boolean] = None,

@Group(HelpGroup.ScalaJs.toString)
@Tag(tags.implementation)
@HelpMessage("The Scala.js module split style: fewestmodules, smallestmodules, smallmodulesfor")
jsModuleSplitStyle: Option[String] = None,

@Group(HelpGroup.ScalaJs.toString)
@Tag(tags.implementation)
@HelpMessage("Create as many small modules as possible for the classes in the passed packages and their subpackages.")
jsSmallModuleForPackage: List[String] = Nil,

@Group(HelpGroup.ScalaJs.toString)
@Tag(tags.should)
@HelpMessage("The Scala.js ECMA Script version: es5_1, es2015, es2016, es2017, es2018, es2019, es2020, es2021")
Expand All @@ -86,18 +100,21 @@ final case class ScalaJsOptions(
@Tag(tags.implementation)
@Hidden
jsLinkerPath: Option[String] = None,

@Group(HelpGroup.ScalaJs.toString)
@HelpMessage(s"Scala.js CLI version to use for linking (${Constants.scalaJsCliVersion} by default).")
@ValueDescription("version")
@Tag(tags.implementation)
@Hidden
jsCliVersion: Option[String] = None,

@Group(HelpGroup.ScalaJs.toString)
@HelpMessage("Scala.js CLI Java options")
@Tag(tags.implementation)
@ValueDescription("option")
@Hidden
jsCliJavaArg: List[String] = Nil,

@Group(HelpGroup.ScalaJs.toString)
@HelpMessage("Whether to run the Scala.js CLI on the JVM or using a native executable")
@Tag(tags.implementation)
Expand Down
Expand Up @@ -244,7 +244,8 @@ final case class SharedOptions(
moduleSplitStyleStr = jsModuleSplitStyle,
smallModuleForPackage = jsSmallModuleForPackage,
esVersionStr = jsEsVersion,
noOpt = jsNoOpt
noOpt = jsNoOpt,
remapEsModuleImportMap = jsEsModuleImportMap.filter(_.trim.nonEmpty).map(os.Path(_, Os.pwd))
)
}

Expand Down
Expand Up @@ -6,6 +6,9 @@ import scala.build.errors.BuildException
import scala.build.options.{BuildOptions, JavaOpt, ScalaJsMode, ScalaJsOptions, ShadowingSeq}
import scala.build.{Logger, Positioned, options}
import scala.cli.commands.SpecificationLevel
import scala.util.Try
import build.Ops.EitherOptOps
import os.Path

@DirectiveGroupName("Scala.js options")
@DirectiveExamples("//> using jsModuleKind common")
Expand Down Expand Up @@ -39,6 +42,8 @@ import scala.cli.commands.SpecificationLevel
|`//> using jsModuleSplitStyleStr` _value_
|
|`//> using jsEsVersionStr` _value_
|
|`//> using jsEsModuleImportMap` _value_
|""".stripMargin
)
@DirectiveDescription("Add Scala.js options")
Expand All @@ -51,6 +56,7 @@ final case class ScalaJs(
jsModuleKind: Option[String] = None,
jsCheckIr: Option[Boolean] = None,
jsEmitSourceMaps: Option[Boolean] = None,
jsEsModuleImportMap: Option[String] = None,
jsSmallModuleForPackage: List[String] = Nil,
jsDom: Option[Boolean] = None,
jsHeader: Option[String] = None,
Expand All @@ -61,7 +67,7 @@ final case class ScalaJs(
jsEsVersionStr: Option[String] = None
) extends HasBuildOptions {
// format: on
def buildOptions: Either[BuildException, BuildOptions] = either {
def buildOptions: Either[BuildException, BuildOptions] =
val scalaJsOptions = ScalaJsOptions(
version = jsVersion,
mode = ScalaJsMode(jsMode),
Expand All @@ -76,14 +82,37 @@ final case class ScalaJs(
avoidLetsAndConsts = jsAvoidLetsAndConsts,
moduleSplitStyleStr = jsModuleSplitStyleStr,
esVersionStr = jsEsVersionStr,
noOpt = jsNoOpt
noOpt = jsNoOpt,
)
BuildOptions(
scalaJsOptions = scalaJsOptions

def absFilePath(pathStr: String): Either[ImportMapNotFound, Path] = {
Try {
os.Path(pathStr)
}.orElse(
Try{
os.Path(pathStr, base = os.pwd)
}
).toEither.fold(ex =>
Left(ImportMapNotFound(s"""Invalid path to EsImportMap. Please check your "using jsEsModuleImportMap xxxx" directive. Does this file exist $pathStr ?""", ex)),
path =>
os.isFile(path) && os.exists(path) match {
case false => Left(ImportMapNotFound(s"""Invalid path to EsImportMap. Please check your "using jsEsModuleImportMap xxxx" directive. Does this file exist $pathStr ?""", null))
case true => Right(path)
}
)
}
val jsImportMapAsPath = jsEsModuleImportMap.map(absFilePath).sequence
jsImportMapAsPath.map( _ match
case None => BuildOptions(scalaJsOptions = scalaJsOptions)
case Some(importmap) =>
BuildOptions(
scalaJsOptions = scalaJsOptions.copy(remapEsModuleImportMap = Some(importmap))
)
)
}
}

class ImportMapNotFound(message: String, cause: Throwable) extends BuildException(message, cause = cause)

object ScalaJs {
val handler: DirectiveHandler[ScalaJs] = DirectiveHandler.derive
}
}
Expand Up @@ -291,6 +291,152 @@ trait RunScalaJsTestDefinitions { _: RunTestDefinitions =>
}
}

test("remap imports directive") {
val importmapFile = "importmap.json"
val outDir = "out"
val fileName = os.rel / "run.scala"

val inputs = TestInputs(
fileName ->
s"""//> using jsEsModuleImportMap $importmapFile
| //> using jsModuleKind es
| //> using jsMode fastLinkJS
| //> using platform js
|
|import scala.scalajs.js
|import scala.scalajs.js.annotation.JSImport
|import scala.scalajs.js.typedarray.Float64Array
|
|object Foo {
| def main(args: Array[String]): Unit = {
| println(Array(-10.0, 10.0, 10).mkString(", "))
| println(linspace(0, 10, 10).mkString(", "))
| }
|}
|
|@js.native
|@JSImport("@stdlib/linspace", JSImport.Default)
|object linspace extends js.Object {
| def apply(start: Double, stop: Double, num: Int): Float64Array = js.native
|}""".stripMargin,
os.rel / importmapFile -> """{"imports": {"@stdlib/linspace": "https://cdn.skypack.dev/@stdlib/linspace"}}""".stripMargin
)
inputs.fromRoot { root =>
val absOutDir = root / outDir
val outFile = absOutDir / "main.js"
os.makeDir.all(absOutDir)
os.proc(
TestUtil.cli,
"--power",
"package",
fileName,
"--js",
"--js-module-kind", "ESModule",
"--js-cli-version", "1.15.0.1",
"-o", outFile,
"-f"
).call(cwd = root).out.trim()
expect(os.read(outFile).contains("https://cdn.skypack.dev/@stdlib/linspace"))
}
}

test("remap imports directive error") {
val fileName = os.rel / "run.scala"
val notexist = "I_DONT_EXIST.json"
val inputs = TestInputs(
fileName ->
s"""//> using jsEsModuleImportMap $notexist
| //> using jsModuleKind es
| //> using jsMode fastLinkJS
| //> using platform js
|
|import scala.scalajs.js
|import scala.scalajs.js.annotation.JSImport
|import scala.scalajs.js.typedarray.Float64Array
|
|object Foo {
| def main(args: Array[String]): Unit = {
| println(Array(-10.0, 10.0, 10).mkString(", "))
| println(linspace(0, 10, 10).mkString(", "))
| }
|}
|
|@js.native
|@JSImport("@stdlib/linspace", JSImport.Default)
|object linspace extends js.Object {
| def apply(start: Double, stop: Double, num: Int): Float64Array = js.native
|}""".stripMargin
)
inputs.fromRoot { root =>
val absOutDir = root / "outDir"
val outFile = absOutDir / "main.js"
os.makeDir.all(absOutDir)
val result = os.proc(
TestUtil.cli,
"--power",
"package",
fileName,
"--js",
"--js-module-kind", "ESModule",
"--js-cli-version", "1.15.0.1",
"-o", outFile,
"-f"
).call(cwd = root,check = false, mergeErrIntoOut = true).out.trim()
expect(result.contains(notexist))
expect(result.contains("Invalid path to EsImportMap."))
}
}

test("remap imports cmd") {
val importmapFile = "importmap.json"
val outDir = "out"
val fileName = os.rel / "run.scala"

val inputs = TestInputs(
fileName ->
s"""
| //> using jsModuleKind es
| //> using jsMode fastLinkJS
| //> using platform js
|
|import scala.scalajs.js
|import scala.scalajs.js.annotation.JSImport
|import scala.scalajs.js.typedarray.Float64Array
|
|object Foo {
| def main(args: Array[String]): Unit = {
| println(Array(-10.0, 10.0, 10).mkString(", "))
| println(linspace(0, 10, 10).mkString(", "))
| }
|}
|
|@js.native
|@JSImport("@stdlib/linspace", JSImport.Default)
|object linspace extends js.Object {
| def apply(start: Double, stop: Double, num: Int): Float64Array = js.native
|}""".stripMargin,
os.rel / importmapFile -> """{"imports": {"@stdlib/linspace": "https://cdn.skypack.dev/@stdlib/linspace"}}""".stripMargin
)
inputs.fromRoot { root =>
val absOutDir = root / outDir
val outFile = absOutDir / "main.js"
os.makeDir.all(absOutDir)
os.proc(
TestUtil.cli,
"--power",
"package",
fileName,
"--js",
"--js-module-kind", "ESModule",
"--js-cli-version", "1.15.0.1",
"-o", outFile,
"-f",
"--js-es-module-import-map", importmapFile
).call(cwd = root, stdout = os.Inherit).out.trim()
expect(os.read(outFile).contains("https://cdn.skypack.dev/@stdlib/linspace"))
}
}

test("js defaults & toolkit default") {
val msg = "Hello"
TestInputs(
Expand Down
Expand Up @@ -10,7 +10,8 @@ final case class ScalaJsLinkerConfig(
esFeatures: ScalaJsLinkerConfig.ESFeatures = ScalaJsLinkerConfig.ESFeatures(),
jsHeader: Option[String] = None,
prettyPrint: Boolean = false,
relativizeSourceMapBase: Option[String] = None
relativizeSourceMapBase: Option[String] = None,
remapEsModuleImportMap: Option[os.Path] = None,
) {
def linkerCliArgs: Seq[String] = {
val moduleKindArgs = Seq("--moduleKind", moduleKind)
Expand All @@ -30,6 +31,8 @@ final case class ScalaJsLinkerConfig(
if (prettyPrint) Seq("--prettyPrint")
else Nil
val jsHeaderArg = if (jsHeader.nonEmpty) Seq("--jsHeader", jsHeader.getOrElse("")) else Nil
val jsEsModuleImportMap = if(remapEsModuleImportMap.nonEmpty) Seq("--importmap", remapEsModuleImportMap.getOrElse(os.pwd / "importmap.json").toString) else Nil

val configArgs = Seq[os.Shellable](
moduleKindArgs,
moduleSplitStyleArgs,
Expand All @@ -39,7 +42,8 @@ final case class ScalaJsLinkerConfig(
sourceMapArgs,
relativizeSourceMapBaseArgs,
jsHeaderArg,
prettyPrintArgs
prettyPrintArgs,
jsEsModuleImportMap
)

configArgs.flatMap(_.value)
Expand Down
Expand Up @@ -16,6 +16,7 @@ final case class ScalaJsOptions(
checkIr: Option[Boolean] = None,
emitSourceMaps: Boolean = false,
sourceMapsDest: Option[os.Path] = None,
remapEsModuleImportMap: Option[os.Path] = None,
dom: Option[Boolean] = None,
header: Option[String] = None,
allowBigIntsForLongs: Option[Boolean] = None,
Expand Down Expand Up @@ -148,7 +149,8 @@ final case class ScalaJsOptions(
moduleSplitStyle = moduleSplitStyle(logger),
smallModuleForPackage = smallModuleForPackage,
esFeatures = esFeatures,
jsHeader = header
jsHeader = header,
remapEsModuleImportMap = remapEsModuleImportMap
)
}
}
Expand Down
4 changes: 4 additions & 0 deletions website/docs/reference/cli-options.md
Expand Up @@ -1276,6 +1276,10 @@ Emit source maps

Set the destination path of source maps

### `--js-es-module-import-map`

A file relative to the root directory containing import maps for ES module imports

### `--js-dom`

Enable jsdom
Expand Down

0 comments on commit feea21d

Please sign in to comment.