Mastodon hachyterm.io

I built my personal website in Eleventy a few years ago.

For this, I followed a course called Learn Eleventy From Scratch. A couple of years ago, Eleventy made a big splash as a simple JavaScript framework to build static websites.
Nowadays, Astro is probably more popular, but I have been happy with Eleventy so far.

My Eleventy site has a Gulp pipeline to optimize images, get fonts, and transform sass.

I used Common JS syntax:

const { src, dest } = require('gulp')

const images = () => {
  // more code
}

module.exports = images

One of the packages in the pipeline is gulp-imagemin, which helps to minify PNG, JPG and other images.

The author has made the package pure ESM in its latest version, thus you cannot use it in a gulp pipeline that uses CommonJS.

In the following blog post, I’ll write down how to transfer the gulp pipeline to ESM.

This article follows the excellent guide Moving gulpfile from CommonJS (CJS) to ECMAScript Modules(ESM).

How I updated my gulp pipeline to ESM

1. Update packages

I updated my packages to the latest versions. Here is the excerpt from package.json:

{
  "devDependencies": {
    "get-google-fonts": "^1.2.2",
    "gulp": "^4.0.2",
    "gulp-clean-css": "^4.3.0",
    "gulp-imagemin": "^8.0.0",
    "gulp-sass": "^5.1.0",
    "sass": "^1.64.1"
  }
}

2. Update gulp to use ESM

I changed the file endings of all gulp files to .mjs:

  • gulp-tasks/fonts.mjs
  • gulp-tasks/images.mjs
  • gulp-tasks/sass.mjs
  • gulpfile.mjs

3. Change syntax

Now comes the tedious part where you have to adjust how to import and export your modules correctly.

gulpfile.mjs:

import gulp from 'gulp'
const { parallel, watch: gulpWatch } = gulp

// Pull in each task
import fonts from './gulp-tasks/fonts.mjs'
import images from './gulp-tasks/images.mjs'
import sass from './gulp-tasks/sass.mjs'

// Set each directory and contents that we want to watch and
// assign the relevant task. `ignoreInitial` set to true will
// prevent the task being run when we run `gulp watch`, but it
// will run when a file changes.
const watcher = () => {
  gulpWatch('./src/images/**/*', { ignoreInitial: true }, images)
  gulpWatch('./src/scss/**/*.scss', { ignoreInitial: true }, sass)
}

// The default (if someone just runs `gulp`) is to run each task in parallel
export default parallel(fonts, images, sass)

// This is our watcher task that instructs gulp to watch directories and
// act accordingly
export const watch = watcher

gulp-tasks/fonts.mjs:

import GetGoogleFonts from 'get-google-fonts'

const fonts = async () => {
  // Setup of the library instance by setting where we want
  // the output to go. CSS is relative to output font directory
  const instance = new GetGoogleFonts({
    outputDir: './dist/fonts',
    cssFile: './fonts.css',
  })

  // Grabs fonts and CSS from google and puts in the dist folder
  const result = await instance.download(
    'https://fonts.googleapis.com/css2?family=Literata:ital,wght@0,400;0,700;1,400&family=Red+Hat+Display:wght@400;900'
  )

  return result
}

export default fonts

gulp-tasks/images.mjs:

import gulp from 'gulp'
import imagemin, { mozjpeg, optipng } from 'gulp-imagemin'

// Grabs all images, runs them through imagemin
// and plops them in the dist folder
const images = () => {
  // We have specific configs for jpeg and png files to try
  // to really pull down asset sizes
  return gulp
    .src('./src/assets/images/**/*')
    .pipe(
      imagemin(
        [
          mozjpeg({ quality: 60, progressive: true }),
          optipng({ optimizationLevel: 5, interlaced: null }),
        ],
        {
          silent: true,
        }
      )
    )
    .pipe(gulp.dest('./dist/assets/images'))
}

export default images

gulp-tasks/sass.mjs:

import gulp from 'gulp'
import cleanCSS from 'gulp-clean-css'
import * as dartSass from 'sass'
import gulpSass from 'gulp-sass'
const sassProcessor = gulpSass(dartSass)

// Flags whether we compress the output etc
const __prod__ = process.env.NODE_ENV === 'production'

// An array of outputs that should be sent over to includes
const criticalStyles = [
  'critical.scss',
  'home.scss',
  'page.scss',
  'project.scss',
]

// Takes the arguments passed by `dest` and determines where the output file goes
const calculateOutput = ({ history }) => {
  // By default, we want a CSS file in our dist directory, so the
  // HTML can grab it with a <link />
  let response = './dist/css'

  // Get everything after the last slash
  const sourceFileName = /[^/]*$/.exec(history[0])[0]

  // If this is critical CSS though, we want it to go
  // to the css directory, so nunjucks can include it
  // directly in a <style>
  if (criticalStyles.includes(sourceFileName)) {
    response = './src/css'
  }

  return response
}

// The main Sass method grabs all root Sass files,
// processes them, then sends them to the output calculator
const sass = () => {
  return gulp
    .src('./src/scss/*.scss')
    .pipe(sassProcessor().on('error', sassProcessor.logError))
    .pipe(
      cleanCSS(
        __prod__
          ? {
              level: 2,
            }
          : {}
      )
    )
    .pipe(gulp.dest(calculateOutput, { sourceMaps: !__prod__ }))
}

export default sass

Recap

While a boring task, it’s actually pretty straightforward to support ESM for gulp and it works fine with Eleventy as a build pipeline.

Check out the links below for more info: