Repository: 11ty/eleventy (js)

Issue:
Use .js for Data Files (Global, Directory, Template)
Hello,

Is it possible to pass different data to eleventy based on the environment?

My use case is building a website that will use different API in its front-end. The final deployed build should hit production APIs, with production credentials. But while developing locally, I'd like to hit staging APIs, with staging credentials.

I thought about using data in `_data` folder but it seems that it only accepts JSON files, and not execuatble `.js` files, right? I also tried to see if I could inject custom data through the config function, but it not seems that it has access to data.

How would you suggest doing this? Am I missing some config/feature somewhere?

Pull request:
Js data files, fixes #155
#155

Patch:
diff --git a/config.js b/config.js
index 448547dc..c6112278 100644
--- a/config.js
+++ b/config.js
@@ -25,6 +25,7 @@ module.exports = function(config) {
     dataTemplateEngine: "liquid",
     passthroughFileCopy: true,
     htmlOutputSuffix: "-o",
+    jsDataFileSuffix: ".11tydata",
     keys: {
       package: "pkg",
       layout: "layout",
diff --git a/src/EleventyExtensionMap.js b/src/EleventyExtensionMap.js
index 4accebce..05f2eced 100644
--- a/src/EleventyExtensionMap.js
+++ b/src/EleventyExtensionMap.js
@@ -61,7 +61,19 @@ class EleventyExtensionMap {
     return this.keyMapToExtension[key];
   }
 
-  get keyMapToExtension() {
+  static removeTemplateExtension(path) {
+    for (var key in EleventyExtensionMap.keyMap) {
+      if (path.endsWith(EleventyExtensionMap.keyMap[key])) {
+        return path.substr(
+          0,
+          path.length - 1 - EleventyExtensionMap.keyMap[key].length
+        );
+      }
+    }
+    return path;
+  }
+
+  static get keyMap() {
     return {
       ejs: "ejs",
       md: "md",
@@ -76,6 +88,10 @@ class EleventyExtensionMap {
       "11ty.js": "11ty.js"
     };
   }
+
+  get keyMapToExtension() {
+    return EleventyExtensionMap.keyMap;
+  }
 }
 
 module.exports = EleventyExtensionMap;
diff --git a/src/TemplateData.js b/src/TemplateData.js
index a8d2107e..385c1760 100644
--- a/src/TemplateData.js
+++ b/src/TemplateData.js
@@ -7,7 +7,9 @@ const lodashUniq = require("lodash.uniq");
 const TemplateRender = require("./TemplateRender");
 const TemplatePath = require("./TemplatePath");
 const TemplateGlob = require("./TemplateGlob");
+const EleventyExtensionMap = require("./EleventyExtensionMap");
 const config = require("./Config");
+const debugWarn = require("debug")("Eleventy:Warnings");
 const debug = require("debug")("Eleventy:TemplateData");
 const debugDev = require("debug")("Dev:Eleventy:TemplateData");
 
@@ -15,15 +17,15 @@ function TemplateData(inputDir) {
   this.config = config.getConfig();
   this.dataTemplateEngine = this.config.dataTemplateEngine;
 
+  this.inputDirNeedsCheck = false;
   this.setInputDir(inputDir);
 
-  this.fetchedRawImports = false;
   this.rawImports = {};
-
   this.globalData = null;
 }
 
 TemplateData.prototype.setInputDir = function(inputDir) {
+  this.inputDirNeedsCheck = true;
   this.inputDir = inputDir;
   this.dataDir =
     inputDir + "/" + (this.config.dir.data !== "." ? this.config.dir.data : "");
@@ -33,12 +35,15 @@ TemplateData.prototype.setDataTemplateEngine = function(engineName) {
   this.dataTemplateEngine = engineName;
 };
 
-TemplateData.prototype.getRawImports = async function() {
-  if (!this.fetchedRawImports) {
-    this.fetchedRawImports = true;
+TemplateData.prototype.getRawImports = function() {
+  let pkgPath = TemplatePath.localPath("package.json");
 
-    this.rawImports[this.config.keys.package] = await this.getJsonRaw(
-      TemplatePath.localPath("package.json")
+  try {
+    this.rawImports[this.config.keys.package] = require(pkgPath);
+  } catch (e) {
+    debug(
+      "Could not find and/or require package.json for data preprocessing at %o",
+      pkgPath
     );
   }
 
@@ -59,25 +64,39 @@ TemplateData.prototype.cacheData = async function() {
   return this.getData();
 };
 
-TemplateData.prototype.getGlobalDataGlob = async function() {
-  let dir = ".";
+TemplateData.prototype._getGlobalDataGlobByExtension = async function(
+  dir,
+  extension
+) {
+  return TemplateGlob.normalizePath(
+    dir,
+    "/",
+    this.config.dir.data !== "." ? this.config.dir.data : "",
+    `/**/*.${extension}`
+  );
+};
 
-  if (this.inputDir) {
+TemplateData.prototype._checkInputDir = async function() {
+  if (this.inputDirNeedsCheck) {
     let globalPathStat = await fs.stat(this.inputDir);
 
     if (!globalPathStat.isDirectory()) {
       throw new Error("Could not find data path directory: " + this.inputDir);
     }
 
+    this.inputDirNeedsCheck = false;
+  }
+};
+
+TemplateData.prototype.getGlobalDataGlob = async function() {
+  let dir = ".";
+
+  if (this.inputDir) {
+    await this._checkInputDir();
     dir = this.inputDir;
   }
 
-  return TemplateGlob.normalizePath(
-    dir,
-    "/",
-    this.config.dir.data !== "." ? this.config.dir.data : "",
-    "/**/*.json"
-  );
+  return [await this._getGlobalDataGlobByExtension(dir, "(json|js)")];
 };
 
 TemplateData.prototype.getGlobalDataFiles = async function() {
@@ -94,23 +113,38 @@ TemplateData.prototype.getObjectPathForDataFile = function(path) {
 };
 
 TemplateData.prototype.getAllGlobalData = async function() {
-  let rawImports = await this.getRawImports();
+  let rawImports = this.getRawImports();
   let globalData = {};
   let files = TemplatePath.addLeadingDotSlashArray(
     await this.getGlobalDataFiles()
   );
+  let dataFileConflicts = {};
 
   for (let j = 0, k = files.length; j < k; j++) {
     let folders = await this.getObjectPathForDataFile(files[j]);
+
+    // TODO maybe merge these two? (if both valid objects)
+    if (dataFileConflicts[folders]) {
+      debugWarn(
+        `Warning: the key for a global data file (${
+          files[j]
+        }) will overwrite data from an already existing global data file (${
+          dataFileConflicts[folders]
+        })`
+      );
+    }
+    dataFileConflicts[folders] = files[j];
+
     debug(`Found global data file ${files[j]} and adding as: ${folders}`);
-    let data = await this.getJson(files[j], rawImports);
+    let data = await this.getDataValue(files[j], rawImports);
     lodashset(globalData, folders, data);
   }
+
   return globalData;
 };
 
 TemplateData.prototype.getData = async function() {
-  let rawImports = await this.getRawImports();
+  let rawImports = this.getRawImports();
 
   if (!this.globalData) {
     let globalJson = await this.getAllGlobalData();
@@ -122,13 +156,13 @@ TemplateData.prototype.getData = async function() {
 };
 
 TemplateData.prototype.combineLocalData = async function(localDataPaths) {
-  let rawImports = await this.getRawImports();
+  let rawImports = this.getRawImports();
   let localData = {};
   if (!Array.isArray(localDataPaths)) {
     localDataPaths = [localDataPaths];
   }
   for (let path of localDataPaths) {
-    let dataForPath = await this.getJson(path, rawImports, true);
+    let dataForPath = await this.getDataValue(path, rawImports, true);
     lodashMerge(localData, dataForPath);
     // debug("`combineLocalData` (iterating) for %o: %O", path, localData);
   }
@@ -136,7 +170,7 @@ TemplateData.prototype.combineLocalData = async function(localDataPaths) {
 };
 
 TemplateData.prototype.getLocalData = async function(templatePath) {
-  let localDataPaths = this.getLocalDataPaths(templatePath);
+  let localDataPaths = await this.getLocalDataPaths(templatePath);
   let importedData = await this.combineLocalData(localDataPaths);
   let globalData = await this.getData();
 
@@ -145,7 +179,7 @@ TemplateData.prototype.getLocalData = async function(templatePath) {
   return localData;
 };
 
-TemplateData.prototype._getLocalJson = async function(path) {
+TemplateData.prototype._getLocalJsonString = async function(path) {
   let rawInput;
   try {
     rawInput = await fs.readFile(path, "utf-8");
@@ -155,44 +189,46 @@ TemplateData.prototype._getLocalJson = async function(path) {
   return rawInput;
 };
 
-TemplateData.prototype.getJsonRaw = async function(path) {
-  let rawContent = await this._getLocalJson(path);
-  return rawContent ? JSON.parse(rawContent) : {};
-};
-
-TemplateData.prototype.getJson = async function(
+TemplateData.prototype.getDataValue = async function(
   path,
   rawImports,
   ignoreProcessing
 ) {
-  let rawInput = await this._getLocalJson(path);
-  let engineName;
-
-  if (!ignoreProcessing) {
-    engineName = this.dataTemplateEngine;
-  }
-
-  if (rawInput) {
-    let str;
-
-    if (engineName) {
-      let fn = await new TemplateRender(engineName).getCompiledTemplate(
-        rawInput
-      );
-
-      // pass in rawImports, don’t pass in global data, that’s what we’re parsing
-      str = await fn(rawImports);
+  if (ignoreProcessing || TemplatePath.getExtension(path) === "js") {
+    let localPath = TemplatePath.localPath(path);
+    if (await fs.pathExists(localPath)) {
+      let returnValue = require(localPath);
+      if (typeof returnValue === "function") {
+        return await returnValue();
+      }
+      return returnValue;
     } else {
-      str = rawInput;
+      return {};
+    }
+  } else {
+    let engineName;
+    if (!ignoreProcessing) {
+      engineName = this.dataTemplateEngine;
+    }
+    let rawInput = await this._getLocalJsonString(path);
+    if (rawInput) {
+      if (engineName) {
+        let fn = await new TemplateRender(engineName).getCompiledTemplate(
+          rawInput
+        );
+
+        // pass in rawImports, don’t pass in global data, that’s what we’re parsing
+        return JSON.parse(await fn(rawImports));
+      } else {
+        return JSON.parse(rawInput);
+      }
     }
-
-    return JSON.parse(str);
   }
 
   return {};
 };
 
-TemplateData.prototype.getLocalDataPaths = function(templatePath) {
+TemplateData.prototype.getLocalDataPaths = async function(templatePath) {
   let paths = [];
   let parsed = parsePath(templatePath);
   let inputDir = TemplatePath.addLeadingDotSlash(
@@ -202,26 +238,37 @@ TemplateData.prototype.getLocalDataPaths = function(templatePath) {
   debugDev("parsed.dir: %o", parsed.dir);
 
   if (parsed.dir) {
-    let filePath = parsed.dir + "/" + parsed.name + ".json";
-    paths.push(filePath);
+    let fileNameNoExt = EleventyExtensionMap.removeTemplateExtension(
+      parsed.base
+    );
+    let filePathNoExt = parsed.dir + "/" + fileNameNoExt;
+    let dataSuffix = this.config.jsDataFileSuffix;
+    debug("Using %o to find data files.", dataSuffix);
+    paths.push(filePathNoExt + dataSuffix + ".js");
+    paths.push(filePathNoExt + dataSuffix + ".json");
+    paths.push(filePathNoExt + ".json");
 
     let allDirs = TemplatePath.getAllDirs(parsed.dir);
     debugDev("allDirs: %o", allDirs);
     for (let dir of allDirs) {
       let lastDir = TemplatePath.getLastDir(dir);
-      let dirPath = dir + "/" + lastDir + ".json";
+      let dirPathNoExt = dir + "/" + lastDir;
 
       if (!inputDir) {
-        paths.push(dirPath);
+        paths.push(dirPathNoExt + dataSuffix + ".js");
+        paths.push(dirPathNoExt + dataSuffix + ".json");
+        paths.push(dirPathNoExt + ".json");
       } else {
         debugDev("dirStr: %o; inputDir: %o", dir, inputDir);
         if (dir.indexOf(inputDir) === 0 && dir !== inputDir) {
-          paths.push(dirPath);
+          paths.push(dirPathNoExt + dataSuffix + ".js");
+          paths.push(dirPathNoExt + dataSuffix + ".json");
+          paths.push(dirPathNoExt + ".json");
         }
       }
     }
   }
-  debugDev("getLocalDataPaths(%o): %o", templatePath, paths);
+
   debug("getLocalDataPaths(%o): %o", templatePath, paths);
   return lodashUniq(paths).reverse();
 };
diff --git a/src/TemplatePath.js b/src/TemplatePath.js
index e7462aaa..7d2dbe73 100644
--- a/src/TemplatePath.js
+++ b/src/TemplatePath.js
@@ -135,9 +135,18 @@ TemplatePath.convertToGlob = function(path) {
   return path;
 };
 
+TemplatePath.getExtension = function(path) {
+  let split = path.split(".");
+  if (split.length > 1) {
+    return split.pop();
+  }
+  return "";
+};
+
 TemplatePath.removeExtension = function(path, extension) {
   let split = path.split(".");
 
+  // only remove extension if extension is passed in and an extension is found
   if (extension && split.length > 1) {
     let ext = split.pop();
     if (extension.charAt(0) === ".") {
diff --git a/src/UserConfig.js b/src/UserConfig.js
index ebea7069..1523670c 100644
--- a/src/UserConfig.js
+++ b/src/UserConfig.js
@@ -3,7 +3,6 @@ const chalk = require("chalk");
 const semver = require("semver");
 const { DateTime } = require("luxon");
 const debug = require("debug")("Eleventy:UserConfig");
-const debugWarn = require("debug")("Eleventy:Warnings");
 const pkg = require("../package.json");
 const Benchmark = require("./Benchmark");
 
@@ -477,15 +476,15 @@ class UserConfig {
         let percent = (totalForBenchmark * 100) / totalTimeSpent;
         if (percent > thresholdPercent) {
           let str = chalk.yellow(
-            `A slow "${name}" ${type} was found in your config file (${bench.getTotal()}ms, ${percent.toFixed(
+            `Warning: a slow "${name}" ${type} was found in your config file (${bench.getTotal()}ms, ${percent.toFixed(
               1
             )}%)`
           );
           if (isVerbose) {
-            console.log("Warning:", str);
+            console.log(str);
+          } else {
+            debug(str);
           }
-
-          debugWarn(str);
         }
       }
     }


Test patch:
diff --git a/test/EleventyExtensionMapTest.js b/test/EleventyExtensionMapTest.js
index 7bc30872..6a217b5d 100644
--- a/test/EleventyExtensionMapTest.js
+++ b/test/EleventyExtensionMapTest.js
@@ -75,3 +75,21 @@ test("fileList with dir in path and dir", t => {
     "_includes/layouts/filename.pug"
   ]);
 });
+
+test("removeTemplateExtension", t => {
+  t.is(
+    EleventyExtensionMap.removeTemplateExtension("component.njk"),
+    "component"
+  );
+  t.is(
+    EleventyExtensionMap.removeTemplateExtension("component.11ty.js"),
+    "component"
+  );
+
+  t.is(EleventyExtensionMap.removeTemplateExtension(""), "");
+  t.is(EleventyExtensionMap.removeTemplateExtension("component"), "component");
+  t.is(
+    EleventyExtensionMap.removeTemplateExtension("component.js"),
+    "component.js"
+  );
+});
diff --git a/test/TemplateDataTest.js b/test/TemplateDataTest.js
index 4ac2ba63..ee7bc68b 100644
--- a/test/TemplateDataTest.js
+++ b/test/TemplateDataTest.js
@@ -43,7 +43,7 @@ test("Data dir does not exist", async t => {
   await t.throws(dataObj.getData());
 });
 
-test("addLocalData()", async t => {
+test("Add local data", async t => {
   let dataObj = new TemplateData("./test/stubs/");
   let data = await dataObj.getData();
 
@@ -51,11 +51,26 @@ test("addLocalData()", async t => {
   t.is(data.globalData.datakey2, "@11ty/eleventy");
 
   let withLocalData = await dataObj.getLocalData(
-    "./test/stubs/component/component.json"
+    "./test/stubs/component/component.njk"
   );
   t.is(withLocalData.globalData.datakey1, "datavalue1");
   t.is(withLocalData.globalData.datakey2, "@11ty/eleventy");
   t.is(withLocalData.localdatakey1, "localdatavalue1");
+
+  // from the js file
+  t.is(withLocalData.localdatakeyfromjs, "howdydoody");
+  t.is(withLocalData.localdatakeyfromjs2, "howdy2");
+});
+
+test("Get local data async JS", async t => {
+  let dataObj = new TemplateData("./test/stubs/");
+
+  let withLocalData = await dataObj.getLocalData(
+    "./test/stubs/component-async/component.njk"
+  );
+
+  // from the js file
+  t.is(withLocalData.localdatakeyfromjs, "howdydoody");
 });
 
 test("addLocalData() doesn’t exist but doesn’t fail (template file does exist)", async t => {
@@ -88,13 +103,15 @@ test("addLocalData() doesn’t exist but doesn’t fail (template file does not
 test("Global Dir Directory", async t => {
   let dataObj = new TemplateData();
 
-  t.is(await dataObj.getGlobalDataGlob(), "./_data/**/*.json");
+  t.deepEqual(await dataObj.getGlobalDataGlob(), ["./_data/**/*.(json|js)"]);
 });
 
 test("Global Dir Directory with Constructor Path Arg", async t => {
   let dataObj = new TemplateData("./test/stubs/");
 
-  t.is(await dataObj.getGlobalDataGlob(), "./test/stubs/_data/**/*.json");
+  t.deepEqual(await dataObj.getGlobalDataGlob(), [
+    "./test/stubs/_data/**/*.(json|js)"
+  ]);
 });
 
 test("getAllGlobalData() with other data files", async t => {
@@ -108,22 +125,54 @@ test("getAllGlobalData() with other data files", async t => {
       return path.indexOf("./test/stubs/_data/globalData.json") === 0;
     }).length > 0
   );
-  t.not(typeof data.testData, "undefined");
 
+  t.truthy(data.globalData);
+  t.is(data.globalData.datakey1, "datavalue1");
+
+  t.truthy(data.testData);
   t.deepEqual(data.testData, {
     testdatakey1: "testdatavalue1"
   });
-
   t.deepEqual(data.subdir.testDataSubdir, {
     subdirkey: "subdirvalue"
   });
 });
 
-test("getJson() without a dataTemplateEngine", async t => {
+test("getAllGlobalData() with js object data file", async t => {
+  let dataObj = new TemplateData("./test/stubs/");
+  let data = await dataObj.cacheData();
+  let dataFilePaths = await dataObj.getGlobalDataFiles();
+
+  t.true(
+    dataFilePaths.filter(path => {
+      return path.indexOf("./test/stubs/_data/globalData2.js") === 0;
+    }).length > 0
+  );
+
+  t.truthy(data.globalData2);
+  t.is(data.globalData2.datakeyfromjs, "howdy");
+});
+
+test("getAllGlobalData() with js function data file", async t => {
+  let dataObj = new TemplateData("./test/stubs/");
+  let data = await dataObj.cacheData();
+  let dataFilePaths = await dataObj.getGlobalDataFiles();
+
+  t.true(
+    dataFilePaths.filter(path => {
+      return path.indexOf("./test/stubs/_data/globalData2.js") === 0;
+    }).length > 0
+  );
+
+  t.truthy(data.globalDataFn);
+  t.is(data.globalDataFn.datakeyfromjsfn, "howdy");
+});
+
+test("getDataValue() without a dataTemplateEngine", async t => {
   let dataObj = new TemplateData("./test/stubs/");
   dataObj.setDataTemplateEngine(false);
 
-  let data = await dataObj.getJson("./test/stubs/_data/testDataEjs.json", {
+  let data = await dataObj.getDataValue("./test/stubs/_data/testDataEjs.json", {
     pkg: { name: "pkgname" }
   });
 
@@ -133,11 +182,11 @@ test("getJson() without a dataTemplateEngine", async t => {
   });
 });
 
-test("getJson() without dataTemplateEngine changed to `ejs`", async t => {
+test("getDataValue() without dataTemplateEngine changed to `ejs`", async t => {
   let dataObj = new TemplateData("./test/stubs/");
   dataObj.setDataTemplateEngine("ejs");
 
-  let data = await dataObj.getJson("./test/stubs/_data/testDataEjs.json", {
+  let data = await dataObj.getDataValue("./test/stubs/_data/testDataEjs.json", {
     pkg: { name: "pkgname" }
   });
 
@@ -149,36 +198,72 @@ test("getJson() without dataTemplateEngine changed to `ejs`", async t => {
 
 test("getLocalDataPaths", async t => {
   let dataObj = new TemplateData("./test/stubs/");
-  let paths = dataObj.getLocalDataPaths(
+  let paths = await dataObj.getLocalDataPaths(
     "./test/stubs/component/component.liquid"
   );
 
-  t.deepEqual(paths, ["./test/stubs/component/component.json"]);
+  t.deepEqual(paths, [
+    "./test/stubs/component/component.json",
+    "./test/stubs/component/component.11tydata.json",
+    "./test/stubs/component/component.11tydata.js"
+  ]);
+});
+
+test("getLocalDataPaths with an 11ty js template", async t => {
+  let dataObj = new TemplateData("./test/stubs/");
+  let paths = await dataObj.getLocalDataPaths(
+    "./test/stubs/component/component.11ty.js"
+  );
+
+  t.deepEqual(paths, [
+    "./test/stubs/component/component.json",
+    "./test/stubs/component/component.11tydata.json",
+    "./test/stubs/component/component.11tydata.js"
+  ]);
 });
 
 test("getLocalDataPaths with inputDir passed in (trailing slash)", async t => {
   let dataObj = new TemplateData("./test/stubs/");
-  let paths = dataObj.getLocalDataPaths(
+  let paths = await dataObj.getLocalDataPaths(
     "./test/stubs/component/component.liquid"
   );
 
-  t.deepEqual(paths, ["./test/stubs/component/component.json"]);
+  t.deepEqual(paths, [
+    "./test/stubs/component/component.json",
+    "./test/stubs/component/component.11tydata.json",
+    "./test/stubs/component/component.11tydata.js"
+  ]);
 });
 
 test("getLocalDataPaths with inputDir passed in (no trailing slash)", async t => {
   let dataObj = new TemplateData("./test/stubs/");
-  let paths = dataObj.getLocalDataPaths(
+  let paths = await dataObj.getLocalDataPaths(
     "./test/stubs/component/component.liquid"
   );
 
-  t.deepEqual(paths, ["./test/stubs/component/component.json"]);
+  t.deepEqual(paths, [
+    "./test/stubs/component/component.json",
+    "./test/stubs/component/component.11tydata.json",
+    "./test/stubs/component/component.11tydata.js"
+  ]);
 });
 
 test("getLocalDataPaths with inputDir passed in (no leading slash)", async t => {
   let dataObj = new TemplateData("test/stubs");
-  let paths = dataObj.getLocalDataPaths(
+  let paths = await dataObj.getLocalDataPaths(
     "./test/stubs/component/component.liquid"
   );
 
-  t.deepEqual(paths, ["./test/stubs/component/component.json"]);
+  t.deepEqual(paths, [
+    "./test/stubs/component/component.json",
+    "./test/stubs/component/component.11tydata.json",
+    "./test/stubs/component/component.11tydata.js"
+  ]);
+});
+
+test("getRawImports", async t => {
+  let dataObj = new TemplateData("test/stubs");
+  let data = dataObj.getRawImports();
+
+  t.is(data.pkg.name, "@11ty/eleventy");
 });
diff --git a/test/TemplatePathTest.js b/test/TemplatePathTest.js
index 57279f11..ea3dc211 100644
--- a/test/TemplatePathTest.js
+++ b/test/TemplatePathTest.js
@@ -154,6 +154,13 @@ test("Convert to glob", t => {
   t.is(TemplatePath.convertToGlob("./test/stubs/"), "./test/stubs/**");
 });
 
+test("Get extension", t => {
+  t.is(TemplatePath.getExtension(""), "");
+  t.is(TemplatePath.getExtension("test/stubs"), "");
+  t.is(TemplatePath.getExtension("test/stubs.njk"), "njk");
+  t.is(TemplatePath.getExtension("test/stubs.hbs"), "hbs");
+});
+
 test("Remove extension", t => {
   t.is(TemplatePath.removeExtension(""), "");
   t.is(TemplatePath.removeExtension("", "hbs"), "");
diff --git a/test/TemplateTest.js b/test/TemplateTest.js
index b86a3f18..ddfcd537 100644
--- a/test/TemplateTest.js
+++ b/test/TemplateTest.js
@@ -447,8 +447,10 @@ test("Local template data file import (without a global data json)", async t =>
   );
 
   let data = await tmpl.getData();
-  t.deepEqual(dataObj.getLocalDataPaths(tmpl.getInputPath()), [
-    "./test/stubs/component/component.json"
+  t.deepEqual(await dataObj.getLocalDataPaths(tmpl.getInputPath()), [
+    "./test/stubs/component/component.json",
+    "./test/stubs/component/component.11tydata.json",
+    "./test/stubs/component/component.11tydata.js"
   ]);
   t.is(data.localdatakey1, "localdatavalue1");
   t.is(await tmpl.render(), "localdatavalue1");
@@ -465,10 +467,16 @@ test("Local template data file import (two subdirectories deep)", async t => {
     dataObj
   );
 
-  t.deepEqual(dataObj.getLocalDataPaths(tmpl.getInputPath()), [
+  t.deepEqual(await dataObj.getLocalDataPaths(tmpl.getInputPath()), [
     "./test/stubs/firstdir/firstdir.json",
+    "./test/stubs/firstdir/firstdir.11tydata.json",
+    "./test/stubs/firstdir/firstdir.11tydata.js",
     "./test/stubs/firstdir/seconddir/seconddir.json",
-    "./test/stubs/firstdir/seconddir/component.json"
+    "./test/stubs/firstdir/seconddir/seconddir.11tydata.json",
+    "./test/stubs/firstdir/seconddir/seconddir.11tydata.js",
+    "./test/stubs/firstdir/seconddir/component.json",
+    "./test/stubs/firstdir/seconddir/component.11tydata.json",
+    "./test/stubs/firstdir/seconddir/component.11tydata.js"
   ]);
 });
 
@@ -483,10 +491,14 @@ test("Posts inherits local JSON, layouts", async t => {
     dataObj
   );
 
-  let localDataPaths = dataObj.getLocalDataPaths(tmpl.getInputPath());
+  let localDataPaths = await dataObj.getLocalDataPaths(tmpl.getInputPath());
   t.deepEqual(localDataPaths, [
     "./test/stubs/posts/posts.json",
-    "./test/stubs/posts/post1.json"
+    "./test/stubs/posts/posts.11tydata.json",
+    "./test/stubs/posts/posts.11tydata.js",
+    "./test/stubs/posts/post1.json",
+    "./test/stubs/posts/post1.11tydata.json",
+    "./test/stubs/posts/post1.11tydata.js"
   ]);
 
   let localData = await dataObj.getLocalData(tmpl.getInputPath());
@@ -514,8 +526,12 @@ test("Template and folder name are the same, make sure data imports work ok", as
     dataObj
   );
 
-  let localDataPaths = dataObj.getLocalDataPaths(tmpl.getInputPath());
-  t.deepEqual(localDataPaths, ["./test/stubs/posts/posts.json"]);
+  let localDataPaths = await dataObj.getLocalDataPaths(tmpl.getInputPath());
+  t.deepEqual(localDataPaths, [
+    "./test/stubs/posts/posts.json",
+    "./test/stubs/posts/posts.11tydata.json",
+    "./test/stubs/posts/posts.11tydata.js"
+  ]);
 
   let localData = await dataObj.getLocalData(tmpl.getInputPath());
   t.is(localData.layout, "mylocallayout.njk");
diff --git a/test/stubs/_data/globalData2.js b/test/stubs/_data/globalData2.js
new file mode 100644
index 00000000..16a70585
--- /dev/null
+++ b/test/stubs/_data/globalData2.js
@@ -0,0 +1,3 @@
+module.exports = {
+  datakeyfromjs: "howdy"
+};
diff --git a/test/stubs/_data/globalDataFn.js b/test/stubs/_data/globalDataFn.js
new file mode 100644
index 00000000..436c48c3
--- /dev/null
+++ b/test/stubs/_data/globalDataFn.js
@@ -0,0 +1,5 @@
+module.exports = function() {
+  return {
+    datakeyfromjsfn: "howdy"
+  };
+};
diff --git a/test/stubs/component-async/component.11tydata.js b/test/stubs/component-async/component.11tydata.js
new file mode 100644
index 00000000..cc248eaf
--- /dev/null
+++ b/test/stubs/component-async/component.11tydata.js
@@ -0,0 +1,9 @@
+module.exports = async function() {
+  return new Promise(resolve => {
+    setTimeout(function() {
+      resolve({
+        localdatakeyfromjs: "howdydoody"
+      });
+    }, 1);
+  });
+};
diff --git a/test/stubs/component-async/component.njk b/test/stubs/component-async/component.njk
new file mode 100644
index 00000000..e52e6bb8
--- /dev/null
+++ b/test/stubs/component-async/component.njk
@@ -0,0 +1,1 @@
+{{localdatakey1}}
\ No newline at end of file
diff --git a/test/stubs/component/component.11tydata.js b/test/stubs/component/component.11tydata.js
new file mode 100644
index 00000000..bb1bb505
--- /dev/null
+++ b/test/stubs/component/component.11tydata.js
@@ -0,0 +1,3 @@
+module.exports = {
+  localdatakeyfromjs: "howdydoody"
+};
diff --git a/test/stubs/component/component.11tydata.json b/test/stubs/component/component.11tydata.json
new file mode 100644
index 00000000..d0d63736
--- /dev/null
+++ b/test/stubs/component/component.11tydata.json
@@ -0,0 +1,4 @@
+{
+  "localdatakeyfromjs": "this_is_overridden",
+  "localdatakeyfromjs2": "howdy2"
+}
\ No newline at end of file
diff --git a/test/stubs/component/component.json b/test/stubs/component/component.json
index 856c116d..68bc9171 100644
--- a/test/stubs/component/component.json
+++ b/test/stubs/component/component.json
@@ -1,3 +1,4 @@
 {
-  "localdatakey1": "localdatavalue1"
+  "localdatakey1": "localdatavalue1",
+  "localdatakeyfromjs": "this_is_also_overridden"
 }