From 90c8349716e287c4b619507dce3914d81f802218 Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Thu, 9 Jan 2025 08:36:34 -0500 Subject: [PATCH] fix(core): match on full segments instead of substrings --- e2e/nx/src/run.test.ts | 37 +++++--- .../src/utils/find-matching-projects.spec.ts | 86 +++++++++++++++++-- .../nx/src/utils/find-matching-projects.ts | 21 ++--- 3 files changed, 113 insertions(+), 31 deletions(-) diff --git a/e2e/nx/src/run.test.ts b/e2e/nx/src/run.test.ts index ebec31688b318..4f1d9702b8b81 100644 --- a/e2e/nx/src/run.test.ts +++ b/e2e/nx/src/run.test.ts @@ -46,26 +46,37 @@ describe('Nx Running Tests', () => { }); }); - it('should support running using a substring of the project name', () => { - // Note: actual project name will have some numbers at the end. - expect(() => runCLI(`echo proj`)).not.toThrow(); - }); - - it('should error when multiple projects match a substring', () => { - const test1 = uniq('test1'); - const test2 = uniq('test2'); - runCLI(`generate @nx/js:lib libs/${test1}`); - runCLI(`generate @nx/js:lib libs/${test2}`); - updateJson(`libs/${test1}/project.json`, (c) => { + it('should support running with simple names (i.e. matching on full segments)', () => { + const foo = uniq('foo'); + const bar = uniq('bar'); + const nested = uniq('nested'); + runCLI(`generate @nx/js:lib libs/${foo}`); + runCLI(`generate @nx/js:lib libs/${bar}`); + runCLI(`generate @nx/js:lib libs/nested/${nested}`); + updateJson(`libs/${foo}/project.json`, (c) => { + c.name = `@acme/${foo}`; c.targets['echo'] = { command: 'echo TEST' }; return c; }); - updateJson(`libs/${test2}/project.json`, (c) => { + updateJson(`libs/${bar}/project.json`, (c) => { + c.name = `@acme/${bar}`; c.targets['echo'] = { command: 'echo TEST' }; return c; }); + updateJson(`libs/nested/${nested}/project.json`, (c) => { + c.name = `@acme/nested/${bar}`; // The last segment is a duplicate + c.targets['echo'] = { command: 'echo TEST' }; + return c; + }); + + // Full segments should match + expect(() => runCLI(`echo ${foo}`)).not.toThrow(); + + // Multiple matches should fail + expect(() => runCLI(`echo ${bar}`)).toThrow(); - expect(() => runCLI(`echo test`)).toThrow(); + // Partial segments should not match (Note: project foo has numbers in the end that aren't matched fully) + expect(() => runCLI(`echo foo`)).toThrow(); }); it.each([ diff --git a/packages/nx/src/utils/find-matching-projects.spec.ts b/packages/nx/src/utils/find-matching-projects.spec.ts index 25f9dd4e27d60..e2b8c7a91cbc5 100644 --- a/packages/nx/src/utils/find-matching-projects.spec.ts +++ b/packages/nx/src/utils/find-matching-projects.spec.ts @@ -47,6 +47,39 @@ describe('findMatchingProjects', () => { tags: [], }, }, + '@acme/foo': { + name: '@acme/foo', + type: 'lib', + data: { + root: 'lib/foo', + tags: [], + }, + }, + '@acme/bar': { + name: '@acme/bar', + type: 'lib', + data: { + root: 'lib/bar', + tags: [], + }, + }, + '@acme/foobar': { + name: '@acme/foobar', + type: 'lib', + data: { + root: 'lib/foobar', + tags: [], + }, + }, + // Technically, this isn't a valid npm package name, but we can handle it anyway just in case. + '@acme/nested/foo': { + name: '@acme/nested/foo', + type: 'lib', + data: { + root: 'lib/nested/foo', + tags: [], + }, + }, }; it('should return no projects when passed no patterns', () => { @@ -68,6 +101,10 @@ describe('findMatchingProjects', () => { 'b', 'c', 'nested', + '@acme/foo', + '@acme/bar', + '@acme/foobar', + '@acme/nested/foo', ]); }); @@ -77,6 +114,10 @@ describe('findMatchingProjects', () => { 'b', 'c', 'nested', + '@acme/foo', + '@acme/bar', + '@acme/foobar', + '@acme/nested/foo', ]); expect(findMatchingProjects(['a', '!*'], projectGraph)).toEqual([]); }); @@ -120,6 +161,10 @@ describe('findMatchingProjects', () => { 'b', 'c', 'nested', + '@acme/foo', + '@acme/bar', + '@acme/foobar', + '@acme/nested/foo', ]); }); @@ -131,7 +176,7 @@ describe('findMatchingProjects', () => { projectGraph ); expect(matches).toEqual(expect.arrayContaining(['a', 'b', 'nested'])); - expect(matches.length).toEqual(3); + expect(matches.length).toEqual(7); }); it('should expand generic glob patterns for tags', () => { @@ -156,6 +201,9 @@ describe('findMatchingProjects', () => { 'test-project', 'a', 'b', + '@acme/foo', + '@acme/bar', + '@acme/foobar', ]); expect(findMatchingProjects(['apps/*'], projectGraph)).toEqual(['c']); expect(findMatchingProjects(['**/nested'], projectGraph)).toEqual([ @@ -169,21 +217,47 @@ describe('findMatchingProjects', () => { 'b', 'c', 'nested', + '@acme/foo', + '@acme/bar', + '@acme/foobar', + '@acme/nested/foo', ]); expect(findMatchingProjects(['!tag:api'], projectGraph)).toEqual([ 'b', 'nested', + '@acme/foo', + '@acme/bar', + '@acme/foobar', + '@acme/nested/foo', ]); expect( findMatchingProjects(['!tag:api', 'test-project'], projectGraph) - ).toEqual(['b', 'nested', 'test-project']); + ).toEqual([ + 'b', + 'nested', + '@acme/foo', + '@acme/bar', + '@acme/foobar', + '@acme/nested/foo', + 'test-project', + ]); }); - it('should match on substring of names', () => { - expect(findMatchingProjects(['test', 'nest'], projectGraph)).toEqual([ - 'test-project', - 'nested', + it('should match on name segments', () => { + expect(findMatchingProjects(['foo'], projectGraph)).toEqual([ + '@acme/foo', + '@acme/nested/foo', + ]); + expect(findMatchingProjects(['bar'], projectGraph)).toEqual(['@acme/bar']); + expect(findMatchingProjects(['foobar'], projectGraph)).toEqual([ + '@acme/foobar', + ]); + expect(findMatchingProjects(['nested/foo'], projectGraph)).toEqual([ + '@acme/nested/foo', ]); + // Only full segments are matched + expect(findMatchingProjects(['fo'], projectGraph)).toEqual([]); + expect(findMatchingProjects(['nested/fo'], projectGraph)).toEqual([]); }); }); diff --git a/packages/nx/src/utils/find-matching-projects.ts b/packages/nx/src/utils/find-matching-projects.ts index 7730cd811f9e3..f4d929de8c763 100644 --- a/packages/nx/src/utils/find-matching-projects.ts +++ b/packages/nx/src/utils/find-matching-projects.ts @@ -167,18 +167,15 @@ function addMatchingProjectsByName( } if (!isGlobPattern(pattern.value)) { - // Only matching substrings when string is long enough, otherwise there will be too many matches. - // This is consistent with the behavior of run-one.ts. - if (pattern.value.length > 1) { - const matchingProjects = Object.keys(projects).filter((name) => - name.includes(pattern.value) - ); - for (const projectName of matchingProjects) { - if (pattern.exclude) { - matchedProjects.delete(projectName); - } else { - matchedProjects.add(projectName); - } + const regex = new RegExp(`\\b${pattern.value}\\b`); + const matchingProjects = Object.keys(projects).filter((name) => + regex.test(name) + ); + for (const projectName of matchingProjects) { + if (pattern.exclude) { + matchedProjects.delete(projectName); + } else { + matchedProjects.add(projectName); } } return;