Eleventy with Theo, Sass and PostCSS
Eleventy is very flexible on how you want to process a file. In the recent post “How Virga processes PostCSS”, I explained how I use JavaScript as Eleventy templates to process PostCSS.
With the same idea with little changes, you can do more complex process.
I’ll demonstrate this in this post with using Theo, Sass and PostCSS(again).
Before I get into the details, I believe I owe an explanation on Theo and Sass.
If you want to see the example repo I have created for this post, It’s at 11ty-theo-sass-postcss-example.
What is Theo? #
Theo is an abstraction for transforming and formatting Design Tokens which is open sourced by Salesforce.
In short, Theo transforms yaml files which define Design Tokens into various formats like sass variables
.
This is how Design Tokens are defined in Lightning Design System of Salesforce.
Design Tokens now have own W3C Community Group and I’m really excited about this.
Dart Sass, not Node Sass #
Have you noticed that Sass has changed its primary implementation of Sass?
Sass was originally written in Ruby and then LibSass is a C/C++ port of the Sass engine. LibSass is just a library, so it has been used with a wrapper like Node Sass.
Now Sass is written in Dart, and it is the primary implementation of Sass which means it gets new features before any other implementation.
In this post, when I mention Sass, I’m taking about Dart Sass.
You can install Dart Sass via npm
.
npm install sass (--save-deps|--save)
Importing Theo’s yaml
into Sass #
Theo provides a command line tool to transform yaml
files into various format.
theo tokens.yml --format map.scss --dest .
Above command means tokens.yml
file will be transformed into map.scss
format and output file in .
(which is same directly as the command executed).
The output file, in this case will be tokens.map.scss
which contains something like this:
$tokens-map: (
'${props}': (${value}),
);
However, Sass has importer
option which can be utilized to do some thing like this in Sass file.
@import './tokens/token.yml';
importer
executes JavaScript function(s) when a @use
rule or an @import
rule is encountered.
That means Sass can run theo
in importer
function to do the transformation.
/* Stolen from https://basalt.io/blog/theo-design-tokens-using-node-sass-importer-for-any-build-method */
const {resolve, parse} = require('path');
const theo = require('theo');
/**
* Theo Design Token Sass Importer
* Import scss variables from Yaml files directly
* @param {string} url - path to the file passed into import statement, i.e. `@import "design-tokens.yml";`
* @param {string} prev - path to the file the import statement is located at, useful for calculating relative paths
* @link <https://www.npmjs.com/package/theo>
*/
function theoImporter(url, prev) {
// If the imported file doesn't end in `.yml` or `.yaml`, then `return null` early to tell node-sass that we're not going to do anything. It'll go on to the next function or just try to handle the import itself.
if (!/\.ya?ml$/.test(url)) return null;
// `prev` is the where it was imported from, we just want the directory it is in
const prevDirectory = parse(prev).dir;
// imports are almost always relative, so let's figure out how to get to there from here so we end up with an absolute url
const designTokenFilePath = resolve(prevDirectory, url);
const theoConverted = theo.convertSync({
transform: {
type: 'web',
file: designTokenFilePath,
},
format: {
// This can be any format Theo supports (or your own custom one!) <https://www.npmjs.com/package/theo#formats>
// I'm choosing map.scss
type: 'map.scss',
},
});
return {
contents: theoConverted,
};
}
module.exports = theoImporter;
And you can “load” this importer
function like:
const theoImporter = require('../../_theo-importer');
const sassResult = sass.renderSync({
file: rawFilepath,
outputStyle: 'expanded',
importer: [
theoImporter, // Here
],
});
With this importer
(and JavaScript API from Sass), I can use same technique from “How Virga processes PostCSS”.
Eleventy JavaScript Templates is powerful #
So to recap, what I want to do is:
importer
function option in Sass will process Theo- Sass will process
scss
to CSS - CSS will be processed with Autoprefixer and cssnano
All this can be done with Eleventy JavaScript Templates.
I’ll explain interesting parts.
const sassResult = sass
.renderSync({
file: rawFilepath,
outputStyle: 'expanded',
importer: [theoImporter],
})
.css.toString();
As I showed few lines above, this is Sass JavaScript API which use importer
option.
module.exports = class {
data() {
return {
permalink: `_css/${fileName.css}`,
};
}
render(data) {
return postcss(plugins)
.process(sassResult, {
from: rawFilepath,
})
.then((result) => result.css);
}
};
And this is Eleventy JavaScript Templates part.
I was very happy since it looked like easy thing to do.
But, sass
command line doesn’t come with importer
option #
node-sass
cli does have importer
option but Dart Sass doesn’t.
# This works
node-sass --importer ./theo-importer.js src/_sass/main.scss src/_includes/assets/styles/main.css
# But this IS NOT
sass --importer ./theo-importer.js src/_sass/main.scss src/_includes/assets/styles/main.css
importater
has been “Experimental” in node-sass
. I’m not sure if Dart Sass will have this available in the future.
So I’ve decided to reuse sass.11ty.js
to run from command line.
You can do node src/_sass/sass.11ty.js
to invoke it.
And since I only run this from command line in production, I’ve added this lines of codes.
if (process.env.ELEVENTY_ENV === 'production') {
console.log(`Writing CSS to ${prodDistpath}...`);
fs.writeFileSync(prodDistpath, sassResult);
}
Well, actually you’d need fs.writeFileSync(prodDistpath, sassResult);
this line to make it work.postcss
CLI will pickup the file generated from above and do Autoprefixer and cssnano.
How to run the example? #
I have set up an example files on GitHub.
https://github.com/frontendweekly/11ty-theo-sass-postcss-example
Fork this repo and clone it into your local machine then:
npm install
npm start
for watching file changes and run build and server. CSS file will be atdist/_css/main.css
- (Quit the server)
npm run build:prpduction
will generate minified CSS file insrc/_includes/assets/styles/main.css
I’m assuming you have node
installed on your machine.