diff --git a/.changeset/repo-tools-type-deps-ambient-jest.md b/.changeset/repo-tools-type-deps-ambient-jest.md index 87c15e6f80..d1a0feed01 100644 --- a/.changeset/repo-tools-type-deps-ambient-jest.md +++ b/.changeset/repo-tools-type-deps-ambient-jest.md @@ -2,4 +2,4 @@ '@backstage/repo-tools': patch --- -The `type-deps` command now detects ambient global types from the `jest` namespace in declaration files, rather than only looking for explicit imports and reference directives. +The `type-deps` command now follows relative imports and re-exports into declaration chunk files, and detects ambient global types such as the `jest` namespace. diff --git a/packages/repo-tools/src/commands/type-deps/type-deps.ts b/packages/repo-tools/src/commands/type-deps/type-deps.ts index 80d30a41a8..46dd0f67c5 100644 --- a/packages/repo-tools/src/commands/type-deps/type-deps.ts +++ b/packages/repo-tools/src/commands/type-deps/type-deps.ts @@ -78,6 +78,50 @@ function shouldCheckTypes(pkg: Package & PackageJsonWithTypes) { ); } +/** + * Read a declaration file and recursively follow all relative imports and + * re-exports, collecting the source content of each file encountered. + */ +function collectDeclSources(entryPath: string): string[] { + const visited = new Set(); + const sources: string[] = []; + + function visit(filePath: string) { + const resolved = resolvePath(filePath); + if (visited.has(resolved)) { + return; + } + visited.add(resolved); + + // Handle .js -> .d.ts extension mapping used by declaration chunk files + let actualPath = resolved; + if (!fs.existsSync(actualPath)) { + actualPath = resolved.replace(/\.js$/, '.d.ts'); + } + if (!fs.existsSync(actualPath)) { + return; + } + + const src = fs.readFileSync(actualPath, 'utf8'); + sources.push(src); + + // Follow relative imports and re-exports into chunk files + const relativeRefs = ( + src.match(/^(?:import|export) .* from '\..*';$/gm) || [] + ) + .map(match => match.match(/from '(.*)'/)?.[1] ?? '') + .filter(n => n.startsWith('.')); + + const dir = resolvePath(actualPath, '..'); + for (const ref of relativeRefs) { + visit(resolvePath(dir, ref)); + } + } + + visit(entryPath); + return sources; +} + function findAllDeps(declSrc: string) { const importedDeps = (declSrc.match(/^import .* from '.*';$/gm) || []) .map(match => match.match(/from '(.*)'/)?.[1] ?? '') @@ -112,11 +156,8 @@ function findAllDeps(declSrc: string) { */ function checkTypes(pkg: Package) { const allDeps = getPackageExportDetails(pkg.packageJson).flatMap(exp => { - const typeDecl = fs.readFileSync( - resolvePath(pkg.dir, 'dist', exp.distPath), - 'utf8', - ); - return findAllDeps(typeDecl); + const entryPath = resolvePath(pkg.dir, 'dist', exp.distPath); + return collectDeclSources(entryPath).flatMap(src => findAllDeps(src)); }); const deps = Array.from(new Set(allDeps));