Skip to content

chore: replace lodash with native utilities#8189

Open
jherr wants to merge 3 commits intomainfrom
chore/replace-lodash-with-native
Open

chore: replace lodash with native utilities#8189
jherr wants to merge 3 commits intomainfrom
chore/replace-lodash-with-native

Conversation

@jherr
Copy link
Copy Markdown
Contributor

@jherr jherr commented Apr 22, 2026

Summary

Removes lodash from the CLI's runtime dependencies in favor of tiny native implementations in src/utils/object-utilities.ts.

  • Replaces lodash/isEmpty, lodash/pick, lodash/merge, lodash/throttle, and lodash/isObject across 8 call sites.
  • Drops lodash and @types/lodash from package.json.
  • Adds JSDoc header blocks to each new helper documenting its intentionally narrow contract (leading-edge-only throttle, merge semantics that overwrite arrays and don't mutate inputs, etc.).
  • Adds unit tests at tests/unit/utils/object-utilities.test.ts covering nullish handling, falsy-value preservation, non-mutation, recursive merge, array overwrite, and throttle timing behavior (26 tests).

Part of the Milestone 1 dependency-pruning work from the CLI modernization RFC. node-fetch and backoff are deliberately left for follow-up PRs to keep this one reviewable.

Test plan

  • npm run build
  • npx tsc --project tsconfig.build.json --noEmit
  • npm run test:unit — 344 passed; the 5 failures (exec-fetcher, hash-fns) are pre-existing on main and unrelated (flaky CDN download in traffic-mesh fetch).
  • npm run test:integration — 545 passed; 5 flaky failures unrelated to this change (interactive completion prompts, Next.js/Hugo fixture tests, a dev plugin-hook test). None touch the lodash replacement call sites.
  • CI (unit + integration + e2e) green

Made with Cursor

jherr added 2 commits April 22, 2026 08:56
Replace lodash/isEmpty, lodash/pick, lodash/isObject, lodash/merge, and
lodash/throttle with lightweight native implementations in a new
src/utils/lang.ts module. Remove lodash and @types/lodash from
dependencies (~4.4 MB install size reduction).

Made-with: Cursor
Renames the new utility module to a more descriptive name, documents each
helper with a header block explaining its narrow contract vs lodash, and
adds unit tests covering the behaviors the CLI actually depends on
(nullish handling, falsy preservation, non-mutation, throttle leading-edge
semantics). Intended to set a baseline code-quality example for future
utility modules.

Made-with: Cursor
@jherr jherr requested a review from a team as a code owner April 22, 2026 17:06
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: dfd4afab-2a6d-413b-a324-e97e0973ba6a

📥 Commits

Reviewing files that changed from the base of the PR and between ee9a92e and 0da2a40.

📒 Files selected for processing (3)
  • src/commands/deploy/deploy.ts
  • src/utils/object-utilities.ts
  • tests/unit/utils/object-utilities.test.ts
✅ Files skipped from review due to trivial changes (2)
  • src/commands/deploy/deploy.ts
  • tests/unit/utils/object-utilities.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/utils/object-utilities.ts

📝 Walkthrough

Summary by CodeRabbit

  • Refactor

    • Replaced an external utility dependency with internal implementations to centralize common helpers and simplify maintenance.
  • Chores

    • Removed the now-unused external package from the project manifest.
  • Tests

    • Added comprehensive unit tests for the new internal utilities to validate behavior and prevent regressions.

Walkthrough

The PR removes lodash from dependencies and adds src/utils/object-utilities.ts exporting isEmpty, pick, deepMerge, and throttle. Code across multiple command and utility files replaces lodash imports with these local utilities; base-command.ts now uses deepMerge and deploy.ts uses a stricter runtime check for published_deploy.locked. A new unit test file tests/unit/utils/object-utilities.test.ts exercises the four utilities. package.json entries for lodash and @types/lodash are removed.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly summarizes the main change: replacing lodash with native utility implementations across the codebase.
Description check ✅ Passed The PR description comprehensively describes the changeset, detailing which lodash utilities were replaced, where they were replaced, the new utility module created, test coverage added, and the testing performed.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/replace-lodash-with-native

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 22, 2026

📊 Benchmark results

Comparing with cb924d4

  • Dependency count: 1,061 (no change)
  • Package size: 355 MB (no change)
  • Number of ts-expect-error directives: 356 (no change)

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/commands/base-command.ts (1)

193-207: ⚠️ Potential issue | 🟠 Major

Potential regression: existing auth.github.{user,token} will now be overwritten with undefined on every login.

lodash/merge skipped source properties whose value is undefined when a destination value existed, so the explicit github: { user: undefined, token: undefined } block was effectively a no-op when the user already had GitHub auth stored. The new deepMerge (see src/utils/object-utilities.ts L44–63) assigns undefined unconditionally, which will clear those fields on every storeToken call. See the root-cause comment on deepMerge for a proposed fix to restore lodash semantics.

If you prefer to fix it here instead, drop the explicit undefined fields:

🔧 Alternative: remove the no-op undefineds
   const userData = deepMerge(globalConfig.get(`users.${userId}`), {
     id: userId,
     name,
     email,
     auth: {
       token: accessToken,
-      github: {
-        user: undefined,
-        token: undefined,
-      },
     },
   })

(Only safe if clearing those fields was never the intent.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/base-command.ts` around lines 193 - 207, The deepMerge call in
base-command.ts is overwriting existing users.${userId}.auth.github.user and
.token with explicit undefined values; either fix deepMerge to preserve
destination values when source properties are undefined (restore lodash.merge
semantics in src/utils/object-utilities.ts) or remove the explicit github: {
user: undefined, token: undefined } from the deepMerge payload here (inside the
storeToken/update user flow) so existing GitHub auth fields are not cleared when
calling deepMerge/globalConfig.set for userData. Ensure the change targets the
deepMerge usage or the payload creation around userData to prevent unconditional
clearing of auth.github fields.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/commands/deploy/deploy.ts`:
- Around line 9-13: The import ordering is broken because the isEmpty import is
placed between third-party imports (inquirer and `@netlify/headers-parser`); move
the isEmpty import statement so it sits with the other local utility imports
(e.g., next to the ../../utils/command-helpers.js import) rather than between
third-party modules, then re-run the formatter/Prettier to ensure imports are
grouped and sorted correctly.

In `@src/utils/object-utilities.ts`:
- Around line 1-90: Prettier check failed for this file; run the project
formatter and commit the changes so formatting matches CI. Run the repo's format
script (e.g. npm run format or prettier --write) on
src/utils/object-utilities.ts, then review and stage the updated file (covering
exports isEmpty, pick, deepMerge, throttle) and push the formatted version so
prettier --check passes in CI.
- Around line 44-63: deepMerge currently overwrites existing values with
undefined from the source; update the deepMerge function to mirror lodash
_.merge semantics by skipping assignment when sourceVal is undefined (i.e., if
sourceVal === undefined and result already has a non-undefined value, do not set
result[key] = sourceVal), while preserving the recursive merge path for nested
plain objects; ensure this change fixes the storeToken call site behavior (where
github.user/github.token may be passed as undefined) so existing auth.github.*
values are not wiped out unintentionally.

In `@tests/unit/utils/object-utilities.test.ts`:
- Around line 137-147: The test "drops calls that arrive within the throttle
interval" currently only asserts before the interval expires and could pass with
a trailing-call implementation; update the test to advance timers past the
throttle interval after the extra calls and assert the original fn was not
called again (i.e., call throttle(fn, 100), invoke throttled(), advance 50ms,
invoke throttled() twice, then advance timers by at least the remaining 50ms or
slightly more and assert expect(fn).toHaveBeenCalledTimes(1)) to ensure no
trailing invocation is scheduled by the throttle implementation.

---

Outside diff comments:
In `@src/commands/base-command.ts`:
- Around line 193-207: The deepMerge call in base-command.ts is overwriting
existing users.${userId}.auth.github.user and .token with explicit undefined
values; either fix deepMerge to preserve destination values when source
properties are undefined (restore lodash.merge semantics in
src/utils/object-utilities.ts) or remove the explicit github: { user: undefined,
token: undefined } from the deepMerge payload here (inside the storeToken/update
user flow) so existing GitHub auth fields are not cleared when calling
deepMerge/globalConfig.set for userData. Ensure the change targets the deepMerge
usage or the payload creation around userData to prevent unconditional clearing
of auth.github fields.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 23910191-460e-4845-b600-0822211ac907

📥 Commits

Reviewing files that changed from the base of the PR and between cb924d4 and ee9a92e.

📒 Files selected for processing (11)
  • package.json
  • src/commands/base-command.ts
  • src/commands/deploy/deploy.ts
  • src/commands/init/init.ts
  • src/commands/link/link.ts
  • src/commands/sites/sites-create.ts
  • src/lib/extensions.ts
  • src/utils/dev.ts
  • src/utils/object-utilities.ts
  • src/utils/proxy.ts
  • tests/unit/utils/object-utilities.test.ts
💤 Files with no reviewable changes (1)
  • package.json

Comment thread src/commands/deploy/deploy.ts
Comment on lines +1 to +90
/**
* Checks whether a value is effectively empty.
*
* A value is considered empty when it is `null`, `undefined`, or an object
* with no own enumerable keys. Intended as a minimal stand-in for the handful
* of `lodash/isEmpty` call sites that only ever receive plain objects or
* nullish values (e.g. API response payloads that may be absent).
*
* @param obj - The object to check, or a nullish value.
* @returns `true` if the value is nullish or has no own enumerable keys.
*/
export const isEmpty = (obj: object | null | undefined): boolean => obj == null || Object.keys(obj).length === 0

/**
* Returns a new object containing only the specified keys from the source.
*
* Mirrors the shape of `lodash/pick` but is intentionally loosely typed so it
* can accept string keys that are not declared on the source type. This is
* pragmatic for CLI output filtering of API responses whose generated types
* sometimes omit fields the server actually returns.
*
* Keys that are not present on the source are silently skipped.
*
* @param obj - The source object to pick properties from.
* @param keys - The property names to include in the result.
* @returns A new object with only the requested keys that exist on the source.
*/
export const pick = <T extends object>(obj: T, keys: readonly string[]): Partial<T> =>
Object.fromEntries(keys.filter((k) => k in obj).map((k) => [k, (obj as Record<string, unknown>)[k]])) as Partial<T>

/**
* Recursively merges the properties of `source` into a copy of `target`.
*
* Only plain (non-array) objects are merged recursively; arrays and primitive
* values from `source` overwrite the corresponding value in `target`. Neither
* argument is mutated. Behavior is intentionally narrow: this is not a full
* replacement for `lodash/merge`, only enough to cover the existing call sites
* that merge shallow-nested config objects.
*
* @param target - The base object to merge into. `undefined` is treated as `{}`.
* @param source - The object whose properties take precedence over `target`.
* @returns A new object containing the combined properties.
*/
export function deepMerge<T extends Record<string, unknown>>(target: T | undefined, source: Record<string, unknown>): T {
const result: Record<string, unknown> = { ...(target ?? {}) }
for (const key of Object.keys(source)) {
const targetVal = result[key]
const sourceVal = source[key]
if (
targetVal != null &&
sourceVal != null &&
typeof targetVal === 'object' &&
typeof sourceVal === 'object' &&
!Array.isArray(targetVal) &&
!Array.isArray(sourceVal)
) {
result[key] = deepMerge(targetVal as Record<string, unknown>, sourceVal as Record<string, unknown>)
} else {
result[key] = sourceVal
}
}
return result as T
}

/**
* Wraps a function so that it runs at most once per `intervalMs` milliseconds.
*
* Uses leading-edge invocation: the first call runs immediately, and any calls
* that arrive within `intervalMs` of the previous invocation are dropped.
* There is no trailing call -- this is intentional for fire-and-forget
* side-effect callers (e.g. rate-limited activity pings) where losing the
* final invocation is acceptable.
*
* @param fn - The function to throttle.
* @param intervalMs - Minimum time between invocations, in milliseconds.
* @returns A throttled wrapper around `fn` with the same arguments signature.
*/
export function throttle<Args extends unknown[]>(
fn: (...args: Args) => void,
intervalMs: number,
): (...args: Args) => void {
let lastCall = 0
return (...args: Args) => {
const now = Date.now()
if (now - lastCall >= intervalMs) {
lastCall = now
fn(...args)
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Prettier --check is failing on this file.

The CI Format job reports formatting issues. Please run npm run format (or equivalent) before pushing so prettier --check passes.

🧰 Tools
🪛 GitHub Actions: Format

[warning] 1-1: Prettier --check reported formatting/style issues in this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/object-utilities.ts` around lines 1 - 90, Prettier check failed for
this file; run the project formatter and commit the changes so formatting
matches CI. Run the repo's format script (e.g. npm run format or prettier
--write) on src/utils/object-utilities.ts, then review and stage the updated
file (covering exports isEmpty, pick, deepMerge, throttle) and push the
formatted version so prettier --check passes in CI.

Comment thread src/utils/object-utilities.ts Outdated
Comment thread tests/unit/utils/object-utilities.test.ts Outdated
- deepMerge: skip undefined source values to match lodash.merge semantics.
  This preserves the storeToken call site in base-command.ts, which passes
  github.user/github.token as undefined and relied on the existing auth
  fields being left intact.
- Move the misplaced object-utilities import in deploy.ts into the local
  imports block.
- Run Prettier on the affected files to match CI formatting.
- Strengthen the throttle test to advance past the interval and assert no
  trailing invocation is scheduled.
- Update deepMerge tests: null still overwrites; undefined no longer does,
  plus coverage for the storeToken-style nested-undefined patch.

Made-with: Cursor
@jherr
Copy link
Copy Markdown
Contributor Author

jherr commented Apr 22, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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