Snowpack + Eleventy + Sass + PostCSS

10th Jun 2020

I was able to create an Eleventy + Snowpack + Sass + PostCSS setup that works pretty well. I want to share this setup with you in this article.

File structure

There are basically two ways to structure source code:

  1. Put everything you write inside an src folder
  2. Split up the src folder up into two: views and public.

I usually use the first method. But I decided to try the second method this time round. I tried it because Snowpack makes it easy to mount folders onto a development server.

I wanted to split up src into views and static because I can use them for specific responsibilities:

  • views is used for everything 11ty-related
  • static is used for all static assets (CSS, JS, Images, etc).

Note: We’ll use CSS here for now. I’ll add Sass later.

project
  |- views
    |- _includes
    |- index.njk
    |- Other 11ty related files
  |- static
    |- css
      |- styles.css
    |- images
      |- image.png
    |-js
      |- main.js

Eleventy config

First, you need to install Eleventy.

npm install @11ty/eleventy --save-dev

For this file structure to work, I configured Eleventy with an .eleventy.js file.

Here, I set input to views and output to dist.

module.exports = function (eleventyConfig) {
  return {
    dir: {
      input: "views",
      output: "dist",
    },
  }
}

Snowpack Dev Server

If you don’t know about Snowpack, I suggest reading Snowpack’s intro blog post and my notes on Snowpack before you continue with the rest of this article.

First, make sure to install Snowpack.

npm install snowpack --save-dev

We can run commands for Snowpack inside a snowpack.config.json file. Here, I made dist the root of Snowpack’s server with a mount script.

// snowpack.config.json
"scripts": {
  "mount:eleventy": "mount dist --to /",
}

I also had to mount the static folder so I could access my CSS, images, and other assets.

"scripts": {
  "mount:eleventy": "mount dist --to /",
  "mount:static": "mount static --to /",
}

I can access static assets from my Eleventy files like this:

<!-- CSS -->
<link rel="stylesheet" href="/css/styles.css">

<!-- JavaScript -->
<script src="/js/main.js"></script>

<!-- Images -->
<img src="/images/image.png" />

The next step is to run eleventy. Here, we can use a run script. We also watch eleventy with a ::watch modifier.

"scripts": {
  "mount:eleventy": "mount dist --to /",
  "mount:static": "mount static --to /",
  "run:eleventy": "eleventy",
  "run:eleventy::watch": "$1 --watch",
}

Let’s run snowpack:

npx snowpack dev

You should see this log. A browser will also open up. This browser directs you to http://localhost:8080 (which is where the Snowpack’s dev server is hosted).

I eventually set snowpack dev as an npm script so I can use npm run dev. npm run dev is easier for me to write because I have an alias for it.

// package.json
"scripts": {
  "dev": "snowpack dev"
}

Adjusting the dev server

I set four options for the dev server:

"devOptions": {
  "port": 3000,
  "open": "none",
  "bundle": false,
  "out": "dist"
}
  • port because I prefer using :3000 over :8080.
  • open to none because I don’t want browsers to open a tab.
  • bundle to false to silence the bundle log.
  • out to dist because I want my final build to be in a dist folder. (This appears when you run snowpack build).

Try running snowpack build. You should see a dist. This dist folder should contain your 11ty-generated files + static files.

npx snowpack build
Snowpack Dist folder.

Again, I used an npm script. I set this to build.

"scripts": {
  "build": "snowpack build"
}

JavaScript

Snowpack lets you import packages from npm out of the box. Here, you need to make sure you save your package as a dependency.

For example, let’s install a canvas-confetti package that creates confetti (which comes bundled in Snowpack’s examples 😃).

npm install canvas-confetti --save

We can import this canvas-confetti package in the JavaScript file.

// main.js
import confetti from 'canvas-confetti'

// Adds a canvas element to the HTML
const canvas = document.createElement('canvas')
canvas.style.width = '100vw';
canvas.style.height = '100vh';
document.body.appendChild(canvas)

// Makes confetti
confetti.create(canvas, {
  resize: true,
  useWorker: true
})({ particleCount: 200, spread: 200 })

When you run snowpack build, you should see a web_modules folder. This web_modules folder contains dependencies we imported.

Snowpack JavaScript.

Snowpack will also point your import statements to this /web_modules folder automatically.

// In dist/js/main.js
import confetti from '/web_modules/canvas-confetti'

Sass + PostCSS

Snowpack has a weird quirk: Scripts you write in snowpack.config.json runs asynchronously. You cannot make one script depend on another.

This means we cannot use Sass, then PostCSS inside with run scripts. This is a huge problem.

The best solution I found is to run Sass via PostCSS. We can do this with postcss-sass by Jonathan Neal. To use postcss-sass, we need to install three modules:

  1. postcss-cli
  2. postcss-scss
  3. @csstools/postcss-sass

Here’s the command to install all three modules:

npm install postcss-cli postcss-scss @csstools/postcss-sass --save-dev

Configuring PostCSS

We can configure PostCSS with a postcss.config.js file. I set syntax to postcss-scss to tell PostCSS to expect a .scss file. Then, I used postcss-sass plugin to do the Sass part.

I also added an includePaths option so I can import libraries from the node_modules.

module.exports = {
  syntax: 'postcss-scss',
  plugins: [
    require('@csstools/postcss-sass')({
      includePaths: ['./node_modules']
    }),
  ]
}

I also added Autoprefixer to the build.

npm install autoprefixer --save-dev
module.exports = {
  syntax: 'postcss-scss',
  plugins: [
    require('@csstools/postcss-sass')({
      includePaths: ['./node_modules']
    }),
    require('autoprefixer')
  ]
}

Configuring Snowpack for Sass + PostCSS

Since I want static to contain source files, I changed the static/css to static/scss.

project
  |- static
    |- scss
      |- styles.scss
  |- ...

The run script I came with looks like this:

"scripts": {
  "run:css": "postcss static/scss/**/[^_]*.scss --dir dist/css --ext css",
  "run:css::watch": "$1 --watch"
},

It does five things:

  1. Grab all .scss files (except for partials)
  2. Transform them with postcss
  3. Output the files inside dist/css
  4. Change the file extension to .css
  5. Watch .scss files for changes (when using snowpack dev).

This process works pretty well, but I’m concerned about two things.

Postcss-sass may be outdated

Dart Sass and Node Sass are actively worked on. Last I checked, Node Sass was updated 20 days ago. Jonathan’s postcss-sass plugin, however, was updated 14 months ago.

There’s a considerable difference in Sass versions as well. (10 minor versions when I checked).

I’m concerned update delays in this package. I’ll be super happy if Jonathan keeps the package up to date, but we can’t be sure this happens.

I’d prefer using the traditional Sass -> PostCSS approach if possible… but it’s impossible to set this up with Snowpack (unless I’m getting it wrong).

Problems with Sourcemaps

Sourcemaps generated with the above process doesn’t link back to .scss files. It links to the generated .css file… so the sourcemaps aren’t very useful.

I can’t find a solution for this after hours of Googling so I gave up.

Cleanup

I like to clean up generated files before I run snowpack dev or snowpack build. I do this with del.

npm install del-cli --save-dev
"scripts": {
  "clean": "del dist",
  "dev": "npm run clean && snowpack dev",
  "build": "npm run clean && snowpack build"
}

Deploying

I can now build a site with npm run build. The generated dist folder is ready for deploy.

The deploy process is already complete if I use Netlify to serve my sites. It does further optimisations and cache-busting for me automatically.

But it’s another story if I don’t use Netlify.

Minification and Optimization

Snowpack has build scripts that run after run scripts.I imagine we can run cssnano to minify CSS and terser to minify JavaScript. I didn’t test them out since I don’t have the time to do it yet.

I suppose we can also do image optimisation with imagemin… So there are no real problems with optimisation.

The real problem is cache-busting.

Cache-busting

We can cache bust in three ways:

  • Using query strings (like style.css?v=3.4.1)
  • Adding a hash to the file name (like style.232124.css)
  • Using ETags

CSS Tricks has a complete article that talks about each of these strategies if you want to find out more.

Traditionally, the best way to cache bust is to add a hash to the file. This hash is based on file contents, so it changes when the contents change. Webpack uses this method to generate hashes.

I also used the hash method to generate hashes for this blog.

However, to use a hash with a static site generator like Eleventy, you need to:

  1. Compile the CSS and JavaScript files first.
  2. Save the hash details somewhere
  3. Feed the hash details into Eleventy.
  4. Let Eleventy generate the files.

Which means we have to use Snowpack as part of a npm build process or gulp build script. It feels like I’m mixing too many tools together and I don’t like this. Maybe I’m missing something here.

Second problem with caching: How do we best cache ESM files? I haven’t researched about this process so I don’t have answers yet.

Anyway, cache-busting is a problem I still struggle with in a Snowpack environment. If you have answers, please let me know how you handle it! My eyes are open :)

If you enjoyed this article, please tell a friend about it! Share it on Twitter. If you spot a typo, I’d appreciate if you can correct it on GitHub. Thank you!

Hold on while i sign you up…

🤗
Woohoo! You’re in!
Now, hold on while I redirect you.