Skip to content

Commit

Permalink
Fix scala-js#2797: Add an optional global fallback to @JSImport.
Browse files Browse the repository at this point in the history
If the codebase refers to an `@JSImport` entity, such as

    @js.native
    @jsimport("foo.js", "Bar")
    class Bar extends js.Object

the codebase must be linked with `CommonJSModule` (or, in the
future, other `ModuleKind`s supporting modules). Linking without
module support causes a linking error.

This causes facades for JS libraries supporting both module-style
and script-style to be faced with an early choice:

* either support linking with modules and use `@JSImport`, or
* support linking without module support, and use `@JSGlobal` with
  whatever global names the JS library uses.

It is however impossible to write a facade library that supports
both use cases for their end users.

This commit addresses this issue by adding an optional "global
fallback" to `@JSImport`. We can now define a facade as follows:

    @js.native
    @jsimport("foo.js", "Bar", globalFallback = "Foo.Bar")
    class Bar extends js.Object

That facade will successfully link both with and without module
support. With module support, it links as if declared like in the
first snippet. Without module support, it links as if declared as

    @js.native
    @jsglobal("Foo.Bar")
    class Bar extends js.Object

Using this global fallback, a facade library can cater both for
users who want to take advantages of modules, and users who want to
stick to the script style.

---

Implementation-wise, this is quite easy to implement. We simply
add a new `JSNativeLoadSpec.ImportWithGlobalFallback`. It wraps
both a `JSNativeLoadSpec.Import` and `Global`. That JS native load
spec goes through the entire pipeline and reaches the emitter as
is. The emitter decides which one to use depending on the
`moduleKind`.
  • Loading branch information
sjrd committed May 20, 2017
1 parent 6047919 commit 6862105
Show file tree
Hide file tree
Showing 12 changed files with 623 additions and 78 deletions.
Expand Up @@ -708,6 +708,8 @@ abstract class PrepJSInterop extends plugins.PluginComponent

private def checkAndComputeJSNativeLoadSpecOf(pos: Position,
sym: Symbol): JSNativeLoadSpec = {
import JSNativeLoadSpec._

if (enclosingOwner is OwnerKind.JSNativeMod) {
for {
annot <- sym.annotations
Expand All @@ -732,10 +734,15 @@ abstract class PrepJSInterop extends plugins.PluginComponent

val ownerLoadSpec = jsInterop.jsNativeLoadSpecOf(sym.owner)
ownerLoadSpec match {
case JSNativeLoadSpec.Global(path) =>
JSNativeLoadSpec.Global(path :+ jsName)
case JSNativeLoadSpec.Import(module, path) =>
JSNativeLoadSpec.Import(module, path :+ jsName)
case Global(path) =>
Global(path :+ jsName)
case Import(module, path) =>
Import(module, path :+ jsName)
case ImportWithGlobalFallback(
Import(module, modulePath), Global(globalPath)) =>
ImportWithGlobalFallback(
Import(module, modulePath :+ jsName),
Global(globalPath :+ jsName))
}
} else {
def parsePath(pathName: String): List[String] =
Expand Down Expand Up @@ -782,7 +789,13 @@ abstract class PrepJSInterop extends plugins.PluginComponent
"" // do not care because it does not compile anyway
}
val path = annot.stringArg(1).fold[List[String]](Nil)(parsePath)
JSNativeLoadSpec.Import(module, path)
val importSpec = Import(module, path)
annot.stringArg(2).fold[JSNativeLoadSpec] {
importSpec
} { globalPathName =>
val globalSpec = Global(parsePath(globalPathName))
ImportWithGlobalFallback(importSpec, globalSpec)
}

case Some(annot) if annot.symbol == JSNameAnnotation =>
if (!scalaJSOpts.suppressMissingJSGlobalDeprecations) {
Expand Down Expand Up @@ -1280,8 +1293,8 @@ abstract class PrepJSInterop extends plugins.PluginComponent
for {
annot <- sym.getAnnotation(JSImportAnnotation)
} {
assert(annot.args.size == 2,
s"@JSImport annotation $annot does not have exactly 2 arguments")
assert(annot.args.size == 2 || annot.args.size == 3,
s"@JSImport annotation $annot does not have exactly 2 or 3 arguments")

val firstArgIsValid = annot.stringArg(0).isDefined
if (!firstArgIsValid) {
Expand All @@ -1298,6 +1311,13 @@ abstract class PrepJSInterop extends plugins.PluginComponent
"The second argument to @JSImport must be literal string or the " +
"JSImport.Namespace object.")
}

val thirdArgIsValid = annot.args.size < 3 || annot.stringArg(2).isDefined
if (!thirdArgIsValid) {
reporter.error(annot.args(2).pos,
"The third argument to @JSImport, when present, must be a " +
"literal string.")
}
}
}

Expand Down

0 comments on commit 6862105

Please sign in to comment.