Skip to content

vrugtehagel/eleventy-asset-hash

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

32 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

eleventy-asset-hash

Adds a hash query parameter to URLs in Eleventy projects (or even outside of Eleventy projects).

Installation

To install, run any of the following commands:

# For npm:
npx jsr add @vrugtehagel/eleventy-asset-hash
# For yarn:
yarn dlx jsr add @vrugtehagel/eleventy-asset-hash
# For pnpm:
pnpm dlx jsr add @vrugtehagel/eleventy-asset-hash
# For deno:
deno add @vrugtehagel/eleventy-asset-hash

Config

In your Eleventy configuration file (usually .eleventy.js), import/require the module and add the plugin using .addPlugin():

import EleventyDocumentOutline from "@vrugtehagel/eleventy-asset-hash";

export default function (eleventyConfig) {
  // …
  eleventyConfig.addPlugin(EleventyDocumentOutline, {
    algorithm: "SHA-256",
    include: ["**/*.html"],
    includeAssets: ["**/*.{css,js}"],
  });
  // …
}

As shown above, there are additional options one may pass as second argument to the .addPlugin() call, as an object. It may have the following keys:

  • algorithm: A hashing algorithm, as supported by the standardized crypto.subtle.digest(), such as 'SHA-1', 'SHA-256', 'SHA-384', or 'SHA-512'. Defaults to 'SHA-256'. For other algorithms, see the computeChecksum option.
  • maxLength: An optional maximum length for the hash. If the hash exceeds this lengths, it is trimmed to be maxLength. This exists mostly to reduce the impact of the added hash at the cost of some clash-resilience.
  • param: The name of the query param to use; defaults to 'v'.
  • directory: The output directory to process files in. Defaults to dir.ouput as specified in your Eleventy config. This is simultaneously the directory that found assets are resolved against, though see pathPrefix to adjust this.
  • include: An array of globs for the files to process. These globs are applied to the output directory (see the directory option), not the CWD.
  • exclude: An array of globs for files not to process. Any files matched by this is ignored even if they are matched by the include option.
  • pathPrefix: A prefix to cut off of absolute URLs. This is useful if your Eleventy output is uploaded to a site's subdirectory; for example, if your site is uploaded under /foo/, the setting pathPrefix: '/foo/' causes absolute URLs such as /foo/bar.js to look up bar.js (relative to the output directory).
  • includeAssets: An array of globs for the assets to hash and add query parameters for. Note that assets are not hashed if they are not referenced by any of the files processed, since then there's nowhere to add the query parameter.
  • excludeAssets: An array of globs for assets not to hash.
  • computeChecksum: A custom function to compute checksums based on a file's contents. It must accept one argument, an ArrayBuffer, and return the checksum as a string.
  • onMissing: An missing asset is one that is matched by includeAssets, not excluded by excludeAssets and referenced by a processed entry file, but not found in the file system. This option determines what should happen when such a file is found; set to either "ignore" to ignore missing assets, "warn" to log a warning and continue, or "error" to throw an error, halting the asset hashing before ever writing to the file system. By default, it is set to "warn".

About the codebase

At first glance, I thought asset hashing would be simple. Just hash the files, add a query parameter to references to said files, and done. Easy, right? Nope!

First of all, the strategy of hashing first and then replacing hashes doesn't work, because modifying the files by adding query parameters also changes their hash. For example, if A depends on B, and B depends on C, then A indirectly also depends on C. Specifically if C's hash changes, then the reference to C in B changes, which causes B's hash to change, which in turn changes A's as well. If we hash first, and then add the query parameters, then A never notices any changes in C, and requests an old, incorrect version of B, which includes C's old hash. No bueno!

The second issue is circular dependencies. We could have dependency loops of any given length, and we can't really just hash one first because it would (could) come with the same issue described above. Instead, we can hash dependency loops as a whole; if any of the files in a loop changes, then all other files in the loop need to get an updated hash anyway. But we need to be a little careful; if A depends on B, B on C, and C on B, then we must first hash B and C together, and only then handle A (even if A is also part of another dependency loop). This is because B and C would not depend on A whatsoever, and so including A in their hash would cause too many files to be invalidated.

Now, on to turning all this blabbering into a single generic (ideally performant) algorithm.

  • Step 1. Create an index of all files to process.
  • Step 2. Scan each file, creating a list of each asset referenced and their position.
  • Step 3. Using the references, find a smallest loop of (potentially just one) indexed dependencies and hash them together. After hashing, remove them (or it) from the index.
  • Step 4. Repeat step 3 until the index is empty.

And that's it!

Releases

No releases published

Packages

No packages published