Skip to content

fix(deps): remove lodash runtime dependency - #483

Draft
joris974 wants to merge 1 commit into
mainfrom
fix/remove-lodash
Draft

fix(deps): remove lodash runtime dependency#483
joris974 wants to merge 1 commit into
mainfrom
fix/remove-lodash

Conversation

@joris974

Copy link
Copy Markdown
Member

Generated from the Dead Weight audit of 31 active Freckle JS/TS repos (sequence step 4: trivial lodash removals).

lodash was in dependencies — a runtime dependency of a published package — to support one expression of shipped code. The other three usages were test-only.

Per-call-site replacement

1. src/use-extra-deps/index.ts:70 — the only shipped usage

-    extraDepValues: mapValues(extraDeps, ({value}) => value) as T
+    extraDepValues: Object.fromEntries(
+      Object.entries(extraDeps).map(([key, {value}]) => [key, value])
+    ) as T

extraDeps is always a plain object literal at every call site, and the iteratee is a destructuring arrow function — no lodash-specific collection or shorthand behavior in play. Object.entries/fromEntries visit own enumerable keys, matching mapValues' baseForOwn. The pre-existing as T cast (needed because the parameter is a mapped conditional type) is unchanged.

🔎 One thing worth a reviewer's call: Object.fromEntries is ES2019, and tsconfig.json still declares "target": "ES2015". It type-checks (TypeScript 6 resolves lib up through ES2019 here — I verified .at(), which is ES2022, does fail while fromEntries passes), and tsc emits builtins verbatim rather than downleveling them, so the published dist/ calls Object.fromEntries directly. Every browser that can run this package's react >= 19.2.8 peer dependency supports it (Chrome 73+, Safari 12.1+, FF 63+), so I judged this safe — but if you want to hold the ES2015 line strictly, say so and I'll switch it to an Object.keys loop.

2. src/use-extra-deps/index.test.tsx:14 — test only

-      symbol = last(allDeps)
+      symbol = allDeps[allDeps.length - 1]

Not .at(-1): Array.prototype.at is ES2022 and is not in this project's resolved lib, so it fails to compile (TS2550). Index arithmetic matches lodash/last exactly, including returning undefined on an empty array.

3 & 4. src/use-safe-effect/index.test.tsx:225 and src/use-safe-imperative-handle/index.test.tsx:234 — test only

           // Deep comparison of arrays
-          p2: {value: p2, comparator: (a, b) => isEqual(a, b)}
+          p2: {value: p2, comparator: stringArraysEqual}

with, in each file:

// Element-wise comparison of the `Array<string>` values compared below. For an
// array of strings this is exactly the deep comparison `lodash/isEqual` did.
const stringArraysEqual = (a: Array<string>, b: Array<string>): boolean =>
  a.length === b.length && a.every((x, i) => x === b[i])

This is deliberately not a hand-rolled generic deep-equal. In both files the comparator is only ever applied to a p2 declared Array<string>, and for an array of strings element-wise comparison is deep equality — the type system guarantees there is no nesting to recurse into.

I looked for the runner's own deep-equality first, as the safer option. This repo is a Jest holdout ("test": "jest", ts-jest preset), so there is no vitest expect.equals available, and Jest 30 does not export a boolean deep-equal on the public expect surface — its equals is only reachable as this.equals inside a custom matcher or by importing the undeclared transitive @jest/expect-utils. Wrapping expect(a).toEqual(b) in try/catch would work but pollutes assertion counts inside a render callback. The four-line local helper, scoped to the test file, is the cleaner trade.

Behavior is unchanged: the tests still distinguish ['a', 'b'] from ['b', 'a'] (order-sensitive) and still treat two structurally-equal arrays at different memory locations as equal — which is the whole point of those two cases.

Note on lodash in the lockfile

For this repo lodash genuinely does leave yarn.lock — it has no @freckle/maybe or @freckle/non-empty dependency to pull it back in. The remaining lodash-ish lockfile matches are lodash.debounce and lodash.memoize, which are separate packages arriving transitively.

Worth stating for the sweep as a whole, though: in the sibling PRs (ajax-js, query-params-js) lodash does stay in the lockfile, because @freckle/maybe and @freckle/non-empty depend on it directly. Those PRs remove each repo's own direct dependency and usage, not the lockfile entry.

Verification

Package manager respected: yarn@1.22.22 (Yarn Classic) via corepack yarn, and the v1 lockfile format is preserved.

yarn build (tsc -d):

$ tsc -d
Done in 0.86s.

yarn lint (eslint, cache cleared first):

$ eslint src --cache
Done in 1.69s.

yarn test (Jest):

Test Suites: 5 passed, 5 total
Tests:       21 passed, 21 total
Snapshots:   0 total
Time:        1.755 s
Ran all test suites.

yarn run check-git-clean, re-run after committing the rebuilt dist/:

$ ./check-git-clean.sh
Done in 0.24s.

The regenerated dist/use-extra-deps/index.js is committed — the lodash require and the __importDefault interop helper are both gone from the build output.

On yarn format: running it reformats three files with pre-existing prettier drift unrelated to lodash (the PrimitiveDep union and ExtraDeps indentation in use-extra-deps/index.ts, a curried-arrow wrap in use-safe-imperative-handle/index.test.tsx:158, and { p1 } spacing in use-safe-callback/index.test.tsx:27). format is not run in CI, so this backlog has accumulated. I reverted all of it to keep this diff scoped to the dependency removal — the code I added is prettier-clean on its own. The backlog is worth its own PR.

🤖 Generated with Claude Code

lodash sat in `dependencies` for a single expression of shipped code.
Four usages, one of them shipped:

- `src/use-extra-deps/index.ts` (shipped): `mapValues(extraDeps, ({value}) => value)`
  becomes `Object.fromEntries(Object.entries(extraDeps).map(...))`. The
  existing `as T` cast is unchanged.
- `src/use-extra-deps/index.test.tsx`: `last(allDeps)` becomes
  `allDeps[allDeps.length - 1]`. `Array.prototype.at` is ES2022 and this
  package's `lib` only reaches ES2019, so `.at(-1)` does not compile.
- `src/use-safe-effect/index.test.tsx` and
  `src/use-safe-imperative-handle/index.test.tsx`: the `isEqual` deep
  comparator is only ever applied to `Array<string>`, for which an
  element-wise comparison is deep equality. Each file gets a four-line
  local `stringArraysEqual` rather than a hand-rolled generic deep equal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant