├── 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 |
--------------------------------------------------------------------------------