Last active
November 2, 2025 16:17
-
-
Save Nooshu/edfc2e382bc249e92ab238779357c93e to your computer and use it in GitHub Desktop.
ESM manipulation file I use on my 11ty v3.0.0 blog for manipulating my CSS.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // CSS Manipulation Module for Eleventy | |
| // Processes, minifies, and compresses CSS with cache busting and Brotli compression | |
| import crypto from "crypto"; | |
| import fs from "fs"; | |
| import path from "path"; | |
| import { brotliCompressSync } from "zlib"; | |
| import CleanCSS from "clean-css"; | |
| import env from "../_data/env.js"; | |
| // CleanCSS configuration for minification | |
| const cleanCSS = new CleanCSS({ | |
| level: { | |
| 1: { | |
| cleanupCharsets: true, | |
| normalizeUrls: true, | |
| optimizeBackground: true, | |
| optimizeBorderRadius: true, | |
| optimizeFilter: true, | |
| optimizeFont: true, | |
| optimizeFontWeight: true, | |
| optimizeOutline: true, | |
| removeEmpty: true, | |
| removeNegativePaddings: true, | |
| removeQuotes: true, | |
| removeWhitespace: true, | |
| replaceMultipleZeros: true, | |
| replaceTimeUnits: true, | |
| replaceZeroUnits: true, | |
| roundingPrecision: 2, | |
| selectorsSortingMethod: "standard", | |
| specialComments: "none", | |
| tidyAtRules: true, | |
| tidyBlockScopes: true, | |
| tidySelectors: true, | |
| transform: function () {}, | |
| }, | |
| 2: { | |
| mergeAdjacentRules: true, | |
| mergeIntoShorthands: true, | |
| mergeMedia: true, | |
| mergeNonAdjacentRules: true, | |
| mergeSemantically: false, | |
| overrideProperties: true, | |
| removeEmpty: true, | |
| reducePadding: true, | |
| reducePositions: true, | |
| reduceTimingFunctions: true, | |
| reduceTransforms: true, | |
| restructureRules: false, | |
| skipProperties: [], | |
| }, | |
| }, | |
| format: { | |
| breaks: { | |
| afterAtRule: false, | |
| afterBlockBegins: false, | |
| afterBlockEnds: false, | |
| afterComment: false, | |
| afterProperty: false, | |
| afterRuleBegins: false, | |
| afterRuleEnds: false, | |
| beforeBlockEnds: false, | |
| betweenSelectors: false, | |
| }, | |
| indentBy: 0, | |
| indentWith: "space", | |
| spaces: { | |
| aroundSelectorRelation: false, | |
| beforeBlockBegins: false, | |
| beforeValue: false, | |
| }, | |
| wrapAt: false, | |
| }, | |
| inline: ["none"], | |
| rebase: false, | |
| returnPromise: false, | |
| }); | |
| const DEFAULT_BROTLI_COMPRESSION_LEVEL = 11; | |
| const cssBuildCache = new Map(); | |
| const directoriesCreated = new Set(); | |
| export function manipulateCSS(eleventyConfig) { | |
| eleventyConfig.addShortcode("customCSS", async function (cssPath) { | |
| // Skip processing in local development | |
| if (env.isLocal) { | |
| return `<link rel="stylesheet" href="${cssPath}">`; | |
| } | |
| // Check in-memory cache | |
| if (cssBuildCache.has(cssPath)) { | |
| const cached = cssBuildCache.get(cssPath); | |
| if (cached instanceof Promise) { | |
| return await cached; | |
| } | |
| return cached.htmlOutput; | |
| } | |
| const fileStartTime = Date.now(); | |
| const inputFile = path.join("./public", cssPath); | |
| const outputDirectory = path.join("./_site", "css"); | |
| const cacheDirectory = path.join("./.cache", "css"); | |
| const brotliCompressionLevel = parseInt( | |
| process.env.BROTLI_COMPRESSION_LEVEL || DEFAULT_BROTLI_COMPRESSION_LEVEL, | |
| 10, | |
| ); | |
| const processingPromise = (async () => { | |
| try { | |
| if (!fs.existsSync(inputFile)) { | |
| console.error(`Input CSS file not found: ${inputFile}`); | |
| return ""; | |
| } | |
| const dirKey = `${cacheDirectory}:${outputDirectory}`; | |
| if (!directoriesCreated.has(dirKey)) { | |
| for (const dir of [cacheDirectory, outputDirectory]) { | |
| if (!fs.existsSync(dir)) { | |
| fs.mkdirSync(dir, { recursive: true }); | |
| } | |
| } | |
| directoriesCreated.add(dirKey); | |
| } | |
| let stepStartTime = Date.now(); | |
| const inputCSS = await fs.promises.readFile(inputFile, "utf8"); | |
| const inputSize = inputCSS.length; | |
| const hash = crypto | |
| .createHash("sha256") | |
| .update(inputCSS) | |
| .digest("hex") | |
| .slice(0, 10); | |
| console.log( | |
| `⏱️ [${Date.now() - stepStartTime}ms] CSS Read & Hash: ${cssPath} (${(inputSize / 1024).toFixed(2)} KB)`, | |
| ); | |
| const cacheKey = `${hash}-${cssPath.replace(/[/\\]/g, "-")}`; | |
| const cachePath = path.join(cacheDirectory, cacheKey); | |
| let processedCSS; | |
| stepStartTime = Date.now(); | |
| if (fs.existsSync(cachePath)) { | |
| processedCSS = await fs.promises.readFile(cachePath, "utf8"); | |
| console.log( | |
| `⏱️ [${Date.now() - stepStartTime}ms] CSS Cache Hit: ${cssPath}`, | |
| ); | |
| } else { | |
| processedCSS = cleanCSS.minify(inputCSS).styles; | |
| await fs.promises.writeFile(cachePath, processedCSS); | |
| const processedSize = processedCSS.length; | |
| console.log( | |
| `⏱️ [${Date.now() - stepStartTime}ms] CSS Minify: ${cssPath} (${(inputSize / 1024).toFixed(2)} KB → ${(processedSize / 1024).toFixed(2)} KB)`, | |
| ); | |
| } | |
| stepStartTime = Date.now(); | |
| const parsedPath = path.parse(inputFile); | |
| const finalFilename = path.join( | |
| outputDirectory, | |
| `${parsedPath.name}-${hash}${parsedPath.ext}`, | |
| ); | |
| if (!fs.existsSync(finalFilename)) { | |
| await fs.promises.writeFile(finalFilename, processedCSS); | |
| console.log( | |
| `⏱️ [${Date.now() - stepStartTime}ms] CSS Write: ${cssPath}`, | |
| ); | |
| } else { | |
| console.log( | |
| `⏱️ [${Date.now() - stepStartTime}ms] CSS Write Skip: ${cssPath} (already exists)`, | |
| ); | |
| } | |
| const brotliFilename = `${finalFilename}.br`; | |
| stepStartTime = Date.now(); | |
| if (!fs.existsSync(brotliFilename)) { | |
| const brotliOptions = { level: brotliCompressionLevel }; | |
| const brotliBuffer = brotliCompressSync( | |
| Buffer.from(processedCSS), | |
| brotliOptions, | |
| ); | |
| const brotliSize = brotliBuffer.length; | |
| const processedSize = processedCSS.length; | |
| await fs.promises.writeFile(brotliFilename, brotliBuffer); | |
| console.log( | |
| `⏱️ [${Date.now() - stepStartTime}ms] CSS Brotli: ${cssPath} (level ${brotliCompressionLevel}, ${(processedSize / 1024).toFixed(2)} KB → ${(brotliSize / 1024).toFixed(2)} KB)`, | |
| ); | |
| } else { | |
| console.log( | |
| `⏱️ [${Date.now() - stepStartTime}ms] CSS Brotli Skip: ${cssPath} (already exists)`, | |
| ); | |
| } | |
| const totalTime = Date.now() - fileStartTime; | |
| const hashedPath = brotliFilename | |
| .replace(path.join("./_site"), "") | |
| .replace(/\\/g, "/"); | |
| console.log( | |
| `✅ CSS Complete: ${cssPath} in ${totalTime}ms (${(totalTime / 1000).toFixed(2)}s)`, | |
| ); | |
| const result = `<link rel="stylesheet" href="${hashedPath}">`; | |
| cssBuildCache.set(cssPath, { hash, processedCSS, htmlOutput: result }); | |
| return result; | |
| } catch (err) { | |
| const totalTime = | |
| typeof fileStartTime !== "undefined" ? Date.now() - fileStartTime : 0; | |
| console.error(`❌ CSS Error: ${cssPath} after ${totalTime}ms:`, err); | |
| return ""; | |
| } | |
| })(); | |
| cssBuildCache.set(cssPath, processingPromise); | |
| return await processingPromise; | |
| }); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment