Repository: 11ty/eleventy
Pull request #2219 (2022-02-15 20:44:04)

Swap to use internal `isPlainObject` utility
In service of: https://github.com/11ty/eleventy/issues/2214


Patch:
diff --git a/src/ComputedDataProxy.js b/src/ComputedDataProxy.js
index e32ad91d..49594108 100644
--- a/src/ComputedDataProxy.js
+++ b/src/ComputedDataProxy.js
@@ -1,6 +1,6 @@
 const lodashSet = require("lodash/set");
 const lodashGet = require("lodash/get");
-const lodashIsPlainObject = require("lodash/isPlainObject");
+const isPlainObject = require("./Util/IsPlainObject");
 
 /* Calculates computed data using Proxies */
 class ComputedDataProxy {
@@ -13,7 +13,7 @@ class ComputedDataProxy {
   }
 
   isArrayOrPlainObject(data) {
-    return Array.isArray(data) || lodashIsPlainObject(data);
+    return Array.isArray(data) || isPlainObject(data);
   }
 
   getProxyData(data, keyRef) {
@@ -97,7 +97,7 @@ class ComputedDataProxy {
   }
 
   _getProxyData(data, keyRef, parentKey = "") {
-    if (lodashIsPlainObject(data)) {
+    if (isPlainObject(data)) {
       return this._getProxyForObject(data, keyRef, parentKey);
     } else if (Array.isArray(data)) {
       return this._getProxyForArray(data, keyRef, parentKey);
diff --git a/src/Plugins/RenderPlugin.js b/src/Plugins/RenderPlugin.js
index c67c0688..10948a27 100644
--- a/src/Plugins/RenderPlugin.js
+++ b/src/Plugins/RenderPlugin.js
@@ -1,6 +1,6 @@
 const fs = require("fs");
 const fsp = fs.promises;
-const lodashIsPlainObject = require("lodash/isPlainObject");
+const isPlainObject = require("../Util/IsPlainObject");
 
 // TODO add a first-class Markdown component to expose this using Markdown-only syntax (will need to be synchronous for markdown-it)
 
@@ -276,7 +276,7 @@ function EleventyPlugin(eleventyConfig, options = {}) {
     }
 
     // if the user passes a string or other literal, remap to an object.
-    if (!lodashIsPlainObject(data)) {
+    if (!isPlainObject(data)) {
       data = {
         _: data,
       };
@@ -309,7 +309,7 @@ function EleventyPlugin(eleventyConfig, options = {}) {
     }
 
     // if the user passes a string or other literal, remap to an object.
-    if (!lodashIsPlainObject(data)) {
+    if (!isPlainObject(data)) {
       data = {
         _: data,
       };
diff --git a/src/Template.js b/src/Template.js
index 7cac3cd6..6d116621 100755
--- a/src/Template.js
+++ b/src/Template.js
@@ -6,7 +6,7 @@ const mkdir = util.promisify(fs.mkdir);
 const os = require("os");
 const path = require("path");
 const normalize = require("normalize-path");
-const isPlainObject = require("lodash/isPlainObject");
+const isPlainObject = require("./Util/IsPlainObject");
 const lodashGet = require("lodash/get");
 const lodashSet = require("lodash/set");
 const { DateTime } = require("luxon");
diff --git a/src/TemplateBehavior.js b/src/TemplateBehavior.js
index c8a92422..9fd82cc5 100644
--- a/src/TemplateBehavior.js
+++ b/src/TemplateBehavior.js
@@ -1,4 +1,4 @@
-const isPlainObject = require("lodash/isPlainObject");
+const isPlainObject = require("./Util/IsPlainObject");
 
 class TemplateBehavior {
   constructor(config) {
diff --git a/src/TemplateMap.js b/src/TemplateMap.js
index f07f0531..f441e43a 100644
--- a/src/TemplateMap.js
+++ b/src/TemplateMap.js
@@ -1,4 +1,4 @@
-const isPlainObject = require("lodash/isPlainObject");
+const isPlainObject = require("./Util/IsPlainObject");
 const DependencyGraph = require("dependency-graph").DepGraph;
 const TemplateCollection = require("./TemplateCollection");
 const EleventyErrorUtil = require("./EleventyErrorUtil");
diff --git a/src/TemplatePermalink.js b/src/TemplatePermalink.js
index f9a4111e..c99f3928 100644
--- a/src/TemplatePermalink.js
+++ b/src/TemplatePermalink.js
@@ -1,7 +1,7 @@
 const path = require("path");
 const TemplatePath = require("./TemplatePath");
 const normalize = require("normalize-path");
-const isPlainObject = require("lodash/isPlainObject");
+const isPlainObject = require("./Util/IsPlainObject");
 const serverlessUrlFilter = require("./Filters/ServerlessUrl");
 
 class TemplatePermalink {
diff --git a/src/Util/IsPlainObject.js b/src/Util/IsPlainObject.js
new file mode 100644
index 00000000..1d4c7527
--- /dev/null
+++ b/src/Util/IsPlainObject.js
@@ -0,0 +1,24 @@
+/* Prior art: this utility was created for https://github.com/11ty/eleventy/issues/2214
+
+ * Inspired by implementations from `is-what`, `typechecker`, `jQuery`, and `lodash`
+
+ * `is-what`
+ * More reading at https://www.npmjs.com/package/is-what#user-content-isplainobject-vs-isanyobject
+ * if (Object.prototype.toString.call(value).slice(8, -1) !== 'Object') return false;
+ * return value.constructor === Object && Object.getPrototypeOf(value) === Object.prototype;
+
+ * `typechecker`
+ * return value !== null && typeof value === 'object' && value.__proto__ === Object.prototype;
+
+ * Notably jQuery and lodash have very similar implementations.
+
+ * For later, remember the `value === Object(value)` trick
+ */
+
+module.exports = function (value) {
+  if (value === null || typeof value !== "object") {
+    return false;
+  }
+  let proto = Object.getPrototypeOf(value);
+  return !proto || proto === Object.prototype;
+};
diff --git a/src/Util/Merge.js b/src/Util/Merge.js
index 17cb3eeb..3427dbac 100644
--- a/src/Util/Merge.js
+++ b/src/Util/Merge.js
@@ -1,4 +1,4 @@
-const isPlainObject = require("lodash/isPlainObject");
+const isPlainObject = require("./IsPlainObject");
 const OVERRIDE_PREFIX = "override:";
 
 function getMergedItem(target, source, parentKey) {


Test patch:
diff --git a/test/IsPlainObjectTest.js b/test/IsPlainObjectTest.js
new file mode 100644
index 00000000..117770d6
--- /dev/null
+++ b/test/IsPlainObjectTest.js
@@ -0,0 +1,80 @@
+const test = require("ava");
+const isPlainObject = require("../src/Util/IsPlainObject");
+
+test("isPlainObject", (t) => {
+  t.is(isPlainObject(null), false);
+  t.is(isPlainObject(undefined), false);
+  t.is(isPlainObject(1), false);
+  t.is(isPlainObject(true), false);
+  t.is(isPlainObject(false), false);
+  t.is(isPlainObject("string"), false);
+  t.is(isPlainObject([]), false);
+  t.is(isPlainObject(Symbol("a")), false);
+  t.is(
+    isPlainObject(function () {}),
+    false
+  );
+});
+
+// https://github.com/lodash/lodash/blob/ddfd9b11a0126db2302cb70ec9973b66baec0975/test/test.js#L11447
+// Notably, did not include the test for DOM Elements.
+test("Test from lodash.isPlainObject", (t) => {
+  t.is(isPlainObject({}), true);
+  t.is(isPlainObject({ a: 1 }), true);
+
+  function Foo(a) {
+    this.a = 1;
+  }
+
+  t.is(isPlainObject({ constructor: Foo }), true);
+  t.is(isPlainObject([1, 2, 3]), false);
+  t.is(isPlainObject(new Foo(1)), false);
+});
+
+test("Test from lodash.isPlainObject: should return `true` for objects with a `[[Prototype]]` of `null`", (t) => {
+  let obj = Object.create(null);
+  t.is(isPlainObject(obj), true);
+
+  obj.constructor = Object.prototype.constructor;
+  t.is(isPlainObject(obj), true);
+});
+
+test("Test from lodash.isPlainObject: should return `true` for objects with a `valueOf` property", (t) => {
+  t.is(isPlainObject({ valueOf: 0 }), true);
+});
+
+test("Test from lodash.isPlainObject: should return `true` for objects with a writable `Symbol.toStringTag` property", (t) => {
+  let obj = {};
+  obj[Symbol.toStringTag] = "X";
+
+  t.is(isPlainObject(obj), true);
+});
+
+test("Test from lodash.isPlainObject: should return `false` for objects with a custom `[[Prototype]]`", (t) => {
+  let obj = Object.create({ a: 1 });
+  t.is(isPlainObject(obj), false);
+});
+
+test("Test from lodash.isPlainObject (modified): should return `false` for non-Object objects", (t) => {
+  t.is(isPlainObject(arguments), true); // WARNING: lodash was false
+  t.is(isPlainObject(Error), false);
+  t.is(isPlainObject(Math), true); // WARNING: lodash was false
+});
+
+test("Test from lodash.isPlainObject: should return `false` for non-objects", (t) => {
+  t.is(isPlainObject(true), false);
+  t.is(isPlainObject("a"), false);
+  t.is(isPlainObject(Symbol("a")), false);
+});
+
+test("Test from lodash.isPlainObject (modified): should return `true` for objects with a read-only `Symbol.toStringTag` property", (t) => {
+  var object = {};
+  Object.defineProperty(object, Symbol.toStringTag, {
+    configurable: true,
+    enumerable: false,
+    writable: false,
+    value: "X",
+  });
+
+  t.is(isPlainObject(object), true); // WARNING: lodash was false
+});