From a28cf3afe0a7fcedc3555e239fff6bcf4ccc9d15 Mon Sep 17 00:00:00 2001 From: Zach Leatherman Date: Tue, 30 Apr 2024 11:45:33 -0500 Subject: [PATCH] Prepare to bundle with 3.0 core. + Add `bundles: false` option to skip built-in bundles. + Throws an error on incompatible eleventy versions. + Adds `addBundle` and `getBundleManagers` methods to eleventyConfig. + Add option to set file extension separate from key #22 + Handle multiple addPlugin calls. + Skip transform if no bundles in play or if page being processed did not fetch bundles. + Changes tests to use ESM and 3.0+ --- BundleFileOutput.js | 7 ++++- codeManager.js | 11 ++++++- eleventy.bundle.js | 36 +++++++++++++++------ eleventy.bundleManagers.js | 59 +++++++++++++++++++++++++++++++++++ eleventy.shortcodes.js | 64 ++++++++++++++++++-------------------- package.json | 9 +++--- sample/sample-config.js | 25 ++++++++++++++- sample/test.njk | 4 ++- test/{test.js => test.mjs} | 56 +++++++++++++-------------------- 9 files changed, 185 insertions(+), 86 deletions(-) create mode 100644 eleventy.bundleManagers.js rename test/{test.js => test.mjs} (84%) diff --git a/BundleFileOutput.js b/BundleFileOutput.js index 0330161..5e3804a 100644 --- a/BundleFileOutput.js +++ b/BundleFileOutput.js @@ -13,6 +13,11 @@ class BundleFileOutput { this.outputDirectory = outputDirectory; this.bundleDirectory = bundleDirectory; this.hashLength = 10; + this.fileExtension = undefined; + } + + setFileExtension(ext) { + this.fileExtension = ext; } getFilenameHash(content) { @@ -44,7 +49,7 @@ class BundleFileOutput { let dir = path.join(this.outputDirectory, this.bundleDirectory); let filenameHash = this.getFilenameHash(content); - let filename = this.getFilename(filenameHash, type); + let filename = this.getFilename(filenameHash, this.fileExtension || type); if(writeToFileSystem) { let fullPath = path.join(dir, filename); diff --git a/codeManager.js b/codeManager.js index 151cae7..d9c381f 100644 --- a/codeManager.js +++ b/codeManager.js @@ -1,6 +1,8 @@ const BundleFileOutput = require("./BundleFileOutput"); const debug = require("debug")("Eleventy:Bundle"); +const DEBUG_LOG_TRUNCATION_SIZE = 200; + class CodeManager { // code is placed in this bucket by default static DEFAULT_BUCKET_NAME = "default"; @@ -14,6 +16,11 @@ class CodeManager { this.reset(); this.transforms = []; this.isHoisting = true; + this.fileExtension = undefined; + } + + setFileExtension(ext) { + this.fileExtension = ext; } setHoisting(enabled) { @@ -72,7 +79,8 @@ class CodeManager { for(let b of buckets) { this._initBucket(pageUrl, b); - debug("Adding code to bundle %o for %o (bucket: %o): %o", this.name, pageUrl, b, codeContent); + let debugLoggedContent = codeContent.join("\n"); + debug("Adding code to bundle %o for %o (bucket: %o, size: %o): %o", this.name, pageUrl, b, debugLoggedContent.length, debugLoggedContent.length > DEBUG_LOG_TRUNCATION_SIZE ? debugLoggedContent.slice(0, DEBUG_LOG_TRUNCATION_SIZE) + "…" : debugLoggedContent); for(let content of codeContent) { this.pages[pageUrl][b].add(content); } @@ -144,6 +152,7 @@ class CodeManager { // TODO the bundle output URL might be useful in the transforms for sourcemaps let content = await this.getForPage(pageData, buckets); let writer = new BundleFileOutput(output, bundle); + writer.setFileExtension(this.fileExtension); return writer.writeBundle(content, this.name, write); } diff --git a/eleventy.bundle.js b/eleventy.bundle.js index 2f9bd2b..80eef35 100644 --- a/eleventy.bundle.js +++ b/eleventy.bundle.js @@ -1,31 +1,49 @@ const pkg = require("./package.json"); +const bundleManagersPlugin = require("./eleventy.bundleManagers.js"); const shortcodesPlugin = require("./eleventy.shortcodes.js"); +const debug = require("debug")("Eleventy:Bundle"); function normalizeOptions(options = {}) { options = Object.assign({ // Plugin defaults - bundles: [], // extra bundles: css, js, and html are guaranteed + bundles: [], // extra bundles: css, js, and html are guaranteed unless `bundles: false` toFileDirectory: "bundle", // post-process transforms: [], hoistDuplicateBundlesFor: [], }, options); - options.bundles = Array.from(new Set(["css", "js", "html", ...(options.bundles || [])])); + if(options.bundles !== false) { + options.bundles = Array.from(new Set(["css", "js", "html", ...(options.bundles || [])])); + } return options; } -function eleventyBundlePlugin(eleventyConfig, options = {}) { - try { - eleventyConfig.versionCheck(pkg["11ty"].compatibility); - } catch(e) { - console.log( `WARN: Eleventy Plugin (${pkg.name}) Compatibility: ${e.message}` ); +function eleventyBundlePlugin(eleventyConfig, pluginOptions = {}) { + eleventyConfig.versionCheck(pkg["11ty"].compatibility); + + pluginOptions = normalizeOptions(pluginOptions); + + if(!("getBundleManagers" in eleventyConfig) && !("addBundle" in eleventyConfig)) { + bundleManagersPlugin(eleventyConfig, pluginOptions); } - options = normalizeOptions(options); + shortcodesPlugin(eleventyConfig, pluginOptions); - shortcodesPlugin(eleventyConfig, options); + if(Array.isArray(pluginOptions.bundles)) { + debug("Adding bundles via `addPlugin`: %o", pluginOptions.bundles) + pluginOptions.bundles.forEach(name => { + let hoist = Array.isArray(pluginOptions.hoistDuplicateBundlesFor) && pluginOptions.hoistDuplicateBundlesFor.includes(name); + + eleventyConfig.addBundle(name, { + hoist, + outputFileExtension: name, // default as `name` + shortcodeName: name, // `false` will skip shortcode + transforms: pluginOptions.transforms, + }); + }); + } }; // This is used to find the package name for this plugin (used in eleventy-plugin-webc to prevent dupes) diff --git a/eleventy.bundleManagers.js b/eleventy.bundleManagers.js new file mode 100644 index 0000000..92c3631 --- /dev/null +++ b/eleventy.bundleManagers.js @@ -0,0 +1,59 @@ + +const pkg = require("./package.json"); +const CodeManager = require("./codeManager.js"); +const debug = require("debug")("Eleventy:Bundle"); + +module.exports = function(eleventyConfig, pluginOptions = {}) { + if("getBundleManagers" in eleventyConfig || "addBundle" in eleventyConfig) { + throw new Error("Duplicate plugin calls for " + pkg.name); + } + + let managers = {}; + + function addBundle(name, bundleOptions = {}) { + if(name in managers) { + debug("Bundle exists %o, skipping.", name); + // note: shortcode must still be added + } else { + debug("Creating new bundle %o", name); + managers[name] = new CodeManager(name); + + if(bundleOptions.hoist !== undefined) { + managers[name].setHoisting(bundleOptions.hoist); + } + + if(bundleOptions.outputFileExtension) { + managers[name].setFileExtension(bundleOptions.outputFileExtension); + } + + if(bundleOptions.transforms) { + managers[name].setTransforms(bundleOptions.transforms); + } + } + + // if undefined, defaults to `name` + if(bundleOptions.shortcodeName !== false) { + let shortcodeName = bundleOptions.shortcodeName || name; + + // e.g. `css` shortcode to add code to page bundle + // These shortcode names are not configurable on purpose (for wider plugin compatibility) + eleventyConfig.addPairedShortcode(shortcodeName, function addContent(content, bucket, urlOverride) { + let url = urlOverride || this.page.url; + managers[name].addToPage(url, content, bucket); + return ""; + }); + } + }; + + eleventyConfig.addBundle = addBundle; + + eleventyConfig.getBundleManagers = function() { + return managers; + }; + + eleventyConfig.on("eleventy.before", async () => { + for(let key in managers) { + managers[key].reset(); + } + }); +}; diff --git a/eleventy.shortcodes.js b/eleventy.shortcodes.js index c3d76d9..426ebaf 100644 --- a/eleventy.shortcodes.js +++ b/eleventy.shortcodes.js @@ -1,38 +1,18 @@ -const CodeManager = require("./codeManager.js"); const OutOfOrderRender = require("./outOfOrderRender.js"); const debug = require("debug")("Eleventy:Bundle"); -module.exports = function(eleventyConfig, options = {}) { - // TODO throw an error if addPlugin is called more than once per build here. - - let managers = {}; - - options.bundles.forEach(name => { - managers[name] = new CodeManager(name); - - if(Array.isArray(options.hoistDuplicateBundlesFor) && options.hoistDuplicateBundlesFor.includes(name)) { - managers[name].setHoisting(true); - } else { - managers[name].setHoisting(false); - } - - managers[name].setTransforms(options.transforms); - - // e.g. `css` shortcode to add code to page bundle - // These shortcode names are not configurable on purpose (for wider plugin compatibility) - eleventyConfig.addPairedShortcode(name, function addContent(content, bucket, urlOverride) { - let url = urlOverride || this.page.url; - managers[name].addToPage(url, content, bucket); - return ""; - }); - }); - +module.exports = function(eleventyConfig, pluginOptions = {}) { + let managers = eleventyConfig.getBundleManagers(); let writeToFileSystem = true; + let pagesUsingBundles = {}; + eleventyConfig.on("eleventy.before", async ({ outputMode }) => { - for(let key in managers) { - managers[key].reset(); + if(Object.keys(managers).length === 0) { + return; } + pagesUsingBundles = {}; + if(outputMode !== "fs") { writeToFileSystem = false; debug("Skipping writing to the file system due to output mode: %o", outputMode); @@ -43,8 +23,12 @@ module.exports = function(eleventyConfig, options = {}) { // bucket can be an array // This shortcode name is not configurable on purpose (for wider plugin compatibility) eleventyConfig.addShortcode("getBundle", function getContent(type, bucket) { - if(!type || !(type in managers)) { - throw new Error("Invalid bundle type: " + type); + if(!type || !(type in managers) || Object.keys(managers).length === 0) { + throw new Error(`Invalid bundle type: ${type}. Available options: ${Object.keys(managers)}`); + } + + if(this.page.url) { + pagesUsingBundles[this.page.url] = true; } return OutOfOrderRender.getAssetKey("get", type, bucket); @@ -53,8 +37,12 @@ module.exports = function(eleventyConfig, options = {}) { // write a bundle to the file system // This shortcode name is not configurable on purpose (for wider plugin compatibility) eleventyConfig.addShortcode("getBundleFileUrl", function(type, bucket) { - if(!type || !(type in managers)) { - throw new Error("Invalid bundle type: " + type); + if(!type || !(type in managers) || Object.keys(managers).length === 0) { + throw new Error(`Invalid bundle type: ${type}. Available options: ${Object.keys(managers)}`); + } + + if(this.page.url) { + pagesUsingBundles[this.page.url] = true; } return OutOfOrderRender.getAssetKey("file", type, bucket); @@ -62,19 +50,27 @@ module.exports = function(eleventyConfig, options = {}) { eleventyConfig.addTransform("@11ty/eleventy-bundle", async function(content) { // `page.outputPath` is required to perform bundle transform, unless - // we're running in an Eleventy Serverless context. + // we're running in Eleventy Serverless. let missingOutputPath = !this.page.outputPath && process.env.ELEVENTY_SERVERLESS !== "true"; if(missingOutputPath || typeof content !== "string") { return content; } + // Only run if managers are in play + // Only run on pages that have fetched bundles via `getBundle` or `getBundleFileUrl` + if(Object.keys(managers).length === 0 || this.page.url && !pagesUsingBundles[this.page.url]) { + return content; + } + + debug("Processing %o", this.page.url); + let render = new OutOfOrderRender(content); for(let key in managers) { render.setAssetManager(key, managers[key]); } render.setOutputDirectory(eleventyConfig.dir.output); - render.setBundleDirectory(options.toFileDirectory); + render.setBundleDirectory(pluginOptions.toFileDirectory); render.setWriteToFileSystem(writeToFileSystem); return render.replaceAll(this.page); diff --git a/package.json b/package.json index 2488d56..88934e0 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Little bundles of code, little bundles of joy.", "main": "eleventy.bundle.js", "scripts": { - "sample": "DEBUG=Eleventy:Bundle npx @11ty/eleventy --config=sample/sample-config.js --input=sample", + "sample": "DEBUG=Eleventy:Bundle npx @11ty/eleventy --config=sample/sample-config.js --input=sample --serve", "test": "npx ava" }, "publishConfig": { @@ -15,7 +15,7 @@ "node": ">=14" }, "11ty": { - "compatibility": ">=2.0.0-beta.1" + "compatibility": ">=3.0.0-alpha" }, "funding": { "type": "opencollective", @@ -39,7 +39,8 @@ "ava": { "failFast": true, "files": [ - "test/*.js" + "test/*.js", + "test/*.mjs" ], "ignoredByWatcher": [ "**/_site/**", @@ -47,7 +48,7 @@ ] }, "devDependencies": { - "@11ty/eleventy": "^2.0.0", + "@11ty/eleventy": "3.0.0-alpha.9", "ava": "^5.3.1", "postcss": "^8.4.31", "postcss-nested": "^6.0.1", diff --git a/sample/sample-config.js b/sample/sample-config.js index 61210c8..95048fc 100644 --- a/sample/sample-config.js +++ b/sample/sample-config.js @@ -1,6 +1,29 @@ const bundlePlugin = require("../"); module.exports = function(eleventyConfig) { + // This call is what Eleventy will do in the default config in 3.0.0-alpha.10 + eleventyConfig.addPlugin(bundlePlugin, { + bundles: false, + immediate: true + }); + + // adds html, css, js (maintain existing API) eleventyConfig.addPlugin(bundlePlugin); - eleventyConfig.addPlugin(bundlePlugin); + + // ignored, already exists + eleventyConfig.addBundle("css"); + // ignored, already exists + eleventyConfig.addBundle("css"); + // ignored, already exists + eleventyConfig.addBundle("css"); + // ignored, already exists + eleventyConfig.addBundle("html"); + + // new! + eleventyConfig.addBundle("stylesheet", { + outputFileExtension: "css", + shortcodeName: "stylesheet", + transforms: [], + // hoist: true, + }); }; diff --git a/sample/test.njk b/sample/test.njk index a926810..74a7636 100644 --- a/sample/test.njk +++ b/sample/test.njk @@ -4,6 +4,7 @@ {%- css %}* { color: blue; }{% endcss %} {%- css %}* { color: red; }{% endcss %} +{%- stylesheet %}/* lololololol sdlkfjkdlsfsldkjflksd sdlfkj */{% endstylesheet %} `); }); test("Bundle with render plugin", async t => { - const sass = require("sass"); - let elev = new Eleventy("test/stubs/bundle-render/", undefined, { configPath: "eleventy.bundle.js", config: function(eleventyConfig) { - eleventyConfig.addPlugin(EleventyRenderPlugin); + eleventyConfig.addPlugin(RenderPlugin); eleventyConfig.addExtension("scss", { outputFileExtension: "css", @@ -237,7 +235,7 @@ h1 .test { }); test("No bundling", async t => { - let elev = new Eleventy("test/stubs/no-bundles/", null, { configPath: "eleventy.bundle.js" }); + let elev = new Eleventy("test/stubs/no-bundles/", "_site", { configPath: "eleventy.bundle.js" }); let results = await elev.toJSON(); t.deepEqual(normalize(results[0].content), ` @@ -334,15 +332,3 @@ test("`defer` hoisting", async t => { `); }); - -test("Ignore missing `file.outputPath` when running under Serverless", async t => { - let elev = new EleventyServerless("test1", { - path: "/", - query: {}, - inputDir: "./test/stubs/serverless-stubs/", - functionsDir: "./test/stubs/serverless-stubs/functions/", - }); - - let results = await elev.getOutput(); - t.deepEqual(normalize(results[0].content), ``); -});