Say I have an assets.js that does a require on local images, fonts, etc. The result of the require is a URL that changes based on webpack settings. At webpack compile-time any broken links break the build.
module.exports = {
imageA: require('./image-a.jpg'),
imageB: require('./image-b.jpg'),
imageC: require('./image-c.jpg'),
};
Now say I want to use it in Scala.JS, the simplest option is:
@JSImport("experiment-webpack/assets", JSImport.Namespace)
@js.native
object Assets extends js.Object {
val imageA: String = js.native
val imageB: String = js.native
val imageC: String = js.native
}
The problem is that this breaks the (webpack) compile-time safety. If I remove imageC from assets.js, everything still appears to work. It's much safer and more correct (including for DCE) to change the facade to be like this:
object Assets {
@JSImport("experiment-webpack/assets", "imageA")
@js.native
private object _imageA extends js.Any
def imageA = _imageA.asInstanceOf[String]
@JSImport("experiment-webpack/assets", "imageB")
@js.native
private object _imageB extends js.Any
def imageB = _imageB.asInstanceOf[String]
@JSImport("experiment-webpack/assets", "imageC")
@js.native
private object _imageC extends js.Any
def imageC = _imageC.asInstanceOf[String]
}
This way, 1) anything unused is dropped from the webpack output, and 2) any incorrect links or discrepancies between JS & Scala become errors during webpack compilation.
The problem is hopefully self-evident: the verbosity. That's a lot of boilerplate. The annotations are fine for larger facades but relatively huge in proportion to small underlying values, like a simple String.
Could we not add a syntax similar to:
object Assets {
def imageA = js.require[String]("experiment-webpack/assets/imageA")
def imageB = js.require[String]("experiment-webpack/assets/imageB")
def imageC = js.require[String]("experiment-webpack/assets/imageC")
}
Erm, I meant:
object Assets {
def imageA = js.require[String]("experiment-webpack/assets", "imageA")
def imageB = js.require[String]("experiment-webpack/assets", "imageB")
def imageC = js.require[String]("experiment-webpack/assets", "imageC")
}
No, we can't do that, because that would not be implementable once we generate ECMAScript modules. The latter do not support dynamic imports, so it's impossible to support a dynamic require call inside executable code. Imports must be declarative.
However, I suspect that once we do emit ECMAScript modules, your compile-time safety problem will be automatically addressed even in the very first snippet, precisely because ECMAScript module imports are declarative.
Besides, did you actually test that your verbose option behaves better than the naive one? Unless I'm mistaken, both should produce exactly the same .js code.
I re-tested this to paste snippets of what I wanted to demonstrate only to find that things weren't going as I expected. For some reason, (given the above assets.js), both of these snippets pass webpack compilation and result in undefined at runtime:
object Assets {
@JSImport("experiment-webpack/assets", "i_dont_exist.jpg")
@js.native
private object _blah extends js.Any
def i_dont_exist = _blah.asInstanceOf[String]
}
@JSImport("experiment-webpack/assets", JSImport.Namespace)
@js.native
object Assets extends js.Object {
val i_dont_exist : String = js.native
}
I tested this before and could've sworn it worked. Anyway, trying again it turns out that referencing the resource directly from SJS like this works:
object Assets {
@JSImport("experiment-webpack/imageA.jpg", JSImport.Namespace)
@js.native
private object _imageA extends js.Any
def imageA = _imageA.asInstanceOf[String]
}
and this fails at webpack compilation time as desired
object Assets {
@JSImport("experiment-webpack/i_dont_exist.jpg", JSImport.Namespace)
@js.native
private object _i_dont_exist extends js.Any
def i_dont_exist = _i_dont_exist.asInstanceOf[String]
}
Ok good. Now moving on:
Firstly I don't know JS as well as you so I'm confused when you say "once we generate ECMAScript modules". So what we're generating now is CommonJS modules but ECMAScript modules (ES6 modules?) will provide more checking? If so, is that coming to SJS soon?
Now that I've retested and found there's only one working way, the verbose way, that answers "did you actually test that your verbose option behaves better than the naive one".
I get what you're saying about dynamic require not being supported but that's not what I'm asking for. I'm hoping for a more concise way of doing that verbose thing that I did. If said solution rejects dynamic values at compile-time that's fine. In fact, I could even implement what I'm asking for myself as a macro in userspace that just expands to the verbose version. Something like this simplified pseudo code:
def require[T](c)(module: c.Expr[String]) = {
val moduleName = c.xxx match {
case Constant(s: String) => s
case _ => fail("dynamic require not allowed")
}
val name = c match { case q"val (n) = ... }
// Then something like:
q"""
lazy val $name: T = {
@JSImport("$moduleName", JSImport.Namespace)
@js.native
object x extends js.Any
x.asInstanceOf[$T]
}
"""
// or maybe
q"""
object $name {
@JSImport("$moduleName", JSImport.Namespace)
@js.native
private[this] object _js extends js.Any
def value = __js.asInstanceOf[$T]
}
"""
}
As modules and webpack become more widely used this becomes more relevant to general SJS users so I thought more concise language support might be nice.
So what we're generating now is CommonJS modules but ECMAScript modules (ES6 modules?) will provide more checking?
Yes, that's right.
If so, is that coming to SJS soon?
Define "soon" ... More seriously, in 1.0.0 or in a release soon after 1.0.0. So 3-4 months from now by my estimations. Unfortunately, it is impossible to implement it in 0.6.x due to an obscure internal feature that we cannot make work in ES 6 modules.
In fact, I could even implement what I'm asking for myself as a macro in userspace that just expands to the verbose version.
I think that would be nice! Experimentation with macros is good. Especially in this case, I think even such a feature implemented in Core would do the same as the macro you suggest, only it would be more complicated (because
Perhaps a macro annotation would be even nicer. You could have:
@JSImportVal("experiment-webpack/imageA.jpg")
val imageA: String = js.native
and have the macro annotation expand it into
@js.native
@JSImport("experiment-webpack/imageA.jpg", JSImport.Namespace)
private object __imageA extends js.Any
val imageA: String = __imageA.asInstanceOf[String]
With #2800, this will be solved, as you will be able to define in user-space the following:
@js.native
@JSGlobalScope
object AssetLoader extends js.Any {
def require[A](path: String): A = js.native
}
And then basically write your very first proposed snippet, i.e.,
def imageA: String = AssetLoader.require[String]("experiment-webpack/assets/imageA")
Fixed by 0b776fa0037e720d89bd59c0cfa13a4f0e486a96, as explained in my previous comment.
Most helpful comment
With #2800, this will be solved, as you will be able to define in user-space the following:
And then basically write your very first proposed snippet, i.e.,