Repository: 11ty/eleventy
Pull request #3452 (2024-09-19 16:30:25)

`addPreprocessor` support for `11ty.js` and Custom templates (via `useJavaScriptImport` property)
* Adds `useJavaScriptImport` method to TemplateEngine class and enables by default for `JavaScript` (11ty.js) templates.
* `useJavaScriptImport` takes precedence over `needsToReadFileContents` (Custom template engine `read` property)
* Adds an `useJavaScriptImport` property to Custom template options (boolean)

https://www.11ty.dev/docs/languages/custom/#full-options-list


Patch:
diff --git a/src/Engines/Custom.js b/src/Engines/Custom.js
index 55a18e1b..e5d9be22 100644
--- a/src/Engines/Custom.js
+++ b/src/Engines/Custom.js
@@ -45,6 +45,49 @@ class CustomEngine extends TemplateEngine {
 		this._defaultEngine = defaultEngine;
 	}
 
+	async getInstanceFromInputPath(inputPath) {
+		if (
+			"getInstanceFromInputPath" in this.entry &&
+			typeof this.entry.getInstanceFromInputPath === "function"
+		) {
+			// returns Promise
+			return this.entry.getInstanceFromInputPath(inputPath);
+		}
+
+		// aliased upstream type
+		if (
+			this._defaultEngine &&
+			"getInstanceFromInputPath" in this._defaultEngine &&
+			typeof this._defaultEngine.getInstanceFromInputPath === "function"
+		) {
+			// returns Promise
+			return this._defaultEngine.getInstanceFromInputPath(inputPath);
+		}
+
+		return false;
+	}
+
+	/**
+	 * Whether to use the module loader directly
+	 *
+	 * @override
+	 */
+	useJavaScriptImport() {
+		if ("useJavaScriptImport" in this.entry) {
+			return this.entry.useJavaScriptImport;
+		}
+
+		if (
+			this._defaultEngine &&
+			"useJavaScriptImport" in this._defaultEngine &&
+			typeof this._defaultEngine.useJavaScriptImport === "function"
+		) {
+			return this._defaultEngine.useJavaScriptImport();
+		}
+
+		return false;
+	}
+
 	/**
 	 * @override
 	 */
@@ -110,15 +153,6 @@ class CustomEngine extends TemplateEngine {
 			return data;
 		}
 
-		// if getData is not false or a function then `getInstanceFromInputPath` must exist
-		if (!("getInstanceFromInputPath" in this.entry)) {
-			return Promise.reject(
-				new Error(
-					`getInstanceFromInputPath callback missing from ${this.name} template engine plugin.`,
-				),
-			);
-		}
-
 		let keys = new Set();
 		if (this.entry.getData === true) {
 			keys.add("data");
@@ -131,7 +165,18 @@ class CustomEngine extends TemplateEngine {
 		let dataBench = this.benchmarks.aggregate.get(`Engine (${this.name}) Get Data From File`);
 		dataBench.before();
 
-		let inst = await this.entry.getInstanceFromInputPath(inputPath);
+		let inst = await this.getInstanceFromInputPath(inputPath);
+
+		if (inst === false) {
+			dataBench.after();
+
+			return Promise.reject(
+				new Error(
+					`\`getInstanceFromInputPath\` callback missing from '${this.name}' template engine plugin. It is required when \`getData\` is in use. You can set \`getData: false\` to opt-out of this.`,
+				),
+			);
+		}
+
 		// override keys set at the plugin level in the individual template
 		if (inst.eleventyDataKey) {
 			keys = new Set(inst.eleventyDataKey);
diff --git a/src/Engines/JavaScript.js b/src/Engines/JavaScript.js
index b20db4e0..5ef27dfc 100644
--- a/src/Engines/JavaScript.js
+++ b/src/Engines/JavaScript.js
@@ -123,6 +123,15 @@ class JavaScript extends TemplateEngine {
 		return false;
 	}
 
+	/**
+	 * Use the module loader directly
+	 *
+	 * @override
+	 */
+	useJavaScriptImport() {
+		return true;
+	}
+
 	async getExtraDataFromFile(inputPath) {
 		let inst = await this.getInstanceFromInputPath(inputPath);
 		return getJavaScriptData(inst, inputPath);
diff --git a/src/Engines/TemplateEngine.js b/src/Engines/TemplateEngine.js
index 05050b68..47db94ff 100644
--- a/src/Engines/TemplateEngine.js
+++ b/src/Engines/TemplateEngine.js
@@ -111,6 +111,10 @@ class TemplateEngine {
 		return fn(data);
 	}
 
+	useJavaScriptImport() {
+		return false;
+	}
+
 	// JavaScript files defer to the module loader rather than read the files to strings
 	needsToReadFileContents() {
 		return true;
diff --git a/src/TemplateContent.js b/src/TemplateContent.js
index 143d9582..db3b641b 100644
--- a/src/TemplateContent.js
+++ b/src/TemplateContent.js
@@ -184,6 +184,13 @@ class TemplateContent {
 		let content = await this.inputContent;
 
 		if (content || content === "") {
+			if (this.engine.useJavaScriptImport()) {
+				return {
+					data: {},
+					content,
+				};
+			}
+
 			let options = this.config.frontMatterParsingOptions || {};
 			let fm;
 			try {
@@ -266,9 +273,6 @@ class TemplateContent {
 
 	async getInputContent() {
 		let tr = await this.getTemplateRender();
-		if (!tr.engine.needsToReadFileContents()) {
-			return "";
-		}
 
 		let virtualTemplateDefinition = this.getVirtualTemplateDefinition();
 		if (virtualTemplateDefinition) {
@@ -276,6 +280,17 @@ class TemplateContent {
 			return content;
 		}
 
+		if (
+			tr.engine.useJavaScriptImport() &&
+			typeof tr.engine.getInstanceFromInputPath === "function"
+		) {
+			return tr.engine.getInstanceFromInputPath(this.inputPath);
+		}
+
+		if (!tr.engine.needsToReadFileContents()) {
+			return "";
+		}
+
 		let templateBenchmark = this.bench.get("Template Read");
 		templateBenchmark.before();
 


Test patch:
diff --git a/test/EleventyTest-Preprocessors.js b/test/EleventyTest-Preprocessors.js
index 111117ec..2ff5ed08 100644
--- a/test/EleventyTest-Preprocessors.js
+++ b/test/EleventyTest-Preprocessors.js
@@ -101,3 +101,183 @@ test("#188: Content preprocessing (wildcard)", async (t) => {
   t.is(results.length, 1);
   t.is(results[0].content, `Hello Before`);
 });
+
+
+test("addPreprocessor with 11ty.js, Issue #3433", async (t) => {
+  t.plan(5);
+
+  let elev = new Eleventy("./test/stubs-virtual/", undefined, {
+    config: eleventyConfig => {
+      eleventyConfig.addPreprocessor("testing", "11ty.js", (data, content) => {
+        t.is( typeof content, "function" );
+        t.is(content(), "Hello!");
+
+        return {
+          render: function() {
+            return "naw";
+          }
+        };
+      });
+
+      eleventyConfig.addTemplate("template.11ty.js", function() {
+        return "Hello!"
+      });
+    }
+  });
+
+  let results = await elev.toJSON();
+  t.is(results.length, 1);
+  t.is(results[0].url, `/template/`);
+  t.is(results[0].content.trim(), `naw`);
+});
+
+
+test("addPreprocessor and addExtension, Issue #3433", async (t) => {
+  t.plan(5);
+
+  let elev = new Eleventy("./test/stubs-virtual/", undefined, {
+    config: eleventyConfig => {
+      eleventyConfig.addTemplateFormats("11ty.test");
+      eleventyConfig.addExtension("11ty.test", {
+        key: "11ty.js",
+      });
+
+      eleventyConfig.addPreprocessor("testing", "11ty.test", (data, content) => {
+        t.is( typeof content, "function" );
+        t.is(content(), "Hello!");
+
+        return {
+          render: function() {
+            return "naw";
+          }
+        };
+      });
+
+      eleventyConfig.addTemplate("template.11ty.test", function() {
+        return "Hello!"
+      });
+    }
+  });
+
+  let results = await elev.toJSON();
+  t.is(results.length, 1);
+  t.is(results[0].url, `/template/`);
+  t.is(results[0].content.trim(), `naw`);
+});
+
+test("addPreprocessor and addExtension with custom `compile` (defaultRenderer), Issue #3433", async (t) => {
+  t.plan(5);
+
+  let elev = new Eleventy("./test/stubs-virtual/", undefined, {
+    config: eleventyConfig => {
+      eleventyConfig.addTemplateFormats("11ty.test");
+      eleventyConfig.addExtension("11ty.test", {
+        key: "11ty.js",
+        compile: function() {
+          return this.defaultRenderer;
+        }
+      });
+
+      eleventyConfig.addPreprocessor("testing", "11ty.test", (data, content) => {
+        t.is( typeof content, "function" );
+        t.is(content(), "Hello!");
+
+        return {
+          render: function() {
+            return "naw";
+          }
+        };
+      });
+
+      eleventyConfig.addTemplate("template.11ty.test", function() {
+        return "Hello!"
+      });
+    }
+  });
+
+  let results = await elev.toJSON();
+  t.is(results.length, 1);
+  t.is(results[0].url, `/template/`);
+  t.is(results[0].content.trim(), `naw`);
+});
+
+test("addPreprocessor and addExtension with custom `compile` (re-use render function directly), Issue #3433", async (t) => {
+  t.plan(5);
+
+  let elev = new Eleventy("./test/stubs-virtual/", undefined, {
+    config: eleventyConfig => {
+      eleventyConfig.addTemplateFormats("11ty.test");
+      eleventyConfig.addExtension("11ty.test", {
+        key: "11ty.js",
+        compile: function(content) {
+          return function() {
+            return content.render();
+          }
+        }
+      });
+
+      eleventyConfig.addPreprocessor("testing", "11ty.test", (data, content) => {
+        t.is( typeof content, "function" );
+        t.is(content(), "Hello!");
+
+        return {
+          render: function() {
+            return "naw";
+          }
+        };
+      });
+
+      eleventyConfig.addTemplate("template.11ty.test", function() {
+        return "Hello!"
+      });
+    }
+  });
+
+  let results = await elev.toJSON();
+  t.is(results.length, 1);
+  t.is(results[0].url, `/template/`);
+  t.is(results[0].content.trim(), `naw`);
+});
+
+test("addPreprocessor and addExtension with custom `compile` (new render function), Issue #3433", async (t) => {
+  t.plan(7);
+
+  let elev = new Eleventy("./test/stubs-virtual/", undefined, {
+    config: eleventyConfig => {
+      eleventyConfig.addTemplateFormats("11ty.test");
+      eleventyConfig.addExtension("11ty.test", {
+        key: "11ty.js",
+        compile: function(content) {
+          // check preprocessor override
+          t.is( typeof content.render, "function" );
+          t.is(content.render(), "Preprocessor override");
+
+          return function() {
+            return "Compiled content";
+          }
+        }
+      });
+
+      eleventyConfig.addPreprocessor("testing", "11ty.test", (data, content) => {
+        // check template content directly
+        t.is( typeof content, "function" );
+        t.is(content(), "Original template content");
+
+        return {
+          render: function() {
+            return "Preprocessor override";
+          }
+        };
+      });
+
+      eleventyConfig.addTemplate("template.11ty.test", function() {
+        return "Original template content"
+      });
+    }
+  });
+
+  let results = await elev.toJSON();
+  t.is(results.length, 1);
+  t.is(results[0].url, `/template/`);
+  t.is(results[0].content.trim(), `Compiled content`);
+});
diff --git a/test/TemplateTest-CustomExtensions.js b/test/TemplateTest-CustomExtensions.js
index fefabcfb..03a2ceba 100644
--- a/test/TemplateTest-CustomExtensions.js
+++ b/test/TemplateTest-CustomExtensions.js
@@ -74,7 +74,7 @@ test("Using getData: true without getInstanceFromInputPath should error", async
   await t.throwsAsync(async () => {
     await tmpl.getData();
   }, {
-    message: "getInstanceFromInputPath callback missing from txt template engine plugin."
+    message: "`getInstanceFromInputPath` callback missing from \'txt\' template engine plugin. It is required when `getData` is in use. You can set `getData: false` to opt-out of this."
   });
 });
 
@@ -111,7 +111,7 @@ test("Using getData: [] without getInstanceFromInputPath should error", async (t
   await t.throwsAsync(async () => {
     await tmpl.getData();
   }, {
-    message: "getInstanceFromInputPath callback missing from txt template engine plugin."
+    message: "`getInstanceFromInputPath` callback missing from \'txt\' template engine plugin. It is required when `getData` is in use. You can set `getData: false` to opt-out of this."
   });
 });