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

Scala.js: Implement the PrepJSInterop phase, minus exports handling. #9725

Merged
merged 2 commits into from
Sep 14, 2020

Conversation

sjrd
Copy link
Member

@sjrd sjrd commented Sep 4, 2020

The PrepJSInterop phase is responsible for:

  • Performing all kinds of Scala.js-specific compile-time checks, and emitting the appropriate compile errors.
  • Perform some transformations that are necessary for JavaScript interop, notably generating exports forwarders.

This commit ports all the functionality of PrepJSInterop from Scala 2, except the following:

  • Handling of scala.Enumerations: it is unclear whether we still want to support that in the core, or if it should be handled by an optional compiler plugin in the future.
  • Exports: they will be done later.
  • Warnings about duplicate fields in js.Dynamic.literal: mostly because they are non-essential.

The test cases are ported from the Scala.js compiler tests.

Copy link
Member

@dottybot dottybot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, and thank you for opening this PR! 🎉

All contributors have signed the CLA, thank you! ❤️

Commit Messages

We want to keep history, but for that to actually be useful we have
some rules on how to format our commit messages (relevant xkcd).

Please stick to these guidelines for commit messages:

  1. Separate subject from body with a blank line
  2. When fixing an issue, start your commit message with Fix #<ISSUE-NBR>:
  3. Limit the subject line to 72 characters
  4. Capitalize the subject line
  5. Do not end the subject line with a period
  6. Use the imperative mood in the subject line ("Add" instead of "Added")
  7. Wrap the body at 80 characters
  8. Use the body to explain what and why vs. how

adapted from https://chris.beams.io/posts/git-commit

Have an awesome day! ☀️

@sjrd sjrd force-pushed the sjs-prep-js-interop branch 2 times, most recently from 7f75e30 to 23d59e4 Compare September 7, 2020 12:10

/** Nicer syntax for `allEnclosingOwners isnt kind`. */
private object noEnclosingOwner {
@inline def is(kind: OwnerKind): Boolean =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@inline def is(kind: OwnerKind): Boolean =
inline def is(kind: OwnerKind): Boolean =

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out noEnclosingOwner was dead code, so I removed it entirely. I changed the @inline defs inside OwnerKind itself to inline, except | for which I could not do it (I added a comment).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those that one get inlined by the Scala2 backend? Is that valid bytecode?

Copy link
Member Author

@sjrd sjrd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review! I think I have addressed all your comments.


/** Nicer syntax for `allEnclosingOwners isnt kind`. */
private object noEnclosingOwner {
@inline def is(kind: OwnerKind): Boolean =
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It turns out noEnclosingOwner was dead code, so I removed it entirely. I changed the @inline defs inside OwnerKind itself to inline, except | for which I could not do it (I added a comment).

@gzm0
Copy link
Contributor

gzm0 commented Sep 13, 2020

Diff between PrepJSInterop.scala in this PR and in Scala.js v1.2.0: https://gist.github.com/gzm0/8f515e48bb0918c56efab31cccbaaf17

Copy link
Contributor

@gzm0 gzm0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly higher level comments. Hopefully we can settle the overall approach question before I continue the review. IMO it quite changes what I'm looking for.

compiler/src/dotty/tools/dotc/core/SymDenotations.scala Outdated Show resolved Hide resolved
!sym.isSubClass(jsDefinitions.JSAnyClass)

object perRunInfo {
private val jsNativeLoadSpecs = new MutableSymbolMap[JSNativeLoadSpec]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this common practice in dotty? IMO we have resorted to mutable, per-run maps in Scala.js for 2.x because we couldn't recover all of the required info from the trees (or we couldn't store them on the symbol). However, I don't think in general this is a good pattern: It introduces mutable state in a pipeline that doesn't look like it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's not common practice. I went for the smallest logical diff wrt. our Scala 2 implementation.

I'll try to move this computation to the back-end and see if we can avoid the issue.

genApplyJSClassMethod(module, sym, arguments = Nil)
} else {
genApplyMethod(module, sym, arguments = Nil)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems odd that the distinction of receiver types needs to be made here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's also done in scala-js/scala-js:
https://github.com/scala-js/scala-js/blob/730931a3a11b351ea850a5c6107cfbd332c52a9b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala#L6171-L6188

That is necessary because the owner SomeObject referred to in an @JSName(SomeObject.aSymbolVal) can be a Scala object, a native JS object or a non-native JS object.

@@ -50,6 +52,11 @@ final class JSDefinitions()(using Context) {
@threadUnsafe lazy val PseudoUnionType: TypeRef = requiredClassRef("scala.scalajs.js.|")
def PseudoUnionClass(using Context) = PseudoUnionType.symbol.asClass

@threadUnsafe lazy val PseudoUnionModuleRef = requiredModuleRef("scala.scalajs.js.|")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we still need to special case this? I thought dotty supports union types.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dotty supports union types, but it can also interoperate with Scala 2 libraries. And in particular with the pseudo | type in scalajs-library, which could itself be referenced by other methods in the std lib or in third-party libs. In Scala 3.0, we'll still need to recognize Scala.js' | type and correctly handle it. In 3.1, when we get rid of the compatibility with Scala 2 binaries, we'll be able to entirely drop scala.scalajs.js.| from the library, and have all usages of | be the true union type of dotty.

Also note that we cannot "reinterpret" scala.scalajs.js.| as a dotty union type during Scala2Unpickler because they do not have the same erasure: scala.scalajs.js.|[A, B] erases to any with a ClassRef("scala.scalajs.js.|"), whereas a true A | B erases to the JVM lub of A and B, with a corresponding ClassRef.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also note that we cannot "reinterpret" scala.scalajs.js.| as a dotty union type during Scala2Unpickler because they do not have the same erasure

The same is true for Scala 2 intersection types versus Dotty &, I have a wip branch which erases those differently, so we could extend that mechanism to handle unions too if needed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean we could reinterpret scala.scalajs.js.| as a special brand of union type that somehow erases differently? That would be awesome!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that should be doable.

@@ -16,4 +25,27 @@ class SJSPlatform()(using Context) extends JavaPlatform {
defn.isFunctionClass(cls)
|| jsDefinitions.isJSFunctionClass(cls)
|| jsDefinitions.isJSThisFunctionClass(cls)

override def shouldReceiveJavaSerializationMethods(sym: ClassSymbol)(using Context): Boolean =
!sym.isSubClass(jsDefinitions.JSAnyClass)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this not simply false? This seems to add private methods with magic names relevant for Java serialization. Scala.js doesn't support this anyways, no?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the Scala classes and objects might be transitively used by macros and other compile-time code. I would rather have them be somewhat equivalent to the ones we would get in a JVM project. The JVM back-end will slap an extends java.io.Serializable to them, so we should be consistent and also emit the proper serialization methods, I believe.

object PrepJSExports {
import tpd._

def registerClassOrModuleExports(sym: Symbol)(using Context): Unit = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly to how to handle load specs: I think for dotty, where JS support is considered part of the same compiler, we should consider adding the relevant infrastructure that language specific compilers can store specific information on Symbols. (because this is what is essentially happening here).

IMO this will feel much more natural than having these mutable maps.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll see if we can do that, yes. But whether or not we can, we'll still have a registerClassOrModuleExports here to actually store that data, whatever the mechanism. So I believe that this method shell can stay regardless. Only its body would change depending on whether we store things inside Symbols or in side-channels Maps.

-- Error: tests/neg-scalajs/abstract-local-js-class.scala:6:19 ---------------------------------------------------------
6 | abstract class AbstractLocalJSClass extends js.Object // error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| Implementation restriction: local JS classes cannot be abstract
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have this restriction? (especially post scala-js/scala-js#4117, it feels pretty clear that abstract is merely a compile time concept).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of the way we implement the local captures of the constructor. We need ExplicitJSClasses to create a fake New(TheLocalJSClass, fakeParams), which lambdaLift (or its equivalent) enhances with the captures for local vals. The back-end captures the transformed fake New to recover those.

If the class is abstract, we cannot create that New without triggering internal inconsistencies. We also cannot blindly remove the Abstract flag, because the class could contain abstract methods, which still creates inconsistencies.

-- Error: tests/neg-scalajs/internal-annotations.scala:16:0 ------------------------------------------------------------
16 |@ExposedJSMember class D // error
|^^^^^^^^^^^^^^^^
|@scala.scalajs.js.annotation.internal.ExposedJSMember is for compiler internal use only. Do not use it yourself.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar comment about storing info on Symbols: IMO we should store these things in Symbols and store it in class files (if necessary) in the same way other dotty specific info is stored.

Or in other words: Scala.js compiler internal annotations should not be necessary anymore.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the very least, we need them for 2-way compatibility with Scala 2-compiled code. So in Scala 3.0 we don't have any other option. We could revisit and simplify in Scala 3.1 when we get rid of the binary compatibility with Scala 2.

@@ -0,0 +1,27 @@
-- Error: tests/neg-scalajs/jsconstructortag1.scala:9:39 ---------------------------------------------------------------
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These test names (jsconstructortag1 / jsconstructortag2) are not very good. If there is a semantic reason why there are two checks (and it seems there is), they should be part of the name.

Same for cosntructorof.

/** Whether to check that we have proper literals in some crucial places.
*
* This is always true in dotc. We keep the definition so that the code
* code can be as similar as possible to the scalac phase.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reasoning (keep things close together) seems to be inconsistently applied across this file. For example: fixPublicBeforeTyper has been fully removed (instead of replaced by an identity function).

I think we should stick to one approach here. Personally, I think it is probably worth diverging, but doing it properly (i.e. remove things we don't need, etc.).

OTOH, we should invest to share tests suites as much as we can to avoid (and/or identify) feature/behavior divergence.

Normally, annotations applied to a class have no business being
replicated on a synthetic companion object. One exception was the
`@alpha` method, which is supposed to apply as well. Instead of
trying to identify those annotations without the symbols during
`Desugar`, we change `erasedName` to go look on the companion class
of synthetic module classes.
Copy link
Member Author

@sjrd sjrd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only some answers to the comments for now. I'll take action on them tomorrow.

genApplyJSClassMethod(module, sym, arguments = Nil)
} else {
genApplyMethod(module, sym, arguments = Nil)
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's also done in scala-js/scala-js:
https://github.com/scala-js/scala-js/blob/730931a3a11b351ea850a5c6107cfbd332c52a9b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala#L6171-L6188

That is necessary because the owner SomeObject referred to in an @JSName(SomeObject.aSymbolVal) can be a Scala object, a native JS object or a non-native JS object.

@@ -50,6 +52,11 @@ final class JSDefinitions()(using Context) {
@threadUnsafe lazy val PseudoUnionType: TypeRef = requiredClassRef("scala.scalajs.js.|")
def PseudoUnionClass(using Context) = PseudoUnionType.symbol.asClass

@threadUnsafe lazy val PseudoUnionModuleRef = requiredModuleRef("scala.scalajs.js.|")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dotty supports union types, but it can also interoperate with Scala 2 libraries. And in particular with the pseudo | type in scalajs-library, which could itself be referenced by other methods in the std lib or in third-party libs. In Scala 3.0, we'll still need to recognize Scala.js' | type and correctly handle it. In 3.1, when we get rid of the compatibility with Scala 2 binaries, we'll be able to entirely drop scala.scalajs.js.| from the library, and have all usages of | be the true union type of dotty.

Also note that we cannot "reinterpret" scala.scalajs.js.| as a dotty union type during Scala2Unpickler because they do not have the same erasure: scala.scalajs.js.|[A, B] erases to any with a ClassRef("scala.scalajs.js.|"), whereas a true A | B erases to the JVM lub of A and B, with a corresponding ClassRef.

@@ -16,4 +25,27 @@ class SJSPlatform()(using Context) extends JavaPlatform {
defn.isFunctionClass(cls)
|| jsDefinitions.isJSFunctionClass(cls)
|| jsDefinitions.isJSThisFunctionClass(cls)

override def shouldReceiveJavaSerializationMethods(sym: ClassSymbol)(using Context): Boolean =
!sym.isSubClass(jsDefinitions.JSAnyClass)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the Scala classes and objects might be transitively used by macros and other compile-time code. I would rather have them be somewhat equivalent to the ones we would get in a JVM project. The JVM back-end will slap an extends java.io.Serializable to them, so we should be consistent and also emit the proper serialization methods, I believe.

!sym.isSubClass(jsDefinitions.JSAnyClass)

object perRunInfo {
private val jsNativeLoadSpecs = new MutableSymbolMap[JSNativeLoadSpec]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's not common practice. I went for the smallest logical diff wrt. our Scala 2 implementation.

I'll try to move this computation to the back-end and see if we can avoid the issue.


import dotty.tools.backend.sjs.JSDefinitions.jsdefn

object JSInteropUtils {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll rename this to JSSymUtils, which is consistent with transform.SymUtils that already exists in dotty (and which is also a bunch of extension methods on Symbol).

For JSName, do you mean move it inside another object called JSData?

object PrepJSExports {
import tpd._

def registerClassOrModuleExports(sym: Symbol)(using Context): Unit = {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll see if we can do that, yes. But whether or not we can, we'll still have a registerClassOrModuleExports here to actually store that data, whatever the mechanism. So I believe that this method shell can stay regardless. Only its body would change depending on whether we store things inside Symbols or in side-channels Maps.

-- Error: tests/neg-scalajs/abstract-local-js-class.scala:6:19 ---------------------------------------------------------
6 | abstract class AbstractLocalJSClass extends js.Object // error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| Implementation restriction: local JS classes cannot be abstract
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of the way we implement the local captures of the constructor. We need ExplicitJSClasses to create a fake New(TheLocalJSClass, fakeParams), which lambdaLift (or its equivalent) enhances with the captures for local vals. The back-end captures the transformed fake New to recover those.

If the class is abstract, we cannot create that New without triggering internal inconsistencies. We also cannot blindly remove the Abstract flag, because the class could contain abstract methods, which still creates inconsistencies.

-- Error: tests/neg-scalajs/internal-annotations.scala:16:0 ------------------------------------------------------------
16 |@ExposedJSMember class D // error
|^^^^^^^^^^^^^^^^
|@scala.scalajs.js.annotation.internal.ExposedJSMember is for compiler internal use only. Do not use it yourself.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the very least, we need them for 2-way compatibility with Scala 2-compiled code. So in Scala 3.0 we don't have any other option. We could revisit and simplify in Scala 3.1 when we get rid of the binary compatibility with Scala 2.

Copy link
Member Author

@sjrd sjrd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. I believe I have addressed all the comments so far. The only question is where to put JSName, but IMO it's find where it is.

I've kept things in different commits for an easier re-review, but all the commits except the first one should be squashed before merging.

@@ -50,6 +52,11 @@ final class JSDefinitions()(using Context) {
@threadUnsafe lazy val PseudoUnionType: TypeRef = requiredClassRef("scala.scalajs.js.|")
def PseudoUnionClass(using Context) = PseudoUnionType.symbol.asClass

@threadUnsafe lazy val PseudoUnionModuleRef = requiredModuleRef("scala.scalajs.js.|")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean we could reinterpret scala.scalajs.js.| as a special brand of union type that somehow erases differently? That would be awesome!

@sjrd sjrd requested a review from gzm0 September 14, 2020 10:29
Copy link
Contributor

@gzm0 gzm0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only minor comments.

}

private def transformScalaValOrDefDef(tree: ValOrDefDef)(using Context): Tree = {
// There is nothing special to do for a Scala val or def
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider removing this helper and simply moving the comment to the call-site.

tree)
}
super.transform(tree)
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented out on purpose?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed it.

sym.isAnonymousClass && AllJSFunctionClasses.exists(sym.isSubClass(_))
if (isJSLambda)
transformJSLambdaClassDef(classDef)
else*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I take it lambdas reach this phase as a special node? If yes, I think all of this code should be removed in this PR.

case parentSym if parentSym == defn.DynamicClass =>
/* We have to allow scala.Dynamic to be able to define js.Dynamic
* and similar constructs.
* This causes the unsoundness filed as #1385.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

errorPos)
}

// Check for overrides with different JS names - issue #1983
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

def checkGlobalRefPath(pathName: String): Unit =
checkGlobalRefName(firstElementOfPath(pathName))

checkAndGetJSNativeLoadingSpecAnnotOf(pos, sym) match {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit confusing IMO. Because both the helper and this method check things (and there is only this call site of the helper). Not sure how to improve it easily though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was bothering me as well, but I didn't find a better name, and I still don't find one now, so I'll leave it as is for now.

val dotIndex = pathName.indexOf('.')
if (dotIndex < 0) pathName
else pathName.substring(0, dotIndex)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider inlining this to checkGlobalRefPath (the only call site).

}

case nme.equals_ if sym.info.matches(defn.Any_equals.info) =>
report.warning(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider making this fatal since we have the chance? (Ideally we'd just make these final IIUC, not sure if this works, given that js.Any is a trait).

report.error("@JSBracketCall is not allowed in non-native JS classes", tree)
} else {
// JS bracket calls must have at least one non-repeated parameter
sym.info.stripPoly match {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check doesn't flatten parameter lists. Is this on purpose? (IIUC, Scala.js 2.x does flatten lists).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

def isJSAny(sym: Symbol)(using Context): Boolean =
sym.isSubClass(jsdefn.JSAnyClass)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this deliberately different from sym.isJSType?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I have added a comment.

The `PrepJSInterop` phase is responsible for:

* Performing all kinds of Scala.js-specific compile-time checks,
  and emitting the appropriate compile errors.
* Perform some transformations that are necessary for JavaScript
  interop, notably generating exports forwarders.

This commit ports all the functionality of `PrepJSInterop` from
Scala 2, except the following:

* Handling of `scala.Enumeration`s: it is unclear whether we still
  want to support that in the core, or if it should be handled by
  an optional compiler plugin in the future.
* Exports: they will be done later.
* Warnings about duplicate fields in `js.Dynamic.literal`: mostly
  because they are non-essential.

The test cases are ported from the Scala.js compiler tests.
@sjrd sjrd merged commit cbb05c8 into scala:master Sep 14, 2020
@sjrd sjrd deleted the sjs-prep-js-interop branch September 14, 2020 21:04
@aappddeevv
Copy link

I'm glad to see this.

I'm not sure how the build pipeline works, but does this show up in the nightly build and becomes usable if we use the nightly dotty build or do we need to wait until the related scala.js sbt plugins are updated?

@sjrd
Copy link
Member Author

sjrd commented Sep 15, 2020

It should be available in the nightly builds of dotty. In any case it's orthogonal to the version of sbt-scalajs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants