From 0ddc71359d9fd1f3a56ff9e67956f8d57fdc3a68 Mon Sep 17 00:00:00 2001 From: Alex Russell Date: Sun, 1 Nov 2020 14:39:03 -0800 Subject: [PATCH 1/9] Disgusting hack to prevent NJK from over-compiling --- src/Engines/Nunjucks.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Engines/Nunjucks.js b/src/Engines/Nunjucks.js index 34b685e7b..451895ae1 100755 --- a/src/Engines/Nunjucks.js +++ b/src/Engines/Nunjucks.js @@ -4,6 +4,34 @@ const TemplatePath = require("../TemplatePath"); const EleventyErrorUtil = require("../EleventyErrorUtil"); const EleventyBaseError = require("../EleventyBaseError"); +/* +// HACKITYHACKHACKHACKHACK +*/ +let pathMap = new Map(); +let tc = NunjucksLib.Template.prototype._compile; +let getKey = (obj) => { + return `${obj.path} :: ${obj.tmplStr.length}`; +}; +NunjucksLib.Template.prototype._compile = function (...args) { + if (!this.tmplProps && pathMap.has(getKey(this))) { + // console.log(`### cached ${getKey(this)}`); + let pathProps = pathMap.get(getKey(this)); + this.blocks = pathProps.blocks; + this.rootRenderFunc = pathProps.rootRenderFunc; + this.compiled = true; + } else { + tc.call(this, ...args); + // console.log(`### caching for ${getKey(this)}`); + pathMap.set(getKey(this), { + blocks: this.blocks, + rootRenderFunc: this.rootRenderFunc, + }); + } +}; +/* +// END HACKITYHACKHACKHACKHACK +*/ + class EleventyShortcodeError extends EleventyBaseError {} class Nunjucks extends TemplateEngine { From b69f602f6e3bc94ecbf6f7c803706e0ec4b8690c Mon Sep 17 00:00:00 2001 From: Alex Russell Date: Tue, 24 Nov 2020 11:47:26 -0800 Subject: [PATCH 2/9] Correctness fixes on NJK tmpl caching. --- src/Engines/Nunjucks.js | 93 +++++++++++++++++++++++++++++++---------- 1 file changed, 70 insertions(+), 23 deletions(-) diff --git a/src/Engines/Nunjucks.js b/src/Engines/Nunjucks.js index 451895ae1..c87b98f01 100755 --- a/src/Engines/Nunjucks.js +++ b/src/Engines/Nunjucks.js @@ -5,32 +5,76 @@ const EleventyErrorUtil = require("../EleventyErrorUtil"); const EleventyBaseError = require("../EleventyBaseError"); /* -// HACKITYHACKHACKHACKHACK -*/ + * HACKITYHACKHACKHACKHACK + */ +let inc = ((i) => { + return () => { + return i++; + }; +})(0); let pathMap = new Map(); -let tc = NunjucksLib.Template.prototype._compile; -let getKey = (obj) => { - return `${obj.path} :: ${obj.tmplStr.length}`; -}; -NunjucksLib.Template.prototype._compile = function (...args) { - if (!this.tmplProps && pathMap.has(getKey(this))) { - // console.log(`### cached ${getKey(this)}`); - let pathProps = pathMap.get(getKey(this)); - this.blocks = pathProps.blocks; - this.rootRenderFunc = pathProps.rootRenderFunc; - this.compiled = true; - } else { - tc.call(this, ...args); - // console.log(`### caching for ${getKey(this)}`); - pathMap.set(getKey(this), { - blocks: this.blocks, - rootRenderFunc: this.rootRenderFunc, - }); + +let clearCache = (reason = "") => { + if (pathMap.size > 0) { + pathMap.clear(); } }; + +let clearAndWrap = (name, obj = NunjucksLib.Environment) => { + let orig = obj.prototype[name]; + obj.prototype[name] = function (...args) { + clearCache(name); + return orig.call(this, ...args); + }; +}; + +clearAndWrap("addExtension"); +clearAndWrap("removeExtension"); + +(function () { + // IFFE to prevent temp var leakage + let getKey = (obj) => { + let eids = obj.env.extensionsList.reduce((v, c) => { + return c.__id ? v + " : " + c.__id : v; + }, ""); + let k = + `${obj.path || obj.tmplStr} :: ${obj.tmplStr.length} :: ` + + `${obj.env.asyncFilters.length} :: (${eids})`; /* + + ( + (obj.env.extensionsList.length) ? + `${Object.keys(obj.env.extensions).join("|")} :: (${eids})` : + "" + ); + */ + return k; + }; + let tc = NunjucksLib.Template.prototype._compile; + NunjucksLib.Template.prototype._compile = function (...args) { + // console.log(`NunjucksLib.Template.prototype._compile`); + if (!this.compiled && !this.tmplProps && pathMap.has(getKey(this))) { + let pathProps = pathMap.get(getKey(this)); + this.blocks = pathProps.blocks; + this.rootRenderFunc = pathProps.rootRenderFunc; + this.compiled = true; + } else { + tc.call(this, ...args); + pathMap.set(getKey(this), { + blocks: this.blocks, + rootRenderFunc: this.rootRenderFunc, + }); + } + }; + + let ae = NunjucksLib.Environment.prototype.addExtension; + NunjucksLib.Environment.prototype.addExtension = function (...args) { + let e = args[1]; + e.__id = inc(); + return ae.call(this, ...args); + }; +})(); /* -// END HACKITYHACKHACKHACKHACK -*/ + * END HACKITYHACKHACKHACKHACK + */ class EleventyShortcodeError extends EleventyBaseError {} @@ -92,6 +136,7 @@ class Nunjucks extends TemplateEngine { ); } + clearCache("addTag"); this.njkEnv.addExtension(name, tagObj); } @@ -165,7 +210,6 @@ class Nunjucks extends TemplateEngine { }); } else { try { - // console.log( shortcodeFn.toString() ); return new NunjucksLib.runtime.SafeString( shortcodeFn.call( Nunjucks._normalizeShortcodeContext(context), @@ -183,6 +227,8 @@ class Nunjucks extends TemplateEngine { }; } + // Invalidate the caches. + clearCache("addShortcode"); this.njkEnv.addExtension(shortcodeName, new ShortcodeFunction()); } @@ -253,6 +299,7 @@ class Nunjucks extends TemplateEngine { }; } + clearCache("addPairedShortcode"); this.njkEnv.addExtension(shortcodeName, new PairedShortcodeFunction()); } From bf43bface7c8487783c2d306c4e1be9bb62c2c93 Mon Sep 17 00:00:00 2001 From: Alex Russell Date: Sun, 1 Nov 2020 14:39:03 -0800 Subject: [PATCH 3/9] Disgusting hack to prevent NJK from over-compiling --- src/Engines/Nunjucks.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Engines/Nunjucks.js b/src/Engines/Nunjucks.js index 01e349488..94ead1027 100755 --- a/src/Engines/Nunjucks.js +++ b/src/Engines/Nunjucks.js @@ -4,6 +4,34 @@ const TemplatePath = require("../TemplatePath"); const EleventyErrorUtil = require("../EleventyErrorUtil"); const EleventyBaseError = require("../EleventyBaseError"); +/* +// HACKITYHACKHACKHACKHACK +*/ +let pathMap = new Map(); +let tc = NunjucksLib.Template.prototype._compile; +let getKey = (obj) => { + return `${obj.path} :: ${obj.tmplStr.length}`; +}; +NunjucksLib.Template.prototype._compile = function (...args) { + if (!this.tmplProps && pathMap.has(getKey(this))) { + // console.log(`### cached ${getKey(this)}`); + let pathProps = pathMap.get(getKey(this)); + this.blocks = pathProps.blocks; + this.rootRenderFunc = pathProps.rootRenderFunc; + this.compiled = true; + } else { + tc.call(this, ...args); + // console.log(`### caching for ${getKey(this)}`); + pathMap.set(getKey(this), { + blocks: this.blocks, + rootRenderFunc: this.rootRenderFunc, + }); + } +}; +/* +// END HACKITYHACKHACKHACKHACK +*/ + class EleventyShortcodeError extends EleventyBaseError {} class Nunjucks extends TemplateEngine { From e932b41bf050e7db2abe6d7d3f8e794f80b335f9 Mon Sep 17 00:00:00 2001 From: Alex Russell Date: Tue, 24 Nov 2020 11:47:26 -0800 Subject: [PATCH 4/9] Correctness fixes on NJK tmpl caching. --- src/Engines/Nunjucks.js | 93 +++++++++++++++++++++++++++++++---------- 1 file changed, 70 insertions(+), 23 deletions(-) diff --git a/src/Engines/Nunjucks.js b/src/Engines/Nunjucks.js index 94ead1027..6e46d9dd1 100755 --- a/src/Engines/Nunjucks.js +++ b/src/Engines/Nunjucks.js @@ -5,32 +5,76 @@ const EleventyErrorUtil = require("../EleventyErrorUtil"); const EleventyBaseError = require("../EleventyBaseError"); /* -// HACKITYHACKHACKHACKHACK -*/ + * HACKITYHACKHACKHACKHACK + */ +let inc = ((i) => { + return () => { + return i++; + }; +})(0); let pathMap = new Map(); -let tc = NunjucksLib.Template.prototype._compile; -let getKey = (obj) => { - return `${obj.path} :: ${obj.tmplStr.length}`; -}; -NunjucksLib.Template.prototype._compile = function (...args) { - if (!this.tmplProps && pathMap.has(getKey(this))) { - // console.log(`### cached ${getKey(this)}`); - let pathProps = pathMap.get(getKey(this)); - this.blocks = pathProps.blocks; - this.rootRenderFunc = pathProps.rootRenderFunc; - this.compiled = true; - } else { - tc.call(this, ...args); - // console.log(`### caching for ${getKey(this)}`); - pathMap.set(getKey(this), { - blocks: this.blocks, - rootRenderFunc: this.rootRenderFunc, - }); + +let clearCache = (reason = "") => { + if (pathMap.size > 0) { + pathMap.clear(); } }; + +let clearAndWrap = (name, obj = NunjucksLib.Environment) => { + let orig = obj.prototype[name]; + obj.prototype[name] = function (...args) { + clearCache(name); + return orig.call(this, ...args); + }; +}; + +clearAndWrap("addExtension"); +clearAndWrap("removeExtension"); + +(function () { + // IFFE to prevent temp var leakage + let getKey = (obj) => { + let eids = obj.env.extensionsList.reduce((v, c) => { + return c.__id ? v + " : " + c.__id : v; + }, ""); + let k = + `${obj.path || obj.tmplStr} :: ${obj.tmplStr.length} :: ` + + `${obj.env.asyncFilters.length} :: (${eids})`; /* + + ( + (obj.env.extensionsList.length) ? + `${Object.keys(obj.env.extensions).join("|")} :: (${eids})` : + "" + ); + */ + return k; + }; + let tc = NunjucksLib.Template.prototype._compile; + NunjucksLib.Template.prototype._compile = function (...args) { + // console.log(`NunjucksLib.Template.prototype._compile`); + if (!this.compiled && !this.tmplProps && pathMap.has(getKey(this))) { + let pathProps = pathMap.get(getKey(this)); + this.blocks = pathProps.blocks; + this.rootRenderFunc = pathProps.rootRenderFunc; + this.compiled = true; + } else { + tc.call(this, ...args); + pathMap.set(getKey(this), { + blocks: this.blocks, + rootRenderFunc: this.rootRenderFunc, + }); + } + }; + + let ae = NunjucksLib.Environment.prototype.addExtension; + NunjucksLib.Environment.prototype.addExtension = function (...args) { + let e = args[1]; + e.__id = inc(); + return ae.call(this, ...args); + }; +})(); /* -// END HACKITYHACKHACKHACKHACK -*/ + * END HACKITYHACKHACKHACKHACK + */ class EleventyShortcodeError extends EleventyBaseError {} @@ -93,6 +137,7 @@ class Nunjucks extends TemplateEngine { ); } + clearCache("addTag"); this.njkEnv.addExtension(name, tagObj); } @@ -176,7 +221,6 @@ class Nunjucks extends TemplateEngine { }); } else { try { - // console.log( shortcodeFn.toString() ); return new NunjucksLib.runtime.SafeString( shortcodeFn.call( Nunjucks._normalizeShortcodeContext(context), @@ -194,6 +238,8 @@ class Nunjucks extends TemplateEngine { }; } + // Invalidate the caches. + clearCache("addShortcode"); this.njkEnv.addExtension(shortcodeName, new ShortcodeFunction()); } @@ -264,6 +310,7 @@ class Nunjucks extends TemplateEngine { }; } + clearCache("addPairedShortcode"); this.njkEnv.addExtension(shortcodeName, new PairedShortcodeFunction()); } From 3a005de2124677722643729fb8ea96a44368b5cc Mon Sep 17 00:00:00 2001 From: Alex Russell Date: Tue, 24 Nov 2020 13:35:56 -0800 Subject: [PATCH 5/9] Cleanup variables and scopes per review comments. --- src/Engines/Nunjucks.js | 91 +++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 54 deletions(-) diff --git a/src/Engines/Nunjucks.js b/src/Engines/Nunjucks.js index 6e46d9dd1..2a71ea46d 100755 --- a/src/Engines/Nunjucks.js +++ b/src/Engines/Nunjucks.js @@ -5,76 +5,60 @@ const EleventyErrorUtil = require("../EleventyErrorUtil"); const EleventyBaseError = require("../EleventyBaseError"); /* - * HACKITYHACKHACKHACKHACK + * This IFFE applies a monkey-patch to Nunjucks internals to cache + * compiled templates and re-use them where possible. It's relatively + * pessimistic (conservative) about cache clearing to ensure correctness. */ -let inc = ((i) => { - return () => { - return i++; - }; -})(0); -let pathMap = new Map(); - -let clearCache = (reason = "") => { - if (pathMap.size > 0) { - pathMap.clear(); - } -}; +(function () { + let templateCache = new Map(); -let clearAndWrap = (name, obj = NunjucksLib.Environment) => { - let orig = obj.prototype[name]; - obj.prototype[name] = function (...args) { - clearCache(name); - return orig.call(this, ...args); + let clearAndWrap = (name, obj = NunjucksLib.Environment) => { + let orig = obj.prototype[name]; + obj.prototype[name] = function (...args) { + templateCache.clear(); + return orig.call(this, ...args); + }; }; -}; -clearAndWrap("addExtension"); -clearAndWrap("removeExtension"); + clearAndWrap("addExtension"); + clearAndWrap("removeExtension"); -(function () { - // IFFE to prevent temp var leakage let getKey = (obj) => { - let eids = obj.env.extensionsList.reduce((v, c) => { - return c.__id ? v + " : " + c.__id : v; - }, ""); - let k = - `${obj.path || obj.tmplStr} :: ${obj.tmplStr.length} :: ` + - `${obj.env.asyncFilters.length} :: (${eids})`; /* + - ( - (obj.env.extensionsList.length) ? - `${Object.keys(obj.env.extensions).join("|")} :: (${eids})` : - "" - ); - */ - return k; + return [ + obj.path || obj.tmplStr, + obj.tmplStr.length, + obj.env.asyncFilters.length, + obj.env.extensionsList + .map((e) => { + return e.__id || ""; + }) + .join(":"), + ].join(" :: "); }; - let tc = NunjucksLib.Template.prototype._compile; + + let _compile = NunjucksLib.Template.prototype._compile; NunjucksLib.Template.prototype._compile = function (...args) { - // console.log(`NunjucksLib.Template.prototype._compile`); - if (!this.compiled && !this.tmplProps && pathMap.has(getKey(this))) { - let pathProps = pathMap.get(getKey(this)); + if (!this.compiled && !this.tmplProps && templateCache.has(getKey(this))) { + let pathProps = templateCache.get(getKey(this)); this.blocks = pathProps.blocks; this.rootRenderFunc = pathProps.rootRenderFunc; this.compiled = true; } else { - tc.call(this, ...args); - pathMap.set(getKey(this), { + _compile.call(this, ...args); + templateCache.set(getKey(this), { blocks: this.blocks, rootRenderFunc: this.rootRenderFunc, }); } }; - let ae = NunjucksLib.Environment.prototype.addExtension; - NunjucksLib.Environment.prototype.addExtension = function (...args) { - let e = args[1]; - e.__id = inc(); - return ae.call(this, ...args); + let extensionIdCounter = 0; + let addExtension = NunjucksLib.Environment.prototype.addExtension; + NunjucksLib.Environment.prototype.addExtension = function (name, ext) { + ext.__id = extensionIdCounter++; + return addExtension.call(this, name, ext); }; })(); -/* - * END HACKITYHACKHACKHACKHACK - */ class EleventyShortcodeError extends EleventyBaseError {} @@ -137,7 +121,7 @@ class Nunjucks extends TemplateEngine { ); } - clearCache("addTag"); + // clearCache(); this.njkEnv.addExtension(name, tagObj); } @@ -238,8 +222,7 @@ class Nunjucks extends TemplateEngine { }; } - // Invalidate the caches. - clearCache("addShortcode"); + // clearCache(); this.njkEnv.addExtension(shortcodeName, new ShortcodeFunction()); } @@ -310,7 +293,7 @@ class Nunjucks extends TemplateEngine { }; } - clearCache("addPairedShortcode"); + // clearCache(); this.njkEnv.addExtension(shortcodeName, new PairedShortcodeFunction()); } From acad9bdb475c3b9b8ae2c3e8f71a74ce0cc3a6f9 Mon Sep 17 00:00:00 2001 From: Alex Russell Date: Tue, 24 Nov 2020 13:42:14 -0800 Subject: [PATCH 6/9] Further cleanup. --- src/Engines/Nunjucks.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Engines/Nunjucks.js b/src/Engines/Nunjucks.js index 2a71ea46d..86971c734 100755 --- a/src/Engines/Nunjucks.js +++ b/src/Engines/Nunjucks.js @@ -37,7 +37,7 @@ const EleventyBaseError = require("../EleventyBaseError"); }; let _compile = NunjucksLib.Template.prototype._compile; - NunjucksLib.Template.prototype._compile = function (...args) { + NunjucksLib.Template.prototype._compile = function _wrap_compile(...args) { if (!this.compiled && !this.tmplProps && templateCache.has(getKey(this))) { let pathProps = templateCache.get(getKey(this)); this.blocks = pathProps.blocks; @@ -54,7 +54,10 @@ const EleventyBaseError = require("../EleventyBaseError"); let extensionIdCounter = 0; let addExtension = NunjucksLib.Environment.prototype.addExtension; - NunjucksLib.Environment.prototype.addExtension = function (name, ext) { + NunjucksLib.Environment.prototype.addExtension = function _wrap_addExtension( + name, + ext + ) { ext.__id = extensionIdCounter++; return addExtension.call(this, name, ext); }; @@ -121,7 +124,6 @@ class Nunjucks extends TemplateEngine { ); } - // clearCache(); this.njkEnv.addExtension(name, tagObj); } @@ -222,7 +224,6 @@ class Nunjucks extends TemplateEngine { }; } - // clearCache(); this.njkEnv.addExtension(shortcodeName, new ShortcodeFunction()); } @@ -293,7 +294,6 @@ class Nunjucks extends TemplateEngine { }; } - // clearCache(); this.njkEnv.addExtension(shortcodeName, new PairedShortcodeFunction()); } From e15ae7914c342a87e0b014c7ac32e4183666aa23 Mon Sep 17 00:00:00 2001 From: Alex Russell Date: Thu, 26 Nov 2020 11:03:44 -0800 Subject: [PATCH 7/9] Less aggressive cache invalidation for a small speedup. --- src/Engines/Nunjucks.js | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/Engines/Nunjucks.js b/src/Engines/Nunjucks.js index 86971c734..474f8244e 100755 --- a/src/Engines/Nunjucks.js +++ b/src/Engines/Nunjucks.js @@ -12,17 +12,6 @@ const EleventyBaseError = require("../EleventyBaseError"); (function () { let templateCache = new Map(); - let clearAndWrap = (name, obj = NunjucksLib.Environment) => { - let orig = obj.prototype[name]; - obj.prototype[name] = function (...args) { - templateCache.clear(); - return orig.call(this, ...args); - }; - }; - - clearAndWrap("addExtension"); - clearAndWrap("removeExtension"); - let getKey = (obj) => { return [ obj.path || obj.tmplStr, @@ -58,7 +47,9 @@ const EleventyBaseError = require("../EleventyBaseError"); name, ext ) { - ext.__id = extensionIdCounter++; + if (!("__id" in ext)) { + ext.__id = extensionIdCounter++; + } return addExtension.call(this, name, ext); }; })(); From cce873d6cfde4da12392615705c4bde175dd0dc6 Mon Sep 17 00:00:00 2001 From: Alex Russell Date: Thu, 3 Dec 2020 10:35:40 -0800 Subject: [PATCH 8/9] Optmises the hottest loop inside Nunjucks for another 10-20%. --- src/Engines/Nunjucks.js | 47 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/src/Engines/Nunjucks.js b/src/Engines/Nunjucks.js index 474f8244e..42184f861 100755 --- a/src/Engines/Nunjucks.js +++ b/src/Engines/Nunjucks.js @@ -6,8 +6,7 @@ const EleventyBaseError = require("../EleventyBaseError"); /* * This IFFE applies a monkey-patch to Nunjucks internals to cache - * compiled templates and re-use them where possible. It's relatively - * pessimistic (conservative) about cache clearing to ensure correctness. + * compiled templates and re-use them where possible. */ (function () { let templateCache = new Map(); @@ -52,6 +51,50 @@ const EleventyBaseError = require("../EleventyBaseError"); } return addExtension.call(this, name, ext); }; + + // NunjucksLib.runtime.Frame.prototype.set is the hotest in-template method. + // We replace it with a version that doesn't allocate a `parts` array on + // repeat key use. + let partsCache = new Map(); + let partsFromCache = (name) => { + if (partsCache.has(name)) { + return partsCache.get(name); + } + + let parts = name.split("."); + partsCache.set(name, parts); + return parts; + }; + + let frameSet = NunjucksLib.runtime.Frame.prototype.set; + NunjucksLib.runtime.Frame.prototype.set = function _replacement_set( + name, + val, + resolveUp + ) { + let parts = partsFromCache(name); + let frame = this; + let obj = frame.variables; + + if (resolveUp) { + if ((frame = this.resolve(parts[0], true))) { + frame.set(name, val); + return; + } + } + + // A slightly faster version of the intermediate object allocation loop + let count = parts.length - 1; + let i = 0; + let id = parts[0]; + while (i < count) { + if (!obj.hasOwnProperty(id)) { + obj = obj[id] = {}; + } + id = parts[++i]; + } + obj[id] = val; + }; })(); class EleventyShortcodeError extends EleventyBaseError {} From 8a47a0752fc91c2d76a7dd451e0da8d7b6e3b9de Mon Sep 17 00:00:00 2001 From: Alex Russell Date: Fri, 4 Dec 2020 15:09:03 -0800 Subject: [PATCH 9/9] Re-enables NJK's internal caching by invalidating caches on change. --- src/Eleventy.js | 3 ++- src/Engines/Nunjucks.js | 35 ++++++++++++++++++++++++----------- src/EventBus.js | 17 +++++++++++++++++ src/TemplateContent.js | 4 ++++ test/TemplateTest.js | 28 ++++++++++++++++++---------- 5 files changed, 65 insertions(+), 22 deletions(-) create mode 100644 src/EventBus.js diff --git a/src/Eleventy.js b/src/Eleventy.js index 0e210d5db..40b3d03a7 100644 --- a/src/Eleventy.js +++ b/src/Eleventy.js @@ -18,6 +18,7 @@ const deleteRequireCache = require("./Util/DeleteRequireCache"); const config = require("./Config"); const bench = require("./BenchmarkManager"); const debug = require("debug")("Eleventy"); +const eventBus = require("./EventBus"); /** * @module 11ty/eleventy/Eleventy @@ -449,7 +450,7 @@ Arguments: * @param {String} changedFilePath - File that triggered a re-run (added or modified) */ async _addFileToWatchQueue(changedFilePath) { - TemplateContent.deleteCached(changedFilePath); + eventBus.emit("resourceModified", changedFilePath); this.watchManager.addToPendingQueue(changedFilePath); } diff --git a/src/Engines/Nunjucks.js b/src/Engines/Nunjucks.js index 42184f861..dfcc9614f 100755 --- a/src/Engines/Nunjucks.js +++ b/src/Engines/Nunjucks.js @@ -3,9 +3,10 @@ const TemplateEngine = require("./TemplateEngine"); const TemplatePath = require("../TemplatePath"); const EleventyErrorUtil = require("../EleventyErrorUtil"); const EleventyBaseError = require("../EleventyBaseError"); +const eventBus = require("../EventBus"); /* - * This IFFE applies a monkey-patch to Nunjucks internals to cache + * The IFFE below apply a monkey-patch to Nunjucks internals to cache * compiled templates and re-use them where possible. */ (function () { @@ -24,6 +25,17 @@ const EleventyBaseError = require("../EleventyBaseError"); ].join(" :: "); }; + let evictByPath = (path) => { + let keys = templateCache.keys(); + // Likely to be slow; do we care? + for (let k of keys) { + if (k.indexOf(path) >= 0) { + templateCache.delete(k); + } + } + }; + eventBus.on("resourceModified", evictByPath); + let _compile = NunjucksLib.Template.prototype._compile; NunjucksLib.Template.prototype._compile = function _wrap_compile(...args) { if (!this.compiled && !this.tmplProps && templateCache.has(getKey(this))) { @@ -109,16 +121,17 @@ class Nunjucks extends TemplateEngine { } setLibrary(env) { - this.njkEnv = - env || - new NunjucksLib.Environment( - new NunjucksLib.FileSystemLoader( - [super.getIncludesDir(), TemplatePath.getWorkingDir()], - { - noCache: true, - } - ) - ); + let fsLoader = new NunjucksLib.FileSystemLoader([ + super.getIncludesDir(), + TemplatePath.getWorkingDir(), + ]); + this.njkEnv = env || new NunjucksLib.Environment(fsLoader); + // Correct, but overbroad. Better would be to evict more granularly, but + // resolution from paths isn't straightforward. + eventBus.on("resourceModified", (path) => { + this.njkEnv.invalidateCache(); + }); + this.setEngineLib(this.njkEnv); this.addFilters(this.config.nunjucksFilters); diff --git a/src/EventBus.js b/src/EventBus.js new file mode 100644 index 000000000..682b87c3f --- /dev/null +++ b/src/EventBus.js @@ -0,0 +1,17 @@ +const EventEmitter = require("./Util/AsyncEventEmitter"); +const debug = require("debug")("Eleventy:EventBus"); + +/** + * @module 11ty/eleventy/EventBus + */ + +debug("Setting up global EventBus."); +/** + * Provides a global event bus that modules deep down in the stack can + * subscribe to from a global singleton for decoupled pub/sub. + * @type * {module:11ty/eleventy/Util/AsyncEventEmitter~AsyncEventEmitter} + */ +let bus = new EventEmitter(); +bus.setMaxListeners(100); + +module.exports = bus; diff --git a/src/TemplateContent.js b/src/TemplateContent.js index 7fefe5810..7853e77aa 100644 --- a/src/TemplateContent.js +++ b/src/TemplateContent.js @@ -14,6 +14,7 @@ const config = require("./Config"); const debug = require("debug")("Eleventy:TemplateContent"); const debugDev = require("debug")("Dev:Eleventy:TemplateContent"); const bench = require("./BenchmarkManager").get("Aggregate"); +const eventBus = require("./EventBus"); class TemplateContentFrontMatterError extends EleventyBaseError {} class TemplateContentCompileError extends EleventyBaseError {} @@ -277,5 +278,8 @@ class TemplateContent { TemplateContent._inputCache = new Map(); TemplateContent._compileEngineCache = new Map(); +eventBus.on("resourceModified", (path) => { + TemplateContent.deleteCached(path); +}); module.exports = TemplateContent; diff --git a/test/TemplateTest.js b/test/TemplateTest.js index cd016bcd3..95cfd99f5 100644 --- a/test/TemplateTest.js +++ b/test/TemplateTest.js @@ -2049,11 +2049,9 @@ test("Engine Singletons", async (t) => { }); test("Make sure layout cache takes new changes during watch (nunjucks)", async (t) => { - await fsp.writeFile( - "./test/stubs-layout-cache/_includes/include-script-1.js", - `alert("hi");`, - { encoding: "utf8" } - ); + let filePath = "./test/stubs-layout-cache/_includes/include-script-1.js"; + + await fsp.writeFile(filePath, `alert("hi");`, { encoding: "utf8" }); let tmpl = getNewTemplate( "./test/stubs-layout-cache/test.njk", @@ -2065,13 +2063,23 @@ test("Make sure layout cache takes new changes during watch (nunjucks)", async ( t.is((await tmpl.render(data)).trim(), ''); - await fsp.writeFile( - "./test/stubs-layout-cache/_includes/include-script-1.js", - `alert("bye");`, - { encoding: "utf8" } - ); + let eventBus = require("../src/EventBus"); + let chokidar = require("chokidar"); + let watcher = chokidar.watch(filePath, { interval: 10, persistent: true }); + watcher.on("change", (path, stats) => { + eventBus.emit("resourceModified", path); + }); + + await fsp.writeFile(filePath, `alert("bye");`, { encoding: "utf8" }); + + // Give Chokidar time to see the change; + await new Promise((res, rej) => { + setTimeout(res, 200); + }); t.is((await tmpl.render(data)).trim(), ''); + + await watcher.close(); }); test("Make sure layout cache takes new changes during watch (liquid)", async (t) => {