Skip to content

Commit 14903e5

Browse files
committed
fix(module): keep the closing bracket when removing consecutive inline entries
1 parent dd68ab8 commit 14903e5

2 files changed

Lines changed: 54 additions & 6 deletions

File tree

packages/nuxt-cli/src/utils/config.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,14 @@ export async function removeNuxtConfigEntries(config: NuxtConfigFile, entries: C
150150
continue
151151
}
152152

153-
edits.push(...doomed.map(element => buildRemoval(source, array, element)))
153+
const removals = doomed.map(element => buildRemoval(source, array, element, doomed))
154154

155155
const orphanedComma = findOrphanedComma(source, array, doomed)
156156
if (orphanedComma !== undefined) {
157-
edits.push({ start: orphanedComma, end: orphanedComma + 1, text: '' })
157+
removals.push({ start: orphanedComma, end: orphanedComma + 1, text: '' })
158158
}
159+
160+
edits.push(...mergeRemovals(removals))
159161
}
160162

161163
if (!edits.length) {
@@ -165,6 +167,27 @@ export async function removeNuxtConfigEntries(config: NuxtConfigFile, entries: C
165167
await writeFile(config.file, applyEdits(source, edits), 'utf8')
166168
}
167169

170+
/**
171+
* Combine deletions that overlap into one range.
172+
*
173+
* Removing consecutive entries produces ranges that reach over each other,
174+
* because each entry takes the separator that attached it to the one before.
175+
* Applied separately they would delete text twice over.
176+
*/
177+
function mergeRemovals(removals: Edit[]): Edit[] {
178+
const sorted = [...removals].sort((a, b) => a.start - b.start)
179+
const merged: Edit[] = []
180+
for (const removal of sorted) {
181+
const previous = merged.at(-1)
182+
if (previous && removal.start <= previous.end) {
183+
previous.end = Math.max(previous.end, removal.end)
184+
continue
185+
}
186+
merged.push({ ...removal })
187+
}
188+
return merged
189+
}
190+
168191
/**
169192
* Offset of a separator that would be left dangling after a removal.
170193
*
@@ -207,11 +230,20 @@ function readNames(location: ConfigLocation, key: ConfigKey): string[] {
207230
return location.keys[key].elements.map(element => element.name).filter((name): name is string => name !== null)
208231
}
209232

210-
/** Splice edits into `source`, working backwards so earlier offsets stay valid. */
233+
/**
234+
* Splice edits into `source`, working backwards so earlier offsets stay valid.
235+
*
236+
* An edit reaching into one that has already been applied is clipped rather than
237+
* left to consume the text that moved into its range, which would corrupt the
238+
* file rather than simply misplacing a separator.
239+
*/
211240
function applyEdits(source: string, edits: Edit[]): string {
212241
let result = source
242+
let applied = source.length
213243
for (const edit of [...edits].sort((a, b) => b.start - a.start)) {
214-
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end)
244+
const end = Math.max(edit.start, Math.min(edit.end, applied))
245+
result = result.slice(0, edit.start) + edit.text + result.slice(end)
246+
applied = edit.start
215247
}
216248
return result
217249
}
@@ -293,7 +325,7 @@ function buildInsert(source: string, location: ConfigLocation, array: ArrayLocat
293325
return { start: close, end: close, text: separator + entries.join(', ') }
294326
}
295327

296-
function buildRemoval(source: string, location: ArrayLocation, element: ArrayElement): Edit {
328+
function buildRemoval(source: string, location: ArrayLocation, element: ArrayElement, doomed: ArrayElement[]): Edit {
297329
let start = element.start
298330
let end = element.end
299331
if (source[end] === ',') {
@@ -314,8 +346,12 @@ function buildRemoval(source: string, location: ArrayLocation, element: ArrayEle
314346
end++
315347
}
316348
if (source[end] === ']') {
349+
// The separator before the entry goes with it, but only back as far as an
350+
// entry that is staying: reaching into one that is also being removed would
351+
// overlap its own edit and swallow the text between them.
317352
const index = location.elements.indexOf(element)
318-
start = location.elements[index - 1]?.end ?? start
353+
const previous = location.elements.slice(0, index).filter(candidate => !doomed.includes(candidate)).at(-1)
354+
start = previous?.end ?? start
319355
}
320356
else if (source[end] === '\n' || source[end] === '\r') {
321357
// The entry ended the line, so take the space that separated it from the

packages/nuxt-cli/test/unit/utils/config.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,18 @@ describe.each([
221221
expect(result).toBe('export default defineNuxtConfig({\n modules: [\n \'b\',\n ],\n})\n')
222222
})
223223

224+
it('should remove consecutive trailing entries from an inline array', async () => {
225+
const result = await remove('export default defineNuxtConfig({ modules: [\'a\', \'b\', \'c\'] })\n', ['b', 'c'])
226+
227+
expect(result).toBe('export default defineNuxtConfig({ modules: [\'a\'] })\n')
228+
})
229+
230+
it('should remove consecutive trailing entries from an inline array with a trailing comma', async () => {
231+
const result = await remove('export default defineNuxtConfig({ modules: [\'a\', \'b\', \'c\',] })\n', ['b', 'c'])
232+
233+
expect(result).toBe('export default defineNuxtConfig({ modules: [\'a\'] })\n')
234+
})
235+
224236
it('should keep array-form entries that were not named', async () => {
225237
const result = await remove('export default defineNuxtConfig({\n modules: [\n [\'a\', { q: 1 }],\n \'b\',\n ],\n})\n', ['b'])
226238

0 commit comments

Comments
 (0)