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. <!-- 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 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) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
This commit is contained in:
parent
a77e3ef083
commit
c6cb024a06
@ -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([
|
it.each([
|
||||||
'--watch false',
|
'--watch false',
|
||||||
'--watch=false',
|
'--watch=false',
|
||||||
|
|||||||
@ -9,7 +9,10 @@ import {
|
|||||||
createProjectGraphAsync,
|
createProjectGraphAsync,
|
||||||
readProjectsConfigurationFromProjectGraph,
|
readProjectsConfigurationFromProjectGraph,
|
||||||
} from '../../project-graph/project-graph';
|
} 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 { NxJsonConfiguration } from '../../config/nx-json';
|
||||||
import { workspaceRoot } from '../../utils/workspace-root';
|
import { workspaceRoot } from '../../utils/workspace-root';
|
||||||
import { splitTarget } from '../../utils/split-target';
|
import { splitTarget } from '../../utils/split-target';
|
||||||
@ -18,6 +21,7 @@ import { TargetDependencyConfig } from '../../config/workspace-json-project-json
|
|||||||
import { readNxJson } from '../../config/configuration';
|
import { readNxJson } from '../../config/configuration';
|
||||||
import { calculateDefaultProjectName } from '../../config/calculate-default-project-name';
|
import { calculateDefaultProjectName } from '../../config/calculate-default-project-name';
|
||||||
import { generateGraph } from '../graph/graph';
|
import { generateGraph } from '../graph/graph';
|
||||||
|
import { findMatchingProjects } from '../../utils/find-matching-projects';
|
||||||
|
|
||||||
export async function runOne(
|
export async function runOne(
|
||||||
cwd: string,
|
cwd: string,
|
||||||
@ -60,7 +64,7 @@ export async function runOne(
|
|||||||
|
|
||||||
await connectToNxCloudIfExplicitlyAsked(nxArgs);
|
await connectToNxCloudIfExplicitlyAsked(nxArgs);
|
||||||
|
|
||||||
const { projects } = getProjects(projectGraph, opts.project);
|
const { projects, projectName } = getProjects(projectGraph, opts.project);
|
||||||
|
|
||||||
if (nxArgs.graph) {
|
if (nxArgs.graph) {
|
||||||
const projectNames = projects.map((t) => t.name);
|
const projectNames = projects.map((t) => t.name);
|
||||||
@ -84,7 +88,7 @@ export async function runOne(
|
|||||||
{ nxJson },
|
{ nxJson },
|
||||||
nxArgs,
|
nxArgs,
|
||||||
overrides,
|
overrides,
|
||||||
opts.project,
|
projectName,
|
||||||
extraTargetDependencies,
|
extraTargetDependencies,
|
||||||
extraOptions
|
extraOptions
|
||||||
);
|
);
|
||||||
@ -92,19 +96,48 @@ export async function runOne(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProjects(projectGraph: ProjectGraph, project: string): any {
|
function getProjects(
|
||||||
if (!projectGraph.nodes[project]) {
|
projectGraph: ProjectGraph,
|
||||||
|
projectName: string
|
||||||
|
): {
|
||||||
|
projectName: string;
|
||||||
|
projects: ProjectGraphProjectNode[];
|
||||||
|
projectsMap: Record<string, ProjectGraphProjectNode>;
|
||||||
|
} {
|
||||||
|
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({
|
output.error({
|
||||||
title: `Cannot find project '${project}'`,
|
title: `Multiple projects matched:`,
|
||||||
|
bodyLines:
|
||||||
|
projects.length > 100 ? [...projects.slice(0, 100), '...'] : projects,
|
||||||
});
|
});
|
||||||
process.exit(1);
|
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 = {
|
const targetAliases = {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { output } from '../../utils/output';
|
|||||||
import { createProjectGraphAsync } from '../../project-graph/project-graph';
|
import { createProjectGraphAsync } from '../../project-graph/project-graph';
|
||||||
import { ShowProjectOptions } from './command-object';
|
import { ShowProjectOptions } from './command-object';
|
||||||
import { generateGraph } from '../graph/graph';
|
import { generateGraph } from '../graph/graph';
|
||||||
|
import { findMatchingProjects } from '../../utils/find-matching-projects';
|
||||||
|
|
||||||
export async function showProjectHandler(
|
export async function showProjectHandler(
|
||||||
args: ShowProjectOptions
|
args: ShowProjectOptions
|
||||||
@ -9,11 +10,30 @@ export async function showProjectHandler(
|
|||||||
performance.mark('code-loading:end');
|
performance.mark('code-loading:end');
|
||||||
performance.measure('code-loading', 'init-local', 'code-loading:end');
|
performance.measure('code-loading', 'init-local', 'code-loading:end');
|
||||||
const graph = await createProjectGraphAsync();
|
const graph = await createProjectGraphAsync();
|
||||||
const node = graph.nodes[args.projectName];
|
let node = graph.nodes[args.projectName];
|
||||||
if (!node) {
|
if (!node) {
|
||||||
|
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}`);
|
console.log(`Could not find project ${args.projectName}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (args.json) {
|
if (args.json) {
|
||||||
console.log(JSON.stringify(node.data));
|
console.log(JSON.stringify(node.data));
|
||||||
} else if (args.web) {
|
} else if (args.web) {
|
||||||
|
|||||||
@ -47,6 +47,39 @@ describe('findMatchingProjects', () => {
|
|||||||
tags: [],
|
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', () => {
|
it('should return no projects when passed no patterns', () => {
|
||||||
@ -68,6 +101,10 @@ describe('findMatchingProjects', () => {
|
|||||||
'b',
|
'b',
|
||||||
'c',
|
'c',
|
||||||
'nested',
|
'nested',
|
||||||
|
'@acme/foo',
|
||||||
|
'@acme/bar',
|
||||||
|
'foo_bar1',
|
||||||
|
'@acme/nested/foo',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -77,6 +114,10 @@ describe('findMatchingProjects', () => {
|
|||||||
'b',
|
'b',
|
||||||
'c',
|
'c',
|
||||||
'nested',
|
'nested',
|
||||||
|
'@acme/foo',
|
||||||
|
'@acme/bar',
|
||||||
|
'foo_bar1',
|
||||||
|
'@acme/nested/foo',
|
||||||
]);
|
]);
|
||||||
expect(findMatchingProjects(['a', '!*'], projectGraph)).toEqual([]);
|
expect(findMatchingProjects(['a', '!*'], projectGraph)).toEqual([]);
|
||||||
});
|
});
|
||||||
@ -120,6 +161,10 @@ describe('findMatchingProjects', () => {
|
|||||||
'b',
|
'b',
|
||||||
'c',
|
'c',
|
||||||
'nested',
|
'nested',
|
||||||
|
'@acme/foo',
|
||||||
|
'@acme/bar',
|
||||||
|
'foo_bar1',
|
||||||
|
'@acme/nested/foo',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -131,7 +176,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(3);
|
expect(matches.length).toEqual(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should expand generic glob patterns for tags', () => {
|
it('should expand generic glob patterns for tags', () => {
|
||||||
@ -156,6 +201,9 @@ describe('findMatchingProjects', () => {
|
|||||||
'test-project',
|
'test-project',
|
||||||
'a',
|
'a',
|
||||||
'b',
|
'b',
|
||||||
|
'@acme/foo',
|
||||||
|
'@acme/bar',
|
||||||
|
'foo_bar1',
|
||||||
]);
|
]);
|
||||||
expect(findMatchingProjects(['apps/*'], projectGraph)).toEqual(['c']);
|
expect(findMatchingProjects(['apps/*'], projectGraph)).toEqual(['c']);
|
||||||
expect(findMatchingProjects(['**/nested'], projectGraph)).toEqual([
|
expect(findMatchingProjects(['**/nested'], projectGraph)).toEqual([
|
||||||
@ -169,14 +217,50 @@ describe('findMatchingProjects', () => {
|
|||||||
'b',
|
'b',
|
||||||
'c',
|
'c',
|
||||||
'nested',
|
'nested',
|
||||||
|
'@acme/foo',
|
||||||
|
'@acme/bar',
|
||||||
|
'foo_bar1',
|
||||||
|
'@acme/nested/foo',
|
||||||
]);
|
]);
|
||||||
expect(findMatchingProjects(['!tag:api'], projectGraph)).toEqual([
|
expect(findMatchingProjects(['!tag:api'], projectGraph)).toEqual([
|
||||||
'b',
|
'b',
|
||||||
'nested',
|
'nested',
|
||||||
|
'@acme/foo',
|
||||||
|
'@acme/bar',
|
||||||
|
'foo_bar1',
|
||||||
|
'@acme/nested/foo',
|
||||||
]);
|
]);
|
||||||
expect(
|
expect(
|
||||||
findMatchingProjects(['!tag:api', 'test-project'], projectGraph)
|
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([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -167,6 +167,21 @@ function addMatchingProjectsByName(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isGlobPattern(pattern.value)) {
|
if (!isGlobPattern(pattern.value)) {
|
||||||
|
// Custom regex that is basically \b without underscores, so "foo" pattern matches "foo_bar".
|
||||||
|
const regex = new RegExp(
|
||||||
|
`(?<![a-zA-Z0-9])${pattern.value}(?![a-zA-Z0-9])`,
|
||||||
|
'i'
|
||||||
|
);
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user