From c6cb024a06ee820ea21ee0dca6f8a8eda9e65cbf Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Thu, 9 Jan 2025 13:39:24 -0500 Subject: [PATCH] feat(core): allow tasks to run with a substring of project name (#29552) When projects use only `package.json` and not `project.json`, we need to set the simple name in the `nx` property of `package.json`. This isn't ideal because it's yet another Nx-specific thing that needs to be configured when our goal is to reduce boilerplate. This PR allows users to pass a substring that matches exactly one project when running a task. For example, if `@acme/foo` is the name in `package.json`, then running `nx build foo` will match it. If more than one projects match, then an error is thrown showing all the matched projects, and the user has to be more specific or type in the fully qualified name. ## Current Behavior Users need to pass exact matches for project names when running tasks. ## Expected Behavior User can pass a substring matching the project name. ## Related Issue(s) Fixes # --- e2e/nx/src/run.test.ts | 33 +++++++ packages/nx/src/command-line/run/run-one.ts | 61 ++++++++++--- packages/nx/src/command-line/show/project.ts | 26 +++++- .../src/utils/find-matching-projects.spec.ts | 88 ++++++++++++++++++- .../nx/src/utils/find-matching-projects.ts | 15 ++++ 5 files changed, 204 insertions(+), 19 deletions(-) diff --git a/e2e/nx/src/run.test.ts b/e2e/nx/src/run.test.ts index e71ee43661..4f1d9702b8 100644 --- a/e2e/nx/src/run.test.ts +++ b/e2e/nx/src/run.test.ts @@ -46,6 +46,39 @@ describe('Nx Running Tests', () => { }); }); + 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/${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(); + + // 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([ '--watch false', '--watch=false', diff --git a/packages/nx/src/command-line/run/run-one.ts b/packages/nx/src/command-line/run/run-one.ts index 4f17d76f44..dc12dd52a9 100644 --- a/packages/nx/src/command-line/run/run-one.ts +++ b/packages/nx/src/command-line/run/run-one.ts @@ -9,7 +9,10 @@ import { createProjectGraphAsync, readProjectsConfigurationFromProjectGraph, } from '../../project-graph/project-graph'; -import { ProjectGraph } from '../../config/project-graph'; +import { + ProjectGraph, + ProjectGraphProjectNode, +} from '../../config/project-graph'; import { NxJsonConfiguration } from '../../config/nx-json'; import { workspaceRoot } from '../../utils/workspace-root'; import { splitTarget } from '../../utils/split-target'; @@ -18,6 +21,7 @@ import { TargetDependencyConfig } from '../../config/workspace-json-project-json import { readNxJson } from '../../config/configuration'; import { calculateDefaultProjectName } from '../../config/calculate-default-project-name'; import { generateGraph } from '../graph/graph'; +import { findMatchingProjects } from '../../utils/find-matching-projects'; export async function runOne( cwd: string, @@ -60,7 +64,7 @@ export async function runOne( await connectToNxCloudIfExplicitlyAsked(nxArgs); - const { projects } = getProjects(projectGraph, opts.project); + const { projects, projectName } = getProjects(projectGraph, opts.project); if (nxArgs.graph) { const projectNames = projects.map((t) => t.name); @@ -84,7 +88,7 @@ export async function runOne( { nxJson }, nxArgs, overrides, - opts.project, + projectName, extraTargetDependencies, extraOptions ); @@ -92,19 +96,48 @@ export async function runOne( } } -function getProjects(projectGraph: ProjectGraph, project: string): any { - if (!projectGraph.nodes[project]) { - output.error({ - title: `Cannot find project '${project}'`, - }); - process.exit(1); +function getProjects( + projectGraph: ProjectGraph, + projectName: string +): { + projectName: string; + projects: ProjectGraphProjectNode[]; + projectsMap: Record; +} { + if (projectGraph.nodes[projectName]) { + return { + projectName: projectName, + projects: [projectGraph.nodes[projectName]], + projectsMap: { + [projectName]: projectGraph.nodes[projectName], + }, + }; + } else { + const projects = findMatchingProjects([projectName], projectGraph.nodes); + if (projects.length === 1) { + const projectName = projects[0]; + const project = projectGraph.nodes[projectName]; + return { + projectName, + projects: [project], + projectsMap: { + [project.data.name]: project, + }, + }; + } else if (projects.length > 1) { + output.error({ + title: `Multiple projects matched:`, + bodyLines: + projects.length > 100 ? [...projects.slice(0, 100), '...'] : projects, + }); + process.exit(1); + } } - let projects = [projectGraph.nodes[project]]; - let projectsMap = { - [project]: projectGraph.nodes[project], - }; - return { projects, projectsMap }; + output.error({ + title: `Cannot find project '${projectName}'`, + }); + process.exit(1); } const targetAliases = { diff --git a/packages/nx/src/command-line/show/project.ts b/packages/nx/src/command-line/show/project.ts index ab093399fe..1ca8b17bb8 100644 --- a/packages/nx/src/command-line/show/project.ts +++ b/packages/nx/src/command-line/show/project.ts @@ -2,6 +2,7 @@ import { output } from '../../utils/output'; import { createProjectGraphAsync } from '../../project-graph/project-graph'; import { ShowProjectOptions } from './command-object'; import { generateGraph } from '../graph/graph'; +import { findMatchingProjects } from '../../utils/find-matching-projects'; export async function showProjectHandler( args: ShowProjectOptions @@ -9,10 +10,29 @@ export async function showProjectHandler( performance.mark('code-loading:end'); performance.measure('code-loading', 'init-local', 'code-loading:end'); const graph = await createProjectGraphAsync(); - const node = graph.nodes[args.projectName]; + let node = graph.nodes[args.projectName]; if (!node) { - console.log(`Could not find project ${args.projectName}`); - process.exit(1); + const projects = findMatchingProjects([args.projectName], graph.nodes); + if (projects.length === 1) { + const projectName = projects[0]; + node = graph.nodes[projectName]; + } else if (projects.length > 1) { + output.error({ + title: `Multiple projects matched:`, + bodyLines: + projects.length > 100 ? [...projects.slice(0, 100), '...'] : projects, + }); + console.log( + `Multiple projects matched:\n ${(projects.length > 100 + ? [...projects.slice(0, 100), '...'] + : projects + ).join(' \n')}` + ); + process.exit(1); + } else { + console.log(`Could not find project ${args.projectName}`); + process.exit(1); + } } if (args.json) { console.log(JSON.stringify(node.data)); diff --git a/packages/nx/src/utils/find-matching-projects.spec.ts b/packages/nx/src/utils/find-matching-projects.spec.ts index 7420b22faa..8964568b63 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: [], + }, + }, + foo_bar1: { + name: 'foo_bar11', + type: 'lib', + data: { + root: 'lib/foo_bar1', + 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', + 'foo_bar1', + '@acme/nested/foo', ]); }); @@ -77,6 +114,10 @@ describe('findMatchingProjects', () => { 'b', 'c', 'nested', + '@acme/foo', + '@acme/bar', + 'foo_bar1', + '@acme/nested/foo', ]); expect(findMatchingProjects(['a', '!*'], projectGraph)).toEqual([]); }); @@ -120,6 +161,10 @@ describe('findMatchingProjects', () => { 'b', 'c', 'nested', + '@acme/foo', + '@acme/bar', + 'foo_bar1', + '@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', + 'foo_bar1', ]); expect(findMatchingProjects(['apps/*'], projectGraph)).toEqual(['c']); expect(findMatchingProjects(['**/nested'], projectGraph)).toEqual([ @@ -169,14 +217,50 @@ describe('findMatchingProjects', () => { 'b', 'c', 'nested', + '@acme/foo', + '@acme/bar', + 'foo_bar1', + '@acme/nested/foo', ]); expect(findMatchingProjects(['!tag:api'], projectGraph)).toEqual([ 'b', 'nested', + '@acme/foo', + '@acme/bar', + 'foo_bar1', + '@acme/nested/foo', ]); expect( findMatchingProjects(['!tag:api', 'test-project'], projectGraph) - ).toEqual(['b', 'nested', 'test-project']); + ).toEqual([ + 'b', + 'nested', + '@acme/foo', + '@acme/bar', + 'foo_bar1', + '@acme/nested/foo', + 'test-project', + ]); + }); + + it('should match on name segments', () => { + expect(findMatchingProjects(['foo'], projectGraph)).toEqual([ + '@acme/foo', + 'foo_bar1', + '@acme/nested/foo', + ]); + expect(findMatchingProjects(['bar'], projectGraph)).toEqual(['@acme/bar']); + // Case insensitive + expect(findMatchingProjects(['Bar1'], projectGraph)).toEqual(['foo_bar1']); + expect(findMatchingProjects(['foo_bar1'], projectGraph)).toEqual([ + 'foo_bar1', + ]); + 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 b119e653f4..1cdeefdd26 100644 --- a/packages/nx/src/utils/find-matching-projects.ts +++ b/packages/nx/src/utils/find-matching-projects.ts @@ -167,6 +167,21 @@ function addMatchingProjectsByName( } if (!isGlobPattern(pattern.value)) { + // Custom regex that is basically \b without underscores, so "foo" pattern matches "foo_bar". + const regex = new RegExp( + `(? + regex.test(name) + ); + for (const projectName of matchingProjects) { + if (pattern.exclude) { + matchedProjects.delete(projectName); + } else { + matchedProjects.add(projectName); + } + } return; }