Repository: 11ty/eleventy (js)

Issue:
Global data doesn’t have explicit rules about data overrides when data file roots are the same
In template and directory data files override is explicit:

`component.11tydata.js` overrides `component.11tydata.json` overrides `component.json`

No such explicit convention exists for global data files. It does one generic glob selector for these files.

https://github.com/11ty/eleventy/blob/ccbaf5af8cafeccbf5b0ecde9e8b75c2219f07c3/src/TemplateData.js#L136

Surfaced by @gloryofrobots in #635.

Pull request description:
Global data files priority according to file extensions  #794
This PR resolves #794 and also adds merging of global data from different files with the same base filename into one object

For example if we have files
```
//_data/data.js
module.exports = {
  key1: "js-val1"
}

// _data/data.json
{
   "key1": "json-val1",
   "key2": "json-val2"
}
```
resulting data objects will be merged according to extension priority so ```js``` will override ```json```
```
{
  "key1":  "js-val1",
  "key2":  "json-val2"
}
```
Working on this PR I found that 11tydata.{json|js} files are not supported due to code in ```TemplateData.getObjectPathForDataFile```
I do not know if it is a bug or a normal behavior. To clarify this I created #820 

Patch:
diff --git a/src/TemplateData.js b/src/TemplateData.js
index 83862e14..670e7f42 100644
--- a/src/TemplateData.js
+++ b/src/TemplateData.js
@@ -2,6 +2,7 @@ const fs = require("fs-extra");
 const fastglob = require("fast-glob");
 const parsePath = require("parse-filepath");
 const lodashset = require("lodash/set");
+const lodashget = require("lodash/get");
 const lodashUniq = require("lodash/uniq");
 const merge = require("./Util/Merge");
 const TemplateRender = require("./TemplateRender");
@@ -135,26 +136,52 @@ class TemplateData {
 
   async getGlobalDataGlob() {
     let dir = await this.getInputDir();
-    let userExtensions = "";
-    // creating glob string for user extensions
-    if (this.hasUserDataExtensions()) {
-      userExtensions = this.getUserDataExtensions().join("|") + "|";
-    }
 
-    return [
-      this._getGlobalDataGlobByExtension(dir, "(" + userExtensions + "json|js)")
-    ];
+    let extGlob = this.getGlobalDataExtensionPriorities().join("|");
+    return [this._getGlobalDataGlobByExtension(dir, "(" + extGlob + ")")];
   }
 
   getWatchPathCache() {
     return this.pathCache;
   }
 
+  getGlobalDataExtensionPriorities() {
+    return this.getUserDataExtensions().concat(["json", "js"]);
+  }
+
+  static calculateExtensionPriority(path, priorities) {
+    for (let i = 0; i < priorities.length; i++) {
+      let ext = priorities[i];
+      if (path.endsWith(ext)) {
+        return i;
+      }
+    }
+    return priorities.length;
+  }
+
   async getGlobalDataFiles() {
+    let priorities = this.getGlobalDataExtensionPriorities();
+
     let paths = await fastglob(await this.getGlobalDataGlob(), {
       caseSensitiveMatch: false,
       dot: true
     });
+
+    // sort paths according to extension priorities
+    // here we use reverse ordering, because paths with bigger index in array will override the first ones
+    // example [path/file.json, path/file.js] here js will override json
+    paths = paths.sort((first, second) => {
+      let p1 = TemplateData.calculateExtensionPriority(first, priorities);
+      let p2 = TemplateData.calculateExtensionPriority(second, priorities);
+      if (p1 < p2) {
+        return -1;
+      }
+      if (p1 > p2) {
+        return 1;
+      }
+      return 0;
+    });
+
     this.pathCache = paths;
     return paths;
   }
@@ -179,17 +206,19 @@ class TemplateData {
 
     for (let j = 0, k = files.length; j < k; j++) {
       let folders = await this.getObjectPathForDataFile(files[j]);
+      let data = await this.getDataValue(files[j], rawImports);
 
-      // 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]})`
+          `merging global data from ${files[j]} with an already existing global data file (${dataFileConflicts[folders]}). Overriding existing keys`
         );
+
+        let oldData = lodashget(globalData, folders);
+        data = TemplateData.mergeDeep(this.config, oldData, data);
       }
-      dataFileConflicts[folders] = files[j];
 
+      dataFileConflicts[folders] = files[j];
       debug(`Found global data file ${files[j]} and adding as: ${folders}`);
-      let data = await this.getDataValue(files[j], rawImports);
       lodashset(globalData, folders, data);
     }
 


Test patch:
diff --git a/test/UserDataExtensionsTest.js b/test/UserDataExtensionsTest.js
index fa5cc1e6..cffb98b5 100644
--- a/test/UserDataExtensionsTest.js
+++ b/test/UserDataExtensionsTest.js
@@ -70,7 +70,6 @@ test("Global data", async t => {
     "./test/stubs-630/_data/**/*.(nosj|yaml|json|js)"
   ]);
 
-  let dataFilePaths = await dataObj.getGlobalDataFiles();
   let data = await dataObj.getData();
 
   // JS GLOBAL DATA
@@ -90,3 +89,21 @@ test("Global data", async t => {
 
   t.is(data.subdir.globalDataSubdir.keyyaml, "yaml");
 });
+
+test("Global data merging and priority", async t => {
+  let dataObj = new TemplateData("./test/stubs-630/");
+  injectDataExtensions(dataObj);
+
+  let data = await dataObj.getData();
+
+  // TESTING GLOBAL DATA PRIORITY AND MERGING
+  t.is(data.mergingGlobalData.datakey1, "js-value1");
+  t.is(data.mergingGlobalData.datakey2, "json-value2");
+  t.is(data.mergingGlobalData.datakey3, "yaml-value3");
+  t.is(data.mergingGlobalData.datakey4, "nosj-value4");
+
+  t.is(data.mergingGlobalData.jskey, "js");
+  t.is(data.mergingGlobalData.jsonkey, "json");
+  t.is(data.mergingGlobalData.yamlkey, "yaml");
+  t.is(data.mergingGlobalData.nosjkey, "nosj");
+});
diff --git a/test/stubs-630/_data/mergingGlobalData.js b/test/stubs-630/_data/mergingGlobalData.js
new file mode 100644
index 00000000..f99fc138
--- /dev/null
+++ b/test/stubs-630/_data/mergingGlobalData.js
@@ -0,0 +1,4 @@
+module.exports = {
+  datakey1: "js-value1",
+  jskey: "js"
+};
diff --git a/test/stubs-630/_data/mergingGlobalData.json b/test/stubs-630/_data/mergingGlobalData.json
new file mode 100644
index 00000000..68aa73e1
--- /dev/null
+++ b/test/stubs-630/_data/mergingGlobalData.json
@@ -0,0 +1,5 @@
+{
+  "datakey1": "json-value1",
+  "datakey2": "json-value2",
+  "jsonkey": "json"
+}
diff --git a/test/stubs-630/_data/mergingGlobalData.nosj b/test/stubs-630/_data/mergingGlobalData.nosj
new file mode 100644
index 00000000..6a015229
--- /dev/null
+++ b/test/stubs-630/_data/mergingGlobalData.nosj
@@ -0,0 +1,7 @@
+{
+  "datakey1": "nosj-value1",
+  "datakey2": "nosj-value2",
+  "datakey3": "nosj-value3",
+  "datakey4": "nosj-value4",
+  "nosjkey": "nosj"
+}
diff --git a/test/stubs-630/_data/mergingGlobalData.yaml b/test/stubs-630/_data/mergingGlobalData.yaml
new file mode 100644
index 00000000..916ef7b7
--- /dev/null
+++ b/test/stubs-630/_data/mergingGlobalData.yaml
@@ -0,0 +1,4 @@
+datakey1: "yaml-value1"
+datakey2: "yaml-value2"
+datakey3: "yaml-value3"
+yamlkey: "yaml"