@11ty/eleventy-plugin-bundle
Advanced tools
Comparing version 1.0.1 to 1.0.2
@@ -1,60 +0,11 @@ | ||
const fs = require("fs"); | ||
const path = require("path"); | ||
// const fsp = fs.promises; | ||
const { createHash } = require("crypto"); | ||
const hashCache = {}; | ||
const directoryExistsCache = {}; | ||
const BundleFileOutput = require("./BundleFileOutput"); | ||
const debug = require("debug")("Eleventy:Bundle"); | ||
class BundleFileOutput { | ||
constructor(outputDirectory, bundleDirectory) { | ||
this.outputDirectory = outputDirectory; | ||
this.bundleDirectory = bundleDirectory; | ||
this.hashLength = 10; | ||
} | ||
class CodeManager { | ||
// code is placed in this bucket by default | ||
static DEFAULT_BUCKET_NAME = "default"; | ||
getFilenameHash(content) { | ||
if(hashCache[content]) { | ||
return hashCache[content]; | ||
} | ||
// code is hoisted to this bucket when necessary | ||
static HOISTED_BUCKET_NAME = "default"; | ||
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) { | ||
let dir = path.join(this.outputDirectory, this.bundleDirectory); | ||
let filenameHash = this.getFilenameHash(content); | ||
let filename = this.getFilename(filenameHash, type); | ||
if(writeToFileSystem) { | ||
if(!directoryExistsCache[dir]) { | ||
fs.mkdirSync(dir, { recursive: true }); | ||
directoryExistsCache[dir] = true; | ||
} | ||
let fullPath = path.join(dir, filename); | ||
debug("Writing bundle %o", fullPath); | ||
fs.writeFileSync(fullPath, content); | ||
} | ||
return this.modifyPathToUrl(this.bundleDirectory, filename); | ||
} | ||
} | ||
class CodeManager { | ||
constructor(name) { | ||
@@ -65,4 +16,9 @@ this.name = name; | ||
this.transforms = []; | ||
this.isHoisting = true; | ||
} | ||
setHoisting(enabled) { | ||
this.isHoisting = !!enabled; | ||
} | ||
reset() { | ||
@@ -78,3 +34,3 @@ this.pages = {}; | ||
} | ||
return ["default"]; | ||
return [CodeManager.DEFAULT_BUCKET_NAME]; | ||
} | ||
@@ -90,2 +46,8 @@ | ||
_initBucket(pageUrl, bucket) { | ||
if(!this.pages[pageUrl][bucket]) { | ||
this.pages[pageUrl][bucket] = new Set(); | ||
} | ||
} | ||
addToPage(pageUrl, code = [], bucket) { | ||
@@ -104,16 +66,18 @@ if(!Array.isArray(code) && code) { | ||
let buckets = CodeManager.normalizeBuckets(bucket); | ||
for(let b of buckets) { | ||
if(!this.pages[pageUrl][b]) { | ||
this.pages[pageUrl][b] = new Set(); | ||
let codeContent = code.map(entry => { | ||
if(this.trimOnAdd) { | ||
return entry.trim(); | ||
} | ||
return entry; | ||
}); | ||
let content = code.map(entry => { | ||
if(this.trimOnAdd) { | ||
return entry.trim(); | ||
} | ||
return entry; | ||
}).join("\n"); | ||
debug("Adding %o to bundle %o for %o (bucket: %o)", content, this.name, pageUrl, b); | ||
this.pages[pageUrl][b].add(content); | ||
for(let b of buckets) { | ||
this._initBucket(pageUrl, b); | ||
debug("Adding code to bundle %o for %o (bucket: %o): %o", this.name, pageUrl, b, codeContent); | ||
for(let content of codeContent) { | ||
this.pages[pageUrl][b].add(content); | ||
} | ||
} | ||
@@ -135,2 +99,10 @@ } | ||
getBucketsForPage(pageData) { | ||
let pageUrl = pageData.url; | ||
if(!this.pages[pageUrl]) { | ||
return []; | ||
} | ||
return Object.keys(this.pages[pageUrl]); | ||
} | ||
async getForPage(pageData, buckets) { | ||
@@ -177,8 +149,33 @@ let url = pageData.url; | ||
let content = await this.getForPage(pageData, buckets); | ||
let writer = new BundleFileOutput(output, bundle); | ||
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]; | ||
} | ||
} | ||
module.exports = CodeManager; |
@@ -10,3 +10,4 @@ const pkg = require("./package.json"); | ||
// post-process | ||
transforms: [] | ||
transforms: [], | ||
hoistDuplicateBundlesFor: [], | ||
}, options); | ||
@@ -13,0 +14,0 @@ |
@@ -6,2 +6,4 @@ const CodeManager = require("./codeManager.js"); | ||
module.exports = function(eleventyConfig, options = {}) { | ||
// TODO throw an error if addPlugin is called more than once per build here. | ||
let managers = {}; | ||
@@ -11,2 +13,9 @@ | ||
managers[name] = new CodeManager(name); | ||
if(Array.isArray(options.hoistDuplicateBundlesFor) && options.hoistDuplicateBundlesFor.includes(name)) { | ||
managers[name].setHoisting(true); | ||
} else { | ||
managers[name].setHoisting(false); | ||
} | ||
managers[name].setTransforms(options.transforms); | ||
@@ -13,0 +22,0 @@ |
@@ -0,1 +1,3 @@ | ||
const debug = require("debug")("Eleventy:Bundle"); | ||
/* This class defers any `bundleGet` calls to a post-build transform step, | ||
@@ -13,2 +15,3 @@ * to allow `getBundle` to be called before all of the `css` additions have been processed | ||
// type if `get` (return string) or `file` (bundle writes to file, returns file url) | ||
static getAssetKey(type, name, bucket) { | ||
@@ -57,5 +60,61 @@ if(Array.isArray(bucket)) { | ||
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 => { | ||
@@ -67,11 +126,10 @@ if(typeof match === "string") { | ||
let {type, name, bucket} = match; | ||
if(!this.managers[name]) { | ||
throw new Error(`No asset manager found for ${name}. Known keys: ${Object.keys(this.managers)}`); | ||
} | ||
let manager = this.getManager(name); | ||
if(type === "get") { | ||
// returns promise | ||
return this.managers[name].getForPage(pageData, bucket); | ||
return manager.getForPage(pageData, bucket); | ||
} else if(type === "file") { | ||
// returns promise | ||
return this.managers[name].writeBundle(pageData, bucket, { | ||
return manager.writeBundle(pageData, bucket, { | ||
output: this.outputDirectory, | ||
@@ -85,2 +143,9 @@ bundle: this.bundleDirectory, | ||
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(""); | ||
@@ -87,0 +152,0 @@ } |
{ | ||
"name": "@11ty/eleventy-plugin-bundle", | ||
"version": "1.0.1", | ||
"version": "1.0.2", | ||
"description": "Little bundles of code, little bundles of joy.", | ||
@@ -28,2 +28,8 @@ "main": "eleventy.bundle.js", | ||
], | ||
"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": { | ||
@@ -45,7 +51,7 @@ "name": "Zach Leatherman", | ||
"devDependencies": { | ||
"@11ty/eleventy": "2.0.0-beta.3", | ||
"ava": "^5.1.0", | ||
"@11ty/eleventy": "^2.0.0", | ||
"ava": "^5.2.0", | ||
"postcss": "^8.4.21", | ||
"postcss-nested": "^6.0.0", | ||
"sass": "^1.58.0" | ||
"postcss-nested": "^6.0.1", | ||
"sass": "^1.58.3" | ||
}, | ||
@@ -52,0 +58,0 @@ "dependencies": { |
@@ -5,5 +5,5 @@ # eleventy-plugin-bundle | ||
Create minimal per-page or app-level bundles of CSS, JavaScript, or HTML bundles to be included in your Eleventy project. | ||
Create minimal per-page or app-level bundles of CSS, JavaScript, or HTML to be included in your Eleventy project. | ||
Makes implementing Critical CSS, per-page in-use-only CSS/JS bundles, SVG icon libraries, secondary HTML content to load via XHR. | ||
Makes it easy to implement Critical CSS, in-use-only CSS/JS bundles, SVG icon libraries, or secondary HTML content to load via XHR. | ||
@@ -49,8 +49,11 @@ ## Why? | ||
// Default bundle types | ||
bundles: ["css", "js", "html"], | ||
// Extra bundle names ("css", "js", "html" are guaranteed) | ||
bundles: [], | ||
// Array of async-friendly callbacks to transform bundle content. | ||
// Works with getBundle and getBundleFileUrl | ||
transforms: [] | ||
transforms: [], | ||
// Array of bundle names eligible for duplicate bundle hoisting | ||
hoistDuplicateBundlesFor: [], // e.g. ["css", "js"] | ||
}); | ||
@@ -60,2 +63,4 @@ }; | ||
Read more about [`hoistDuplicateBundlesFor` and duplicate bundle hoisting](https://github.com/11ty/eleventy-plugin-bundle/issues/5). | ||
</details> | ||
@@ -65,3 +70,3 @@ | ||
The following shortcodes are provided by this plugin: | ||
The following Universal Shortcodes (available in `njk`, `liquid`, `hbs`, `11ty.js`, and `webc`) are provided by this plugin: | ||
@@ -71,2 +76,4 @@ * `css`, `js`, and `html` to add code to a bundle. | ||
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). | ||
### Add bundle code in a Markdown file in Eleventy | ||
@@ -240,5 +247,5 @@ | ||
Starting with `@11ty/eleventy-plugin-webc@0.9.0` 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. | ||
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 page bundle in WebC, you would use a `<style>` element in a WebC page or component: | ||
To add CSS to a bundle in WebC, you would use a `<style>` element in a WebC page or component: | ||
@@ -257,5 +264,5 @@ ```html | ||
* Existing calls via WebC helpers `getCss` or `getJs` (e.g. `<style @raw="getCss(page.url)">`) have been wired up to `getBundle('css')` and `getBundle('js')` automatically. | ||
* 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`](https://www.11ty.dev/docs/languages/webc/#css-and-js-(bundler-mode)) were available to access CSS and JS bundles but are considered deprecated in favor of new bundle plugin Universal Shortcodes `getBundle("css")` and `getBundle("js")` respectively. | ||
* Outside of WebC, the [Universal Filters `webcGetCss` and `webcGetJs`](https://www.11ty.dev/docs/languages/webc/#css-and-js-(bundler-mode)) were available to access CSS and JS bundles but are considered deprecated in favor of the `getBundle` Universal Shortcode (`{% getBundle "css" %}` and `{% getBundle "js" %}` respectively). | ||
@@ -262,0 +269,0 @@ #### Modify the bundle output |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
No website
QualityPackage does not have a website.
Found 1 instance in 1 package
26025
7
411
0
332
1