Use LightningCSS with Eleventy 3
I recently started using Eleventy 3 and wanted to use LightningCSS the same way I had been using it with Eleventy 2. With Eleventy 2, I used Stephanie Eckles' excellent LightningCSS plugin (she also discusses it in this article).
LightningCSS has several useful features, but the main one that interests me is the bundler. I want to be able to break my CSS up into partial stylesheets and import the partials inline into a single style.css
file. This is one of the key features of Sass and LightningCSS allows us to do this as well.
/* style.css */
/* Note the CSS partials are all prefixed with "_" */
@import "_reset.css";
@import "_theme.css";
@import "_typography.css";
@import "_layout.css";
Then I want the single style.css
file automatically copied over to a CSS folder in the final output (build) folder. I can then link to the single CSS file.
<head>
...
<link rel="stylesheet" href="./css/style.css" />
...
</head>
Stephanie's plugin allowed me to do this with Eleventy 2, but it hasn't been updated for Eleventy 3. It has Eleventy 2 as a dependency and is written in CommonJS instead of ESM, which Eleventy 3 prefers.
Adapting the LightningCSS plugin for Eleventy 3
Starting from Stephanie's plugin, here are the steps I followed to get LightningCSS working with Eleventy 3.
1) Install lightningcss
and browserlist
npm install --save-dev lightningcss browserslist
2) Create a CSS folder in the input folder
I happened to be using the Eleventy Base Blog starter which uses content
as the input (source) folder, so the CSS folder was /content/css
(traditionally src
is a common name for the input folder).
3) Create a css.json
file in the CSS folder
Inside the CSS folder, create a directory data file named css.json
(/content/css/css.json
) with this content:
{
"eleventyExcludeFromCollections": true,
"layout": false
}
4) Create a local plugin in the /_config
folder
Create a _config
folder in the root of the project folder if one doesn't already exist. This folder should be at the same level as the input folder, not inside the input folder.
Then create a lightningcss-plugin.js
file containing the following code, which is a translation of Stephanie's plugin from CommonJS to ESM.
// /_config/lightningcss-plugin.js
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import browserslist from "browserslist";
import { bundle, browserslistToTargets, composeVisitors } from "lightningcss";
// Get __dirname equivalent in ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Set default transpiling targets
let browserslistTargets = "> 0.2% and not dead";
// Check for user's browserslist
try {
const packagePath = path.resolve(process.cwd(), "package.json");
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8"));
if (packageJson.browserslist) {
browserslistTargets = packageJson.browserslist;
} else {
try {
const browserslistrcPath = path.resolve(process.cwd(), ".browserslistrc");
const data = fs.readFileSync(browserslistrcPath, "utf8");
if (data.length) {
browserslistTargets = [];
}
data.split(/\r?\n/).forEach((line) => {
if (line.length && !line.startsWith("#")) {
browserslistTargets.push(line);
}
});
} catch (err) {
// no .browserslistrc
}
}
} catch (err) {
// no package browserslist
}
export default function (eleventyConfig, options = {}) {
const defaults = {
importPrefix: "_",
nesting: false,
customMedia: false,
minify: false,
sourceMap: false,
visitors: [],
customAtRules: {},
};
const {
importPrefix,
nesting,
customMedia,
minify,
sourceMap,
visitors,
customAtRules,
} = {
...defaults,
...options,
};
// Recognize CSS as a "template language"
eleventyConfig.addTemplateFormats("css");
// Process CSS with LightningCSS
eleventyConfig.addExtension("css", {
outputFileExtension: "css",
compile: async function (_inputContent, inputPath) {
let parsed = path.parse(inputPath);
if (parsed.name.startsWith(importPrefix)) {
return;
}
// Support @import triggering regeneration for incremental builds
if (_inputContent.includes("@import")) {
// for each file create a list of files to look at
const fileList = [];
// get a list of import on the file your reading
const importRuleRegex =
/@import\s+(?:url\()?['"]?([^'"\);]+)['"]?\)?.*;/g;
let match;
while ((match = importRuleRegex.exec(_inputContent))) {
fileList.push(parsed.dir + "/" + match[1]);
}
this.addDependencies(inputPath, fileList);
}
let targets = browserslistToTargets(browserslist(browserslistTargets));
return async () => {
let { code } = await bundle({
filename: inputPath,
minify,
sourceMap,
targets,
drafts: {
nesting,
customMedia,
},
customAtRules,
visitor: composeVisitors(visitors),
});
return code;
};
},
});
}
Note in the configuration section I have turned off most of the LightningCSS features, based on my personal preference. You can turn them back on if you wish.
const defaults = {
importPrefix: "_",
nesting: false,
customMedia: false,
minify: false,
sourceMap: false,
visitors: [],
customAtRules: {},
};
5) Add the plugin to eleventy.config.js
The final step is to add the plugin in your eleventy.config.sys
file.
// eleventy.config.js
import lightningCSS from "./_config/lightningcss-plugin.js";
export default async function (eleventyConfig) {
...
eleventyConfig.addPlugin(lightningCSS, {
minify: false,
});
...
}
No passthrough file copy needed
Note that the plugin automatically copies the final constructed style.css
file into the output folder. You don't need a passthrough file copy statement in eleventy.config.js
for your CSS file!
This setup is working great for me, hopefully it will work for you as well.