Skip to content

Commit

Permalink
fix(core): support subpath exports when constructing the project graph (
Browse files Browse the repository at this point in the history
#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]>
  • Loading branch information
robbtraister and leosvelperez authored Jan 10, 2025
1 parent 0d5bfe3 commit d08ad75
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1011,6 +1011,52 @@ describe('TargetProjectLocator', () => {
expect(result).toEqual('npm:[email protected]');
});
});

describe('findDependencyInWorkspaceProjects', () => {
it.each`
pkgName | project | exports | dependency
${'@org/pkg1'} | ${'pkg1'} | ${undefined} | ${'@org/pkg1'}
${'@org/pkg1'} | ${'pkg1'} | ${undefined} | ${'@org/pkg1/subpath'}
${'@org/pkg1'} | ${'pkg1'} | ${'dist/index.js'} | ${'@org/pkg1'}
${'@org/pkg1'} | ${'pkg1'} | ${{}} | ${'@org/pkg1'}
${'@org/pkg1'} | ${'pkg1'} | ${{}} | ${'@org/pkg1/subpath'}
${'@org/pkg1'} | ${'pkg1'} | ${{ '.': 'dist/index.js' }} | ${'@org/pkg1'}
${'@org/pkg1'} | ${'pkg1'} | ${{ '.': 'dist/index.js' }} | ${'@org/pkg1/subpath'}
${'@org/pkg1'} | ${'pkg1'} | ${{ './subpath': './dist/subpath.js' }} | ${'@org/pkg1'}
${'@org/pkg1'} | ${'pkg1'} | ${{ './subpath': './dist/subpath.js' }} | ${'@org/pkg1/subpath'}
${'@org/pkg1'} | ${'pkg1'} | ${{ import: './dist/index.js', default: './dist/index.js' }} | ${'@org/pkg1'}
${'@org/pkg1'} | ${'pkg1'} | ${{ import: './dist/index.js', default: './dist/index.js' }} | ${'@org/pkg1/subpath'}
`(
'should find "$dependency" as "$project" when exports="$exports"',
({ pkgName, project, exports, dependency }) => {
let projects: Record<string, ProjectGraphProjectNode> = {
[project]: {
name: project,
type: 'lib' as const,
data: {
root: project,
metadata: {
js: {
packageName: pkgName,
packageExports: exports,
},
},
},
},
};

const targetProjectLocator = new TargetProjectLocator(
projects,
{},
new Map()
);
const result =
targetProjectLocator.findDependencyInWorkspaceProjects(dependency);

expect(result).toEqual(project);
}
);
});
});

describe('isBuiltinModuleImport()', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,14 @@ export class TargetProjectLocator {
this.nodes
);

return this.packageEntryPointsToProjectMap[dep]?.name ?? null;
return (
this.packageEntryPointsToProjectMap[dep]?.name ??
// if the package exports do not include ".", look for subpath exports
Object.entries(this.packageEntryPointsToProjectMap).find(([entryPoint]) =>
dep.startsWith(`${entryPoint}/`)
)?.[1]?.name ??
null
);
}

private resolveImportWithTypescript(
Expand Down
19 changes: 17 additions & 2 deletions packages/nx/src/plugins/js/utils/packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,28 @@ export function getPackageEntryPointsToProjectMap<
}

const { packageName, packageExports } = metadata.js;
if (!packageExports || typeof packageExports === 'string') {
if (
!packageExports ||
typeof packageExports === 'string' ||
!Object.keys(packageExports).length
) {
// no `exports` or it points to a file, which would be the equivalent of
// an '.' export, in which case the package name is the entry point
result[packageName] = project;
} else {
for (const entryPoint of Object.keys(packageExports)) {
result[join(packageName, entryPoint)] = project;
// if entrypoint begins with '.', it is a relative subpath export
// otherwise, it is a conditional export
// https://nodejs.org/api/packages.html#conditional-exports
if (entryPoint.startsWith('.')) {
result[join(packageName, entryPoint)] = project;
} else {
result[packageName] = project;
}
}
// if there was no '.' entrypoint, ensure the package name is matched with the project
if (!result[packageName]) {
result[packageName] = project;
}
}
}
Expand Down

0 comments on commit d08ad75

Please sign in to comment.