@@ -3,6 +3,7 @@ import os from 'node:os';
33import path from 'node:path' ;
44
55import * as prompts from '@voidzero-dev/vite-plus-prompts' ;
6+ import { parse as parseJsonc } from 'jsonc-parser' ;
67import { afterEach , describe , expect , it , vi } from 'vitest' ;
78
89import { detectExistingEditors , selectEditors , writeEditorConfigs } from '../editor.js' ;
@@ -165,9 +166,13 @@ describe('writeEditorConfigs', () => {
165166 extraVsCodeSettings : { 'npm.scriptRunner' : 'vp' } ,
166167 } ) ;
167168
168- const settings = JSON . parse (
169- fs . readFileSync ( path . join ( projectRoot , '.vscode' , 'settings.json' ) , 'utf8' ) ,
170- ) as Record < string , unknown > ;
169+ const resultText = fs . readFileSync ( path . join ( projectRoot , '.vscode' , 'settings.json' ) , 'utf8' ) ;
170+
171+ // Comments survive the merge
172+ expect ( resultText ) . toContain ( '// JSONC comment' ) ;
173+ expect ( resultText ) . toContain ( '// preserve existing key' ) ;
174+
175+ const settings = parseJsonc ( resultText ) as Record < string , unknown > ;
171176
172177 // Existing key is preserved (merge never overwrites)
173178 expect ( settings [ 'editor.formatOnSave' ] ) . toBe ( false ) ;
@@ -188,6 +193,252 @@ describe('writeEditorConfigs', () => {
188193 expect ( codeActions [ 'source.fixAll.oxc' ] ) . toBe ( 'explicit' ) ;
189194 } ) ;
190195
196+ it ( 'preserves a top-level comment before an existing setting (Vue Core style)' , async ( ) => {
197+ const projectRoot = createTempDir ( ) ;
198+
199+ const vscodeDir = path . join ( projectRoot , '.vscode' ) ;
200+ fs . mkdirSync ( vscodeDir , { recursive : true } ) ;
201+ fs . writeFileSync (
202+ path . join ( vscodeDir , 'settings.json' ) ,
203+ `{
204+ // Use the project's typescript version
205+ "typescript.tsdk": "node_modules/typescript/lib"
206+ }
207+ ` ,
208+ 'utf8' ,
209+ ) ;
210+
211+ await writeEditorConfigs ( {
212+ projectRoot,
213+ editorId : 'vscode' ,
214+ interactive : false ,
215+ silent : true ,
216+ } ) ;
217+
218+ const resultText = fs . readFileSync ( path . join ( projectRoot , '.vscode' , 'settings.json' ) , 'utf8' ) ;
219+
220+ expect ( resultText ) . toContain ( "// Use the project's typescript version" ) ;
221+
222+ const settings = parseJsonc ( resultText ) as Record < string , unknown > ;
223+ expect ( settings [ 'typescript.tsdk' ] ) . toBe ( 'node_modules/typescript/lib' ) ;
224+ expect ( settings [ 'editor.defaultFormatter' ] ) . toBe ( 'oxc.oxc-vscode' ) ;
225+ } ) ;
226+
227+ it ( 'preserves a nested comment while adding source.fixAll.oxc' , async ( ) => {
228+ const projectRoot = createTempDir ( ) ;
229+
230+ const vscodeDir = path . join ( projectRoot , '.vscode' ) ;
231+ fs . mkdirSync ( vscodeDir , { recursive : true } ) ;
232+ fs . writeFileSync (
233+ path . join ( vscodeDir , 'settings.json' ) ,
234+ `{
235+ "editor.codeActionsOnSave": {
236+ // keep my organize imports
237+ "source.organizeImports": "explicit"
238+ }
239+ }
240+ ` ,
241+ 'utf8' ,
242+ ) ;
243+
244+ await writeEditorConfigs ( {
245+ projectRoot,
246+ editorId : 'vscode' ,
247+ interactive : false ,
248+ silent : true ,
249+ } ) ;
250+
251+ const resultText = fs . readFileSync ( path . join ( projectRoot , '.vscode' , 'settings.json' ) , 'utf8' ) ;
252+
253+ expect ( resultText ) . toContain ( '// keep my organize imports' ) ;
254+
255+ const settings = parseJsonc ( resultText ) as Record < string , unknown > ;
256+ const codeActions = settings [ 'editor.codeActionsOnSave' ] as Record < string , unknown > ;
257+ expect ( codeActions [ 'source.organizeImports' ] ) . toBe ( 'explicit' ) ;
258+ expect ( codeActions [ 'source.fixAll.oxc' ] ) . toBe ( 'explicit' ) ;
259+ } ) ;
260+
261+ it ( 'never overwrites existing values during merge' , async ( ) => {
262+ const projectRoot = createTempDir ( ) ;
263+
264+ const vscodeDir = path . join ( projectRoot , '.vscode' ) ;
265+ fs . mkdirSync ( vscodeDir , { recursive : true } ) ;
266+ fs . writeFileSync (
267+ path . join ( vscodeDir , 'settings.json' ) ,
268+ `{
269+ "editor.formatOnSave": false,
270+ "[typescript]": {
271+ "editor.defaultFormatter": "esbenp.prettier-vscode"
272+ }
273+ }
274+ ` ,
275+ 'utf8' ,
276+ ) ;
277+
278+ await writeEditorConfigs ( {
279+ projectRoot,
280+ editorId : 'vscode' ,
281+ interactive : false ,
282+ silent : true ,
283+ } ) ;
284+
285+ const settings = parseJsonc (
286+ fs . readFileSync ( path . join ( projectRoot , '.vscode' , 'settings.json' ) , 'utf8' ) ,
287+ ) as Record < string , unknown > ;
288+
289+ expect ( settings [ 'editor.formatOnSave' ] ) . toBe ( false ) ;
290+ expect ( settings [ '[typescript]' ] ) . toEqual ( {
291+ 'editor.defaultFormatter' : 'esbenp.prettier-vscode' ,
292+ } ) ;
293+ } ) ;
294+
295+ it ( 'keeps trailing-comma JSONC valid after merge' , async ( ) => {
296+ const projectRoot = createTempDir ( ) ;
297+
298+ const vscodeDir = path . join ( projectRoot , '.vscode' ) ;
299+ fs . mkdirSync ( vscodeDir , { recursive : true } ) ;
300+ fs . writeFileSync (
301+ path . join ( vscodeDir , 'settings.json' ) ,
302+ `{
303+ "editor.codeActionsOnSave": {
304+ "source.organizeImports": "explicit",
305+ },
306+ }
307+ ` ,
308+ 'utf8' ,
309+ ) ;
310+
311+ await writeEditorConfigs ( {
312+ projectRoot,
313+ editorId : 'vscode' ,
314+ interactive : false ,
315+ silent : true ,
316+ } ) ;
317+
318+ const settings = parseJsonc (
319+ fs . readFileSync ( path . join ( projectRoot , '.vscode' , 'settings.json' ) , 'utf8' ) ,
320+ ) as Record < string , unknown > ;
321+
322+ expect ( settings [ 'editor.defaultFormatter' ] ) . toBe ( 'oxc.oxc-vscode' ) ;
323+ const codeActions = settings [ 'editor.codeActionsOnSave' ] as Record < string , unknown > ;
324+ expect ( codeActions [ 'source.organizeImports' ] ) . toBe ( 'explicit' ) ;
325+ expect ( codeActions [ 'source.fixAll.oxc' ] ) . toBe ( 'explicit' ) ;
326+ } ) ;
327+
328+ it ( 'appends extension recommendation without losing comments or duplicating' , async ( ) => {
329+ const projectRoot = createTempDir ( ) ;
330+
331+ const vscodeDir = path . join ( projectRoot , '.vscode' ) ;
332+ fs . mkdirSync ( vscodeDir , { recursive : true } ) ;
333+ fs . writeFileSync (
334+ path . join ( vscodeDir , 'extensions.json' ) ,
335+ `{
336+ "recommendations": [
337+ // keep my favorite extension
338+ "dbaeumer.vscode-eslint",
339+ ],
340+ }
341+ ` ,
342+ 'utf8' ,
343+ ) ;
344+
345+ await writeEditorConfigs ( {
346+ projectRoot,
347+ editorId : 'vscode' ,
348+ interactive : false ,
349+ silent : true ,
350+ } ) ;
351+
352+ const resultText = fs . readFileSync (
353+ path . join ( projectRoot , '.vscode' , 'extensions.json' ) ,
354+ 'utf8' ,
355+ ) ;
356+
357+ expect ( resultText ) . toContain ( '// keep my favorite extension' ) ;
358+
359+ const extensions = parseJsonc ( resultText ) as { recommendations : string [ ] } ;
360+ expect ( extensions . recommendations ) . toContain ( 'dbaeumer.vscode-eslint' ) ;
361+ expect (
362+ extensions . recommendations . filter ( ( r ) => r === 'VoidZero.vite-plus-extension-pack' ) ,
363+ ) . toHaveLength ( 1 ) ;
364+ } ) ;
365+
366+ it ( 'is idempotent: a second merge makes no textual change' , async ( ) => {
367+ const projectRoot = createTempDir ( ) ;
368+
369+ const vscodeDir = path . join ( projectRoot , '.vscode' ) ;
370+ fs . mkdirSync ( vscodeDir , { recursive : true } ) ;
371+ fs . writeFileSync (
372+ path . join ( vscodeDir , 'settings.json' ) ,
373+ `{
374+ // JSONC comment
375+ "editor.formatOnSave": false,
376+ }
377+ ` ,
378+ 'utf8' ,
379+ ) ;
380+
381+ const settingsPath = path . join ( projectRoot , '.vscode' , 'settings.json' ) ;
382+
383+ await writeEditorConfigs ( {
384+ projectRoot,
385+ editorId : 'vscode' ,
386+ interactive : false ,
387+ silent : true ,
388+ } ) ;
389+ const afterFirst = fs . readFileSync ( settingsPath , 'utf8' ) ;
390+
391+ await writeEditorConfigs ( {
392+ projectRoot,
393+ editorId : 'vscode' ,
394+ interactive : false ,
395+ silent : true ,
396+ } ) ;
397+ const afterSecond = fs . readFileSync ( settingsPath , 'utf8' ) ;
398+
399+ expect ( afterSecond ) . toBe ( afterFirst ) ;
400+ } ) ;
401+
402+ it ( 'preserves an existing Zed JSONC comment while adding nested settings' , async ( ) => {
403+ const projectRoot = createTempDir ( ) ;
404+
405+ const zedDir = path . join ( projectRoot , '.zed' ) ;
406+ fs . mkdirSync ( zedDir , { recursive : true } ) ;
407+ fs . writeFileSync (
408+ path . join ( zedDir , 'settings.json' ) ,
409+ `{
410+ // my zed settings
411+ "lsp": {
412+ "oxlint": {
413+ // keep this comment
414+ "initialization_options": {}
415+ }
416+ }
417+ }
418+ ` ,
419+ 'utf8' ,
420+ ) ;
421+
422+ await writeEditorConfigs ( {
423+ projectRoot,
424+ editorId : 'zed' ,
425+ interactive : false ,
426+ silent : true ,
427+ } ) ;
428+
429+ const resultText = fs . readFileSync ( path . join ( projectRoot , '.zed' , 'settings.json' ) , 'utf8' ) ;
430+
431+ expect ( resultText ) . toContain ( '// my zed settings' ) ;
432+ expect ( resultText ) . toContain ( '// keep this comment' ) ;
433+
434+ const settings = parseJsonc ( resultText ) as {
435+ lsp ?: { oxfmt ?: { initialization_options ?: { settings ?: Record < string , unknown > } } } ;
436+ } ;
437+ expect ( settings . lsp ?. oxfmt ?. initialization_options ?. settings ?. [ 'fmt.configPath' ] ) . toBe (
438+ './vite.config.ts' ,
439+ ) ;
440+ } ) ;
441+
191442 it ( 'does not apply extraVsCodeSettings to zed editor' , async ( ) => {
192443 const projectRoot = createTempDir ( ) ;
193444
0 commit comments