Skip to content

Commit ade123e

Browse files
authored
Support remapping imports at link time (#47)
* Maps work - struggling with the facade, though * success! * add back previuos tests * Dont fold over ir files
1 parent ad7c968 commit ade123e

File tree

5 files changed

+202
-10
lines changed

5 files changed

+202
-10
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
/.bsp/
22
out/
33
.idea/
4+
.metals
5+
.vscode
6+
.bloop

build.sc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ trait Cli extends ScalaModule with ScalaJsCliPublishModule {
2525
def artifactName = "scalajs" + super.artifactName()
2626
def ivyDeps = super.ivyDeps() ++ Seq(
2727
ivy"org.scala-js::scalajs-linker:$scalaJsVersion",
28-
ivy"com.github.scopt::scopt:4.1.0"
28+
ivy"com.github.scopt::scopt:4.1.0",
29+
ivy"com.lihaoyi::os-lib:0.9.2",
30+
ivy"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-core:2.13.5.2", // This is the java8 version of jsoniter, according to scala-cli build
31+
ivy"com.github.plokhotnyuk.jsoniter-scala::jsoniter-scala-macros:2.13.5.2", // This is the java8 version of jsoniter, according to scala-cli build
32+
ivy"com.armanbilge::scalajs-importmap:0.1.1"
2933
)
3034
def mainClass = Some("org.scalajs.cli.Scalajsld")
3135

cli/src/org/scalajs/cli/Scalajsld.scala

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ import java.net.URI
2626
import java.nio.file.Path
2727
import java.lang.NoClassDefFoundError
2828
import org.scalajs.cli.internal.{EsVersionParser, ModuleSplitStyleParser}
29+
import org.scalajs.cli.internal.ImportMapJsonIr.ImportMap
30+
31+
import com.github.plokhotnyuk.jsoniter_scala.core._
32+
import org.scalajs.cli.internal.ImportMapJsonIr
2933

3034
object Scalajsld {
3135

@@ -48,7 +52,8 @@ object Scalajsld {
4852
checkIR: Boolean = false,
4953
stdLib: Seq[File] = Nil,
5054
jsHeader: String = "",
51-
logLevel: Level = Level.Info
55+
logLevel: Level = Level.Info,
56+
importMap: Option[File] = None
5257
)
5358

5459
private def moduleInitializer(
@@ -134,6 +139,12 @@ object Scalajsld {
134139
.valueName("<dir>")
135140
.action { (x, c) => c.copy(outputDir = Some(x)) }
136141
.text("Output directory of linker (required)")
142+
opt[File]("importmap")
143+
.valueName("<path/to/file>.json")
144+
.action { (x, c) => c.copy(importMap = Some(x)) }
145+
.text("""Absolute path to an existing json file, e.g. importmap.json the contents of which respect
146+
| https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap#import_map_json_representation
147+
| e.g. {"imports": {"square": "./module/shapes/square.js"},"scopes": {"/modules/customshapes/": {"square": "https://example.com/modules/shapes/square.js"}}}""")
137148
opt[Unit]('f', "fastOpt")
138149
.action { (_, c) =>
139150
c.copy(noOpt = false, fullOpt = false)
@@ -247,10 +258,26 @@ object Scalajsld {
247258
)
248259
}
249260

250-
if (c.outputDir.isDefined == c.output.isDefined)
261+
val outputCheck = if (c.outputDir.isDefined == c.output.isDefined)
251262
failure("exactly one of --output or --outputDir have to be defined")
252263
else
253264
success
265+
266+
val importMapCheck = c.importMap match {
267+
case None => success
268+
case Some(value) => {
269+
if (!value.exists()) {
270+
failure(s"importmap file at path ${value} does not exist.")
271+
} else {
272+
success
273+
}
274+
}
275+
}
276+
val allValidations = Seq(outputCheck, importMapCheck)
277+
allValidations.forall(_.isRight) match {
278+
case true => success
279+
case false => failure(allValidations.filter(_.isLeft).map(_.left.get).mkString("\n\n"))
280+
}
254281
}
255282

256283
override def showUsageOnError = Some(true)
@@ -291,19 +318,25 @@ object Scalajsld {
291318
val result = PathIRContainer
292319
.fromClasspath(classpath)
293320
.flatMap(containers => cache.cached(containers._1))
294-
.flatMap { irFiles =>
321+
.flatMap { irFiles: Seq[IRFile] =>
322+
323+
val irImportMappedFiles = options.importMap match {
324+
case None => irFiles
325+
case Some(importMap) => ImportMapJsonIr.remapImports(importMap, irFiles)
326+
}
327+
295328
(options.output, options.outputDir) match {
296329
case (Some(jsFile), None) =>
297330
(DeprecatedLinkerAPI: DeprecatedLinkerAPI).link(
298331
linker,
299-
irFiles.toList,
332+
irImportMappedFiles.toList,
300333
moduleInitializers,
301334
jsFile,
302335
logger
303336
)
304337
case (None, Some(outputDir)) =>
305338
linker.link(
306-
irFiles,
339+
irImportMappedFiles,
307340
moduleInitializers,
308341
PathOutputDirectory(outputDir.toPath()),
309342
logger
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package org.scalajs.cli.internal
2+
3+
import com.github.plokhotnyuk.jsoniter_scala.core._
4+
import com.github.plokhotnyuk.jsoniter_scala.macros._
5+
import org.scalajs.linker.interface.IRFile
6+
import java.io.File
7+
import com.armanbilge.sjsimportmap.ImportMappedIRFile
8+
9+
object ImportMapJsonIr {
10+
11+
type Scope = Map[String, String]
12+
13+
final case class ImportMap(
14+
val imports: Map[String, String],
15+
val scopes: Option[Map[String, Scope]]
16+
)
17+
18+
object ImportMap {
19+
implicit val codec: JsonValueCodec[ImportMap] = JsonCodecMaker.make
20+
}
21+
22+
def remapImports(pathToImportPath: File, irFiles: Seq[IRFile]): Seq[IRFile] = {
23+
val path = os.Path(pathToImportPath)
24+
val importMapJson = if(os.exists(path))(
25+
readFromString[ImportMap](os.read(path))
26+
) else {
27+
throw new AssertionError(s"importmap file at path ${path} does not exist.")
28+
}
29+
if (importMapJson.scopes.nonEmpty) {
30+
throw new AssertionError("importmap scopes are not supported.")
31+
}
32+
val importsOnly : Map[String, String] = importMapJson.imports
33+
34+
val remapFct = importsOnly.toSeq.foldLeft((in: String) => in){ case(fct, (s1, s2)) =>
35+
val fct2 : (String => String) = (in => in.replace(s1, s2))
36+
(in => fct(fct2(in)))
37+
}
38+
39+
irFiles.map{ImportMappedIRFile.fromIRFile(_)(remapFct)}
40+
}
41+
}

tests/test/src/org/scalajs/cli/tests/Tests.scala

Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class Tests extends munit.FunSuite {
2424
.out
2525
.trim()
2626

27-
def getScalaJsCompilerPlugin(cwd: os.Path) = os.proc("cs", "fetch", "--intransitive", s"org.scala-js:scalajs-compiler_2.13.6:$scalaJsVersion")
27+
def getScalaJsCompilerPlugin(cwd: os.Path) = os.proc("cs", "fetch", "--intransitive", s"org.scala-js:scalajs-compiler_2.13.12:$scalaJsVersion")
2828
.call(cwd = cwd).out.trim()
2929

3030
test("tests") {
@@ -48,7 +48,7 @@ class Tests extends munit.FunSuite {
4848
os.proc(
4949
"cs",
5050
"launch",
51-
"scalac:2.13.6",
51+
"scalac:2.13.12",
5252
"--",
5353
"-classpath",
5454
scalaJsLibraryCp,
@@ -134,7 +134,7 @@ class Tests extends munit.FunSuite {
134134
os.proc(
135135
"cs",
136136
"launch",
137-
"scalac:2.13.6",
137+
"scalac:2.13.12",
138138
"--",
139139
"-classpath",
140140
scalaJsLibraryCp,
@@ -188,7 +188,7 @@ class Tests extends munit.FunSuite {
188188
os.proc(
189189
"cs",
190190
"launch",
191-
"scalac:2.13.6",
191+
"scalac:2.13.12",
192192
"--",
193193
"-classpath",
194194
scalaJsLibraryCp,
@@ -223,4 +223,115 @@ class Tests extends munit.FunSuite {
223223

224224
assert(runRes.err.trim().contains("UndefinedBehaviorError"))
225225
}
226+
227+
test("import map") {
228+
val dir = os.temp.dir()
229+
os.write(
230+
dir / "foo.scala",
231+
"""
232+
|import scala.scalajs.js
233+
|import scala.scalajs.js.annotation.JSImport
234+
|import scala.scalajs.js.typedarray.Float64Array
235+
|
236+
|object Foo {
237+
| def main(args: Array[String]): Unit = {
238+
| println(linspace(-10.0, 10.0, 10))
239+
| }
240+
|}
241+
|
242+
|@js.native
243+
|@JSImport("@stdlib/linspace", JSImport.Default)
244+
|object linspace extends js.Object {
245+
| def apply(start: Double, stop: Double, num: Int): Float64Array = js.native
246+
|}
247+
|""".stripMargin
248+
)
249+
250+
val scalaJsLibraryCp = getScalaJsLibraryCp(dir)
251+
252+
os.makeDir.all(dir / "bin")
253+
os.proc(
254+
"cs",
255+
"launch",
256+
"scalac:2.13.12",
257+
"--",
258+
"-classpath",
259+
scalaJsLibraryCp,
260+
s"-Xplugin:${getScalaJsCompilerPlugin(dir)}",
261+
"-d",
262+
"bin",
263+
"foo.scala"
264+
).call(cwd = dir, stdin = os.Inherit, stdout = os.Inherit)
265+
266+
val notThereYet = dir / "no-worky.json"
267+
val launcherRes = os.proc(
268+
launcher,
269+
"--stdlib",
270+
scalaJsLibraryCp,
271+
"--fastOpt",
272+
"-s",
273+
"-o",
274+
"test.js",
275+
"-mm",
276+
"Foo.main",
277+
"bin",
278+
"--importmap",
279+
notThereYet
280+
)
281+
.call(cwd = dir, mergeErrIntoOut = true)
282+
283+
assert(launcherRes.exitCode == 0) // as far as I can tell launcher returns code 0 for failed validation?
284+
assert(launcherRes.out.trim().contains(s"importmap file at path ${notThereYet} does not exist"))
285+
286+
os.write(notThereYet, "...")
287+
288+
val failToParse = os.proc(
289+
launcher,
290+
"--stdlib",
291+
scalaJsLibraryCp,
292+
"--fastOpt",
293+
"-s",
294+
"-o",
295+
"test.js",
296+
"-mm",
297+
"Foo.main",
298+
"bin",
299+
"--importmap",
300+
notThereYet
301+
)
302+
.call(cwd = dir, check = false, mergeErrIntoOut = true, stderr = os.Pipe)
303+
304+
assert(failToParse.out.text().contains("com.github.plokhotnyuk.jsoniter_scala.core.JsonReaderException"))
305+
306+
val importmap = dir / "importmap.json"
307+
val substTo = "https://cdn.jsdelivr.net/gh/stdlib-js/array-base-linspace@esm/index.mjs"
308+
os.write(importmap, s"""{ "imports": {"@stdlib/linspace":"$substTo"}}""")
309+
310+
val out = os.makeDir.all(dir / "out")
311+
312+
val worky = os.proc(
313+
launcher,
314+
"--stdlib",
315+
scalaJsLibraryCp,
316+
"--fastOpt",
317+
"-s",
318+
"--outputDir",
319+
"out",
320+
"-mm",
321+
"Foo.main",
322+
"bin",
323+
"--moduleKind",
324+
"ESModule",
325+
"--importmap",
326+
importmap
327+
)
328+
.call(cwd = dir, check = false, mergeErrIntoOut = true, stderr = os.Pipe)
329+
os.write( dir / "out" / "index.html", """<html><head><script type="module" src="main.js"></script></head><body></body></html>""")
330+
331+
// You can serve the HTML file here and check the console output of the index.html file, hosted in a simple webserver to prove the concept
332+
//println(dir)
333+
assert(os.exists(dir / "out" / "main.js"))
334+
val rawJs = os.read.lines(dir / "out" / "main.js")
335+
assert(rawJs(1).contains(substTo))
336+
}
226337
}

0 commit comments

Comments
 (0)