Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
JavaScript CSS PostCSS Source-to-Source Compilers

The JavaScript CSS Mess

CSS is simple: you have a .css file, you include it in your HTML, and all the styling gets applied to it.

However: CSS evolves rapidly and browsers often can’t keep up..

Autoprefixer

For example the current box-shadow property was first implemented only in Chrome, and that property was called -webkit-box-shadow. This is a convention that browser vendors follow when they implement their own browser-specific CSS properties.

Firefox-specific CSS properties are prefixed with -moz.

Similarly the CSS Grid was first implemented by Internet Explorer, and so its prefix was -ms-grid instead of just grid. https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Grid_Layout/CSS_Grid_and_Progressive_Enhancement#the_internet_explorer_and_edge_situation

Once these properties are standardized, then they lose the browser-specific extension.

So we now have box-shadow and display: grid. These are now supported in all modern browsers, without vendor specific prefix.

However, lots of users still use older browsers that need these prefixes. For example large corporates don’t upgrade their browsers frequently, and many of them might be stuck in old versions of IE. If we want our websites to work well everywhere, we have to now include the latest properties as well as their browser-vendor-specific versions.

So we have to keep writing all these non-standard browser-specific CSS property names for a long time. And keeping up with that is difficult.

Here is where Autoprefixer comes. Visit https://autoprefixer.github.io/. You can see that it parses our input CSS and emits corresponding “auto-prefixed” CSS.

Thus we don’t have to think or worry about browser prefixes. Just write the most modern CSS we need and autoprefixer will take care of it.

This is similar to Babel: Babel lets us write the most modern JavaScript, and it will compile that code into an older version of JS so it runs on all older browsers.

Both these tools fall in the category of “source-to-source” compilers. Input is a source file, output is a slightly transformed source file.

Common Infrastructure for Source-to-Source compilation

Autoprefixing is just one instance of a place where we need to do transform existing CSS.

The SASS compiler for example is another case. SASS allows us to define variables and functions inside CSS, but compiles all that down into plain CSS.

Or what if you want to do your own programming in CSS?

All that requires some common infra:

  • the ability to read and parse the CSS into an AST.
  • iterate over this AST and transform it into a new valid shape
  • output it back into plain CSS

This is what PostCSS does!

It gives you a pipeline into which you can plug-in your custom logic. Autoprefixer is such a plugin.

So instead of every new CSS tooling author creating their own AST parser and serializer, we can simply use the common PostCSS infra and focus on just the portions we want.

That’s same with Babel and Webpack! We can create plugins that hook into its infra for our own custom code.

See the documentation on “Writing Plugins for PostCSS” to learn more about this - https://github.com/postcss/postcss/blob/main/docs/writing-a-plugin.md

So by now we know the relation between PostCSS and Autoprefixer.

TailwindCSS is a PostCSS plugin just like Autoprefixer.

create-react-app and CRACO

To create a React front-end application, all we need is an empty directory, a package.json which includes the React npm libraries, Babel, and a webpack configuration that applies Babel to all our JS.

It is quite a bit for a beginner, but you get used to it over time.

create-react-app was created to make this process easier. It comes with a set of preconfigured options for all this webpack and babel and postcss stuff so we don’t have to keep writing config for every new project.

It works quite well also.

However, we can’t modify any of those settings that create-react-app comes with by default. For that the option is to “eject” the config. Aka: create-react-app will spit out all the webpack and babel configs that it internally uses, and removes itself from the scene. https://create-react-app.dev/docs/available-scripts/#npm-run-eject

But that’s a huge config. This is the problem with “prepackaged” solutions. Instead of learning the building blocks, you now have to mess with this pile of code.

Bandaids on top of bandaids.

At first look, CRACO is such a band-aid. This is the first time I’m seeing it so take it with a pinch of salt. CRACO lets us change create-react-app’s configuration without “ejecting” the config.

We need CRACO here, as per the Tailwind config, because create-react-app does not allow us to modify its internal PostCSS configuration, which Tailwind needs to operate.

So bandaid on top of bandaid on top of bandaid.

What I do

I start with a blank directory. Then setup my toolchain from scratch - https://reactjs.org/docs/create-a-new-react-app.html#creating-a-toolchain-from-scratch

The ordering is this:

  • blank directory
  • install webpack and make it generate a bundled JS output
  • install Babel, integrate it with webpack (steps are in Babel homepage)
  • install React libraries, make it work
  • install PostCSS and integrate it into the webpack build pipeline.
  • install TailwindCSS as a PostCSS plugin.

This will take you a day or more to get all working. But once you do it, you know the entire JS build pipeline stack top to bottom.

Then you can go ahead and start using create-react-app because you know exactly what’s going on underneath.

Oh, but what is Webpack?

Imagine you have:

index.js 
	 import HomePage from ‘./HomePage.js’
	 
HomePage.js
	 import {addDate} from ‘./utils.js’
	 
utils.js
	 ..

And in the HTML file, you want to call index.js.

So we want to bundle all these three files together into a final build output. That is what Webpack does. It traverses through each of these import statements, and brings those files into the same final build output.

That is the “bundling” aspect.

For Webpack to figure out this dependency tree, it has to parse the JS, and iterate through all the JS files in the project tree. That’s some common infra that can be useful to other source-to-source JS tools.. like Babel!

So.. Webpack lets us put these plugins into its build pipeline so those tools can utilize the common infrastructure.

Bandaids, on top of bandaids, on top of a mountain of bandaids.

That’s the JavaScript story. People complain about it. I will not recommend it. Yes it is bad. No, not just bad, it is terrible.

But it lets us ship amazing full-featured software that works in all sorts of devices, in all sorts of display resolutions, across networks all over the world.

JS is like that because history is not a well-ordered mathematical equation. History is the result of people doing stuff, most things failing, some things working out.. and often in completely different manner than what people thought they would.. and other people coming along and patching things together and cobbling things on top of things.

That’s how it works. No one sat and designed a system. It evolved haphazardly. That’s the only way it could’ve been done because when JS got started, nobody knew what the future was going to be. To design a perfect system, one needs to know how the future is going to unfold and that is still an unsolved problem.

So we have what we have. We should enjoy it, make good stuff with it, and bucket about it every once in a while. Ultimately we can try making better tools, write better documentation, we can try to simplify the stack in our own ways. There is so much opportunity out there!

@Deep-Codes
Copy link

Deep-Codes commented Mar 3, 2021

Thanks a ton! ⚡️

@arnabsen1729
Copy link

arnabsen1729 commented Mar 3, 2021

This actually clears so many of my doubts and misconceptions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment