feat(core): analyze changes to nx.json and workspace.json (#2338)
This commit is contained in:
parent
616fbd6c0d
commit
b30930f95f
85
e2e/affected-git.test.ts
Normal file
85
e2e/affected-git.test.ts
Normal file
@ -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}"`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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) => {
|
||||
|
||||
@ -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']);
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
};
|
||||
@ -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']);
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
};
|
||||
@ -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 &&
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user