diff --git a/packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts b/packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts index 6e008bca2a7e0..ba16b50b938ae 100644 --- a/packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts +++ b/packages/nx/src/project-graph/utils/project-configuration-utils.spec.ts @@ -1878,6 +1878,46 @@ describe('project-configuration-utils', () => { // other source map entries should be left unchanged expect(sourceMap['targets']).toEqual(['dummy', 'dummy.ts']); }); + + it('should not overwrite dependsOn', () => { + const sourceMap: Record = { + targets: ['dummy', 'dummy.ts'], + 'targets.build': ['dummy', 'dummy.ts'], + 'targets.build.options': ['dummy', 'dummy.ts'], + 'targets.build.options.command': ['dummy', 'dummy.ts'], + 'targets.build.options.cwd': ['project.json', 'nx/project-json'], + 'targets.build.dependsOn': ['project.json', 'nx/project-json'], + }; + const result = mergeTargetDefaultWithTargetDefinition( + 'build', + { + name: 'myapp', + root: 'apps/myapp', + targets: { + build: { + executor: 'nx:run-commands', + options: { + command: 'echo', + cwd: '{workspaceRoot}', + }, + dependsOn: [], + }, + }, + }, + { + options: { + command: 'tsc', + cwd: 'apps/myapp', + }, + dependsOn: ['^build'], + }, + sourceMap + ); + + // Command was defined by a core plugin so it should + // not be replaced by target default + expect(result.dependsOn).toEqual([]); + }); }); }); diff --git a/packages/nx/src/tasks-runner/create-task-graph.spec.ts b/packages/nx/src/tasks-runner/create-task-graph.spec.ts index 878bb07b0789f..6b26c3a93d964 100644 --- a/packages/nx/src/tasks-runner/create-task-graph.spec.ts +++ b/packages/nx/src/tasks-runner/create-task-graph.spec.ts @@ -1,4 +1,9 @@ -import { DependencyType, ProjectGraph } from '../config/project-graph'; +import { + DependencyType, + ProjectGraph, + ProjectGraphProjectNode, +} from '../config/project-graph'; +import { ProjectConfiguration } from '../config/workspace-json-project-json'; import { createTaskGraph } from './create-task-graph'; describe('createTaskGraph', () => { @@ -1677,4 +1682,311 @@ describe('createTaskGraph', () => { 'lib3:build', ]); }); + + it('should handle multiple dependsOn task groups', () => { + const taskGraph = createTaskGraph( + { + nodes: { + a: { + name: 'a', + type: 'app', + data: { + root: 'a-root', + targets: { + deploy: { + executor: 'nx:run-commands', + dependsOn: [{ target: 'build' }], + }, + build: { + executor: 'nx:run-commands', + dependsOn: [{ target: 'compile' }], + }, + compile: { + executor: 'nx:run-commands', + dependsOn: ['^compile'], + }, + }, + }, + }, + b: { + name: 'b', + type: 'lib', + data: { + root: 'b-root', + targets: { + deploy: { + executor: 'nx:run-commands', + dependsOn: [{ target: 'build' }], + }, + build: { + executor: 'nx:run-commands', + dependsOn: [{ target: 'compile' }], + }, + compile: { + executor: 'nx:run-commands', + dependsOn: ['^compile'], + }, + }, + }, + }, + c: { + name: 'c', + type: 'lib', + data: { + root: 'c-root', + targets: { + deploy: { + executor: 'nx:run-commands', + dependsOn: [{ target: 'build' }], + }, + build: { + executor: 'nx:run-commands', + dependsOn: [{ target: 'compile' }], + }, + compile: { + executor: 'nx:run-commands', + dependsOn: ['^compile'], + }, + }, + }, + }, + d: { + name: 'd', + type: 'lib', + data: { + root: 'd-root', + targets: { + deploy: { + executor: 'nx:run-commands', + dependsOn: [{ target: 'build' }], + }, + build: { + executor: 'nx:run-commands', + dependsOn: [{ target: 'compile' }], + }, + compile: { + executor: 'nx:run-commands', + dependsOn: ['^compile'], + }, + }, + }, + }, + }, + dependencies: { + a: [], + b: [ + { + source: 'b', + target: 'd', + type: 'static', + }, + ], + c: [ + { + source: 'c', + target: 'd', + type: 'static', + }, + ], + d: [], + }, + }, + {}, + ['a', 'b'], + ['deploy'], + null, + {} + ); + + expect(taskGraph.dependencies['a:deploy']).toEqual(['a:build']); + expect(taskGraph.dependencies['a:build']).toEqual(['a:compile']); + expect(taskGraph.dependencies['a:compile']).toEqual([]); + expect(taskGraph.dependencies['b:deploy']).toEqual(['b:build']); + expect(taskGraph.dependencies['b:build']).toEqual(['b:compile']); + expect(taskGraph.dependencies['b:compile']).toEqual(['d:compile']); + expect(taskGraph.dependencies['d:compile']).toEqual([]); + }); + + it('should handle deep dependsOn groups', () => { + const taskGraph = createTaskGraph( + new GraphBuilder() + .addProjectConfiguration({ + name: 'app-1', + targets: { + deploy: { + executor: 'foo', + dependsOn: ['build'], + }, + build: { + executor: 'foo', + dependsOn: ['^build', 'codegen'], + }, + codegen: { + executor: 'foo', + }, + }, + }) + .addProjectConfiguration({ + name: 'app-2', + targets: { + deploy: { + executor: 'foo', + dependsOn: ['build'], + }, + build: { + executor: 'foo', + dependsOn: [ + '^build', + { + target: 'codegen', + params: 'forward', + }, + ], + }, + codegen: { + executor: 'foo', + }, + }, + }) + .addProjectConfiguration({ + name: 'app-3', + targets: { + deploy: { + executor: 'foo', + dependsOn: ['build'], + }, + build: { + executor: 'foo', + dependsOn: [ + '^build', + { + target: 'codegen', + params: 'forward', + }, + ], + }, + codegen: { + executor: 'foo', + }, + }, + }) + .addProjectConfiguration({ + name: 'lib-1', + targets: { + build: { + executor: 'foo', + dependsOn: ['^build', 'codegen'], + }, + codegen: { + executor: 'foo', + }, + }, + }) + .addProjectConfiguration({ + name: 'lib-2', + targets: { + build: { + executor: 'foo', + dependsOn: ['^build', 'codegen'], + }, + codegen: { + executor: 'foo', + }, + }, + }) + .addDependencies({ + 'app-1': ['lib-1'], + 'app-2': ['lib-2'], + 'app-3': ['lib-2'], + 'lib-1': ['lib-2'], + 'lib-2': [], + }) + .build(), + {}, + ['app-1', 'app-2', 'app-3'], + ['deploy', 'test'], + null, + {}, + false + ); + + expect(taskGraph.dependencies).toMatchInlineSnapshot(` + { + "app-1:build": [ + "lib-1:build", + "app-1:codegen", + ], + "app-1:codegen": [], + "app-1:deploy": [ + "app-1:build", + ], + "app-2:build": [ + "lib-2:build", + "app-2:codegen", + ], + "app-2:codegen": [], + "app-2:deploy": [ + "app-2:build", + ], + "app-3:build": [ + "lib-2:build", + "app-3:codegen", + ], + "app-3:codegen": [], + "app-3:deploy": [ + "app-3:build", + ], + "lib-1:build": [ + "lib-2:build", + "lib-1:codegen", + ], + "lib-1:codegen": [], + "lib-2:build": [ + "lib-2:codegen", + ], + "lib-2:codegen": [], + } + `); + }); }); + +class GraphBuilder { + nodes: Record = {}; + deps: Record = {}; + + addProjectConfiguration( + project: Omit, + type?: ProjectGraph['nodes'][string]['type'] + ) { + const t = type ?? 'lib'; + this.nodes[project.name] = { + name: project.name, + type: t, + data: { ...project, root: `${t}/${project.name}` }, + }; + return this; + } + + addDependencies(deps: Record) { + for (const source of Object.keys(deps)) { + if (!this.deps[source]) { + this.deps[source] = []; + } + this.deps[source].push(...deps[source]); + } + return this; + } + + build(): ProjectGraph { + return { + nodes: this.nodes, + dependencies: Object.fromEntries( + Object.entries(this.deps).map(([k, v]) => [ + k, + v.map((d) => ({ source: k, target: d, type: 'static' })), + ]) + ), + externalNodes: {}, + }; + } +} diff --git a/packages/nx/src/tasks-runner/utils.spec.ts b/packages/nx/src/tasks-runner/utils.spec.ts index 9c69104c6667b..eb0bc01b54ffc 100644 --- a/packages/nx/src/tasks-runner/utils.spec.ts +++ b/packages/nx/src/tasks-runner/utils.spec.ts @@ -1,12 +1,14 @@ import { expandDependencyConfigSyntaxSugar, + expandWildcardTargetConfiguration, getDependencyConfigs, getOutputsForTargetAndConfiguration, interpolate, transformLegacyOutputs, validateOutputs, } from './utils'; -import { ProjectGraphProjectNode } from '../config/project-graph'; +import { ProjectGraph, ProjectGraphProjectNode } from '../config/project-graph'; +import { ProjectConfiguration } from '../config/workspace-json-project-json'; describe('utils', () => { function getNode(build): ProjectGraphProjectNode { @@ -619,6 +621,137 @@ describe('utils', () => { }, ]); }); + + it('should support multiple dependsOn chains', () => { + const graph = new GraphBuilder() + .addProjectConfiguration({ + name: 'foo', + targets: { + build: { + dependsOn: ['build:one'], + }, + 'build:one': { + dependsOn: [{ target: 'build:two' }], + }, + 'build:two': {}, + }, + }) + .addProjectConfiguration({ + name: 'bar', + targets: { + build: { + dependsOn: ['build:one'], + }, + 'build:one': { + dependsOn: [{ target: 'build:two' }], + }, + 'build:two': {}, + }, + }) + .build(); + + const getTargetDependencies = (project: string, target: string) => + getDependencyConfigs( + { + project, + target, + }, + {}, + graph, + ['build', 'build:one', 'build:two'] + ); + + expect(getTargetDependencies('foo', 'build')).toEqual([ + { + target: 'build:one', + projects: ['foo'], + }, + ]); + + expect(getTargetDependencies('foo', 'build:one')).toEqual([ + { + target: 'build:two', + projects: ['foo'], + }, + ]); + + expect(getTargetDependencies('foo', 'build:two')).toEqual([]); + + expect(getTargetDependencies('bar', 'build')).toEqual([ + { + target: 'build:one', + projects: ['bar'], + }, + ]); + + expect(getTargetDependencies('bar', 'build:one')).toEqual([ + { + target: 'build:two', + projects: ['bar'], + }, + ]); + + expect(getTargetDependencies('bar', 'build:two')).toEqual([]); + }); + }); + + describe('expandWildcardDependencies', () => { + it('should expand wildcard dependencies', () => { + const allTargets = ['build', 'build:test', 'build:prod', 'build:dev']; + const results = expandWildcardTargetConfiguration( + { + target: 'build*', + projects: ['a'], + }, + allTargets + ); + + expect(results).toEqual([ + { + target: 'build', + projects: ['a'], + }, + { + target: 'build:test', + projects: ['a'], + }, + { + target: 'build:prod', + projects: ['a'], + }, + { + target: 'build:dev', + projects: ['a'], + }, + ]); + + const results2 = expandWildcardTargetConfiguration( + { + target: 'build*', + projects: ['b'], + }, + allTargets + ); + + expect(results2).toEqual([ + { + target: 'build', + projects: ['b'], + }, + { + target: 'build:test', + projects: ['b'], + }, + { + target: 'build:prod', + projects: ['b'], + }, + { + target: 'build:dev', + projects: ['b'], + }, + ]); + }); }); describe('validateOutputs', () => { @@ -641,3 +774,28 @@ describe('utils', () => { }); }); }); + +class GraphBuilder { + nodes: Record = {}; + + addProjectConfiguration( + project: Omit, + type?: ProjectGraph['nodes'][string]['type'] + ) { + const t = type ?? 'lib'; + this.nodes[project.name] = { + name: project.name, + type: t, + data: { ...project, root: `${t}/${project.name}` }, + }; + return this; + } + + build(): ProjectGraph { + return { + nodes: this.nodes, + dependencies: {}, + externalNodes: {}, + }; + } +} diff --git a/packages/nx/src/tasks-runner/utils.ts b/packages/nx/src/tasks-runner/utils.ts index b3282b8d17657..938d80404a73d 100644 --- a/packages/nx/src/tasks-runner/utils.ts +++ b/packages/nx/src/tasks-runner/utils.ts @@ -113,36 +113,46 @@ export function expandDependencyConfigSyntaxSugar( const patternResultCache = new WeakMap< string[], // Map< Pattern, Dependency Configs > - Map + Map >(); -export function expandWildcardTargetConfiguration( - dependencyConfig: NormalizedTargetDependencyConfig, - allTargetNames: string[] -): NormalizedTargetDependencyConfig[] { - if (!isGlobPattern(dependencyConfig.target)) { - return [dependencyConfig]; - } +function findMatchingTargets(pattern: string, allTargetNames: string[]) { let cache = patternResultCache.get(allTargetNames); if (!cache) { cache = new Map(); patternResultCache.set(allTargetNames, cache); } - const cachedResult = cache.get(dependencyConfig.target); + + const cachedResult = cache.get(pattern); if (cachedResult) { return cachedResult; } - const matcher = minimatch.filter(dependencyConfig.target); + const matcher = minimatch.filter(pattern); const matchingTargets = allTargetNames.filter((t) => matcher(t)); + cache.set(pattern, matchingTargets); + return matchingTargets; +} + +export function expandWildcardTargetConfiguration( + dependencyConfig: NormalizedTargetDependencyConfig, + allTargetNames: string[] +): NormalizedTargetDependencyConfig[] { + if (!isGlobPattern(dependencyConfig.target)) { + return [dependencyConfig]; + } + + const matchingTargets = findMatchingTargets( + dependencyConfig.target, + allTargetNames + ); - const result = matchingTargets.map((t) => ({ - ...dependencyConfig, + return matchingTargets.map((t) => ({ target: t, + projects: dependencyConfig.projects, + dependencies: dependencyConfig.dependencies, })); - cache.set(dependencyConfig.target, result); - return result; } export function readProjectAndTargetFromTargetString( diff --git a/packages/nx/src/utils/globs.spec.ts b/packages/nx/src/utils/globs.spec.ts new file mode 100644 index 0000000000000..457cff487dfdb --- /dev/null +++ b/packages/nx/src/utils/globs.spec.ts @@ -0,0 +1,11 @@ +import { isGlobPattern } from './globs'; + +describe('isGlobPattern', () => { + it.each([ + [true, '{a,b}'], + [true, 'a*'], + [false, 'some-project'], + ])('should return %s for %s', (expected, pattern) => { + expect(isGlobPattern(pattern)).toBe(expected); + }); +});