Skip to content

Commit

Permalink
Scala 3 support (#494)
Browse files Browse the repository at this point in the history
* WIP: SBT syncs

Dependencies have been updated to versions supporting Scala 3, IntelliJ manages to sync the project, this is a nice first step.

Introduces a new source folders `scala-2` & `scala-3`, for now the assumption is that `scala-2.13+` will be included for Scala 3.

Set Scala 3 as default version.

* WIP: Read/Write Macros split

Read/Write is now split between Scala 2&3, Scala 2 compiles as before, Scala 3 is still missing an implementation.

* ScalaJSReactInterop cross compile Scala 2&3

* WIP: Core macros split

Macros in Core is now split between Scala 2&3, Scala 2 compiles as before, Scala 3 is still missing an implementation.

* wip: make more modules compile on Scala 3

* comment out tests that don't compile yet on Scala 3

* basic product derivation for reader/writer using mirrors

* readwrite: add basic sum type derivation; disable union types

* readwrite: support scala 3 union through macros

* initial impl of macros for providers and functional component name

* tests: uncomment already working ones

* readwrite: fix c&p mistakes in external props provider; use by-name implicits to fix self-ref

* readwrite: first pass on value type support

* readwrite: support recursive case classes and existing in-scope instances

* readwrite: hacky support for default values

* readwrite: move all extra type-info macros into one place

* readwrite: add fallback reader/writer to general macro

* fix cross-compilation with scala 2

* wip: restore scala-2-only tests; use scala 2.13 as default in build

* Drop scalajs 0.6 from CI matrix; bump 1.x to 1.6.0

* fix by-name implicits used in scala 2; format code

* format build files

* don't build native and vr with scala 3

* fix build format once again

* bump scalajs-dom to 2.0.0

* review comments

* Update CHANGELOG.md

* Update CHANGELOG.md

Co-authored-by: Arman Bilge <[email protected]>

Co-authored-by: Alexander Samsig <[email protected]>
Co-authored-by: Arman Bilge <[email protected]>
  • Loading branch information
3 people authored Nov 1, 2021
1 parent b1dd3f4 commit e8571ec
Show file tree
Hide file tree
Showing 61 changed files with 1,303 additions and 544 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/sbt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
scalajs: ["0.6.33", "1.5.1"]
scalajs: ["1.6.0"]
es2015_enabled: ["false", "true"]
steps:
- name: Configure git to disable Windows line feeds
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ idea/
.vscode/
project/.bloop/
publishing-setup/

.bsp/
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
# Changelog

## vNEXT
### Highlights :tada:
+ Added preliminary Scala 3 support
+ no `@react`/`Props`-`apply` sugar
+ expected to require `-source:3.0-migration` and produce warnings
+ may not encode the same props in the same way as Scala 2 version due to different encoder generation mechanism.

### Breaking Changes :warning:
+ Dropped Scala.js 0.6 and upgraded 1.x line to 1.6.0 to simplify building with Scala 3.
+ Updated scalajs-dom to v2.0.0 which is cross-published for Scala 3

## [v0.6.8](https://slinky.dev)
### Bug Fixes :bug:
Expand Down
76 changes: 52 additions & 24 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@ ThisBuild / organization := "me.shadaj"
Global / onChangedBuildSource := ReloadOnSourceChanges
turbo := true

ThisBuild / libraryDependencies += compilerPlugin(scalafixSemanticdb)
addCommandAlias("style", "compile:scalafix; test:scalafix; compile:scalafmt; test:scalafmt; scalafmtSbt")
addCommandAlias(
"styleCheck",
"compile:scalafix --check; test:scalafix --check; compile:scalafmtCheck; test:scalafmtCheck; scalafmtSbtCheck"
)

val scala212 = "2.12.10"
val scala213 = "2.13.2"
val scala212 = "2.12.14"
val scala213 = "2.13.6"
val scala3 = "3.0.1"

ThisBuild / scalaVersion := scala213
ThisBuild / semanticdbEnabled := true

lazy val slinky = project
.in(file("."))
Expand Down Expand Up @@ -42,23 +43,28 @@ addCommandAlias(
)

lazy val crossScalaSettings = Seq(
crossScalaVersions := Seq(scala212, scala213),
Compile / unmanagedSourceDirectories += {
crossScalaVersions := Seq(scala212, scala213, scala3),
Compile / unmanagedSourceDirectories ++= {
val sourceDir = (Compile / sourceDirectory).value
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, n)) if n >= 13 => sourceDir / "scala-2.13+"
case _ => sourceDir / "scala-2.13-"
case Some((3, _)) => Seq(sourceDir / "scala-2.13+")
case Some((2, n)) if n >= 13 => Seq(sourceDir / "scala-2.13+")
case _ => Seq(sourceDir / "scala-2.13-")
}
},
Test / unmanagedSourceDirectories += {
Test / unmanagedSourceDirectories ++= {
val sourceDir = (Test / sourceDirectory).value
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, n)) if n >= 13 => sourceDir / "scala-2.13+"
case _ => sourceDir / "scala-2.13-"
case Some((3, _)) => Seq(sourceDir / "scala-2.13+")
case Some((2, n)) if n >= 13 => Seq(sourceDir / "scala-2.13+")
case _ => Seq(sourceDir / "scala-2.13-")
}
}
)

lazy val crossScala2OnlySettings =
(crossScalaVersions := Seq(scala212, scala213)) +: crossScalaSettings.tail

lazy val librarySettings = Seq(
scalacOptions += {
val origVersion = version.value
Expand All @@ -68,13 +74,29 @@ lazy val librarySettings = Seq(
s"v$origVersion"
}

val a = baseDirectory.value.toURI
val g = "https://raw.githubusercontent.com/shadaj/slinky"
s"-P:scalajs:mapSourceURI:$a->$g/$githubVersion/${baseDirectory.value.getName}/"
val a = baseDirectory.value.toURI
val g = "https://raw.githubusercontent.com/shadaj/slinky"
val opt = if (scalaVersion.value == scala3) "-scalajs-mapSourceURI" else "-P:scalajs:mapSourceURI"
s"$opt:$a->$g/$githubVersion/${baseDirectory.value.getName}/"
},
scalacOptions ++= Seq(
"-Ywarn-unused:imports"
)
"-encoding",
"UTF-8",
"-feature",
"-language:implicitConversions"
) ++ (CrossVersion.partialVersion(scalaVersion.value) match {
case Some((3, _)) =>
Seq(
"-unchecked",
"-source:3.0-migration"
)
case _ =>
Seq(
"-deprecation",
"-language:higherKinds",
"-Ywarn-unused:imports,privates,locals"
)
})
)

lazy val macroAnnotationSettings = Seq(
Expand All @@ -84,8 +106,9 @@ lazy val macroAnnotationSettings = Seq(
else Seq.empty
},
libraryDependencies ++= {
if (scalaVersion.value == scala213) Seq.empty
else Seq(compilerPlugin(("org.scalamacros" % "paradise" % "2.1.1").cross(CrossVersion.full)))
if (scalaVersion.value == scala212)
Seq(compilerPlugin(("org.scalamacros" % "paradise" % "2.1.1").cross(CrossVersion.full)))
else Seq.empty
}
)

Expand Down Expand Up @@ -156,20 +179,25 @@ lazy val reactrouter =
lazy val testRenderer = project.settings(macroAnnotationSettings, librarySettings, crossScalaSettings).dependsOn(core)

lazy val native =
project.settings(macroAnnotationSettings, librarySettings, crossScalaSettings).dependsOn(core, testRenderer % Test)
project
.settings(macroAnnotationSettings, librarySettings, crossScala2OnlySettings)
.dependsOn(core, testRenderer % Test)

lazy val vr =
project.settings(macroAnnotationSettings, librarySettings, crossScalaSettings).dependsOn(core, testRenderer % Test)
project
.settings(macroAnnotationSettings, librarySettings, crossScala2OnlySettings)
.dependsOn(core, testRenderer % Test)

lazy val hot = project.settings(macroAnnotationSettings, librarySettings, crossScalaSettings).dependsOn(core)

val scalaJSVersion =
Option(System.getenv("SCALAJS_VERSION")).getOrElse("0.6.33")
Option(System.getenv("SCALAJS_VERSION")).getOrElse("1.6.0")

lazy val scalajsReactInterop = project
.settings(
macroAnnotationSettings,
librarySettings
librarySettings,
crossScalaSettings
)
.dependsOn(core, web % Test)

Expand All @@ -181,7 +209,7 @@ lazy val docsMacros = project.settings(macroAnnotationSettings).dependsOn(web, h
lazy val docs =
project.settings(librarySettings, macroAnnotationSettings).dependsOn(web, hot, docsMacros, reactrouter, history)

updateIntellij in ThisBuild := {}
ThisBuild / updateIntellij := {}
val intelliJVersion = "203.6682.168" // 2020.3

lazy val coreIntellijSupport = project.settings(
Expand All @@ -190,5 +218,5 @@ lazy val coreIntellijSupport = project.settings(
): _*
)

intellijBuild in ThisBuild := intelliJVersion
packageMethod in ThisBuild := PackagingMethod.Skip()
ThisBuild / intellijBuild := intelliJVersion
ThisBuild / packageMethod := PackagingMethod.Skip()
10 changes: 9 additions & 1 deletion core/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@ enablePlugins(ScalaJSPlugin)

name := "slinky-core"

libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value
libraryDependencies ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, _)) =>
Seq(
"org.scala-lang" % "scala-reflect" % scalaVersion.value
)
case _ => Seq.empty
}
}

scalacOptions ++= {
if (scalaJSVersion.startsWith("0.6.")) Seq("-P:scalajs:sjsDefinedByDefault")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import slinky.readwrite.Writer

import scala.scalajs.js
import scala.scalajs.js.|
import scala.language.experimental.macros
import scala.reflect.macros.whitebox

final class BuildingComponent[E, R <: js.Object](private val args: js.Array[js.Any]) extends AnyVal {
def apply(mods: TagMod[E]*): BuildingComponent[E, R] = {
Expand Down Expand Up @@ -116,19 +114,3 @@ abstract class ExternalComponentNoPropsWithRefType[R <: js.Object]
extends ExternalComponentNoPropsWithAttributesWithRefType[Nothing, R]

abstract class ExternalComponentNoProps extends ExternalComponentNoPropsWithAttributes[Nothing]

// same as PropsWriterProvider except it always returns the typeclass instead of nulling it out in fullOpt mode
trait ExternalPropsWriterProvider extends js.Object
object ExternalPropsWriterProvider {
def impl(c: whitebox.Context): c.Expr[ExternalPropsWriterProvider] = {
import c.universe._
val compName = c.internal.enclosingOwner.owner.asClass
val q"$_; val x: $typedReaderType = null" = c.typecheck(
q"@_root_.scala.annotation.unchecked.uncheckedStable val comp: $compName = null; val x: _root_.slinky.readwrite.Writer[comp.Props] = null"
) // scalafix:ok
val tpcls = c.inferImplicitValue(typedReaderType.tpe.asInstanceOf[c.Type])
c.Expr(q"$tpcls.asInstanceOf[_root_.slinky.core.ExternalPropsWriterProvider]")
}

implicit def get: ExternalPropsWriterProvider = macro impl
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package slinky.core

import scala.scalajs.js

import scala.language.experimental.macros
import scala.reflect.macros.whitebox

// same as PropsWriterProvider except it always returns the typeclass instead of nulling it out in fullOpt mode
trait ExternalPropsWriterProvider extends js.Object
object ExternalPropsWriterProvider {
def impl(c: whitebox.Context): c.Expr[ExternalPropsWriterProvider] = {
import c.universe._
val compName = c.internal.enclosingOwner.owner.asClass
val q"$_; val x: $typedReaderType = null" = c.typecheck(
q"@_root_.scala.annotation.unchecked.uncheckedStable val comp: $compName = null; val x: _root_.slinky.readwrite.Writer[comp.Props] = null"
) // scalafix:ok
val tpcls = c.inferImplicitValue(typedReaderType.tpe.asInstanceOf[c.Type])
c.Expr(q"$tpcls.asInstanceOf[_root_.slinky.core.ExternalPropsWriterProvider]")
}

implicit def get: ExternalPropsWriterProvider = macro impl
}
31 changes: 31 additions & 0 deletions core/src/main/scala-2/slinky/core/FunctionalComponentName.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package slinky.core

import scala.language.experimental.macros
import scala.reflect.macros.whitebox

final class FunctionalComponentName(val name: String) extends AnyVal
object FunctionalComponentName {
implicit def get: FunctionalComponentName = macro FunctionalComponentNameMacros.impl
}

object FunctionalComponentNameMacros {
def impl(c: whitebox.Context): c.Expr[FunctionalComponentName] = {
import c.universe._

// from lihaoyi/sourcecode
def isSyntheticName(name: String) =
name == "<init>" || (name.startsWith("<local ") && name.endsWith(">")) || name == "component"

@scala.annotation.tailrec
def findNonSyntheticOwner(current: Symbol): Symbol =
if (isSyntheticName(current.name.decodedName.toString.trim)) {
findNonSyntheticOwner(current.owner)
} else {
current
}

c.Expr(
q"new _root_.slinky.core.FunctionalComponentName(${findNonSyntheticOwner(c.internal.enclosingOwner).name.decodedName.toString})"
)
}
}
23 changes: 23 additions & 0 deletions core/src/main/scala-2/slinky/core/StateReaderProvider.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package slinky.core

import scala.scalajs.js

import scala.language.experimental.macros
import scala.reflect.macros.whitebox

trait StateReaderProvider extends js.Object
object StateReaderProvider {
def impl(c: whitebox.Context): c.Expr[StateReaderProvider] = {
import c.universe._
val compName = c.internal.enclosingOwner.owner.asClass
val q"$_; val x: $typedReaderType = null" = c.typecheck(
q"@_root_.scala.annotation.unchecked.uncheckedStable val comp: $compName = null; val x: _root_.slinky.readwrite.Reader[comp.State] = null"
) // scalafix:ok
val tpcls = c.inferImplicitValue(typedReaderType.tpe.asInstanceOf[c.Type], silent = false)
c.Expr(
q"if (_root_.scala.scalajs.LinkingInfo.productionMode) null else $tpcls.asInstanceOf[_root_.slinky.core.StateReaderProvider]"
)
}

implicit def get: StateReaderProvider = macro impl
}
23 changes: 23 additions & 0 deletions core/src/main/scala-2/slinky/core/StateWriterProvider.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package slinky.core

import scala.scalajs.js

import scala.language.experimental.macros
import scala.reflect.macros.whitebox

trait StateWriterProvider extends js.Object
object StateWriterProvider {
def impl(c: whitebox.Context): c.Expr[StateWriterProvider] = {
import c.universe._
val compName = c.internal.enclosingOwner.owner.asClass
val q"$_; val x: $typedReaderType = null" = c.typecheck(
q"@_root_.scala.annotation.unchecked.uncheckedStable val comp: $compName = null; val x: _root_.slinky.readwrite.Writer[comp.State] = null"
) // scalafix:ok
val tpcls = c.inferImplicitValue(typedReaderType.tpe.asInstanceOf[c.Type], silent = false)
c.Expr(
q"if (_root_.scala.scalajs.LinkingInfo.productionMode) null else $tpcls.asInstanceOf[_root_.slinky.core.StateWriterProvider]"
)
}

implicit def get: StateWriterProvider = macro impl
}
6 changes: 6 additions & 0 deletions core/src/main/scala-3/slinky/core/ComponentWrapper.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package slinky.core

abstract class ComponentWrapper(implicit sr: => StateReaderProvider, sw: => StateWriterProvider)
extends BaseComponentWrapper(sr, sw) {
override type Definition = DefinitionBase[Props, State, Snapshot]
}
Loading

0 comments on commit e8571ec

Please sign in to comment.