@@ -148,6 +148,70 @@ export function findVitestV5ConfigFiles(sources: ReadonlyMap<string, string>): S
148148 return files ;
149149}
150150
151+ function hasConfigMerge ( editor : SourceEditor ) : boolean {
152+ let found = false ;
153+ traverse ( editor . ast , {
154+ CallExpression ( p ) {
155+ if ( importedName ( p , p . node . callee , CONFIG_SOURCES ) === 'mergeConfig' ) {
156+ found = true ;
157+ }
158+ } ,
159+ } ) ;
160+ return found ;
161+ }
162+
163+ /** Defaults on one merge fragment can override explicit settings in another.
164+ * Include local imported configs so the preflight and finalization agree. */
165+ export function findVitestV5MergedConfigFiles (
166+ sources : ReadonlyMap < string , string > ,
167+ configFiles : ReadonlySet < string > ,
168+ ) : Set < string > {
169+ const merged = new Set < string > ( ) ;
170+ const imports = new Map < string , string [ ] > ( ) ;
171+ const extensions = [ '.ts' , '.mts' , '.cts' , '.js' , '.mjs' , '.cjs' , '.tsx' , '.jsx' ] ;
172+ for ( const file of configFiles ) {
173+ try {
174+ const editor = new SourceEditor ( file , sources . get ( file ) ! ) ;
175+ if ( hasConfigMerge ( editor ) ) {
176+ merged . add ( file ) ;
177+ }
178+ const dependencies : string [ ] = [ ] ;
179+ for ( const node of editor . ast . program . body ) {
180+ if (
181+ node . type !== 'ImportDeclaration' &&
182+ node . type !== 'ExportNamedDeclaration' &&
183+ node . type !== 'ExportAllDeclaration'
184+ ) {
185+ continue ;
186+ }
187+ const reference = node . source ?. value ;
188+ if ( ! reference ?. startsWith ( '.' ) ) {
189+ continue ;
190+ }
191+ const target = path . resolve ( path . dirname ( file ) , reference ) ;
192+ const dependency = [
193+ target ,
194+ target . replace ( / \. ( [ c m ] ? ) j s $ / , '.$1ts' ) ,
195+ ...extensions . map ( ( extension ) => `${ target } ${ extension } ` ) ,
196+ ...extensions . map ( ( extension ) => path . join ( target , `index${ extension } ` ) ) ,
197+ ] . find ( ( candidate ) => configFiles . has ( candidate ) ) ;
198+ if ( dependency ) {
199+ dependencies . push ( dependency ) ;
200+ }
201+ }
202+ imports . set ( file , dependencies ) ;
203+ } catch {
204+ // The normal config pass reports unsupported syntax.
205+ }
206+ }
207+ for ( const file of merged ) {
208+ for ( const dependency of imports . get ( file ) ?? [ ] ) {
209+ merged . add ( dependency ) ;
210+ }
211+ }
212+ return merged ;
213+ }
214+
151215interface BrowserTestScope {
152216 root ?: string ;
153217 include ?: string [ ] ;
@@ -373,9 +437,23 @@ export function resolveVitestV5BrowserModes(
373437 ) ;
374438}
375439
376- export function migrateVitestV5Config ( file : string , source : string , options : SourceOptions ) {
440+ export function migrateVitestV5Config (
441+ file : string ,
442+ source : string ,
443+ options : SourceOptions ,
444+ mergedConfig = false ,
445+ ) {
377446 const editor = new SourceEditor ( file , source ) ;
378447 const visited = new Set < t . ObjectExpression > ( ) ;
448+ const merged = mergedConfig || hasConfigMerge ( editor ) ;
449+ const preserveDefaults = options . preserveV4 && ! merged ;
450+ if ( merged && ( options . preserveV4 || options . reviewV4 ) ) {
451+ editor . report (
452+ undefined ,
453+ 'merged-config-defaults' ,
454+ 'Review the effective merged config before adding v4 defaults for clearMocks, browser locators, fake timers, reporters, and projects. Defaults were not added to config fragments because they can override explicit settings in another fragment.' ,
455+ ) ;
456+ }
379457
380458 function nested (
381459 object : t . ObjectExpression ,
@@ -442,7 +520,7 @@ export function migrateVitestV5Config(file: string, source: string, options: Sou
442520 }
443521 }
444522 }
445- if ( ! options . preserveV4 || ! [ 'json' , 'junit' ] . includes ( name . value ) ) {
523+ if ( ! preserveDefaults || ! [ 'json' , 'junit' ] . includes ( name . value ) ) {
446524 continue ;
447525 }
448526 if ( output && ( ! staticObject ( output . value ) || objectProperty ( output . value , name . value ) ) ) {
@@ -475,14 +553,21 @@ export function migrateVitestV5Config(file: string, source: string, options: Sou
475553 }
476554 }
477555
478- function testOptions ( test : t . ObjectExpression , inherits : boolean ) {
479- if ( options . preserveV4 && ! inherits ) {
556+ function testOptions (
557+ test : t . ObjectExpression ,
558+ inherits : boolean ,
559+ parentTest ?: t . ObjectExpression ,
560+ ) {
561+ if ( preserveDefaults && ! inherits ) {
480562 editor . add ( test , 'clearMocks' , 'false' ) ;
481563 }
482564 const browser = objectProperty ( test , 'browser' ) ;
483565 if ( browser && staticObject ( browser . value ) ) {
484566 const value = browser . value ;
485- if ( options . preserveV4 ) {
567+ // An inherited browser config already receives its defaults at the parent.
568+ // Do not replace its explicit or dynamic locator setting in the child.
569+ const inheritsBrowser = inherits && parentTest && objectProperty ( parentTest , 'browser' ) ;
570+ if ( preserveDefaults && ! inheritsBrowser ) {
486571 nested ( value , 'locators' , 'exact: false' , ( locators ) =>
487572 editor . add ( locators , 'exact' , 'false' ) ,
488573 ) ;
@@ -526,7 +611,7 @@ export function migrateVitestV5Config(file: string, source: string, options: Sou
526611 ) ;
527612 }
528613
529- if ( options . preserveV4 && options . temporalPolyfill ) {
614+ if ( preserveDefaults && options . temporalPolyfill ) {
530615 nested ( test , 'fakeTimers' , "toNotFake: ['Temporal']" , ( timers ) =>
531616 editor . add ( timers , 'toNotFake' , "['Temporal']" ) ,
532617 ) ;
@@ -545,7 +630,7 @@ export function migrateVitestV5Config(file: string, source: string, options: Sou
545630 }
546631 const thresholds = objectProperty ( coverage . value , 'thresholds' ) ;
547632 if (
548- options . preserveV4 &&
633+ preserveDefaults &&
549634 thresholds &&
550635 staticObject ( thresholds . value ) &&
551636 isTrue ( objectProperty ( thresholds . value , 'perFile' ) ?. value )
@@ -614,7 +699,7 @@ export function migrateVitestV5Config(file: string, source: string, options: Sou
614699 return ;
615700 }
616701 const hasInline = projects . value . elements . some ( ( item ) => item && item . type !== 'StringLiteral' ) ;
617- if ( hasInline && options . preserveV4 ) {
702+ if ( hasInline && preserveDefaults ) {
618703 editor . add ( test , 'sharedViteServer' , 'false' ) ;
619704 }
620705 for ( const project of projects . value . elements ) {
@@ -630,29 +715,29 @@ export function migrateVitestV5Config(file: string, source: string, options: Sou
630715 continue ;
631716 }
632717 const extendsValue = objectProperty ( project , 'extends' ) ;
633- if ( options . preserveV4 ) {
718+ if ( preserveDefaults ) {
634719 editor . add ( project , 'extends' , 'false' ) ;
635720 }
636- config ( project , isTrue ( extendsValue ?. value ) ) ;
721+ config ( project , isTrue ( extendsValue ?. value ) , test ) ;
637722 }
638723 }
639724
640- function config ( object : t . ObjectExpression , inherits = false ) {
725+ function config ( object : t . ObjectExpression , inherits = false , parentTest ?: t . ObjectExpression ) {
641726 if ( visited . has ( object ) ) {
642727 return ;
643728 }
644729 visited . add ( object ) ;
645730 const test = objectProperty ( object , 'test' ) ;
646731 if ( ! test ) {
647- if ( options . preserveV4 && ! inherits ) {
732+ if ( preserveDefaults && ! inherits ) {
648733 editor . add (
649734 object ,
650735 'test' ,
651736 `{ clearMocks: false${ options . temporalPolyfill ? ", fakeTimers: { toNotFake: ['Temporal'] }" : '' } }` ,
652737 ) ;
653738 }
654739 } else if ( staticObject ( test . value ) ) {
655- testOptions ( test . value , inherits ) ;
740+ testOptions ( test . value , inherits , parentTest ) ;
656741 } else {
657742 editor . report (
658743 test ,
0 commit comments