fix(core): handle npm scope when matching project substring (#31160)

This PR fixes an issue if a JS project uses the same name as the npm
scope.

For example, if you have `@acme/acme` and `@acme/acme-e2e` projects,
then running `nx lint acme` will fail with an error:

```
 NX   Multiple projects matched:

@acme/acme-e2e
@acme/acme
```
## Current Behavior
Nx fails to run the task

## Expected Behavior
Nx should find the project to run the task for (ignoring scope)

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Jack Hsu 2025-05-09 18:43:36 -04:00 committed by GitHub
parent b620ea7dc1
commit 5aa0c4050f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 44 additions and 3 deletions

View File

@ -88,6 +88,22 @@ describe('findMatchingProjects', () => {
tags: [], tags: [],
}, },
}, },
'@test/test': {
name: '@test/test',
type: 'app',
data: {
root: 'apps/test',
tags: [],
},
},
'@test/test-e2e': {
name: '@test/test-e2e',
type: 'app',
data: {
root: 'apps/test-e2e',
tags: [],
},
},
}; };
it('should return no projects when passed no patterns', () => { it('should return no projects when passed no patterns', () => {
@ -114,6 +130,8 @@ describe('findMatchingProjects', () => {
'@acme/bar', '@acme/bar',
'foo_bar1', 'foo_bar1',
'@acme/nested/foo', '@acme/nested/foo',
'@test/test',
'@test/test-e2e',
]); ]);
}); });
@ -128,6 +146,8 @@ describe('findMatchingProjects', () => {
'@acme/bar', '@acme/bar',
'foo_bar1', 'foo_bar1',
'@acme/nested/foo', '@acme/nested/foo',
'@test/test',
'@test/test-e2e',
]); ]);
expect(findMatchingProjects(['a', '!*'], projectGraph)).toEqual([]); expect(findMatchingProjects(['a', '!*'], projectGraph)).toEqual([]);
}); });
@ -176,6 +196,8 @@ describe('findMatchingProjects', () => {
'@acme/bar', '@acme/bar',
'foo_bar1', 'foo_bar1',
'@acme/nested/foo', '@acme/nested/foo',
'@test/test',
'@test/test-e2e',
]); ]);
}); });
@ -187,7 +209,7 @@ describe('findMatchingProjects', () => {
projectGraph projectGraph
); );
expect(matches).toEqual(expect.arrayContaining(['a', 'b', 'nested'])); expect(matches).toEqual(expect.arrayContaining(['a', 'b', 'nested']));
expect(matches.length).toEqual(8); expect(matches.length).toEqual(10);
}); });
it('should expand generic glob patterns for tags', () => { it('should expand generic glob patterns for tags', () => {
@ -217,7 +239,11 @@ describe('findMatchingProjects', () => {
'@acme/bar', '@acme/bar',
'foo_bar1', 'foo_bar1',
]); ]);
expect(findMatchingProjects(['apps/*'], projectGraph)).toEqual(['c']); expect(findMatchingProjects(['apps/*'], projectGraph)).toEqual([
'c',
'@test/test',
'@test/test-e2e',
]);
expect(findMatchingProjects(['**/nested'], projectGraph)).toEqual([ expect(findMatchingProjects(['**/nested'], projectGraph)).toEqual([
'nested', 'nested',
]); ]);
@ -234,6 +260,8 @@ describe('findMatchingProjects', () => {
'@acme/bar', '@acme/bar',
'foo_bar1', 'foo_bar1',
'@acme/nested/foo', '@acme/nested/foo',
'@test/test',
'@test/test-e2e',
]); ]);
expect(findMatchingProjects(['!tag:api'], projectGraph)).toEqual([ expect(findMatchingProjects(['!tag:api'], projectGraph)).toEqual([
'b', 'b',
@ -243,6 +271,8 @@ describe('findMatchingProjects', () => {
'@acme/bar', '@acme/bar',
'foo_bar1', 'foo_bar1',
'@acme/nested/foo', '@acme/nested/foo',
'@test/test',
'@test/test-e2e',
]); ]);
expect( expect(
findMatchingProjects(['!tag:api', 'test-project'], projectGraph) findMatchingProjects(['!tag:api', 'test-project'], projectGraph)
@ -254,6 +284,8 @@ describe('findMatchingProjects', () => {
'@acme/bar', '@acme/bar',
'foo_bar1', 'foo_bar1',
'@acme/nested/foo', '@acme/nested/foo',
'@test/test',
'@test/test-e2e',
'test-project', 'test-project',
]); ]);
}); });
@ -280,6 +312,15 @@ describe('findMatchingProjects', () => {
expect(findMatchingProjects(['fo'], projectGraph)).toEqual([]); expect(findMatchingProjects(['fo'], projectGraph)).toEqual([]);
expect(findMatchingProjects(['nested/fo'], projectGraph)).toEqual([]); expect(findMatchingProjects(['nested/fo'], projectGraph)).toEqual([]);
}); });
it('should handle case where scope and names are the same', () => {
expect(findMatchingProjects(['test'], projectGraph)).toEqual([
'@test/test',
]);
expect(findMatchingProjects(['test-e2e'], projectGraph)).toEqual([
'@test/test-e2e',
]);
});
}); });
const projects = [ const projects = [

View File

@ -169,7 +169,7 @@ function addMatchingProjectsByName(
if (!isGlobPattern(pattern.value)) { if (!isGlobPattern(pattern.value)) {
// Custom regex that is basically \b but includes hyphens (-) and excludes underscores (_), so "foo" pattern matches "foo_bar" but not "foo-e2e". // Custom regex that is basically \b but includes hyphens (-) and excludes underscores (_), so "foo" pattern matches "foo_bar" but not "foo-e2e".
const regex = new RegExp( const regex = new RegExp(
`(?<![a-zA-Z0-9-])${pattern.value}(?![a-zA-Z0-9-])`, `(?<![@a-zA-Z0-9-])${pattern.value}(?![@a-zA-Z0-9-])`,
'i' 'i'
); );
const matchingProjects = Object.keys(projects).filter((name) => const matchingProjects = Object.keys(projects).filter((name) =>