Skip to content

Commit 0fd1e36

Browse files
robbtraisterleosvelperez
authored andcommitted
fix(core): support subpath exports when constructing the project graph (#29577)
Sibling dependencies that rely exclusively on subpath exports are excluded from the dependency graph because there is no exact match. This adds a fallback to look for subpath exports if the exact match is not found. This also adds logic to respect conditional exports independent from subpath exports. <!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> <!-- If this is a particularly complex change or feature addition, you can request a dedicated Nx release for this pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate. --> ## Current Behavior Importing a workspace dependency via subpath export fails to match the package name, and so is not included in the dependency graph. ### Example ```apps/api/package.json``` ```json "name": "@my-org/api", "dependencies": { "@my-org/services": "workspace:*" } ``` ```libs/services/package.json``` ```json "name": "@my-org/services", "exports": { "./email": "./dist/email.js" } ``` The `@my-org/api` app should be able to import the email service with `import { EmailService } from "@my-org/services/email"`. However, the `getPackageEntryPointsToProjectMap` implementation results in an object with a key of `@my-org/services/email`, but not `@my-org/services`. This is not specifically a problem, except that `findDependencyInWorkspaceProjects` only considers exact matches within those object keys. ## Expected Behavior Importing a workspace dependency via subpath export should be included in the dependency graph. I also addressed a related issue where the following resulted in keys of `@my-org/services/default` and `@my-org/services/types`, which is incorrect according to the subpath/conditional export rules. ```json "exports": { "default": "./dist/index.js", "types": "./dist/index.d.ts" } ``` ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #29486 --------- Co-authored-by: Leosvel Pérez Espinosa <[email protected]> (cherry picked from commit d08ad75)
1 parent ba48f0a commit 0fd1e36

File tree

3 files changed

+71
-3
lines changed

3 files changed

+71
-3
lines changed

packages/nx/src/plugins/js/project-graph/build-dependencies/target-project-locator.spec.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,52 @@ describe('TargetProjectLocator', () => {
10111011
expect(result).toEqual('npm:[email protected]');
10121012
});
10131013
});
1014+
1015+
describe('findDependencyInWorkspaceProjects', () => {
1016+
it.each`
1017+
pkgName | project | exports | dependency
1018+
${'@org/pkg1'} | ${'pkg1'} | ${undefined} | ${'@org/pkg1'}
1019+
${'@org/pkg1'} | ${'pkg1'} | ${undefined} | ${'@org/pkg1/subpath'}
1020+
${'@org/pkg1'} | ${'pkg1'} | ${'dist/index.js'} | ${'@org/pkg1'}
1021+
${'@org/pkg1'} | ${'pkg1'} | ${{}} | ${'@org/pkg1'}
1022+
${'@org/pkg1'} | ${'pkg1'} | ${{}} | ${'@org/pkg1/subpath'}
1023+
${'@org/pkg1'} | ${'pkg1'} | ${{ '.': 'dist/index.js' }} | ${'@org/pkg1'}
1024+
${'@org/pkg1'} | ${'pkg1'} | ${{ '.': 'dist/index.js' }} | ${'@org/pkg1/subpath'}
1025+
${'@org/pkg1'} | ${'pkg1'} | ${{ './subpath': './dist/subpath.js' }} | ${'@org/pkg1'}
1026+
${'@org/pkg1'} | ${'pkg1'} | ${{ './subpath': './dist/subpath.js' }} | ${'@org/pkg1/subpath'}
1027+
${'@org/pkg1'} | ${'pkg1'} | ${{ import: './dist/index.js', default: './dist/index.js' }} | ${'@org/pkg1'}
1028+
${'@org/pkg1'} | ${'pkg1'} | ${{ import: './dist/index.js', default: './dist/index.js' }} | ${'@org/pkg1/subpath'}
1029+
`(
1030+
'should find "$dependency" as "$project" when exports="$exports"',
1031+
({ pkgName, project, exports, dependency }) => {
1032+
let projects: Record<string, ProjectGraphProjectNode> = {
1033+
[project]: {
1034+
name: project,
1035+
type: 'lib' as const,
1036+
data: {
1037+
root: project,
1038+
metadata: {
1039+
js: {
1040+
packageName: pkgName,
1041+
packageExports: exports,
1042+
},
1043+
},
1044+
},
1045+
},
1046+
};
1047+
1048+
const targetProjectLocator = new TargetProjectLocator(
1049+
projects,
1050+
{},
1051+
new Map()
1052+
);
1053+
const result =
1054+
targetProjectLocator.findDependencyInWorkspaceProjects(dependency);
1055+
1056+
expect(result).toEqual(project);
1057+
}
1058+
);
1059+
});
10141060
});
10151061

10161062
describe('isBuiltinModuleImport()', () => {

packages/nx/src/plugins/js/project-graph/build-dependencies/target-project-locator.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,14 @@ export class TargetProjectLocator {
259259
this.nodes
260260
);
261261

262-
return this.packageEntryPointsToProjectMap[dep]?.name ?? null;
262+
return (
263+
this.packageEntryPointsToProjectMap[dep]?.name ??
264+
// if the package exports do not include ".", look for subpath exports
265+
Object.entries(this.packageEntryPointsToProjectMap).find(([entryPoint]) =>
266+
dep.startsWith(`${entryPoint}/`)
267+
)?.[1]?.name ??
268+
null
269+
);
263270
}
264271

265272
private resolveImportWithTypescript(

packages/nx/src/plugins/js/utils/packages.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,28 @@ export function getPackageEntryPointsToProjectMap<
1515
}
1616

1717
const { packageName, packageExports } = metadata.js;
18-
if (!packageExports || typeof packageExports === 'string') {
18+
if (
19+
!packageExports ||
20+
typeof packageExports === 'string' ||
21+
!Object.keys(packageExports).length
22+
) {
1923
// no `exports` or it points to a file, which would be the equivalent of
2024
// an '.' export, in which case the package name is the entry point
2125
result[packageName] = project;
2226
} else {
2327
for (const entryPoint of Object.keys(packageExports)) {
24-
result[join(packageName, entryPoint)] = project;
28+
// if entrypoint begins with '.', it is a relative subpath export
29+
// otherwise, it is a conditional export
30+
// https://nodejs.org/api/packages.html#conditional-exports
31+
if (entryPoint.startsWith('.')) {
32+
result[join(packageName, entryPoint)] = project;
33+
} else {
34+
result[packageName] = project;
35+
}
36+
}
37+
// if there was no '.' entrypoint, ensure the package name is matched with the project
38+
if (!result[packageName]) {
39+
result[packageName] = project;
2540
}
2641
}
2742
}

0 commit comments

Comments
 (0)