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 { ProjectGraphProjectNode } from '../../config/project-graph';
|
||||||
import {
|
import { normalizeImplicitDependencies } from './normalize-project-nodes';
|
||||||
normalizeImplicitDependencies,
|
|
||||||
normalizeProjectTargets,
|
|
||||||
} from './normalize-project-nodes';
|
|
||||||
|
|
||||||
describe('workspace-projects', () => {
|
describe('workspace-projects', () => {
|
||||||
let projectGraph: Record<string, ProjectGraphProjectNode> = {
|
let projectGraph: Record<string, ProjectGraphProjectNode> = {
|
||||||
@ -73,9 +70,41 @@ describe('workspace-projects', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
expect(
|
const results = normalizeImplicitDependencies(
|
||||||
normalizeImplicitDependencies('test-project', ['b*'], projectGraphMod)
|
'test-project',
|
||||||
).toEqual(['b', 'b-1', 'b-2']);
|
['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,
|
TargetConfiguration,
|
||||||
} from '../../config/workspace-json-project-json';
|
} from '../../config/workspace-json-project-json';
|
||||||
import { findMatchingProjects } from '../../utils/find-matching-projects';
|
import { findMatchingProjects } from '../../utils/find-matching-projects';
|
||||||
import {
|
|
||||||
readProjectConfigurationsFromRootMap,
|
|
||||||
resolveNxTokensInOptions,
|
|
||||||
} from '../utils/project-configuration-utils';
|
|
||||||
import { CreateDependenciesContext } from '../plugins';
|
import { CreateDependenciesContext } from '../plugins';
|
||||||
|
|
||||||
export async function normalizeProjectNodes(
|
export async function normalizeProjectNodes(
|
||||||
@ -111,12 +107,38 @@ export function normalizeImplicitDependencies(
|
|||||||
if (!implicitDependencies?.length) {
|
if (!implicitDependencies?.length) {
|
||||||
return implicitDependencies ?? [];
|
return implicitDependencies ?? [];
|
||||||
}
|
}
|
||||||
const matches = findMatchingProjects(implicitDependencies, projects);
|
|
||||||
return (
|
// Implicit dependencies handle negatives in a different
|
||||||
matches
|
// way from most other `projects` fields. This is because
|
||||||
.filter((x) => x !== source)
|
// they are used for multiple purposes.
|
||||||
// implicit dependencies that start with ! should hang around, to be processed by
|
const positivePatterns: string[] = [];
|
||||||
// implicit-project-dependencies.ts after explicit deps are added to graph.
|
const negativePatterns: string[] = [];
|
||||||
.concat(implicitDependencies.filter((x) => x.startsWith('!')))
|
|
||||||
|
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',
|
'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
|
||||||
: { __overrides_unparsed__: [] };
|
: { __overrides_unparsed__: [] };
|
||||||
if (dependencyConfig.projects) {
|
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(
|
this.processTasksForMatchingProjects(
|
||||||
dependencyConfig,
|
dependencyConfig,
|
||||||
projectUsedToDeriveDependencies,
|
|
||||||
configuration,
|
configuration,
|
||||||
task,
|
task,
|
||||||
taskOverrides,
|
taskOverrides,
|
||||||
@ -140,7 +172,6 @@ export class ProcessTasks {
|
|||||||
|
|
||||||
private processTasksForMatchingProjects(
|
private processTasksForMatchingProjects(
|
||||||
dependencyConfig: TargetDependencyConfig,
|
dependencyConfig: TargetDependencyConfig,
|
||||||
projectUsedToDeriveDependencies: string,
|
|
||||||
configuration: string,
|
configuration: string,
|
||||||
task: Task,
|
task: Task,
|
||||||
taskOverrides: Object | { __overrides_unparsed__: any[] },
|
taskOverrides: Object | { __overrides_unparsed__: any[] },
|
||||||
@ -150,54 +181,28 @@ export class ProcessTasks {
|
|||||||
typeof dependencyConfig.projects === 'string'
|
typeof dependencyConfig.projects === 'string'
|
||||||
? [dependencyConfig.projects]
|
? [dependencyConfig.projects]
|
||||||
: dependencyConfig.projects;
|
: dependencyConfig.projects;
|
||||||
for (const projectSpecifier of targetProjectSpecifiers) {
|
const matchingProjects = findMatchingProjects(
|
||||||
// Lerna uses `dependencies` in `prepNxOptions`, so we need to maintain
|
targetProjectSpecifiers,
|
||||||
// support for it until lerna can be updated to use the syntax.
|
this.projectGraph.nodes
|
||||||
// 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);
|
|
||||||
|
|
||||||
if (matchingProjects.length === 0) {
|
if (matchingProjects.length === 0) {
|
||||||
output.warn({
|
output.warn({
|
||||||
title: `\`dependsOn\` is misconfigured for ${task.target.project}:${task.target.target}`,
|
title: `\`dependsOn\` is misconfigured for ${task.target.project}:${task.target.target}`,
|
||||||
bodyLines: [
|
bodyLines: [
|
||||||
`Project pattern "${projectSpecifier}" does not match any projects.`,
|
`Project patterns "${targetProjectSpecifiers}" does not match any projects.`,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
for (const projectName of matchingProjects) {
|
||||||
for (const projectName of matchingProjects) {
|
this.processTasksForSingleProject(
|
||||||
this.processTasksForSingleProject(
|
task,
|
||||||
task,
|
projectName,
|
||||||
projectName,
|
dependencyConfig,
|
||||||
dependencyConfig,
|
configuration,
|
||||||
configuration,
|
taskOverrides,
|
||||||
taskOverrides,
|
overrides
|
||||||
overrides
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -162,6 +162,22 @@ describe('findMatchingProjects', () => {
|
|||||||
'nested',
|
'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 = [
|
const projects = [
|
||||||
|
|||||||
@ -39,6 +39,16 @@ export function findMatchingProjects(
|
|||||||
|
|
||||||
const matchedProjects: Set<string> = new Set();
|
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) {
|
for (const stringPattern of patterns) {
|
||||||
if (!stringPattern.length) {
|
if (!stringPattern.length) {
|
||||||
continue;
|
continue;
|
||||||
@ -205,11 +215,15 @@ function addMatchingProjectsByTag(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isExcludePattern(pattern: string): boolean {
|
||||||
|
return pattern.startsWith('!');
|
||||||
|
}
|
||||||
|
|
||||||
function parseStringPattern(
|
function parseStringPattern(
|
||||||
pattern: string,
|
pattern: string,
|
||||||
projects: Record<string, ProjectGraphProjectNode>
|
projects: Record<string, ProjectGraphProjectNode>
|
||||||
): ProjectPattern {
|
): ProjectPattern {
|
||||||
const isExclude = pattern.startsWith('!');
|
const isExclude = isExcludePattern(pattern);
|
||||||
|
|
||||||
// Support for things like: `!{type}:value`
|
// Support for things like: `!{type}:value`
|
||||||
if (isExclude) {
|
if (isExclude) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user