From 67a625a5df636a062b9e42430a8362d4b511bc3a Mon Sep 17 00:00:00 2001 From: David Barri Date: Fri, 30 Jul 2021 17:30:24 +1000 Subject: [PATCH 1/5] Core compiles and passes --- .github/workflows/ci.yml | 6 +- build.sbt | 2 + .../scalacss/internal/mutable/Mutex.scala | 2 +- .../main/scala/scalacss/js/PlatformJs.scala | 42 +-- .../scalacss/internal/mutable/Mutex.scala | 2 +- .../scalacss/internal/Macros.scala | 10 + .../scala-3/scalacss/internal/Macros.scala | 252 ++++++++++++++++++ .../main/scala/scalacss/internal/Dsl.scala | 14 +- .../main/scala/scalacss/internal/Media.scala | 8 +- .../main/scala/scalacss/internal/Pseudo.scala | 2 +- .../scalacss/internal/mutable/Settings.scala | 14 +- .../internal/mutable/StyleSheet.scala | 4 +- .../src/main/scala/scalacss/package.scala | 8 +- .../scala/scalacss/full/DefaultsTest.scala | 6 +- .../scala/scalacss/internal/ColorTest.scala | 6 +- .../scala/scalacss/internal/ComposeTest.scala | 2 +- .../scala/scalacss/internal/RandomData.scala | 12 +- .../scala/scalacss/internal/UsageTest.scala | 2 +- .../internal/mutable/RegisterTest.scala | 4 +- project/Build.scala | 65 +++-- project/Dependencies.scala | 5 +- project/Lib.scala | 30 +-- 22 files changed, 389 insertions(+), 109 deletions(-) rename core/shared/src/main/{scala => scala-2}/scalacss/internal/Macros.scala (96%) create mode 100644 core/shared/src/main/scala-3/scalacss/internal/Macros.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61dffb37..7fa32b7c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,12 +13,10 @@ jobs: fail-fast: false matrix: include: - - java: 8 - scala: 2.13.6 - - java: 11 - scala: 2.13.6 - java: 16 scala: 2.13.6 + - java: 8 + scala: 3.0.1 steps: - name: Git checkout diff --git a/build.sbt b/build.sbt index ba2eee70..aa899ac8 100644 --- a/build.sbt +++ b/build.sbt @@ -1,6 +1,8 @@ name := "ScalaCSS" startYear := Some(2015) +ThisBuild / shellPrompt := ((s: State) => Project.extract(s).currentRef.project + "> ") + // Mutability & NameGen causes occasional test failures ThisBuild / parallelExecution := false diff --git a/core/js/src/main/scala/scalacss/internal/mutable/Mutex.scala b/core/js/src/main/scala/scalacss/internal/mutable/Mutex.scala index db169eea..b6defa89 100644 --- a/core/js/src/main/scala/scalacss/internal/mutable/Mutex.scala +++ b/core/js/src/main/scala/scalacss/internal/mutable/Mutex.scala @@ -12,5 +12,5 @@ final class Mutex { } object Mutex { - implicit val mutex = new Mutex + implicit val mutex: Mutex = new Mutex } diff --git a/core/js/src/main/scala/scalacss/js/PlatformJs.scala b/core/js/src/main/scala/scalacss/js/PlatformJs.scala index 0eb8cf23..cbd78d65 100644 --- a/core/js/src/main/scala/scalacss/js/PlatformJs.scala +++ b/core/js/src/main/scala/scalacss/js/PlatformJs.scala @@ -1,7 +1,7 @@ package scalacss.js import scala.annotation.nowarn -import scala.scalajs.js._ +import scala.scalajs.js object PlatformJs { @@ -16,38 +16,38 @@ object PlatformJs { /** * Returns `undefined` in NodeJS unit tests. */ - lazy val value: UndefOr[PlatformJs] = + lazy val value: js.UndefOr[PlatformJs] = try { - eval(sourceCode) - Dynamic.global.platform.asInstanceOf[UndefOr[PlatformJs]] + js.eval(sourceCode) + js.Dynamic.global.platform.asInstanceOf[js.UndefOr[PlatformJs]] } catch { case e: Throwable => e.printStackTrace(System.err) - undefined + js.undefined } } /** * https://github.com/bestiejs/platform.js/blob/master/doc/README.md#readme */ -@native +@js.native @nowarn -trait PlatformJs extends Object { - val description : UndefOr[String] = native - val layout : UndefOr[String] = native - val manufacturer: UndefOr[String] = native - val name : UndefOr[String] = native - val prerelease : UndefOr[String] = native - val product : UndefOr[String] = native - val ua : UndefOr[String] = native - val version : UndefOr[String] = native - val os : UndefOr[PlatformJsOS] = native +trait PlatformJs extends js.Object { + val description : js.UndefOr[String] = js.native + val layout : js.UndefOr[String] = js.native + val manufacturer: js.UndefOr[String] = js.native + val name : js.UndefOr[String] = js.native + val prerelease : js.UndefOr[String] = js.native + val product : js.UndefOr[String] = js.native + val ua : js.UndefOr[String] = js.native + val version : js.UndefOr[String] = js.native + val os : js.UndefOr[PlatformJsOS] = js.native } -@native +@js.native @nowarn -trait PlatformJsOS extends Object { - val architecture: UndefOr[Int] = native - val family : UndefOr[String] = native - val version : UndefOr[String] = native +trait PlatformJsOS extends js.Object { + val architecture: js.UndefOr[Int] = js.native + val family : js.UndefOr[String] = js.native + val version : js.UndefOr[String] = js.native } diff --git a/core/jvm/src/main/scala/scalacss/internal/mutable/Mutex.scala b/core/jvm/src/main/scala/scalacss/internal/mutable/Mutex.scala index c54b58a0..67ccec8d 100644 --- a/core/jvm/src/main/scala/scalacss/internal/mutable/Mutex.scala +++ b/core/jvm/src/main/scala/scalacss/internal/mutable/Mutex.scala @@ -12,5 +12,5 @@ final class Mutex { } object Mutex { - implicit def mutex = new Mutex + implicit def mutex: Mutex = new Mutex } diff --git a/core/shared/src/main/scala/scalacss/internal/Macros.scala b/core/shared/src/main/scala-2/scalacss/internal/Macros.scala similarity index 96% rename from core/shared/src/main/scala/scalacss/internal/Macros.scala rename to core/shared/src/main/scala-2/scalacss/internal/Macros.scala index 59b1ab81..515a641d 100644 --- a/core/shared/src/main/scala/scalacss/internal/Macros.scala +++ b/core/shared/src/main/scala-2/scalacss/internal/Macros.scala @@ -9,6 +9,11 @@ object Macros { object Dsl { + trait Base { + @inline final implicit def colourLiteralMacro(sc: StringContext): Macros.ColourLiteral = + new Macros.ColourLiteral(sc) + } + private def name(c: Context): String = { val localName = c.internal.enclosingOwner.name.toString.trim @@ -179,6 +184,11 @@ object Macros { // =================================================================================================================== + trait DevOrProdDefaults { + def devOrProdDefaults: Exports with Settings = + macro Macros.devOrProdDefaults + } + def devOrProdDefaults(c: Context): c.Expr[Exports with mutable.Settings] = { import c.universe._ c.Expr[Exports with mutable.Settings](q""" diff --git a/core/shared/src/main/scala-3/scalacss/internal/Macros.scala b/core/shared/src/main/scala-3/scalacss/internal/Macros.scala new file mode 100644 index 00000000..5f6621bb --- /dev/null +++ b/core/shared/src/main/scala-3/scalacss/internal/Macros.scala @@ -0,0 +1,252 @@ +package scalacss.internal + +import java.util.regex.Pattern +import scala.quoted._ +import scalacss.defaults.Exports +import scala.language.`3.0` +import scala.reflect.NameTransformer + +object Macros { + + object Dsl { + + trait Base { + extension (inline sc: StringContext) { + /** c"#fc6" provides a validates Color */ + inline def c(inline args: Any*): Color = + ${ ColorLiteral.impl('sc) } + } + } + + inline def name(): String = + ${ nameQuoted } + + private def nameQuoted(using Quotes): Expr[String] = { + import quotes.reflect._ + + val owner = { + var o = Symbol.spliceOwner + while (o.flags.is(Flags.Synthetic)) + o = o.owner + o + } + + val isStyleSheet: Symbol => Boolean = { + val mss = TypeRepr.of[mutable.StyleSheet.Inline] + sym => + try { + sym.tree match { + case ClassDef(_, _, parents, _, _) => + parents.exists { + case p: TypeTree => p.tpe <:< mss + case _ => false + } + case _ => false + } + } catch { + case _: Throwable => false + } + } + + val classOwner = { + var o = owner.owner + while (!isStyleSheet(o) && !o.maybeOwner.isNoSymbol) + o = o.owner + o + } + + def fixName(s: String): String = + s.replace("$", "") + + val localName = + fixName(owner.name) + + // `style()` instead of `val x = style()` results in "" + val finalName = + if (localName startsWith "<") + "" + else { + // Try to extract a name, relative to the stylesheet class + val className = fixName(classOwner.fullName) + val fullName = fixName(owner.fullName) + if ((fullName.length > className.length + 1) && fullName.startsWith(className + ".")) { + val relName = fullName.substring(className.length + 1).replace('.', '-') + relName + } else + // Default to local name + localName + } + + Inlined(None, Nil, Literal(StringConstant(finalName))).asExprOf[String] + } + + import DslMacros._ + + trait Mixin { + protected def __macroStyle (name: String): MStyle + protected def __macroStyleF (name: String): MStyleF + protected def __macroKeyframes(name: String): MKeyframes + protected def __macroKeyframe : MStyle + protected def __macroFontFace : MFontFace + + final protected inline def style : MStyle = __macroStyle(name()) + final protected inline def styleF : MStyleF = __macroStyleF(name()) + final protected inline def keyframes: MKeyframes = __macroKeyframes(name()) + final protected inline def keyframe : MStyle = __macroKeyframe + final protected inline def fontFace : MFontFace = __macroFontFace + } + } + + // =================================================================================================================== + + type Color = ValueT[ValueT.Color] + + object ColorLiteral { + + private def cssFnRegex(f: String, as: String*) = { + val args = as mkString "," + Pattern compile s"^$f\\($args\\)$$" + } + + private def int = "(-?\\d+)" + private def dbl = """(-?\d+(?:\.\d+)?|\.\d+)""" + private def pct = s"$dbl%" + + private val ws = "\\s+".r + private val rgbI = cssFnRegex("rgb", int, int, int) + private val rgbP = cssFnRegex("rgb", pct, pct, pct) + private val rgba = cssFnRegex("rgba", int, int, int, dbl) + private val hsl = cssFnRegex("hsl", int, pct, pct) + private val hsla = cssFnRegex("hsla", int, pct, pct, dbl) + + private def isHex: Char => Boolean = + c => (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') + + def impl(sc: Expr[StringContext])(using Quotes): Expr[Color] = { + import quotes.reflect._ + + import report.{throwError => fail} + + val arg: Expr[String] = + sc match { + case '{ StringContext(${Varargs(parts)}*) } => + parts.toList match { + case a :: Nil => a + case x => fail(s"Expected exactly 1 StringContext part, got: $x") + } + case _ => + fail(s"Failed to extract StringContext parts") + } + + val value: String = + arg.asTerm match { + case Literal(StringConstant(v)) => v + case Inlined(_, _, Literal(StringConstant(v))) => v + case x => fail("Don't know how to handle: " + x) + } + + attempt(value) match { + case Right(c) => + val l = Inlined(None, Nil, Literal(StringConstant(c))).asExprOf[String] + Inlined(None, Nil, '{ Color($l) }.asTerm).asExprOf[Color] + case Left(e) => + report.throwError(e) + } + } + + def runtime(text: String): Color = + attempt(text) match { + case Right(c) => Color(c) + case Left(e) => throw new IllegalArgumentException(e) + } + + def attempt(text0: String): Either[String, String] = { + val text = ws.replaceAllIn(text0, "").toLowerCase + + var errorResult: Left[String, String] = + null + + def pass = true + def undecided = false + def fail(err: String) = { + errorResult = Left(err) + false + } + + def validateHex: Boolean = + (text.charAt(0) == '#') && { + val v = text.drop(1) + v.length match { + case (3 | 6 | 4 | 8) if v.forall(isHex) => pass + case _ => fail("Hex notation must be #RGB, #RRGGBB, #RGBA, or #RRGGBBAA.") + } + } + + type V = String => Unit + + def validateInt(name: String, max: Int): V = + s => { + val i = s.toInt + if (i < 0 || i > max) + fail(s"Invalid $name value: $s") + } + + def validateDbl(name: String, max: Double): V = + s => { + val d = s.toDouble + if (d < 0 || d > max) + fail(s"Invalid $name value: $s") + } + + def validatePct(name: String): V = + validateDbl(name, 100) + + def validateFn(p: Pattern, a: V, b: V, c: V, d: V = null): Boolean = { + val m = p.matcher(text) + if (m.matches) { + a(m group 1) + b(m group 2) + c(m group 3) + if (d ne null) d(m group 4) + pass + } else + undecided + } + + def ri = validateInt("red", 255) + def gi = validateInt("green", 255) + def bi = validateInt("blue", 255) + def rp = validatePct("red") + def gp = validatePct("green") + def bp = validatePct("blue") + def h = validateInt("hue", 359) + def s = validatePct("saturation") + def l = validatePct("lightness") + def a = validateDbl("alpha", 1) + + def validateRgbI = validateFn(rgbI, ri, gi, bi) + def validateRgbP = validateFn(rgbP, rp, gp, bp) + def validateRgba = validateFn(rgba, ri, gi, bi, a) + def validateHsl = validateFn(hsl, h, s, l) + def validateHsla = validateFn(hsla, h, s, l, a) + + val validated = + validateHex || validateRgbI || validateRgbP || validateRgba || validateHsl || validateHsla + + if (validated && (errorResult == null)) + Right(text) + else + Option(errorResult).getOrElse(Left("Invalid colour: \"" + text0 + "\"")) + } + } + + // =================================================================================================================== + + trait DevOrProdDefaults { + inline final def devOrProdDefaults: Exports with mutable.Settings = + if (scalacss.internal.Platform.DevMode) + scalacss.DevDefaults + else + scalacss.ProdDefaults + } +} diff --git a/core/shared/src/main/scala/scalacss/internal/Dsl.scala b/core/shared/src/main/scala/scalacss/internal/Dsl.scala index f954529d..20f56ef7 100644 --- a/core/shared/src/main/scala/scalacss/internal/Dsl.scala +++ b/core/shared/src/main/scala/scalacss/internal/Dsl.scala @@ -10,8 +10,8 @@ object DslBase { object MediaQueryEmpty extends TypeAOps[Query] with FeatureOps[Query] { override protected def F = f => new Query(Right(f), Vector.empty) override protected def T = t => new Query(Left(Just(t)), Vector.empty) - def not = new MediaQueryNeedType(Not) - def only = new MediaQueryNeedType(Only) + def not = new MediaQueryNeedType(Not.apply) + def only = new MediaQueryNeedType(Only.apply) } // media.{not,only} @@ -154,6 +154,7 @@ import DslBase._ // ===================================================================================================================== abstract class DslBase extends AttrAliasesAndValueTRules + with Macros.Dsl.Base with TypedLiteralAliases with ColorOps[ValueT[Color]] { @@ -186,7 +187,7 @@ abstract class DslBase @inline implicit final def autoDslAV (a: AV) : DslAV = new DslAV(a) @inline implicit final def autoDslAVs (a: AVs) : DslAVs = new DslAVs(a) - implicit final def DslCond[C](x: C)(implicit f: C => Cond): DslCond = new DslCond(x, this) + implicit final def DslCond[C](x: C)(implicit f: C => Cond): DslCond = new DslCond(f(x), this) @inline implicit final def ToAVToAV(x: ToAV): AV = x.av @@ -196,8 +197,8 @@ abstract class DslBase implicit final def ToStyleToAV (x: ToAV) : ToStyle = ToStyleAV(x.av) implicit final def ToStyleAV (x: AV) : ToStyle = ToStyleAVs(AVs(x)) implicit final def ToStyleAVs (x: AVs) : ToStyle = new ToStyle(StyleS.data1(Cond.empty, x)) - implicit final def ToStyleCAV [C] (x: (C, AV)) (implicit f: C => Cond): ToStyle = new ToStyle(StyleS.data1(x._1, AVs(x._2))) - implicit final def ToStyleCAVs[C] (x: (C, AVs))(implicit f: C => Cond): ToStyle = new ToStyle(StyleS.data1(x._1, x._2)) + implicit final def ToStyleCAV [C] (x: (C, AV)) (implicit f: C => Cond): ToStyle = new ToStyle(StyleS.data1(f(x._1), AVs(x._2))) + implicit final def ToStyleCAVs[C] (x: (C, AVs))(implicit f: C => Cond): ToStyle = new ToStyle(StyleS.data1(f(x._1), x._2)) implicit final def ToStyleUnsafeExt (x: UnsafeExt) : ToStyle = ToStyleUnsafeExts(Vector1(x)) implicit final def ToStyleUnsafeExts(x: UnsafeExts) : ToStyle = new ToStyle(StyleS.empty.copy(unsafeExts = x)) @inline implicit final def ToStyleStyleS (x: StyleS) : ToStyle = new ToStyle(x) @@ -230,9 +231,6 @@ abstract class DslBase def mixinIfElse(b: Boolean)(t: ToStyle*)(f: ToStyle*)(implicit c: Compose): StyleS = styleS((if (b) t else f): _*)(c) - - @inline implicit def colourLiteralMacro(sc: StringContext) = - new Macros.ColourLiteral(sc) } // ===================================================================================================================== diff --git a/core/shared/src/main/scala/scalacss/internal/Media.scala b/core/shared/src/main/scala/scalacss/internal/Media.scala index 2c7f3b17..044bcea0 100644 --- a/core/shared/src/main/scala/scalacss/internal/Media.scala +++ b/core/shared/src/main/scala/scalacss/internal/Media.scala @@ -206,13 +206,13 @@ object Media { `(` ~ c ~ `)` private def opt[T](ov: Option[T], name: String)(implicit tc: T => String): String = - ov.fold(name)(name ~ `:` ~ _) + ov.fold(name)(name ~ `:` ~ tc(_)) private def cssValueExpr[T](e: ValueExpr[T], name: String)(implicit tc: T => String): String = e match { - case Eql(t) => name ~ `:` ~ t - case Min(t) => min ~ name ~ `:` ~ t - case Max(t) => max ~ name ~ `:` ~ t + case Eql(t) => name ~ `:` ~ tc(t) + case Min(t) => min ~ name ~ `:` ~ tc(t) + case Max(t) => max ~ name ~ `:` ~ tc(t) } private def cssValueExprO[T](o: Option[ValueExpr[T]], name: String)(implicit tc: T => String): String = diff --git a/core/shared/src/main/scala/scalacss/internal/Pseudo.scala b/core/shared/src/main/scala/scalacss/internal/Pseudo.scala index 0b9d204d..eed8948b 100644 --- a/core/shared/src/main/scala/scalacss/internal/Pseudo.scala +++ b/core/shared/src/main/scala/scalacss/internal/Pseudo.scala @@ -28,7 +28,7 @@ object PseudoType { sealed abstract class Pseudo extends Pseudo.ChainOps[Pseudo] { import Pseudo._ - val cssValue: String + def cssValue: String // apply() is used by Dsl final def modSelector(sel: CssSelector): CssSelector = diff --git a/core/shared/src/main/scala/scalacss/internal/mutable/Settings.scala b/core/shared/src/main/scala/scalacss/internal/mutable/Settings.scala index bdeb2bb5..776f4f5f 100644 --- a/core/shared/src/main/scala/scalacss/internal/mutable/Settings.scala +++ b/core/shared/src/main/scala/scalacss/internal/mutable/Settings.scala @@ -19,13 +19,13 @@ trait Settings { object Settings { trait Delegate extends Settings { - override def cssRegisterNameGen = cssSettings.cssRegisterNameGen - override def cssRegisterMacroName = cssSettings.cssRegisterMacroName - override def cssRegisterErrorHandler = cssSettings.cssRegisterErrorHandler - override implicit def cssStringRenderer = cssSettings.cssStringRenderer - override implicit def cssComposition = cssSettings.cssComposition - override implicit def cssEnv = cssSettings.cssEnv - override implicit def cssRegister = cssSettings.cssRegister + override def cssRegisterNameGen : NameGen = cssSettings.cssRegisterNameGen + override def cssRegisterMacroName : MacroName = cssSettings.cssRegisterMacroName + override def cssRegisterErrorHandler: ErrorHandler = cssSettings.cssRegisterErrorHandler + override implicit def cssStringRenderer : Renderer[String] = cssSettings.cssStringRenderer + override implicit def cssComposition : Compose = cssSettings.cssComposition + override implicit def cssEnv : Env = cssSettings.cssEnv + override implicit def cssRegister : Register = cssSettings.cssRegister protected[scalacss] def cssSettings: Settings } diff --git a/core/shared/src/main/scala/scalacss/internal/mutable/StyleSheet.scala b/core/shared/src/main/scala/scalacss/internal/mutable/StyleSheet.scala index 8bdb6325..b03e4009 100644 --- a/core/shared/src/main/scala/scalacss/internal/mutable/StyleSheet.scala +++ b/core/shared/src/main/scala/scalacss/internal/mutable/StyleSheet.scala @@ -38,7 +38,9 @@ object StyleSheet { @inline final def Color(literal: String) = scalacss.internal.Color(literal) - @inline implicit final def toCondOps[C](x: C)(implicit f: C => Cond) = new CondOps(x) + @inline implicit final def toCondOps[C](x: C)(implicit f: C => Cond): CondOps = + new CondOps(f(x)) + final class CondOps(val cond: Cond) { @inline def - = new DslCond(cond, dsl) } diff --git a/core/shared/src/main/scala/scalacss/package.scala b/core/shared/src/main/scala/scalacss/package.scala index 5612584e..c49edda6 100644 --- a/core/shared/src/main/scala/scalacss/package.scala +++ b/core/shared/src/main/scala/scalacss/package.scala @@ -1,4 +1,7 @@ -package object scalacss extends scalacss.defaults.Exports { +package object scalacss + extends scalacss.defaults.Exports + with internal.Macros.DevOrProdDefaults { + import internal.mutable.Settings import defaults.{DefaultSettings, Exports} @@ -11,7 +14,4 @@ package object scalacss extends scalacss.defaults.Exports { "\n 3. val CssSettings = scalacss.devOrProdDefaults; import CssSettings._", "0.5.3") object Defaults extends Exports with DefaultSettings.Dev - - def devOrProdDefaults: Exports with Settings = - macro internal.Macros.devOrProdDefaults } diff --git a/core/shared/src/test/scala/scalacss/full/DefaultsTest.scala b/core/shared/src/test/scala/scalacss/full/DefaultsTest.scala index 1763e2c3..c93a7b53 100644 --- a/core/shared/src/test/scala/scalacss/full/DefaultsTest.scala +++ b/core/shared/src/test/scala/scalacss/full/DefaultsTest.scala @@ -17,7 +17,7 @@ object DefaultsTest extends TestSuite { class SharedStyles(implicit reg: StyleSheet.Register) extends StyleSheet.Inline { import dsl._ - implicit def compose = CssComposer.trust + implicit def compose: CssComposer = CssComposer.trust val header = style(backgroundColor(c"#333")) val footer = style(backgroundColor(c"#666")) } @@ -39,7 +39,7 @@ object DefaultsTest extends TestSuite { val shared = new SharedStyles } - implicit def cssEnv = CssEnv.empty + implicit def cssEnv: CssEnv = CssEnv.empty val css = SS.render[String] def norm(css: String) = css.trim @@ -92,7 +92,7 @@ object DefaultsTest extends TestSuite { val blah = style(width.inherit) } - implicit def cssEnv = CssEnv.empty + implicit def cssEnv: CssEnv = CssEnv.empty val css1 = SS1.render[String] val css2 = SS2.render[String] diff --git a/core/shared/src/test/scala/scalacss/internal/ColorTest.scala b/core/shared/src/test/scala/scalacss/internal/ColorTest.scala index 67733fa3..9b60dc85 100644 --- a/core/shared/src/test/scala/scalacss/internal/ColorTest.scala +++ b/core/shared/src/test/scala/scalacss/internal/ColorTest.scala @@ -37,10 +37,8 @@ object ColorTest extends TestSuite { "invalid" - { @nowarn("cat=unused") def assertFailure(e: CompileError) = () - def assertErrorContains(e: CompileError, frag: String): Unit = { - val err = e.msg - assert(err contains frag) - } + def assertErrorContains(e: CompileError, frag: String): Unit = + assertContains(e.msg, frag) "hex" - { def test(e: CompileError): Unit = assertErrorContains(e, "Hex notation must be") diff --git a/core/shared/src/test/scala/scalacss/internal/ComposeTest.scala b/core/shared/src/test/scala/scalacss/internal/ComposeTest.scala index 44e0240c..5cdcf888 100644 --- a/core/shared/src/test/scala/scalacss/internal/ComposeTest.scala +++ b/core/shared/src/test/scala/scalacss/internal/ComposeTest.scala @@ -85,7 +85,7 @@ object ComposeTest extends TestSuite { "values" - { import Dsl._ - implicit def c = Compose.safe + implicit def c: Compose = Compose.safe def test(s: StyleS)(avs: AV*)(ws: String*): Unit = { assertEq(s.data(Cond.empty).avIterator.toVector, avs.toVector) diff --git a/core/shared/src/test/scala/scalacss/internal/RandomData.scala b/core/shared/src/test/scala/scalacss/internal/RandomData.scala index db92c055..632b1bd5 100644 --- a/core/shared/src/test/scala/scalacss/internal/RandomData.scala +++ b/core/shared/src/test/scala/scalacss/internal/RandomData.scala @@ -35,10 +35,14 @@ object RandomData { Visited, After, Before, FirstLetter, FirstLine, Selection) val needNthQuery = NonEmptyList.of[NthQuery => Pseudo]( - NthChild, NthLastChild, NthLastOfType, NthOfType) + NthChild.apply, + NthLastChild.apply, + NthLastOfType.apply, + NthOfType.apply, + ) val needStr = NonEmptyList.of[String => Pseudo]( - Custom(_, PseudoType.Class), Custom(_, PseudoType.Element), Lang, new Not(_), AttrExists) + Custom(_, PseudoType.Class), Custom(_, PseudoType.Element), Lang.apply, new Not(_), AttrExists.apply) val need2Str = NonEmptyList.of[(String, String) => Pseudo]( Pseudo.Attr _, AttrContains _, AttrStartsWith _, AttrEndsWith _) @@ -64,7 +68,7 @@ object RandomData { } def unsafeExt(g: Gen[StyleS]): Gen[UnsafeExt] = - Gen.apply3(UnsafeExt)(unsafeCssSelEndo, cond, g) + Gen.apply3(UnsafeExt.apply)(unsafeCssSelEndo, cond, g) private def sized(from: Int, jvm: Int, js: Int): SizeSpec = from to (jvm `JVM|JS` js) @@ -74,7 +78,7 @@ object RandomData { unsafeExt(g).vector(sized(1, 8, 3))) val warning: Gen[Warning] = - Gen.apply2(Warning)(cond, str) + Gen.apply2(Warning.apply)(cond, str) val className: Gen[ClassName] = str1 map ClassName.apply diff --git a/core/shared/src/test/scala/scalacss/internal/UsageTest.scala b/core/shared/src/test/scala/scalacss/internal/UsageTest.scala index 6079db8e..15644db0 100644 --- a/core/shared/src/test/scala/scalacss/internal/UsageTest.scala +++ b/core/shared/src/test/scala/scalacss/internal/UsageTest.scala @@ -7,7 +7,7 @@ object UsageTest extends TestSuite { import Dsl._ import Pseudo._ - implicit def composition = Compose.safe + implicit def composition: Compose = Compose.safe val thing = 2.ex diff --git a/core/shared/src/test/scala/scalacss/internal/mutable/RegisterTest.scala b/core/shared/src/test/scala/scalacss/internal/mutable/RegisterTest.scala index 46c1506d..f98a5408 100644 --- a/core/shared/src/test/scala/scalacss/internal/mutable/RegisterTest.scala +++ b/core/shared/src/test/scala/scalacss/internal/mutable/RegisterTest.scala @@ -29,14 +29,14 @@ object RegisterTest extends TestSuite { assertEq(l, l.distinct) } - implicit def env = Env.empty + implicit def env: Env = Env.empty def stylesToCssMap(s: Vector[StyleA]) = Css.styles(s).map(e => (e.sel, e.content)).toMap override def tests = Tests { val reg = new Register(NameGen.numbered(), MacroName.Use, ErrorHandler.noisy) - implicit def cnh = ClassNameHint("blah") + implicit def cnh: ClassNameHint = ClassNameHint("blah") "registerS" - { val a1 = reg registerS ss1 diff --git a/project/Build.scala b/project/Build.scala index 4837ec59..a5aa7b10 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -20,22 +20,28 @@ object ScalaCssBuild { private val publicationSettings = Lib.publicationSettings(ghProject) - def scalacFlags = - Seq( - "-deprecation", - "-unchecked", - "-feature", - "-language:postfixOps", - "-language:implicitConversions", - "-language:higherKinds", - "-language:existentials", - "-opt:l:inline", - "-opt-inline-from:japgolly.univeq.**", - "-opt-inline-from:scalacss.**", - "-Ywarn-dead-code", - "-Ywarn-unused", - // "-Ywarn-value-discard", - ) + def scalacCommonFlags = Seq( + "-deprecation", + "-unchecked", + "-feature", + "-language:postfixOps", + "-language:implicitConversions", + "-language:higherKinds", + "-language:existentials", + ) + + def scalac2Flags = Seq( + "-opt:l:inline", + "-opt-inline-from:japgolly.univeq.**", + "-opt-inline-from:scalacss.**", + "-Ywarn-dead-code", + "-Ywarn-unused", + // "-Ywarn-value-discard", + ) + + def scalac3Flags = Seq( + "-source", "3.0-migration", + ) val commonSettings = ConfigureBoth( _.settings( @@ -43,9 +49,10 @@ object ScalaCssBuild { homepage := Some(url("https://github.com/japgolly/scalacss")), licenses += ("Apache-2.0", url("http://opensource.org/licenses/Apache-2.0")), scalaVersion := Ver.scala2, - crossScalaVersions := Seq(Ver.scala2), - scalacOptions ++= scalacFlags, - ThisBuild / shellPrompt := ((s: State) => Project.extract(s).currentRef.project + "> "), + crossScalaVersions := Seq(Ver.scala2, Ver.scala3), + scalacOptions ++= scalacCommonFlags, + scalacOptions ++= scalac2Flags.filter(_ => scalaVersion.value.startsWith("2")), + scalacOptions ++= scalac3Flags.filter(_ => scalaVersion.value.startsWith("3")), // incOptions := incOptions.value.withNameHashing(true).withLogRecompileOnMacro(false), updateOptions := updateOptions.value.withCachedResolution(true), releasePublishArtifactsAction := PgpKeys.publishSigned.value, @@ -54,11 +61,21 @@ object ScalaCssBuild { def definesMacros = ConfigureBoth( _.settings( - scalacOptions += "-language:experimental.macros", - libraryDependencies ++= Seq( - Dep.scalaReflect.value, - Dep.scalaCompiler.value % Provided, - ), + scalacOptions ++= { + if (scalaVersion.value.startsWith("2")) + "-language:experimental.macros" :: Nil + else + Nil + }, + libraryDependencies ++= { + if (scalaVersion.value.startsWith("2")) + List( + Dep.scalaReflect.value, + Dep.scalaCompiler.value % Provided, + ) + else + Nil + }, ) ) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 7336cec6..8c8b79bc 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -10,6 +10,7 @@ object Dependencies { // Exported val scala2 = "2.13.6" + val scala3 = "3.0.1" val scalaJsDom = "1.1.0" val scalaJsReact = "2.0.0-RC2" val scalatags = "0.9.4" @@ -30,12 +31,12 @@ object Dependencies { val nyayaProp = Def.setting("com.github.japgolly.nyaya" %%% "nyaya-prop" % Ver.nyaya) val nyayaTest = Def.setting("com.github.japgolly.nyaya" %%% "nyaya-test" % Ver.nyaya) val scalaCompiler = Def.setting("org.scala-lang" % "scala-compiler" % scalaVersion.value) - val scalaJsDom = Def.setting("org.scala-js" %%% "scalajs-dom" % Ver.scalaJsDom) + val scalaJsDom = Def.setting("org.scala-js" %%% "scalajs-dom" % Ver.scalaJsDom cross CrossVersion.for3Use2_13) val scalaJsReactCore = Def.setting("com.github.japgolly.scalajs-react" %%% "core" % Ver.scalaJsReact) val scalaJsReactCoreGen = Def.setting("com.github.japgolly.scalajs-react" %%% "core-generic" % Ver.scalaJsReact) val scalaJsReactTest = Def.setting("com.github.japgolly.scalajs-react" %%% "test" % Ver.scalaJsReact) val scalaReflect = Def.setting("org.scala-lang" % "scala-reflect" % scalaVersion.value) - val scalatags = Def.setting("com.lihaoyi" %%% "scalatags" % Ver.scalatags) + val scalatags = Def.setting("com.lihaoyi" %%% "scalatags" % Ver.scalatags cross CrossVersion.for3Use2_13) val univEq = Def.setting("com.github.japgolly.univeq" %%% "univeq" % Ver.univEq) val utest = Def.setting("com.lihaoyi" %%% "utest" % Ver.utest) } diff --git a/project/Lib.scala b/project/Lib.scala index afcf7eb1..10ab5eec 100644 --- a/project/Lib.scala +++ b/project/Lib.scala @@ -53,24 +53,22 @@ object Lib { .jsConfigure( sourceMapsToGithub(ghProject)) - def sourceMapsToGithub(ghProject: String): PE = + def sourceMapsToGithub(ghProject: String): Project => Project = p => p.settings( - scalacOptions ++= (if (isSnapshot.value) Seq.empty else Seq({ - val a = p.base.toURI.toString.replaceFirst("[^/]+/?$", "") - val g = s"https://raw.githubusercontent.com/japgolly/$ghProject" - s"-P:scalajs:mapSourceURI:$a->$g/v${version.value}/" - })) + scalacOptions ++= { + val isDotty = scalaVersion.value startsWith "3" + val ver = version.value + if (isSnapshot.value) + Nil + else { + val a = p.base.toURI.toString.replaceFirst("[^/]+/?$", "") + val g = s"https://raw.githubusercontent.com/japgolly/$ghProject" + val flag = if (isDotty) "-scalajs-mapSourceURI" else "-P:scalajs:mapSourceURI" + s"$flag:$a->$g/v$ver/" :: Nil + } + } ) def preventPublication: PE = - _.settings( - publish / skip := true, - publish := (()), - publishLocal := (()), - publishSigned := (()), - publishLocalSigned := (()), - publishArtifact := false, - publishTo := Some(Resolver.file("Unused transient repository", target.value / "fakepublish")), - packagedArtifacts := Map.empty) - // .disablePlugins(plugins.IvyPlugin) + _.settings(publish / skip := true) } From 00c28ee290772c19c5f2be9ba7092aa02fb0a63c Mon Sep 17 00:00:00 2001 From: David Barri Date: Fri, 30 Jul 2021 19:07:56 +1000 Subject: [PATCH 2/5] Disable scalatags and elisionTest for Scala 3 --- .../src/test/scala/scalacss/ReactTest.scala | 2 +- project/Build.scala | 7 ++++--- project/Dependencies.scala | 1 + project/Lib.scala | 17 +++++++++++++++++ 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/ext-react/src/test/scala/scalacss/ReactTest.scala b/ext-react/src/test/scala/scalacss/ReactTest.scala index 04857a1f..58423754 100644 --- a/ext-react/src/test/scala/scalacss/ReactTest.scala +++ b/ext-react/src/test/scala/scalacss/ReactTest.scala @@ -4,13 +4,13 @@ import japgolly.scalajs.react._ import japgolly.scalajs.react.vdom.html_<^._ import org.scalajs.dom.document import org.scalajs.dom.raw.HTMLStyleElement -import scalacss.DevDefaults._ import scalacss.ScalaCssReact._ import scalacss.TestUtil._ import scalacss.internal.mutable.StyleSheetRegistry import utest._ object ReactTest extends TestSuite { + import scalacss.DevDefaults._ object MyStyles extends StyleSheet.Inline { import dsl._ diff --git a/project/Build.scala b/project/Build.scala index a5aa7b10..e94a33d6 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -139,7 +139,7 @@ object ScalaCssBuild { lazy val elisionTest = crossProject(JSPlatform, JVMPlatform) .in(file("elision-test")) .configureCross(commonSettings, utestSettings) - .configure(preventPublication) + .configure(preventPublication, onlyScala2) .dependsOn(core) .settings( scalacOptions ++= Seq("-Xelide-below", "OFF"), @@ -149,9 +149,9 @@ object ScalaCssBuild { lazy val extScalatagsJS = extScalatags.js lazy val extScalatags = crossProject(JSPlatform, JVMPlatform) .in(file("ext-scalatags")) - .configureCross(commonSettings, publicationSettings) .dependsOn(core) - .configureCross(utestSettings) + .configureCross(commonSettings, publicationSettings, utestSettings) + .configure(onlyScala2) .settings( moduleName := "ext-scalatags", libraryDependencies ++= Seq( @@ -170,6 +170,7 @@ object ScalaCssBuild { moduleName := "ext-react", libraryDependencies ++= Seq( Dep.scalaJsReactCoreGen.value % Provided, + Dep.scalaJsReactDummy.value % Provided, Dep.scalaJsReactTest.value % Test, Dep.cats.value % Test, ), diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 8c8b79bc..1beddf9c 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -34,6 +34,7 @@ object Dependencies { val scalaJsDom = Def.setting("org.scala-js" %%% "scalajs-dom" % Ver.scalaJsDom cross CrossVersion.for3Use2_13) val scalaJsReactCore = Def.setting("com.github.japgolly.scalajs-react" %%% "core" % Ver.scalaJsReact) val scalaJsReactCoreGen = Def.setting("com.github.japgolly.scalajs-react" %%% "core-generic" % Ver.scalaJsReact) + val scalaJsReactDummy = Def.setting("com.github.japgolly.scalajs-react" %%% "util-dummy-defaults" % Ver.scalaJsReact) val scalaJsReactTest = Def.setting("com.github.japgolly.scalajs-react" %%% "test" % Ver.scalaJsReact) val scalaReflect = Def.setting("org.scala-lang" % "scala-reflect" % scalaVersion.value) val scalatags = Def.setting("com.lihaoyi" %%% "scalatags" % Ver.scalatags cross CrossVersion.for3Use2_13) diff --git a/project/Lib.scala b/project/Lib.scala index 10ab5eec..2dad17f5 100644 --- a/project/Lib.scala +++ b/project/Lib.scala @@ -71,4 +71,21 @@ object Lib { def preventPublication: PE = _.settings(publish / skip := true) + + def onlyScala2(p: Project) = { + def clearWhenDisabled[A](key: SettingKey[Seq[A]]) = + Def.setting[Seq[A]] { + val disabled = scalaVersion.value.startsWith("3") + val as = key.value + if (disabled) Nil else as + } + p.settings( + libraryDependencies := clearWhenDisabled(libraryDependencies).value, + Compile / unmanagedSourceDirectories := clearWhenDisabled(Compile / unmanagedSourceDirectories).value, + Test / unmanagedSourceDirectories := clearWhenDisabled(Test / unmanagedSourceDirectories).value, + publish / skip := ((publish / skip).value || scalaVersion.value.startsWith("3")), + Test / test := { if (scalaVersion.value.startsWith("2")) (Test / test).value }, + ) + } + } From 126faadfcc7fc770bcc9700cd3aff2e0c143f039 Mon Sep 17 00:00:00 2001 From: David Barri Date: Fri, 30 Jul 2021 19:11:56 +1000 Subject: [PATCH 3/5] Fix --- core/shared/src/main/scala-2/scalacss/internal/Macros.scala | 2 +- core/shared/src/main/scala/scalacss/package.scala | 1 - doc/history/0.8.md | 1 + ext-react/src/test/scala/scalacss/ReactTest.scala | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/shared/src/main/scala-2/scalacss/internal/Macros.scala b/core/shared/src/main/scala-2/scalacss/internal/Macros.scala index 515a641d..8d0d26d2 100644 --- a/core/shared/src/main/scala-2/scalacss/internal/Macros.scala +++ b/core/shared/src/main/scala-2/scalacss/internal/Macros.scala @@ -185,7 +185,7 @@ object Macros { // =================================================================================================================== trait DevOrProdDefaults { - def devOrProdDefaults: Exports with Settings = + def devOrProdDefaults: Exports with mutable.Settings = macro Macros.devOrProdDefaults } diff --git a/core/shared/src/main/scala/scalacss/package.scala b/core/shared/src/main/scala/scalacss/package.scala index c49edda6..0e02920a 100644 --- a/core/shared/src/main/scala/scalacss/package.scala +++ b/core/shared/src/main/scala/scalacss/package.scala @@ -2,7 +2,6 @@ package object scalacss extends scalacss.defaults.Exports with internal.Macros.DevOrProdDefaults { - import internal.mutable.Settings import defaults.{DefaultSettings, Exports} object DevDefaults extends Exports with DefaultSettings.Dev diff --git a/doc/history/0.8.md b/doc/history/0.8.md index 6ed7262a..d0f2efa9 100644 --- a/doc/history/0.8.md +++ b/doc/history/0.8.md @@ -1,3 +1,4 @@ # 0.8.0 ([commit log](https://github.com/japgolly/scalacss/compare/v0.7.0..v0.8.0-RC1)) +* Add support for Scala 3 (`scalatags` module excepted) * Upgrade deps diff --git a/ext-react/src/test/scala/scalacss/ReactTest.scala b/ext-react/src/test/scala/scalacss/ReactTest.scala index 58423754..dd411962 100644 --- a/ext-react/src/test/scala/scalacss/ReactTest.scala +++ b/ext-react/src/test/scala/scalacss/ReactTest.scala @@ -1,4 +1,4 @@ -package scalacss +package scalacss.external import japgolly.scalajs.react._ import japgolly.scalajs.react.vdom.html_<^._ From c27bc5684728823fdc860e8fa32fb29b47462a5a Mon Sep 17 00:00:00 2001 From: David Barri Date: Sat, 31 Jul 2021 08:58:20 +1000 Subject: [PATCH 4/5] Harden Scala 3 macro --- core/shared/src/main/scala-3/scalacss/internal/Macros.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/shared/src/main/scala-3/scalacss/internal/Macros.scala b/core/shared/src/main/scala-3/scalacss/internal/Macros.scala index 5f6621bb..b41b5e7b 100644 --- a/core/shared/src/main/scala-3/scalacss/internal/Macros.scala +++ b/core/shared/src/main/scala-3/scalacss/internal/Macros.scala @@ -32,13 +32,13 @@ object Macros { } val isStyleSheet: Symbol => Boolean = { - val mss = TypeRepr.of[mutable.StyleSheet.Inline] + val ss = TypeRepr.of[mutable.StyleSheet.Base] sym => try { sym.tree match { case ClassDef(_, _, parents, _, _) => parents.exists { - case p: TypeTree => p.tpe <:< mss + case p: TypeTree => (p.tpe <:< ss) case _ => false } case _ => false From 5680a82849e00f3c6c60cd8ebdf70d168cd07933 Mon Sep 17 00:00:00 2001 From: David Barri Date: Sat, 31 Jul 2021 10:42:37 +1000 Subject: [PATCH 5/5] Fix up dev/prod mode selection --- .github/workflows/ci.yml | 12 +- bin/ci | 57 ++++++++ bin/get_scala_version | 11 ++ build.sbt | 2 - .../scalacss/internal/ModeMacros.scala | 52 +++++++ .../scalacss/internal/ModeMacros.scala | 55 ++++++++ .../scala/scalacss/internal/Platform.scala | 11 -- .../scalacss/internal/ModeMacros.scala | 44 ++++++ .../scalacss/internal/ModeMacros.scala | 47 +++++++ .../scala/scalacss/internal/Platform.scala | 10 -- .../scala-2/scalacss/internal/Macros.scala | 18 --- .../scala-3/scalacss/internal/Macros.scala | 13 +- .../scalacss/defaults/DefaultSettings.scala | 6 - .../src/main/scala/scalacss/package.scala | 2 +- .../scala/scalacss/full/DefaultsTest.scala | 8 +- doc/features/basics.md | 19 +-- doc/history/0.8.md | 5 + downstream-tests/build.sbt | 119 ++++++++++++++++ .../js/src/main/scala/downstream/Main.scala | 10 ++ .../test/scala/downstream/RuntimeTest.scala | 27 ++++ .../test/scala/downstream/JsOutputTest.scala | 53 +++++++ .../jvm/src/test/scala/downstream/Props.scala | 34 +++++ downstream-tests/project/Dependencies.scala | 1 + downstream-tests/project/Lib.scala | 1 + downstream-tests/project/build.properties | 1 + downstream-tests/project/plugins.sbt | 1 + downstream-tests/run | 130 ++++++++++++++++++ downstream-tests/version.sbt | 1 + .../scala/scalacss/elision/DefaultsTest.scala | 34 ----- project/Build.scala | 48 +------ project/Dependencies.scala | 31 +++-- project/Lib.scala | 34 +++++ 32 files changed, 713 insertions(+), 184 deletions(-) create mode 100755 bin/ci create mode 100755 bin/get_scala_version create mode 100644 core/js/src/main/scala-2/scalacss/internal/ModeMacros.scala create mode 100644 core/js/src/main/scala-3/scalacss/internal/ModeMacros.scala create mode 100644 core/jvm/src/main/scala-2/scalacss/internal/ModeMacros.scala create mode 100644 core/jvm/src/main/scala-3/scalacss/internal/ModeMacros.scala create mode 100644 downstream-tests/build.sbt create mode 100644 downstream-tests/js/src/main/scala/downstream/Main.scala create mode 100644 downstream-tests/js/src/test/scala/downstream/RuntimeTest.scala create mode 100644 downstream-tests/jvm/src/test/scala/downstream/JsOutputTest.scala create mode 100644 downstream-tests/jvm/src/test/scala/downstream/Props.scala create mode 120000 downstream-tests/project/Dependencies.scala create mode 120000 downstream-tests/project/Lib.scala create mode 120000 downstream-tests/project/build.properties create mode 120000 downstream-tests/project/plugins.sbt create mode 100755 downstream-tests/run create mode 120000 downstream-tests/version.sbt delete mode 100644 elision-test/shared/src/test/scala/scalacss/elision/DefaultsTest.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fa32b7c..b1ee65ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,9 +14,9 @@ jobs: matrix: include: - java: 16 - scala: 2.13.6 + scala: 2 - java: 8 - scala: 3.0.1 + scala: 3 steps: - name: Git checkout @@ -40,12 +40,8 @@ jobs: - name: Build and test shell: bash - run: sbt ++${{ matrix.scala }} test - - - name: Test fullOptJS - shell: bash - run: sbt ++${{ matrix.scala }} 'set ThisBuild / scalaJSStage := FullOptStage' JS/test + run: bin/ci ${{ matrix.scala }} - name: Show JS sizes shell: bash - run: sbt ++${{ matrix.scala }} cmpJsSize jsSizes + run: sbt "++$(bin/get_scala_version ${{ matrix.scala }})" cmpJsSize jsSizes diff --git a/bin/ci b/bin/ci new file mode 100755 index 00000000..92692e5f --- /dev/null +++ b/bin/ci @@ -0,0 +1,57 @@ +#!/bin/bash +set -euo pipefail +cd "$(dirname "$0")/.." + +if [ $# -gt 1 ]; then + echo "Usage: $0 []" + echo "" + echo "Where" + echo " = 2 or 3" + echo + exit 1 +fi + +case "${1:-}" in + "") + "$0" 2 + echo + echo + "$0" 3 + exit 0 + ;; + 2|3) + SCALA_VER="$(bin/get_scala_version $1)" + echo "Scala version: $SCALA_VER" + ;; + *) + echo "Unrecognised version: $1" >&2 + exit 2 + ;; +esac + +dryrun= +# dryrun=-n + +# See how much memory is available +free -h +echo + +# Test upstream +rm -rf */target/scala-*/{,test-}classes +cmd=( + sbt + -J-Xmx3G + -J-XX:+UseG1GC + ++$SCALA_VER + 'set ThisBuild / parallelExecution := false' + 'set Global / concurrentRestrictions += Tags.limit(ScalaJSTags.Link, 1)' + test # Test development-mode + 'set ThisBuild / scalaJSStage := FullOptStage' test # Test production-mode + publishLocal # For downstream tests +) +echo "> $(printf "%s\n " "${cmd[@]}")" +[[ $dryrun == "-n" ]] || "${cmd[@]}" + +# Test downstream +SCALA_MAJOR_VER="${SCALA_VER:0:1}" +downstream-tests/run -$SCALA_MAJOR_VER $dryrun diff --git a/bin/get_scala_version b/bin/get_scala_version new file mode 100755 index 00000000..153f69e9 --- /dev/null +++ b/bin/get_scala_version @@ -0,0 +1,11 @@ +#!/bin/bash +set -euo pipefail +cd "$(dirname "$0")/.." +tmp="$(fgrep " scala$1 " project/Dependencies.scala)" +r='[^"]*"([^"]*)"[^"]*' +if [[ "$tmp" =~ $r ]]; then + echo "${BASH_REMATCH[1]}" +else + echo "Unable to determine Scala $1 version" >&2 + exit 3 +fi diff --git a/build.sbt b/build.sbt index aa899ac8..2b08041d 100644 --- a/build.sbt +++ b/build.sbt @@ -12,8 +12,6 @@ val rootJS = ScalaCssBuild.rootJS val coreJVM = ScalaCssBuild.coreJVM val coreJS = ScalaCssBuild.coreJS -val elisionTestJVM = ScalaCssBuild.elisionTestJVM -val elisionTestJS = ScalaCssBuild.elisionTestJS val extScalatagsJVM = ScalaCssBuild.extScalatagsJVM val extScalatagsJS = ScalaCssBuild.extScalatagsJS val extReact = ScalaCssBuild.extReact diff --git a/core/js/src/main/scala-2/scalacss/internal/ModeMacros.scala b/core/js/src/main/scala-2/scalacss/internal/ModeMacros.scala new file mode 100644 index 00000000..130f58be --- /dev/null +++ b/core/js/src/main/scala-2/scalacss/internal/ModeMacros.scala @@ -0,0 +1,52 @@ +package scalacss.internal + +import scala.reflect.macros.whitebox.Context +import scalacss.defaults._ + +// ========================== +// ==== ==== +// ==== Scala 2 / JS ==== +// ==== ==== +// ========================== + +object ModeMacros { + + private def readConfig(key: String): Option[String] = + (Option(System.getProperty(key, null)) orElse Option(System.getenv(key))) + .map(_.trim.toLowerCase) + .filter(_.nonEmpty) + + trait DevOrProdDefaults { + def devOrProdDefaults: Exports with mutable.Settings = + macro ModeMacros.devOrProdDefaults + } + + def devOrProdDefaults(c: Context): c.Expr[Exports with mutable.Settings] = { + import c.universe._ + + def fail(msg: String): Nothing = + c.abort(c.enclosingPosition, msg) + + c.Expr[Exports with mutable.Settings]( + readConfig("scalacss.mode") match { + + case None => + q""" + if (_root_.scala.scalajs.LinkingInfo.developmentMode) + _root_.scalacss.DevDefaults + else + _root_.scalacss.ProdDefaults + """ + + case Some("dev") => + q"_root_.scalacss.DevDefaults" + + case Some("prod") => + q"_root_.scalacss.ProdDefaults" + + case Some(x) => + fail(s"Unrecognise option for scalacss.mode: $x. Legal values are 'dev' and 'prod'.") + } + ) + } +} diff --git a/core/js/src/main/scala-3/scalacss/internal/ModeMacros.scala b/core/js/src/main/scala-3/scalacss/internal/ModeMacros.scala new file mode 100644 index 00000000..539835ff --- /dev/null +++ b/core/js/src/main/scala-3/scalacss/internal/ModeMacros.scala @@ -0,0 +1,55 @@ +package scalacss.internal + +import scalacss.defaults._ +import scala.language.`3.0` +import scala.quoted._ + +// ========================== +// ==== ==== +// ==== Scala 3 / JS ==== +// ==== ==== +// ========================== + +object ModeMacros { + + private def readConfig(key: String): Option[String] = + (Option(System.getProperty(key, null)) orElse Option(System.getenv(key))) + .map(_.trim.toLowerCase) + .filter(_.nonEmpty) + + trait DevOrProdDefaults { + transparent inline def devOrProdDefaults: Exports with mutable.Settings = + ${ ModeMacros.devOrProdDefaults } + } + + def devOrProdDefaults(using Quotes): Expr[Exports with mutable.Settings] = { + import quotes.reflect._ + + def fail(msg: String): Nothing = + report.throwError(msg) + + type S = Exports with mutable.Settings + val expr: Expr[S] = + readConfig("scalacss.mode") match { + + case None => + '{ + if (_root_.scala.scalajs.LinkingInfo.developmentMode) + _root_.scalacss.DevDefaults + else + _root_.scalacss.ProdDefaults + } + + case Some("dev") => + '{ scalacss.DevDefaults } + + case Some("prod") => + '{ scalacss.ProdDefaults } + + case Some(x) => + fail(s"Unrecognise option for scalacss.mode: $x. Legal values are 'dev' and 'prod'.") + } + + Inlined(None, Nil, expr.asTerm).asExprOf[S] + } +} diff --git a/core/js/src/main/scala/scalacss/internal/Platform.scala b/core/js/src/main/scala/scalacss/internal/Platform.scala index 4dce0e98..9a2d13ca 100644 --- a/core/js/src/main/scala/scalacss/internal/Platform.scala +++ b/core/js/src/main/scala/scalacss/internal/Platform.scala @@ -1,7 +1,5 @@ package scalacss.internal -import scala.annotation.elidable - // ================ // ==== ==== // ==== JS ==== @@ -10,15 +8,6 @@ import scala.annotation.elidable object Platform { - /** - * Use the scalac `-Xelide-below` flag to switch from development- to production-mode. - */ - @elidable(elidable.ASSERTION) - @inline def DevMode: Boolean = - true -// @inline def DevMode: Boolean = -// scalajs.LinkingInfo.developmentMode - implicit def env: Env = Env.empty } diff --git a/core/jvm/src/main/scala-2/scalacss/internal/ModeMacros.scala b/core/jvm/src/main/scala-2/scalacss/internal/ModeMacros.scala new file mode 100644 index 00000000..bf451144 --- /dev/null +++ b/core/jvm/src/main/scala-2/scalacss/internal/ModeMacros.scala @@ -0,0 +1,44 @@ +package scalacss.internal + +import scala.reflect.macros.whitebox.Context +import scalacss.defaults._ + +// =========================== +// ==== ==== +// ==== Scala 2 / JVM ==== +// ==== ==== +// =========================== + +object ModeMacros { + + private def readConfig(key: String): Option[String] = + (Option(System.getProperty(key, null)) orElse Option(System.getenv(key))) + .map(_.trim.toLowerCase) + .filter(_.nonEmpty) + + trait DevOrProdDefaults { + def devOrProdDefaults: Exports with mutable.Settings = + macro ModeMacros.devOrProdDefaults + } + + def devOrProdDefaults(c: Context): c.Expr[Exports with mutable.Settings] = { + import c.universe._ + + def fail(msg: String): Nothing = + c.abort(c.enclosingPosition, msg) + + c.Expr[Exports with mutable.Settings]( + readConfig("scalacss.mode") match { + + case Some("dev") | None => + q"_root_.scalacss.DevDefaults" + + case Some("prod") => + q"_root_.scalacss.ProdDefaults" + + case Some(x) => + fail(s"Unrecognise option for scalacss.mode: $x. Legal values are 'dev' and 'prod'.") + } + ) + } +} diff --git a/core/jvm/src/main/scala-3/scalacss/internal/ModeMacros.scala b/core/jvm/src/main/scala-3/scalacss/internal/ModeMacros.scala new file mode 100644 index 00000000..04fd09d0 --- /dev/null +++ b/core/jvm/src/main/scala-3/scalacss/internal/ModeMacros.scala @@ -0,0 +1,47 @@ +package scalacss.internal + +import scalacss.defaults._ +import scala.language.`3.0` +import scala.quoted._ + +// =========================== +// ==== ==== +// ==== Scala 3 / JVM ==== +// ==== ==== +// =========================== + +object ModeMacros { + + private def readConfig(key: String): Option[String] = + (Option(System.getProperty(key, null)) orElse Option(System.getenv(key))) + .map(_.trim.toLowerCase) + .filter(_.nonEmpty) + + trait DevOrProdDefaults { + transparent inline def devOrProdDefaults: Exports with mutable.Settings = + ${ ModeMacros.devOrProdDefaults } + } + + def devOrProdDefaults(using Quotes): Expr[Exports with mutable.Settings] = { + import quotes.reflect._ + + def fail(msg: String): Nothing = + report.throwError(msg) + + type S = Exports with mutable.Settings + val expr: Expr[S] = + readConfig("scalacss.mode") match { + + case Some("dev") | None => + '{ scalacss.DevDefaults } + + case Some("prod") => + '{ scalacss.ProdDefaults } + + case Some(x) => + fail(s"Unrecognise option for scalacss.mode: $x. Legal values are 'dev' and 'prod'.") + } + + Inlined(None, Nil, expr.asTerm).asExprOf[S] + } +} diff --git a/core/jvm/src/main/scala/scalacss/internal/Platform.scala b/core/jvm/src/main/scala/scalacss/internal/Platform.scala index 6309a31b..8a227a96 100644 --- a/core/jvm/src/main/scala/scalacss/internal/Platform.scala +++ b/core/jvm/src/main/scala/scalacss/internal/Platform.scala @@ -1,7 +1,5 @@ package scalacss.internal -import scala.annotation.elidable - // ================= // ==== ==== // ==== JVM ==== @@ -10,14 +8,6 @@ import scala.annotation.elidable object Platform { - /** - * Use the scalac `-Xelide-below` flag to switch from development- to production-mode. - */ - @elidable(elidable.ASSERTION) - @inline def DevMode: Boolean = - true - implicit def env: Env = Env.empty - } diff --git a/core/shared/src/main/scala-2/scalacss/internal/Macros.scala b/core/shared/src/main/scala-2/scalacss/internal/Macros.scala index 8d0d26d2..d365cf84 100644 --- a/core/shared/src/main/scala-2/scalacss/internal/Macros.scala +++ b/core/shared/src/main/scala-2/scalacss/internal/Macros.scala @@ -3,7 +3,6 @@ package scalacss.internal import java.util.regex.Pattern import scala.reflect.NameTransformer import scala.reflect.macros.blackbox.Context -import scalacss.defaults.Exports object Macros { @@ -181,21 +180,4 @@ object Macros { /** c"#fc6" provides a validates Color */ def c(args: Any*): Color = macro ColorLiteral.impl } - - // =================================================================================================================== - - trait DevOrProdDefaults { - def devOrProdDefaults: Exports with mutable.Settings = - macro Macros.devOrProdDefaults - } - - def devOrProdDefaults(c: Context): c.Expr[Exports with mutable.Settings] = { - import c.universe._ - c.Expr[Exports with mutable.Settings](q""" - if (_root_.scalacss.internal.Platform.DevMode) - _root_.scalacss.DevDefaults - else - _root_.scalacss.ProdDefaults - """) - } } diff --git a/core/shared/src/main/scala-3/scalacss/internal/Macros.scala b/core/shared/src/main/scala-3/scalacss/internal/Macros.scala index b41b5e7b..f2104ec6 100644 --- a/core/shared/src/main/scala-3/scalacss/internal/Macros.scala +++ b/core/shared/src/main/scala-3/scalacss/internal/Macros.scala @@ -1,10 +1,9 @@ package scalacss.internal import java.util.regex.Pattern -import scala.quoted._ -import scalacss.defaults.Exports import scala.language.`3.0` import scala.reflect.NameTransformer +import scala.quoted._ object Macros { @@ -239,14 +238,4 @@ object Macros { Option(errorResult).getOrElse(Left("Invalid colour: \"" + text0 + "\"")) } } - - // =================================================================================================================== - - trait DevOrProdDefaults { - inline final def devOrProdDefaults: Exports with mutable.Settings = - if (scalacss.internal.Platform.DevMode) - scalacss.DevDefaults - else - scalacss.ProdDefaults - } } diff --git a/core/shared/src/main/scala/scalacss/defaults/DefaultSettings.scala b/core/shared/src/main/scala/scalacss/defaults/DefaultSettings.scala index dcbccc3d..8e9091cd 100644 --- a/core/shared/src/main/scala/scalacss/defaults/DefaultSettings.scala +++ b/core/shared/src/main/scala/scalacss/defaults/DefaultSettings.scala @@ -29,10 +29,4 @@ object DefaultSettings { override implicit def cssStringRenderer : Renderer[String] = StringRenderer.formatTiny override implicit def cssComposition : Compose = Compose.trust } - - def devOrProd: Settings = - if (Platform.DevMode) - Dev - else - Prod } diff --git a/core/shared/src/main/scala/scalacss/package.scala b/core/shared/src/main/scala/scalacss/package.scala index 0e02920a..fb279631 100644 --- a/core/shared/src/main/scala/scalacss/package.scala +++ b/core/shared/src/main/scala/scalacss/package.scala @@ -1,6 +1,6 @@ package object scalacss extends scalacss.defaults.Exports - with internal.Macros.DevOrProdDefaults { + with internal.ModeMacros.DevOrProdDefaults { import defaults.{DefaultSettings, Exports} diff --git a/core/shared/src/test/scala/scalacss/full/DefaultsTest.scala b/core/shared/src/test/scala/scalacss/full/DefaultsTest.scala index c93a7b53..0c0028eb 100644 --- a/core/shared/src/test/scala/scalacss/full/DefaultsTest.scala +++ b/core/shared/src/test/scala/scalacss/full/DefaultsTest.scala @@ -2,17 +2,13 @@ package scalacss.full import japgolly.microlibs.testutil.TestUtil._ import scalacss._ -import scalacss.internal.Platform import utest._ object DefaultsTest extends TestSuite { override def tests = Tests { - "platform" - assert(Platform.DevMode) - "defaults" - assert(devOrProdDefaults eq DevDefaults) - - "dev" - Dev .test() - "prod" - Prod.test() + "dev" - Dev .test() + "prod" - Prod.test() } class SharedStyles(implicit reg: StyleSheet.Register) extends StyleSheet.Inline { diff --git a/doc/features/basics.md b/doc/features/basics.md index 1e560030..26a59718 100644 --- a/doc/features/basics.md +++ b/doc/features/basics.md @@ -6,7 +6,10 @@ The first thing you'll want to do is import some settings by using one of these import scalacss.DevDefaults._ // Always use dev settings import scalacss.ProdDefaults._ // Always use prod settings -// This will choose between dev/prod depending on your scalac `-Xelide-below` setting +// This will choose between dev/prod depending on: +// 1. `sbt -Dscalacss.mode=dev` or `sbt -Dscalacss.mode=prod` +// 2. Defaults to dev-mode unless in `fullOptJS` +// val CssSettings = scalacss.devOrProdDefaults import CssSettings._ ``` @@ -20,20 +23,6 @@ Error handling | Print to `stderr` | Ignore Class name generation | `.-`
Eg. `.MyStyles-outer` | `._a0`
`._a1`
etc CSS output | Pretty-print.
Indents, spaces, newlines. | Minified.
No whitespace. -`Defaults` will choose a mode to use based on its `devMode: Boolean` method -which uses -[elision](http://www.scala-lang.org/api/current/scala/annotation/elidable.html) -to determine production-mode. - -For instance, in my production SBT settings I generally have: - -```scala -// Prod settings -scalacOptions ++= Seq("-Xelide-below", "OFF") -``` - -_Note: `-Xelide-below OFF` doesn't turn eliding off, it means "elide everything". There must've been painting going on in the building with no windows open when that was named._ - Defaults aren't mandatory, you're free to customise as needed. (See [DefaultSettings.scala](https://github.com/japgolly/scalacss/blob/master/core/shared/src/main/scala/scalacss/defaults/DefaultSettings.scala).) diff --git a/doc/history/0.8.md b/doc/history/0.8.md index d0f2efa9..28adca32 100644 --- a/doc/history/0.8.md +++ b/doc/history/0.8.md @@ -1,4 +1,9 @@ # 0.8.0 ([commit log](https://github.com/japgolly/scalacss/compare/v0.7.0..v0.8.0-RC1)) * Add support for Scala 3 (`scalatags` module excepted) + +* Changed how `scalacss.devOrProdDefaults` works: + * You can now control it via `sbt -Dscalacss.mode=dev` or `sbt -Dscalacss.mode=prod` + * Defaults to dev-mode unless in `fullOptJS` + * Upgrade deps diff --git a/downstream-tests/build.sbt b/downstream-tests/build.sbt new file mode 100644 index 00000000..26cfcad3 --- /dev/null +++ b/downstream-tests/build.sbt @@ -0,0 +1,119 @@ +import java.util.Properties +import org.scalajs.linker.interface._ +import Dependencies._ +import Lib._ + +ThisBuild / organization := "scalacss-test" +ThisBuild / shellPrompt := ((s: State) => Project.extract(s).currentRef.project + "> ") + +def scalacCommonFlags: Seq[String] = Seq( + "-deprecation", + "-feature", + "-language:postfixOps", + "-language:implicitConversions", + "-language:higherKinds", + "-language:existentials", + "-unchecked", +) + +def scalac2Flags = Seq( + "-Wconf:msg=may.not.be.exhaustive:e", // Make non-exhaustive matches errors instead of warnings + "-Wconf:msg=Reference.to.uninitialized.value:e", // Make uninitialised value calls errors instead of warnings + "-Wunused:explicits", // Warn if an explicit parameter is unused. + "-Wunused:implicits", // Warn if an implicit parameter is unused. + "-Wunused:imports", // Warn if an import selector is not referenced. + "-Wunused:locals", // Warn if a local definition is unused. + "-Wunused:nowarn", // Warn if a @nowarn annotation does not suppress any warnings. + "-Wunused:patvars", // Warn if a variable bound in a pattern is unused. + "-Wunused:privates", // Warn if a private member is unused. + "-Xlint:adapted-args", // An argument list was modified to match the receiver. + "-Xlint:constant", // Evaluation of a constant arithmetic expression resulted in an error. + "-Xlint:delayedinit-select", // Selecting member of DelayedInit. + "-Xlint:deprecation", // Enable -deprecation and also check @deprecated annotations. + "-Xlint:eta-zero", // Usage `f` of parameterless `def f()` resulted in eta-expansion, not empty application `f()`. + "-Xlint:implicit-not-found", // Check @implicitNotFound and @implicitAmbiguous messages. + "-Xlint:inaccessible", // Warn about inaccessible types in method signatures. + "-Xlint:infer-any", // A type argument was inferred as Any. + "-Xlint:missing-interpolator", // A string literal appears to be missing an interpolator id. + "-Xlint:nonlocal-return", // A return statement used an exception for flow control. + "-Xlint:nullary-unit", // `def f: Unit` looks like an accessor; add parens to look side-effecting. + "-Xlint:option-implicit", // Option.apply used an implicit view. + "-Xlint:poly-implicit-overload", // Parameterized overloaded implicit methods are not visible as view bounds. + "-Xlint:private-shadow", // A private field (or class parameter) shadows a superclass field. + "-Xlint:stars-align", // In a pattern, a sequence wildcard `_*` should match all of a repeated parameter. + "-Xlint:valpattern", // Enable pattern checks in val definitions. + "-Xmixin-force-forwarders:false", // Only generate mixin forwarders required for program correctness. + "-Yjar-compression-level", "9", // compression level to use when writing jar files + "-Ymacro-annotations", // Enable support for macro annotations, formerly in macro paradise. + "-Ypatmat-exhaust-depth", "off", +) + +def scalac3Flags = Seq( + "-source:3.0-migration", +) + +def commonSettings: Project => Project = _ + .configure(preventPublication) + .settings( + scalaVersion := Ver.scala2, + crossScalaVersions := Seq(Ver.scala2, Ver.scala3), + scalacOptions ++= scalacCommonFlags, + scalacOptions ++= scalac2Flags.filter(_ => scalaVersion.value.startsWith("2")), + scalacOptions ++= scalac3Flags.filter(_ => scalaVersion.value.startsWith("3")), + ) + +lazy val cleanTestAll = taskKey[Unit]("cleanTestAll") + +lazy val root = Project("root", file(".")) + .configure(commonSettings) + .aggregate(jvm, js) + .settings( + cleanTestAll := ( + Def.sequential( + jvm / clean, + js / clean, + Test / compile, + jvm / Test / test, + js / Test / test, + ).value + ), + ) + +val useFullOptJS = System.getProperty("downstream_tests.fullOptJS") != null +val jsStage = if (useFullOptJS) FullOptStage else FastOptStage +val jsOptKey = if (useFullOptJS) fullOptJS else fastOptJS + +lazy val jvm = project + .in(file("jvm")) + .configure(commonSettings, utestSettings.jvm) + .settings( + libraryDependencies ++= Seq( + Dep.microlibsTestUtil.value % Test, + ), + Test / fork := true, + Test / javaOptions ++= + sys.props.iterator + .filter(_._1.matches("(downstream_tests|japgolly|scalacss).*")) + .map(x => s"-D${x._1}=${x._2}") + .toSeq, + Test / javaOptions += { + val jsFile = (js / Compile / jsOptKey).value + s"-Djs_file=${jsFile.data.absolutePath}" + }, + ) + +lazy val js = project + .in(file("js")) + .enablePlugins(ScalaJSPlugin) + .configure(commonSettings, utestSettings.js) + .settings( + scalaJSStage := jsStage, + libraryDependencies ++= { + val ver = version.value.stripSuffix("-SNAPSHOT") + "-SNAPSHOT" + Seq( + "com.github.japgolly.scalacss" %%% "core" % ver, + Dep.microlibsCompileTime.value % Test, + Dep.microlibsTestUtil.value % Test, + ) + }, + ) diff --git a/downstream-tests/js/src/main/scala/downstream/Main.scala b/downstream-tests/js/src/main/scala/downstream/Main.scala new file mode 100644 index 00000000..e74c5ca4 --- /dev/null +++ b/downstream-tests/js/src/main/scala/downstream/Main.scala @@ -0,0 +1,10 @@ +package downstream + +import scala.scalajs.js.annotation._ + +@JSExportTopLevel("MAIN") +object Main { + + @JSExport + val CssSettings = scalacss.devOrProdDefaults +} diff --git a/downstream-tests/js/src/test/scala/downstream/RuntimeTest.scala b/downstream-tests/js/src/test/scala/downstream/RuntimeTest.scala new file mode 100644 index 00000000..0ac93138 --- /dev/null +++ b/downstream-tests/js/src/test/scala/downstream/RuntimeTest.scala @@ -0,0 +1,27 @@ +package downstream + +import japgolly.microlibs.compiletime.CompileTimeInfo +import japgolly.microlibs.testutil.TestUtil._ +import scalacss._ +import utest._ + +object RuntimeTest extends TestSuite { + + val mode = CompileTimeInfo.sysProp("scalacss.mode") + val expectProd = CompileTimeInfo.sysProp("downstream_tests.expect.prod").contains("1") + + override def tests = Tests { + + "settings" - { + + "isDev" - assertEq( + actual = (Main.CssSettings: AnyRef) eq DevDefaults, + expect = !expectProd) + + "isProd" - assertEq( + actual = (Main.CssSettings: AnyRef) eq ProdDefaults, + expect = expectProd) + } + + } +} diff --git a/downstream-tests/jvm/src/test/scala/downstream/JsOutputTest.scala b/downstream-tests/jvm/src/test/scala/downstream/JsOutputTest.scala new file mode 100644 index 00000000..f2af2c8d --- /dev/null +++ b/downstream-tests/jvm/src/test/scala/downstream/JsOutputTest.scala @@ -0,0 +1,53 @@ +package downstream + +import japgolly.microlibs.testutil.TestUtil._ +import japgolly.microlibs.testutil.TestUtilInternals._ +import scala.Console._ +import sourcecode.Line +import utest._ + +object JsOutputTest extends TestSuite { + import Props._ + + private def fgrep(term: String): Unit = { + println(s"> fgrep '$term'") + content + .linesIterator + .zipWithIndex + .filter(_._1.contains(term)) + .map { case (s, l) => s"$GREEN$l:$RESET " + s.replace(term, MAGENTA_B + WHITE + term + RESET) } + .foreach(println) + } + + private def contentTest(substrToExpect: (String, Boolean)*)(implicit l: Line): Unit = { + System.out.flush() + System.err.flush() + var errors = List.empty[String] + for ((substr, expect) <- substrToExpect) { + val actual = content.contains(substr) + val pass = actual == expect + val result = if (pass) s"${GREEN}pass$RESET" else s"${RED_B}${WHITE}FAIL$RESET" + val should = if (expect) "should" else "shouldn't" + val strCol = if (expect) (GREEN + BRIGHT_GREEN) else BRIGHT_BLACK + println(s"[$result] JS $should contain $strCol$substr$RESET") + if (!pass) errors ::= s"JS $should contain $substr" + } + System.out.flush() + if (errors.nonEmpty) { + for ((substr, _) <- substrToExpect) + fgrep(substr) + fail(errors.sorted.mkString(", ")) + } + } + + override def tests = Tests { + + "size" - "%,d bytes".format(content.length) + + "defaults" - contentTest( + "DevDefaults" -> !expectProd, + "ProdDefaults" -> expectProd, + ) + + } +} diff --git a/downstream-tests/jvm/src/test/scala/downstream/Props.scala b/downstream-tests/jvm/src/test/scala/downstream/Props.scala new file mode 100644 index 00000000..bf2678eb --- /dev/null +++ b/downstream-tests/jvm/src/test/scala/downstream/Props.scala @@ -0,0 +1,34 @@ +package downstream + +import java.io.File +import scala.Console._ +import scala.io.Source + +object Props { + + private object Prop { + def get(property: String): Option[String] = { + val o = Option(System.getProperty(property)) + println(s"$CYAN$property$RESET = $YELLOW${o.getOrElse("")}$RESET") + o + } + + def get(property: String, default: String): String = + get(property).getOrElse(default) + + def need(property: String): String = + get(property).getOrElse(throw new RuntimeException("Property not defined: " + property)) + } + + val jsFilename = Prop.need("js_file") + + val fastOptJS = jsFilename.contains("fast") + + val content: String = { + val s = Source.fromFile(new File(jsFilename)) + try s.mkString finally s.close() + } + + val mode = Prop.get("scalacss.mode") + val expectProd = Prop.get("downstream_tests.expect.prod").contains("1") +} diff --git a/downstream-tests/project/Dependencies.scala b/downstream-tests/project/Dependencies.scala new file mode 120000 index 00000000..921dece9 --- /dev/null +++ b/downstream-tests/project/Dependencies.scala @@ -0,0 +1 @@ +../../project/Dependencies.scala \ No newline at end of file diff --git a/downstream-tests/project/Lib.scala b/downstream-tests/project/Lib.scala new file mode 120000 index 00000000..b6d6acc2 --- /dev/null +++ b/downstream-tests/project/Lib.scala @@ -0,0 +1 @@ +../../project/Lib.scala \ No newline at end of file diff --git a/downstream-tests/project/build.properties b/downstream-tests/project/build.properties new file mode 120000 index 00000000..5b4fb9be --- /dev/null +++ b/downstream-tests/project/build.properties @@ -0,0 +1 @@ +../../project/build.properties \ No newline at end of file diff --git a/downstream-tests/project/plugins.sbt b/downstream-tests/project/plugins.sbt new file mode 120000 index 00000000..ed05ef9c --- /dev/null +++ b/downstream-tests/project/plugins.sbt @@ -0,0 +1 @@ +../../project/plugins.sbt \ No newline at end of file diff --git a/downstream-tests/run b/downstream-tests/run new file mode 100755 index 00000000..c1491a11 --- /dev/null +++ b/downstream-tests/run @@ -0,0 +1,130 @@ +#!/bin/bash +set -euo pipefail +cd "$(dirname "$0")" || exit 1 + +# "Skip Scala 2" +skip2=skip2 + +# Tests +all_tests_values=( + "-Ddownstream_tests.expect.prod=0" + "-Ddownstream_tests.expect.prod=0 -Dscalacss.mode=dev" + "-Ddownstream_tests.expect.prod=1 -Dscalacss.mode=prod" + "-Ddownstream_tests.expect.prod=1 -Ddownstream_tests.fullOptJS" + "-Ddownstream_tests.expect.prod=0 -Ddownstream_tests.fullOptJS -Dscalacss.mode=dev" + "-Ddownstream_tests.expect.prod=1 -Ddownstream_tests.fullOptJS -Dscalacss.mode=prod" +) +all_tests_keys=() +declare -A all_tests +i=0 +for t in "${all_tests_values[@]}"; do + i=$((i+1)) + all_tests_keys+=($i) + all_tests[$i]="$t" +done + +# Parse options +dryrun=0 +filter=0 +parse=1 +while [ $parse -eq 1 ]; do + case "${1:-}" in + -h|-help|--help) + echo "Usage: $0 [-2 | -3] [-n] [...]" + echo + echo "Options:" + echo " -2 -- Only run Scala 2 tests." + echo " -3 -- Only run Scala 3 tests." + echo " -n -- Dry-run." + echo + echo "Tests:" + for n in "${all_tests_keys[@]}"; do + echo " $n) ${all_tests[$n]}" + done + echo + exit 0 + ;; + -2) + if [ $filter -eq 3 ]; then + filter=0 + else + filter=2 + fi + shift + ;; + -3) + if [ $filter -eq 2 ]; then + filter=0 + else + filter=3 + fi + shift + ;; + -n) + dryrun=1 + shift + ;; + *) + parse=0 + ;; + esac +done + +# Decide which tests to run +tests=("${all_tests_keys[@]}") +if [ $# -ne 0 ]; then + tests=("$@") +fi + +# Make sure all declared tests actually exist +all_good=1 +for t in "${tests[@]}"; do + if [ "${all_tests[$t]-x}" = x ]; then + echo "Invalid test: $t" >&2 + all_good=0 + fi +done +[ $all_good -eq 1 ] || exit 2 + +# Get Scala versions +scalaVer2="$(../bin/get_scala_version 2)" +scalaVer3="$(../bin/get_scala_version 3)" + +# Declare test runner +function run { + local args="$1" + local s2=1 + local s3=1 + if [[ "$args" == *"$skip2"* ]]; then + args="${args/$skip2/}" + s2=0 + fi + # echo "$filter/$s2/$s3 [$args]" + local task=cleanTestAll + local sbt_cmds=() + if [[ "$filter-$s3" =~ [03]-1 ]]; then + sbt_cmds+=( ++$scalaVer3 $task ) + fi + if [[ "$filter-$s2" =~ [02]-1 ]]; then + sbt_cmds+=( ++$scalaVer2 $task ) + fi + if [ "${#sbt_cmds[@]}" -eq 0 ]; then + echo "Skipped." + else + local cmd=(sbt --warn $args "${sbt_cmds[@]}") + echo "> ${cmd[@]}" + [ $dryrun -eq 1 ] || "${cmd[@]}" + fi +} + +# Run all tests +sep="===================================================================================================" +i=0 +for t in "${tests[@]}"; do + i=$((i+1)) + echo "$sep" + echo "[$i/${#tests[@]}] Test #$t" + run "${all_tests[$t]}" +done +echo "$sep" +echo "Done. All tests passed." diff --git a/downstream-tests/version.sbt b/downstream-tests/version.sbt new file mode 120000 index 00000000..868dd38f --- /dev/null +++ b/downstream-tests/version.sbt @@ -0,0 +1 @@ +../version.sbt \ No newline at end of file diff --git a/elision-test/shared/src/test/scala/scalacss/elision/DefaultsTest.scala b/elision-test/shared/src/test/scala/scalacss/elision/DefaultsTest.scala deleted file mode 100644 index b5cf0973..00000000 --- a/elision-test/shared/src/test/scala/scalacss/elision/DefaultsTest.scala +++ /dev/null @@ -1,34 +0,0 @@ -package scalacss.elision - -import scala.annotation.{elidable, nowarn} -import scalacss._ -import scalacss.internal.Platform -import utest._ - -object DefaultsTest extends TestSuite { - - def assert(ok: Boolean, msg: String) = - if (!ok) fail(msg) - - def fail(msg: String) = { - val e = new java.lang.AssertionError(msg) - e.setStackTrace(Array.empty) - throw e - } - - override def tests = Tests { - - "elision" - { - var on = false - - @elidable(elidable.ASSERTION) - @nowarn("cat=unused") - def test(): Unit = on = true - - assert(!on, "ELISION") - } - - "platform" - assert(!Platform.DevMode, "PLATFORM") - "defaults" - assert(devOrProdDefaults eq ProdDefaults, "DEFAULTS") - } -} diff --git a/project/Build.scala b/project/Build.scala index e94a33d6..51642de7 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -4,7 +4,6 @@ import com.jsuereth.sbtpgp.PgpKeys import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport._ import org.scalajs.jsdependencies.sbtplugin.JSDependenciesPlugin import org.scalajs.jsdependencies.sbtplugin.JSDependenciesPlugin.autoImport._ -import org.scalajs.jsenv.jsdomnodejs.JSDOMNodeJSEnv import org.scalajs.sbtplugin.ScalaJSPlugin import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ import sbtcrossproject.CrossPlugin.autoImport.{crossProject, _} @@ -59,38 +58,6 @@ object ScalaCssBuild { releaseTagComment := s"v${(ThisBuild / version).value}", releaseVcsSign := true)) - def definesMacros = ConfigureBoth( - _.settings( - scalacOptions ++= { - if (scalaVersion.value.startsWith("2")) - "-language:experimental.macros" :: Nil - else - Nil - }, - libraryDependencies ++= { - if (scalaVersion.value.startsWith("2")) - List( - Dep.scalaReflect.value, - Dep.scalaCompiler.value % Provided, - ) - else - Nil - }, - ) - ) - - def utestSettings = ConfigureBoth( - _.settings( - libraryDependencies ++= Seq( - Dep.utest.value % Test, - Dep.microlibsTestUtil.value % Test, - ), - testFrameworks := Seq(new TestFramework("utest.runner.Framework")), - ) - ).jsConfigure( - _.settings(jsEnv := new JSDOMNodeJSEnv) - ) - // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ lazy val root = @@ -101,12 +68,12 @@ object ScalaCssBuild { lazy val rootJVM = Project("JVM", file(".rootJVM")) .configure(commonSettings.jvm, preventPublication) - .aggregate(coreJVM, elisionTestJVM, extScalatagsJVM) + .aggregate(coreJVM, extScalatagsJVM) lazy val rootJS = Project("JS", file(".rootJS")) .configure(commonSettings.jvm, preventPublication) - .aggregate(coreJS, elisionTestJS, extScalatagsJS, extReact) + .aggregate(coreJS, extScalatagsJS, extReact) // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ @@ -134,17 +101,6 @@ object ScalaCssBuild { initialCommands := "import scalacss._", ) - lazy val elisionTestJVM = elisionTest.jvm - lazy val elisionTestJS = elisionTest.js - lazy val elisionTest = crossProject(JSPlatform, JVMPlatform) - .in(file("elision-test")) - .configureCross(commonSettings, utestSettings) - .configure(preventPublication, onlyScala2) - .dependsOn(core) - .settings( - scalacOptions ++= Seq("-Xelide-below", "OFF"), - ) - lazy val extScalatagsJVM = extScalatags.jvm lazy val extScalatagsJS = extScalatags.js lazy val extScalatags = crossProject(JSPlatform, JVMPlatform) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 1beddf9c..2a441fb7 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -25,21 +25,22 @@ object Dependencies { } object Dep { - val cats = Def.setting("org.typelevel" %%% "cats-core" % Ver.cats) - val microlibsTestUtil = Def.setting("com.github.japgolly.microlibs" %%% "test-util" % Ver.microlibs) - val nyayaGen = Def.setting("com.github.japgolly.nyaya" %%% "nyaya-gen" % Ver.nyaya) - val nyayaProp = Def.setting("com.github.japgolly.nyaya" %%% "nyaya-prop" % Ver.nyaya) - val nyayaTest = Def.setting("com.github.japgolly.nyaya" %%% "nyaya-test" % Ver.nyaya) - val scalaCompiler = Def.setting("org.scala-lang" % "scala-compiler" % scalaVersion.value) - val scalaJsDom = Def.setting("org.scala-js" %%% "scalajs-dom" % Ver.scalaJsDom cross CrossVersion.for3Use2_13) - val scalaJsReactCore = Def.setting("com.github.japgolly.scalajs-react" %%% "core" % Ver.scalaJsReact) - val scalaJsReactCoreGen = Def.setting("com.github.japgolly.scalajs-react" %%% "core-generic" % Ver.scalaJsReact) - val scalaJsReactDummy = Def.setting("com.github.japgolly.scalajs-react" %%% "util-dummy-defaults" % Ver.scalaJsReact) - val scalaJsReactTest = Def.setting("com.github.japgolly.scalajs-react" %%% "test" % Ver.scalaJsReact) - val scalaReflect = Def.setting("org.scala-lang" % "scala-reflect" % scalaVersion.value) - val scalatags = Def.setting("com.lihaoyi" %%% "scalatags" % Ver.scalatags cross CrossVersion.for3Use2_13) - val univEq = Def.setting("com.github.japgolly.univeq" %%% "univeq" % Ver.univEq) - val utest = Def.setting("com.lihaoyi" %%% "utest" % Ver.utest) + val cats = Def.setting("org.typelevel" %%% "cats-core" % Ver.cats) + val microlibsCompileTime = Def.setting("com.github.japgolly.microlibs" %%% "compile-time" % Ver.microlibs) + val microlibsTestUtil = Def.setting("com.github.japgolly.microlibs" %%% "test-util" % Ver.microlibs) + val nyayaGen = Def.setting("com.github.japgolly.nyaya" %%% "nyaya-gen" % Ver.nyaya) + val nyayaProp = Def.setting("com.github.japgolly.nyaya" %%% "nyaya-prop" % Ver.nyaya) + val nyayaTest = Def.setting("com.github.japgolly.nyaya" %%% "nyaya-test" % Ver.nyaya) + val scalaCompiler = Def.setting("org.scala-lang" % "scala-compiler" % scalaVersion.value) + val scalaJsDom = Def.setting("org.scala-js" %%% "scalajs-dom" % Ver.scalaJsDom cross CrossVersion.for3Use2_13) + val scalaJsReactCore = Def.setting("com.github.japgolly.scalajs-react" %%% "core" % Ver.scalaJsReact) + val scalaJsReactCoreGen = Def.setting("com.github.japgolly.scalajs-react" %%% "core-generic" % Ver.scalaJsReact) + val scalaJsReactDummy = Def.setting("com.github.japgolly.scalajs-react" %%% "util-dummy-defaults" % Ver.scalaJsReact) + val scalaJsReactTest = Def.setting("com.github.japgolly.scalajs-react" %%% "test" % Ver.scalaJsReact) + val scalaReflect = Def.setting("org.scala-lang" % "scala-reflect" % scalaVersion.value) + val scalatags = Def.setting("com.lihaoyi" %%% "scalatags" % Ver.scalatags cross CrossVersion.for3Use2_13) + val univEq = Def.setting("com.github.japgolly.univeq" %%% "univeq" % Ver.univEq) + val utest = Def.setting("com.lihaoyi" %%% "utest" % Ver.utest) } def addReactJsDependencies(scope: Configuration): Project => Project = diff --git a/project/Lib.scala b/project/Lib.scala index 2dad17f5..870bccbd 100644 --- a/project/Lib.scala +++ b/project/Lib.scala @@ -1,11 +1,13 @@ import sbt._ import Keys._ import com.jsuereth.sbtpgp.PgpKeys._ +import org.scalajs.jsenv.jsdomnodejs.JSDOMNodeJSEnv import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ import sbtcrossproject.CrossPlugin.autoImport._ import sbtcrossproject.CrossProject import scalajscrossproject.ScalaJSCrossPlugin.autoImport._ import xerial.sbt.Sonatype.autoImport._ +import Dependencies._ object Lib { type CPE = CrossProject => CrossProject @@ -88,4 +90,36 @@ object Lib { ) } + def definesMacros = ConfigureBoth( + _.settings( + scalacOptions ++= { + if (scalaVersion.value.startsWith("2")) + "-language:experimental.macros" :: Nil + else + Nil + }, + libraryDependencies ++= { + if (scalaVersion.value.startsWith("2")) + List( + Dep.scalaReflect.value, + Dep.scalaCompiler.value % Provided, + ) + else + Nil + }, + ) + ) + + def utestSettings = ConfigureBoth( + _.settings( + libraryDependencies ++= Seq( + Dep.utest.value % Test, + Dep.microlibsTestUtil.value % Test, + ), + testFrameworks := Seq(new TestFramework("utest.runner.Framework")), + ) + ).jsConfigure( + _.settings(jsEnv := new JSDOMNodeJSEnv) + ) + }