Skip to content

Commit d403664

Browse files
committed
fix(test): skip re-exported interfaces and validate no duplicates after merge
1 parent 7202326 commit d403664

1 file changed

Lines changed: 90 additions & 5 deletions

File tree

packages/test/build.ts

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2138,13 +2138,58 @@ async function patchModuleAugmentations() {
21382138

21392139
const innerContent = content.slice(braceStart + 1, braceEnd).trim();
21402140

2141-
// Merge augmented types into the target .d.ts file
2141+
// Merge only NEW type declarations into the target .d.ts file.
2142+
// Interfaces that already exist (e.g., ExpectStatic, Assertion, MatcherState) must NOT
2143+
// be re-declared, as that would shadow extends clauses and break call signatures.
21422144
if (innerContent && existsSync(targetFile)) {
21432145
let targetContent = await readFile(targetFile, 'utf-8');
2144-
const exportBlock = innerContent.replace(/^(\t*)interface /gm, '$1export interface ');
2145-
targetContent += `\n// Merged from module augmentation: declare module "${bareSpecifier}"\n${exportBlock}\n`;
2146-
await writeFile(targetFile, targetContent, 'utf-8');
2147-
console.log(` Merged augmentation "${bareSpecifier}" into ${basename(targetFile)}`);
2146+
2147+
// Extract individual interface blocks from the augmentation content
2148+
const interfaceRegex = /(?:export\s+)?interface\s+(\w+)(?:<[^>]*>)?\s*\{/g;
2149+
let match;
2150+
const newDeclarations: string[] = [];
2151+
2152+
while ((match = interfaceRegex.exec(innerContent)) !== null) {
2153+
const name = match[1];
2154+
// Only merge if this interface does NOT already exist in the target file.
2155+
// Check both direct declarations (interface Name) and re-exports (export type { Name }).
2156+
const hasDirectDecl = new RegExp(`\\binterface\\s+${name}\\b`).test(targetContent);
2157+
const exportTypeMatch = targetContent.match(/export\s+type\s*\{([^}]*)\}/);
2158+
const isReExported = exportTypeMatch != null && new RegExp(`\\b${name}\\b`).test(exportTypeMatch[1]);
2159+
if (hasDirectDecl || isReExported) {
2160+
console.log(` Skipped existing interface "${name}" (already in ${basename(targetFile)})`);
2161+
continue;
2162+
}
2163+
2164+
// Extract this interface block using brace matching
2165+
const ifaceStart = match.index;
2166+
const ifaceBraceStart = innerContent.indexOf('{', ifaceStart);
2167+
let ifaceDepth = 0;
2168+
let ifaceBraceEnd = -1;
2169+
for (let i = ifaceBraceStart; i < innerContent.length; i++) {
2170+
if (innerContent[i] === '{') ifaceDepth++;
2171+
else if (innerContent[i] === '}') {
2172+
ifaceDepth--;
2173+
if (ifaceDepth === 0) {
2174+
ifaceBraceEnd = i;
2175+
break;
2176+
}
2177+
}
2178+
}
2179+
if (ifaceBraceEnd === -1) continue;
2180+
2181+
let block = innerContent.slice(ifaceStart, ifaceBraceEnd + 1).trim();
2182+
if (!block.startsWith('export')) {
2183+
block = `export ${block}`;
2184+
}
2185+
newDeclarations.push(block);
2186+
console.log(` Merged new interface "${name}" into ${basename(targetFile)}`);
2187+
}
2188+
2189+
if (newDeclarations.length > 0) {
2190+
targetContent += `\n// Merged from module augmentation: declare module "${bareSpecifier}"\n${newDeclarations.join('\n')}\n`;
2191+
await writeFile(targetFile, targetContent, 'utf-8');
2192+
}
21482193
}
21492194

21502195
// Rewrite declare module path to relative
@@ -2169,6 +2214,46 @@ async function patchModuleAugmentations() {
21692214
console.log(' Added BrowserCommands re-export to context.d.ts');
21702215
}
21712216
}
2217+
2218+
// Validate: ensure no duplicate top-level interface declarations were introduced by merging.
2219+
// Only count interfaces at the module scope (not nested inside declare global, namespace, etc.)
2220+
for (const [bareSpecifier, { targetFile }] of Object.entries(augmentationMappings)) {
2221+
if (!existsSync(targetFile)) continue;
2222+
const finalContent = await readFile(targetFile, 'utf-8');
2223+
2224+
// Extract top-level interface names by tracking brace depth
2225+
const topLevelInterfaces: string[] = [];
2226+
let depth = 0;
2227+
for (let i = 0; i < finalContent.length; i++) {
2228+
if (finalContent[i] === '{') {
2229+
depth++;
2230+
} else if (finalContent[i] === '}') {
2231+
depth--;
2232+
} else if (depth === 0) {
2233+
const remaining = finalContent.slice(i);
2234+
const m = remaining.match(/^interface\s+(\w+)/);
2235+
if (m) {
2236+
topLevelInterfaces.push(m[1]);
2237+
i += m[0].length - 1;
2238+
}
2239+
}
2240+
}
2241+
2242+
const counts = new Map<string, number>();
2243+
for (const name of topLevelInterfaces) {
2244+
counts.set(name, (counts.get(name) || 0) + 1);
2245+
}
2246+
2247+
for (const [name, count] of counts) {
2248+
if (count > 1) {
2249+
throw new Error(
2250+
`Interface "${name}" is declared ${count} times at top level in ${basename(targetFile)}. ` +
2251+
`Module augmentation merge for "${bareSpecifier}" likely created a duplicate ` +
2252+
`declaration that will shadow extends clauses and break type signatures.`,
2253+
);
2254+
}
2255+
}
2256+
}
21722257
}
21732258

21742259
/**

0 commit comments

Comments
 (0)