@@ -931,8 +931,9 @@ async function rewriteVitestImports(leafDepToVendorPath: Map<string, string>) {
931931 vitest : resolve ( distDir , 'index.js' ) ,
932932 'vitest/node' : resolve ( distDir , 'node.js' ) ,
933933 'vitest/config' : resolve ( distDir , 'config.js' ) ,
934- // vitest/browser exports page, server, etc from @vitest/browser
935- 'vitest/browser' : resolve ( distDir , '@vitest/browser/index.js' ) ,
934+ // vitest/browser exports page, server, CDPSession, BrowserCommands, etc from @vitest/browser/context
935+ // This matches vitest's own package.json exports: "./browser" -> "./browser/context.d.ts"
936+ 'vitest/browser' : resolve ( distDir , '@vitest/browser/context.js' ) ,
936937 // vitest/internal/browser exports browser-safe __INTERNAL and stringify (NOT @vitest/browser/index.js which has Node.js code)
937938 'vitest/internal/browser' : resolve ( distDir , 'browser.js' ) ,
938939 'vitest/runners' : resolve ( distDir , 'runners.js' ) ,
@@ -2064,7 +2065,7 @@ export * from '../dist/@vitest/browser/context.d.ts'
20642065}
20652066
20662067/**
2067- * Patch module augmentations in global.d.*.d.ts files to use relative paths .
2068+ * Patch module augmentations in global.d.*.d.ts files.
20682069 *
20692070 * The original vitest types use module augmentation like:
20702071 * declare module "@vitest/expect" { interface Assertion<T> { toMatchSnapshot: ... } }
@@ -2073,12 +2074,11 @@ export * from '../dist/@vitest/browser/context.d.ts'
20732074 * "@vitest/expect" doesn't exist as a package for consumers. This breaks the
20742075 * module augmentation - TypeScript can't find @vitest/expect to augment.
20752076 *
2076- * The fix: Change module augmentation to use relative paths that TypeScript CAN resolve:
2077- * declare module "../@vitest/expect/index.js" { ... }
2078- *
2079- * This makes TypeScript augment the same module that our index.d.ts imports from,
2080- * so the augmented properties (toMatchSnapshot, toMatchInlineSnapshot, etc.)
2081- * appear on the Assertion type that consumers import.
2077+ * The fix has two parts:
2078+ * 1. Change module augmentation to use relative paths that TypeScript CAN resolve:
2079+ * declare module "../@vitest/expect/index.js" { ... }
2080+ * 2. Merge augmented interface/type definitions into the target .d.ts files so that
2081+ * downstream DTS bundlers (rolldown) can resolve them without cross-file augmentation.
20822082 */
20832083async function patchModuleAugmentations ( ) {
20842084 console . log ( '\nPatching module augmentations in global.d.*.d.ts files...' ) ;
@@ -2096,31 +2096,180 @@ async function patchModuleAugmentations() {
20962096 return ;
20972097 }
20982098
2099- // Module augmentation mappings: bare specifier -> relative path from chunks/
2100- const augmentationMappings : Record < string , string > = {
2101- '@vitest/expect' : '../@vitest/expect/index.js' ,
2102- '@vitest/runner' : '../@vitest/runner/index.js' ,
2099+ // Module augmentation mappings: bare specifier -> [relative path, target .d.ts file]
2100+ const augmentationMappings : Record < string , { relativePath : string ; targetFile : string } > = {
2101+ '@vitest/expect' : {
2102+ relativePath : '../@vitest/expect/index.js' ,
2103+ targetFile : join ( distDir , '@vitest/expect/index.d.ts' ) ,
2104+ } ,
2105+ '@vitest/runner' : {
2106+ relativePath : '../@vitest/runner/index.js' ,
2107+ targetFile : join ( distDir , '@vitest/runner/utils.d.ts' ) ,
2108+ } ,
21032109 } ;
21042110
21052111 for ( const file of globalDtsFiles ) {
21062112 let content = await readFile ( file , 'utf-8' ) ;
21072113 let modified = false ;
21082114
2109- for ( const [ bareSpecifier , relativePath ] of Object . entries ( augmentationMappings ) ) {
2115+ for ( const [ bareSpecifier , { relativePath, targetFile } ] of Object . entries (
2116+ augmentationMappings ,
2117+ ) ) {
21102118 const oldPattern = `declare module "${ bareSpecifier } "` ;
2111- const newPattern = `declare module "${ relativePath } "` ;
21122119
2113- if ( content . includes ( oldPattern ) ) {
2114- content = content . replaceAll ( oldPattern , newPattern ) ;
2115- modified = true ;
2116- console . log ( ` Patched: ${ bareSpecifier } -> ${ relativePath } in ${ basename ( file ) } ` ) ;
2120+ // Extract the augmentation block content using brace matching
2121+ const startIdx = content . indexOf ( oldPattern ) ;
2122+ const braceStart = startIdx !== - 1 ? content . indexOf ( '{' , startIdx ) : - 1 ;
2123+ if ( braceStart === - 1 ) {
2124+ continue ;
2125+ }
2126+
2127+ let depth = 0 ;
2128+ let braceEnd = - 1 ;
2129+ for ( let i = braceStart ; i < content . length ; i ++ ) {
2130+ if ( content [ i ] === '{' ) {
2131+ depth ++ ;
2132+ } else if ( content [ i ] === '}' ) {
2133+ depth -- ;
2134+ if ( depth === 0 ) {
2135+ braceEnd = i ;
2136+ break ;
2137+ }
2138+ }
2139+ }
2140+ if ( braceEnd === - 1 ) {
2141+ continue ;
2142+ }
2143+
2144+ const innerContent = content . slice ( braceStart + 1 , braceEnd ) . trim ( ) ;
2145+
2146+ // Merge only NEW type declarations into the target .d.ts file.
2147+ // Interfaces that already exist (e.g., ExpectStatic, Assertion, MatcherState) must NOT
2148+ // be re-declared, as that would shadow extends clauses and break call signatures.
2149+ if ( innerContent && existsSync ( targetFile ) ) {
2150+ let targetContent = await readFile ( targetFile , 'utf-8' ) ;
2151+
2152+ // Extract individual interface blocks from the augmentation content
2153+ const interfaceRegex = / (?: e x p o r t \s + ) ? i n t e r f a c e \s + ( \w + ) (?: < [ ^ > ] * > ) ? \s * \{ / g;
2154+ let match ;
2155+ const newDeclarations : string [ ] = [ ] ;
2156+
2157+ while ( ( match = interfaceRegex . exec ( innerContent ) ) !== null ) {
2158+ const name = match [ 1 ] ;
2159+ // Only merge if this interface does NOT already exist in the target file.
2160+ // Check both direct declarations (interface Name) and re-exports (export type { Name }).
2161+ const hasDirectDecl = new RegExp ( `\\binterface\\s+${ name } \\b` ) . test ( targetContent ) ;
2162+ const exportTypeMatch = targetContent . match ( / e x p o r t \s + t y p e \s * \{ ( [ ^ } ] * ) \} / ) ;
2163+ const isReExported =
2164+ exportTypeMatch != null && new RegExp ( `\\b${ name } \\b` ) . test ( exportTypeMatch [ 1 ] ) ;
2165+ if ( hasDirectDecl || isReExported ) {
2166+ console . log (
2167+ ` Skipped existing interface "${ name } " (already in ${ basename ( targetFile ) } )` ,
2168+ ) ;
2169+ continue ;
2170+ }
2171+
2172+ // Extract this interface block using brace matching
2173+ const ifaceStart = match . index ;
2174+ const ifaceBraceStart = innerContent . indexOf ( '{' , ifaceStart ) ;
2175+ let ifaceDepth = 0 ;
2176+ let ifaceBraceEnd = - 1 ;
2177+ for ( let i = ifaceBraceStart ; i < innerContent . length ; i ++ ) {
2178+ if ( innerContent [ i ] === '{' ) {
2179+ ifaceDepth ++ ;
2180+ } else if ( innerContent [ i ] === '}' ) {
2181+ ifaceDepth -- ;
2182+ if ( ifaceDepth === 0 ) {
2183+ ifaceBraceEnd = i ;
2184+ break ;
2185+ }
2186+ }
2187+ }
2188+ if ( ifaceBraceEnd === - 1 ) {
2189+ continue ;
2190+ }
2191+
2192+ let block = innerContent . slice ( ifaceStart , ifaceBraceEnd + 1 ) . trim ( ) ;
2193+ if ( ! block . startsWith ( 'export' ) ) {
2194+ block = `export ${ block } ` ;
2195+ }
2196+ newDeclarations . push ( block ) ;
2197+ console . log ( ` Merged new interface "${ name } " into ${ basename ( targetFile ) } ` ) ;
2198+ }
2199+
2200+ if ( newDeclarations . length > 0 ) {
2201+ targetContent += `\n// Merged from module augmentation: declare module "${ bareSpecifier } "\n${ newDeclarations . join ( '\n' ) } \n` ;
2202+ await writeFile ( targetFile , targetContent , 'utf-8' ) ;
2203+ }
21172204 }
2205+
2206+ // Rewrite declare module path to relative
2207+ const newPattern = `declare module "${ relativePath } "` ;
2208+ content = content . replaceAll ( oldPattern , newPattern ) ;
2209+ modified = true ;
2210+ console . log ( ` Patched: ${ bareSpecifier } -> ${ relativePath } in ${ basename ( file ) } ` ) ;
21182211 }
21192212
21202213 if ( modified ) {
21212214 await writeFile ( file , content , 'utf-8' ) ;
21222215 }
21232216 }
2217+
2218+ // Re-export BrowserCommands from context.d.ts (imported but not exported)
2219+ const contextDtsPath = join ( distDir , '@vitest/browser/context.d.ts' ) ;
2220+ if ( existsSync ( contextDtsPath ) ) {
2221+ let content = await readFile ( contextDtsPath , 'utf-8' ) ;
2222+ if (
2223+ content . includes ( 'BrowserCommands' ) &&
2224+ ! content . match ( / e x p o r t \s + ( t y p e \s + ) ? \{ [ ^ } ] * B r o w s e r C o m m a n d s / )
2225+ ) {
2226+ content += '\nexport type { BrowserCommands };\n' ;
2227+ await writeFile ( contextDtsPath , content , 'utf-8' ) ;
2228+ console . log ( ' Added BrowserCommands re-export to context.d.ts' ) ;
2229+ }
2230+ }
2231+
2232+ // Validate: ensure no duplicate top-level interface declarations were introduced by merging.
2233+ // Only count interfaces at the module scope (not nested inside declare global, namespace, etc.)
2234+ for ( const [ bareSpecifier , { targetFile } ] of Object . entries ( augmentationMappings ) ) {
2235+ if ( ! existsSync ( targetFile ) ) {
2236+ continue ;
2237+ }
2238+ const finalContent = await readFile ( targetFile , 'utf-8' ) ;
2239+
2240+ // Extract top-level interface names by tracking brace depth
2241+ const topLevelInterfaces : string [ ] = [ ] ;
2242+ let depth = 0 ;
2243+ for ( let i = 0 ; i < finalContent . length ; i ++ ) {
2244+ if ( finalContent [ i ] === '{' ) {
2245+ depth ++ ;
2246+ } else if ( finalContent [ i ] === '}' ) {
2247+ depth -- ;
2248+ } else if ( depth === 0 ) {
2249+ const remaining = finalContent . slice ( i ) ;
2250+ const m = remaining . match ( / ^ i n t e r f a c e \s + ( \w + ) / ) ;
2251+ if ( m ) {
2252+ topLevelInterfaces . push ( m [ 1 ] ) ;
2253+ i += m [ 0 ] . length - 1 ;
2254+ }
2255+ }
2256+ }
2257+
2258+ const counts = new Map < string , number > ( ) ;
2259+ for ( const name of topLevelInterfaces ) {
2260+ counts . set ( name , ( counts . get ( name ) || 0 ) + 1 ) ;
2261+ }
2262+
2263+ for ( const [ name , count ] of counts ) {
2264+ if ( count > 1 ) {
2265+ throw new Error (
2266+ `Interface "${ name } " is declared ${ count } times at top level in ${ basename ( targetFile ) } . ` +
2267+ `Module augmentation merge for "${ bareSpecifier } " likely created a duplicate ` +
2268+ `declaration that will shadow extends clauses and break type signatures.` ,
2269+ ) ;
2270+ }
2271+ }
2272+ }
21242273}
21252274
21262275/**
0 commit comments