From b30930f95fd8b17e8edf3dde38be1d09162ade93 Mon Sep 17 00:00:00 2001 From: Jason Jean Date: Fri, 24 Jan 2020 12:59:31 -0500 Subject: [PATCH] feat(core): analyze changes to nx.json and workspace.json (#2338) --- e2e/affected-git.test.ts | 85 ++++++++ .../affected-project-graph.ts | 6 +- .../locators/nx-json-changes.spec.ts | 194 ++++++++++++++++++ .../locators/nx-json-changes.ts | 46 +++++ .../locators/workspace-json-changes.spec.ts | 183 +++++++++++++++++ .../locators/workspace-json-changes.ts | 52 +++++ scripts/e2e-ci1.sh | 1 + 7 files changed, 566 insertions(+), 1 deletion(-) create mode 100644 e2e/affected-git.test.ts create mode 100644 packages/workspace/src/core/affected-project-graph/locators/nx-json-changes.spec.ts create mode 100644 packages/workspace/src/core/affected-project-graph/locators/nx-json-changes.ts create mode 100644 packages/workspace/src/core/affected-project-graph/locators/workspace-json-changes.spec.ts create mode 100644 packages/workspace/src/core/affected-project-graph/locators/workspace-json-changes.ts diff --git a/e2e/affected-git.test.ts b/e2e/affected-git.test.ts new file mode 100644 index 0000000000..c728e7bb30 --- /dev/null +++ b/e2e/affected-git.test.ts @@ -0,0 +1,85 @@ +import { + ensureProject, + readJson, + runCommand, + uniq, + updateFile, + runCLI, + forEachCli, + workspaceConfigName +} from './utils'; +import { NxJson } from '@nrwl/workspace/src/core/shared-interfaces'; + +forEachCli(() => { + describe('Affected (with Git)', () => { + let myapp = uniq('myapp'); + let myapp2 = uniq('myapp'); + let mylib = uniq('mylib'); + it('should not affect other projects by generating a new project', () => { + ensureProject(); + + const nxJson: NxJson = readJson('nx.json'); + + delete nxJson.implicitDependencies; + + updateFile('nx.json', JSON.stringify(nxJson)); + runCommand(`git init`); + runCommand(`git config user.email "test@test.com"`); + runCommand(`git config user.name "Test"`); + runCommand( + `git add . && git commit -am "initial commit" && git checkout -b master` + ); + runCLI(`generate @nrwl/angular:app ${myapp}`); + expect(runCommand('yarn affected:apps')).toContain(myapp); + runCommand(`git add . && git commit -am "add ${myapp}"`); + + runCLI(`generate @nrwl/angular:app ${myapp2}`); + expect(runCommand('yarn affected:apps')).not.toContain(myapp); + expect(runCommand('yarn affected:apps')).toContain(myapp2); + runCommand(`git add . && git commit -am "add ${myapp2}"`); + + runCLI(`generate @nrwl/angular:lib ${mylib}`); + expect(runCommand('yarn affected:apps')).not.toContain(myapp); + expect(runCommand('yarn affected:apps')).not.toContain(myapp2); + expect(runCommand('yarn affected:libs')).toContain(mylib); + runCommand(`git add . && git commit -am "add ${mylib}"`); + }, 1000000); + + it('should detect changes to projects based on the nx.json', () => { + const nxJson: NxJson = readJson('nx.json'); + + nxJson.projects[myapp].tags = ['tag']; + updateFile('nx.json', JSON.stringify(nxJson)); + expect(runCommand('yarn affected:apps')).toContain(myapp); + expect(runCommand('yarn affected:apps')).not.toContain(myapp2); + expect(runCommand('yarn affected:libs')).not.toContain(mylib); + runCommand(`git add . && git commit -am "add tag to ${myapp}"`); + }); + + it('should detect changes to projects based on the workspace.json', () => { + const workspaceJson = readJson(workspaceConfigName()); + + workspaceJson.projects[myapp].prefix = 'my-app'; + updateFile(workspaceConfigName(), JSON.stringify(workspaceJson)); + expect(runCommand('yarn affected:apps')).toContain(myapp); + expect(runCommand('yarn affected:apps')).not.toContain(myapp2); + expect(runCommand('yarn affected:libs')).not.toContain(mylib); + runCommand(`git add . && git commit -am "change prefix for ${myapp}"`); + }); + + it('should affect all projects by removing projects', () => { + const workspaceJson = readJson(workspaceConfigName()); + delete workspaceJson.projects[mylib]; + updateFile(workspaceConfigName(), JSON.stringify(workspaceJson)); + + const nxJson = readJson('nx.json'); + delete nxJson.projects[mylib]; + updateFile('nx.json', JSON.stringify(nxJson)); + + expect(runCommand('yarn affected:apps')).toContain(myapp); + expect(runCommand('yarn affected:apps')).toContain(myapp2); + expect(runCommand('yarn affected:libs')).not.toContain(mylib); + runCommand(`git add . && git commit -am "remove ${mylib}"`); + }); + }); +}); diff --git a/packages/workspace/src/core/affected-project-graph/affected-project-graph.ts b/packages/workspace/src/core/affected-project-graph/affected-project-graph.ts index 942b9f4bdc..57e29db52f 100644 --- a/packages/workspace/src/core/affected-project-graph/affected-project-graph.ts +++ b/packages/workspace/src/core/affected-project-graph/affected-project-graph.ts @@ -17,6 +17,8 @@ import { TouchedProjectLocator } from './affected-project-graph-models'; import { normalizeNxJson } from '../normalize-nx-json'; +import { getTouchedProjectsInNxJson } from './locators/nx-json-changes'; +import { getTouchedProjectsInWorkspaceJson } from './locators/workspace-json-changes'; export function filterAffected( graph: ProjectGraph, @@ -31,7 +33,9 @@ export function filterAffected( getTouchedProjects, getImplicitlyTouchedProjects, getTouchedNpmPackages, - getImplicitlyTouchedProjectsByJsonChanges + getImplicitlyTouchedProjectsByJsonChanges, + getTouchedProjectsInNxJson, + getTouchedProjectsInWorkspaceJson ]; const touchedProjects = touchedProjectLocators.reduce( (acc, f) => { diff --git a/packages/workspace/src/core/affected-project-graph/locators/nx-json-changes.spec.ts b/packages/workspace/src/core/affected-project-graph/locators/nx-json-changes.spec.ts new file mode 100644 index 0000000000..966b5a3c46 --- /dev/null +++ b/packages/workspace/src/core/affected-project-graph/locators/nx-json-changes.spec.ts @@ -0,0 +1,194 @@ +import { getTouchedProjectsInNxJson } from './nx-json-changes'; +import { WholeFileChange } from '../../file-utils'; +import { DiffType } from '../../../utils/json-diff'; + +describe('getTouchedProjectsInNxJson', () => { + it('should not return changes when nx.json is not touched', () => { + const result = getTouchedProjectsInNxJson( + [ + { + file: 'source.ts', + ext: '.ts', + mtime: 0, + getChanges: () => [new WholeFileChange()] + } + ], + {}, + { + npmScope: 'proj', + projects: { + proj1: { + tags: [] + } + } + } + ); + expect(result).toEqual([]); + }); + + it('should return all projects for a whole file change', () => { + const result = getTouchedProjectsInNxJson( + [ + { + file: 'nx.json', + ext: '.json', + mtime: 0, + getChanges: () => [new WholeFileChange()] + } + ], + {}, + { + npmScope: 'proj', + projects: { + proj1: { + tags: [] + }, + proj2: { + tags: [] + } + } + } + ); + expect(result).toEqual(['proj1', 'proj2']); + }); + + it('should return all projects for changes to npmScope', () => { + const result = getTouchedProjectsInNxJson( + [ + { + file: 'nx.json', + ext: '.json', + mtime: 0, + getChanges: () => [ + { + type: DiffType.Modified, + path: ['npmScope'], + value: { + lhs: 'proj', + rhs: 'awesome-proj' + } + } + ] + } + ], + {}, + { + npmScope: 'proj', + projects: { + proj1: { + tags: [] + }, + proj2: { + tags: [] + } + } + } + ); + expect(result).toEqual(['proj1', 'proj2']); + }); + + it('should return projects added in nx.json', () => { + const result = getTouchedProjectsInNxJson( + [ + { + file: 'nx.json', + ext: '.json', + mtime: 0, + getChanges: () => [ + { + type: DiffType.Added, + path: ['projects', 'proj1', 'tags'], + value: { + lhs: undefined, + rhs: [] + } + } + ] + } + ], + {}, + { + npmScope: 'proj', + projects: { + proj1: { + tags: [] + }, + proj2: { + tags: [] + } + } + } + ); + expect(result).toEqual(['proj1']); + }); + + it('should not return projects removed in nx.json', () => { + const result = getTouchedProjectsInNxJson( + [ + { + file: 'nx.json', + ext: '.json', + mtime: 0, + getChanges: () => [ + { + type: DiffType.Deleted, + path: ['projects', 'proj3', 'tags'], + value: { + lhs: [], + rhs: undefined + } + } + ] + } + ], + {}, + { + npmScope: 'proj', + projects: { + proj1: { + tags: [] + }, + proj2: { + tags: [] + } + } + } + ); + expect(result).toEqual(['proj1', 'proj2']); + }); + + it('should return projects modified in nx.json', () => { + const result = getTouchedProjectsInNxJson( + [ + { + file: 'nx.json', + ext: '.json', + mtime: 0, + getChanges: () => [ + { + type: DiffType.Modified, + path: ['projects', 'proj1', 'tags', '0'], + value: { + lhs: 'scope:feat', + rhs: 'scope:shared' + } + } + ] + } + ], + {}, + { + npmScope: 'proj', + projects: { + proj1: { + tags: [] + }, + proj2: { + tags: [] + } + } + } + ); + expect(result).toEqual(['proj1']); + }); +}); diff --git a/packages/workspace/src/core/affected-project-graph/locators/nx-json-changes.ts b/packages/workspace/src/core/affected-project-graph/locators/nx-json-changes.ts new file mode 100644 index 0000000000..ea48c88be6 --- /dev/null +++ b/packages/workspace/src/core/affected-project-graph/locators/nx-json-changes.ts @@ -0,0 +1,46 @@ +import { isWholeFileChange, WholeFileChange } from '../../file-utils'; +import { DiffType, isJsonChange, JsonChange } from '../../../utils/json-diff'; +import { TouchedProjectLocator } from '../affected-project-graph-models'; + +export const getTouchedProjectsInNxJson: TouchedProjectLocator< + WholeFileChange | JsonChange +> = (touchedFiles, workspaceJson, nxJson): string[] => { + const nxJsonChange = touchedFiles.find(change => change.file === 'nx.json'); + if (!nxJsonChange) { + return []; + } + + const changes = nxJsonChange.getChanges(); + + if ( + changes.some(change => { + if (isJsonChange(change)) { + return change.path[0] !== 'projects'; + } + if (isWholeFileChange(change)) { + return true; + } + return false; + }) + ) { + return Object.keys(nxJson.projects); + } + + const touched = []; + for (let i = 0; i < changes.length; i++) { + const change = changes[i]; + if (!isJsonChange(change) || change.path[0] !== 'projects') { + return; + } + + if (nxJson.projects[change.path[1]]) { + touched.push(change.path[1]); + } else { + // The project was deleted so affect all projects + touched.push(...Object.keys(nxJson.projects)); + // Break out of the loop after all projects have been added. + break; + } + } + return touched; +}; diff --git a/packages/workspace/src/core/affected-project-graph/locators/workspace-json-changes.spec.ts b/packages/workspace/src/core/affected-project-graph/locators/workspace-json-changes.spec.ts new file mode 100644 index 0000000000..331d5ca2a0 --- /dev/null +++ b/packages/workspace/src/core/affected-project-graph/locators/workspace-json-changes.spec.ts @@ -0,0 +1,183 @@ +import { getTouchedProjectsInWorkspaceJson } from './workspace-json-changes'; +import { WholeFileChange } from '../../file-utils'; +import { DiffType } from '../../../utils/json-diff'; + +describe('getTouchedProjectsInWorkspaceJson', () => { + it('should not return changes when angular.json is not touched', () => { + const result = getTouchedProjectsInWorkspaceJson( + [ + { + file: 'source.ts', + ext: '.ts', + mtime: 0, + getChanges: () => [new WholeFileChange()] + } + ], + {}, + { + npmScope: 'proj', + projects: { + proj1: { + tags: [] + } + } + } + ); + expect(result).toEqual([]); + }); + + it('should return all projects for a whole file change', () => { + const result = getTouchedProjectsInWorkspaceJson( + [ + { + file: 'angular.json', + ext: '.json', + mtime: 0, + getChanges: () => [new WholeFileChange()] + } + ], + { + npmScope: 'proj', + projects: { + proj1: { + tags: [] + }, + proj2: { + tags: [] + } + } + } + ); + expect(result).toEqual(['proj1', 'proj2']); + }); + + it('should return all projects for changes to newProjectRoot', () => { + const result = getTouchedProjectsInWorkspaceJson( + [ + { + file: 'angular.json', + ext: '.json', + mtime: 0, + getChanges: () => [ + { + type: DiffType.Modified, + path: ['newProjectRoot'], + value: { + lhs: '', + rhs: 'projects' + } + } + ] + } + ], + { + newProjectRoot: 'projects', + projects: { + proj1: { + tags: [] + }, + proj2: { + tags: [] + } + } + } + ); + expect(result).toEqual(['proj1', 'proj2']); + }); + + it('should return projects added in angular.json', () => { + const result = getTouchedProjectsInWorkspaceJson( + [ + { + file: 'angular.json', + ext: '.json', + mtime: 0, + getChanges: () => [ + { + type: DiffType.Added, + path: ['projects', 'proj1', 'tags'], + value: { + lhs: undefined, + rhs: [] + } + } + ] + } + ], + { + projects: { + proj1: { + root: 'proj1' + } + } + } + ); + expect(result).toEqual(['proj1']); + }); + + it('should affect all projects if a project is removed from angular.json', () => { + const result = getTouchedProjectsInWorkspaceJson( + [ + { + file: 'angular.json', + ext: '.json', + mtime: 0, + getChanges: () => [ + { + type: DiffType.Deleted, + path: ['projects', 'proj3', 'root'], + value: { + lhs: 'proj3', + rhs: undefined + } + } + ] + } + ], + { + projects: { + proj1: { + root: 'proj1' + }, + proj2: { + root: 'proj2' + } + } + } + ); + expect(result).toEqual(['proj1', 'proj2']); + }); + + it('should return projects modified in angular.json', () => { + const result = getTouchedProjectsInWorkspaceJson( + [ + { + file: 'angular.json', + ext: '.json', + mtime: 0, + getChanges: () => [ + { + type: DiffType.Modified, + path: ['projects', 'proj1', 'root'], + value: { + lhs: 'proj3', + rhs: 'proj1' + } + } + ] + } + ], + { + projects: { + proj1: { + root: 'proj1' + }, + proj2: { + root: 'proj2' + } + } + } + ); + expect(result).toEqual(['proj1']); + }); +}); diff --git a/packages/workspace/src/core/affected-project-graph/locators/workspace-json-changes.ts b/packages/workspace/src/core/affected-project-graph/locators/workspace-json-changes.ts new file mode 100644 index 0000000000..308a234830 --- /dev/null +++ b/packages/workspace/src/core/affected-project-graph/locators/workspace-json-changes.ts @@ -0,0 +1,52 @@ +import { + isWholeFileChange, + WholeFileChange, + workspaceFileName +} from '../../file-utils'; +import { isJsonChange, JsonChange } from '../../../utils/json-diff'; +import { TouchedProjectLocator } from '../affected-project-graph-models'; + +export const getTouchedProjectsInWorkspaceJson: TouchedProjectLocator< + WholeFileChange | JsonChange +> = (touchedFiles, workspaceJson): string[] => { + const workspaceChange = touchedFiles.find( + change => change.file === workspaceFileName() + ); + if (!workspaceChange) { + return []; + } + + const changes = workspaceChange.getChanges(); + + if ( + changes.some(change => { + if (isJsonChange(change)) { + return change.path[0] !== 'projects'; + } + if (isWholeFileChange(change)) { + return true; + } + return false; + }) + ) { + return Object.keys(workspaceJson.projects); + } + + const touched = []; + for (let i = 0; i < changes.length; i++) { + const change = changes[i]; + if (!isJsonChange(change) || change.path[0] !== 'projects') { + return; + } + + if (workspaceJson.projects[change.path[1]]) { + touched.push(change.path[1]); + } else { + // The project was deleted so affect all projects + touched.push(...Object.keys(workspaceJson.projects)); + // Break out of the loop after all projects have been added. + break; + } + } + return touched; +}; diff --git a/scripts/e2e-ci1.sh b/scripts/e2e-ci1.sh index 0e5078ba79..32056be5a8 100755 --- a/scripts/e2e-ci1.sh +++ b/scripts/e2e-ci1.sh @@ -10,6 +10,7 @@ mkdir -p tmp/nx # This should be every file under e2e except for utils.js up to next.test.ts export SELECTED_CLI=$1 jest --maxWorkers=1 ./build/e2e/affected.test.js && +jest --maxWorkers=1 ./build/e2e/affected-git.test.js && jest --maxWorkers=1 ./build/e2e/bazel.test.js && jest --maxWorkers=1 ./build/e2e/command-line.test.js && jest --maxWorkers=1 ./build/e2e/cypress.test.js &&