Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to remap EsModule imports at link time #2737

Merged
merged 2 commits into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@ out/
dest/
target/

*/scoverage.coverage

# ignore vim backup files
*.sw[op]

Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package scala.build.preprocessing.directives

import os.Path

import scala.build.EitherCps.{either, value}
import scala.build.Ops.EitherOptOps
import scala.build.directives.*
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

@DirectiveGroupName("Scala.js options")
@DirectiveExamples("//> using jsModuleKind common")
Expand Down Expand Up @@ -39,6 +43,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 +57,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 +68,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 +83,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
}
}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading
Loading