diff --git a/img.js b/img.js index 97cd4fd..9d4e733 100644 --- a/img.js +++ b/img.js @@ -1,25 +1,30 @@ const path = require("path"); const fs = require("fs"); const fsp = fs.promises; -const { URL } = require("url"); + const { createHash } = require("crypto"); const {default: PQueue} = require("p-queue"); const getImageSize = require("image-size"); const sharp = require("sharp"); const brotliSize = require("brotli-size"); -const {RemoteAssetCache, queue} = require("@11ty/eleventy-fetch"); +const { RemoteAssetCache, queue } = require("@11ty/eleventy-fetch"); const svgHook = require("./src/format-hooks/svg.js"); const MemoryCache = require("./src/memory-cache.js"); +const DiskCache = require("./src/disk-cache.js"); +const Util = require("./src/util.js"); -const debug = require("debug")("EleventyImg"); +const debug = require("debug")("Eleventy:Image"); -const globalOptions = { - widths: [null], +const GLOBAL_OPTIONS = { + widths: ["auto"], formats: ["webp", "jpeg"], // "png", "svg", "avif" - concurrency: 10, + + concurrency: 20, + urlPath: "/img/", outputDir: "img/", + // true to skip raster formats if SVG input is found // "size" to skip raster formats if larger than SVG input svgShortCircuit: false, @@ -31,11 +36,13 @@ const globalOptions = { sharpPngOptions: {}, // options passed to the Sharp png output method sharpJpegOptions: {}, // options passed to the Sharp jpeg output method sharpAvifOptions: {}, // options passed to the Sharp avif output method - extensions: {}, + formatHooks: { svg: svgHook, }, + cacheDuration: "1d", // deprecated, use cacheOptions.duration + // disk cache for remote assets cacheOptions: { // duration: "1d", @@ -75,6 +82,12 @@ const globalOptions = { // be generated (400px). // Read more at https://github.com/11ty/eleventy-img/issues/184 and https://github.com/11ty/eleventy-img/pull/190 minimumThreshold: 1.25, + + // During --serve mode in Eleventy, this will generate images on request instead of part of the build skipping + // writes to the file system and speeding up builds! + transformOnRequest: false, + + // v5 `extensions` was removed (option to override output format with new file extension), it wasn’t being used anywhere or documented }; const MIME_TYPES = { @@ -92,49 +105,25 @@ const FORMAT_ALIASES = { "svg+xml": "svg", }; -class Util { - /* - * Does not mutate, returns new Object. - */ - static getSortedObject(unordered) { - let keys = Object.keys(unordered).sort(); - let obj = {}; - for(let key of keys) { - obj[key] = unordered[key]; - } - return obj; - } - - static isRemoteUrl(url) { - try { - const validUrl = new URL(url); - - if (validUrl.protocol.startsWith("https:") || validUrl.protocol.startsWith("http:")) { - return true; - } - - return false; - } catch(e) - - { - // invalid url OR local path - return false; - } - } -} - -// Temporary alias for changes made in https://github.com/11ty/eleventy-img/pull/138 -Util.isFullUrl = Util.isRemoteUrl; - class Image { - constructor(src, options) { + constructor(src, options = {}) { if(!src) { throw new Error("`src` is a required argument to the eleventy-img utility (can be a String file path, String URL, or Buffer)."); } this.src = src; this.isRemoteUrl = typeof src === "string" && Util.isRemoteUrl(src); - this.options = Object.assign({}, globalOptions, options); + + this.options = Object.assign({}, GLOBAL_OPTIONS, options); + + // Compatible with eleventy-dev-server and Eleventy 3.0.0-alpha.7+ in serve mode. + if(this.options.transformOnRequest && !this.options.urlFormat) { + this.options.urlFormat = function({ src, width, format }/*, imageOptions*/) { + return `/.11ty/image/?src=${encodeURIComponent(src)}&width=${width}&format=${format}`; + }; + + this.options.statsOnly = true; + } if(this.isRemoteUrl) { this.cacheOptions = Object.assign({ @@ -177,17 +166,19 @@ class Image { return false; } - // perf: check to make sure it’s not a string first - if(typeof this.src !== "string" && Buffer.isBuffer(this.src)) { - this._contents = this.src; - } - - // TODO @zachleat add a smarter cache here (not too aggressive! must handle input file changes) if(!this._contents) { - debug("Reading from file system: %o", this.src); - this._contents = fs.readFileSync(this.src); + // perf: check to make sure it’s not a string first + if(typeof this.src !== "string" && Buffer.isBuffer(this.src)) { + this._contents = this.src; + } else { + // TODO @zachleat make this aggressively async. + // TODO @zachleat add a smarter cache here (not too aggressive! must handle input file changes) + // debug("Reading from file system: %o", this.src); + this._contents = fs.readFileSync(this.src); + } } + return this._contents; } @@ -336,28 +327,34 @@ class Image { return {}; } - async getInput() { - if(this.isRemoteUrl) { - // fetch remote image Buffer - if(queue) { - // eleventy-fetch 3.0+ and eleventy-cache-assets 2.0.4+ - return queue(this.src, () => this.assetCache.fetch()); + // Returns promise + getInput() { + // internal cache + if(!this.inputPromise) { + if(this.isRemoteUrl) { + // fetch remote image Buffer + if(queue) { + // eleventy-fetch 3.0+ and eleventy-cache-assets 2.0.4+ + this.inputPromise = queue(this.src, () => this.assetCache.fetch()); + } else { + // eleventy-cache-assets 2.0.3 and below + this.inputPromise = this.assetCache.fetch(this.cacheOptions); + } + } else { + // TODO @zachleat (multiread): read local file contents here and always return a buffer + this.inputPromise = Promise.resolve(this.src); } - - // eleventy-cache-assets 2.0.3 and below - return this.assetCache.fetch(this.cacheOptions); } - // TODO @zachleat (multiread): read local file contents here and always return a buffer - return this.src; + return this.inputPromise; } getHash() { if (this.computedHash) { - debug("Re-using computed hash for %o: %o", this.src, this.computedHash); return this.computedHash; } + // debug("Creating hash for %o", this.src); let hash = createHash("sha256"); if(fs.existsSync(this.src)) { @@ -419,7 +416,7 @@ class Image { getStat(outputFormat, width, height) { let url; let outputFilename; - let outputExtension = this.options.extensions[outputFormat] || outputFormat; + if(this.options.urlFormat && typeof this.options.urlFormat === "function") { let hash; if(!this.options.statsOnly) { @@ -430,11 +427,11 @@ class Image { hash, src: this.src, width, - format: outputExtension, + format: outputFormat, }, this.options); } else { let hash = this.getHash(); - outputFilename = ImagePath.getFilename(hash, this.src, width, outputExtension, this.options); + outputFilename = ImagePath.getFilename(hash, this.src, width, outputFormat, this.options); url = ImagePath.convertFilePathToUrl(this.options.urlPath, outputFilename); } @@ -487,6 +484,7 @@ class Image { if(!outputFormat || outputFormat === "auto") { throw new Error("When using statsSync or statsByDimensionsSync, `formats: [null | auto]` to use the native image format is not supported."); } + if(outputFormat === "svg") { if((metadata.format || this.options.overrideInputFormat) === "svg") { let svgStats = this.getStat("svg", metadata.width, metadata.height); @@ -536,7 +534,7 @@ class Image { let fullStats = this.getFullStats(metadata); for(let outputFormat in fullStats) { for(let stat of fullStats[outputFormat]) { - if(this.options.useCache && fs.existsSync(stat.outputPath)){ + if(this.options.useCache && diskCache.isCached(stat.outputPath)){ // Cached images already exist in output let contents; if(this.options.dryRun) { @@ -638,7 +636,11 @@ class Image { } if(stat.outputPath) { - debug( "Wrote %o", stat.outputPath ); + if(this.options.dryRun) { + debug( "Generated %o", stat.url ); + } else { + debug( "Wrote %o", stat.outputPath ); + } } } } @@ -711,10 +713,11 @@ class ImagePath { /* Size Cache */ let memCache = new MemoryCache(); +let diskCache = new DiskCache(); /* Queue */ let processingQueue = new PQueue({ - concurrency: globalOptions.concurrency + concurrency: GLOBAL_OPTIONS.concurrency }); processingQueue.on("active", () => { debug( `Concurrency: ${processingQueue.concurrency}, Size: ${processingQueue.size}, Pending: ${processingQueue.pending}` ); @@ -723,8 +726,9 @@ processingQueue.on("active", () => { function queueImage(src, opts) { let img = new Image(src, opts); let key; + let resolvedOptions = img.options; - if(img.options.useCache) { + if(resolvedOptions.useCache) { // we don’t know the output format yet, but this hash is just for the in memory cache key = img.getInMemoryCacheKey(); let cached = memCache.get(key); @@ -733,28 +737,32 @@ function queueImage(src, opts) { } } - debug("In-memory cache miss for %o, options: %o", src, opts); + debug("Processing %o (in-memory cache miss), options: %o", src, opts); let promise = (async () => { - if(typeof src === "string" && opts && opts.statsOnly) { + if(typeof src === "string" && resolvedOptions.statsOnly) { if(Util.isRemoteUrl(src)) { - if(!opts.remoteImageMetadata || !opts.remoteImageMetadata.width || !opts.remoteImageMetadata.height) { - throw new Error("When using `statsOnly` and remote images, you must supply a `remoteImageMetadata` object with { width, height, format? }"); + if(opts.remoteImageMetadata?.width && opts.remoteImageMetadata?.height) { + return img.getFullStats({ + width: opts.remoteImageMetadata.width, + height: opts.remoteImageMetadata.height, + format: opts.remoteImageMetadata.format, // only required if you want to use the "auto" format + guess: true, + }); } - return img.getFullStats({ - width: opts.remoteImageMetadata.width, - height: opts.remoteImageMetadata.height, - format: opts.remoteImageMetadata.format, // only required if you want to use the "auto" format - guess: true, - }); - } else { // Local images - let { width, height, type } = getImageSize(src); - return img.getFullStats({ - width, - height, - format: type // only required if you want to use the "auto" format - }); + + // Fetch remote image to operate on it + src = await img.getInput(); } + + // Local images + let { width, height, type } = getImageSize(src); + + return img.getFullStats({ + width, + height, + format: type // only required if you want to use the "auto" format + }); } let input = await img.getInput(); @@ -805,3 +813,6 @@ module.exports.eleventyImageWebcOptionsPlugin = eleventyWebcOptionsPlugin; const { eleventyImageTransformPlugin } = require("./src/transform-plugin.js"); module.exports.eleventyImageTransformPlugin = eleventyImageTransformPlugin; + +const { eleventyImageOnRequestDuringServePlugin } = require("./src/on-request-during-serve-plugin.js"); +module.exports.eleventyImageOnRequestDuringServePlugin = eleventyImageOnRequestDuringServePlugin; diff --git a/package.json b/package.json index 04df5dd..d07f248 100644 --- a/package.json +++ b/package.json @@ -37,25 +37,26 @@ }, "homepage": "https://github.com/11ty/eleventy-img#readme", "dependencies": { - "@11ty/eleventy-fetch": "^4.0.0", + "@11ty/eleventy-fetch": "^4.0.1", + "@11ty/eleventy-utils": "^1.0.2", "brotli-size": "^4.0.0", "debug": "^4.3.4", "entities": "^4.5.0", "image-size": "^1.1.1", "p-queue": "^6.6.2", - "sharp": "^0.33.2" + "sharp": "^0.33.3" }, "devDependencies": { - "@11ty/eleventy": "^2.0.1", - "@11ty/eleventy-plugin-webc": "^0.11.1", - "ava": "^6.1.1", + "@11ty/eleventy": "3.0.0-alpha.6", + "@11ty/eleventy-plugin-webc": "^0.11.2", + "ava": "^6.1.2", "eslint": "^8.56.0", "pixelmatch": "^5.3.0" }, "ava": { "failFast": false, "files": [ - "./test/*.js" + "./test/*.{js,cjs,mjs}" ], "watchMode": { "ignoreChanges": [ diff --git a/src/disk-cache.js b/src/disk-cache.js new file mode 100644 index 0000000..90d8418 --- /dev/null +++ b/src/disk-cache.js @@ -0,0 +1,20 @@ +const fs = require("fs"); +// const debug = require("debug")("Eleventy:Image"); + +class DiskCache { + constructor() { + this.hitCounter = 0; + } + + isCached(path) { + if(fs.existsSync(path)) { + this.hitCounter++; + // debug("Images re-used (via disk cache): %o", this.hitCounter); + return true; + } + + return false; + } +} + +module.exports = DiskCache; diff --git a/src/global-options.js b/src/global-options.js index 4402256..4c9df3e 100644 --- a/src/global-options.js +++ b/src/global-options.js @@ -1,14 +1,14 @@ const path = require("path"); -function getGlobalOptions(eleventyDirectories, options) { +function getGlobalOptions(directories, options) { let globalOptions = Object.assign({ packages: { image: require("../"), }, - outputDir: path.join(eleventyDirectories.output, options.urlPath || ""), + outputDir: path.join(directories.output, options.urlPath || ""), }, options); - globalOptions.eleventyDirectories = eleventyDirectories; + globalOptions.directories = directories; return globalOptions; } diff --git a/src/memory-cache.js b/src/memory-cache.js index f88e50e..5a58334 100644 --- a/src/memory-cache.js +++ b/src/memory-cache.js @@ -1,8 +1,9 @@ -const debug = require("debug")("EleventyImg"); +const debug = require("debug")("Eleventy:Image"); class MemoryCache { constructor() { this.cache = {}; + this.hitCounter = 0; } add(key, results) { @@ -10,19 +11,18 @@ class MemoryCache { results }; - debug("Added %o to cache (size: %o)", key, Object.keys(this.cache).length); + debug("Unique images processed: %o", Object.keys(this.cache).length); } get(key) { if(this.cache[key]) { + this.hitCounter++; + // debug("Images re-used (via in-memory cache): %o", this.hitCounter); + // may return promise - // debug("Cache size %o", Object.keys(this.cache).length); - // debug("Found cached for %o", key); return this.cache[key].results; } - debug("Cache miss for %o", key); - return false; } } diff --git a/src/on-request-during-serve-plugin.js b/src/on-request-during-serve-plugin.js new file mode 100644 index 0000000..fa88bbf --- /dev/null +++ b/src/on-request-during-serve-plugin.js @@ -0,0 +1,95 @@ +const fs = require("fs"); +const { TemplatePath } = require("@11ty/eleventy-utils"); + +const eleventyImage = require("../img.js"); +const Util = require("./util.js"); + +const debug = require("debug")("Eleventy:Image"); + +function eleventyImageOnRequestDuringServePlugin(eleventyConfig, options = {}) { + try { + // Throw an error if the application is not using Eleventy 3.0.0-alpha.7 or newer (including prereleases). + eleventyConfig.versionCheck(">=3.0.0-alpha.7"); + } catch(e) { + console.log( `[11ty/eleventy-img] Warning: your version of Eleventy is incompatible with the dynamic image rendering plugin (see \`eleventyImageOnRequestDuringServePlugin\`). Any dynamically rendered images will 404 (be missing) during --serve mode but will not affect the standard build output: ${e.message}` ); + } + + // Eleventy 3.0 or newer only. + eleventyConfig.setServerOptions({ + onRequest: { + // TODO work with dev-server’s option for `injectedScriptsFolder` + "/.11ty/image/": async function({ url }) { + // src could be file path or full url + let src = decodeURIComponent(url.searchParams.get("src")); + let imageFormat = url.searchParams.get("format"); + let width = url.searchParams.get("width"); + let via = url.searchParams.get("via"); + + let defaultOptions; + if(via === "webc") { + defaultOptions = eleventyConfig.getFilter("__private_eleventyImageConfigurationOptions")(); + } else if(via === "transform") { + defaultOptions = eleventyConfig.getFilter("__private_eleventyImageTransformConfigurationOptions")(); + } + // if using this plugin directly (not via webc or transform), global default options will need to be passed in to the `addPlugin` call directly + + // Prefer options passed to this plugin, fallback to Transform plugin or WebC options if the image source was generated via those options. + let opts = Object.assign({}, defaultOptions, options, { + widths: [width || "auto"], + formats: [imageFormat || "auto"], + transformOnRequest: false, // use the built images so we don’t go in a loop + + dryRun: true, + cacheOptions: { + // We *do* want to write files to .cache for re-use here. + dryRun: false + } + }); + + debug( `%o transformed on request to %o at %o width.`, src, imageFormat, width ); + + try { + if(!Util.isFullUrl(src)) { + // Image path on file system must be in working directory + src = TemplatePath.absolutePath(".", src); + + if(!fs.existsSync(src) || !src.startsWith(TemplatePath.absolutePath("."))) { + throw new Error(`Invalid path: ${src}`); + } + } + + let stats = await eleventyImage(src, opts); + + let format = Object.keys(stats).pop(); + let stat = stats[format][0]; + if(!stat) { + throw new Error("Invalid image format."); + } + + return { + headers: { + // TODO Set cache headers to match eleventy-fetch cache options (though remote fetchs are still written to .cache) + "Content-Type": stat.sourceType, + }, + body: stat.buffer, + }; + } catch (error) { + debug("Error attempting to transform %o: %O", src, error); + + return { + status: 500, + headers: { + "Content-Type": "image/svg+xml", + "x-error-message": error.message + }, + body: ``, + }; + } + } + } + }); +} + +module.exports = { + eleventyImageOnRequestDuringServePlugin, +}; diff --git a/src/transform-plugin.js b/src/transform-plugin.js index b8ef1da..f938a08 100644 --- a/src/transform-plugin.js +++ b/src/transform-plugin.js @@ -1,38 +1,16 @@ const path = require("path"); +const Util = require("./util.js"); const { imageAttributesToPosthtmlNode, getOutputDirectory, cleanTag, isIgnored } = require("./image-attrs-to-posthtml-node.js"); const { getGlobalOptions } = require("./global-options.js"); - -function isFullUrl(url) { - try { - new URL(url); - return true; - } catch(e) { - return false; - } -} - -function normalizeImageSource({ inputPath, contentDir }, src) { - if(isFullUrl(src)) { - return src; - } - - if(!path.isAbsolute(src)) { - // if the image src is relative, make it relative to the template file (inputPath); - let dir = path.dirname(inputPath); - return path.join(dir, src); - } - - // if the image src is absolute, make it relative to the content directory. - return path.join(contentDir, src); -} +const { eleventyImageOnRequestDuringServePlugin } = require("./on-request-during-serve-plugin.js"); function transformTag(context, node, opts) { let originalSource = node.attrs.src; let { inputPath, outputPath, url } = context.page; - node.attrs.src = normalizeImageSource({ + node.attrs.src = Util.normalizeImageSource({ + input: opts.directories.input, inputPath, - contentDir: opts.eleventyDirectories.input, }, originalSource); let instanceOptions = {}; @@ -41,12 +19,12 @@ function transformTag(context, node, opts) { if(outputDirectory) { if(path.isAbsolute(outputDirectory)) { instanceOptions = { - outputDir: path.join(opts.eleventyDirectories.output, outputDirectory), + outputDir: path.join(opts.directories.output, outputDirectory), urlPath: outputDirectory, }; } else { instanceOptions = { - outputDir: path.join(opts.eleventyDirectories.output, url, outputDirectory), + outputDir: path.join(opts.directories.output, url, outputDirectory), urlPath: path.join(url, outputDirectory), }; } @@ -55,7 +33,7 @@ function transformTag(context, node, opts) { } else if(path.isAbsolute(originalSource)) { // if the path is an absolute one (relative to the content directory) write to a global output directory to avoid duplicate writes for identical source images. instanceOptions = { - outputDir: path.join(opts.eleventyDirectories.output, "/img/"), + outputDir: path.join(opts.directories.output, "/img/"), urlPath: "/img/", }; } else { @@ -79,18 +57,23 @@ function transformTag(context, node, opts) { function eleventyImageTransformPlugin(eleventyConfig, options = {}) { options = Object.assign({ extensions: "html", + transformOnRequest: process.env.ELEVENTY_RUN_MODE === "serve", }, options); - let eleventyDirectories; - eleventyConfig.on("eleventy.directories", (dirs) => { - eleventyDirectories = dirs; + if(options.transformOnRequest !== false) { + // Add the on-request plugin automatically (unless opt-out in this plugins options only) + eleventyConfig.addPlugin(eleventyImageOnRequestDuringServePlugin); + } + + // Notably, global options are not shared automatically with the WebC `eleventyImagePlugin` above. + // Devs can pass in the same object to both if they want! + let opts = getGlobalOptions(eleventyConfig.directories, options); + + eleventyConfig.addJavaScriptFunction("__private_eleventyImageTransformConfigurationOptions", () => { + return opts; }); function posthtmlPlugin(context) { - // Notably, global options are not shared automatically with the WebC `eleventyImagePlugin` above. - // Devs can pass in the same object to both if they want! - let opts = getGlobalOptions(eleventyDirectories, options); - return (tree) => { let promises = []; tree.match({ tag: 'img' }, (node) => { diff --git a/src/util.js b/src/util.js new file mode 100644 index 0000000..7155f86 --- /dev/null +++ b/src/util.js @@ -0,0 +1,51 @@ +const path = require("path"); +const { URL } = require("url"); + +class Util { + /* + * Does not mutate, returns new Object. + */ + static getSortedObject(unordered) { + let keys = Object.keys(unordered).sort(); + let obj = {}; + for(let key of keys) { + obj[key] = unordered[key]; + } + return obj; + } + + static isRemoteUrl(url) { + try { + const validUrl = new URL(url); + + if (validUrl.protocol.startsWith("https:") || validUrl.protocol.startsWith("http:")) { + return true; + } + + return false; + } catch(e) { + // invalid url OR local path + return false; + } + } + + static normalizeImageSource({ input, inputPath }, src) { + if(Util.isFullUrl(src)) { + return src; + } + + if(!path.isAbsolute(src)) { + // if the image src is relative, make it relative to the template file (inputPath); + let dir = path.dirname(inputPath); + return path.join(dir, src); + } + + // if the image src is absolute, make it relative to the input/content directory. + return path.join(input, src); + } +} + +// Temporary alias for changes made in https://github.com/11ty/eleventy-img/pull/138 +Util.isFullUrl = Util.isRemoteUrl; + +module.exports = Util; diff --git a/src/webc-options-plugin.js b/src/webc-options-plugin.js index b3d62e2..0f26045 100644 --- a/src/webc-options-plugin.js +++ b/src/webc-options-plugin.js @@ -1,16 +1,21 @@ const { getGlobalOptions } = require("./global-options.js"); +const { eleventyImageOnRequestDuringServePlugin } = require("./on-request-during-serve-plugin.js"); function eleventyWebcOptionsPlugin(eleventyConfig, options = {}) { - let eleventyDirectories; - eleventyConfig.on("eleventy.directories", (dirs) => { - eleventyDirectories = dirs; - }); + options = Object.assign({ + transformOnRequest: process.env.ELEVENTY_RUN_MODE === "serve", + }, options); // Notably, global options are not shared automatically with the `eleventyImageTransformPlugin` below. // Devs can pass in the same object to both if they want! eleventyConfig.addJavaScriptFunction("__private_eleventyImageConfigurationOptions", () => { - return getGlobalOptions(eleventyDirectories, options); + return getGlobalOptions(eleventyConfig.directories, options); }); + + if(options.transformOnRequest !== false) { + // Add the on-request plugin automatically (unless opt-out in this plugins options only) + eleventyConfig.addPlugin(eleventyImageOnRequestDuringServePlugin); + } } module.exports = { diff --git a/test/test-webc.js b/test/test-webc.mjs similarity index 60% rename from test/test-webc.js rename to test/test-webc.mjs index 992c8f1..431d738 100644 --- a/test/test-webc.js +++ b/test/test-webc.mjs @@ -1,5 +1,7 @@ -const test = require("ava"); -const Eleventy = require("@11ty/eleventy"); +import test from "ava"; +import Eleventy from "@11ty/eleventy"; +import eleventyWebcPlugin from "@11ty/eleventy-plugin-webc"; +import { eleventyImagePlugin } from "../img.js"; test("Using ", async t => { let elev = new Eleventy( "test/webc/simple.webc", "test/webc/_site", { @@ -37,3 +39,31 @@ test("With url-path", async t => { t.is(results[0].content, `photo of my tabby cat`); }); +test("With transform on request during dev mode", async t => { + let elev = new Eleventy( "test/webc/simple.webc", "test/webc/_site", { + config: eleventyConfig => { + // WebC + eleventyConfig.addPlugin(eleventyWebcPlugin, { + components: [ + // Add as a global WebC component + "eleventy-image.webc", + ] + }); + + // Image plugin + eleventyConfig.addPlugin(eleventyImagePlugin, { + // Set global default options + formats: ["auto"], + dryRun: true, + transformOnRequest: true, + + defaultAttributes: { + loading: "lazy", + } + }); + } + }); + + let results = await elev.toJSON(); + t.is(results[0].content, `My ugly mug`); +}); diff --git a/test/transform-test.mjs b/test/transform-test.mjs new file mode 100644 index 0000000..7b60e5d --- /dev/null +++ b/test/transform-test.mjs @@ -0,0 +1,101 @@ +import test from "ava"; +import Eleventy from "@11ty/eleventy"; +import { eleventyImageTransformPlugin } from "../img.js"; + +test("Using the transform plugin", async t => { + let elev = new Eleventy( "test", "test/_site", { + config: eleventyConfig => { + eleventyConfig.addTemplate("virtual.html", `My ugly mug`); + + eleventyConfig.addPlugin(eleventyImageTransformPlugin, { + dryRun: true // don’t write image files! + }); + } + }); + + let results = await elev.toJSON(); + t.is(results[0].content, `My ugly mug`); +}); + + +test("Using the transform plugin (override options)", async t => { + let elev = new Eleventy( "test", "test/_site", { + config: eleventyConfig => { + eleventyConfig.addTemplate("virtual.html", `My ugly mug`); + + eleventyConfig.addPlugin(eleventyImageTransformPlugin, { + formats: ["auto"], + dryRun: true // don’t write image files! + }); + } + }); + + let results = await elev.toJSON(); + t.is(results[0].content, `My ugly mug`); +}); + +test("Using the transform plugin with transform on request during dev mode", async t => { + let elev = new Eleventy( "test", "test/_site", { + config: eleventyConfig => { + eleventyConfig.addTemplate("virtual.html", `My ugly mug`); + + eleventyConfig.addPlugin(eleventyImageTransformPlugin, { + formats: ["auto"], + transformOnRequest: true, + dryRun: true, // don’t write image files! + + defaultAttributes: {} + }); + } + }); + + let results = await elev.toJSON(); + t.is(results[0].content, `My ugly mug`); +}); + +test("Using the transform plugin with transform on request during dev mode (with default attributes)", async t => { + let elev = new Eleventy( "test", "test/_site", { + config: eleventyConfig => { + eleventyConfig.addTemplate("virtual.html", `My ugly mug`); + + eleventyConfig.addPlugin(eleventyImageTransformPlugin, { + formats: ["auto"], + transformOnRequest: true, + dryRun: true, // don’t write image files! + + defaultAttributes: { + loading: "lazy", + } + }); + } + }); + + let results = await elev.toJSON(); + t.is(results[0].content, `My ugly mug`); +}); + + +test("Using the transform plugin with transform on request during dev mode but don’t override existing urlFormat", async t => { + let elev = new Eleventy( "test", "test/_site", { + config: eleventyConfig => { + eleventyConfig.addTemplate("virtual.html", `My ugly mug`); + + eleventyConfig.addPlugin(eleventyImageTransformPlugin, { + urlFormat: function(src) { + return 'https://example.com/'; + }, + formats: ["auto"], + transformOnRequest: true, + dryRun: true, // don’t write image files! + + defaultAttributes: { + loading: "lazy", + } + }); + } + }); + + let results = await elev.toJSON(); + t.is(results[0].content, `My ugly mug`); +}); +