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; }