diff --git a/eleventy.bundle.js b/eleventy.bundle.js index cb221cd..1afb132 100644 --- a/eleventy.bundle.js +++ b/eleventy.bundle.js @@ -1,5 +1,6 @@ import { createRequire } from "node:module"; import bundleManagersPlugin from "./src/eleventy.bundleManagers.js"; +import pruneEmptyBundlesPlugin from "./src/eleventy.pruneEmptyBundles.js"; import shortcodesPlugin from "./src/eleventy.shortcodes.js"; import debugUtil from "debug"; @@ -34,10 +35,11 @@ function eleventyBundlePlugin(eleventyConfig, pluginOptions = {}) { bundleManagersPlugin(eleventyConfig, pluginOptions); } + pruneEmptyBundlesPlugin(eleventyConfig, pluginOptions); + // should this be unique too? shortcodesPlugin(eleventyConfig, pluginOptions); - if(Array.isArray(pluginOptions.bundles)) { debug("Adding bundles via `addPlugin`: %o", pluginOptions.bundles) pluginOptions.bundles.forEach(name => { diff --git a/package.json b/package.json index dcaa0c2..87dc783 100644 --- a/package.json +++ b/package.json @@ -49,13 +49,14 @@ ] }, "devDependencies": { - "@11ty/eleventy": "3.0.0-alpha.19", + "@11ty/eleventy": "3.0.0-alpha.20", "ava": "^5.3.1", "postcss": "^8.4.31", "postcss-nested": "^6.0.1", "sass": "^1.69.5" }, "dependencies": { - "debug": "^4.3.4" + "debug": "^4.3.4", + "posthtml-match-helper": "^2.0.2" } } diff --git a/src/eleventy.pruneEmptyBundles.js b/src/eleventy.pruneEmptyBundles.js new file mode 100644 index 0000000..8ece03a --- /dev/null +++ b/src/eleventy.pruneEmptyBundles.js @@ -0,0 +1,88 @@ +import matchHelper from "posthtml-match-helper"; +import debugUtil from "debug"; + +const debug = debugUtil("Eleventy:Bundle"); + +const ATTRS = { + keep: "eleventy:keep" +} + +function getTextNodeContent(node) { + if (!node.content) { + return ""; + } + + return node.content + .map((entry) => { + if (typeof entry === "string") { + return entry; + } + if (Array.isArray(entry.content)) { + return getTextNodeContent(entry); + } + return ""; + }) + .join(""); +} + +function eleventyPruneEmptyBundles(eleventyConfig, options = {}) { + // Right now script[src],link[rel="stylesheet"] nodes are removed if the final bundles are empty. + // `false` to disable + options.pruneEmptySelector = options.pruneEmptySelector ?? `style,script,link[rel="stylesheet"]`; + + // `false` disables this plugin + if(options.pruneEmptySelector === false) { + return; + } + + if(!eleventyConfig.htmlTransformer || !eleventyConfig.htmlTransformer?.constructor?.SUPPORTS_PLUGINS_ENABLED_CALLBACK) { + debug("You will need to upgrade your version of Eleventy core to remove empty bundle tags automatically (v3 or newer)."); + return; + } + + eleventyConfig.htmlTransformer.addPosthtmlPlugin( + "html", + function (pluginOptions = {}) { + return function (tree) { + tree.match(matchHelper(options.pruneEmptySelector), function (node) { + if(node.attrs && node.attrs[ATTRS.keep] !== undefined) { + delete node.attrs[ATTRS.keep]; + return node; + } + + // + if(node.tag === "link") { + if(node.attrs?.rel === "stylesheet" && (node.attrs?.href || "").trim().length === 0) { + return false; + } + } else { + let content = getTextNodeContent(node); + + if(!content) { + // or + if(node.tag === "script" && (node.attrs?.src || "").trim().length === 0) { + return false; + } + + // + if(node.tag === "style") { + return false; + } + } + } + + + return node; + }); + }; + }, + { + // the `enabled` callback for plugins is available on v3.0.0-alpha.20+ and v3.0.0-beta.2+ + enabled: () => { + return Object.keys(eleventyConfig.getBundleManagers()).length > 0; + } + } + ); +} + +export default eleventyPruneEmptyBundles; diff --git a/test/test.js b/test/test.js index f5b5b50..046e889 100644 --- a/test/test.js +++ b/test/test.js @@ -2,6 +2,7 @@ import test from "ava"; import fs from "fs"; import Eleventy, { RenderPlugin } from "@11ty/eleventy"; import * as sass from "sass"; +import bundlePlugin from "../eleventy.bundle.js"; function normalize(str) { if(typeof str !== "string") { @@ -56,7 +57,7 @@ test("SVG", async t => { let elev = new Eleventy("test/stubs/nunjucks-svg/", "_site", { configPath: "eleventy.bundle.js" }); let results = await elev.toJSON(); t.deepEqual(normalize(results[0].content), ``) }); @@ -90,8 +91,7 @@ test("CSS, two buckets, explicit `default`", async t => { t.deepEqual(normalize(results[0].content), ` -`) +* { color: red; }`) }); test("CSS, get two buckets at once", async t => { @@ -185,7 +185,7 @@ test("toFile Filter (write files, out of order)", async t => { test("Bundle in Layout file", async t => { let elev = new Eleventy("test/stubs/bundle-in-layout/", "_site", { configPath: "eleventy.bundle.js" }); let results = await elev.toJSON(); - t.deepEqual(normalize(results[0].content), `
`); + t.deepEqual(normalize(results[0].content), ``); }); test("Bundle with render plugin", async t => { @@ -275,9 +275,7 @@ test("Output `defer` bucket multiple times (does hoisting)", async t => { let results = await elev.toJSON(); t.deepEqual(normalize(results[0].content), ` - -`); +* { color: red; }`); }); test("Output `default` bucket multiple times (no hoisting)", async t => { @@ -303,17 +301,12 @@ test("`defer` hoisting", async t => { return -1; }) - t.deepEqual(normalize(results[0].content), ` - + t.deepEqual(normalize(results[0].content), ` `); - t.deepEqual(normalize(results[1].content), ` - -`); + t.deepEqual(normalize(results[1].content), ``); - t.deepEqual(normalize(results[2].content), ` - -`); + t.deepEqual(normalize(results[2].content), ``); }); test("Bundle export key as string (11ty.js)", async t => { @@ -333,3 +326,87 @@ test("Bundle export key as string, using separate bundleExportKey’s (11ty.js)" let results = await elev.toJSON(); t.deepEqual(normalize(results[0].content), ``) }); + +test("Empty CSS bundle (trimmed) removes empty +{%- css %} {% endcss %}`) + } + }); + let results = await elev.toJSON(); + t.deepEqual(normalize(results[0].content), ``) +}); + +test("Empty JS bundle (trimmed) removes empty +{%- js %} {% endjs %}`) + } + }); + let results = await elev.toJSON(); + t.deepEqual(normalize(results[0].content), ``) +}); + +test("Empty CSS bundle (trimmed) removes empty tag", async t => { + let elev = new Eleventy("test/stubs-virtual/", "_site", { + config: function(eleventyConfig) { + eleventyConfig.addPlugin(bundlePlugin); + + eleventyConfig.addTemplate('test.njk', ` +{%- css %} {% endcss %}`) + } + }); + let results = await elev.toJSON(); + t.deepEqual(normalize(results[0].content), ``) +}); + +test("Empty CSS bundle (trimmed) does *not* remove empty +{%- css %} {% endcss %}`) + } + }); + let results = await elev.toJSON(); + t.deepEqual(normalize(results[0].content), ``) +}); + +test("Empty CSS bundle (trimmed) does *not* remove empty +{%- css %} {% endcss %}`) + } + }); + + let results = await elev.toJSON(); + t.deepEqual(normalize(results[0].content), ``) +}); + +// This one requires a new Eleventy v3.0.0-alpha.20 or -beta.2 release, per the `enabled` option on plugins to HtmlTransformer +test("`) + } + }); + + let results = await elev.toJSON(); + t.deepEqual(normalize(results[0].content), ``) +});