Skip to content

Commit 723d0f8

Browse files
tamasangyan2heal1
andauthored
fix(third-party-dts-extractor): correctly sets the source of the pack… (#3467)
Co-authored-by: Hanric <[email protected]>
1 parent b99d57c commit 723d0f8

File tree

4 files changed

+100
-9
lines changed

4 files changed

+100
-9
lines changed

.changeset/chilled-eels-shout.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@module-federation/third-party-dts-extractor': patch
3+
---
4+
5+
fix(third-party-dts-extractor): correctly sets the source of the package types

packages/third-party-dts-extractor/src/ThirdPartyExtractor.spec.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,17 @@ describe('ThirdPartyExtractor', () => {
1212
exclude: ['ignore-pkg', /ignore-pkg2-/, /ignore-pkg3/.toString()],
1313
});
1414

15-
it('should correctly infer pkg dir with types field in package.json', () => {
15+
it("should correctly infer pkg's types dir with types/typings field in package.json", () => {
1616
const tsupDir = thirdPartyExtractor.inferPkgDir('tsup');
17-
const pkgJson = fs.readJSONSync(`${tsupDir}/package.json`);
1817

19-
expect(pkgJson.name).toBe('tsup');
20-
expect(Boolean(pkgJson.types || pkgJson.typings)).toEqual(true);
18+
if (!tsupDir) {
19+
throw new Error('tsup dir not found');
20+
}
21+
22+
const dirContent = fs.readdirSync(tsupDir);
23+
const dtsFiles = dirContent.filter((file) => file.endsWith('.d.ts'));
24+
25+
expect(dtsFiles.length).toBeGreaterThan(0);
2126
});
2227

2328
it('should correctly infer pkg types dir without types field in package.json', () => {

packages/third-party-dts-extractor/src/ThirdPartyExtractor.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import findPkg from 'find-pkg';
22
import fs from 'fs-extra';
33
import path from 'path';
44
import resolve from 'resolve';
5-
import { getTypedName } from './utils';
5+
import { getTypedName, getPackageRootDir } from './utils';
66

77
const ignoredPkgs = ['typescript'];
88

@@ -74,7 +74,8 @@ class ThirdPartyExtractor {
7474
if (isNodeUtils(importEntry, importPath)) {
7575
return;
7676
}
77-
const pkgJsonPath = findPkg.sync(importEntry) as string;
77+
const packageDir = getPackageRootDir(importPath);
78+
const pkgJsonPath = path.join(packageDir, 'package.json');
7879

7980
const dir = path.dirname(pkgJsonPath);
8081
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')) as Record<
@@ -87,8 +88,9 @@ class ThirdPartyExtractor {
8788
}
8889

8990
if (types) {
90-
this.addPkgs(pkg.name, dir);
91-
return dir;
91+
const typesDir = path.dirname(path.resolve(dir, types));
92+
this.addPkgs(pkg.name, typesDir);
93+
return typesDir;
9294
} else if (fs.existsSync(path.resolve(dir, 'index.d.ts'))) {
9395
this.addPkgs(pkg.name, dir);
9496
return dir;
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,84 @@
1+
import path from 'node:path';
2+
import fs from 'node:fs';
3+
14
function getTypedName(name: string) {
25
return `@types/${name.replace(/^@/, '').replace('/', '__')}`;
36
}
47

5-
export { getTypedName };
8+
/**
9+
* Locates the directory of the package.json for the given packageName.
10+
* 1. Resolves the entry file via require.resolve().
11+
* 2. Looks for package.json in that same directory (fallback).
12+
* 3. Climb upward until the folder name matches localName
13+
* (the part after a scope slash, e.g. "@scope/sdk" => "sdk").
14+
* 4. If that folder's package.json has "name" === packageName, return that directory.
15+
* 5. Otherwise, return the fallback directory (if it contains a package.json).
16+
* 6. If all else fails, throw an error.
17+
*/
18+
function getPackageRootDir(packageName: string | undefined): string {
19+
if (!packageName) {
20+
throw new Error('No package name provided.');
21+
}
22+
23+
// 1) Resolve the entry file
24+
const entryFile = require.resolve(packageName);
25+
const entryDir = path.dirname(entryFile);
26+
27+
// 2) Fallback: check if there's a package.json in entryDir
28+
let fallbackPackageJsonPath: string | null = path.join(
29+
entryDir,
30+
'package.json',
31+
);
32+
if (!fs.existsSync(fallbackPackageJsonPath)) {
33+
fallbackPackageJsonPath = null;
34+
}
35+
36+
// Figure out localName (e.g., "@scope/sdk" -> "sdk", "lodash" -> "lodash")
37+
const match = packageName.match(/^@[^/]+\/(.+)$/);
38+
const localName = match ? match[1] : packageName;
39+
40+
// Start climbing from the entryDir
41+
let currentDir = entryDir;
42+
43+
// 3) Use a while loop that continues until we either:
44+
// a) find a folder whose name is localName, or
45+
// b) reach the filesystem root (when path.dirname(currentDir) === currentDir)
46+
while (
47+
path.basename(currentDir) !== localName && // Keep going if names differ
48+
path.dirname(currentDir) !== currentDir // Stop if we're at root
49+
) {
50+
try {
51+
currentDir = path.dirname(currentDir);
52+
} catch (err) {
53+
// Permission error
54+
if (err.code === 'EACCES') {
55+
continue;
56+
} else {
57+
throw err;
58+
}
59+
}
60+
}
61+
62+
// 4) If we ended because the folder name now matches localName, check package.json
63+
if (path.basename(currentDir) === localName) {
64+
const candidate = path.join(currentDir, 'package.json');
65+
if (fs.existsSync(candidate)) {
66+
const pkgContent = JSON.parse(fs.readFileSync(candidate, 'utf8'));
67+
if (pkgContent.name === packageName) {
68+
return currentDir; // Found the correct root folder
69+
}
70+
}
71+
}
72+
73+
// 5) If we didn't find a matching package.json, fall back (if available)
74+
if (fallbackPackageJsonPath) {
75+
return entryDir;
76+
}
77+
78+
// 6) Otherwise, throw an error
79+
throw new Error(
80+
`Could not find a matching package.json for "${packageName}" and no fallback was found.`,
81+
);
82+
}
83+
84+
export { getTypedName, getPackageRootDir };

0 commit comments

Comments
 (0)