aesthetic-suite / framework

🎨 Aesthetic is an end-to-end multi-platform styling framework that offers a strict design system, robust atomic CSS-in-JS engine, a structural style sheet specification (SSS), a low-runtime solution, and much more!
https://aestheticsuite.dev
MIT License
203 stars 5 forks source link

[WIP] new: Add low-runtime Babel plugin. #143

Closed milesj closed 1 year ago

milesj commented 3 years ago

Summary

This PR adds a low-runtime Babel plugin that will attempt to extract, evaluate, and compile CSS expressions so that they're not triggered at runtime. The result of this will be passed to a bundler like Webpack and served as raw CSS files.

Do note that this is a "low-runtime" library and not a "zero-runtime" library. From what I know (at this time), it's not possible to do zero-runtime for the following reasons:

How to extract CSS out?

This is the problem I'm trying to solve. Say an application has 5 themes, each with a different color scheme, contrast level, and design token combinations. We then have this very simple style sheet:

import { createComponentStyles } from '@aesthetic/react';

const styleSheet = createComponentStyles((css) => ({
  element: {
    color: css.tokens.palette.danger.bg.base,
    textAlign: 'left',
  }
}));

Seems pretty simple right? I wish... The css.tokens.palette.danger.bg.base variable is a CSS hexcode that will be different between every theme, which results in a different class name for each theme. How would this be transformed?

import { createComponentStyles } from '@aesthetic/react';

const styleSheet = createComponentStyles((css) => ({
  element: {
    color: {
      theme1: 'a',
      theme2: 'b',
      theme3: 'c',
      theme4: 'd',
      theme5: 'e',
    },
    textAlign: {
      default: 'f',
    },
  }
}));

In the example above, we pass an object that maps the theme name to the generated class name, for every property. This solves that problem, but now we have another problem, as this output would greatly increase the bundle size. Maybe we can flatten and remove the properties?

import { createComponentStyles } from '@aesthetic/react';

const styleSheet = createComponentStyles((css) => ({
  element: {
    theme1: 'a f',
    theme2: 'b f',
    theme3: 'c f',
    theme4: 'd f',
    theme5: 'e f',
  }
}));

This reduces the file size by quite a bit, but it could still potentially be too large. However, there are still other problems! The above example assumes the application is LTR only, but what if they support LTR and RTL? This means we need to convert text-align for both directions, so how would that look???

import { createComponentStyles } from '@aesthetic/react';

const styleSheet = createComponentStyles((css) => ({
  element: {
    // [ltr, rtl]
    theme1: ['a f', 'a g'],
    theme2: ['b f', 'b g'],
    theme3: ['c f', 'c g'],
    theme4: ['d f', 'd g'],
    theme5: ['e f', 'e g'],
  }
}));

Now we're back to this transformation becoming too large. But it still doesn't solve all problems!!! Because we still need to take vendor prefixes into account (if it's enabled)! And the default unit suffix (if not px)! This problem will only grow over time when new features are added to the framework.

The only solution I can think of at the moment is supporting an array of every permutation. We can optimize the implementation a bit by using a function + bitmasks that handle the heavy lifting. For example:

import { createComponentStyles } from '@aesthetic/react';
import { c } from '@aesthetic/core/runtime';

const styleSheet = createComponentStyles((css) => ({
  element: [
    // theme1, ltr
    c('a f', 'theme1'),
    // theme1, rtl
    c('a g', 'theme1', 1),
    // theme1, ltr, vendor prefixes
    c('a h', 'theme1', 1),
    // theme1, rtl, vendor prefixes
    c('a i', 'theme1', 2)
    // ...
    // lots of permutations
  ],
}));

This is still not ideal, but the path forward is not easy...

Screenshots

Checklist