fix(core): handle projects inside dependsOn correctly (#26392)
This commit is contained in:
parent
fea232ee32
commit
356479b332
@ -1,8 +1,5 @@
|
||||
import { ProjectGraphProjectNode } from '../../config/project-graph';
|
||||
import {
|
||||
normalizeImplicitDependencies,
|
||||
normalizeProjectTargets,
|
||||
} from './normalize-project-nodes';
|
||||
import { normalizeImplicitDependencies } from './normalize-project-nodes';
|
||||
|
||||
describe('workspace-projects', () => {
|
||||
let projectGraph: Record<string, ProjectGraphProjectNode> = {
|
||||
@ -73,9 +70,41 @@ describe('workspace-projects', () => {
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(
|
||||
normalizeImplicitDependencies('test-project', ['b*'], projectGraphMod)
|
||||
).toEqual(['b', 'b-1', 'b-2']);
|
||||
const results = normalizeImplicitDependencies(
|
||||
'test-project',
|
||||
['b*'],
|
||||
projectGraphMod
|
||||
);
|
||||
expect(results).toEqual(expect.arrayContaining(['b', 'b-1', 'b-2']));
|
||||
expect(results).not.toContain('a');
|
||||
});
|
||||
|
||||
it('should handle negative projects correctly', () => {
|
||||
const results = normalizeImplicitDependencies(
|
||||
'test-project',
|
||||
['*', '!a'],
|
||||
projectGraph
|
||||
);
|
||||
// a is excluded
|
||||
expect(results).not.toContain('a');
|
||||
// b and c are included by wildcard
|
||||
expect(results).toEqual(expect.arrayContaining(['b', 'c']));
|
||||
// !a should remain in the list, to remove deps on a if they exist
|
||||
expect(results).toContain('!a');
|
||||
});
|
||||
|
||||
it('should handle negative patterns', () => {
|
||||
const results = normalizeImplicitDependencies(
|
||||
'test-project',
|
||||
['!tag:api'],
|
||||
projectGraph
|
||||
);
|
||||
// No projects are included by provided patterns
|
||||
expect(results.filter((x) => !x.startsWith('!'))).toHaveLength(0);
|
||||
// tag:api was expanded, and results included for later processing.
|
||||
expect(results).toEqual(
|
||||
expect.arrayContaining(['!a', '!c', '!test-project'])
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -5,10 +5,6 @@ import {
|
||||
TargetConfiguration,
|
||||
} from '../../config/workspace-json-project-json';
|
||||
import { findMatchingProjects } from '../../utils/find-matching-projects';
|
||||
import {
|
||||
readProjectConfigurationsFromRootMap,
|
||||
resolveNxTokensInOptions,
|
||||
} from '../utils/project-configuration-utils';
|
||||
import { CreateDependenciesContext } from '../plugins';
|
||||
|
||||
export async function normalizeProjectNodes(
|
||||
@ -111,12 +107,38 @@ export function normalizeImplicitDependencies(
|
||||
if (!implicitDependencies?.length) {
|
||||
return implicitDependencies ?? [];
|
||||
}
|
||||
const matches = findMatchingProjects(implicitDependencies, projects);
|
||||
return (
|
||||
matches
|
||||
.filter((x) => x !== source)
|
||||
// implicit dependencies that start with ! should hang around, to be processed by
|
||||
// implicit-project-dependencies.ts after explicit deps are added to graph.
|
||||
.concat(implicitDependencies.filter((x) => x.startsWith('!')))
|
||||
|
||||
// Implicit dependencies handle negatives in a different
|
||||
// way from most other `projects` fields. This is because
|
||||
// they are used for multiple purposes.
|
||||
const positivePatterns: string[] = [];
|
||||
const negativePatterns: string[] = [];
|
||||
|
||||
for (const dep of implicitDependencies) {
|
||||
if (dep.startsWith('!')) {
|
||||
negativePatterns.push(dep);
|
||||
} else {
|
||||
positivePatterns.push(dep);
|
||||
}
|
||||
}
|
||||
|
||||
// Finds all projects that match a positive pattern and are not excluded by a negative pattern
|
||||
const deps = positivePatterns.length
|
||||
? findMatchingProjects(
|
||||
positivePatterns.concat(negativePatterns),
|
||||
projects
|
||||
).filter((x) => x !== source)
|
||||
: [];
|
||||
|
||||
// Expands negative patterns to equal project names
|
||||
const alwaysIgnoredDeps = findMatchingProjects(
|
||||
negativePatterns.map((x) => x.slice(1)),
|
||||
projects
|
||||
);
|
||||
|
||||
// We return the matching deps, but keep the negative patterns in the list
|
||||
// so that they can be processed later by implicit-project-dependencies.ts
|
||||
// This is what allows using a negative implicit dep to remove a dependency
|
||||
// detected by createDependencies.
|
||||
return deps.concat(alwaysIgnoredDeps.map((x) => '!' + x)) as string[];
|
||||
}
|
||||
|
||||
@ -1553,4 +1553,86 @@ describe('createTaskGraph', () => {
|
||||
'lib2:build',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle negative patterns in dependsOn', () => {
|
||||
const graph: ProjectGraph = {
|
||||
nodes: {
|
||||
app1: {
|
||||
name: 'app1',
|
||||
type: 'app',
|
||||
data: {
|
||||
root: 'app1-root',
|
||||
targets: {
|
||||
build: {
|
||||
executor: 'nx:run-commands',
|
||||
dependsOn: [{ target: 'build', projects: '!lib1' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
lib1: {
|
||||
name: 'lib1',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: 'lib1-root',
|
||||
targets: {
|
||||
build: {
|
||||
executor: 'nx:run-commands',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
lib2: {
|
||||
name: 'lib2',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: 'lib2-root',
|
||||
targets: {
|
||||
build: {
|
||||
executor: 'nx:run-commands',
|
||||
},
|
||||
foo: {
|
||||
executor: 'nx:noop',
|
||||
dependsOn: [
|
||||
{
|
||||
target: 'build',
|
||||
projects: ['lib*', '!lib1'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
lib3: {
|
||||
name: 'lib3',
|
||||
type: 'lib',
|
||||
data: {
|
||||
root: 'lib3-root',
|
||||
targets: {
|
||||
build: {
|
||||
executor: 'nx:run-commands',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dependencies: {
|
||||
app1: [],
|
||||
},
|
||||
};
|
||||
const taskGraph = createTaskGraph(graph, {}, ['app1'], ['build'], null, {});
|
||||
expect(taskGraph.tasks).toHaveProperty('app1:build');
|
||||
expect(taskGraph.tasks).not.toHaveProperty('lib1:build');
|
||||
expect(taskGraph.tasks).toHaveProperty('lib2:build');
|
||||
expect(taskGraph.dependencies['app1:build']).toEqual([
|
||||
'lib2:build',
|
||||
'lib3:build',
|
||||
]);
|
||||
const taskGraph2 = createTaskGraph(graph, {}, ['lib2'], ['foo'], null, {});
|
||||
expect(taskGraph2.tasks).toHaveProperty('lib2:foo');
|
||||
expect(taskGraph2.dependencies['lib2:foo']).toEqual([
|
||||
'lib2:build',
|
||||
'lib3:build',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@ -108,9 +108,41 @@ export class ProcessTasks {
|
||||
? overrides
|
||||
: { __overrides_unparsed__: [] };
|
||||
if (dependencyConfig.projects) {
|
||||
/** LERNA SUPPORT START - Remove in v20 */
|
||||
// Lerna uses `dependencies` in `prepNxOptions`, so we need to maintain
|
||||
// support for it until lerna can be updated to use the syntax.
|
||||
//
|
||||
// This should have been removed in v17, but the updates to lerna had not
|
||||
// been made yet.
|
||||
//
|
||||
// TODO(@agentender): Remove this part in v20
|
||||
if (typeof dependencyConfig.projects === 'string') {
|
||||
if (dependencyConfig.projects === 'self') {
|
||||
this.processTasksForSingleProject(
|
||||
task,
|
||||
task.target.project,
|
||||
dependencyConfig,
|
||||
configuration,
|
||||
taskOverrides,
|
||||
overrides
|
||||
);
|
||||
continue;
|
||||
} else if (dependencyConfig.projects === 'dependencies') {
|
||||
this.processTasksForDependencies(
|
||||
projectUsedToDeriveDependencies,
|
||||
dependencyConfig,
|
||||
configuration,
|
||||
task,
|
||||
taskOverrides,
|
||||
overrides
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
/** LERNA SUPPORT END - Remove in v17 */
|
||||
|
||||
this.processTasksForMatchingProjects(
|
||||
dependencyConfig,
|
||||
projectUsedToDeriveDependencies,
|
||||
configuration,
|
||||
task,
|
||||
taskOverrides,
|
||||
@ -140,7 +172,6 @@ export class ProcessTasks {
|
||||
|
||||
private processTasksForMatchingProjects(
|
||||
dependencyConfig: TargetDependencyConfig,
|
||||
projectUsedToDeriveDependencies: string,
|
||||
configuration: string,
|
||||
task: Task,
|
||||
taskOverrides: Object | { __overrides_unparsed__: any[] },
|
||||
@ -150,54 +181,28 @@ export class ProcessTasks {
|
||||
typeof dependencyConfig.projects === 'string'
|
||||
? [dependencyConfig.projects]
|
||||
: dependencyConfig.projects;
|
||||
for (const projectSpecifier of targetProjectSpecifiers) {
|
||||
// Lerna uses `dependencies` in `prepNxOptions`, so we need to maintain
|
||||
// support for it until lerna can be updated to use the syntax.
|
||||
// TODO(@agentender): Remove this part in v17
|
||||
if (
|
||||
projectSpecifier === 'dependencies' &&
|
||||
!this.projectGraph.nodes[projectSpecifier]
|
||||
) {
|
||||
this.processTasksForDependencies(
|
||||
projectUsedToDeriveDependencies,
|
||||
dependencyConfig,
|
||||
configuration,
|
||||
task,
|
||||
taskOverrides,
|
||||
overrides
|
||||
);
|
||||
} else {
|
||||
// Since we need to maintain support for dependencies, it is more coherent
|
||||
// that we also support self.
|
||||
// TODO(@agentender): Remove this part in v17
|
||||
const matchingProjects =
|
||||
/** LERNA SUPPORT START - Remove in v17 */
|
||||
projectSpecifier === 'self' &&
|
||||
!this.projectGraph.nodes[projectSpecifier]
|
||||
? [task.target.project]
|
||||
: /** LERNA SUPPORT END */
|
||||
findMatchingProjects([projectSpecifier], this.projectGraph.nodes);
|
||||
const matchingProjects = findMatchingProjects(
|
||||
targetProjectSpecifiers,
|
||||
this.projectGraph.nodes
|
||||
);
|
||||
|
||||
if (matchingProjects.length === 0) {
|
||||
output.warn({
|
||||
title: `\`dependsOn\` is misconfigured for ${task.target.project}:${task.target.target}`,
|
||||
bodyLines: [
|
||||
`Project pattern "${projectSpecifier}" does not match any projects.`,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
for (const projectName of matchingProjects) {
|
||||
this.processTasksForSingleProject(
|
||||
task,
|
||||
projectName,
|
||||
dependencyConfig,
|
||||
configuration,
|
||||
taskOverrides,
|
||||
overrides
|
||||
);
|
||||
}
|
||||
}
|
||||
if (matchingProjects.length === 0) {
|
||||
output.warn({
|
||||
title: `\`dependsOn\` is misconfigured for ${task.target.project}:${task.target.target}`,
|
||||
bodyLines: [
|
||||
`Project patterns "${targetProjectSpecifiers}" does not match any projects.`,
|
||||
],
|
||||
});
|
||||
}
|
||||
for (const projectName of matchingProjects) {
|
||||
this.processTasksForSingleProject(
|
||||
task,
|
||||
projectName,
|
||||
dependencyConfig,
|
||||
configuration,
|
||||
taskOverrides,
|
||||
overrides
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -162,6 +162,22 @@ describe('findMatchingProjects', () => {
|
||||
'nested',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should support "all except" style patterns', () => {
|
||||
expect(findMatchingProjects(['!a'], projectGraph)).toEqual([
|
||||
'test-project',
|
||||
'b',
|
||||
'c',
|
||||
'nested',
|
||||
]);
|
||||
expect(findMatchingProjects(['!tag:api'], projectGraph)).toEqual([
|
||||
'b',
|
||||
'nested',
|
||||
]);
|
||||
expect(
|
||||
findMatchingProjects(['!tag:api', 'test-project'], projectGraph)
|
||||
).toEqual(['b', 'nested', 'test-project']);
|
||||
});
|
||||
});
|
||||
|
||||
const projects = [
|
||||
|
||||
@ -39,6 +39,16 @@ export function findMatchingProjects(
|
||||
|
||||
const matchedProjects: Set<string> = new Set();
|
||||
|
||||
// If the first pattern is an exclude pattern,
|
||||
// we add a wildcard pattern at the first to select
|
||||
// all projects, except the ones that match the exclude pattern.
|
||||
// e.g. ['!tag:someTag', 'project2'] will match all projects except
|
||||
// the ones with the tag 'someTag', and also match the project 'project2',
|
||||
// regardless of its tags.
|
||||
if (isExcludePattern(patterns[0])) {
|
||||
patterns.unshift('*');
|
||||
}
|
||||
|
||||
for (const stringPattern of patterns) {
|
||||
if (!stringPattern.length) {
|
||||
continue;
|
||||
@ -205,11 +215,15 @@ function addMatchingProjectsByTag(
|
||||
}
|
||||
}
|
||||
|
||||
function isExcludePattern(pattern: string): boolean {
|
||||
return pattern.startsWith('!');
|
||||
}
|
||||
|
||||
function parseStringPattern(
|
||||
pattern: string,
|
||||
projects: Record<string, ProjectGraphProjectNode>
|
||||
): ProjectPattern {
|
||||
const isExclude = pattern.startsWith('!');
|
||||
const isExclude = isExcludePattern(pattern);
|
||||
|
||||
// Support for things like: `!{type}:value`
|
||||
if (isExclude) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user