Initial commit

This commit is contained in:
2024-11-03 17:41:45 +01:00
commit c1640c1754
8043 changed files with 775536 additions and 0 deletions

337
node_modules/@11ty/eleventy-plugin-bundle/README.md generated vendored Normal file
View File

@@ -0,0 +1,337 @@
# eleventy-plugin-bundle
Little bundles of code, little bundles of joy.
Create minimal per-page or app-level bundles of CSS, JavaScript, or HTML to be included in your Eleventy project.
Makes it easy to implement Critical CSS, in-use-only CSS/JS bundles, SVG icon libraries, or secondary HTML content to load via XHR.
## Why?
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 youd like to modify the code).
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.
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).
## Installation
No installation necessary. Starting with Eleventy `v3.0.0-alpha.10` and newer, this plugin is now bundled with Eleventy.
## Usage
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.
To create a bundle type, use `eleventyConfig.addBundle` in your Eleventy configuration file (default `.eleventy.js`):
```js
// .eleventy.js
export default function(eleventyConfig) {
eleventyConfig.addBundle("css");
};
```
This does two things:
1. Creates a new `css` shortcode for adding arbitrary code to this bundle
2. Adds `"css"` as an eligible type argument to the `getBundle` and `getBundleFileUrl` shortcodes.
### Full options list
```js
export default function(eleventyConfig) {
eleventyConfig.addBundle("css", {
// (Optional) Folder (relative to output directory) files will write to
toFileDirectory: "bundle",
// (Optional) File extension used for bundle file output, defaults to bundle name
outputFileExtension: "css",
// (Optional) Name of shortcode for use in templates, defaults to bundle name
shortcodeName: "css",
// shortcodeName: false, // disable this feature.
// (Optional) Modify bundle content
transforms: [],
// (Optional) If two identical code blocks exist in non-default buckets, theyll be hoisted to the first bucket in common.
hoist: true,
// (Optional) In 11ty.js templates, having a named export of `bundle` will populate your bundles.
bundleExportKey: "bundle",
// bundleExportKey: false, // disable this feature.
});
};
```
Read more about [`hoist` and duplicate bundle hoisting](https://github.com/11ty/eleventy-plugin-bundle/issues/5).
### Universal Shortcodes
The following Universal Shortcodes (available in `njk`, `liquid`, `hbs`, `11ty.js`, and `webc`) are provided by this plugin:
* `getBundle` to retrieve bundled code as a string.
* `getBundleFileUrl` to create a bundle file on disk and retrieve the URL to that file.
Heres 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).
### Example: Add bundle code in a Markdown file in Eleventy
```md
# My Blog Post
This is some content, I am writing markup.
{% css %}
em { font-style: italic; }
{% endcss %}
## More Markdown
{% css %}
strong { font-weight: bold; }
{% endcss %}
```
Renders to:
```html
<h1>My Blog Post</h1>
<p>This is some content, I am writing markup.</p>
<h2>More Markdown</h2>
```
Note that the bundled code is excluded!
_There are a few [more examples below](#examples)!_
### Render bundle code
```html
<!-- Use this *anywhere*: a layout file, content template, etc -->
<style>{% getBundle "css" %}</style>
<!--
You can add more code to the bundle after calling
getBundle and it will be included.
-->
{% css %}* { color: orange; }{% endcss %}
```
### Write a bundle to a file
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:
```html
<link rel="stylesheet" href="{% getBundleFileUrl "css" %}">
```
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).
### Asset bucketing
```html
<!-- This goes into a `defer` bucket (the bucket can be any string value) -->
{% css "defer" %}em { font-style: italic; }{% endcss %}
```
```html
<!-- Pass the arbitrary `defer` bucket name as an additional argument -->
<style>{% getBundle "css", "defer" %}</style>
<link rel="stylesheet" href="{% getBundleFileUrl 'css', 'defer' %}">
```
A `default` bucket is implied:
```html
<!-- These two statements are the same -->
{% css %}em { font-style: italic; }{% endcss %}
{% css "default" %}em { font-style: italic; }{% endcss %}
<!-- These two are the same too -->
<style>{% getBundle "css" %}</style>
<style>{% getBundle "css", "default" %}</style>
```
### Examples
#### Critical CSS
```js
// .eleventy.js
export default function(eleventyConfig) {
eleventyConfig.addBundle("css");
};
```
Use asset bucketing to divide CSS between the `default` bucket and a `defer` bucket, loaded asynchronously.
_(Note that some HTML boilerplate has been omitted from the sample below)_
```html
<!-- … -->
<head>
<!-- Inlined critical styles -->
<style>{% getBundle "css" %}</style>
<!-- Deferred non-critical styles -->
<link rel="stylesheet" href="{% getBundleFileUrl 'css', 'defer' %}" media="print" onload="this.media='all'">
<noscript>
<link rel="stylesheet" href="{% getBundleFileUrl 'css', 'defer' %}">
</noscript>
</head>
<body>
<!-- This goes into a `default` bucket -->
{% css %}/* Inline in the head, great with @font-face! */{% endcss %}
<!-- This goes into a `defer` bucket (the bucket can be any string value) -->
{% css "defer" %}/* Load me later */{% endcss %}
</body>
<!-- … -->
```
**Related**:
* 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.
* 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).
#### SVG Icon Library
Here an `svg` is bundle is created.
```js
// .eleventy.js
export default function(eleventyConfig) {
eleventyConfig.addBundle("svg");
};
```
```html
<svg width="0" height="0" aria-hidden="true" style="position: absolute;">
<defs>{% getBundle "svg" %}</defs>
</svg>
<!-- And anywhere on your page you can add icons to the set -->
{% svg %}
<g id="icon-close"><path d="…" /></g>
{% endsvg %}
And now you can use `icon-close` in as many SVG instances as youd like (without repeating the heftier SVG content).
<svg><use xlink:href="#icon-close"></use></svg>
<svg><use xlink:href="#icon-close"></use></svg>
<svg><use xlink:href="#icon-close"></use></svg>
<svg><use xlink:href="#icon-close"></use></svg>
```
#### React Helmet-style `<head>` additions
```js
// .eleventy.js
export default function(eleventyConfig) {
eleventyConfig.addBundle("html");
};
```
This might exist in an Eleventy layout file:
```html
<head>
{% getBundle "html", "head" %}
</head>
```
And then in your content you might want to page-specific `preconnect`:
```html
{% html "head" %}
<link href="https://v1.opengraph.11ty.dev" rel="preconnect" crossorigin>
{% endhtml %}
```
#### Bundle Sass with the Render Plugin
You can render template syntax inside of the `{% css %}` shortcode too, if youd like to do more advanced things using Eleventy template types.
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.
```html
{% css %}
{% renderTemplate "scss" %}
h1 { .test { color: red; } }
{% endrenderTemplate %}
{% endcss %}
```
Now the compiled Sass is available in your default bundle and will show up in `getBundle` and `getBundleFileUrl`.
#### Use with [WebC](https://www.11ty.dev/docs/languages/webc/)
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.
To add CSS to a bundle in WebC, you would use a `<style>` element in a WebC page or component:
```html
<style>/* This is bundled. */</style>
<style webc:keep>/* Do not bundle me—leave as is */</style>
```
To add JS to a page bundle in WebC, you would use a `<script>` element in a WebC page or component:
```html
<script>/* This is bundled. */</script>
<script webc:keep>/* Do not bundle me—leave as is */</script>
```
* Existing calls via WebC helpers `getCss` or `getJs` (e.g. `<style @raw="getCss(page.url)">`) have been wired up to `getBundle` (for `"css"` and `"js"` respectively) automatically.
* For consistency, you may prefer using the bundle plugin method names everywhere: `<style @raw="getBundle('css')">` and `<script @raw="getBundle('js')">` both work fine.
* Outside of WebC, the Universal Filters `webcGetCss` and `webcGetJs` were removed in Eleventy `v3.0.0-alpha.10` in favor of the `getBundle` Universal Shortcode (`{% getBundle "css" %}` and `{% getBundle "js" %}` respectively).
#### Modify the bundle output
You can wire up your own async-friendly callbacks to transform the bundle output too. Heres a quick example of [`postcss` integration](https://github.com/postcss/postcss#js-api).
```js
const postcss = require("postcss");
const postcssNested = require("postcss-nested");
export default function(eleventyConfig) {
eleventyConfig.addBundle("css", {
transforms: [
async function(content) {
// this.type returns the bundle name.
// Same as Eleventy transforms, this.page is available here.
let result = await postcss([postcssNested]).process(content, { from: this.page.inputPath, to: null });
return result.css;
}
]
});
};
```
## Advanced
### Limitations
Bundles do not support nesting or recursion (yet?). If this will be useful to you, please file an issue!
<!--
Version Two:
* Think about Eleventy transform order, scenarios where this transform needs to run first.
* JavaScript API independent of eleventy
* Clean up the _site/bundle folder on exit?
* Example ideas:
* App bundle and page bundle
* can we make this work for syntax highlighting? or just defer to WebC for this?
{% css %}
<style>
em { font-style: italic; }
</style>
{% endcss %}
* a way to declare dependencies? or just defer to buckets here
* What if we want to add code duplicates? Adding `alert(1);` `alert(1);` to alert twice?
* sourcemaps (maybe via magic-string module or https://www.npmjs.com/package/concat-with-sourcemaps)
-->

View File

@@ -0,0 +1,66 @@
import { createRequire } from "node:module";
import bundleManagersPlugin from "./src/eleventy.bundleManagers.js";
import pruneEmptyBundlesPlugin from "./src/eleventy.pruneEmptyBundles.js";
import shortcodesPlugin from "./src/eleventy.shortcodes.js";
import debugUtil from "debug";
const require = createRequire(import.meta.url);
const debug = debugUtil("Eleventy:Bundle");
const pkg = require("./package.json");
function normalizeOptions(options = {}) {
options = Object.assign({
// Plugin defaults
bundles: [], // extra bundles: css, js, and html are guaranteed unless `bundles: false`
toFileDirectory: "bundle",
// post-process
transforms: [],
hoistDuplicateBundlesFor: [],
bundleExportKey: "bundle", // use a `bundle` export in a 11ty.js template to populate bundles
}, options);
if(options.bundles !== false) {
options.bundles = Array.from(new Set(["css", "js", "html", ...(options.bundles || [])]));
}
return options;
}
function eleventyBundlePlugin(eleventyConfig, pluginOptions = {}) {
eleventyConfig.versionCheck(pkg["11ty"].compatibility);
pluginOptions = normalizeOptions(pluginOptions);
if(!("getBundleManagers" in eleventyConfig) && !("addBundle" in eleventyConfig)) {
bundleManagersPlugin(eleventyConfig, pluginOptions);
}
pruneEmptyBundlesPlugin(eleventyConfig, pluginOptions);
// should this be unique too?
shortcodesPlugin(eleventyConfig, pluginOptions);
if(Array.isArray(pluginOptions.bundles)) {
debug("Adding bundles via `addPlugin`: %o", pluginOptions.bundles)
pluginOptions.bundles.forEach(name => {
let isHoisting = Array.isArray(pluginOptions.hoistDuplicateBundlesFor) && pluginOptions.hoistDuplicateBundlesFor.includes(name);
eleventyConfig.addBundle(name, {
hoist: isHoisting,
outputFileExtension: name, // default as `name`
shortcodeName: name, // `false` will skip shortcode
transforms: pluginOptions.transforms,
toFileDirectory: pluginOptions.toFileDirectory,
bundleExportKey: pluginOptions.bundleExportKey, // `false` will skip bundle export
});
});
}
};
// This is used to find the package name for this plugin (used in eleventy-plugin-webc to prevent dupes)
Object.defineProperty(eleventyBundlePlugin, "eleventyPackage", {
value: pkg.name
});
export default eleventyBundlePlugin;
export { normalizeOptions };

62
node_modules/@11ty/eleventy-plugin-bundle/package.json generated vendored Normal file
View File

@@ -0,0 +1,62 @@
{
"name": "@11ty/eleventy-plugin-bundle",
"version": "3.0.0",
"description": "Little bundles of code, little bundles of joy.",
"main": "eleventy.bundle.js",
"type": "module",
"scripts": {
"sample": "DEBUG=Eleventy:Bundle npx @11ty/eleventy --config=sample/sample-config.js --input=sample --serve",
"test": "npx ava"
},
"publishConfig": {
"access": "public"
},
"license": "MIT",
"engines": {
"node": ">=18"
},
"11ty": {
"compatibility": ">=3.0.0-alpha"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/11ty"
},
"keywords": [
"eleventy",
"eleventy-plugin"
],
"repository": {
"type": "git",
"url": "git://github.com/11ty/eleventy-plugin-bundle.git"
},
"bugs": "https://github.com/11ty/eleventy-plugin-bundle/issues",
"homepage": "https://www.11ty.dev/",
"author": {
"name": "Zach Leatherman",
"email": "zachleatherman@gmail.com",
"url": "https://zachleat.com/"
},
"ava": {
"failFast": true,
"files": [
"test/*.js",
"test/*.mjs"
],
"ignoredByWatcher": [
"**/_site/**",
".cache"
]
},
"devDependencies": {
"@11ty/eleventy": "3.0.0-alpha.20",
"ava": "^5.3.1",
"postcss": "^8.4.31",
"postcss-nested": "^6.0.1",
"sass": "^1.69.5"
},
"dependencies": {
"debug": "^4.3.4",
"posthtml-match-helper": "^2.0.2"
}
}

View File

@@ -0,0 +1,76 @@
import fs from "node:fs";
import path from "node:path";
import { createHash } from "node:crypto";
import debugUtil from "debug";
const debug = debugUtil("Eleventy:Bundle");
const hashCache = {};
const directoryExistsCache = {};
const writingCache = new Set();
class BundleFileOutput {
constructor(outputDirectory, bundleDirectory) {
this.outputDirectory = outputDirectory;
this.bundleDirectory = bundleDirectory || "";
this.hashLength = 10;
this.fileExtension = undefined;
}
setFileExtension(ext) {
this.fileExtension = ext;
}
getFilenameHash(content) {
if(hashCache[content]) {
return hashCache[content];
}
let hash = createHash("sha256");
hash.update(content);
let base64hash = hash.digest('base64').replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
let filenameHash = base64hash.substring(0, this.hashLength);
hashCache[content] = filenameHash;
return filenameHash;
}
getFilename(filename, extension) {
return filename + (extension && !extension.startsWith(".") ? `.${extension}` : "");
}
modifyPathToUrl(dir, filename) {
return "/" + path.join(dir, filename).split(path.sep).join("/");
}
writeBundle(content, type, writeToFileSystem) {
// do not write a bundle, do not return a file name is content is empty
if(!content) {
return;
}
let dir = path.join(this.outputDirectory, this.bundleDirectory);
let filenameHash = this.getFilenameHash(content);
let filename = this.getFilename(filenameHash, this.fileExtension || type);
if(writeToFileSystem) {
let fullPath = path.join(dir, filename);
// no duplicate writes, this may be improved with a fs exists check, but it would only save the first write
if(!writingCache.has(fullPath)) {
writingCache.add(fullPath);
if(!directoryExistsCache[dir]) {
fs.mkdirSync(dir, { recursive: true });
directoryExistsCache[dir] = true;
}
debug("Writing bundle %o", fullPath);
fs.writeFileSync(fullPath, content);
}
}
return this.modifyPathToUrl(this.bundleDirectory, filename);
}
}
export { BundleFileOutput };

View File

@@ -0,0 +1,209 @@
import { BundleFileOutput } from "./BundleFileOutput.js";
import debugUtil from "debug";
const debug = debugUtil("Eleventy:Bundle");
const DEBUG_LOG_TRUNCATION_SIZE = 200;
class CodeManager {
// code is placed in this bucket by default
static DEFAULT_BUCKET_NAME = "default";
// code is hoisted to this bucket when necessary
static HOISTED_BUCKET_NAME = "default";
constructor(name) {
this.name = name;
this.trimOnAdd = true;
// TODO unindent on add
this.reset();
this.transforms = [];
this.isHoisting = true;
this.fileExtension = undefined;
this.toFileDirectory = undefined;
this.bundleExportKey = "bundle";
}
setFileExtension(ext) {
this.fileExtension = ext;
}
setHoisting(enabled) {
this.isHoisting = !!enabled;
}
setBundleDirectory(dir) {
this.toFileDirectory = dir;
}
setBundleExportKey(key) {
this.bundleExportKey = key;
}
getBundleExportKey() {
return this.bundleExportKey;
}
reset() {
this.pages = {};
}
static normalizeBuckets(bucket) {
if(Array.isArray(bucket)) {
return bucket;
} else if(typeof bucket === "string") {
return bucket.split(",");
}
return [CodeManager.DEFAULT_BUCKET_NAME];
}
setTransforms(transforms) {
if(!Array.isArray(transforms)) {
throw new Error("Array expected to setTransforms");
}
this.transforms = transforms;
}
_initBucket(pageUrl, bucket) {
if(!this.pages[pageUrl][bucket]) {
this.pages[pageUrl][bucket] = new Set();
}
}
addToPage(pageUrl, code = [], bucket) {
if(!Array.isArray(code) && code) {
code = [code];
}
if(code.length === 0) {
return;
}
if(!this.pages[pageUrl]) {
this.pages[pageUrl] = {};
}
let buckets = CodeManager.normalizeBuckets(bucket);
let codeContent = code.map(entry => {
if(this.trimOnAdd) {
return entry.trim();
}
return entry;
});
for(let b of buckets) {
this._initBucket(pageUrl, b);
let debugLoggedContent = codeContent.join("\n");
debug("Adding code to bundle %o for %o (bucket: %o, size: %o): %o", this.name, pageUrl, b, debugLoggedContent.length, debugLoggedContent.length > DEBUG_LOG_TRUNCATION_SIZE ? debugLoggedContent.slice(0, DEBUG_LOG_TRUNCATION_SIZE) + "…" : debugLoggedContent);
for(let content of codeContent) {
if(content) {
this.pages[pageUrl][b].add(content);
}
}
}
}
async runTransforms(str, pageData, buckets) {
for (let callback of this.transforms) {
str = await callback.call(
{
page: pageData,
type: this.name,
buckets: buckets
},
str
);
}
return str;
}
getBucketsForPage(pageData) {
let pageUrl = pageData.url;
if(!this.pages[pageUrl]) {
return [];
}
return Object.keys(this.pages[pageUrl]);
}
getRawForPage(pageData, buckets = undefined) {
let url = pageData.url;
if(!this.pages[url]) {
debug("No bundle code found for %o on %o, %O", this.name, url, this.pages);
return new Set();
}
buckets = CodeManager.normalizeBuckets(buckets);
debug("Retrieving %o for %o (buckets: %o)", this.name, url, buckets);
let set = new Set();
for(let b of buckets) {
if(!this.pages[url][b]) {
// Just continue, if you retrieve code from a bucket that doesnt exist or has no code, it will return an empty set
continue;
}
for(let entry of this.pages[url][b]) {
set.add(entry);
}
}
return set;
}
async getForPage(pageData, buckets = undefined) {
let set = this.getRawForPage(pageData, buckets);
let bundleContent = Array.from(set).join("\n");
// returns promise
return this.runTransforms(bundleContent, pageData, buckets);
}
async writeBundle(pageData, buckets, options = {}) {
let url = pageData.url;
if(!this.pages[url]) {
debug("No bundle code found for %o on %o, %O", this.name, url, this.pages);
return "";
}
let { output, write } = options;
buckets = CodeManager.normalizeBuckets(buckets);
// TODO the bundle output URL might be useful in the transforms for sourcemaps
let content = await this.getForPage(pageData, buckets);
let writer = new BundleFileOutput(output, this.toFileDirectory);
writer.setFileExtension(this.fileExtension);
return writer.writeBundle(content, this.name, write);
}
// Used when a bucket is output multiple times on a page and needs to be hoisted
hoistBucket(pageData, bucketName) {
let newTargetBucketName = CodeManager.HOISTED_BUCKET_NAME;
if(!this.isHoisting || bucketName === newTargetBucketName) {
return;
}
let url = pageData.url;
if(!this.pages[url] || !this.pages[url][bucketName]) {
debug("No bundle code found for %o on %o, %O", this.name, url, this.pages);
return;
}
debug("Code in bucket (%o) is being hoisted to a new bucket (%o)", bucketName, newTargetBucketName);
this._initBucket(url, newTargetBucketName);
for(let codeEntry of this.pages[url][bucketName]) {
this.pages[url][bucketName].delete(codeEntry);
this.pages[url][newTargetBucketName].add(codeEntry);
}
// delete the bucket
delete this.pages[url][bucketName];
}
}
export { CodeManager };

View File

@@ -0,0 +1,153 @@
import debugUtil from "debug";
const debug = debugUtil("Eleventy:Bundle");
/* This class defers any `bundleGet` calls to a post-build transform step,
* to allow `getBundle` to be called before all of the `css` additions have been processed
*/
class OutOfOrderRender {
static SPLIT_REGEX = /(\/\*__EleventyBundle:[^:]*:[^:]*:[^:]*:EleventyBundle__\*\/)/;
static SEPARATOR = ":";
constructor(content) {
this.content = content;
this.managers = {};
}
// type if `get` (return string) or `file` (bundle writes to file, returns file url)
static getAssetKey(type, name, bucket) {
if(Array.isArray(bucket)) {
bucket = bucket.join(",");
} else if(typeof bucket === "string") {
} else {
bucket = "";
}
return `/*__EleventyBundle:${type}:${name}:${bucket || "default"}:EleventyBundle__*/`
}
static parseAssetKey(str) {
if(str.startsWith("/*__EleventyBundle:")) {
let [prefix, type, name, bucket, suffix] = str.split(OutOfOrderRender.SEPARATOR);
return { type, name, bucket };
}
return false;
}
setAssetManager(name, assetManager) {
this.managers[name] = assetManager;
}
setOutputDirectory(dir) {
this.outputDirectory = dir;
}
normalizeMatch(match) {
let ret = OutOfOrderRender.parseAssetKey(match)
return ret || match;
}
findAll() {
let matches = this.content.split(OutOfOrderRender.SPLIT_REGEX);
let ret = [];
for(let match of matches) {
ret.push(this.normalizeMatch(match));
}
return ret;
}
setWriteToFileSystem(isWrite) {
this.writeToFileSystem = isWrite;
}
getAllBucketsForPage(pageData) {
let availableBucketsForPage = new Set();
for(let name in this.managers) {
for(let bucket of this.managers[name].getBucketsForPage(pageData)) {
availableBucketsForPage.add(`${name}::${bucket}`);
}
}
return availableBucketsForPage;
}
getManager(name) {
if(!this.managers[name]) {
throw new Error(`No asset manager found for ${name}. Known names: ${Object.keys(this.managers)}`);
}
return this.managers[name];
}
async replaceAll(pageData) {
let matches = this.findAll();
let availableBucketsForPage = this.getAllBucketsForPage(pageData);
let usedBucketsOnPage = new Set();
let bucketsOutputStringCount = {};
let bucketsFileCount = {};
for(let match of matches) {
if(typeof match === "string") {
continue;
}
// type is `file` or `get`
let {type, name, bucket} = match;
let key = `${name}::${bucket}`;
if(!usedBucketsOnPage.has(key)) {
usedBucketsOnPage.add(key);
}
if(type === "get") {
if(!bucketsOutputStringCount[key]) {
bucketsOutputStringCount[key] = 0;
}
bucketsOutputStringCount[key]++;
} else if(type === "file") {
if(!bucketsFileCount[key]) {
bucketsFileCount[key] = 0;
}
bucketsFileCount[key]++;
}
}
// Hoist code in non-default buckets that are output multiple times
// Only hoist if 2+ `get` OR 1+ `get` and 1+ `file`
for(let bucketInfo in bucketsOutputStringCount) {
let stringOutputCount = bucketsOutputStringCount[bucketInfo];
if(stringOutputCount > 1 || stringOutputCount === 1 && bucketsFileCount[bucketInfo] > 0) {
let [name, bucketName] = bucketInfo.split("::");
this.getManager(name).hoistBucket(pageData, bucketName);
}
}
let content = await Promise.all(matches.map(match => {
if(typeof match === "string") {
return match;
}
let {type, name, bucket} = match;
let manager = this.getManager(name);
if(type === "get") {
// returns promise
return manager.getForPage(pageData, bucket);
} else if(type === "file") {
// returns promise
return manager.writeBundle(pageData, bucket, {
output: this.outputDirectory,
write: this.writeToFileSystem,
});
}
return "";
}));
for(let bucketInfo of availableBucketsForPage) {
if(!usedBucketsOnPage.has(bucketInfo)) {
let [name, bucketName] = bucketInfo.split("::");
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`);
}
}
return content.join("");
}
}
export { OutOfOrderRender };

View File

@@ -0,0 +1,74 @@
import { createRequire } from "node:module";
import debugUtil from "debug";
import { CodeManager } from "./CodeManager.js";
const require = createRequire(import.meta.url);
const debug = debugUtil("Eleventy:Bundle");
const pkg = require("../package.json");
function eleventyBundleManagers(eleventyConfig, pluginOptions = {}) {
if("getBundleManagers" in eleventyConfig || "addBundle" in eleventyConfig) {
throw new Error("Duplicate plugin calls for " + pkg.name);
}
let managers = {};
function addBundle(name, bundleOptions = {}) {
if(name in managers) {
debug("Bundle exists %o, skipping.", name);
// note: shortcode must still be added
} else {
debug("Creating new bundle %o", name);
managers[name] = new CodeManager(name);
if(bundleOptions.hoist !== undefined) {
managers[name].setHoisting(bundleOptions.hoist);
}
if(bundleOptions.bundleExportKey !== undefined) {
managers[name].setBundleExportKey(bundleOptions.bundleExportKey);
}
if(bundleOptions.outputFileExtension) {
managers[name].setFileExtension(bundleOptions.outputFileExtension);
}
if(bundleOptions.toFileDirectory) {
managers[name].setBundleDirectory(bundleOptions.toFileDirectory);
}
if(bundleOptions.transforms) {
managers[name].setTransforms(bundleOptions.transforms);
}
}
// if undefined, defaults to `name`
if(bundleOptions.shortcodeName !== false) {
let shortcodeName = bundleOptions.shortcodeName || name;
// e.g. `css` shortcode to add code to page bundle
// These shortcode names are not configurable on purpose (for wider plugin compatibility)
eleventyConfig.addPairedShortcode(shortcodeName, function addContent(content, bucket, explicitUrl) {
let url = explicitUrl || this.page?.url;
if(url) { // dont add if a file doesnt have an output URL
managers[name].addToPage(url, content, bucket);
}
return "";
});
}
};
eleventyConfig.addBundle = addBundle;
eleventyConfig.getBundleManagers = function() {
return managers;
};
eleventyConfig.on("eleventy.before", async () => {
for(let key in managers) {
managers[key].reset();
}
});
};
export default eleventyBundleManagers;

View File

@@ -0,0 +1,88 @@
import matchHelper from "posthtml-match-helper";
import debugUtil from "debug";
const debug = debugUtil("Eleventy:Bundle");
const ATTRS = {
keep: "eleventy:keep"
}
function getTextNodeContent(node) {
if (!node.content) {
return "";
}
return node.content
.map((entry) => {
if (typeof entry === "string") {
return entry;
}
if (Array.isArray(entry.content)) {
return getTextNodeContent(entry);
}
return "";
})
.join("");
}
function eleventyPruneEmptyBundles(eleventyConfig, options = {}) {
// Right now script[src],link[rel="stylesheet"] nodes are removed if the final bundles are empty.
// `false` to disable
options.pruneEmptySelector = options.pruneEmptySelector ?? `style,script,link[rel="stylesheet"]`;
// `false` disables this plugin
if(options.pruneEmptySelector === false) {
return;
}
if(!eleventyConfig.htmlTransformer || !eleventyConfig.htmlTransformer?.constructor?.SUPPORTS_PLUGINS_ENABLED_CALLBACK) {
debug("You will need to upgrade your version of Eleventy core to remove empty bundle tags automatically (v3 or newer).");
return;
}
eleventyConfig.htmlTransformer.addPosthtmlPlugin(
"html",
function (pluginOptions = {}) {
return function (tree) {
tree.match(matchHelper(options.pruneEmptySelector), function (node) {
if(node.attrs && node.attrs[ATTRS.keep] !== undefined) {
delete node.attrs[ATTRS.keep];
return node;
}
// <link rel="stylesheet" href="">
if(node.tag === "link") {
if(node.attrs?.rel === "stylesheet" && (node.attrs?.href || "").trim().length === 0) {
return false;
}
} else {
let content = getTextNodeContent(node);
if(!content) {
// <script></script> or <script src=""></script>
if(node.tag === "script" && (node.attrs?.src || "").trim().length === 0) {
return false;
}
// <style></style>
if(node.tag === "style") {
return false;
}
}
}
return node;
});
};
},
{
// the `enabled` callback for plugins is available on v3.0.0-alpha.20+ and v3.0.0-beta.2+
enabled: () => {
return Object.keys(eleventyConfig.getBundleManagers()).length > 0;
}
}
);
}
export default eleventyPruneEmptyBundles;

View File

@@ -0,0 +1,83 @@
import { OutOfOrderRender } from "./OutOfOrderRender.js";
import debugUtil from "debug";
const debug = debugUtil("Eleventy:Bundle");
function eleventyBundleShortcodes(eleventyConfig, pluginOptions = {}) {
let managers = eleventyConfig.getBundleManagers();
let writeToFileSystem = true;
let pagesUsingBundles = {};
eleventyConfig.on("eleventy.before", async ({ outputMode }) => {
if(Object.keys(managers).length === 0) {
return;
}
pagesUsingBundles = {};
if(outputMode !== "fs") {
writeToFileSystem = false;
debug("Skipping writing to the file system due to output mode: %o", outputMode);
}
});
// e.g. `getBundle` shortcode to get code in current page bundle
// bucket can be an array
// This shortcode name is not configurable on purpose (for wider plugin compatibility)
eleventyConfig.addShortcode("getBundle", function getContent(type, bucket, explicitUrl) {
if(!type || !(type in managers) || Object.keys(managers).length === 0) {
throw new Error(`Invalid bundle type: ${type}. Available options: ${Object.keys(managers)}`);
}
let url = explicitUrl || this.page?.url;
if(url) {
pagesUsingBundles[url] = true;
}
return OutOfOrderRender.getAssetKey("get", type, bucket);
});
// write a bundle to the file system
// This shortcode name is not configurable on purpose (for wider plugin compatibility)
eleventyConfig.addShortcode("getBundleFileUrl", function(type, bucket, explicitUrl) {
if(!type || !(type in managers) || Object.keys(managers).length === 0) {
throw new Error(`Invalid bundle type: ${type}. Available options: ${Object.keys(managers)}`);
}
let url = explicitUrl || this.page?.url;
if(url) {
pagesUsingBundles[url] = true;
}
return OutOfOrderRender.getAssetKey("file", type, bucket);
});
eleventyConfig.addTransform("@11ty/eleventy-bundle", function(content) {
// `page.outputPath` is required to perform bundle transform, unless
// we're running in Eleventy Serverless.
let missingOutputPath = !this.page.outputPath && process.env.ELEVENTY_SERVERLESS !== "true";
if(missingOutputPath || typeof content !== "string") {
return content;
}
// Only run if managers are in play
// Only run on pages that have fetched bundles via `getBundle` or `getBundleFileUrl`
if(Object.keys(managers).length === 0 || this.page.url && !pagesUsingBundles[this.page.url]) {
return content;
}
debug("Processing %o", this.page.url);
let render = new OutOfOrderRender(content);
for(let key in managers) {
render.setAssetManager(key, managers[key]);
}
render.setOutputDirectory(eleventyConfig.directories.output);
render.setWriteToFileSystem(writeToFileSystem);
return render.replaceAll(this.page);
});
};
export default eleventyBundleShortcodes;