From 24b40be80997b54b140ab7e9b6ad2db133b8c6bb Mon Sep 17 00:00:00 2001 From: Sander Verweij Date: Sat, 20 Jul 2024 17:00:29 +0200 Subject: [PATCH] refactor: replaces lodash (#950) ## Description - [x] centralizes remaining uses of lodash in central utilities - [x] in the central utilities replace the lodash array functions with native alternatives that work in our context - [x] in the central utilities replace the lodash object functions with native alternatives that work in our context - [x] removes lodash from the package manifest ## Motivation and Context lodash is a great library, however - it's not really maintained anymore - it has fairly large download size (1.4Mb [according to pkg-size](https://pkg-size.dev/lodash)) - while we only use a small subset of it ## How Has This Been Tested? - [x] green ci ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] Documentation only change - [x] Refactor (non-breaking change which fixes an issue without changing functionality) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) ## Checklist - [x] :book: - My change doesn't require a documentation update, or ... - it _does_ and I have updated it - [x] :balance_scale: - The contribution will be subject to [The MIT license](https://github.com/sverweij/dependency-cruiser/blob/main/LICENSE), and I'm OK with that. - The contribution is my own original work. - I am ok with the stuff in [**CONTRIBUTING.md**](https://github.com/sverweij/dependency-cruiser/blob/main/.github/CONTRIBUTING.md). --- package-lock.json | 13 ----- package.json | 2 - src/cli/index.mjs | 2 +- src/cli/normalize-cli-options.mjs | 2 +- .../merge-configs.mjs | 3 +- src/enrich/summarize/summarize-modules.mjs | 2 +- src/extract/extract-dependencies.mjs | 3 +- .../resolve/external-module-helpers.mjs | 6 +-- src/graph-utl/consolidate-modules.mjs | 2 +- src/main/helpers.mjs | 6 +-- src/main/rule-set/assert-validity.mjs | 7 ++- src/report/dot/index.mjs | 6 +-- src/report/dot/theming.mjs | 14 +++--- src/utl/array-util.mjs | 24 ++++++++++ src/utl/object-util.mjs | 40 ++++++++++++++++ test/utl/object-util.spec.mjs | 48 +++++++++++++++++++ 16 files changed, 137 insertions(+), 43 deletions(-) create mode 100644 src/utl/object-util.mjs create mode 100644 test/utl/object-util.spec.mjs diff --git a/package-lock.json b/package-lock.json index ae1397ba9..691427699 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,6 @@ "interpret": "^3.1.1", "is-installed-globally": "1.0.0", "json5": "2.2.3", - "lodash": "4.17.21", "memoize": "10.0.0", "picocolors": "1.0.1", "picomatch": "4.0.2", @@ -46,7 +45,6 @@ "@babel/plugin-transform-modules-commonjs": "7.24.8", "@babel/preset-typescript": "7.24.7", "@swc/core": "1.7.0", - "@types/lodash": "4.17.7", "@types/node": "20.14.11", "@types/prompts": "2.4.9", "@typescript-eslint/eslint-plugin": "7.16.1", @@ -1219,13 +1217,6 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, - "node_modules/@types/lodash": { - "version": "4.17.7", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", - "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { "version": "20.14.11", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", @@ -4756,10 +4747,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "dev": true, diff --git a/package.json b/package.json index 20f8c43ea..dc441a453 100644 --- a/package.json +++ b/package.json @@ -217,7 +217,6 @@ "interpret": "^3.1.1", "is-installed-globally": "1.0.0", "json5": "2.2.3", - "lodash": "4.17.21", "memoize": "10.0.0", "picocolors": "1.0.1", "picomatch": "4.0.2", @@ -234,7 +233,6 @@ "@babel/plugin-transform-modules-commonjs": "7.24.8", "@babel/preset-typescript": "7.24.7", "@swc/core": "1.7.0", - "@types/lodash": "4.17.7", "@types/node": "20.14.11", "@types/prompts": "2.4.9", "@typescript-eslint/eslint-plugin": "7.16.1", diff --git a/src/cli/index.mjs b/src/cli/index.mjs index 0d01a6619..8f642ee82 100644 --- a/src/cli/index.mjs +++ b/src/cli/index.mjs @@ -1,6 +1,5 @@ import { join } from "node:path"; import picomatch from "picomatch"; -import set from "lodash/set.js"; import isInstalledGlobally from "is-installed-globally"; import pc from "picocolors"; @@ -10,6 +9,7 @@ import { write } from "./utl/io.mjs"; import setUpCliFeedbackListener from "./listeners/cli-feedback.mjs"; import setUpPerformanceLogListener from "./listeners/performance-log/index.mjs"; import setUpNDJSONListener from "./listeners/ndjson.mjs"; +import { set } from "#utl/object-util.mjs"; import cruise from "#main/cruise.mjs"; import { INFO, bus } from "#utl/bus.mjs"; diff --git a/src/cli/normalize-cli-options.mjs b/src/cli/normalize-cli-options.mjs index 15163c189..63ff1121c 100644 --- a/src/cli/normalize-cli-options.mjs +++ b/src/cli/normalize-cli-options.mjs @@ -1,6 +1,5 @@ import { accessSync, R_OK } from "node:fs"; import { isAbsolute } from "node:path"; -import set from "lodash/set.js"; import { RULES_FILE_NAME_SEARCH_ARRAY, DEFAULT_BASELINE_FILE_NAME, @@ -11,6 +10,7 @@ import { BABEL_CONFIG, OLD_DEFAULT_RULES_FILE_NAME, } from "./defaults.mjs"; +import { set } from "#utl/object-util.mjs"; import loadConfig from "#config-utl/extract-depcruise-config/index.mjs"; function getOptionValue(pDefault) { diff --git a/src/config-utl/extract-depcruise-config/merge-configs.mjs b/src/config-utl/extract-depcruise-config/merge-configs.mjs index 5e72cdbba..673e1590e 100644 --- a/src/config-utl/extract-depcruise-config/merge-configs.mjs +++ b/src/config-utl/extract-depcruise-config/merge-configs.mjs @@ -1,6 +1,5 @@ import { isDeepStrictEqual } from "node:util"; -import uniqBy from "lodash/uniqBy.js"; -import uniqWith from "lodash/uniqWith.js"; +import { uniqBy, uniqWith } from "#utl/array-util.mjs"; function extendNamedRule(pExtendedRule, pForbiddenArrayBase) { return pForbiddenArrayBase diff --git a/src/enrich/summarize/summarize-modules.mjs b/src/enrich/summarize/summarize-modules.mjs index 25fa19b66..0f2ef18d9 100644 --- a/src/enrich/summarize/summarize-modules.mjs +++ b/src/enrich/summarize/summarize-modules.mjs @@ -1,7 +1,7 @@ -import uniqWith from "lodash/uniqWith.js"; import isSameViolation from "./is-same-violation.mjs"; import { findRuleByName } from "#graph-utl/rule-set.mjs"; import compare from "#graph-utl/compare.mjs"; +import { uniqWith } from "#utl/array-util.mjs"; function cutNonTransgressions(pModule) { return { diff --git a/src/extract/extract-dependencies.mjs b/src/extract/extract-dependencies.mjs index c7b47fd1f..3fda90f8b 100644 --- a/src/extract/extract-dependencies.mjs +++ b/src/extract/extract-dependencies.mjs @@ -1,5 +1,4 @@ import { join, extname, dirname } from "node:path"; -import uniqBy from "lodash/uniqBy.js"; import { extract as acornExtract } from "./acorn/extract.mjs"; import { extract as tscExtract, @@ -14,7 +13,7 @@ import { detectPreCompilationNess, extractModuleAttributes, } from "./helpers.mjs"; -import { intersects } from "#utl/array-util.mjs"; +import { uniqBy, intersects } from "#utl/array-util.mjs"; function extractWithTsc(pCruiseOptions, pFileName, pTranspileOptions) { let lDependencies = tscExtract(pCruiseOptions, pFileName, pTranspileOptions); diff --git a/src/extract/resolve/external-module-helpers.mjs b/src/extract/resolve/external-module-helpers.mjs index c01ead624..9f90b1256 100644 --- a/src/extract/resolve/external-module-helpers.mjs +++ b/src/extract/resolve/external-module-helpers.mjs @@ -26,7 +26,7 @@ import { isScoped, isRelativeModuleName } from "./module-classifiers.mjs"; * * At this time we don't take situations into account where the caller includes * a node module through a local path (which could make sense if you're on - * non-commonJS and are still using node_modules) e.g. '../node_modules/lodash/fp' + * non-commonJS and are still using node_modules) e.g. '../node_modules/oldash/fp' * * @param {string} pModule a module name * @return {string} the module name root @@ -58,8 +58,8 @@ export function getPackageRoot(pModule) { * returns the contents of the package.json of the given pModule as it would * resolve from pBaseDirectory * - * e.g. to get the package.json of `lodash` that is required bya - * `src/report/something.js` use `getPackageJSON('lodash', 'src/report/');` + * e.g. to get the package.json of `oldash` that is required bya + * `src/report/something.js` use `getPackageJSON('oldash', 'src/report/');` * * The pBaseDirectory parameter is necessary because dependency-cruiser/ this module * will have a different base dir, and will hence resolve either to the diff --git a/src/graph-utl/consolidate-modules.mjs b/src/graph-utl/consolidate-modules.mjs index a2d00c581..3b0a6d980 100644 --- a/src/graph-utl/consolidate-modules.mjs +++ b/src/graph-utl/consolidate-modules.mjs @@ -1,5 +1,5 @@ -import uniqBy from "lodash/uniqBy.js"; import compare from "./compare.mjs"; +import { uniqBy } from "#utl/array-util.mjs"; function mergeModule(pLeftModule, pRightModule) { return { diff --git a/src/main/helpers.mjs b/src/main/helpers.mjs index e34c6d951..b37dc6636 100644 --- a/src/main/helpers.mjs +++ b/src/main/helpers.mjs @@ -1,6 +1,4 @@ -import get from "lodash/get.js"; -import has from "lodash/has.js"; -import set from "lodash/set.js"; +import { has, get, set } from "#utl/object-util.mjs"; const RE_PROPERTIES = [ "path", @@ -30,7 +28,7 @@ export function normalizeREProperties( let lPropertyContainer = structuredClone(pPropertyContainer); for (const lProperty of pREProperties) { - // lProperty can be nested properties, so we use lodash.has and lodash.get + // lProperty can be nested properties, so we use _.has and _.get // instead of elvis operators if (has(lPropertyContainer, lProperty)) { set( diff --git a/src/main/rule-set/assert-validity.mjs b/src/main/rule-set/assert-validity.mjs index 7edc9aadb..af17da0e2 100644 --- a/src/main/rule-set/assert-validity.mjs +++ b/src/main/rule-set/assert-validity.mjs @@ -1,10 +1,9 @@ import Ajv from "ajv"; import safeRegex from "safe-regex"; -import has from "lodash/has.js"; -import get from "lodash/get.js"; import { assertCruiseOptionsValid } from "../options/assert-validity.mjs"; import { normalizeToREAsString } from "../helpers.mjs"; import configurationSchema from "#configuration-schema"; +import { has, get } from "#utl/object-util.mjs"; const ajv = new Ajv(); // the default for this is 25 - as noted in the safe-regex source code already, @@ -29,8 +28,8 @@ function assertSchemaCompliance(pSchema, pConfiguration) { } function hasPath(pObject, pSection, pCondition) { - // pCondition can be nested properties, so we use lodash.has instead - // of elvis operators + // pCondition can be nested properties, so we use a bespoke + // 'has' function instead of simple elvis operators return has(pObject, pSection) && has(pObject[pSection], pCondition); } diff --git a/src/report/dot/index.mjs b/src/report/dot/index.mjs index 330dd978e..dbd4fd66a 100644 --- a/src/report/dot/index.mjs +++ b/src/report/dot/index.mjs @@ -1,11 +1,11 @@ /* eslint-disable prefer-template */ -import get from "lodash/get.js"; import theming from "./theming.mjs"; import moduleUtl from "./module-utl.mjs"; import prepareFolderLevel from "./prepare-folder-level.mjs"; import prepareCustomLevel from "./prepare-custom-level.mjs"; import prepareFlatLevel from "./prepare-flat-level.mjs"; import { applyFilters } from "#graph-utl/filter-bank.mjs"; +import { get } from "#utl/object-util.mjs"; // not importing EOL from "node:os" so output is the same on windows and unices const EOL = "\n"; @@ -151,8 +151,8 @@ function pryReporterOptionsFromResults(pGranularity, pResults) { const lFallbackReporterOptions = pResults?.summary?.optionsUsed?.reporterOptions?.dot; - // using lodash.get here because the reporter options will contain nested - // properties, which it handles for us + // using a bespoke 'get' function here because the reporter options will + // contain nested properties, which it handles for us return get( pResults, GRANULARITY2REPORTER_OPTIONS.get(pGranularity), diff --git a/src/report/dot/theming.mjs b/src/report/dot/theming.mjs index 78ee67133..8649d5318 100644 --- a/src/report/dot/theming.mjs +++ b/src/report/dot/theming.mjs @@ -1,6 +1,5 @@ -import get from "lodash/get.js"; -import has from "lodash/has.js"; import DEFAULT_THEME from "./default-theme.mjs"; +import { has, get } from "#utl/object-util.mjs"; function matchesRE(pValue, pRE) { const lMatchResult = pValue.match && pValue.match(pRE); @@ -14,10 +13,13 @@ function matchesCriterion(pModuleKey, pCriterion) { function moduleOrDependencyMatchesCriteria(pSchemeEntry, pModule) { return Object.keys(pSchemeEntry.criteria).every((pKey) => { - // we use lodash.get here because in the criteria you can enter - // nested keys like "rules[0].severity" : "error", and lodash.get handles - // that for us - const lCriterion = get(pSchemeEntry.criteria, pKey); + // the keys can have paths in them like {"rules[0].severity": "info"} + // To get the criterion treat that key as a string and not as a path + // eslint-disable-next-line security/detect-object-injection + const lCriterion = pSchemeEntry.criteria[pKey]; + // we use a bespoke 'get' here because in the criteria you can enter + // nested keys like "rules[0].severity" : "error", and that function + // handles those for us const lModuleKey = get(pModule, pKey); if (!(lModuleKey || has(pModule, pKey))) { diff --git a/src/utl/array-util.mjs b/src/utl/array-util.mjs index 350fee4e7..e040e3ba3 100644 --- a/src/utl/array-util.mjs +++ b/src/utl/array-util.mjs @@ -13,3 +13,27 @@ export function intersects(pLeftArray, pRightArray) { export function uniq(pArray) { return [...new Set(pArray)]; } + +/** + * @param {any[]} pArray + * @param {function} pIteratee + * @returns {any[]} + */ +export function uniqBy(pArray, pIteratee) { + return pArray.filter( + (pElement, pIndex, pSelf) => + pIndex === pSelf.findIndex((pY) => pIteratee(pElement) === pIteratee(pY)), + ); +} + +/** + * @param {any[]} pArray + * @param {function} pComparator + * @returns {any[]} + */ +export function uniqWith(pArray, pComparator) { + return pArray.filter( + (pElement, pIndex, pSelf) => + pIndex === pSelf.findIndex((pY) => pComparator(pElement, pY)), + ); +} diff --git a/src/utl/object-util.mjs b/src/utl/object-util.mjs new file mode 100644 index 000000000..661c4dbda --- /dev/null +++ b/src/utl/object-util.mjs @@ -0,0 +1,40 @@ +/* eslint-disable security/detect-object-injection */ + +export function get(pObject, pPath, pDefault) { + if (!pObject || !pPath) { + return pDefault; + } + // Regex explained: https://regexr.com/58j0k + const lPathArray = pPath.match(/([^[.\]])+/g); + + const lReturnValue = lPathArray.reduce((pPreviousObject, pKey) => { + return pPreviousObject && pPreviousObject[pKey]; + }, pObject); + + if (!lReturnValue) { + return pDefault; + } + return lReturnValue; +} + +export function set(pObject, pPath, pValue) { + const lPathArray = pPath.match(/([^[.\]])+/g); + + lPathArray.reduce((pPreviousObject, pKey, pIndex) => { + if (pIndex === lPathArray.length - 1) { + pPreviousObject[pKey] = pValue; + } else if (!pPreviousObject[pKey]) { + pPreviousObject[pKey] = {}; + } + return pPreviousObject[pKey]; + }, pObject); +} + +/** + * @param {any} pObject + * @param {string} pPath + * @returns {boolean} + */ +export function has(pObject, pPath) { + return Boolean(get(pObject, pPath)); +} diff --git a/test/utl/object-util.spec.mjs b/test/utl/object-util.spec.mjs new file mode 100644 index 000000000..33b3fbd08 --- /dev/null +++ b/test/utl/object-util.spec.mjs @@ -0,0 +1,48 @@ +/* eslint-disable no-magic-numbers, no-undefined */ +import { equal } from "node:assert/strict"; +import { has, get, set } from "#utl/object-util.mjs"; + +describe("[U] object-util", () => { + describe("[U] has", () => { + it("should return true if the object has the specified path", () => { + const lObject = { a: { b: { c: 123 } } }; + equal(has(lObject, "a.b.c"), true); + }); + + it("should return false if the object does not have the specified path", () => { + const lObject = { a: { b: { c: 123 } } }; + equal(has(lObject, "a.b.d"), false); + }); + }); + + describe("[U] get", () => { + it("should return the value at the specified path", () => { + const lObject = { a: { b: { c: 123 } } }; + equal(get(lObject, "a.b.c"), 123); + }); + + it("should return the default value if the path does not exist", () => { + const lObject = { a: { b: { c: 123 } } }; + equal(get(lObject, "a.b.d", "default"), "default"); + }); + + it("should return undefined if the path does not exist and no default value is provided", () => { + const lObject = { a: { b: { c: 123 } } }; + equal(get(lObject, "a.b.d"), undefined); + }); + }); + + describe("[U] set", () => { + it("should set the value at the specified path", () => { + const lObject = { a: { b: { c: 123 } } }; + set(lObject, "a.b.c", 456); + equal(lObject.a.b.c, 456); + }); + + it("should create nested objects if the path does not exist", () => { + const lObject = {}; + set(lObject, "a.b.c", 123); + equal(lObject.a.b.c, 123); + }); + }); +});