Repository: 11ty/eleventy (js)

Issue:
front matter from subdirectories bleeds into its parent
**Describe the bug**
I see front matter data from an unrelated, nested file in other files.

I discovered that front matter overrides from data files overrides rather than merges so enabled `setDataDeepMerge(true)`. This led to odd behavior I'll show below:

**To Reproduce**
```
$ cat >_data/meta.json
{
  "image": "gimage",
  "description": "gdesc"
}
^D
$ cat >index.mustache
---
meta:
  description: topleveldesc
---

image: {{meta.image}}
desc: {{meta.description}}
^D
```

Now when I build, `_site/index.html` contains what I'd expect to see:
```
$ cat _site/index.html 
image: gimage
desc: topleveldesc
```

Now add another file:
```
$ cat >something.mustache
---
meta:
  description: otherdesc
---

image: {{meta.image}}
desc: {{meta.description}}
^D
```

After I build, `_site/index.html` has changed:
```
$ cat _site/index.html 
image: gimage
desc: otherdesc
```

This doesn't seem right.

**Expected behavior**
I did not expect front matter from one file to affect data in another file.

**Environment:**
 - OS and Version: Mac
 - Eleventy Version: 0.9.0

Pull request:
Merge logic flaw causes data spillage with dataDeepMerge = true
Among other things, this fixes #676 in which data that pertains to one file or directory can show up in completely unrelated files.

In debugging #676, I observed that `Template.getData()` during the async call to `this.addPageDate()` the contents of `mergedData` changes i.e. while we've yielded, another "thread" modifies the data. Essentially `mergedData` comes from various calls to `getData()`. With `dataDeepMerge` set to true, the data is merged rather than replaced.

Due to the logic in `Merge()` objects may end up with embedded shallow copies of objects that may later be modified by subsequent calls to `Merge()`. Here's the specific code in `getMergedItem`:
```
  if (!target) {
    return source;
  } else if (Array.isArray(target) && Array.isArray(source)) {
...
  } else {
    // number, string, class instance, etc
    return source;
  }
```

Note that returning `source` in the second case is fine for a literal value, but problematic in the first case with embedded objects.

This logic has a related issue where key renaming logic isn't applied in these cases. In particular, this (new) test fails:

```js
test("Deep, override: empty", t => {
  t.deepEqual(Merge({}, { a: { b: [3, 4] } }), { a: { b: [3, 4] } });
  t.deepEqual(Merge({}, { a: [2] }), { a: [2] });
  t.deepEqual(Merge({}, { "override:a": [2] }), { a: [2] });
  t.deepEqual(Merge({}, { a: { "override:b": [3, 4] } }), { a: { b: [3, 4] } });
});
```

```
  ✖ MergeTest › Deep, override: empty 

  1 test failed

  MergeTest › Deep, override: empty

  /Users/ahl/src/eleventy/test/MergeTest.js:153

   152:   t.deepEqual(Merge({}, { "override:a": [2] }), { a: [2] });          
   153:   t.deepEqual(Merge({}, { a: { "override:b": [3, 4] } }), { a: { b: […
   154:   });                                                                 

  Difference:

    {
      a: {
  -     'override:b': [
  -       3,
  -       4,
  -     ],
  +     b: [
  +       3,
  +       4,
  +     ],
      },
    }
```

With this fix applied, the test passes and data leakage per #676 is fixed.


Note that #676 requires multiple files being processed simultaneously. I wasn't sure how to write a test that could do that. If there's an existing example please point me to it; if you have an idea for how to test the processing of multiple files, please let me know.

Patch:
diff --git a/src/Util/Merge.js b/src/Util/Merge.js
index 3eb57b90..17cb3eeb 100644
--- a/src/Util/Merge.js
+++ b/src/Util/Merge.js
@@ -7,9 +7,12 @@ function getMergedItem(target, source, parentKey) {
     return source;
   }
 
-  if (!target) {
-    return source;
-  } else if (Array.isArray(target) && Array.isArray(source)) {
+  // deep copy objects to avoid sharing and to effect key renaming
+  if (!target && isPlainObject(source)) {
+    target = {};
+  }
+
+  if (Array.isArray(target) && Array.isArray(source)) {
     return target.concat(source);
   } else if (isPlainObject(target)) {
     if (isPlainObject(source)) {


Test patch:
diff --git a/test/MergeTest.js b/test/MergeTest.js
index f5e99fdd..ce58d6a6 100644
--- a/test/MergeTest.js
+++ b/test/MergeTest.js
@@ -145,3 +145,10 @@ test("Deep, override: prefix at other placements", t => {
     }
   );
 });
+
+test("Deep, override: empty", t => {
+  t.deepEqual(Merge({}, { a: { b: [3, 4] } }), { a: { b: [3, 4] } });
+  t.deepEqual(Merge({}, { a: [2] }), { a: [2] });
+  t.deepEqual(Merge({}, { "override:a": [2] }), { a: [2] });
+  t.deepEqual(Merge({}, { a: { "override:b": [3, 4] } }), { a: { b: [3, 4] } });
+});