diff --git a/src/ComputedDataProxy.js b/src/ComputedDataProxy.js index e32ad91d5..495941081 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 c67c0688f..10948a278 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 7cac3cd6a..6d1166214 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 c8a92422e..9fd82cc5d 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 f07f0531c..f441e43ab 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 f9a4111e5..c99f39287 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 000000000..1d4c75278 --- /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 17cb3eeb6..3427dbac6 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) { diff --git a/test/IsPlainObjectTest.js b/test/IsPlainObjectTest.js new file mode 100644 index 000000000..117770d66 --- /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 +});