@@ -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+ */
211240function 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
0 commit comments