diff --git a/e2e/nx-run/src/affected-graph.test.ts b/e2e/nx-run/src/affected-graph.test.ts index a75c5dea5dba3f..fff641025037bc 100644 --- a/e2e/nx-run/src/affected-graph.test.ts +++ b/e2e/nx-run/src/affected-graph.test.ts @@ -196,6 +196,8 @@ describe('Nx Affected and Graph Tests', () => { }); function generateAll() { + removeFile('apps'); + removeFile('libs'); runCLI(`generate @nrwl/web:app ${myapp}`); runCLI(`generate @nrwl/web:app ${myapp2}`); runCLI(`generate @nrwl/js:lib ${mylib}`); @@ -258,6 +260,23 @@ describe('Nx Affected and Graph Tests', () => { expect(runCLI('affected:apps')).toContain(myapp2); expect(runCLI('affected:libs')).not.toContain(mylib); }); + + it('should detect changes to implicitly dependant projects', () => { + generateAll(); + updateProjectConfig(myapp, (config) => ({ + ...config, + implicitDependencies: ['*', `!${myapp2}`], + })); + + runCommand('git commit -m "setup test"'); + + const affectedApps = runCLI('affected:apps'); + const affectedLibs = runCLI('affected:libs'); + + expect(affectedApps).toContain(myapp); + expect(affectedApps).not.toContain(myapp2); + expect(affectedLibs).toContain(mylib); + }); }); describe('print-affected', () => { diff --git a/packages/nx/src/command-line/run-many.ts b/packages/nx/src/command-line/run-many.ts index 720eba3faafb3c..12eab13b5438c9 100644 --- a/packages/nx/src/command-line/run-many.ts +++ b/packages/nx/src/command-line/run-many.ts @@ -10,6 +10,7 @@ import { createProjectGraphAsync } from '../project-graph/project-graph'; import { TargetDependencyConfig } from '../config/workspace-json-project-json'; import { readNxJson } from '../config/configuration'; import { output } from '../utils/output'; +import { findMatchingProjects } from '../utils/find-matching-projects'; export async function runMany( args: { [k: string]: any }, @@ -66,30 +67,14 @@ export function projectsToRun( selectedProjects.set(projectName, projectGraph.nodes[projectName]); } } else { - for (const nameOrGlob of nxArgs.projects) { - if (validProjects.has(nameOrGlob)) { - selectedProjects.set(nameOrGlob, projectGraph.nodes[nameOrGlob]); - continue; - } else if (projectGraph.nodes[nameOrGlob]) { - invalidProjects.push(nameOrGlob); - continue; - } - - const matchedProjectNames = minimatch.match( - validProjectNames, - nameOrGlob - ); - - if (matchedProjectNames.length === 0) { - throw new Error(`No projects matching: ${nameOrGlob}`); + const matchingProjects = findMatchingProjects( + nxArgs.projects, + Object.keys(projectGraph.nodes) + ); + for (const project of matchingProjects) { + if (!validProjects.has(project)) { + invalidProjects.push(project); } - - matchedProjectNames.forEach((matchedProjectName) => { - selectedProjects.set( - matchedProjectName, - projectGraph.nodes[matchedProjectName] - ); - }); } if (invalidProjects.length > 0) { @@ -100,21 +85,17 @@ export function projectsToRun( } } - for (const nameOrGlob of nxArgs.exclude ?? []) { - const project = selectedProjects.has(nameOrGlob); - if (project) { - selectedProjects.delete(nameOrGlob); - continue; - } + const excludedProjects = findMatchingProjects( + nxArgs.exclude ?? [], + Array.from(selectedProjects.keys()) + ); - const matchedProjects = minimatch.match( - Array.from(selectedProjects.keys()), - nameOrGlob - ); + for (const excludedProject of excludedProjects) { + const project = selectedProjects.has(excludedProject); - matchedProjects.forEach((matchedProjectName) => { - selectedProjects.delete(matchedProjectName); - }); + if (project) { + selectedProjects.delete(excludedProject); + } } return Array.from(selectedProjects.values()); diff --git a/packages/nx/src/project-graph/build-nodes/workspace-projects.spec.ts b/packages/nx/src/project-graph/build-nodes/workspace-projects.spec.ts index d31d2e4a33dca5..b1b6ca04b62db0 100644 --- a/packages/nx/src/project-graph/build-nodes/workspace-projects.spec.ts +++ b/packages/nx/src/project-graph/build-nodes/workspace-projects.spec.ts @@ -20,6 +20,26 @@ describe('workspace-projects', () => { }) ).toEqual(['a', 'b', 'c']); }); + + it('should expand glob based implicit dependencies', () => { + expect( + normalizeImplicitDependencies('test-project', ['b*'], { + fileMap: {}, + filesToProcess: {}, + workspace: { + version: 2, + projects: { + ...makeProject('test-project'), + ...makeProject('a'), + ...makeProject('b'), + ...makeProject('b-1'), + ...makeProject('b-2'), + ...makeProject('c'), + }, + }, + }) + ).toEqual(['b', 'b-1', 'b-2']); + }); }); }); diff --git a/packages/nx/src/project-graph/build-nodes/workspace-projects.ts b/packages/nx/src/project-graph/build-nodes/workspace-projects.ts index 9ce76e8575f076..24159e8bd2c21d 100644 --- a/packages/nx/src/project-graph/build-nodes/workspace-projects.ts +++ b/packages/nx/src/project-graph/build-nodes/workspace-projects.ts @@ -11,8 +11,12 @@ import { ProjectGraphBuilder } from '../project-graph-builder'; import { PackageJson } from '../../utils/package-json'; import { readJsonFile } from '../../utils/fileutils'; import { NxJsonConfiguration } from '../../config/nx-json'; -import { ProjectConfiguration, TargetConfiguration } from '../../config/workspace-json-project-json'; +import { + ProjectConfiguration, + TargetConfiguration, +} from '../../config/workspace-json-project-json'; import { NX_PREFIX } from '../../utils/logger'; +import { findMatchingProjects } from 'nx/src/utils/find-matching-projects'; export function buildWorkspaceProjectNodes( ctx: ProjectGraphProcessorContext, @@ -147,12 +151,10 @@ export function normalizeImplicitDependencies( implicitDependencies: ProjectConfiguration['implicitDependencies'], context: ProjectGraphProcessorContext ) { - return implicitDependencies?.flatMap((target) => { - if (target === '*') { - return Object.keys(context.workspace.projects).filter( - (projectName) => projectName !== source - ); - } - return target; - }); + return findMatchingProjects( + implicitDependencies, + Object.keys(context.workspace.projects).filter( + (projectName) => projectName !== source + ) + ); } diff --git a/packages/nx/src/utils/assert-workspace-validity.ts b/packages/nx/src/utils/assert-workspace-validity.ts index 7ebbd6d218d813..b16a9aa6ce0583 100644 --- a/packages/nx/src/utils/assert-workspace-validity.ts +++ b/packages/nx/src/utils/assert-workspace-validity.ts @@ -90,7 +90,7 @@ function detectAndSetInvalidProjectValues( const projectName = implicit.startsWith('!') ? implicit.substring(1) : implicit; - return !validProjects[projectName]; + return !(implicit === '*' || validProjects[projectName]); }); if (invalidProjects.length > 0) { diff --git a/packages/nx/src/utils/find-matching-projects.ts b/packages/nx/src/utils/find-matching-projects.ts new file mode 100644 index 00000000000000..dd840052698d95 --- /dev/null +++ b/packages/nx/src/utils/find-matching-projects.ts @@ -0,0 +1,38 @@ +import minimatch = require('minimatch'); + +export function findMatchingProjects( + patterns: string[], + projectNames: string[] +): string[] { + const selectedProjects: Set = new Set(); + const excludedProjects: Set = new Set(); + for (const nameOrGlob of patterns) { + if (projectNames.includes(nameOrGlob)) { + selectedProjects.add(nameOrGlob); + continue; + } + + const exclude = nameOrGlob.startsWith('!'); + const pattern = exclude ? nameOrGlob.substring(1) : nameOrGlob; + + const matchedProjectNames = minimatch.match(projectNames, pattern); + + if (matchedProjectNames.length === 0) { + throw new Error(`No projects matching: ${nameOrGlob}`); + } + + matchedProjectNames.forEach((matchedProjectName) => { + if (exclude) { + excludedProjects.add(matchedProjectName); + } else { + selectedProjects.add(matchedProjectName); + } + }); + } + + for (const project of excludedProjects) { + selectedProjects.delete(project); + } + + return Array.from(selectedProjects); +}