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

Facades should be able to support all values of ModuleKind #2797

Closed
japgolly opened this issue Mar 9, 2017 · 25 comments
Closed

Facades should be able to support all values of ModuleKind #2797

japgolly opened this issue Mar 9, 2017 · 25 comments
Assignees
Labels
enhancement Feature request (that does not concern language semantics, see "language") language Affects language semantics.
Milestone

Comments

@japgolly
Copy link
Contributor

japgolly commented Mar 9, 2017

Library authors should be able to write facades in such a way that downstream users can use any scalaJSModuleKind value they want/need.

my vision for the final state is that an @js.native class or object must have exactly 1 annotation among @jsglobal, @jsimport and @JSLoadVia.

Not realising the above vision wasn't being realised just yet, I became very fearful because such a direction would leave me with only 3 unhappy choices for scalajs-react:

  1. Put @JSGlobal on my facades and don't allow users to use CommonJSModule.
  2. Put @JSImport on my facades and force users to use CommonJSModule and probably force them to use scalajs-bundler which will in turn force them to only import deps a certain way and, only output one huge single JS file.
  3. Double build and double publish all of my library artifacts so that users can choose whether they want to use modules / script.

I think it'd be better to allow us to annotate our facades with both @JsGlobal and @JsImport, then downstream at link-time have Scala.JS choose one or the other depending on users' ModuleKind setting.

@julienrf
Copy link
Contributor

julienrf commented Mar 9, 2017

Hi, I’m not sure it would be easy to adapt to scalajs-react but a general pattern to suport both is the following:

  1. define the facade trait
package myfacade

@js.native
trait MyFacade extends js.Object {
  …
}
  1. define a global facade object:
package myfacade
package global

@JSGlobal
object MyFacade extends MyFacade
  1. define a CommonJS module facade
package myfacade
package module

@JSImport("my-facade", …)
object MyFacade extends MyFacade

Then, at use site, users can pick either myfacade.global.MyFacade or myfacade.module.MyFacade.

@japgolly
Copy link
Contributor Author

japgolly commented Mar 9, 2017

@julienrf Unfortunately that approach only works when downstream users are using raw facades directly. In order to provide a nicer dev experience and/or build Scala-based features on top of the facades, the library interfaces with the facades directly and so, has to make a choice about which to use (I mean either myfacade.global.MyFacade or myfacade.module.MyFacade in your example).

This below much more accommodating. What we you think about a scheme like this?

// Great! This will work for both ModuleKind.NoModule and ModuleKind.CommonJSModule!
@JSGlobal
@JSImport("my-facade", …)
@js.native
object MyFacade


// This will only work for the script style.
// Maybe compiler could warn that ModuleKind.CommonJSModule won't be supported if a user uses it.
@JSGlobal
@js.native
object MyFacade


// This will only work for the module style.
// Maybe compiler could warn that ModuleKind.NoModule won't be supported if a user uses it.
@JSImport("my-facade", …)
@js.native
object MyFacade


// This could fail compilation with an error stating that at least 1 annotation is required
@js.native
object MyFacade

@sjrd
Copy link
Member

sjrd commented Mar 9, 2017

Note that even with CommonJSModule, a number of things still must be @JSGlobal, e.g., DOM APIs. Saying that @JSGlobal only applies to script mode is not a valid premise on which we can build something.

@gzm0
Copy link
Contributor

gzm0 commented Mar 9, 2017

Why not simply allow @JSGlobal as a fallback when modules are not available? This is what most JS libraries currently do (and essentially just a formalization of the hacky commonJSName we have in jsDependencies). So:

// Import in module mode, global otherwise.
@JSGlobal
@JSImport("my-facade", …)
@js.native
object MyFacade

// Always global.
@JSGlobal
@js.native
object MyFacade

// Module only.
@JSImport("my-facade", …)
@js.native
object MyFacade

// Depends on method if it works :)
@JSLoadVia(...)
@js.native
object MyFacade

// All illegal

@js.native
object MyFacade

@JSLoadVia(...)
@JSGlobal
@js.native
object MyFacade

@JSLoadVia(...)
@JSImport(...)
@js.native
object MyFacade

@japgolly
Copy link
Contributor Author

I agree with what @gzm0 said. But if @sjrd is still concerned at potential ambiguity of @JSGlobal depending on where it's applied (which is a fair point), another idea could be to introduce a new annotation like @JSGlobalInNoModuleMode (or obviously something shorter) which would be used in place of @JSGlobal in my and @gzm0's snippets above. Then @JSGlobal could be retained for the other cases that are relevant regardless of the ModuleKind setting.

@sjrd
Copy link
Member

sjrd commented Mar 10, 2017

@gzm0's proposal looks reasonable.

Besides the technical aspects, I'd like to discuss the philosophy behind such a feature. My impression, AFAICT, is that the JavaScript ecosystem is transitioning to a module-first world, where "scripts" are not a thing anymore. React itself seems to basically require webpack to be usable.

Should we introduce a new feature now in Scala.js that supports the "old" way of doing things in JavaScript? As libraries in the ecosystem move to being defined only as modules, this feature will become obsolete, but we'll have to continue to support it forever because it's a language feature.

It boils down to a few questions:

  • Do we expect the JavaScript ecosystem to keep supporting this dual module/script approach?
  • Does the benefit for Scala.js users outweigh the added complexity to the language (in terms of spec, not even implementation)?

The second bullet is particularly relevant in the context of Scala, which is often criticized for its complexity.

@gzm0
Copy link
Contributor

gzm0 commented Mar 10, 2017

An alternative would of course to do this in terms of @JSLoadVia in user space. We have one tiny (but relevant) piece missing: Dynamic imports (to the extent allowed by ES6).

Then we could just have a library that supports the cross loading and fade this library out without compromising the language.

I'm not 100% sure such a thing is implementable with reasonable usage sites though.

@sjrd
Copy link
Member

sjrd commented Mar 10, 2017

Also, we should explore non-language solutions to the problem. Could we have a linker configuration that "maps" module names to global variables? For example, I could say: "map the "react.js" module to the global variable React`". The linker would turn what should have been

var $i_React = require("react.js")

into

var $i_React = $g.React

This would not affect the language surface, which shifts the balance utility/complexity. It could also be used on libraries whose authors were not interested in supporting the double module/script scenario. Which is quite important, as it relieves authors from testing in two entirely different environments.

@sjrd
Copy link
Member

sjrd commented Mar 10, 2017

Dynamic imports (to the extent allowed by ES6).

They're not supported at all. There's a proposal for dynamic imports but it's a long way away. In any case, it shouldn't be the default, as it does not allow as much dce between modules.

@japgolly
Copy link
Contributor Author

I'd like to discuss the philosophy behind such a feature. My impression, AFAICT, is that the JavaScript ecosystem is transitioning to a module-first world, where "scripts" are not a thing anymore.

Should we introduce a new feature now in Scala.js that supports the "old" way of doing things in JavaScript? As libraries in the ecosystem move to being defined only as modules, this feature will become obsolete, but we'll have to continue to support it forever because it's a language feature.

I'd feel much more solidarity to you in that regard if browsers also supported JS modules. They don't though. They only support scripts. I don't even see that changing, I see it going to scripts / webassembly, not scripts / JS modules.

I totally take your point that a JS dev community are using modules more and more. On the other hand, I've never in my life seen a more fickle community that the JS community. Every year the community as a whole has some new processes, tools and direction that spreads like wildfire. It's really cool that they keep experimenting and improving the status quo - I like that aspect - but I'd be very wary in making a long-term decision upon whatever the current trend of the year is.

Coming back to technical debate, I think it's fantastic to support modules but I'm wary about reducing support for the "script" option. Browsers are never going to stop supporting script-style JS.

@sjrd
Copy link
Member

sjrd commented Mar 11, 2017

They only support scripts. I don't even see that changing, I see it going to scripts / webassembly, not scripts / JS modules.

They will. The necessary works have been under discussion for some time in the Web committees. It's a matter of details, and time.

Nevertheless, my point is not really on direct support by the browser, but rather that every non-trivial browser app is developed in terms of modules, and packed into scripts in a build step.

Anyway, what about the idea I exposed of a fully link-time based solution?

@ramnivas
Copy link

every non-trivial browser app is developed in terms of modules, and packed into scripts in a build step.

True, but I don't think the modules-first approach is suitable for scala.js projects currently. I added some of my concerns in #2681, so resolving that is going be a prerequisite.

@japgolly
Copy link
Contributor Author

First of all please don't ship 0.6.15 without a solution to this issue. Even if there's just a temporary resolution that will be revisited later, I really don't wan't to push unnecessary build constraints upon my users. They're quite significant.

Nevertheless, my point is not really on direct support by the browser, but rather that every non-trivial browser app is developed in terms of modules, and packed into scripts in a build step.

Should that affect Scala.JS?

Anyway, what about the idea I exposed of a fully link-time based solution?

It will probably work but it might annoy a lot of people to have to copy-paste custom stuff into their SBT (and hope it doesn't go out of date). Especially when the alternative just allowing some annotations in my library, something I'm willing to do. Really I don't see the harm in allowing multiple annotations. It's completely at odds with allowing users to select ModuleKind.

@gzm0
Copy link
Contributor

gzm0 commented Mar 13, 2017

First of all please don't ship 0.6.15 without a solution to this issue.

I don't fully understand this. It's not like 0.6.14 had a solution and we are removing it (unless I'm missing something here). All we are doing is make some of the naming more sane. Feature wise, nothing changes.

Also, I would like to note that 0.6.15 is already behind schedule, so personally I'm not very fond of delaying it for a feature we don't even know how exactly it is supposed to work yet.

@japgolly
Copy link
Contributor Author

It's not like 0.6.14 had a solution and we are removing it

As far as I understand (I haven't tried this yet) in 0.6.14 I can have an object with @js.native, @JSImport and optional @JSName. When a downstream user uses ModuleKind.CommonJS, @JSImport is used; when ModuleKind.NoModule then @JSName or the calculable name is used. Thus in 0.6.14 its possible for a Scala.JS library to be used with either moduleKind setting. Where as in 0.6.15 it seems to be the case that @JSName becomes @JSGlobal in this context and is mutually exclusive with @JSImport forcing me to either not support scalajs-bundler or force downstream users to use it. But again: it's all just discussion for me at this point because I haven't tried it out yet, am I misunderstanding something?

@sjrd
Copy link
Member

sjrd commented Mar 13, 2017

As far as I understand (I haven't tried this yet) in 0.6.14 I can have an object with @js.native, @jsimport and optional @JsName.

No, in 0.6.14 it is not allowed to have both @JSImport and @JSName. And when there is @JSImport but no @JSName, the import is always used (there's a linking error if you try to emit the codebase with NoModule).

Even if there's just a temporary resolution that will be revisited later

Adding something that will be revisited later is not something we can do in Scala.js at this point. Every new feature has to keep working and be supported for a very long time. It is much safer not to add something, so that we have the time to think it through, and eventually implement the right thing when we know what to do.

@japgolly
Copy link
Contributor Author

Oh. That's unfortunate.
In that case, is there any kind of advice or trick that you can suggest for library authors that:

  1. Have libraries where facades aren't used directly by users. And:
  2. Would like their downstream users to be able to choose either moduleKind setting.

@sjrd
Copy link
Member

sjrd commented Mar 14, 2017

Unfortunately not. When we designed @JSImport, the assumption was that libraries would progressively migrate to CommonJS modules (+ scalajs-bundler), and that a library would never have to support both styles.

I think the only reasonable thing to do at this point is to keep using the global variant, and tell scalajs-bundler users to configure it so that the relevant JS library is made available on the global scope.

The idea of my link-time solution is that we can also provide a similar solution for the reverse direction: when the library was designed for modules, but a user wants to use it in script mode. That direction would be both more powerful (dce is still possible for module users) and easier to set up for user (a single line of sbt config).

@japgolly
Copy link
Contributor Author

Thanks for the advice. After a day of learning webpack and experimenting with it in a SJS context, I'm going to take your suggestion pretty much verbatim. I'll keep scalajs-react as it is for now and probably when SJS 1.0 comes out I'll switch the library over to module-style.

For one to use scalajs-react with bundler, the following webpack config is required:

module: {
  rules: [{
      test: require.resolve('react'),
      use: [{loader: 'expose-loader', options: 'React'}]
    }, {
      test: require.resolve('react-dom'),
      use: [{loader: 'expose-loader', options: 'ReactDOM'}]
    }],
  },
  entry: ['react', 'react-dom'],
}

@gzm0
Copy link
Contributor

gzm0 commented Mar 17, 2017

Just my two cents: A linking time solution is essentially another version of commonJSName. IMO it puts the burden of tracking the names a JS library uses on the wrong side: A facade is nothing more than an elaborate description of how a JS library behaves. Therefore I do not see why this is wouldn't be the right place to define how the library behaves in module / non module contexts.

Pushing this out to a linking time solution puts a partial burden of figuring out how the library behaves on the library user. Now all facade writers have to publish a readme with a configuration that everybody has to copy verbatim if they want to use the library in "the other" context.

So if we introduce a feature like this, I think we really should make sure that the relevant definitions are in the right place.

@japgolly
Copy link
Contributor Author

Strongly agree with @gzm0 here.

Also, where there's a will there's a way 😆 In order to reduce burden imposed on my downstream users I've just added this: japgolly/scalajs-react@0d2afd2 They'll still need to add the expose-loader npm package and manually reference the imports they want but it's the nicest workaround I know so far. My point in demonstrating this is that:

  1. Clearly the community would be better if this kind of stuff was done once in upsteram-libraries rather than a culture of having to copy-paste the contents of above patch, forever checking for changes in the instructions and manually updating.
  2. Via the means in above patch, it's already possible to store import info in a library that uses @JSGlobal anyway so we may as well just avoid the hack and provide proper support.
  3. Workarounds like above and all other suggestions, require users to know which imports their app uses and manually specify it. If some code somewhere in their app changes and an import is no longer required, they might never know and keep importing it anyway, preventing DCE. Not really a problem for me/React but for smaller modules it will be more problematic. (Although admittedly, flipping the default from script- to module-style invalidates this point.)

@sjrd sjrd added the enhancement Feature request (that does not concern language semantics, see "language") label Mar 20, 2017
sjrd added a commit to sjrd/scala-js that referenced this issue May 20, 2017
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`.
@sjrd
Copy link
Member

sjrd commented May 20, 2017

After thinking more about this, I have decided that pragmatism needs to win here, and that this deserves to be implemented in the language. I chose a slightly different syntax than suggested above, however. Instead of the suggestion

@js.native
@JSImport("some-module.js", "ModuleMember")
@JSGlobal("TheGlobalName")
object MyFacade extends js.Object

the syntax would be

@js.native
@JSImport("some-module.js", "ModuleMember", globalFallback = "TheGlobalName")
object MyFacade extends js.Object

This is makes much clearer which one of "import" and "global" wins, when both are possible. Because, as stated above, even when linking with modules, @JSGlobals are still the right thing to use for a number of facades.

It also has the benefit in terms of specification and implementation that each facade still needs to have exactly 1 "load spec" annotation, i.e., 1 annotation among @JSGlobal, @JSImport and @JSGlobalScope (and in the future @JSLoadVia).

PR at #2957.

sjrd added a commit to sjrd/scala-js that referenced this issue May 20, 2017
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`.
sjrd added a commit to sjrd/scala-js that referenced this issue May 20, 2017
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`.
sjrd added a commit to sjrd/scala-js that referenced this issue May 20, 2017
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`.
sjrd added a commit that referenced this issue May 21, 2017
Fix #2797: Add an optional global fallback to `@JSImport`.
@sjrd sjrd self-assigned this May 21, 2017
@sjrd sjrd added the language Affects language semantics. label May 21, 2017
@sjrd sjrd added this to the v0.6.17 milestone May 21, 2017
@sjrd
Copy link
Member

sjrd commented May 21, 2017

Implemented in f4896a5

@sjrd sjrd closed this as completed May 21, 2017
@japgolly
Copy link
Contributor Author

Cool, that will work well.

Only concern though, will this scale as more module kinds are added? The goal is for a library to provide enough info for their code to work regardless of what module kind their downstream users choose. Do you foresee other modules kinds being added?

@sjrd
Copy link
Member

sjrd commented May 22, 2017

Yes well definitely have an ES2015 module kind at some point. But semantically it's the same as what we already offer with CommonJS modules. Only the desugaring is different. In general if we ever have more module kinds, the same thing will apply. They will all be semantically based on ES2015 modules.

sjrd referenced this issue in sjrd/scala-js Jun 2, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Feature request (that does not concern language semantics, see "language") language Affects language semantics.
Projects
None yet
Development

No branches or pull requests

5 participants