japgolly / scalacss

Super type-safe CSS for Scala and Scala.JS.
https://japgolly.github.io/scalacss/book
Apache License 2.0
339 stars 44 forks source link

Compile-time CSS generation #88

Open japgolly opened 8 years ago

japgolly commented 8 years ago

It would be fantastic to generate CSS at compile-time instead of runtime, mainly because it would reduce the output JS size to near zero, making #72 obsolete.

Example:

val abc = style(display.block)
// Macro turns ↑ into something like ↓
val abc = StyleA(…, PreGeneratedCss("display:block"))

It would have to handle autoprefixing too.

How/could it determine whether variables are static/dynamic to handle marginLeft(zzz.px * 2)?

Even if it just takes care of the easy, static cases, I wonder how large the JS size savings could be...

edmundnoble commented 7 years ago

Should pre-generated CSS be generated using dev- or prod- settings? How could one adjust the settings used at compile-time?

japgolly commented 7 years ago

When compiling for Scala.JS it's easy because there is a flag known at compile-time which specifies whether it's compiling for dev (on when fastOptJS) or prod (on when fullOptJS).

For JVM compilation there'd have to be a flag that you add to scalaOptions in SBT to indicate (or make inferable) dev vs prod. Normally for this case I use -Xelide-below OFF for prod mode which elides assertions and other debug-type checks.

japgolly commented 6 years ago

This idea's been in my head for a while now:

It would be great to support a macro that just accepts plain CSS as a string, parses it at compile-time, validates it, depending on macro args adds browser prefixes, saves it as a String (thus allowing 99% of this library to disappear at runtime), and then creates a StyleA as normal.

Grogs commented 6 years ago

There's a few libraries with CSS parsers out there that could be used.

I suppose the question is how easy would be be to take one of their ASTs and generate a StyleA.

Rapture HTML by Jon Pretty has one based on CSSParser: https://github.com/propensive/rapture/tree/dev/css/shared/src/main/scala/rapture/css

CssParse (a subproject of FastParse) by Li Haoyi: https://github.com/lihaoyi/fastparse/tree/master/cssparse/shared/src/main/scala/cssparse

japgolly commented 6 years ago

Ok, so it seems

ddworak commented 4 years ago

We use scalacss with compile-time / backend CSS rendering quite extensively in udash-css, while reusing most of the DSL. The output JS is just the classnames (safe to use though!). You can check https://guide.udash.io/frontend/templates or dive into the code https://github.com/UdashFramework/udash-core/tree/master/css, https://github.com/UdashFramework/udash-core/blob/master/macros/src/main/scala/io/udash/css/macros/StyleMacros.scala

japgolly commented 4 years ago

Oh very cool, thanks! How do you handle supporting both dev and prod modes? (Specifically, controlling whether the macros generate verbose or minimised CSS?)

Also, a few weeks ago I did a prototype for scalacss v2 and it worked :tada: I'm envisioning a future where you just paste normal CSS and it calls out to the JS ecosystem at compile-time to do things like validation, minification, auto-prefixing, etc. The idea of a Scala-based DSL for CSS has turned out to be a failure in my opinion.

1) It's too hard to type all possibilities for ~10% of CSS attributes. 2) the CSS world moves too fast and it's not feasible to try to keep up. 3) Personally I prototype my HTML & CSS using just HAML with embedded CSS and then once the prototypes are done, I have to copy the styles from CSS which the whole world understands to a special DSL which only Scala understands. My experience here might not be indicative of the whole ecosystem but it's just become the most efficient workflow for me.

I'm envisioning something like this:

    val header =
      css"""color: #3659e2;
           |margin-bottom: $size;
           |""".stripMargin

    val betterHeader =
      css"""$header
           |font-weight: bold;
           |""".stripMargin

It would still need some custom stuff to be able to support things like addClassNames but should be quite a small surface area.

What do you think?

japgolly commented 4 years ago

If you like my vision or have a similar one we could create it together, or if your vision is different we could create a shared base for both scalacss & udash if it makes sense.

ddworak commented 4 years ago

The dev and prod modes are pretty simple - if it's compile-time generation, sbt calls a runnable class which generates a CSS stylesheet like:

(backend / Compile / runMain).toTask(s" io.app.backend.css.CssRenderer $path false")

where false signifies whether that's prod or dev and can be set in SBT via dependencies from fastOpt and fullOpt respectively. Sometimes we even generate the stylesheets on startup and keep them in JVM memory - backend apps usually have some knowledge about the environment or a config flag anyway.

My experience with CSS in SJS applications (quite a few production cases, including one large enterprise app) is that the less is done on the frontend, the better, especially since the JS size is usually already an issue with Scala.js. We've managed to keep all of our styles as udash-css styles, which means they're pretty strict - and maybe it's for the best. Even complex cases like theming can be handled on the backend if you don't just render one stylesheet at compile time, but defer that to run time. There are some corner cases (I'd say < 5%) where we just had to override the style manually or copied some CSS, and I think in those cases your v2 would fit very nicely. Your approach with compile-time validation etc. may also work on the backend, given that Node.js / jsdom can handle these use-cases.

I'd be happy to take a look at the prototype next week or as soon as you can share. I'll discuss with the team and we can come up with some shared part for sure!