From 7bfcb31e082b0eb5b9118d56647661539e1f7887 Mon Sep 17 00:00:00 2001 From: Aras Abbasi Date: Thu, 19 Oct 2023 18:24:38 +0200 Subject: [PATCH] fix(deps): remove lodash (#2529) --- lib/common.js | 100 +++++++++++++++++++++++++++++++++++++------ lib/match_body.js | 9 +++- package-lock.json | 7 +-- package.json | 1 - tests/test_common.js | 96 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 195 insertions(+), 18 deletions(-) diff --git a/lib/common.js b/lib/common.js index 38c6d31cc..97d7c30e4 100644 --- a/lib/common.js +++ b/lib/common.js @@ -1,8 +1,6 @@ 'use strict' const debug = require('debug')('nock.common') -const isPlainObject = require('lodash/isPlainObject') -const set = require('lodash/set') const timers = require('timers') const url = require('url') const util = require('util') @@ -568,17 +566,6 @@ const dataEqual = (expected, actual) => { return deepEqual(expected, actual) } -/** - * Converts flat objects whose keys use JSON path notation to nested objects. - * - * The input object is not mutated. - * - * @example - * { 'foo[bar][0]': 'baz' } -> { foo: { bar: [ 'baz' ] } } - */ -const expand = input => - Object.entries(input).reduce((acc, [k, v]) => set(acc, k, v), {}) - /** * Performs a recursive strict comparison between two values. * @@ -665,10 +652,97 @@ function isRequestDestroyed(req) { ) } +/** + * Returns true if the given value is a plain object and not an Array. + * @param {*} value + * @returns {boolean} + */ +function isPlainObject(value) { + if (typeof value !== 'object' || value === null) return false + + if (Object.prototype.toString.call(value) !== '[object Object]') return false + + const proto = Object.getPrototypeOf(value) + if (proto === null) return true + + const Ctor = + Object.prototype.hasOwnProperty.call(proto, 'constructor') && + proto.constructor + return ( + typeof Ctor === 'function' && + Ctor instanceof Ctor && + Function.prototype.call(Ctor) === Function.prototype.call(value) + ) +} + +const prototypePollutionBlockList = ['__proto__', 'prototype', 'constructor'] +const blocklistFilter = function (part) { + return prototypePollutionBlockList.indexOf(part) === -1 +} + +/** + * Converts flat objects whose keys use JSON path notation to nested objects. + * + * The input object is not mutated. + * + * @example + * { 'foo[bar][0]': 'baz' } -> { foo: { bar: [ 'baz' ] } } + */ +const expand = input => { + if (input === undefined || input === null) { + return input + } + + const keys = Object.keys(input) + + const result = {} + let resultPtr = result + + for (let path of keys) { + const originalPath = path + if (path.indexOf('[') >= 0) { + path = path.replace(/\[/g, '.').replace(/]/g, '') + } + + const parts = path.split('.') + + const check = parts.filter(blocklistFilter) + + if (check.length !== parts.length) { + return undefined + } + resultPtr = result + const lastIndex = parts.length - 1 + + for (let i = 0; i < parts.length; ++i) { + const part = parts[i] + if (i === lastIndex) { + if (Array.isArray(resultPtr)) { + resultPtr[+part] = input[originalPath] + } else { + resultPtr[part] = input[originalPath] + } + } else { + if (resultPtr[part] === undefined || resultPtr[part] === null) { + const nextPart = parts[i + 1] + if (/^\d+$/.test(nextPart)) { + resultPtr[part] = [] + } else { + resultPtr[part] = {} + } + } + resultPtr = resultPtr[part] + } + } + } + return result +} + module.exports = { contentEncoding, dataEqual, deleteHeadersField, + expand, forEachHeader, formatQueryValue, headersArrayToObject, diff --git a/lib/match_body.js b/lib/match_body.js index f8a1545ab..436b7b53a 100644 --- a/lib/match_body.js +++ b/lib/match_body.js @@ -1,6 +1,5 @@ 'use strict' -const mapValues = require('lodash/mapValues') const querystring = require('querystring') const common = require('./common') @@ -62,6 +61,14 @@ module.exports = function matchBody(options, spec, body) { return common.dataEqual(spec, body) } +function mapValues(object, cb) { + const keys = Object.keys(object) + for (const key of keys) { + object[key] = cb(object[key], key, object) + } + return object +} + /** * Based on lodash issue discussion * https://github.com/lodash/lodash/issues/1244 diff --git a/package-lock.json b/package-lock.json index b25be6559..3ba884eb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "debug": "^4.1.0", "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.21", "propagate": "^2.0.0" }, "devDependencies": { @@ -6348,7 +6347,8 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "node_modules/lodash-es": { "version": "4.17.21", @@ -18734,7 +18734,8 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true }, "lodash-es": { "version": "4.17.21", diff --git a/package.json b/package.json index 4c99d51ab..fc57f8634 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "dependencies": { "debug": "^4.1.0", "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.21", "propagate": "^2.0.0" }, "devDependencies": { diff --git a/tests/test_common.js b/tests/test_common.js index 5e5b3db55..3b6d57dc8 100644 --- a/tests/test_common.js +++ b/tests/test_common.js @@ -545,3 +545,99 @@ it('testing timers are deleted correctly', done => { done() }) }) + +describe('`isPlainObject()`', () => { + const { isPlainObject } = common + + it('custom Object', () => { + function Foo() { + this.a = 1 + } + expect(isPlainObject(new Foo()), false) + }) + + it('Array', () => { + expect(isPlainObject([1, 2, 3]), false) + }) + + it('Date', () => { + expect(isPlainObject(new Date()), false) + }) + + it('RegExp', () => { + expect(isPlainObject(/a/), false) + }) + + it('plain Object', () => { + expect(isPlainObject({}), true) + }) + + it('null', () => { + expect(isPlainObject(null), true) + }) + + it('null-Object /1', () => { + expect(isPlainObject({ __proto__: null }), true) + }) + + it('null-Object /2', () => { + expect(isPlainObject(Object.create(null)), true) + }) +}) + +describe('`expand()`', () => { + const { expand } = common + + it('undefined', () => { + expect(expand(undefined), undefined) + }) + + it('null', () => { + expect(expand(null), null) + }) + + it('throws on constructor', () => { + expect(expand({ constructor: 4 })).equal(undefined) + }) + + it('pure key values', () => { + expect(expand({ a: 4 })).deep.equal({ a: 4 }) + }) + + it('nested object', () => { + expect(expand({ 'a.b': 4 })).deep.equal({ a: { b: 4 } }) + }) + + it('nested object', () => { + expect(expand({ 'a.b': 4, 'a.c': 5 })).deep.equal({ a: { b: 4, c: 5 } }) + }) + + it('nested object', () => { + expect(expand({ 'a.b': 4, 'b.a': 5 })).deep.equal({ + a: { b: 4 }, + b: { a: 5 }, + }) + }) + + it('nested array', () => { + expect(expand({ 'a.0': 4, 'a.1': 5 })).deep.equal({ a: [4, 5] }) + }) + + it('array-like', () => { + expect(expand({ 'a[0]': 4, 'a[1]': 5 })).deep.equal({ a: [4, 5] }) + }) + + it('example', () => { + expect(expand({ 'foo[bar][0]': 'baz' })).deep.equal({ + foo: { bar: ['baz'] }, + }) + }) + + it('does not mutate original', () => { + const original = { 'foo[bar][0]': 'baz' } + const result = expand(original) + expect(result).deep.equal({ foo: { bar: ['baz'] } }) + expect(original).deep.equal({ 'foo[bar][0]': 'baz' }) + expect(original).not.equal(result) + }) +})