├── test ├── stubs-virtual │ └── .gitkeep ├── stubs │ ├── bundle-in-layout │ │ ├── _includes │ │ │ └── layout.liquid │ │ └── index.liquid │ ├── serverless-stubs │ │ ├── functions │ │ │ └── test1 │ │ │ │ ├── eleventy-serverless-map.json │ │ │ │ └── eleventy.config.js │ │ └── test1.liquid │ ├── pluck-html-js │ │ ├── index.njk │ │ └── eleventy.config.js │ ├── export-key-obj │ │ ├── eleventy.config.js │ │ └── template.11ty.mjs │ ├── export-key-str │ │ ├── eleventy.config.js │ │ └── template.11ty.mjs │ ├── pluck-html-css │ │ ├── index.njk │ │ └── eleventy.config.js │ ├── export-key-str-rename │ │ ├── eleventy.config.js │ │ └── template.11ty.mjs │ ├── bundle-render │ │ └── index.liquid │ ├── to-file-duplicates │ │ ├── get-and-get.njk │ │ ├── get-and-file.njk │ │ ├── eleventy.config.js │ │ └── file-and-file.njk │ ├── duplicate-addplugins │ │ └── eleventy.config.js │ ├── to-file-ordering │ │ └── to-file-ordering.liquid │ ├── use-transforms │ │ ├── index.liquid │ │ └── eleventy.config.js │ ├── output-default-multiple-times │ │ ├── eleventy.config.js │ │ └── index.liquid │ ├── output-same-bucket-multiple-times │ │ ├── eleventy.config.js │ │ └── index.liquid │ ├── liquid-js │ │ └── index.liquid │ ├── buckets-get-multiple-comma-sep │ │ └── index.liquid │ ├── handlebars │ │ └── index.hbs │ ├── liquid-buckets │ │ └── index.liquid │ ├── to-file │ │ └── index.njk │ ├── markdown │ │ └── index.md │ ├── nunjucks │ │ └── index.njk │ ├── liquid │ │ └── index.liquid │ ├── to-file-write │ │ └── to-file-write.njk │ ├── buckets-ordering │ │ └── index.liquid │ ├── buckets-get-multiple │ │ └── index.liquid │ ├── no-bundles │ │ └── index.liquid │ ├── use-transforms-on-type │ │ ├── index.liquid │ │ └── eleventy.config.js │ ├── liquid-buckets-default │ │ └── index.liquid │ ├── pluck-html-css-buckets │ │ ├── index.njk │ │ └── eleventy.config.js │ ├── no-prune │ │ └── eleventy.config.js │ ├── output-same-bucket-multiple-times-nohoist │ │ └── index.liquid │ ├── delayed-bundle │ │ └── eleventy.config.js │ └── nunjucks-svg │ │ └── index.njk └── test.js ├── .gitignore ├── .npmignore ├── .editorconfig ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── sample ├── sample-config.js └── test.njk ├── package.json ├── src ├── bundlePlucker.js ├── BundleFileOutput.js ├── eleventy.bundleManagers.js ├── eleventy.shortcodes.js ├── eleventy.pruneEmptyBundles.js ├── OutOfOrderRender.js └── CodeManager.js ├── eleventy.bundle.js └── README.md /test/stubs-virtual/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | _site 3 | **/_site -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | _site 3 | sample 4 | .* 5 | -------------------------------------------------------------------------------- /test/stubs/bundle-in-layout/_includes/layout.liquid: -------------------------------------------------------------------------------- 1 | {% getBundle "html", "head" %} -------------------------------------------------------------------------------- /test/stubs/serverless-stubs/functions/test1/eleventy-serverless-map.json: -------------------------------------------------------------------------------- 1 | { 2 | "/": "./test/stubs/serverless-stubs/test1.liquid" 3 | } 4 | -------------------------------------------------------------------------------- /test/stubs/pluck-html-js/index.njk: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /test/stubs/export-key-obj/eleventy.config.js: -------------------------------------------------------------------------------- 1 | export default function(eleventyConfig) { 2 | eleventyConfig.addBundle("css"); 3 | eleventyConfig.addBundle("js"); 4 | }; -------------------------------------------------------------------------------- /test/stubs/export-key-str/eleventy.config.js: -------------------------------------------------------------------------------- 1 | export default function(eleventyConfig) { 2 | eleventyConfig.addBundle("css"); 3 | eleventyConfig.addBundle("js"); 4 | }; -------------------------------------------------------------------------------- /test/stubs/serverless-stubs/test1.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: 3 | test1: / 4 | --- 5 | 6 | {%- css %}* { color: blue; }{% endcss %} 7 | -------------------------------------------------------------------------------- /test/stubs/bundle-in-layout/index.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | layout: layout.liquid 3 | --- 4 | {% html "head" %} 5 | 6 | {% endhtml %} -------------------------------------------------------------------------------- /test/stubs/pluck-html-css/index.njk: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /test/stubs/export-key-str/template.11ty.mjs: -------------------------------------------------------------------------------- 1 | export const bundle = "/* Hello */"; 2 | 3 | export function render(data) { 4 | return ``; 5 | } -------------------------------------------------------------------------------- /test/stubs/export-key-str-rename/eleventy.config.js: -------------------------------------------------------------------------------- 1 | export default function(eleventyConfig) { 2 | eleventyConfig.addBundle("css", { bundleExportKey: "css" }); 3 | eleventyConfig.addBundle("js", { bundleExportKey: "js" }); 4 | }; -------------------------------------------------------------------------------- /test/stubs/serverless-stubs/functions/test1/eleventy.config.js: -------------------------------------------------------------------------------- 1 | import bundlePlugin from "../../../../../eleventy.bundle.js"; 2 | 3 | export default function(eleventyConfig) { 4 | eleventyConfig.addPlugin(bundlePlugin); 5 | }; 6 | -------------------------------------------------------------------------------- /test/stubs/bundle-render/index.liquid: -------------------------------------------------------------------------------- 1 | {%- css -%} 2 | {%- renderTemplate "scss" -%} 3 | h1 { .test { color: red; } } 4 | {%- endrenderTemplate -%} 5 | {%- endcss -%} 6 | 7 | -------------------------------------------------------------------------------- /test/stubs/to-file-duplicates/get-and-get.njk: -------------------------------------------------------------------------------- 1 | {%- css "defer" %}* { color: blue; }{% endcss %} 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/stubs/duplicate-addplugins/eleventy.config.js: -------------------------------------------------------------------------------- 1 | import bundlePlugin from "../../../eleventy.bundle.js"; 2 | 3 | export default function(eleventyConfig) { 4 | eleventyConfig.addPlugin(bundlePlugin, { 5 | bundles: false, 6 | }); 7 | }; -------------------------------------------------------------------------------- /test/stubs/to-file-ordering/to-file-ordering.liquid: -------------------------------------------------------------------------------- 1 | {%- css %}* { color: blue; }{% endcss -%} 2 | 3 | 4 | {%- css %}* { color: rebeccapurple; }{% endcss -%} -------------------------------------------------------------------------------- /test/stubs/export-key-obj/template.11ty.mjs: -------------------------------------------------------------------------------- 1 | export const bundle = { 2 | css: "/* CSS */", 3 | js: "/* JS */" 4 | }; 5 | 6 | export function render(data) { 7 | return ``; 8 | } -------------------------------------------------------------------------------- /test/stubs/export-key-str-rename/template.11ty.mjs: -------------------------------------------------------------------------------- 1 | export const js = "/* JS */"; 2 | export const css = "/* CSS */"; 3 | 4 | export function render(data) { 5 | return ``; 6 | } -------------------------------------------------------------------------------- /test/stubs/to-file-duplicates/get-and-file.njk: -------------------------------------------------------------------------------- 1 | {%- css "defer" %}* { color: blue; }{% endcss %} 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/stubs/to-file-duplicates/eleventy.config.js: -------------------------------------------------------------------------------- 1 | import bundlePlugin from "../../../eleventy.bundle.js"; 2 | 3 | export default function(eleventyConfig) { 4 | eleventyConfig.addPlugin(bundlePlugin, { 5 | hoistDuplicateBundlesFor: ["css", "js"], 6 | }); 7 | }; -------------------------------------------------------------------------------- /test/stubs/use-transforms/index.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | {%- css %}* { color: blue; }{% endcss %} 4 | {%- css %}* { color: blue; }{% endcss %} 5 | {%- css %}* { color: red; }{% endcss %} 6 | {%- css %}#id { * { color: orange; } }{% endcss %} -------------------------------------------------------------------------------- /test/stubs/output-default-multiple-times/eleventy.config.js: -------------------------------------------------------------------------------- 1 | import bundlePlugin from "../../../eleventy.bundle.js"; 2 | 3 | export default function(eleventyConfig) { 4 | eleventyConfig.addPlugin(bundlePlugin, { 5 | hoistDuplicateBundlesFor: ["css", "js"], 6 | }); 7 | }; -------------------------------------------------------------------------------- /test/stubs/output-same-bucket-multiple-times/eleventy.config.js: -------------------------------------------------------------------------------- 1 | import bundlePlugin from "../../../eleventy.bundle.js"; 2 | 3 | export default function(eleventyConfig) { 4 | eleventyConfig.addPlugin(bundlePlugin, { 5 | hoistDuplicateBundlesFor: ["css", "js"], 6 | }); 7 | }; -------------------------------------------------------------------------------- /test/stubs/to-file-duplicates/file-and-file.njk: -------------------------------------------------------------------------------- 1 | {%- css "defer" %}* { color: blue; }{% endcss %} 2 | 3 | 4 | -------------------------------------------------------------------------------- /test/stubs/liquid-js/index.liquid: -------------------------------------------------------------------------------- 1 | 2 | {%- js %}alert(1);{% endjs %} 3 | 4 | {%- js %}alert(2);{% endjs %} 5 | {%- js %}alert(1);{% endjs %} 6 | 7 | {%- js %}alert(3);{% endjs %} -------------------------------------------------------------------------------- /test/stubs/output-default-multiple-times/index.liquid: -------------------------------------------------------------------------------- 1 | {%- css "default" %}* { color: blue; }{% endcss %} 2 | {%- comment %}nothing is hoisted if you output default multiple times{% endcomment %} 3 | 4 | -------------------------------------------------------------------------------- /test/stubs/buckets-get-multiple-comma-sep/index.liquid: -------------------------------------------------------------------------------- 1 | {%- css "defer" %}* { color: blue; }{% endcss %} 2 | {%- css %}* { color: blue; }{% endcss %} 3 | {%- css "defer" %}* { color: red; }{% endcss %} 4 | {%- css %}* { color: orange; }{% endcss -%} 5 | -------------------------------------------------------------------------------- /test/stubs/handlebars/index.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{# css }}* { color: blue; }{{/ css }} 3 | 4 | {{# css }}* { color: blue; }{{/ css }} 5 | {{# css }}* { color: red; }{{/ css }} 6 | 7 | {{# css }}* { color: orange; }{{/ css }} -------------------------------------------------------------------------------- /test/stubs/liquid-buckets/index.liquid: -------------------------------------------------------------------------------- 1 | {%- css "defer" %}* { color: blue; }{% endcss %} 2 | {%- css %}* { color: blue; }{% endcss %} 3 | {%- css "defer" %}* { color: red; }{% endcss %} 4 | {%- css %}* { color: orange; }{% endcss -%} 5 | 6 | -------------------------------------------------------------------------------- /test/stubs/to-file/index.njk: -------------------------------------------------------------------------------- 1 | {%- css %}* { color: blue; }{% endcss %} 2 | {%- css %}* { color: red; }{% endcss %} 3 | {%- css %}* { color: blue; }{% endcss %} 4 | {%- css %}* { color: orange; }/* lololol */{% endcss -%} 5 | 6 | -------------------------------------------------------------------------------- /test/stubs/markdown/index.md: -------------------------------------------------------------------------------- 1 | 2 | {%- css %}* { color: blue; }{% endcss %} 3 | 4 | {%- css %}* { color: blue; }{% endcss %} 5 | {%- css %}* { color: red; }{% endcss %} 6 | 7 | {%- css %}* { color: orange; }{% endcss %} -------------------------------------------------------------------------------- /test/stubs/nunjucks/index.njk: -------------------------------------------------------------------------------- 1 | 2 | {%- css %}* { color: blue; }{% endcss %} 3 | 4 | {%- css %}* { color: blue; }{% endcss %} 5 | {%- css %}* { color: red; }{% endcss %} 6 | 7 | {%- css %}* { color: orange; }{% endcss %} -------------------------------------------------------------------------------- /test/stubs/liquid/index.liquid: -------------------------------------------------------------------------------- 1 | 2 | {%- css %}* { color: blue; }{% endcss %} 3 | 4 | {%- css %}* { color: blue; }{% endcss %} 5 | {%- css %}* { color: red; }{% endcss %} 6 | 7 | {%- css %}* { color: orange; }{% endcss %} -------------------------------------------------------------------------------- /test/stubs/to-file-write/to-file-write.njk: -------------------------------------------------------------------------------- 1 | {%- css %}* { color: blue; }{% endcss %} 2 | {%- css %}* { color: red; }{% endcss %} 3 | {%- css %}* { color: blue; }{% endcss %} 4 | {%- css %}* { color: orange; }/* lololol2 */{% endcss -%} 5 | 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | end_of_line = lf 7 | insert_final_newline = false 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | 11 | [*.js] 12 | insert_final_newline = true 13 | 14 | [/test/stubs*/**] 15 | insert_final_newline = unset 16 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /test/stubs/buckets-ordering/index.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | buckets: 3 | - defer 4 | - default 5 | --- 6 | {%- css "defer" %}* { color: blue; }{% endcss %} 7 | {%- css %}* { color: blue; }{% endcss %} 8 | {%- css "defer" %}* { color: red; }{% endcss %} 9 | {%- css %}* { color: orange; }{% endcss -%} 10 | -------------------------------------------------------------------------------- /test/stubs/buckets-get-multiple/index.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | buckets: 3 | - default 4 | - defer 5 | --- 6 | {%- css "defer" %}* { color: blue; }{% endcss %} 7 | {%- css %}* { color: blue; }{% endcss %} 8 | {%- css "defer" %}* { color: red; }{% endcss %} 9 | {%- css %}* { color: orange; }{% endcss -%} 10 | -------------------------------------------------------------------------------- /test/stubs/no-bundles/index.liquid: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: false 3 | --- 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/stubs/use-transforms-on-type/index.liquid: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {%- css %}* { color: blue; }{% endcss %} 5 | {%- css %}* { color: blue; }{% endcss %} 6 | {%- css %}* { color: red; }{% endcss %} 7 | {%- css %}#id { * { color: orange; } }{% endcss %} 8 | 9 | {%- js %}console.log("bundle me up"){% endjs %} -------------------------------------------------------------------------------- /test/stubs/liquid-buckets-default/index.liquid: -------------------------------------------------------------------------------- 1 | {%- css "defer" %}* { color: blue; }{% endcss %} 2 | {%- css %}* { color: blue; }{% endcss %} 3 | {%- css "defer" %}* { color: red; }{% endcss %} 4 | {%- css %}* { color: orange; }{% endcss -%} 5 | 6 | 7 | -------------------------------------------------------------------------------- /test/stubs/pluck-html-css-buckets/index.njk: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /test/stubs/pluck-html-css/eleventy.config.js: -------------------------------------------------------------------------------- 1 | import bundlePlugin from "../../../eleventy.bundle.js"; 2 | 3 | export default function(eleventyConfig) { 4 | eleventyConfig.addPlugin(bundlePlugin, { 5 | force: true, // for testing 6 | bundles: false, 7 | immediate: true, 8 | }); 9 | 10 | eleventyConfig.addBundle("css", { 11 | bundleHtmlContentFromSelector: "style", 12 | }) 13 | }; -------------------------------------------------------------------------------- /test/stubs/no-prune/eleventy.config.js: -------------------------------------------------------------------------------- 1 | import bundlePlugin from "../../../eleventy.bundle.js"; 2 | 3 | export default function(eleventyConfig) { 4 | eleventyConfig.addPlugin(bundlePlugin, { 5 | pruneEmptySelector: false, 6 | force: true, 7 | }); 8 | 9 | eleventyConfig.addTemplate('test.njk', `
10 | {%- css %} {% endcss %}`); 11 | }; -------------------------------------------------------------------------------- /test/stubs/pluck-html-css-buckets/eleventy.config.js: -------------------------------------------------------------------------------- 1 | import bundlePlugin from "../../../eleventy.bundle.js"; 2 | 3 | export default function(eleventyConfig) { 4 | eleventyConfig.addPlugin(bundlePlugin, { 5 | force: true, // for testing 6 | bundles: false, 7 | immediate: true, 8 | }); 9 | 10 | eleventyConfig.addBundle("css", { 11 | bundleHtmlContentFromSelector: "style", 12 | }) 13 | }; -------------------------------------------------------------------------------- /test/stubs/output-same-bucket-multiple-times/index.liquid: -------------------------------------------------------------------------------- 1 | {%- css "default" %}* { color: blue; }{% endcss %} 2 | {%- css "defer" %}* { color: blue; }{% endcss %} 3 | {%- css "defer" %}* { color: red; }{% endcss %} 4 | 5 | {%- comment %}bucket is hoisted to default if you output it multiple times{% endcomment %} 6 | 7 | -------------------------------------------------------------------------------- /test/stubs/output-same-bucket-multiple-times-nohoist/index.liquid: -------------------------------------------------------------------------------- 1 | {%- css "default" %}* { color: blue; }{% endcss %} 2 | {%- css "defer" %}* { color: blue; }{% endcss %} 3 | {%- css "defer" %}* { color: red; }{% endcss %} 4 | 5 | {%- comment %}bucket is hoisted to default if you output it multiple times{% endcomment %} 6 | 7 | -------------------------------------------------------------------------------- /test/stubs/pluck-html-js/eleventy.config.js: -------------------------------------------------------------------------------- 1 | import bundlePlugin from "../../../eleventy.bundle.js"; 2 | 3 | export default function(eleventyConfig) { 4 | eleventyConfig.addPlugin(bundlePlugin, { 5 | force: true, // for testing 6 | bundles: false, 7 | immediate: true, 8 | }); 9 | 10 | eleventyConfig.addBundle("js", { 11 | bundleHtmlContentFromSelector: "script", 12 | transforms: [ 13 | function(content) { 14 | return `/* Banner from Transforms */ 15 | ${content}`; 16 | } 17 | ] 18 | }) 19 | }; -------------------------------------------------------------------------------- /test/stubs/use-transforms/eleventy.config.js: -------------------------------------------------------------------------------- 1 | import bundlePlugin from "../../../eleventy.bundle.js"; 2 | import postcss from 'postcss'; 3 | import postcssNested from 'postcss-nested'; 4 | 5 | 6 | export default function(eleventyConfig) { 7 | eleventyConfig.addPlugin(bundlePlugin, { 8 | transforms: [async function(content) { 9 | return new Promise(resolve => { 10 | setTimeout(() => resolve(content), 50); 11 | }); 12 | }, async function(content) { 13 | let result = await postcss([postcssNested]).process(content, { from: null, to: null }) 14 | return result.css; 15 | }] 16 | }); 17 | }; -------------------------------------------------------------------------------- /test/stubs/use-transforms-on-type/eleventy.config.js: -------------------------------------------------------------------------------- 1 | import bundlePlugin from "../../../eleventy.bundle.js"; 2 | import postcss from 'postcss'; 3 | import postcssNested from 'postcss-nested'; 4 | 5 | 6 | export default function(eleventyConfig) { 7 | eleventyConfig.addPlugin(bundlePlugin, { 8 | transforms: [async function(content) { 9 | return new Promise(resolve => { 10 | setTimeout(() => resolve(content), 50); 11 | }); 12 | }, async function(content) { 13 | if (this.type === "css") { 14 | let result = await postcss([postcssNested]).process(content, { from: null, to: null }) 15 | return result.css; 16 | } 17 | return content; 18 | }] 19 | }); 20 | }; -------------------------------------------------------------------------------- /test/stubs/delayed-bundle/eleventy.config.js: -------------------------------------------------------------------------------- 1 | import bundlePlugin from "../../../eleventy.bundle.js"; 2 | 3 | export default function(eleventyConfig) { 4 | eleventyConfig.addPlugin(bundlePlugin, { 5 | force: true, 6 | immediate: true, // immediate required for subsequent addBundle call 7 | }); 8 | 9 | // delayed bundles happen *after* transforms 10 | eleventyConfig.addBundle("svg", { delayed: true }); 11 | 12 | eleventyConfig.addTransform("adds-to-bundle", (content) => { 13 | let { svg: svgBundle } = eleventyConfig.getBundleManagers(); 14 | svgBundle.addToPage("/", [ "this is svg" ]); 15 | return content; 16 | }) 17 | 18 | eleventyConfig.addTemplate('index.njk', `testing:{% getBundle "svg" %}`) 19 | }; -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node Unit Tests 2 | on: [push, pull_request] 3 | permissions: read-all 4 | jobs: 5 | build: 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | matrix: 9 | os: ["ubuntu-latest", "macos-latest", "windows-latest"] 10 | node: ["18", "20", "22"] 11 | name: Node.js ${{ matrix.node }} on ${{ matrix.os }} 12 | steps: 13 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # 4.1.7 14 | - name: Setup node 15 | uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # 4.0.3 16 | with: 17 | node-version: ${{ matrix.node }} 18 | # cache: npm 19 | - run: npm install 20 | - run: npm test 21 | env: 22 | YARN_GPG: no 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release to npm 2 | on: 3 | release: 4 | types: [published] 5 | permissions: read-all 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | environment: GitHub Publish 10 | permissions: 11 | contents: read 12 | id-token: write 13 | steps: 14 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # 4.1.7 15 | - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # 4.0.3 16 | with: 17 | node-version: "22" 18 | registry-url: 'https://registry.npmjs.org' 19 | - run: npm install -g npm@latest 20 | - run: npm ci 21 | - run: npm test 22 | - if: ${{ github.event.release.tag_name != '' && env.NPM_PUBLISH_TAG != '' }} 23 | run: npm publish --provenance --access=public --tag=${{ env.NPM_PUBLISH_TAG }} 24 | env: 25 | NPM_PUBLISH_TAG: ${{ contains(github.event.release.tag_name, '-beta.') && 'beta' || 'latest' }} 26 | -------------------------------------------------------------------------------- /sample/sample-config.js: -------------------------------------------------------------------------------- 1 | import bundlePlugin from "../eleventy.bundle.js"; 2 | 3 | export default function(eleventyConfig) { 4 | // This call is what Eleventy will do in the default config in 3.0.0-alpha.10 5 | eleventyConfig.addPlugin(bundlePlugin, { 6 | bundles: false, 7 | immediate: true 8 | }); 9 | 10 | // adds html, css, js (maintain existing API) 11 | eleventyConfig.addPlugin(bundlePlugin, { 12 | toFileDirectory: "bundle1" 13 | }); 14 | 15 | eleventyConfig.addPlugin(eleventyConfig => { 16 | // ignored, already exists 17 | eleventyConfig.addBundle("css"); 18 | // ignored, already exists 19 | eleventyConfig.addBundle("css"); 20 | // ignored, already exists 21 | eleventyConfig.addBundle("css"); 22 | // ignored, already exists 23 | eleventyConfig.addBundle("html"); 24 | }); 25 | 26 | // new! 27 | eleventyConfig.addBundle("stylesheet", { 28 | outputFileExtension: "css", 29 | shortcodeName: "stylesheet", 30 | transforms: [], 31 | // hoist: true, 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@11ty/eleventy-plugin-bundle", 3 | "version": "3.0.7", 4 | "description": "Little bundles of code, little bundles of joy.", 5 | "main": "eleventy.bundle.js", 6 | "type": "module", 7 | "scripts": { 8 | "sample": "DEBUG=Eleventy:Bundle npx @11ty/eleventy --config=sample/sample-config.js --input=sample --serve", 9 | "test": "npx ava" 10 | }, 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "license": "MIT", 15 | "engines": { 16 | "node": ">=18" 17 | }, 18 | "funding": { 19 | "type": "opencollective", 20 | "url": "https://opencollective.com/11ty" 21 | }, 22 | "keywords": [ 23 | "eleventy", 24 | "eleventy-plugin" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "git://github.com/11ty/eleventy-plugin-bundle.git" 29 | }, 30 | "bugs": "https://github.com/11ty/eleventy-plugin-bundle/issues", 31 | "homepage": "https://www.11ty.dev/", 32 | "author": { 33 | "name": "Zach Leatherman", 34 | "email": "zachleatherman@gmail.com", 35 | "url": "https://zachleat.com/" 36 | }, 37 | "ava": { 38 | "failFast": true, 39 | "files": [ 40 | "test/*.js", 41 | "test/*.mjs" 42 | ], 43 | "watchMode": { 44 | "ignoreChanges": [ 45 | "**/_site/**", 46 | ".cache" 47 | ] 48 | } 49 | }, 50 | "devDependencies": { 51 | "@11ty/eleventy": "^3.0.0", 52 | "ava": "^6.4.1", 53 | "postcss": "^8.5.6", 54 | "postcss-nested": "^7.0.2", 55 | "sass": "^1.94.2" 56 | }, 57 | "dependencies": { 58 | "@11ty/eleventy-utils": "^2.0.2", 59 | "debug": "^4.4.3", 60 | "posthtml-match-helper": "^2.0.3" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/bundlePlucker.js: -------------------------------------------------------------------------------- 1 | import debugUtil from "debug"; 2 | import matchHelper from "posthtml-match-helper"; 3 | 4 | const debug = debugUtil("Eleventy:Bundle"); 5 | 6 | const ATTRS = { 7 | ignore: "eleventy:ignore", 8 | bucket: "eleventy:bucket", 9 | }; 10 | 11 | const POSTHTML_PLUGIN_NAME = "11ty/eleventy/html-bundle-plucker"; 12 | 13 | function hasAttribute(node, name) { 14 | return node?.attrs?.[name] !== undefined; 15 | } 16 | 17 | function addHtmlPlucker(eleventyConfig, bundleManager) { 18 | let matchSelector = bundleManager.getPluckedSelector(); 19 | 20 | if(!matchSelector) { 21 | throw new Error("Internal error: missing plucked selector on bundle manager."); 22 | } 23 | 24 | eleventyConfig.htmlTransformer.addPosthtmlPlugin( 25 | "html", 26 | function (context = {}) { 27 | let pageUrl = context?.url; 28 | if(!pageUrl) { 29 | throw new Error("Internal error: missing `url` property from context."); 30 | } 31 | 32 | return function (tree, ...args) { 33 | tree.match(matchHelper(matchSelector), function (node) { 34 | try { 35 | // ignore 36 | if(hasAttribute(node, ATTRS.ignore)) { 37 | delete node.attrs[ATTRS.ignore]; 38 | return node; 39 | } 40 | 41 | if(Array.isArray(node?.content) && node.content.length > 0) { 42 | // TODO make this better decoupled 43 | if(node?.content.find(entry => entry.includes(`/*__EleventyBundle:`))) { 44 | // preserve {% getBundle %} calls as-is 45 | return node; 46 | } 47 | 48 | let bucketName = node?.attrs?.[ATTRS.bucket]; 49 | bundleManager.addToPage(pageUrl, [ ...node.content ], bucketName); 50 | 51 | return { attrs: [], content: [], tag: false }; 52 | } 53 | } catch(e) { 54 | debug(`Bundle plucker: error adding content to bundle in HTML Assets: %o`, e); 55 | return node; 56 | } 57 | 58 | return node; 59 | }); 60 | }; 61 | }, 62 | { 63 | // pluginOptions 64 | name: POSTHTML_PLUGIN_NAME, 65 | }, 66 | ); 67 | } 68 | 69 | export { addHtmlPlucker }; 70 | -------------------------------------------------------------------------------- /sample/test.njk: -------------------------------------------------------------------------------- 1 | 2 | {%- css %}* { color: blue; }{% endcss %} 3 | 4 | {%- css %}* { color: blue; }{% endcss %} 5 | {%- css %}* { color: red; }{% endcss %} 6 | 7 | {%- stylesheet %}/* lololololol sdlkfjkdlsfsldkjflksd sdlfkj */{% endstylesheet %} 8 | 9 | 12 | 13 | 14 | {% html "svg" %} 15 | 16 | 17 | 18 | {% endhtml %} 19 | 20 | And now you can use `icon-close` in as many SVG instances as you’d like (without repeating the heftier SVG content). 21 | 22 | 23 | 24 | 25 | 26 | 27 | {# #} -------------------------------------------------------------------------------- /test/stubs/nunjucks-svg/index.njk: -------------------------------------------------------------------------------- 1 | 4 | 5 | {%- html "svg" -%} 6 | 7 | {%- endhtml %} 8 | 9 | {%- html "svg" -%} 10 | 11 | {%- endhtml %} -------------------------------------------------------------------------------- /src/BundleFileOutput.js: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | import debugUtil from "debug"; 4 | 5 | import { createHash } from "@11ty/eleventy-utils"; 6 | 7 | const debug = debugUtil("Eleventy:Bundle"); 8 | 9 | const hashCache = {}; 10 | const directoryExistsCache = {}; 11 | const writingCache = new Set(); 12 | 13 | class BundleFileOutput { 14 | constructor(outputDirectory, bundleDirectory) { 15 | this.outputDirectory = outputDirectory; 16 | this.bundleDirectory = bundleDirectory || ""; 17 | this.hashLength = 10; 18 | this.fileExtension = undefined; 19 | } 20 | 21 | setFileExtension(ext) { 22 | this.fileExtension = ext; 23 | } 24 | 25 | async getFilenameHash(content) { 26 | if(hashCache[content]) { 27 | return hashCache[content]; 28 | } 29 | 30 | let base64hash = await createHash(content); 31 | let filenameHash = base64hash.substring(0, this.hashLength); 32 | hashCache[content] = filenameHash; 33 | return filenameHash; 34 | } 35 | 36 | getFilename(filename, extension) { 37 | return filename + (extension && !extension.startsWith(".") ? `.${extension}` : ""); 38 | } 39 | 40 | modifyPathToUrl(dir, filename) { 41 | return "/" + path.join(dir, filename).split(path.sep).join("/"); 42 | } 43 | 44 | async writeBundle(content, type, writeToFileSystem) { 45 | // do not write a bundle, do not return a file name is content is empty 46 | if(!content) { 47 | return; 48 | } 49 | 50 | let dir = path.join(this.outputDirectory, this.bundleDirectory); 51 | let filenameHash = await this.getFilenameHash(content); 52 | let filename = this.getFilename(filenameHash, this.fileExtension || type); 53 | 54 | if(writeToFileSystem) { 55 | let fullPath = path.join(dir, filename); 56 | 57 | // no duplicate writes, this may be improved with a fs exists check, but it would only save the first write 58 | if(!writingCache.has(fullPath)) { 59 | writingCache.add(fullPath); 60 | 61 | if(!directoryExistsCache[dir]) { 62 | fs.mkdirSync(dir, { recursive: true }); 63 | directoryExistsCache[dir] = true; 64 | } 65 | 66 | debug("Writing bundle %o", fullPath); 67 | fs.writeFileSync(fullPath, content); 68 | } 69 | } 70 | 71 | return this.modifyPathToUrl(this.bundleDirectory, filename); 72 | } 73 | } 74 | 75 | export { BundleFileOutput }; 76 | -------------------------------------------------------------------------------- /src/eleventy.bundleManagers.js: -------------------------------------------------------------------------------- 1 | import debugUtil from "debug"; 2 | import { CodeManager } from "./CodeManager.js"; 3 | import { addHtmlPlucker } from "./bundlePlucker.js" 4 | 5 | const debug = debugUtil("Eleventy:Bundle"); 6 | 7 | function eleventyBundleManagers(eleventyConfig, pluginOptions = {}) { 8 | if(pluginOptions.force) { 9 | // no errors 10 | } else if(("getBundleManagers" in eleventyConfig || "addBundle" in eleventyConfig)) { 11 | throw new Error("Duplicate addPlugin calls for @11ty/eleventy-plugin-bundle"); 12 | } 13 | 14 | let managers = {}; 15 | 16 | function addBundle(name, bundleOptions = {}) { 17 | if(name in managers) { 18 | // note: shortcode must still be added 19 | debug("Bundle exists %o, skipping.", name); 20 | } else { 21 | debug("Creating new bundle %o", name); 22 | managers[name] = new CodeManager(name); 23 | 24 | if(bundleOptions.delayed !== undefined) { 25 | managers[name].setDelayed(bundleOptions.delayed); 26 | } 27 | 28 | if(bundleOptions.hoist !== undefined) { 29 | managers[name].setHoisting(bundleOptions.hoist); 30 | } 31 | 32 | if(bundleOptions.bundleHtmlContentFromSelector !== undefined) { 33 | managers[name].setPluckedSelector(bundleOptions.bundleHtmlContentFromSelector); 34 | managers[name].setDelayed(true); // must override `delayed` above 35 | 36 | addHtmlPlucker(eleventyConfig, managers[name]); 37 | } 38 | 39 | if(bundleOptions.bundleExportKey !== undefined) { 40 | managers[name].setBundleExportKey(bundleOptions.bundleExportKey); 41 | } 42 | 43 | if(bundleOptions.outputFileExtension) { 44 | managers[name].setFileExtension(bundleOptions.outputFileExtension); 45 | } 46 | 47 | if(bundleOptions.toFileDirectory) { 48 | managers[name].setBundleDirectory(bundleOptions.toFileDirectory); 49 | } 50 | 51 | if(bundleOptions.transforms) { 52 | managers[name].setTransforms(bundleOptions.transforms); 53 | } 54 | } 55 | 56 | // if undefined, defaults to `name` 57 | if(bundleOptions.shortcodeName !== false) { 58 | let shortcodeName = bundleOptions.shortcodeName || name; 59 | 60 | // e.g. `css` shortcode to add code to page bundle 61 | // These shortcode names are not configurable on purpose (for wider plugin compatibility) 62 | eleventyConfig.addPairedShortcode(shortcodeName, function addContent(content, bucket, explicitUrl) { 63 | let url = explicitUrl || this.page?.url; 64 | if(url) { // don’t add if a file doesn’t have an output URL 65 | managers[name].addToPage(url, content, bucket); 66 | } 67 | return ""; 68 | }); 69 | } 70 | }; 71 | 72 | eleventyConfig.addBundle = addBundle; 73 | 74 | eleventyConfig.getBundleManagers = function() { 75 | return managers; 76 | }; 77 | 78 | eleventyConfig.on("eleventy.before", async () => { 79 | for(let key in managers) { 80 | managers[key].reset(); 81 | } 82 | }); 83 | }; 84 | 85 | export default eleventyBundleManagers; 86 | -------------------------------------------------------------------------------- /eleventy.bundle.js: -------------------------------------------------------------------------------- 1 | import bundleManagersPlugin from "./src/eleventy.bundleManagers.js"; 2 | import pruneEmptyBundlesPlugin from "./src/eleventy.pruneEmptyBundles.js"; 3 | import globalShortcodesAndTransforms from "./src/eleventy.shortcodes.js"; 4 | import debugUtil from "debug"; 5 | 6 | const debug = debugUtil("Eleventy:Bundle"); 7 | 8 | function normalizeOptions(options = {}) { 9 | options = Object.assign({ 10 | // Plugin defaults 11 | 12 | // Extra bundles 13 | // css, js, and html are guaranteed unless `bundles: false` 14 | bundles: [], 15 | toFileDirectory: "bundle", 16 | // post-process 17 | transforms: [], 18 | hoistDuplicateBundlesFor: [], 19 | bundleExportKey: "bundle", // use a `bundle` export in a 11ty.js template to populate bundles 20 | 21 | force: false, // force overwrite of existing getBundleManagers and addBundle configuration API methods 22 | }, options); 23 | 24 | if(options.bundles !== false) { 25 | options.bundles = Array.from(new Set(["css", "js", "html", ...(options.bundles || [])])); 26 | } 27 | 28 | return options; 29 | } 30 | 31 | function eleventyBundlePlugin(eleventyConfig, pluginOptions = {}) { 32 | eleventyConfig.versionCheck(">=3.0.0"); 33 | pluginOptions = normalizeOptions(pluginOptions); 34 | 35 | let alreadyAdded = "getBundleManagers" in eleventyConfig || "addBundle" in eleventyConfig; 36 | if(!alreadyAdded || pluginOptions.force) { 37 | if(alreadyAdded && pluginOptions.force) { 38 | debug("Bundle plugin already added via `addPlugin`, add was forced via `force: true`"); 39 | } 40 | 41 | bundleManagersPlugin(eleventyConfig, pluginOptions); 42 | } 43 | 44 | // These can’t be unique (don’t skip re-add above), when the configuration file resets they need to be added again 45 | pruneEmptyBundlesPlugin(eleventyConfig, pluginOptions); 46 | globalShortcodesAndTransforms(eleventyConfig, pluginOptions); 47 | 48 | // Support subsequent calls like addPlugin(BundlePlugin, { bundles: [] }); 49 | if(Array.isArray(pluginOptions.bundles)) { 50 | debug("Adding bundles via `addPlugin`: %o", pluginOptions.bundles) 51 | pluginOptions.bundles.forEach(name => { 52 | let isHoisting = Array.isArray(pluginOptions.hoistDuplicateBundlesFor) && pluginOptions.hoistDuplicateBundlesFor.includes(name); 53 | 54 | eleventyConfig.addBundle(name, { 55 | hoist: isHoisting, 56 | outputFileExtension: name, // default as `name` 57 | shortcodeName: name, // `false` will skip shortcode 58 | transforms: pluginOptions.transforms, 59 | toFileDirectory: pluginOptions.toFileDirectory, 60 | bundleExportKey: pluginOptions.bundleExportKey, // `false` will skip bundle export 61 | }); 62 | }); 63 | } 64 | }; 65 | 66 | // This is used to find the package name for this plugin (used in eleventy-plugin-webc to prevent dupes) 67 | Object.defineProperty(eleventyBundlePlugin, "eleventyPackage", { 68 | value: "@11ty/eleventy-plugin-bundle" 69 | }); 70 | 71 | export default eleventyBundlePlugin; 72 | export { normalizeOptions }; 73 | -------------------------------------------------------------------------------- /src/eleventy.shortcodes.js: -------------------------------------------------------------------------------- 1 | import { OutOfOrderRender } from "./OutOfOrderRender.js"; 2 | import debugUtil from "debug"; 3 | 4 | const debug = debugUtil("Eleventy:Bundle"); 5 | 6 | export default function(eleventyConfig, pluginOptions = {}) { 7 | let managers = eleventyConfig.getBundleManagers(); 8 | let writeToFileSystem = true; 9 | 10 | function bundleTransform(content, stage = 0) { 11 | // Only run if content is string 12 | // Only run if managers are in play 13 | if(typeof content !== "string" || Object.keys(managers).length === 0) { 14 | return content; 15 | } 16 | 17 | debug("Processing %o", this.page.url); 18 | let render = new OutOfOrderRender(content); 19 | for(let key in managers) { 20 | render.setAssetManager(key, managers[key]); 21 | } 22 | 23 | render.setOutputDirectory(eleventyConfig.directories.output); 24 | render.setWriteToFileSystem(writeToFileSystem); 25 | 26 | return render.replaceAll(this.page, stage); 27 | } 28 | 29 | eleventyConfig.on("eleventy.before", async ({ outputMode }) => { 30 | if(Object.keys(managers).length === 0) { 31 | return; 32 | } 33 | 34 | if(outputMode !== "fs") { 35 | writeToFileSystem = false; 36 | debug("Skipping writing to the file system due to output mode: %o", outputMode); 37 | } 38 | }); 39 | 40 | // e.g. `getBundle` shortcode to get code in current page bundle 41 | // bucket can be an array 42 | // This shortcode name is not configurable on purpose (for wider plugin compatibility) 43 | eleventyConfig.addShortcode("getBundle", function getContent(type, bucket, explicitUrl) { 44 | if(!type || !(type in managers) || Object.keys(managers).length === 0) { 45 | throw new Error(`Invalid bundle type: ${type}. Available options: ${Object.keys(managers)}`); 46 | } 47 | 48 | return OutOfOrderRender.getAssetKey("get", type, bucket); 49 | }); 50 | 51 | // write a bundle to the file system 52 | // This shortcode name is not configurable on purpose (for wider plugin compatibility) 53 | eleventyConfig.addShortcode("getBundleFileUrl", function(type, bucket, explicitUrl) { 54 | if(!type || !(type in managers) || Object.keys(managers).length === 0) { 55 | throw new Error(`Invalid bundle type: ${type}. Available options: ${Object.keys(managers)}`); 56 | } 57 | 58 | return OutOfOrderRender.getAssetKey("file", type, bucket); 59 | }); 60 | 61 | eleventyConfig.addTransform("@11ty/eleventy-bundle", function (content) { 62 | let hasNonDelayedManagers = Boolean(Object.values(eleventyConfig.getBundleManagers()).find(manager => { 63 | return typeof manager.isDelayed !== "function" || !manager.isDelayed(); 64 | })); 65 | if(hasNonDelayedManagers) { 66 | return bundleTransform.call(this, content, 0); 67 | } 68 | return content; 69 | }); 70 | 71 | eleventyConfig.addPlugin((eleventyConfig) => { 72 | // Delayed bundles *MUST* not alter URLs 73 | eleventyConfig.addTransform("@11ty/eleventy-bundle/delayed", function (content) { 74 | let hasDelayedManagers = Boolean(Object.values(eleventyConfig.getBundleManagers()).find(manager => { 75 | return typeof manager.isDelayed === "function" && manager.isDelayed(); 76 | })); 77 | if(hasDelayedManagers) { 78 | return bundleTransform.call(this, content, 1); 79 | } 80 | return content; 81 | }); 82 | }); 83 | }; 84 | -------------------------------------------------------------------------------- /src/eleventy.pruneEmptyBundles.js: -------------------------------------------------------------------------------- 1 | import matchHelper from "posthtml-match-helper"; 2 | import debugUtil from "debug"; 3 | 4 | const debug = debugUtil("Eleventy:Bundle"); 5 | 6 | const ATTRS = { 7 | keep: "eleventy:keep" 8 | }; 9 | 10 | const POSTHTML_PLUGIN_NAME = "11ty/eleventy-bundle/prune-empty"; 11 | 12 | function getTextNodeContent(node) { 13 | if (!node.content) { 14 | return ""; 15 | } 16 | 17 | return node.content 18 | .map((entry) => { 19 | if (typeof entry === "string") { 20 | return entry; 21 | } 22 | if (Array.isArray(entry.content)) { 23 | return getTextNodeContent(entry); 24 | } 25 | return ""; 26 | }) 27 | .join(""); 28 | } 29 | 30 | function eleventyPruneEmptyBundles(eleventyConfig, options = {}) { 31 | // Right now script[src],link[rel="stylesheet"] nodes are removed if the final bundles are empty. 32 | // `false` to disable 33 | options.pruneEmptySelector = options.pruneEmptySelector ?? `style,script,link[rel="stylesheet"]`; 34 | 35 | // Subsequent call can remove a previously added `addPosthtmlPlugin` entry 36 | // htmlTransformer.remove is v3.0.1-alpha.4+ 37 | if(typeof eleventyConfig.htmlTransformer.remove === "function") { 38 | eleventyConfig.htmlTransformer.remove("html", entry => { 39 | if(entry.name === POSTHTML_PLUGIN_NAME) { 40 | return true; 41 | } 42 | 43 | // Temporary workaround for missing `name` property. 44 | let fnStr = entry.fn.toString(); 45 | return !entry.name && fnStr.startsWith("function (pluginOptions = {}) {") && fnStr.includes(`tree.match(matchHelper(options.pruneEmptySelector), function (node)`); 46 | }); 47 | } 48 | 49 | // `false` disables this plugin 50 | if(options.pruneEmptySelector === false) { 51 | return; 52 | } 53 | 54 | if(!eleventyConfig.htmlTransformer || !eleventyConfig.htmlTransformer?.constructor?.SUPPORTS_PLUGINS_ENABLED_CALLBACK) { 55 | debug("You will need to upgrade your version of Eleventy core to remove empty bundle tags automatically (v3 or newer)."); 56 | return; 57 | } 58 | 59 | eleventyConfig.htmlTransformer.addPosthtmlPlugin( 60 | "html", 61 | function bundlePruneEmptyPosthtmlPlugin(pluginOptions = {}) { 62 | return function (tree) { 63 | tree.match(matchHelper(options.pruneEmptySelector), function (node) { 64 | if(node.attrs && node.attrs[ATTRS.keep] !== undefined) { 65 | delete node.attrs[ATTRS.keep]; 66 | return node; 67 | } 68 | 69 | // 70 | if(node.tag === "link") { 71 | if(node.attrs?.rel === "stylesheet" && (node.attrs?.href || "").trim().length === 0) { 72 | return false; 73 | } 74 | } else { 75 | let content = getTextNodeContent(node); 76 | 77 | if(!content) { 78 | // or 79 | if(node.tag === "script" && (node.attrs?.src || "").trim().length === 0) { 80 | return false; 81 | } 82 | 83 | // 84 | if(node.tag === "style") { 85 | return false; 86 | } 87 | } 88 | } 89 | 90 | 91 | return node; 92 | }); 93 | }; 94 | }, 95 | { 96 | name: POSTHTML_PLUGIN_NAME, 97 | // the `enabled` callback for plugins is available on v3.0.0-alpha.20+ and v3.0.0-beta.2+ 98 | enabled: () => { 99 | return Object.keys(eleventyConfig.getBundleManagers()).length > 0; 100 | } 101 | } 102 | ); 103 | } 104 | 105 | export default eleventyPruneEmptyBundles; 106 | -------------------------------------------------------------------------------- /src/OutOfOrderRender.js: -------------------------------------------------------------------------------- 1 | import debugUtil from "debug"; 2 | 3 | const debug = debugUtil("Eleventy:Bundle"); 4 | 5 | /* This class defers any `bundleGet` calls to a post-build transform step, 6 | * to allow `getBundle` to be called before all of the `css` additions have been processed 7 | */ 8 | class OutOfOrderRender { 9 | static SPLIT_REGEX = /(\/\*__EleventyBundle:[^:]*:[^:]*:[^:]*:EleventyBundle__\*\/)/; 10 | static SEPARATOR = ":"; 11 | 12 | constructor(content) { 13 | this.content = content; 14 | this.managers = {}; 15 | } 16 | 17 | // type if `get` (return string) or `file` (bundle writes to file, returns file url) 18 | static getAssetKey(type, name, bucket) { 19 | if(Array.isArray(bucket)) { 20 | bucket = bucket.join(","); 21 | } else if(typeof bucket === "string") { 22 | } else { 23 | bucket = ""; 24 | } 25 | return `/*__EleventyBundle:${type}:${name}:${bucket || "default"}:EleventyBundle__*/` 26 | } 27 | 28 | static parseAssetKey(str) { 29 | if(str.startsWith("/*__EleventyBundle:")) { 30 | let [prefix, type, name, bucket, suffix] = str.split(OutOfOrderRender.SEPARATOR); 31 | return { type, name, bucket }; 32 | } 33 | return false; 34 | } 35 | 36 | setAssetManager(name, assetManager) { 37 | this.managers[name] = assetManager; 38 | } 39 | 40 | setOutputDirectory(dir) { 41 | this.outputDirectory = dir; 42 | } 43 | 44 | normalizeMatch(match) { 45 | let ret = OutOfOrderRender.parseAssetKey(match) 46 | return ret || match; 47 | } 48 | 49 | findAll() { 50 | let matches = this.content.split(OutOfOrderRender.SPLIT_REGEX); 51 | let ret = []; 52 | for(let match of matches) { 53 | ret.push(this.normalizeMatch(match)); 54 | } 55 | return ret; 56 | } 57 | 58 | setWriteToFileSystem(isWrite) { 59 | this.writeToFileSystem = isWrite; 60 | } 61 | 62 | getAllBucketsForPage(pageData) { 63 | let availableBucketsForPage = new Set(); 64 | for(let name in this.managers) { 65 | for(let bucket of this.managers[name].getBucketsForPage(pageData)) { 66 | availableBucketsForPage.add(`${name}::${bucket}`); 67 | } 68 | } 69 | return availableBucketsForPage; 70 | } 71 | 72 | getManager(name) { 73 | if(!this.managers[name]) { 74 | throw new Error(`No asset manager found for ${name}. Known names: ${Object.keys(this.managers)}`); 75 | } 76 | return this.managers[name]; 77 | } 78 | 79 | async replaceAll(pageData, stage = 0) { 80 | let matches = this.findAll(); 81 | let availableBucketsForPage = this.getAllBucketsForPage(pageData); 82 | let usedBucketsOnPage = new Set(); 83 | let bucketsOutputStringCount = {}; 84 | let bucketsFileCount = {}; 85 | 86 | for(let match of matches) { 87 | if(typeof match === "string") { 88 | continue; 89 | } 90 | 91 | // type is `file` or `get` 92 | let {type, name, bucket} = match; 93 | let key = `${name}::${bucket}`; 94 | if(!usedBucketsOnPage.has(key)) { 95 | usedBucketsOnPage.add(key); 96 | } 97 | 98 | if(type === "get") { 99 | if(!bucketsOutputStringCount[key]) { 100 | bucketsOutputStringCount[key] = 0; 101 | } 102 | bucketsOutputStringCount[key]++; 103 | } else if(type === "file") { 104 | if(!bucketsFileCount[key]) { 105 | bucketsFileCount[key] = 0; 106 | } 107 | bucketsFileCount[key]++; 108 | } 109 | } 110 | 111 | // Hoist code in non-default buckets that are output multiple times 112 | // Only hoist if 2+ `get` OR 1+ `get` and 1+ `file` 113 | for(let bucketInfo in bucketsOutputStringCount) { 114 | let stringOutputCount = bucketsOutputStringCount[bucketInfo]; 115 | if(stringOutputCount > 1 || stringOutputCount === 1 && bucketsFileCount[bucketInfo] > 0) { 116 | let [name, bucketName] = bucketInfo.split("::"); 117 | this.getManager(name).hoistBucket(pageData, bucketName); 118 | } 119 | } 120 | 121 | let content = await Promise.all(matches.map(match => { 122 | if(typeof match === "string") { 123 | return match; 124 | } 125 | 126 | let {type, name, bucket} = match; 127 | let manager = this.getManager(name); 128 | 129 | // Quit early if in stage 0, run delayed replacements if in stage 1+ 130 | if(typeof manager.isDelayed === "function" && manager.isDelayed() && stage === 0) { 131 | return OutOfOrderRender.getAssetKey(type, name, bucket); 132 | } 133 | 134 | if(type === "get") { 135 | // returns promise 136 | return manager.getForPage(pageData, bucket); 137 | } else if(type === "file") { 138 | // returns promise 139 | return manager.writeBundle(pageData, bucket, { 140 | output: this.outputDirectory, 141 | write: this.writeToFileSystem, 142 | }); 143 | } 144 | return ""; 145 | })); 146 | 147 | for(let bucketInfo of availableBucketsForPage) { 148 | if(!usedBucketsOnPage.has(bucketInfo)) { 149 | let [name, bucketName] = bucketInfo.split("::"); 150 | debug(`WARNING! \`${pageData.inputPath}\` has unbundled \`${name}\` assets (in the '${bucketName}' bucket) that were not written to or used on the page. You might want to add a call to \`getBundle('${name}', '${bucketName}')\` to your content! Learn more: https://github.com/11ty/eleventy-plugin-bundle#asset-bucketing`); 151 | } 152 | } 153 | 154 | return content.join(""); 155 | } 156 | } 157 | 158 | export { OutOfOrderRender }; 159 | -------------------------------------------------------------------------------- /src/CodeManager.js: -------------------------------------------------------------------------------- 1 | import { BundleFileOutput } from "./BundleFileOutput.js"; 2 | import debugUtil from "debug"; 3 | 4 | const debug = debugUtil("Eleventy:Bundle"); 5 | const DEBUG_LOG_TRUNCATION_SIZE = 200; 6 | 7 | class CodeManager { 8 | // code is placed in this bucket by default 9 | static DEFAULT_BUCKET_NAME = "default"; 10 | 11 | // code is hoisted to this bucket when necessary 12 | static HOISTED_BUCKET_NAME = "default"; 13 | 14 | constructor(name) { 15 | this.name = name; 16 | this.trimOnAdd = true; 17 | // TODO unindent on add 18 | this.reset(); 19 | this.transforms = []; 20 | this.isHoisting = true; 21 | this.fileExtension = undefined; 22 | this.toFileDirectory = undefined; 23 | this.bundleExportKey = "bundle"; 24 | this.runsAfterHtmlTransformer = false; 25 | this.pluckedSelector = undefined; 26 | } 27 | 28 | setDelayed(isDelayed) { 29 | this.runsAfterHtmlTransformer = Boolean(isDelayed); 30 | } 31 | 32 | isDelayed() { 33 | return this.runsAfterHtmlTransformer; 34 | } 35 | 36 | // posthtml-match-selector friendly 37 | setPluckedSelector(selector) { 38 | this.pluckedSelector = selector; 39 | } 40 | 41 | getPluckedSelector() { 42 | return this.pluckedSelector; 43 | } 44 | 45 | setFileExtension(ext) { 46 | this.fileExtension = ext; 47 | } 48 | 49 | setHoisting(enabled) { 50 | this.isHoisting = !!enabled; 51 | } 52 | 53 | setBundleDirectory(dir) { 54 | this.toFileDirectory = dir; 55 | } 56 | 57 | setBundleExportKey(key) { 58 | this.bundleExportKey = key; 59 | } 60 | 61 | getBundleExportKey() { 62 | return this.bundleExportKey; 63 | } 64 | 65 | reset() { 66 | this.pages = {}; 67 | } 68 | 69 | static normalizeBuckets(bucket) { 70 | if(Array.isArray(bucket)) { 71 | return bucket; 72 | } else if(typeof bucket === "string") { 73 | return bucket.split(","); 74 | } 75 | return [CodeManager.DEFAULT_BUCKET_NAME]; 76 | } 77 | 78 | setTransforms(transforms) { 79 | if(!Array.isArray(transforms)) { 80 | throw new Error("Array expected to setTransforms"); 81 | } 82 | 83 | this.transforms = transforms; 84 | } 85 | 86 | _initBucket(pageUrl, bucket) { 87 | if(!this.pages[pageUrl][bucket]) { 88 | this.pages[pageUrl][bucket] = new Set(); 89 | } 90 | } 91 | 92 | addToPage(pageUrl, code = [], bucket) { 93 | if(!Array.isArray(code) && code) { 94 | code = [code]; 95 | } 96 | if(code.length === 0) { 97 | return; 98 | } 99 | 100 | if(!this.pages[pageUrl]) { 101 | this.pages[pageUrl] = {}; 102 | } 103 | 104 | let buckets = CodeManager.normalizeBuckets(bucket); 105 | 106 | let codeContent = code.map(entry => { 107 | if(this.trimOnAdd) { 108 | return entry.trim(); 109 | } 110 | return entry; 111 | }); 112 | 113 | 114 | for(let b of buckets) { 115 | this._initBucket(pageUrl, b); 116 | 117 | for(let content of codeContent) { 118 | if(content) { 119 | if(!this.pages[pageUrl][b].has(content)) { 120 | debug("Adding code to bundle %o for %o (bucket: %o, size: %o): %o", this.name, pageUrl, b, content.length, content.length > DEBUG_LOG_TRUNCATION_SIZE ? content.slice(0, DEBUG_LOG_TRUNCATION_SIZE) + "…" : content); 121 | this.pages[pageUrl][b].add(content); 122 | } 123 | } 124 | } 125 | } 126 | } 127 | 128 | async runTransforms(str, pageData, buckets) { 129 | for (let callback of this.transforms) { 130 | str = await callback.call( 131 | { 132 | page: pageData, 133 | type: this.name, 134 | buckets: buckets 135 | }, 136 | str 137 | ); 138 | } 139 | 140 | return str; 141 | } 142 | 143 | getBucketsForPage(pageData) { 144 | let pageUrl = pageData.url; 145 | if(!this.pages[pageUrl]) { 146 | return []; 147 | } 148 | return Object.keys(this.pages[pageUrl]); 149 | } 150 | 151 | getRawForPage(pageData, buckets = undefined) { 152 | let url = pageData.url; 153 | if(!this.pages[url]) { 154 | debug("No bundle code found for %o on %o, %O", this.name, url, this.pages); 155 | return new Set(); 156 | } 157 | 158 | buckets = CodeManager.normalizeBuckets(buckets); 159 | 160 | let set = new Set(); 161 | let size = 0; 162 | for(let b of buckets) { 163 | if(!this.pages[url][b]) { 164 | // Just continue, if you retrieve code from a bucket that doesn’t exist or has no code, it will return an empty set 165 | continue; 166 | } 167 | 168 | for(let entry of this.pages[url][b]) { 169 | size += entry.length; 170 | set.add(entry); 171 | } 172 | } 173 | 174 | debug("Retrieving %o for %o (buckets: %o, entries: %o, size: %o)", this.name, url, buckets, set.size, size); 175 | return set; 176 | } 177 | 178 | async getForPage(pageData, buckets = undefined) { 179 | let set = this.getRawForPage(pageData, buckets); 180 | let bundleContent = Array.from(set).join("\n"); 181 | 182 | // returns promise 183 | return this.runTransforms(bundleContent, pageData, buckets); 184 | } 185 | 186 | async writeBundle(pageData, buckets, options = {}) { 187 | let url = pageData.url; 188 | if(!this.pages[url]) { 189 | debug("No bundle code found for %o on %o, %O", this.name, url, this.pages); 190 | return ""; 191 | } 192 | 193 | let { output, write } = options; 194 | 195 | buckets = CodeManager.normalizeBuckets(buckets); 196 | 197 | // TODO the bundle output URL might be useful in the transforms for sourcemaps 198 | let content = await this.getForPage(pageData, buckets); 199 | let writer = new BundleFileOutput(output, this.toFileDirectory); 200 | writer.setFileExtension(this.fileExtension); 201 | return writer.writeBundle(content, this.name, write); 202 | } 203 | 204 | // Used when a bucket is output multiple times on a page and needs to be hoisted 205 | hoistBucket(pageData, bucketName) { 206 | let newTargetBucketName = CodeManager.HOISTED_BUCKET_NAME; 207 | if(!this.isHoisting || bucketName === newTargetBucketName) { 208 | return; 209 | } 210 | 211 | let url = pageData.url; 212 | if(!this.pages[url] || !this.pages[url][bucketName]) { 213 | debug("No bundle code found for %o on %o, %O", this.name, url, this.pages); 214 | return; 215 | } 216 | 217 | debug("Code in bucket (%o) is being hoisted to a new bucket (%o)", bucketName, newTargetBucketName); 218 | 219 | this._initBucket(url, newTargetBucketName); 220 | 221 | for(let codeEntry of this.pages[url][bucketName]) { 222 | this.pages[url][bucketName].delete(codeEntry); 223 | this.pages[url][newTargetBucketName].add(codeEntry); 224 | } 225 | 226 | // delete the bucket 227 | delete this.pages[url][bucketName]; 228 | } 229 | } 230 | 231 | export { CodeManager }; 232 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eleventy-plugin-bundle 2 | 3 | Little bundles of code, little bundles of joy. 4 | 5 | Create minimal per-page or app-level bundles of CSS, JavaScript, or HTML to be included in your Eleventy project. 6 | 7 | Makes it easy to implement Critical CSS, in-use-only CSS/JS bundles, SVG icon libraries, or secondary HTML content to load via XHR. 8 | 9 | ## Why? 10 | 11 | This project is a minimum-viable-bundler and asset pipeline in Eleventy. It does not perform any transpilation or code manipulation (by default). The code you put in is the code you get out (with configurable `transforms` if you’d like to modify the code). 12 | 13 | For more larger, more complex use cases you may want to use a more full featured bundler like Vite, Parcel, Webpack, rollup, esbuild, or others. 14 | 15 | But do note that a full-featured bundler has a significant build performance cost, so take care to weigh the cost of using that style of bundler against whether or not this plugin has sufficient functionality for your use case—especially as the platform matures and we see diminishing returns on code transpilation (ES modules everywhere). 16 | 17 | ## Installation 18 | 19 | No installation necessary. Starting with Eleventy `v3.0.0-alpha.10` and newer, this plugin is now bundled with Eleventy. 20 | 21 | ## Usage 22 | 23 | By default, Bundle Plugin v2.0 does not include any default bundles. You must add these yourself via `eleventyConfig.addBundle`. One notable exception happens when using the WebC Eleventy Plugin, which adds `css`, `js`, and `html` bundles for you. 24 | 25 | To create a bundle type, use `eleventyConfig.addBundle` in your Eleventy configuration file (default `.eleventy.js`): 26 | 27 | ```js 28 | // .eleventy.js 29 | export default function(eleventyConfig) { 30 | eleventyConfig.addBundle("css"); 31 | }; 32 | ``` 33 | 34 | This does two things: 35 | 36 | 1. Creates a new `css` shortcode for adding arbitrary code to this bundle 37 | 2. Adds `"css"` as an eligible type argument to the `getBundle` and `getBundleFileUrl` shortcodes. 38 | 39 | ### Full options list 40 | 41 | ```js 42 | export default function(eleventyConfig) { 43 | eleventyConfig.addBundle("css", { 44 | // (Optional) Folder (relative to output directory) files will write to 45 | toFileDirectory: "bundle", 46 | 47 | // (Optional) File extension used for bundle file output, defaults to bundle name 48 | outputFileExtension: "css", 49 | 50 | // (Optional) Name of shortcode for use in templates, defaults to bundle name 51 | shortcodeName: "css", 52 | // shortcodeName: false, // disable this feature. 53 | 54 | // (Optional) Modify bundle content 55 | transforms: [], 56 | 57 | // (Optional) If two identical code blocks exist in non-default buckets, they’ll be hoisted to the first bucket in common. 58 | hoist: true, 59 | 60 | // (Optional) In 11ty.js templates, having a named export of `bundle` will populate your bundles. 61 | bundleExportKey: "bundle", 62 | // bundleExportKey: false, // disable this feature. 63 | }); 64 | }; 65 | ``` 66 | 67 | Read more about [`hoist` and duplicate bundle hoisting](https://github.com/11ty/eleventy-plugin-bundle/issues/5). 68 | 69 | ### Universal Shortcodes 70 | 71 | The following Universal Shortcodes (available in `njk`, `liquid`, `hbs`, `11ty.js`, and `webc`) are provided by this plugin: 72 | 73 | * `getBundle` to retrieve bundled code as a string. 74 | * `getBundleFileUrl` to create a bundle file on disk and retrieve the URL to that file. 75 | 76 | Here’s a [real-world commit showing this in use on the `eleventy-base-blog` project](https://github.com/11ty/eleventy-base-blog/commit/c9595d8f42752fa72c66991c71f281ea960840c9?diff=split). 77 | 78 | ### Example: Add bundle code in a Markdown file in Eleventy 79 | 80 | ```md 81 | # My Blog Post 82 | 83 | This is some content, I am writing markup. 84 | 85 | {% css %} 86 | em { font-style: italic; } 87 | {% endcss %} 88 | 89 | ## More Markdown 90 | 91 | {% css %} 92 | strong { font-weight: bold; } 93 | {% endcss %} 94 | ``` 95 | 96 | Renders to: 97 | 98 | ```html 99 |

My Blog Post

100 | 101 |

This is some content, I am writing markup.

102 | 103 |

More Markdown

104 | ``` 105 | 106 | Note that the bundled code is excluded! 107 | 108 | _There are a few [more examples below](#examples)!_ 109 | 110 | ### Render bundle code 111 | 112 | ```html 113 | 114 | 115 | 116 | 120 | {% css %}* { color: orange; }{% endcss %} 121 | ``` 122 | 123 | ### Write a bundle to a file 124 | 125 | Writes the bundle content to a content-hashed file location in your output directory and returns the URL to the file for use like this: 126 | 127 | ```html 128 | 129 | ``` 130 | 131 | Note that writing bundles to files will likely be slower for empty-cache first time visitors but better cached in the browser for repeat-views (and across multiple pages, too). 132 | 133 | ### Asset bucketing 134 | 135 | ```html 136 | 137 | {% css "defer" %}em { font-style: italic; }{% endcss %} 138 | ``` 139 | 140 | ```html 141 | 142 | 143 | 144 | ``` 145 | 146 | A `default` bucket is implied: 147 | 148 | ```html 149 | 150 | {% css %}em { font-style: italic; }{% endcss %} 151 | {% css "default" %}em { font-style: italic; }{% endcss %} 152 | 153 | 154 | 155 | 156 | ``` 157 | 158 | ### Examples 159 | 160 | #### Critical CSS 161 | 162 | ```js 163 | // .eleventy.js 164 | export default function(eleventyConfig) { 165 | eleventyConfig.addBundle("css"); 166 | }; 167 | ``` 168 | 169 | Use asset bucketing to divide CSS between the `default` bucket and a `defer` bucket, loaded asynchronously. 170 | 171 | _(Note that some HTML boilerplate has been omitted from the sample below)_ 172 | 173 | ```html 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 184 | 185 | 186 | 187 | {% css %}/* Inline in the head, great with @font-face! */{% endcss %} 188 | 189 | {% css "defer" %}/* Load me later */{% endcss %} 190 | 191 | 192 | ``` 193 | 194 | **Related**: 195 | 196 | * Check out the [demo of Critical CSS using Eleventy Edge](https://demo-eleventy-edge.netlify.app/critical-css/) for a repeat view optimization without JavaScript. 197 | * You may want to improve the above code with [`fetchpriority`](https://www.smashingmagazine.com/2022/04/boost-resource-loading-new-priority-hint-fetchpriority/) when [browser support improves](https://caniuse.com/mdn-html_elements_link_fetchpriority). 198 | 199 | #### SVG Icon Library 200 | 201 | Here an `svg` is bundle is created. 202 | 203 | ```js 204 | // .eleventy.js 205 | export default function(eleventyConfig) { 206 | eleventyConfig.addBundle("svg"); 207 | }; 208 | ``` 209 | 210 | ```html 211 | 214 | 215 | 216 | {% svg %} 217 | 218 | {% endsvg %} 219 | 220 | And now you can use `icon-close` in as many SVG instances as you’d like (without repeating the heftier SVG content). 221 | 222 | 223 | 224 | 225 | 226 | ``` 227 | 228 | #### React Helmet-style `` additions 229 | 230 | ```js 231 | // .eleventy.js 232 | export default function(eleventyConfig) { 233 | eleventyConfig.addBundle("html"); 234 | }; 235 | ``` 236 | 237 | This might exist in an Eleventy layout file: 238 | 239 | ```html 240 | 241 | {% getBundle "html", "head" %} 242 | 243 | ``` 244 | 245 | And then in your content you might want to page-specific `preconnect`: 246 | 247 | ```html 248 | {% html "head" %} 249 | 250 | {% endhtml %} 251 | ``` 252 | 253 | #### Bundle Sass with the Render Plugin 254 | 255 | You can render template syntax inside of the `{% css %}` shortcode too, if you’d like to do more advanced things using Eleventy template types. 256 | 257 | This example assumes you have added the [Render plugin](https://www.11ty.dev/docs/plugins/render/) and the [`scss` custom template type](https://www.11ty.dev/docs/languages/custom/) to your Eleventy configuration file. 258 | 259 | ```html 260 | {% css %} 261 | {% renderTemplate "scss" %} 262 | h1 { .test { color: red; } } 263 | {% endrenderTemplate %} 264 | {% endcss %} 265 | ``` 266 | 267 | Now the compiled Sass is available in your default bundle and will show up in `getBundle` and `getBundleFileUrl`. 268 | 269 | #### Use with [WebC](https://www.11ty.dev/docs/languages/webc/) 270 | 271 | Starting with `@11ty/eleventy-plugin-webc@0.9.0` (track at [issue #48](https://github.com/11ty/eleventy-plugin-webc/issues/48)) this plugin is used by default in the Eleventy WebC plugin. Specifically, [WebC Bundler Mode](https://www.11ty.dev/docs/languages/webc/#css-and-js-(bundler-mode)) now uses the bundle plugin under the hood. 272 | 273 | To add CSS to a bundle in WebC, you would use a ` 277 | 278 | ``` 279 | 280 | To add JS to a page bundle in WebC, you would use a ` 284 | 285 | ``` 286 | 287 | * Existing calls via WebC helpers `getCss` or `getJs` (e.g. ` 333 | {% endcss %} 334 | * a way to declare dependencies? or just defer to buckets here 335 | * What if we want to add code duplicates? Adding `alert(1);` `alert(1);` to alert twice? 336 | * sourcemaps (maybe via magic-string module or https://www.npmjs.com/package/concat-with-sourcemaps) 337 | --> -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import fs from "fs"; 3 | import Eleventy, { RenderPlugin } from "@11ty/eleventy"; 4 | import * as sass from "sass"; 5 | import bundlePlugin from "../eleventy.bundle.js"; 6 | 7 | // Special testing notes regarding overriding the bundled-with-core Bundle Plugin and using the Bundle Plugin from this repo: 8 | // Options: use a `configPath` with `force: true` 9 | // Use `eleventy.beforeConfig` event with `config` callback (read more below) 10 | 11 | // Configuration order of execution: 12 | // 1. `config` callback 13 | // 2. `defaultConfig.js` file from core repo 14 | // 3. `configPath` file 15 | 16 | // eleventy.beforeConfig event approach: 17 | // If you want to use a `config` callback to overwrite the built-in Bundle Plugin, use this pattern instead: 18 | // config: function(eleventyConfig) { 19 | // eleventyConfig.on("eleventy.beforeConfig", () => { 20 | // eleventyConfig.addPlugin(bundlePlugin, { 21 | // immediate: true, 22 | // force: true, 23 | // }); 24 | // }); 25 | // } 26 | 27 | function normalize(str) { 28 | if(typeof str !== "string") { 29 | throw new Error("Could not find content: " + str); 30 | } 31 | return str.trim().replace(/\r\n/g, "\n"); 32 | } 33 | 34 | test("CSS (Nunjucks)", async t => { 35 | let elev = new Eleventy("test/stubs/nunjucks/", "_site", { configPath: "eleventy.bundle.js" }); 36 | let results = await elev.toJSON(); 37 | t.deepEqual(normalize(results[0].content), ` 40 | 43 | `) 46 | }); 47 | 48 | test("CSS (Liquid)", async t => { 49 | let elev = new Eleventy("test/stubs/liquid/", "_site", { configPath: "eleventy.bundle.js" }); 50 | let results = await elev.toJSON(); 51 | t.deepEqual(normalize(results[0].content), ` 54 | 57 | `) 60 | }); 61 | 62 | test("CSS (Markdown)", async t => { 63 | let elev = new Eleventy("test/stubs/markdown/", "_site", { configPath: "eleventy.bundle.js" }); 64 | let results = await elev.toJSON(); 65 | t.deepEqual(normalize(results[0].content), ` 68 | 71 | `) 74 | }); 75 | 76 | test("SVG", async t => { 77 | let elev = new Eleventy("test/stubs/nunjucks-svg/", "_site", { configPath: "eleventy.bundle.js" }); 78 | let results = await elev.toJSON(); 79 | t.deepEqual(normalize(results[0].content), ``) 82 | }); 83 | 84 | test("JS", async t => { 85 | let elev = new Eleventy("test/stubs/liquid-js/", "_site", { configPath: "eleventy.bundle.js" }); 86 | let results = await elev.toJSON(); 87 | t.deepEqual(normalize(results[0].content), ` 90 | 93 | `) 96 | }); 97 | 98 | test("CSS, two buckets", async t => { 99 | let elev = new Eleventy("test/stubs/liquid-buckets/", "_site", { configPath: "eleventy.bundle.js" }); 100 | let results = await elev.toJSON(); 101 | t.deepEqual(normalize(results[0].content), ` 103 | `) 105 | }); 106 | 107 | test("CSS, two buckets, explicit `default`", async t => { 108 | let elev = new Eleventy("test/stubs/liquid-buckets-default/", "_site", { configPath: "eleventy.bundle.js" }); 109 | let results = await elev.toJSON(); 110 | 111 | t.deepEqual(normalize(results[0].content), ` 113 | `) 115 | }); 116 | 117 | test("CSS, get two buckets at once", async t => { 118 | let elev = new Eleventy("test/stubs/buckets-get-multiple/", "_site", { configPath: "eleventy.bundle.js" }); 119 | let results = await elev.toJSON(); 120 | t.deepEqual(normalize(results[0].content), ``); // note that blue is only listed once, we de-dupe entries across buckets 123 | }); 124 | 125 | test("CSS, get two buckets at once, reverse order", async t => { 126 | let elev = new Eleventy("test/stubs/buckets-ordering/", "_site", { configPath: "eleventy.bundle.js" }); 127 | let results = await elev.toJSON(); 128 | t.deepEqual(normalize(results[0].content), ``); // note that blue is only listed once, we de-dupe entries across buckets 131 | }); 132 | 133 | test("CSS, get two buckets at once (comma separated list)", async t => { 134 | let elev = new Eleventy("test/stubs/buckets-get-multiple-comma-sep/", "_site", { configPath: "eleventy.bundle.js" }); 135 | let results = await elev.toJSON(); 136 | t.deepEqual(normalize(results[0].content), ``); // note that blue is only listed once, we de-dupe entries across buckets 139 | }); 140 | 141 | test("toFile Filter (no writes)", async t => { 142 | let elev = new Eleventy("test/stubs/to-file/", "_site", { configPath: "eleventy.bundle.js" }); 143 | let results = await elev.toJSON(); 144 | t.deepEqual(normalize(results[0].content), ` 147 | `); // note that blue is only listed once, we de-dupe entries across buckets 148 | 149 | // does *not* write to the file system because of `toJSON` usage above. 150 | t.falsy(fs.existsSync("./_site/bundle/AZBTWWtF0t.css")) 151 | }); 152 | 153 | test("toFile Filter (write files)", async t => { 154 | let elev = new Eleventy("test/stubs/to-file-write/", undefined, { 155 | configPath: "eleventy.bundle.js", 156 | config: function(eleventyConfig) { 157 | eleventyConfig.setQuietMode(true); 158 | } 159 | }); 160 | 161 | await elev.write(); 162 | 163 | t.is(normalize(fs.readFileSync("_site/to-file-write/index.html", "utf8")), ` 166 | `); // note that blue is only listed once, we de-dupe entries across buckets 167 | 168 | // does write to the file system because of `write` usage above. 169 | t.is(normalize(fs.readFileSync("_site/bundle/Es4dSlOfrv.css", "utf8")), `* { color: blue; } 170 | * { color: red; } 171 | * { color: orange; }/* lololol2 */`); 172 | 173 | fs.unlinkSync("_site/to-file-write/index.html"); 174 | fs.unlinkSync("_site/bundle/Es4dSlOfrv.css"); 175 | 176 | t.false(fs.existsSync("_site/to-file-write/index.html")); 177 | t.false(fs.existsSync("_site/bundle/Es4dSlOfrv.css")); 178 | }); 179 | 180 | test("toFile Filter (write files, out of order)", async t => { 181 | let elev = new Eleventy("test/stubs/to-file-ordering/", undefined, { 182 | configPath: "eleventy.bundle.js", 183 | config: function(eleventyConfig) { 184 | eleventyConfig.setQuietMode(true); 185 | } 186 | }); 187 | 188 | await elev.write(); 189 | 190 | t.is(normalize(fs.readFileSync("./_site/to-file-ordering/index.html", "utf8")), ` 192 | `); // note that blue is only listed once, we de-dupe entries across buckets 193 | 194 | // does write to the file system because of `write` usage above. 195 | t.is(normalize(fs.readFileSync("./_site/bundle/6_wo_c5eqX.css", "utf8")), `* { color: blue; } 196 | * { color: rebeccapurple; }`); 197 | 198 | fs.unlinkSync("./_site/to-file-ordering/index.html"); 199 | fs.unlinkSync("./_site/bundle/6_wo_c5eqX.css"); 200 | 201 | t.false(fs.existsSync("./_site/to-file-ordering/index.html")); 202 | t.false(fs.existsSync("./_site/bundle/6_wo_c5eqX.css")); 203 | }); 204 | 205 | test("Bundle in Layout file", async t => { 206 | let elev = new Eleventy("test/stubs/bundle-in-layout/", "_site", { configPath: "eleventy.bundle.js" }); 207 | let results = await elev.toJSON(); 208 | t.deepEqual(normalize(results[0].content), ``); 209 | }); 210 | 211 | test("Bundle with render plugin", async t => { 212 | let elev = new Eleventy("test/stubs/bundle-render/", undefined, { 213 | configPath: "eleventy.bundle.js", 214 | config: function(eleventyConfig) { 215 | eleventyConfig.addPlugin(RenderPlugin); 216 | 217 | eleventyConfig.addExtension("scss", { 218 | outputFileExtension: "css", 219 | 220 | compile: async function(inputContent) { 221 | let result = sass.compileString(inputContent); 222 | 223 | // This is the render function, `data` is the full data cascade 224 | return async (data) => { 225 | return result.css; 226 | }; 227 | } 228 | }); 229 | } 230 | }); 231 | let results = await elev.toJSON(); 232 | t.deepEqual(normalize(results[0].content), ` 233 | `); 238 | }); 239 | 240 | test("No bundling", async t => { 241 | let elev = new Eleventy("test/stubs/no-bundles/", "_site", { configPath: "eleventy.bundle.js" }); 242 | let results = await elev.toJSON(); 243 | t.deepEqual(normalize(results[0].content), ` 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | `); 255 | }); 256 | 257 | test("Use Transforms", async t => { 258 | let elev = new Eleventy("test/stubs/use-transforms/", undefined, { 259 | configPath: "test/stubs/use-transforms/eleventy.config.js" 260 | }); 261 | let results = await elev.toJSON(); 262 | t.deepEqual(normalize(results[0].content), ``); 265 | }); 266 | 267 | test("Use Transforms on specific bundle type", async t => { 268 | let elev = new Eleventy("test/stubs/use-transforms-on-type/", undefined, { 269 | configPath: "test/stubs/use-transforms-on-type/eleventy.config.js" 270 | }); 271 | let results = await elev.toJSON(); 272 | t.deepEqual(normalize(results[0].content), ` 275 | `); 276 | }); 277 | 278 | test("Output `defer` bucket multiple times (hoisting disabled)", async t => { 279 | let elev = new Eleventy("test/stubs/output-same-bucket-multiple-times-nohoist/", undefined, { 280 | configPath: "eleventy.bundle.js" 281 | }); 282 | 283 | let results = await elev.toJSON(); 284 | t.deepEqual(normalize(results[0].content), ` 285 | 287 | `); 289 | }); 290 | 291 | test("Output `defer` bucket multiple times (does hoisting)", async t => { 292 | let elev = new Eleventy("test/stubs/output-same-bucket-multiple-times/", undefined, { 293 | configPath: "test/stubs/output-same-bucket-multiple-times/eleventy.config.js" 294 | }); 295 | 296 | let results = await elev.toJSON(); 297 | t.deepEqual(normalize(results[0].content), ``); 299 | }); 300 | 301 | test("Output `default` bucket multiple times (no hoisting)", async t => { 302 | let elev = new Eleventy("test/stubs/output-default-multiple-times/", undefined, { 303 | configPath: "test/stubs/output-default-multiple-times/eleventy.config.js" 304 | }); 305 | 306 | let results = await elev.toJSON(); 307 | t.deepEqual(normalize(results[0].content), ` 308 | `); 309 | }); 310 | 311 | test("`defer` hoisting", async t => { 312 | let elev = new Eleventy("test/stubs/to-file-duplicates/", undefined, { 313 | configPath: "test/stubs/to-file-duplicates/eleventy.config.js" 314 | }); 315 | 316 | let results = await elev.toJSON(); 317 | results.sort((a, b) => { 318 | if(a.inputPath > b.inputPath) { 319 | return 1; 320 | } 321 | return -1; 322 | }) 323 | 324 | t.deepEqual(normalize(results[0].content), ` 325 | `); 326 | 327 | t.deepEqual(normalize(results[1].content), ``); 328 | 329 | t.deepEqual(normalize(results[2].content), ``); 330 | }); 331 | 332 | test("Bundle export key as string (11ty.js)", async t => { 333 | let elev = new Eleventy("test/stubs/export-key-str/", "_site", { configPath: "test/stubs/export-key-str/eleventy.config.js" }); 334 | let results = await elev.toJSON(); 335 | t.deepEqual(normalize(results[0].content), ``) 336 | }); 337 | 338 | test("Bundle export key as object (11ty.js)", async t => { 339 | let elev = new Eleventy("test/stubs/export-key-obj/", "_site", { configPath: "test/stubs/export-key-obj/eleventy.config.js" }); 340 | let results = await elev.toJSON(); 341 | t.deepEqual(normalize(results[0].content), ``) 342 | }); 343 | 344 | test("Bundle export key as string, using separate bundleExportKey’s (11ty.js)", async t => { 345 | let elev = new Eleventy("test/stubs/export-key-str-rename/", "_site", { configPath: "test/stubs/export-key-str-rename/eleventy.config.js" }); 346 | let results = await elev.toJSON(); 347 | t.deepEqual(normalize(results[0].content), ``) 348 | }); 349 | 350 | test("Empty CSS bundle (trimmed) removes empty 356 | {%- css %} {% endcss %}`) 357 | } 358 | }); 359 | let results = await elev.toJSON(); 360 | t.deepEqual(normalize(results[0].content), `
`) 361 | }); 362 | 363 | test("Empty JS bundle (trimmed) removes empty 369 | {%- js %} {% endjs %}`) 370 | } 371 | }); 372 | let results = await elev.toJSON(); 373 | t.deepEqual(normalize(results[0].content), `
`) 374 | }); 375 | 376 | test("Empty CSS bundle (trimmed) removes empty tag", async t => { 377 | let elev = new Eleventy("test/stubs-virtual/", "_site", { 378 | config: function(eleventyConfig) { 379 | eleventyConfig.addPlugin(bundlePlugin); 380 | 381 | eleventyConfig.addTemplate('test.njk', `
382 | {%- css %} {% endcss %}`) 383 | } 384 | }); 385 | let results = await elev.toJSON(); 386 | t.deepEqual(normalize(results[0].content), `
`) 387 | }); 388 | 389 | // TODO this requires `htmlTransformer.remove` which is core v3.0.1-alpha.4+ 390 | test.skip("Empty CSS bundle (trimmed) does *not* remove empty `) 397 | }); 398 | 399 | // TODO this requires `htmlTransformer.remove` which is core v3.0.1-alpha.4+ 400 | test.skip("Empty CSS bundle (trimmed) does *not* remove empty 406 | {%- css %} {% endcss %}`) 407 | } 408 | }); 409 | 410 | let results = await elev.toJSON(); 411 | t.deepEqual(normalize(results[0].content), `
`) 412 | }); 413 | 414 | // This one requires a new Eleventy v3.0.0-alpha.20 or -beta.2 release, per the `enabled` option on plugins to HtmlTransformer 415 | test("`) 423 | } 424 | }); 425 | 426 | let results = await elev.toJSON(); 427 | t.deepEqual(normalize(results[0].content), `
`) 428 | }); 429 | 430 | test("Delayed Bundle can be modified by transforms", async t => { 431 | let elev = new Eleventy("test/stubs-virtual/", "_site", { 432 | // See testing note at top of this file 433 | configPath: "test/stubs/delayed-bundle/eleventy.config.js", 434 | }); 435 | 436 | let results = await elev.toJSON(); 437 | t.deepEqual(normalize(results[0].content), `testing:this is svg`) 438 | }); 439 | 440 | test("config and configPath handles multiple addPlugin calls just fine", async t => { 441 | let elev = new Eleventy("test/stubs-virtual/", "_site", { 442 | // See testing note at top of this file 443 | configPath: "test/stubs/duplicate-addplugins/eleventy.config.js", 444 | config: function(eleventyConfig) { 445 | eleventyConfig.addPlugin(bundlePlugin, { 446 | bundles: false, 447 | }); 448 | 449 | eleventyConfig.addTemplate('test.njk', `
`) 450 | } 451 | }); 452 | 453 | let results = await elev.toJSON(); 454 | t.deepEqual(normalize(results[0].content), `
`) 455 | }); 456 | 457 | test("
`) 465 | }); 466 | 467 | test("`) 476 | }); 477 | 478 | test("`) 486 | }); 487 | --------------------------------------------------------------------------------