How To Setup Custom ESBuild with SCSS, PurgeCSS & LiveServer?
Asked Answered
C

3

15

Background:

I have a Webpack setup that I use to preprocess SCSS with PurgeCSS with a live HMR server with esbuild-loader for speeding up compiles in Webpack but even then my compile times are still slow and I would like the raw-speed of ESBuild and remove Webpack setup altogether.

The basic setup of ESBuild is easy, you install esbuild using npm and add the following code in your package.json:

{
  ...
  "scripts": {
    ...
    "watch": "esbuild --bundle src/script.js --outfile=dist/script.js --watch"
  },
  ...
}

and run it by using the following command:

npm run watch

This single-line configuration will bundle your scripts and styles (you can import style.css in script.js) and output the files in the dist directory but this doesn't allow advance configuration for ESBuild like outputting a different name for your stylesheet and script files or using plugins.

Problems:

  1. How to configure ESBuild using an external config file?
  2. ESBuild doesn't support SCSS out-of-the-box. How to configure external plugins like esbuild-sass-plugin and to go even further, how to setup PostCSS and its plugins like Autoprefixer?
  3. How to setup dev server with auto-rebuild?
  4. How to setup PurgeCSS?
Conservancy answered 12/12, 2021 at 17:8 Comment(0)
C
44

Solutions:

1. How to configure ESBuild using an external config file?

  1. Create a new file in root: esbuild.mjs with the following contents:
import esbuild from "esbuild";

esbuild
    .build({
        entryPoints: ["src/styles/style.css", "src/scripts/script.js"],
        outdir: "dist",
        bundle: true,
        plugins: [],
    })
    .then(() => console.log("⚡ Build complete! ⚡"))
    .catch(() => process.exit(1));
  1. Add the following code in your package.json:
{
    ...
    "scripts": {
        ...
        "build": "node esbuild.mjs"
    },
...
}
  1. Run the build by using npm run build command and this would bundle up your stylesheets and scripts and output them in dist directory.
  2. For more details and/or adding custom build options, please refer to ESBuild's Build API documentation.

2. ESBuild doesn't support SCSS out-of-the-box. How to configure external plugins like esbuild-sass-plugin and to go even further, how to setup PostCSS and plugins like Autoprefixer?

  1. Install npm dependencies: npm i -D esbuild-sass-plugin postcss autoprefixer
  2. Edit your esbuild.mjs to the following code:
import esbuild from "esbuild";
import { sassPlugin } from "esbuild-sass-plugin";
import postcss from 'postcss';
import autoprefixer from 'autoprefixer';

// Generate CSS/JS Builds
esbuild
    .build({
        entryPoints: ["src/styles/style.scss", "src/scripts/script.js"],
        outdir: "dist",
        bundle: true,
        metafile: true,
        plugins: [
            sassPlugin({
                async transform(source) {
                    const { css } = await postcss([autoprefixer]).process(source);
                    return css;
                },
            }),
        ],
    })
    .then(() => console.log("⚡ Build complete! ⚡"))
    .catch(() => process.exit(1));

3. How to setup dev server with auto-rebuild?

  1. ESBuild has a limitation on this end, you can either pass in watch: true or run its server. It doesn't allow both.
  2. ESBuild also has another limitation, it doesn't have HMR support like Webpack does.
  3. So to live with both limitations and still allowing a server, we can use Live Server. Install it using npm i -D @compodoc/live-server.
  4. Create a new file in root: esbuild_watch.mjs with the following contents:
import liveServer from '@compodoc/live-server';
import esbuild from 'esbuild';
import { sassPlugin } from 'esbuild-sass-plugin';
import postcss from 'postcss';
import autoprefixer from 'autoprefixer';

// Turn on LiveServer on http://localhost:7000
liveServer.start({
    port: 7000,
    host: 'localhost',
    root: '',
    open: true,
    ignore: 'node_modules',
    wait: 0,
});

// Generate CSS/JS Builds
esbuild
    .build({
        logLevel: 'debug',
        metafile: true,
        entryPoints: ['src/styles/style.scss', 'src/scripts/script.js'],
        outdir: 'dist',
        bundle: true,
        watch: true,
        plugins: [
            sassPlugin({
                async transform(source) {
                    const { css } = await postcss([autoprefixer]).process(
                        source
                    );
                    return css;
                },
            }),
        ],
    })
    .then(() => console.log('⚡ Styles & Scripts Compiled! ⚡ '))
    .catch(() => process.exit(1));
  1. Edit the scripts in your package.json:
{
    ...
    "scripts": {
        ...
        "build": "node esbuild.mjs",
        "watch": "node esbuild_watch.mjs"
    },
...
}
  1. To run build use this command npm run build.
  2. To run dev server with auto-rebuild run npm run watch. This is a "hacky" way to do things but does a fair-enough job.

4. How to setup PurgeCSS?

I found a great plugin for this: esbuild-plugin-purgecss by peteryuan but it wasn't allowing an option to be passed for the html/views paths that need to be parsed so I created esbuild-plugin-purgecss-2 that does the job. To set it up, read below:

  1. Install dependencies npm i -D esbuild-plugin-purgecss-2 glob-all.
  2. Add the following code to your esbuild.mjs and esbuild_watch.mjs files:
// Import Dependencies
import glob from 'glob-all';
import purgecssPlugin2 from 'esbuild-plugin-purgecss-2';

esbuild
    .build({
        plugins: [
            ...
            purgecssPlugin2({
                content: glob.sync([
                    // Customize the following URLs to match your setup
                    './*.html',
                    './views/**/*.html'
                ]),
            }),
        ],
    })
    ...
  1. Now running the npm run build or npm run watch will purgeCSS from the file paths mentioned in glob.sync([...] in the code above.

TL;DR:

  1. Create an external config file in root esbuild.mjs and add the command to run it in package.json inside scripts: {..} e.g. "build": "node esbuild.mjs" to reference and run the config file by using npm run build.
  2. ESBuild doesn't support HMR. Also, you can either watch or serve with ESBuild, not both. To overcome, use a separate dev server library like Live Server.
  3. For the complete setup, please refer to my custom-esbuild-with-scss-purgecss-and-liveserver repository on github.

Final Notes:

I know this is a long thread but it took me a lot of time to figure these out. My intention is to have this here for others looking into the same problems and trying to figure out where to get started.

Thanks.

Conservancy answered 12/12, 2021 at 17:8 Comment(8)
kudos to that! :)Behind
Wow, thanks for the thorough answer. It's a real time saver.Rhoda
this answer is great except that the step 3 is outdated, you can now serve and watch at the same time with esbuild v0.17.x (see release 0.17.0) for more infoPresentiment
It's not yet outdated. ESbuild still has issues with live reloading and HMR on html files. I have created a new branch in my github repo for people wanting to use esbuild's native HMR and server for reference: github.com/arslanakram/…Conservancy
Had to use node esbuild.mjs instead of node esbuild.js to get it workingOrcinol
In my package.json in the repo i was using "type": "module", which doesn't require the file extension to be .mjs.Conservancy
@xcvbn updated the answer to avoid confusion.Conservancy
this setup can not resolve urls in scss files. for example 'background-image: url('/assets/images/img/hero.png');' . it will throw ERROR: Could not resolve "/assets/images/img/hero.png" error.Disini
B
4

Update Sept 2023: Please Note that the answer below no longer works for esbuild 0.17 and above. I'm leaving this answer as-is, since this answer will still work for Linux distros that package esbuild 0.16.xx and below.

Adding to Arslan's terrific answer, you can use the PurgeCSS plug-in for postcss to totally eliminate Step 4.

First, install the postcss-purgecss package: npm install @fullhuman/postcss-purgecss

Then, replace the code from Step 2 in Arslan's answer with the code shown below (which eliminates the need for Step 4).

import esbuild from "esbuild";
import { sassPlugin } from "esbuild-sass-plugin";
import postcss from "postcss";
import autoprefixer from "autoprefixer";
import purgecss from "@fullhuman/postcss-purgecss";

// Generate CSS/JS Builds
esbuild
    .build({
        entryPoints: [
            "yourproject/static/sass/project.scss",
            "yourproject/static/js/project.js",
        ],
        outdir: "dist",
        bundle: true,
        loader: {
            ".png": "dataurl",
            ".woff": "dataurl",
            ".woff2": "dataurl",
            ".eot": "dataurl",
            ".ttf": "dataurl",
            ".svg": "dataurl",
        },
        plugins: [
            sassPlugin({
                async transform(source) {
                    const { css } = await postcss([
                        purgecss({
                            content: ["yourproject/templates/**/*.html"],
                        }),
                        autoprefixer,
                    ]).process(source, {
                        from: "yourproject/static/sass/project.scss",
                    });
                    return css;
                },
            }),
        ],
        minify: true,
        metafile: true,
        sourcemap: true,
    })
    .then(() => console.log("⚡ Build complete! ⚡"))
    .catch(() => process.exit(1));
Bristle answered 7/8, 2022 at 0:47 Comment(1)
Thanks for sharing this. I haven't tested it yet but looks good.Conservancy
B
0

Following to impressive answer by @Arslan Akram, the below code worked for me. Just pasting as with latest esbuild, watch was throwing error.

esbuild.mjs

import esbuild from "esbuild";
import { sassPlugin } from "esbuild-sass-plugin";
import postcss from "postcss";
import autoprefixer from "autoprefixer";

const watch = process.argv.includes("--watch");

const args = {
  entryPoints: [
    "src/css/libs.scss",
    "src/css/app.scss",
    "src/js/libs.js",
    "src/js/app.js",
  ],
  outdir: "dist",
  bundle: true,
  metafile: true,
  plugins: [
    sassPlugin({
      async transform(source) {
        const { css } = await postcss([autoprefixer]).process(source, {
          from: undefined,
        });
        return css;
      },
    }),
  ],
};

let ctx;
if (watch) {
  ctx = await esbuild.context(args);
  await ctx.watch();
  console.log("watching...");
} else {
  args.minify = true;
  ctx = await esbuild.build(args);
  console.log("build successful");
}

package.json

{
    "name": "package-name",
    "version": "1.0.0",
    "description": "",
    "main": "index.js",
    "type": "module",
    "scripts": {
      "test": "echo \"Error: no test specified\" && exit 1",
      "watch": "node esbuild.mjs --watch",
      "build": "node esbuild.mjs"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "dependencies": {
      "esbuild": "0.17.18"
    },
    "devDependencies": {
      "autoprefixer": "^10.4.14",
      "esbuild-sass-plugin": "^2.9.0",
      "postcss": "^8.4.23"
    }
  }
Bork answered 28/4, 2023 at 7:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.