Skip to content

Commit 7293498

Browse files
authored
fix(devtools): remove the correct module in uninstallNuxtModule (#1019)
1 parent f71b190 commit 7293498

5 files changed

Lines changed: 64 additions & 274 deletions

File tree

‎packages/devtools/src/server-rpc/npm.ts‎

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import type { NpmCommandOptions, NpmCommandType, NuxtDevtoolsServerContext, Pack
33
import fs from 'node:fs/promises'
44
import { startSubprocess } from '@nuxt/devtools-kit'
55
import { parseModule } from 'magicast'
6-
import { addNuxtModule, getDefaultExportOptions } from 'magicast/helpers'
6+
import { addNuxtModule } from 'magicast/helpers'
77
import { detect } from 'package-manager-detector/detect'
88
import { checkForUpdateOf } from '../npm'
99
import { magicastGuard } from '../utils/magicast'
10+
import { removeNuxtModuleFromCode } from '../utils/nuxt-config'
1011

1112
export function setupNpmRPC({ nuxt }: NuxtDevtoolsServerContext) {
1213
let detectPromise: Promise<DetectResult | null> | undefined
@@ -145,20 +146,7 @@ export function setupNpmRPC({ nuxt }: NuxtDevtoolsServerContext) {
145146
const filepath = nuxt.options._nuxtConfigFile
146147
const source = await fs.readFile(filepath, 'utf-8')
147148
const generated = await magicastGuard(async () => {
148-
const mod = parseModule(source, { sourceFileName: filepath })
149-
150-
// TODO: remove module from config
151-
// removeNuxtModule(mod, name)
152-
const config = getDefaultExportOptions(mod)
153-
config.modules ||= []
154-
if (config.modules.includes(name)) {
155-
Object.values(config.modules).forEach((value, index) => {
156-
if (value === name)
157-
config.modules.splice(index - 1, 1)
158-
})
159-
}
160-
161-
return mod.generate().code
149+
return removeNuxtModuleFromCode(source, name, filepath)
162150
})
163151

164152
const processId = `nuxt:remove-module:${name}`
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { parseModule } from 'magicast'
2+
import { getDefaultExportOptions } from 'magicast/helpers'
3+
4+
/**
5+
* Return `source` with `name` removed from the Nuxt config `modules` array.
6+
* Handles both string entries (`'@nuxt/image'`) and tuple entries
7+
* (`['@nuxt/image', {...}]`). If the module isn't present, returns the code
8+
* unchanged.
9+
*/
10+
export function removeNuxtModuleFromCode(source: string, name: string, filepath?: string): string {
11+
const mod = parseModule(source, filepath ? { sourceFileName: filepath } : undefined)
12+
const config = getDefaultExportOptions(mod)
13+
if (!Array.isArray(config.modules))
14+
return mod.generate().code
15+
16+
// Splice matching entries out in place (reverse order so indices stay
17+
// valid as we remove) rather than reassigning a filtered array — magicast
18+
// regenerates a reassigned array from scratch and drops the surrounding
19+
// formatting/comments of the entries that remain.
20+
for (let i = config.modules.length - 1; i >= 0; i--) {
21+
const entry = config.modules[i]
22+
const matches = typeof entry === 'string'
23+
? entry === name
24+
: Array.isArray(entry) && entry[0] === name
25+
if (matches)
26+
config.modules.splice(i, 1)
27+
}
28+
29+
return mod.generate().code
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { removeNuxtModuleFromCode } from '../src/utils/nuxt-config'
3+
4+
const base = `export default defineNuxtConfig({\n modules: ['@nuxt/image', '@nuxt/content'],\n})\n`
5+
6+
describe('removeNuxtModuleFromCode', () => {
7+
it('removes the requested module and keeps the others', () => {
8+
const out = removeNuxtModuleFromCode(base, '@nuxt/image')
9+
expect(out).not.toContain('@nuxt/image')
10+
expect(out).toContain('@nuxt/content')
11+
})
12+
13+
it('removes the first module without dropping the last', () => {
14+
const out = removeNuxtModuleFromCode(base, '@nuxt/image')
15+
expect(out).toContain('@nuxt/content') // regression guard for the splice(-1) bug
16+
})
17+
18+
it('removes a module declared as a tuple with options', () => {
19+
const src = `export default defineNuxtConfig({\n modules: [['@nuxt/image', { quality: 80 }], '@nuxt/content'],\n})\n`
20+
const out = removeNuxtModuleFromCode(src, '@nuxt/image')
21+
expect(out).not.toContain('@nuxt/image')
22+
expect(out).toContain('@nuxt/content')
23+
})
24+
25+
it('is a no-op when the module is absent', () => {
26+
const out = removeNuxtModuleFromCode(base, '@nuxt/not-there')
27+
expect(out).toContain('@nuxt/image')
28+
expect(out).toContain('@nuxt/content')
29+
})
30+
})

‎plans/003-fix-uninstall-nuxt-module.md‎

Lines changed: 0 additions & 258 deletions
This file was deleted.

‎plans/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ when done.
1717
|------|-------|----------|--------|------------|--------|
1818
| 001 | Establish a Vitest unit-test baseline | P1 | S || DONE |
1919
| 002 | Add a root AGENTS.md | P2 | S || DONE |
20-
| 003 | Fix `uninstallNuxtModule` (removes wrong module → config data loss) | P1 | S | 001 | TODO |
20+
| 003 | Fix `uninstallNuxtModule` (removes wrong module → config data loss) | P1 | S | 001 | DONE |
2121
| 004 | Fix options RPC cache (inverted guard + shared-constant mutation) | P1 | S | 001 | DONE |
2222
| 005 | Harden assets RPC (path containment + collision-filename bug) | P1 | S–M | 001 | TODO |
2323
| 006 | Enforce storage denylist on item access (not just listing) | P1 | S | 001 | TODO |

0 commit comments

Comments
 (0)