Skip to content

feat(module): add module remove command#1306

Merged
danielroe merged 9 commits into
nuxt:mainfrom
Flo0806:feat/module-remove
Jun 17, 2026
Merged

feat(module): add module remove command#1306
danielroe merged 9 commits into
nuxt:mainfrom
Flo0806:feat/module-remove

Conversation

@Flo0806

@Flo0806 Flo0806 commented May 7, 2026

Copy link
Copy Markdown
Member

🔗 Linked issue

Resolves: #1184

📚 Description

Added a module remove command to remove modules from Nuxts modules array, including uninstallation from package.json (along with peer dependencies, provided they are not required by other packages).

Possible params: skipConfig, skipUninstall

Tests

Added tests , similar to add command

@Flo0806 Flo0806 requested a review from danielroe as a code owner May 7, 2026 12:42
@pkg-pr-new

pkg-pr-new Bot commented May 7, 2026

Copy link
Copy Markdown
  • nuxt-cli-playground

    npm i https://pkg.pr.new/create-nuxt@1306
    
    npm i https://pkg.pr.new/nuxi@1306
    
    npm i https://pkg.pr.new/@nuxt/cli@1306
    

commit: 4e1983c

@codecov-commenter

codecov-commenter commented May 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.09497% with 41 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@fab0173). Learn more about missing BASE report.

Files with missing lines Patch % Lines
packages/nuxi/src/commands/module/remove.ts 76.58% 33 Missing and 4 partials ⚠️
packages/nuxi/src/commands/module/_utils.ts 81.25% 2 Missing and 1 partial ⚠️
packages/nuxi/src/commands/module/add.ts 75.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #1306   +/-   ##
=======================================
  Coverage        ?   55.43%           
=======================================
  Files           ?       50           
  Lines           ?     1398           
  Branches        ?      400           
=======================================
  Hits            ?      775           
  Misses          ?      513           
  Partials        ?      110           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@codspeed-hq

codspeed-hq Bot commented May 7, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 2 untouched benchmarks


Comparing Flo0806:feat/module-remove (4e1983c) with main (1c7c55c)

Open in CodSpeed

@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements the nuxi module remove command to uninstall Nuxt modules from projects. The change introduces four shared utility functions for dependency handling and workspace detection, refactors the existing add command to use these utilities, and implements the complete remove command. The remove command validates project configuration, resolves module names (aliases and npm package names) using the Nuxt Modules database, conditionally updates nuxt.config.ts with interactive module selection, computes orphaned peer dependencies, and uninstalls packages via the detected package manager (with pnpm workspace support). Comprehensive unit tests verify removal by alias and npm name, interactive picker selection, flag behavior, and orphaned peer handling across multiple scenarios.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Linked Issues check ❓ Inconclusive The PR implements the module remove functionality requested in #1184 by adding the command with config updates and package uninstallation. However, the comments indicate ongoing discussion about command naming convention that suggests the implementation may not fully align with current design decisions. Clarify whether the command structure (module remove vs. standalone remove) aligns with the final naming decision discussed in PR comments, as reviewers suggested changes to the command naming after the implementation was submitted.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding a new module remove command to the Nuxt CLI.
Description check ✅ Passed The description provides relevant context about the module remove command implementation, parameters, and test coverage, all directly related to the changeset.
Out of Scope Changes check ✅ Passed All changes are focused on implementing the module remove command and its utilities. Updates to add.ts are appropriate refactoring to share utility functions. No unrelated changes detected.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
packages/nuxi/test/unit/commands/module/remove.spec.ts (2)

27-53: 💤 Low value

runCommand and fetchModules spies are set at describe scope and never cleared

Both spies accumulate call counts across tests, and fetchModules won't re-read its mock implementation if it needs to change per-test. Since all four tests currently rely on the same stubs this doesn't cause failures, but it's fragile. Move the spy setup into beforeEach (or call .mockClear() / .mockReset() there alongside updateConfig and removeDependency).

♻️ Proposed fix
-describe('module remove', () => {
-  vi.spyOn(runCommands, 'runCommand').mockImplementation(vi.fn())
-  vi.spyOn(utils, 'fetchModules').mockResolvedValue([...])
-
-  beforeEach(() => {
-    updateConfig.mockClear()
-    removeDependency.mockClear()
-  })
+describe('module remove', () => {
+  beforeEach(() => {
+    updateConfig.mockClear()
+    removeDependency.mockClear()
+    vi.spyOn(runCommands, 'runCommand').mockImplementation(vi.fn())
+    vi.spyOn(utils, 'fetchModules').mockResolvedValue([...])
+  })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nuxi/test/unit/commands/module/remove.spec.ts` around lines 27 - 53,
The spies for runCommands.runCommand and utils.fetchModules are created at
describe scope and retain state between tests; move their setup into a
beforeEach (or call .mockClear()/.mockReset() there) so each test gets a fresh
spy and fetchModules can be re-mocked per-test; also ensure you clear mocks for
related functions like updateConfig and removeDependency in the same beforeEach
to avoid accumulated call counts and stale implementations when tests change
behavior.

7-7: 🏗️ Heavy lift

updateConfig mock bypasses onUpdate — the config-removal logic is entirely untested

The mock resolves immediately without ever calling the onUpdate callback. As a result, removedFromConfig stays empty in every test, readModuleName, the reverse-splice loop (lines 159-166 of remove.ts), and the multiselect picker path are never exercised. The tests only validate the uninstall-from-disk flow.

To test the config-update logic, the mock should capture and invoke the callback:

♻️ Proposed fix
-const updateConfig = vi.fn(() => Promise.resolve())
+const updateConfig = vi.fn(async (opts: { onUpdate?: (config: any) => Promise<void>, onCreate?: () => boolean | void }) => {
+  if (opts.onUpdate) {
+    await opts.onUpdate({ modules: ['@nuxt/content'] })
+  }
+})

Also applies to: 17-17

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nuxi/test/unit/commands/module/remove.spec.ts` at line 7, The test's
updateConfig mock currently resolves without invoking the module removal
callback, so the config-removal path (onUpdate callback) is never exercised;
change the vi.fn mock for updateConfig to accept the same args as the real
function and call the provided onUpdate callback with a simulated config and
return its result so removedFromConfig gets populated; specifically, update the
mock referenced as updateConfig in the spec to call the onUpdate param
(triggering readModuleName, the reverse-splice logic in remove.ts, and the
multiselect picker path) and return the resulting Promise so the tests exercise
both config-update and disk-uninstall flows.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/nuxi/src/commands/module/remove.ts`:
- Around line 127-166: The code assumes config.modules is always an array
causing a TypeError when it's undefined; ensure safe handling by normalizing
config.modules to an array before reading or mutating it (e.g., if
(!Array.isArray(config.modules)) config.modules = []), then build the present
list with readModuleName from config.modules, use modules.length / multiselect
logic as-is, and safely iterate backwards over config.modules to splice and push
into removedFromConfig; this keeps the rest of the flow (multiselect, isCancel,
toRemove, readModuleName, removedFromConfig) unchanged while preventing
undefined access.
- Around line 153-166: The removal logic compares resolved npm names in toRemove
to raw config entries from readModuleName, so alias entries like 'content' won't
match and remain in nuxt.config after uninstall; update removeModules to compare
resolved names by either (A) resolving each config entry with resolveModule (or
using modulesDB/installedNames) before checking (i.e., call resolveModule on the
value returned by readModuleName and compare that resolved name to toRemove) or
(B) populate toRemove with both the original and resolved package names so the
existing loop that uses readModuleName finds matches; update references to
toRemove, readModuleName, resolveModule, removeModules, and
modulesDB/installedNames accordingly.
- Around line 235-244: The removeDependency rejection is currently only logged
and the function returns true, causing the caller to still run prepare; change
the error handling in the removeDependency call so that after logging you either
re-throw the error or return false to propagate failure to the caller (so caller
honor prepare skip when ctx.args.skipUninstall is false), and update the calling
code that checks the boolean result to skip calling prepare when false; also
confirm nypm.removeDependency's signature — if it does not accept string[] then
iterate over allToRemove and call removeDependency(name, options) for each entry
(or use Promise.all on per-name calls) instead of passing the array. Ensure
references to removeDependency, prepare, ctx.args.skipUninstall and allToRemove
are updated accordingly.

---

Nitpick comments:
In `@packages/nuxi/test/unit/commands/module/remove.spec.ts`:
- Around line 27-53: The spies for runCommands.runCommand and utils.fetchModules
are created at describe scope and retain state between tests; move their setup
into a beforeEach (or call .mockClear()/.mockReset() there) so each test gets a
fresh spy and fetchModules can be re-mocked per-test; also ensure you clear
mocks for related functions like updateConfig and removeDependency in the same
beforeEach to avoid accumulated call counts and stale implementations when tests
change behavior.
- Line 7: The test's updateConfig mock currently resolves without invoking the
module removal callback, so the config-removal path (onUpdate callback) is never
exercised; change the vi.fn mock for updateConfig to accept the same args as the
real function and call the provided onUpdate callback with a simulated config
and return its result so removedFromConfig gets populated; specifically, update
the mock referenced as updateConfig in the spec to call the onUpdate param
(triggering readModuleName, the reverse-splice logic in remove.ts, and the
multiselect picker path) and return the resulting Promise so the tests exercise
both config-update and disk-uninstall flows.
🪄 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: 69293820-fddb-4912-b24a-6ce800fe37f7

📥 Commits

Reviewing files that changed from the base of the PR and between d6b1115 and e1f383d.

📒 Files selected for processing (3)
  • packages/nuxi/src/commands/module/index.ts
  • packages/nuxi/src/commands/module/remove.ts
  • packages/nuxi/test/unit/commands/module/remove.spec.ts

Comment thread packages/nuxi/src/commands/module/remove.ts Outdated
Comment thread packages/nuxi/src/commands/module/remove.ts
Comment thread packages/nuxi/src/commands/module/remove.ts Outdated
@danielroe

Copy link
Copy Markdown
Member

we've deprecated module add -> add so I'm not sure whether this would be a good idea

wdyt @atinux?

@atinux

atinux commented May 15, 2026

Copy link
Copy Markdown
Member

I guess it could be nuxt rm directly no?

@Flo0806

Flo0806 commented May 27, 2026

Copy link
Copy Markdown
Member Author

I'll change it to nuxt rm. But I think optional "remove" sounds good (more intuitive), isn't it? @danielroe @atinux

@atinux

atinux commented Jun 3, 2026

Copy link
Copy Markdown
Member

Good for me @Flo0806

@danielroe

Copy link
Copy Markdown
Member

nuxt remove seems better.

@danielroe danielroe changed the title feat(nuxi): add module remove command feat(module): add module remove command Jun 17, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
packages/nuxi/test/unit/commands/module/remove.spec.ts (1)

104-120: ⚡ Quick win

Add cancellation-path tests for interactive prompts.

Please add explicit tests where multiselect and confirm return Symbol.for('clack:cancel') (or mocked cancel symbol) to verify setup aborts and skips uninstall/update side effects on those branches.

Also applies to: 165-229

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nuxi/test/unit/commands/module/remove.spec.ts` around lines 104 -
120, Add new test cases to handle cancellation paths in the remove command
setup. Create additional test blocks where multiselect is mocked to return
Symbol.for('clack:cancel') (and similar for confirm when applicable) to verify
that when users cancel the interactive prompt, the setup method returns early
without executing the removeDependency call or other side effects like
updateConfig. These cancellation tests should follow the same pattern as the
existing test structure but verify the abort behavior instead of the successful
removal behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/nuxi/src/commands/module/_utils.ts`:
- Around line 183-186: The forwardCommandArgs function currently serializes
undefined and null values into command flags like --logLevel=undefined, which is
invalid. Add an additional filter condition in the chain to exclude entries
where the value (v) is undefined or null before the map operation that formats
the arguments as strings. This ensures only valid option values are forwarded to
runCommand invocations.

---

Nitpick comments:
In `@packages/nuxi/test/unit/commands/module/remove.spec.ts`:
- Around line 104-120: Add new test cases to handle cancellation paths in the
remove command setup. Create additional test blocks where multiselect is mocked
to return Symbol.for('clack:cancel') (and similar for confirm when applicable)
to verify that when users cancel the interactive prompt, the setup method
returns early without executing the removeDependency call or other side effects
like updateConfig. These cancellation tests should follow the same pattern as
the existing test structure but verify the abort behavior instead of the
successful removal behavior.
🪄 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: 5d777d25-340a-4298-8530-d0e93fb8b472

📥 Commits

Reviewing files that changed from the base of the PR and between ede7d76 and 4e1983c.

📒 Files selected for processing (4)
  • packages/nuxi/src/commands/module/_utils.ts
  • packages/nuxi/src/commands/module/add.ts
  • packages/nuxi/src/commands/module/remove.ts
  • packages/nuxi/test/unit/commands/module/remove.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/nuxi/src/commands/module/remove.ts

Comment on lines +183 to +186
export function forwardCommandArgs(args: Record<string, unknown>): string[] {
return Object.entries(args)
.filter(([k]) => k in cwdArgs || k in logLevelArgs)
.map(([k, v]) => `--${k}=${v}`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid forwarding undefined/null option values in chained args.

forwardCommandArgs currently serializes absent values into flags like --logLevel=undefined, which can leak invalid values into runCommand(...) invocations.

Suggested patch
 export function forwardCommandArgs(args: Record<string, unknown>): string[] {
   return Object.entries(args)
-    .filter(([k]) => k in cwdArgs || k in logLevelArgs)
-    .map(([k, v]) => `--${k}=${v}`)
+    .filter(([k, v]) =>
+      (Object.prototype.hasOwnProperty.call(cwdArgs, k)
+        || Object.prototype.hasOwnProperty.call(logLevelArgs, k))
+      && v !== undefined
+      && v !== null,
+    )
+    .map(([k, v]) => `--${k}=${String(v)}`)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/nuxi/src/commands/module/_utils.ts` around lines 183 - 186, The
forwardCommandArgs function currently serializes undefined and null values into
command flags like --logLevel=undefined, which is invalid. Add an additional
filter condition in the chain to exclude entries where the value (v) is
undefined or null before the map operation that formats the arguments as
strings. This ensures only valid option values are forwarded to runCommand
invocations.

@danielroe danielroe merged commit a1a7653 into nuxt:main Jun 17, 2026
15 checks passed
@github-actions github-actions Bot mentioned this pull request Jun 17, 2026
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.

[Feature Request]: Support for extra nuxt module commands

4 participants