Repository: 11ty/eleventy
Pull request #3213 (2024-02-23 12:51:34)

Allow custom filters in Liquid partials in strictVariables mode
This PR fixes https://github.com/11ty/eleventy/issues/3206.

The issue here is that in partials rendered using `{% render %}`, the `page` and `eleventy` variables do not exist, since the `{% render %}` tag is introducing a "new" scope that does not inherit the variables from the page it was called from. In `strictVariables` mode (this is an option you can pass to `.setLiquidOptions()`), this causes an error when you use any custom filter in such a partial, because Eleventy is trying to retrieve the value of the `page` and `eleventy` globals for use in the filter's definition.

This PR essentially turns the properties `page` and `eleventy` into getters; meaning that they are not retrieved until the user specifically uses them. This prevents filters that do not use these properties from causing errors in `{% render %}`-style partials.

My other PR, https://github.com/11ty/eleventy/pull/3212, is related, and if the `page` and `eleventy` variables are intended to be accessible from `{% render %}` partials, then this PR may be dismissed.




Patch:
diff --git a/src/Engines/Liquid.js b/src/Engines/Liquid.js
index 2902ce27..a2019aa4 100644
--- a/src/Engines/Liquid.js
+++ b/src/Engines/Liquid.js
@@ -58,8 +58,14 @@ class Liquid extends TemplateEngine {
 	static wrapFilter(fn) {
 		return function (...args) {
 			if (this.context && "get" in this.context) {
-				this.page = this.context.get(["page"]);
-				this.eleventy = this.context.get(["eleventy"]);
+				const exposedProperties = ["page", "eleventy"];
+				for (const property of exposedProperties) {
+					Object.defineProperty(this, property, {
+						configurable: true,
+						enumerable: true,
+						get: () => this.context.get([property]),
+					});
+				}
 			}
 
 			return fn.call(this, ...args);
@@ -130,14 +136,14 @@ class Liquid extends TemplateEngine {
 			let arg = lexer.next();
 			while (arg) {
 				/*{
-          type: 'doubleQuoteString',
-          value: '"test 2"',
-          text: '"test 2"',
-          toString: [Function: tokenToString],
-          offset: 0,
-          lineBreaks: 0,
-          line: 1,
-          col: 1 }*/
+					type: 'doubleQuoteString',
+					value: '"test 2"',
+					text: '"test 2"',
+					toString: [Function: tokenToString],
+					offset: 0,
+					lineBreaks: 0,
+					line: 1,
+					col: 1 }*/
 				if (arg.type.indexOf("ignore:") === -1) {
 					// Push the promise into an array instead of awaiting it here.
 					// This forces the promises to run in order with the correct scope value for each arg.


Test patch:
diff --git a/test/TemplateRenderLiquidTest.js b/test/TemplateRenderLiquidTest.js
index 08bcdd04..936e2f05 100644
--- a/test/TemplateRenderLiquidTest.js
+++ b/test/TemplateRenderLiquidTest.js
@@ -211,6 +211,19 @@ test("Liquid Async Filter", async (t) => {
   t.is((await fn()).trim(), "HItest");
 });
 
+test("Issue 3206: Strict variables and custom filters in includes", async (t) => {
+  let tr = await getNewTemplateRender("liquid", "test/stubs", {
+    liquidOptions: {
+      strictVariables: true
+    }
+  });
+  tr.engine.addFilter("makeItFoo", function () {
+    return "foo";
+  });
+  let fn = await tr.getCompiledTemplate(`<p>{% render "custom-filter", name: "Zach" %}</p>`);
+  t.is((await fn()), "<p>foo</p>");
+});
+
 test("Liquid Custom Tag prefixWithZach", async (t) => {
   let tr = await getNewTemplateRender("liquid", "./test/stubs/");
   tr.engine.addTag("prefixWithZach", function (liquidEngine) {
diff --git a/test/stubs/_includes/custom-filter.liquid b/test/stubs/_includes/custom-filter.liquid
new file mode 100644
index 00000000..4fc3f3d2
--- /dev/null
+++ b/test/stubs/_includes/custom-filter.liquid
@@ -0,0 +1,1 @@
+{{ name | makeItFoo }}
\ No newline at end of file