DEV Community

Mathieu Huot
Mathieu Huot

Posted on • Updated on

Processing sass with 11ty

Even though I've been using the solution described below with good success on POC and side projects, I believe it is not optimal in most situation. It's easier and probably more efficient to use the CLI as described here:

Anyhow, it's nice to know the options.

My Eleventy (11ty) project

I recently discovered 11ty and used the static site generator to build a business website. I like that 11ty allows working with different templates and mixing them too. In this site I used Nunjucks, Markdown and Sass. Now, 11ty doesn't have Sass pre-processing built-in. So I had to find my own way.

Turning scss into css

There are a few approches to solve this problem using filters or Gulp. I had used the Gulp way in another project, but for this one I wanted to do something else. So I wrote a Node script instead!

The code

The neat thing about 11ty is that it's written in JavaScript. So you can run in .eleventy.js config file any executable code that you like as long as it's JavaScript. So here's the function that I use on my project to pre-process scss files.

/config/sass-process.js

const sass = require('sass');
const fs = require('fs-extra');
const path = require('path');

module.exports = (scssPath, cssPath) => {
    //If cssPath directory doesn't exist...
    if(!fs.existsSync(path.dirname(cssPath))) {
        //Encapsulate rendered css from scssPath into result variable
        const result = sass.renderSync({file: scssPath});
        //Create cssPath directory recursively
        fs.mkdir(path.dirname(cssPath), {recursive: true})
        //Then write result css string to cssPath file
        .then(() => fs.writeFile(cssPath, result.css.toString()))
        .catch(error => console.error(error))
    }
    //Watch for changes to scssPath directory...
    fs.watch(path.dirname(scssPath), () => {
        console.log(`Watching ${path.dirname(scssPath)}...`);
        //Encapsulate rendered css from scssPath into watchResult variable
        const watchResult = sass.renderSync({file: scssPath});
        //Then write result css string to cssPath file
        fs.writeFile(cssPath, watchResult.css.toString())
        .catch(error => console.error(error))      
    });
}
Enter fullscreen mode Exit fullscreen mode

And then include that function in .eleventy.js file as so.

.eleventy.js

const sass = require('./config/sass-process');

module.exports = config => {
    //Watching for modificaions in style directory
    sass('./style/index.scss', './docs/style/index.css');
}
Enter fullscreen mode Exit fullscreen mode

Refactoring this code to make it asynchronous

The solution above works fine for my need. Thing is that I'm using the synchronous method to render sass sass.renderSync() and that code is blocking (very little, but still). There are mainstream solutions to render css from sass asynchronously like the node-sass library with its asynchronous render() method witch I can turn into a promise like so.

const sass = require('node-sass');
const cssResultFrom = file => {
    return new Promise((resolve, reject) => {
        sass.render({file: file}, (error, result) => {
            if(error) {
                reject(error);
            }
            resolve(result);
        });
    });
}
//Then to use it...
cssResultFrom(scssPath)
Enter fullscreen mode Exit fullscreen mode

Instead of writing my own Promise wrapper, I could use a module that does it for me like the fairly new node-sass-promise.

//node-sass-promise method
const sass = require('node-sass-promise');
//And to use it...
sass.render({file: scssPath})
Enter fullscreen mode Exit fullscreen mode

A little less flexible then writing my own wrapper, but less code to write! An exemple of code using node-sass-promise :

const sass = require('node-sass-promise');
const fs = require('fs-extra');
const path = require('path');

module.exports = (scssPath, cssPath) => {
    //If cssPath directory doesn't exist...
    if(!fs.existsSync(path.dirname(cssPath))) {
        //Create cssPath directory recursively
        fs.mkdir(path.dirname(cssPath), {recursive: true})
        //Render css from sass
        .then(() => sass.render({file: scssPath}))
        //Then write result css string to cssPath file
        .then(result => fs.writeFile(cssPath, result.css.toString()))
        .catch(error => console.error(error))
    }
    //Watch for changes to scssPath directory...
    fs.watch(path.dirname(scssPath), () => {
        //Render css from sass...
        sass.render({file: scssPath})
        //Then write result css string to cssPath file
        .then(result => fs.writeFile(cssPath, result.css.toString()))
        .catch(error => console.error(error))
        console.log(`Watching ${path.dirname(scssPath)}...`);      
    });
}
Enter fullscreen mode Exit fullscreen mode

My stack

Software version
OS Linux Mint 18.2 Sonya
Node 10.15.0
NPM 6.8.0
11ty 0.8.3

The end

Thanks for reading! :-)

Top comments (9)

Collapse
 
koitaki profile image
Chris Adams • Edited

Mathieu, this looks interesting, but not sure how to get this working?

I added the config/sass-process.js file.
And in my .eleventy.js I added:

• const sass = require('./config/sass-process');
• sass('./assets/sass/style.sass', './assets/css/main.css');
I added this in module.exports = function(eleventyConfig) {...}

I then run npx @11ty/eleventy --serve

It seems to recognise modifications to my style.sass file.
But nothing gets written to main.css.

Anything else I should be doing?

Thanks in advance if you see and respond to this.

Collapse
 
mathieuhuot profile image
Mathieu Huot • Edited

Hi Chris, both the sass and the fs-extra modules need to be installed as dependencies or dev dependencies to your project first. Otherwise, if you get an error when running eleventy, post it here and we'll try to make sense out of it.

Collapse
 
koitaki profile image
Chris Adams • Edited

Thanks Matthieu.

Worked out what I was doing wrong - I didn't quite understand the input and output paths, particularly that the output path needs to be pointing to the distribution path.

  1. I needed to point the SASS path to the source folder, ie. src.
  2. I needed to point the CSS path to the distribution output folder, ie. _site.

And then everything began working nice and easy with my Sass processing.

Many thanks again, appreciate the response!

Thread Thread
 
koitaki profile image
Chris Adams

Mathieu,

Here's a mysterious issue I encountered, but which somehow resolved itself.

I began getting a weird 'EISDIR, open' error.
ie. it seemed to be trying to open a directory, which it thought was non-existant but which did indeed exist, seemingly with adequate permissions.

[Error: EISDIR: illegal operation on a directory, open 'C:\Users\Owner\Projects\football\website\src\assets\css']

Also, it was pointing to the old, source, path that I originally had.

So I checked my paths to make sure I was now pointing to the output CSS folder under _site. Yup, that was definitely the case.

eleventy.js

const sass = require('./utils/sass-process');
sass('./src/assets/sass/main.scss', './_site/assets/css/main.css');

I restarted eleventy process a few times to see if that would get the right path to be read, and clear the error. But the problem persisted.

So, here's what 'seemed' to fix it. I added a console.log statement to the sass-process.js file in order to check the path of the CSS file being read in. No idea what it did, but to my neophyte eyes the change 'seemed to refresh' some cache, and thereafter the correct path was picked up.

config/sass-process.js

  : code, code, code
  :
    //Watch for changes to scssPath directory...
    fs.watch(path.dirname(scssPath), () => {
        console.log(`Watching ${path.dirname(scssPath)}...`);
        //Encapsulate rendered css from scssPath into watchResult variable
        const watchResult = sass.renderSync({file: scssPath});
        console.log("FILE TO WRITE TO: " + cssPath)  // <== ADDED THIS
  :
  : rest of code

Posted for what its worth...in case anyone else has a better explanation, or encounters this issue and it helps them solve it.

Cheers again!

Thread Thread
 
mathieuhuot profile image
Mathieu Huot • Edited

Hi Chris, happy to hear that you solve the problem! When I have time, I like to investigate on errors I'm getting even when I fix things. It helps me to refine my understanding. I had never heard of such error so I search a bit and I found this thread on Stack Overflow. Here's the essential part:

EISDIR stands for "Error, Is Directory". This means that NPM is trying to do something to a file but it is a directory.

In your case, It could have been a cache problem and by updating sass-process.js you invalidate the cache. Hard to say, for what I know. I think it's unlikely that logging to the console would fix this error directly, more as a side effect, I would say.

Thanks for sharing!

Have a good day! 🤗

Thread Thread
 
koitaki profile image
Chris Adams

I take your point Mathieu, that it worked after a console.log was a bit wierd.

Here's another FWIW post, in case someone of my level comes along with the same issue.

I host on Netlify, and I noticed my site no longer deployed. Eventually guessed that Netlify being a static site host, the sass-process call for modifying files might be the problem.

Seemed a simple fix: creating a local ELEVENTY_ENV variable + one on Netlify, and adding an environments toggle. Eg.

eleventy.js

const env = process.env.ELEVENTY_ENV;
  if (env == "development") {
    sass('./src/assets/sass/main.scss', './_site/assets/css/main.css');
  }

That worked, so all good again 🙂

Thread Thread
 
mathieuhuot profile image
Mathieu Huot

Hi Chris, I happen to host my sites on Netlify too! I think I know why you encountered problems with deployment. The sass-process script contains a watch method which is a never ending process (it will watch for changes in target files forever or until you shutdown the script or it encounters an error). Netlify deploy process will build your site with the provided command exactly like you would on your local machine. If your build process never ends (which is the case if it includes a script with a watch method) it will timeout Netlify deploy process. So you'r right, the watch method contained in the sass-process script has to be remove from production (and kept for development only 🙂). Here's how I do it on my project:

sass-process.js

[...]
//If cssPath directory doesn't exist or ELEVENTY_ENV environment variable is set to prod... 
if(!fs.existsSync(path.dirname(cssPath)) || process.env.ELEVENTY_ENV === 'prod') {
    //Create cssPath directory recursively
    fs.mkdir(path.dirname(cssPath), {recursive: true})
    //The .then method will return a promise with the result 
    .then(() => processSass(scssPath, cssPath))
    //Then write result css string to cssPath file
    .then(result => fs.writeFile(cssPath, result.css.toString()))
    .catch(error => console.error(error));
}
//In development environment (default)
if(process.env.ELEVENTY_ENV === 'dev') {
    //Watch for changes to scssPath directory...
    fs.watch(path.dirname(scssPath), () => {
        //Return css as buffer from scssPath file...
        processSass(scssPath, cssPath)
        //Then turn css buffer to string and write result to cssPath file
        .then(result => fs.writeFile(cssPath, result.css.toString()))
        .catch(error => console.error(error));
    });
}

So here, if ELEVENTY_ENV is set to 'production' (or 'prod' in my case) the script will process sass and write the result to the target file, but it will skip the watch part! In this case, I would still be able to run the script in .eleventy.js without any conditions and it wouldn't prevent Netlify deploy from completing (with the minor difference that it would process sass on deploy). I'm only guessing here, but the fact that not calling your sass() function at all during deployment still produced a 'styled' site means that you probably include your output directory (_site) in your git repo or the result would be a site with no css. Anyhow, it's working for you and your project and really that's the only thing that matters in the end. Here's a small open source project I'm working on right now. It contains the sass-process script (in the config folder) and it's hosted on Netlify.

Collapse
 
gabbersepp profile image
Josef Biehler

Thanks! Exactly what I was looking for!

Collapse
 
mathieuhuot profile image
Mathieu Huot

Nice 👍