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+
constremovals=doomed.map(element=>buildRemoval(source,array,element,doomed))
154154

155155
constorphanedComma=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
awaitwriteFile(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+
functionmergeRemovals(removals: Edit[]): Edit[]{
178+
constsorted=[...removals].sort((a,b)=>a.start-b.start)
179+
constmerged: Edit[]=[]
180+
for(constremovalofsorted){
181+
constprevious=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+
returnmerged
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
returnlocation.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
functionapplyEdits(source: string,edits: Edit[]): string{
212241
letresult=source
242+
letapplied=source.length
213243
for(consteditof[...edits].sort((a,b)=>b.start-a.start)){
214-
result=result.slice(0,edit.start)+edit.text+result.slice(edit.end)
244+
constend=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
returnresult
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-
functionbuildRemoval(source: string,location: ArrayLocation,element: ArrayElement): Edit{
328+
functionbuildRemoval(source: string,location: ArrayLocation,element: ArrayElement,doomed: ArrayElement[]): Edit{
297329
letstart=element.start
298330
letend=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
constindex=location.elements.indexOf(element)
318-
start=location.elements[index-1]?.end??start
353+
constprevious=location.elements.slice(0,index).filter(candidate=>!doomed.includes(candidate)).at(-1)
354+
start=previous?.end??start
319355
}
320356
elseif(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+
constresult=awaitremove('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+
constresult=awaitremove('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
constresult=awaitremove('export default defineNuxtConfig({\n modules: [\n [\'a\', { q: 1 }],\n \'b\',\n ],\n})\n',['b'])
226238

0 commit comments

Comments
 (0)